From b650b80ed5de84c059f3343ea380a47811691a45 Mon Sep 17 00:00:00 2001 From: Andrew Bowley Date: Sun, 29 Oct 2017 14:29:02 +1100 Subject: [PATCH 1/5] [Bug 526437] - Android SWT Libraries need to be forked Forked AOSP SWT according to "How to fork Android on GitHub" (https://www.bitleaks.net/blog/fork-android-on-github/). This project was revised to build the dependencies required by Andmore as bundles using Maven/Tycho. The relevant parts were then merged to Andmore as a new trunk named "andmore-swt". The enlarged Andmore project has built to completion with Maven, but with integration test errors. Regardless, the pull request is proceeding, as the android-swt part of the build is successful. Note the orginal Andmore target has been updated and incorporated in the Tycho target platform configuration so both Maven and Eclipse can share the same target. Also note that the original Android base module has been replaced by the new SWT org.eclipse.andmore.swt module. Bug: https://bugs.eclipse.org/bugs/show_bug.cgi?id=526437 Signed-off-by: Andrew Bowley --- andmore-swt/.gitignore | 14 + andmore-swt/.project | 17 + .../org.eclipse.core.resources.prefs | 2 + .../.settings/org.eclipse.m2e.core.prefs | 4 + .../org.eclipse.andmore.ddmuilib/.gitignore | 1 + .../org.eclipse.andmore.ddmuilib/.project | 23 + .../org.eclipse.core.resources.prefs | 2 + .../.settings/org.eclipse.m2e.core.prefs | 4 + .../build.properties | 2 + .../feature.properties | 0 .../org.eclipse.andmore.ddmuilib/feature.xml | 46 + .../findbugsExclusion.xml | 10 + .../org.eclipse.andmore.ddmuilib/pom.xml | 48 + .../sourceTemplateFeature/.gitignore | 0 .../.gitignore | 1 + .../.project | 23 + .../org.eclipse.core.resources.prefs | 2 + .../.settings/org.eclipse.m2e.core.prefs | 4 + .../build.properties | 2 + .../feature.properties | 0 .../feature.xml | 53 + .../findbugsExclusion.xml | 10 + .../pom.xml | 48 + .../sourceTemplateFeature/.gitignore | 0 .../org.eclipse.andmore.sdkstats/.gitignore | 1 + .../org.eclipse.andmore.sdkstats/.project | 23 + .../org.eclipse.core.resources.prefs | 2 + .../.settings/org.eclipse.m2e.core.prefs | 4 + .../build.properties | 2 + .../feature.properties | 0 .../org.eclipse.andmore.sdkstats/feature.xml | 46 + .../findbugsExclusion.xml | 10 + .../org.eclipse.andmore.sdkstats/pom.xml | 48 + .../sourceTemplateFeature/.gitignore | 0 .../org.eclipse.andmore.sdkuilib/.gitignore | 1 + .../org.eclipse.andmore.sdkuilib/.project | 23 + .../org.eclipse.core.resources.prefs | 2 + .../.settings/org.eclipse.m2e.core.prefs | 4 + .../build.properties | 2 + .../feature.properties | 0 .../org.eclipse.andmore.sdkuilib/feature.xml | 46 + .../findbugsExclusion.xml | 10 + .../org.eclipse.andmore.sdkuilib/pom.xml | 48 + .../sourceTemplateFeature/.gitignore | 0 .../org.eclipse.andmore.swt/.gitignore | 1 + .../features/org.eclipse.andmore.swt/.project | 23 + .../org.eclipse.core.resources.prefs | 2 + .../.settings/org.eclipse.m2e.core.prefs | 4 + .../org.eclipse.andmore.swt/build.properties | 2 + .../feature.properties | 0 .../org.eclipse.andmore.swt/feature.xml | 40 + .../findbugsExclusion.xml | 10 + .../features/org.eclipse.andmore.swt/pom.xml | 48 + .../sourceTemplateFeature/.gitignore | 0 .../org.eclipse.andmore.swtmenubar/.gitignore | 1 + .../org.eclipse.andmore.swtmenubar/.project | 23 + .../org.eclipse.core.resources.prefs | 2 + .../.settings/org.eclipse.m2e.core.prefs | 4 + .../build.properties | 2 + .../feature.properties | 0 .../feature.xml | 39 + .../findbugsExclusion.xml | 10 + .../org.eclipse.andmore.swtmenubar/pom.xml | 48 + .../sourceTemplateFeature/.gitignore | 0 .../.gitignore | 1 + .../.project | 23 + .../org.eclipse.core.resources.prefs | 2 + .../.settings/org.eclipse.m2e.core.prefs | 4 + .../build.properties | 2 + .../feature.properties | 0 .../feature.xml | 46 + .../findbugsExclusion.xml | 10 + .../pom.xml | 48 + .../sourceTemplateFeature/.gitignore | 0 .../.gitignore | 1 + .../.project | 23 + .../org.eclipse.core.resources.prefs | 2 + .../.settings/org.eclipse.m2e.core.prefs | 4 + .../build.properties | 2 + .../feature.properties | 0 .../feature.xml | 46 + .../findbugsExclusion.xml | 10 + .../pom.xml | 48 + .../sourceTemplateFeature/.gitignore | 0 .../org.eclipse.andmore.ddmuilib/.classpath | 13 + .../org.eclipse.andmore.ddmuilib/.gitignore | 3 + .../org.eclipse.andmore.ddmuilib/.project | 34 + .../org.eclipse.core.resources.prefs | 3 + .../.settings/org.eclipse.jdt.core.prefs | 13 + .../.settings/org.eclipse.m2e.core.prefs | 4 + .../META-INF/MANIFEST.MF | 31 + .../org.eclipse.andmore.ddmuilib/NOTICE | 190 ++ .../org.eclipse.andmore.ddmuilib/README | 14 + .../org.eclipse.andmore.ddmuilib/build.gradle | 15 + .../build.properties | 7 + .../org.eclipse.andmore.ddmuilib/ddmuilib.iml | 22 + .../plugin.properties | 4 + .../org.eclipse.andmore.ddmuilib/pom.xml | 158 ++ .../ddmuilib/AbstractBufferFindTarget.java | 117 + .../java/com/android/ddmuilib/Addr2Line.java | 373 +++ .../com/android/ddmuilib/AllocationPanel.java | 657 +++++ .../android/ddmuilib/BackgroundThread.java | 50 + .../com/android/ddmuilib/BaseHeapPanel.java | 193 ++ .../android/ddmuilib/ClientDisplayPanel.java | 33 + .../android/ddmuilib/DdmUiPreferences.java | 88 + .../com/android/ddmuilib/DevicePanel.java | 829 +++++++ .../ddmuilib/EmulatorControlPanel.java | 1463 +++++++++++ .../java/com/android/ddmuilib/FindDialog.java | 142 ++ .../java/com/android/ddmuilib/HeapPanel.java | 1307 ++++++++++ .../com/android/ddmuilib/IFindTarget.java | 21 + .../android/ddmuilib/ITableFocusListener.java | 38 + .../com/android/ddmuilib/ImageLoader.java | 206 ++ .../java/com/android/ddmuilib/InfoPanel.java | 211 ++ .../com/android/ddmuilib/NativeHeapPanel.java | 1648 +++++++++++++ .../main/java/com/android/ddmuilib/Panel.java | 49 + .../android/ddmuilib/ScreenShotDialog.java | 352 +++ .../ddmuilib/SelectionDependentPanel.java | 78 + .../com/android/ddmuilib/StackTracePanel.java | 223 ++ .../android/ddmuilib/SyncProgressHelper.java | 100 + .../android/ddmuilib/SyncProgressMonitor.java | 60 + .../com/android/ddmuilib/SysinfoPanel.java | 931 +++++++ .../com/android/ddmuilib/TableHelper.java | 209 ++ .../java/com/android/ddmuilib/TablePanel.java | 132 + .../com/android/ddmuilib/ThreadPanel.java | 573 +++++ .../ddmuilib/actions/ICommonAction.java | 42 + .../ddmuilib/actions/ToolItemAction.java | 71 + .../android/ddmuilib/annotation/UiThread.java | 31 + .../ddmuilib/annotation/WorkerThread.java | 31 + .../android/ddmuilib/console/DdmConsole.java | 91 + .../android/ddmuilib/console/IDdmConsole.java | 47 + .../explorer/DeviceContentProvider.java | 177 ++ .../ddmuilib/explorer/DeviceExplorer.java | 922 +++++++ .../ddmuilib/explorer/FileLabelProvider.java | 160 ++ .../ddmuilib/handler/BaseFileHandler.java | 184 ++ .../handler/MethodProfilingHandler.java | 195 ++ .../heap/NativeDiffAllocationInfo.java | 67 + .../ddmuilib/heap/NativeHeapDataImporter.java | 227 ++ .../ddmuilib/heap/NativeHeapDiffSnapshot.java | 102 + .../heap/NativeHeapLabelProvider.java | 112 + .../ddmuilib/heap/NativeHeapPanel.java | 1137 +++++++++ .../heap/NativeHeapProviderByAllocations.java | 90 + .../heap/NativeHeapProviderByLibrary.java | 95 + .../ddmuilib/heap/NativeHeapSnapshot.java | 133 + .../heap/NativeLibraryAllocationInfo.java | 135 + .../heap/NativeStackContentProvider.java | 56 + .../heap/NativeStackLabelProvider.java | 71 + .../heap/NativeSymbolResolverTask.java | 443 ++++ .../ddmuilib/location/CoordinateControls.java | 249 ++ .../android/ddmuilib/location/GpxParser.java | 373 +++ .../android/ddmuilib/location/KmlParser.java | 210 ++ .../ddmuilib/location/LocationPoint.java | 53 + .../location/TrackContentProvider.java | 48 + .../ddmuilib/location/TrackLabelProvider.java | 87 + .../android/ddmuilib/location/TrackPoint.java | 34 + .../android/ddmuilib/location/WayPoint.java | 42 + .../location/WayPointContentProvider.java | 46 + .../location/WayPointLabelProvider.java | 79 + .../ddmuilib/log/event/BugReportImporter.java | 96 + .../log/event/DisplayFilteredLog.java | 55 + .../ddmuilib/log/event/DisplayGraph.java | 422 ++++ .../ddmuilib/log/event/DisplayLog.java | 381 +++ .../ddmuilib/log/event/DisplaySync.java | 305 +++ .../log/event/DisplaySyncHistogram.java | 181 ++ .../ddmuilib/log/event/DisplaySyncPerf.java | 227 ++ .../ddmuilib/log/event/EventDisplay.java | 975 ++++++++ .../log/event/EventDisplayOptions.java | 961 ++++++++ .../ddmuilib/log/event/EventLogImporter.java | 95 + .../ddmuilib/log/event/EventLogPanel.java | 938 +++++++ .../log/event/EventValueSelector.java | 630 +++++ .../log/event/OccurrenceRenderer.java | 90 + .../ddmuilib/log/event/SyncCommon.java | 173 ++ .../ddmuilib/logcat/EditFilterDialog.java | 397 +++ .../logcat/ILogCatBufferChangeListener.java | 33 + .../ILogCatMessageSelectionListener.java | 26 + .../logcat/LogCatFilterContentProvider.java | 46 + .../ddmuilib/logcat/LogCatFilterData.java | 81 + .../logcat/LogCatFilterLabelProvider.java | 63 + .../logcat/LogCatFilterSettingsDialog.java | 327 +++ .../LogCatFilterSettingsSerializer.java | 211 ++ .../ddmuilib/logcat/LogCatMessageList.java | 116 + .../android/ddmuilib/logcat/LogCatPanel.java | 1607 ++++++++++++ .../ddmuilib/logcat/LogCatReceiver.java | 150 ++ .../logcat/LogCatReceiverFactory.java | 95 + .../logcat/LogCatStackTraceParser.java | 81 + .../android/ddmuilib/logcat/LogColors.java | 27 + .../android/ddmuilib/logcat/LogFilter.java | 556 +++++ .../com/android/ddmuilib/logcat/LogPanel.java | 1626 ++++++++++++ .../android/ddmuilib/net/NetworkPanel.java | 1125 +++++++++ .../screenrecord/ScreenRecorderAction.java | 134 + .../ScreenRecorderOptionsDialog.java | 232 ++ .../vmtrace/VmTraceOptionsDialog.java | 145 ++ .../src/main/java/images/add.png | Bin 0 -> 146 bytes .../src/main/java/images/android.png | Bin 0 -> 3609 bytes .../src/main/java/images/backward.png | Bin 0 -> 136 bytes .../src/main/java/images/capture.png | Bin 0 -> 691 bytes .../src/main/java/images/clear.png | Bin 0 -> 217 bytes .../src/main/java/images/d.png | Bin 0 -> 638 bytes .../src/main/java/images/debug-attach.png | Bin 0 -> 156 bytes .../src/main/java/images/debug-error.png | Bin 0 -> 222 bytes .../src/main/java/images/debug-wait.png | Bin 0 -> 156 bytes .../src/main/java/images/delete.png | Bin 0 -> 107 bytes .../src/main/java/images/device.png | Bin 0 -> 135 bytes .../src/main/java/images/diff.png | Bin 0 -> 213 bytes .../src/main/java/images/displayfilters.png | Bin 0 -> 242 bytes .../src/main/java/images/down.png | Bin 0 -> 141 bytes .../src/main/java/images/e.png | Bin 0 -> 511 bytes .../src/main/java/images/edit.png | Bin 0 -> 223 bytes .../src/main/java/images/empty.png | Bin 0 -> 75 bytes .../src/main/java/images/emulator.png | Bin 0 -> 287 bytes .../src/main/java/images/file.png | Bin 0 -> 157 bytes .../src/main/java/images/folder.png | Bin 0 -> 123 bytes .../src/main/java/images/forward.png | Bin 0 -> 137 bytes .../src/main/java/images/gc.png | Bin 0 -> 165 bytes .../src/main/java/images/groupby.png | Bin 0 -> 413 bytes .../src/main/java/images/halt.png | Bin 0 -> 197 bytes .../src/main/java/images/heap.png | Bin 0 -> 222 bytes .../src/main/java/images/hprof.png | Bin 0 -> 317 bytes .../src/main/java/images/i.png | Bin 0 -> 498 bytes .../src/main/java/images/importBug.png | Bin 0 -> 191 bytes .../src/main/java/images/load.png | Bin 0 -> 163 bytes .../src/main/java/images/pause.png | Bin 0 -> 98 bytes .../src/main/java/images/play.png | Bin 0 -> 138 bytes .../src/main/java/images/pull.png | Bin 0 -> 329 bytes .../src/main/java/images/push.png | Bin 0 -> 228 bytes .../src/main/java/images/save.png | Bin 0 -> 240 bytes .../src/main/java/images/scroll_lock.png | Bin 0 -> 291 bytes .../src/main/java/images/sort_down.png | Bin 0 -> 102 bytes .../src/main/java/images/sort_up.png | Bin 0 -> 105 bytes .../src/main/java/images/thread.png | Bin 0 -> 121 bytes .../src/main/java/images/tracing_start.png | Bin 0 -> 227 bytes .../src/main/java/images/tracing_stop.png | Bin 0 -> 217 bytes .../src/main/java/images/up.png | Bin 0 -> 134 bytes .../src/main/java/images/v.png | Bin 0 -> 587 bytes .../src/main/java/images/w.png | Bin 0 -> 681 bytes .../src/main/java/images/warning.png | Bin 0 -> 147 bytes .../src/main/java/images/zygote.png | Bin 0 -> 345 bytes .../android/ddmuilib/BugReportParserTest.java | 196 ++ .../heap/NativeHeapDataImporterTest.java | 76 + .../LogCatFilterSettingsSerializerTest.java | 77 + .../logcat/LogCatStackTraceParserTest.java | 61 + .../logcat/RollingBufferFindTest.java | 112 + .../.classpath | 7 + .../.gitignore | 2 + .../.project | 34 + .../.settings/README.txt | 2 + .../org.eclipse.core.resources.prefs | 2 + .../.settings/org.eclipse.jdt.core.prefs | 7 + .../.settings/org.eclipse.m2e.core.prefs | 4 + .../META-INF/MANIFEST.MF | 20 + .../NOTICE | 190 ++ .../build.gradle | 16 + .../build.properties | 4 + .../hierarchyviewer2lib.iml | 17 + .../plugin.properties | 4 + .../pom.xml | 37 + .../HierarchyViewerDirector.java | 791 ++++++ .../actions/CapturePSDAction.java | 62 + .../actions/DisplayViewAction.java | 62 + .../actions/DumpDisplayListAction.java | 56 + .../actions/DumpThemeAction.java | 58 + .../actions/EvaluateContrastAction.java | 61 + .../actions/ImageAction.java | 27 + .../actions/InspectScreenshotAction.java | 96 + .../actions/InvalidateAction.java | 58 + .../actions/LoadOverlayAction.java | 62 + .../actions/LoadViewHierarchyAction.java | 96 + .../PixelPerfectAutoRefreshAction.java | 59 + .../actions/PixelPerfectEnabledAction.java | 82 + .../actions/ProfileNodesAction.java | 55 + .../actions/RefreshPixelPerfectAction.java | 58 + .../RefreshPixelPerfectTreeAction.java | 58 + .../actions/RefreshViewAction.java | 58 + .../actions/RefreshWindowsAction.java | 59 + .../actions/RequestLayoutAction.java | 58 + .../actions/SavePixelPerfectAction.java | 62 + .../actions/SaveTreeViewAction.java | 62 + .../actions/SelectedNodeEnabledAction.java | 62 + .../actions/TreeViewEnabledAction.java | 54 + .../device/AbstractHvDevice.java | 67 + .../device/DdmViewDebugDevice.java | 444 ++++ .../device/DeviceBridge.java | 744 ++++++ .../device/DeviceConnection.java | 101 + .../device/HvDeviceFactory.java | 54 + .../hierarchyviewerlib/device/IHvDevice.java | 64 + .../device/ViewServerDevice.java | 175 ++ .../device/WindowUpdater.java | 160 ++ .../models/DeviceSelectionModel.java | 260 ++ .../models/EvaluateContrastModel.java | 390 +++ .../models/PixelPerfectModel.java | 360 +++ .../hierarchyviewerlib/models/ThemeModel.java | 60 + .../models/TreeViewModel.java | 215 ++ .../hierarchyviewerlib/models/ViewNode.java | 369 +++ .../hierarchyviewerlib/models/Window.java | 117 + .../hierarchyviewerlib/ui/CaptureDisplay.java | 218 ++ .../ui/DevicePropertyEditingSupport.java | 321 +++ .../hierarchyviewerlib/ui/DeviceSelector.java | 342 +++ .../ui/DumpThemeDisplay.java | 133 + .../ui/EvaluateContrastDisplay.java | 542 ++++ .../ui/InvokeMethodPrompt.java | 166 ++ .../hierarchyviewerlib/ui/LayoutViewer.java | 372 +++ .../hierarchyviewerlib/ui/PixelPerfect.java | 392 +++ .../ui/PixelPerfectControls.java | 296 +++ .../ui/PixelPerfectLoupe.java | 391 +++ .../ui/PixelPerfectPixelPanel.java | 203 ++ .../ui/PixelPerfectTree.java | 241 ++ .../hierarchyviewerlib/ui/PropertyViewer.java | 420 ++++ .../hierarchyviewerlib/ui/TreeView.java | 1096 ++++++++ .../ui/TreeViewControls.java | 153 ++ .../ui/TreeViewOverview.java | 396 +++ .../ui/util/DrawableViewNode.java | 266 ++ .../hierarchyviewerlib/ui/util/PsdFile.java | 508 ++++ .../ui/util/TreeColumnResizer.java | 114 + .../src/main/java/images/auto-refresh.png | Bin 0 -> 541 bytes .../src/main/java/images/capture-psd.png | Bin 0 -> 339 bytes .../main/java/images/device-view-selected.png | Bin 0 -> 254 bytes .../src/main/java/images/device-view.png | Bin 0 -> 228 bytes .../src/main/java/images/display.png | Bin 0 -> 946 bytes .../src/main/java/images/filtered.png | Bin 0 -> 9242 bytes .../src/main/java/images/green.png | Bin 0 -> 302 bytes .../main/java/images/inspect-screenshot.png | Bin 0 -> 412 bytes .../src/main/java/images/invalidate.png | Bin 0 -> 391 bytes .../src/main/java/images/load-all-views.png | Bin 0 -> 728 bytes .../src/main/java/images/load-overlay.png | Bin 0 -> 549 bytes .../main/java/images/load-view-hierarchy.png | Bin 0 -> 288 bytes .../src/main/java/images/not-selected.png | Bin 0 -> 12468 bytes .../src/main/java/images/on-black.png | Bin 0 -> 157 bytes .../src/main/java/images/on-white.png | Bin 0 -> 158 bytes .../src/main/java/images/picker.png | Bin 0 -> 370 bytes .../images/pixel-perfect-view-selected.png | Bin 0 -> 734 bytes .../main/java/images/pixel-perfect-view.png | Bin 0 -> 733 bytes .../src/main/java/images/profile.png | Bin 0 -> 597 bytes .../src/main/java/images/red.png | Bin 0 -> 383 bytes .../src/main/java/images/refresh-windows.png | Bin 0 -> 872 bytes .../src/main/java/images/request-layout.png | Bin 0 -> 223 bytes .../src/main/java/images/save.png | Bin 0 -> 360 bytes .../java/images/sdk-hierarchyviewer-128.png | Bin 0 -> 17512 bytes .../java/images/sdk-hierarchyviewer-16.png | Bin 0 -> 880 bytes .../java/images/selected-filtered-small.png | Bin 0 -> 5182 bytes .../main/java/images/selected-filtered.png | Bin 0 -> 9015 bytes .../src/main/java/images/selected-small.png | Bin 0 -> 12611 bytes .../src/main/java/images/selected.png | Bin 0 -> 12159 bytes .../src/main/java/images/show-extras.png | Bin 0 -> 330 bytes .../src/main/java/images/show-overlay.png | Bin 0 -> 958 bytes .../main/java/images/tree-view-selected.png | Bin 0 -> 276 bytes .../src/main/java/images/tree-view.png | Bin 0 -> 281 bytes .../src/main/java/images/yellow.png | Bin 0 -> 255 bytes .../models/EvaluateContrastModelTest.java | 233 ++ .../ui/PropertyViewerTest.java | 41 + .../src/test/resources/images/all_black.png | Bin 0 -> 146 bytes .../test/resources/images/black_on_white.png | Bin 0 -> 10523 bytes .../resources/images/dark_on_light_greens.png | Bin 0 -> 1837 bytes .../test/resources/images/white_on_black.png | Bin 0 -> 10783 bytes .../org.eclipse.andmore.sdkstats/.classpath | 7 + .../org.eclipse.andmore.sdkstats/.gitignore | 2 + .../org.eclipse.andmore.sdkstats/.project | 34 + .../.settings/README.txt | 2 + .../org.eclipse.core.resources.prefs | 2 + .../.settings/org.eclipse.jdt.core.prefs | 7 + .../.settings/org.eclipse.m2e.core.prefs | 4 + .../META-INF/MANIFEST.MF | 14 + .../org.eclipse.andmore.sdkstats/NOTICE | 190 ++ .../org.eclipse.andmore.sdkstats/README | 11 + .../org.eclipse.andmore.sdkstats/build.gradle | 15 + .../build.properties | 4 + .../plugin.properties | 4 + .../org.eclipse.andmore.sdkstats/pom.xml | 37 + .../org.eclipse.andmore.sdkstats/sdkstats.iml | 19 + .../android/sdkstats/DdmsPreferenceStore.java | 332 +++ .../sdkstats/SdkStatsPermissionDialog.java | 196 ++ .../com/android/sdkstats/SdkStatsService.java | 558 +++++ .../android/sdkstats/SdkStatsServiceTest.java | 544 ++++ .../org.eclipse.andmore.sdkuilib/.classpath | 12 + .../org.eclipse.andmore.sdkuilib/.gitignore | 1 + .../org.eclipse.andmore.sdkuilib/.project | 34 + .../org.eclipse.core.resources.prefs | 3 + .../.settings/org.eclipse.jdt.core.prefs | 13 + .../.settings/org.eclipse.m2e.core.prefs | 4 + .../META-INF/MANIFEST.MF | 28 + .../MODULE_LICENSE_APACHE2 | 0 .../org.eclipse.andmore.sdkuilib/NOTICE | 190 ++ .../build.properties | 7 + .../libs/commons-compress-1.8.1.jar | Bin 0 -> 365552 bytes .../plugin.properties | 4 + .../org.eclipse.andmore.sdkuilib/pom.xml | 83 + .../com/android/sdklib/AndroidTargetHash.java | 182 ++ .../com/android/sdklib/AndroidVersion.java | 392 +++ .../com/android/sdklib/BuildToolInfo.java | 391 +++ .../com/android/sdklib/IAndroidTarget.java | 334 +++ .../java/com/android/sdklib/ISystemImage.java | 95 + .../android/sdklib/LegacyAndroidVersion.java | 46 + .../java/com/android/sdklib/SdkManager.java | 410 +++ .../com/android/sdklib/SdkVersionInfo.java | 351 +++ .../java/com/android/sdklib/SystemImage.java | 325 +++ .../internal/androidTarget/AddOnTarget.java | 463 ++++ .../internal/androidTarget/MissingTarget.java | 244 ++ .../androidTarget/OptionalLibraryImpl.java | 70 + .../androidTarget/PlatformTarget.java | 456 ++++ .../android/sdklib/internal/avd/AvdInfo.java | 410 +++ .../sdklib/internal/avd/AvdManager.java | 2196 +++++++++++++++++ .../internal/avd/HardwareProperties.java | 327 +++ .../internal/build/BuildConfig.template | 6 + .../internal/build/BuildConfigGenerator.java | 157 ++ .../internal/build/DebugKeyProvider.java | 215 ++ .../sdklib/internal/build/KeystoreHelper.java | 173 ++ .../internal/build/SignedJarBuilder.java | 399 +++ .../sdklib/internal/build/SymbolLoader.java | 102 + .../sdklib/internal/build/SymbolWriter.java | 138 ++ .../internal/project/IPropertySource.java | 26 + .../internal/project/ProjectCreator.java | 1571 ++++++++++++ .../internal/project/ProjectProperties.java | 594 +++++ .../project/ProjectPropertiesWorkingCopy.java | 280 +++ .../internal/repository/AdbWrapper.java | 157 ++ .../repository/AddonsListFetcher.java | 614 +++++ .../repository/CanceledByUserException.java | 35 + .../internal/repository/DownloadCache.java | 848 +++++++ .../sdklib/internal/repository/ITask.java | 31 + .../internal/repository/ITaskFactory.java | 64 + .../internal/repository/ITaskMonitor.java | 152 ++ .../internal/repository/LocalSdkParser.java | 804 ++++++ .../internal/repository/NullTaskMonitor.java | 140 ++ .../sdklib/internal/repository/SdkStats.java | 627 +++++ .../sdklib/internal/repository/UrlOpener.java | 530 ++++ .../internal/repository/UserCredentials.java | 53 + .../repository/archives/ArchFilter.java | 324 +++ .../internal/repository/archives/Archive.java | 375 +++ .../repository/archives/ArchiveInstaller.java | 1220 +++++++++ .../archives/ArchiveReplacement.java | 117 + .../internal/repository/archives/BitSize.java | 66 + .../repository/archives/ChecksumType.java | 54 + .../internal/repository/archives/HostOs.java | 83 + .../repository/archives/LegacyArch.java | 52 + .../repository/archives/LegacyOs.java | 52 + .../repository/packages/AddonPackage.java | 765 ++++++ .../repository/packages/BrokenPackage.java | 241 ++ .../repository/packages/BuildToolPackage.java | 367 +++ .../repository/packages/DocPackage.java | 333 +++ .../repository/packages/ExtraPackage.java | 692 ++++++ .../packages/FullRevisionPackage.java | 164 ++ .../packages/IAndroidVersionProvider.java | 43 + .../packages/IExactApiLevelDependency.java | 54 + .../packages/IFullRevisionProvider.java | 51 + .../packages/ILayoutlibVersion.java | 47 + .../packages/IMinApiLevelDependency.java | 46 + .../packages/IMinPlatformToolsDependency.java | 55 + .../packages/IMinToolsDependency.java | 48 + .../packages/IPlatformDependency.java | 42 + .../packages/LayoutlibVersionMixin.java | 136 + .../packages/MajorRevisionPackage.java | 164 ++ .../repository/packages/MinToolsMixin.java | 134 + .../repository/packages/MinToolsPackage.java | 117 + .../packages/NoPreviewRevisionPackage.java | 163 ++ .../internal/repository/packages/Package.java | 902 +++++++ .../packages/PackageParserUtils.java | 427 ++++ .../repository/packages/PlatformPackage.java | 388 +++ .../packages/PlatformToolPackage.java | 313 +++ .../repository/packages/SamplePackage.java | 571 +++++ .../repository/packages/SourcePackage.java | 377 +++ .../packages/SystemImagePackage.java | 621 +++++ .../repository/packages/ToolPackage.java | 384 +++ .../repository/sources/SdkAddonSource.java | 113 + .../repository/sources/SdkRepoSource.java | 528 ++++ .../repository/sources/SdkSource.java | 999 ++++++++ .../repository/sources/SdkSourceCategory.java | 94 + .../sources/SdkSourceProperties.java | 254 ++ .../repository/sources/SdkSources.java | 444 ++++ .../repository/sources/SdkSysImgSource.java | 114 + .../repository/updater/ArchiveInfo.java | 173 ++ .../repository/updater/ISettingsPage.java | 113 + .../repository/updater/IUpdaterData.java | 49 + .../repository/updater/PackageLoader.java | 502 ++++ .../internal/repository/updater/PkgItem.java | 290 +++ .../repository/updater/SdkUpdaterLogic.java | 1568 ++++++++++++ .../updater/SdkUpdaterNoWindow.java | 715 ++++++ .../updater/SettingsController.java | 392 +++ .../repository/updater/UpdaterData.java | 1336 ++++++++++ .../java/com/android/sdklib/io/IFileOp.java | 161 ++ .../com/android/sdklib/io/LegacyFileOp.java | 488 ++++ .../repository/AddonManifestIniProps.java | 113 + .../sdklib/repository/FullRevision.java | 426 ++++ .../sdklib/repository/IDescription.java | 40 + .../sdklib/repository/IListDescription.java | 34 + .../sdklib/repository/ISdkChangeListener.java | 54 + .../android/sdklib/repository/License.java | 176 ++ .../sdklib/repository/MajorRevision.java | 59 + .../sdklib/repository/NoPreviewRevision.java | 61 + .../android/sdklib/repository/PkgProps.java | 104 + .../sdklib/repository/PreciseRevision.java | 153 ++ .../com/android/sdklib/repository/README.txt | 25 + .../sdklib/repository/RepoConstants.java | 213 ++ .../sdklib/repository/RepoXsdUtil.java | 92 + .../sdklib/repository/SdkAddonConstants.java | 89 + .../repository/SdkAddonsListConstants.java | 109 + .../sdklib/repository/SdkRepoConstants.java | 171 ++ .../sdklib/repository/SdkStatsConstants.java | 92 + .../sdklib/repository/SdkSysImgConstants.java | 90 + .../descriptors/IPkgCapabilities.java | 82 + .../repository/descriptors/IPkgDesc.java | 189 ++ .../repository/descriptors/IPkgDescAddon.java | 40 + .../repository/descriptors/IPkgDescExtra.java | 51 + .../repository/descriptors/IdDisplay.java | 84 + .../repository/descriptors/PkgDesc.java | 1206 +++++++++ .../repository/descriptors/PkgDescAddon.java | 76 + .../repository/descriptors/PkgDescExtra.java | 176 ++ .../repository/descriptors/PkgType.java | 225 ++ .../repository/local/LocalAddonPkgInfo.java | 503 ++++ .../local/LocalAddonSysImgPkgInfo.java | 65 + .../local/LocalBuildToolPkgInfo.java | 57 + .../sdklib/repository/local/LocalDirInfo.java | 275 +++ .../repository/local/LocalDocPkgInfo.java | 47 + .../repository/local/LocalExtraPkgInfo.java | 121 + .../repository/local/LocalNdkPkgInfo.java | 47 + .../sdklib/repository/local/LocalPkgInfo.java | 240 ++ .../local/LocalPlatformPkgInfo.java | 453 ++++ .../local/LocalPlatformToolPkgInfo.java | 45 + .../repository/local/LocalSamplePkgInfo.java | 54 + .../sdklib/repository/local/LocalSdk.java | 1239 ++++++++++ .../repository/local/LocalSourcePkgInfo.java | 52 + .../repository/local/LocalSysImgPkgInfo.java | 135 + .../repository/local/LocalToolPkgInfo.java | 46 + .../repository/local/PackageParserUtils.java | 166 ++ .../sdklib/repository/sdk-addon-01.xsd | 295 +++ .../sdklib/repository/sdk-addon-02.xsd | 361 +++ .../sdklib/repository/sdk-addon-03.xsd | 381 +++ .../sdklib/repository/sdk-addon-04.xsd | 417 ++++ .../sdklib/repository/sdk-addon-05.xsd | 442 ++++ .../sdklib/repository/sdk-addon-06.xsd | 472 ++++ .../sdklib/repository/sdk-addon-07.xsd | 499 ++++ .../sdklib/repository/sdk-addons-list-1.xsd | 71 + .../sdklib/repository/sdk-addons-list-2.xsd | 106 + .../sdklib/repository/sdk-repository-01.xsd | 381 +++ .../sdklib/repository/sdk-repository-02.xsd | 438 ++++ .../sdklib/repository/sdk-repository-03.xsd | 436 ++++ .../sdklib/repository/sdk-repository-04.xsd | 500 ++++ .../sdklib/repository/sdk-repository-05.xsd | 624 +++++ .../sdklib/repository/sdk-repository-06.xsd | 608 +++++ .../sdklib/repository/sdk-repository-07.xsd | 612 +++++ .../sdklib/repository/sdk-repository-08.xsd | 652 +++++ .../sdklib/repository/sdk-repository-09.xsd | 677 +++++ .../sdklib/repository/sdk-repository-10.xsd | 653 +++++ .../sdklib/repository/sdk-repository-11.xsd | 680 +++++ .../android/sdklib/repository/sdk-stats-1.xsd | 96 + .../sdklib/repository/sdk-sys-img-01.xsd | 229 ++ .../sdklib/repository/sdk-sys-img-02.xsd | 249 ++ .../sdklib/repository/sdk-sys-img-03.xsd | 309 +++ .../sdklib/util/CommandLineParser.java | 976 ++++++++ .../com/android/sdklib/util/FormatUtils.java | 53 + .../com/android/sdklib/util/LineUtil.java | 118 + .../internal/repository/AboutDialog.java | 121 + .../repository/ISdkUpdaterWindow.java | 42 + .../internal/repository/ISwtUpdaterData.java | 36 + .../internal/repository/MenuBarWrapper.java | 60 + .../repository/SdkUpdaterChooserDialog.java | 1130 +++++++++ .../internal/repository/SettingsDialog.java | 286 +++ .../internal/repository/SwtUpdaterData.java | 240 ++ .../repository/UpdaterBaseDialog.java | 106 + .../repository/core/PackagesDiffLogic.java | 859 +++++++ .../internal/repository/core/PkgCategory.java | 87 + .../repository/core/PkgCategoryApi.java | 106 + .../repository/core/PkgCategorySource.java | 70 + .../repository/core/PkgContentProvider.java | 237 ++ .../repository/core/SdkLogAdapter.java | 112 + .../repository/core/SwtPackageLoader.java | 69 + .../repository/icons/ImageFactory.java | 205 ++ .../repository/icons/accept_icon16.png | Bin 0 -> 3277 bytes .../repository/icons/addon_pkg_16.png | Bin 0 -> 529 bytes .../repository/icons/android_icon_128.png | Bin 0 -> 17715 bytes .../repository/icons/android_icon_16.png | Bin 0 -> 219 bytes .../repository/icons/archive_icon16.png | Bin 0 -> 493 bytes .../internal/repository/icons/broken_16.png | Bin 0 -> 257 bytes .../repository/icons/broken_pkg_16.png | Bin 0 -> 281 bytes .../repository/icons/buildtool_pkg_16.png | Bin 0 -> 425 bytes .../internal/repository/icons/doc_pkg_16.png | Bin 0 -> 290 bytes .../repository/icons/error_icon_16.png | Bin 0 -> 626 bytes .../repository/icons/extra_pkg_16.png | Bin 0 -> 540 bytes .../repository/icons/incompat_icon16.png | Bin 0 -> 735 bytes .../internal/repository/icons/log_off_16.png | Bin 0 -> 1218 bytes .../internal/repository/icons/log_on_16.png | Bin 0 -> 468 bytes .../repository/icons/nopkg_icon_16.png | Bin 0 -> 397 bytes .../repository/icons/pkg_incompat_16.png | Bin 0 -> 409 bytes .../repository/icons/pkg_installed_16.png | Bin 0 -> 563 bytes .../internal/repository/icons/pkg_new_16.png | Bin 0 -> 262 bytes .../repository/icons/pkg_update_16.png | Bin 0 -> 553 bytes .../internal/repository/icons/pkgcat_16.png | Bin 0 -> 1352 bytes .../repository/icons/pkgcat_other_16.png | Bin 0 -> 335 bytes .../repository/icons/platform_pkg_16.png | Bin 0 -> 2981 bytes .../repository/icons/platformtool_pkg_16.png | Bin 0 -> 393 bytes .../repository/icons/reject_icon16.png | Bin 0 -> 3367 bytes .../repository/icons/sample_pkg_16.png | Bin 0 -> 3188 bytes .../repository/icons/sdkman_logo_128.png | Bin 0 -> 2381 bytes .../repository/icons/source_pkg_16.png | Bin 0 -> 466 bytes .../repository/icons/status_ok_16.png | Bin 0 -> 3277 bytes .../repository/icons/stop_disabled_16.png | Bin 0 -> 809 bytes .../repository/icons/stop_enabled_16.png | Bin 0 -> 642 bytes .../repository/icons/sysimg_pkg_16.png | Bin 0 -> 1145 bytes .../repository/icons/tag_android-tv_16.png | Bin 0 -> 2948 bytes .../repository/icons/tag_android-tv_32.png | Bin 0 -> 2978 bytes .../repository/icons/tag_android-wear_16.png | Bin 0 -> 3115 bytes .../repository/icons/tag_android-wear_32.png | Bin 0 -> 3143 bytes .../repository/icons/tag_default_16.png | Bin 0 -> 3097 bytes .../repository/icons/tag_default_32.png | Bin 0 -> 3064 bytes .../internal/repository/icons/tool_pkg_16.png | Bin 0 -> 393 bytes .../repository/icons/unknown_icon16.png | Bin 0 -> 453 bytes .../repository/icons/warning_icon16.png | Bin 0 -> 259 bytes .../repository/ui/AddonSitesDialog.java | 574 +++++ .../repository/ui/AdtUpdateDialog.java | 494 ++++ .../repository/ui/AvdManagerPage.java | 172 ++ .../repository/ui/AvdManagerWindowImpl1.java | 414 ++++ .../repository/ui/DeviceManagerPage.java | 833 +++++++ .../internal/repository/ui/LogWindow.java | 379 +++ .../internal/repository/ui/PackagesPage.java | 1215 +++++++++ .../repository/ui/PackagesPageIcons.java | 32 + .../repository/ui/PackagesPageImpl.java | 564 +++++ .../ui/PkgTreeColumnViewerLabelProvider.java | 137 + .../repository/ui/SdkUpdaterWindowImpl2.java | 574 +++++ .../repository/ui/ShellSizeAndPos.java | 166 ++ .../internal/tasks/ILogUiProvider.java | 50 + .../internal/tasks/IProgressUiProvider.java | 87 + .../sdkuilib/internal/tasks/ProgressTask.java | 108 + .../internal/tasks/ProgressTaskDialog.java | 520 ++++ .../internal/tasks/ProgressTaskFactory.java | 67 + .../sdkuilib/internal/tasks/ProgressView.java | 376 +++ .../internal/tasks/ProgressViewFactory.java | 48 + .../internal/tasks/TaskMonitorImpl.java | 369 +++ .../internal/widgets/AvdCreationDialog.java | 66 + .../widgets/AvdCreationPresenter.java | 1488 +++++++++++ .../internal/widgets/AvdCreationSwtView.java | 609 +++++ .../internal/widgets/AvdDetailsDialog.java | 162 ++ .../internal/widgets/AvdSelector.java | 1263 ++++++++++ .../internal/widgets/AvdStartDialog.java | 643 +++++ .../widgets/DeviceCreationDialog.java | 1087 ++++++++ .../widgets/HardwarePropertyChooser.java | 150 ++ .../internal/widgets/IMessageBoxLogger.java | 48 + .../internal/widgets/ImgDisabledButton.java | 60 + .../internal/widgets/LegacyAvdEditDialog.java | 1439 +++++++++++ .../internal/widgets/MessageBoxLog.java | 150 ++ .../widgets/ResolutionChooserDialog.java | 123 + .../internal/widgets/SdkTargetSelector.java | 460 ++++ .../internal/widgets/ToggleButton.java | 134 + .../sdkuilib/repository/AvdManagerWindow.java | 96 + .../sdkuilib/repository/SdkUpdaterWindow.java | 113 + .../sdkuilib/ui/AuthenticationDialog.java | 195 ++ .../android/sdkuilib/ui/GridDataBuilder.java | 158 ++ .../com/android/sdkuilib/ui/GridDialog.java | 84 + .../sdkuilib/ui/GridLayoutBuilder.java | 103 + .../android/sdkuilib/ui/SwtBaseDialog.java | 247 ++ .../org.eclipse.andmore.swt/.classpath | 19 + .../org.eclipse.andmore.swt/.gitignore | 2 + andmore-swt/org.eclipse.andmore.swt/.project | 34 + .../org.eclipse.core.resources.prefs | 2 + .../.settings/org.eclipse.jdt.core.prefs | 98 + .../.settings/org.eclipse.m2e.core.prefs | 4 + .../META-INF/MANIFEST.MF | 92 + .../org.eclipse.andmore.swt/about.html | 29 + .../org.eclipse.andmore.swt/build.properties | 9 + .../findbugsExclusion.xml | 16 + .../org.eclipse.andmore.swt/plugin.properties | 9 + .../org.eclipse.andmore.swt/plugin.xml | 5 + andmore-swt/org.eclipse.andmore.swt/pom.xml | 246 ++ .../eclipse/andmore/base/InstallDetails.java | 42 + .../org.eclipse.andmore.swtmenubar/.classpath | 7 + .../org.eclipse.andmore.swtmenubar/.gitignore | 2 + .../org.eclipse.andmore.swtmenubar/.project | 34 + .../.settings/README.txt | 2 + .../org.eclipse.core.resources.prefs | 2 + .../.settings/org.eclipse.jdt.core.prefs | 7 + .../.settings/org.eclipse.m2e.core.prefs | 4 + .../META-INF/MANIFEST.MF | 13 + .../MODULE_LICENSE_EPL | 0 .../org.eclipse.andmore.swtmenubar/NOTICE | 224 ++ .../org.eclipse.andmore.swtmenubar/README | 80 + .../build.gradle | 16 + .../build.properties | 4 + .../plugin.properties | 4 + .../org.eclipse.andmore.swtmenubar/pom.xml | 37 + .../internal/MenuBarEnhancerCocoa.java | 341 +++ .../com/android/menubar/IMenuBarCallback.java | 42 + .../com/android/menubar/IMenuBarEnhancer.java | 73 + .../com/android/menubar/MenuBarEnhancer.java | 248 ++ .../android/menubar/MenuBarEnhancer37.java | 156 ++ .../swtmenubar.iml | 17 + .../.classpath | 7 + .../.gitignore | 2 + .../.project | 34 + .../.settings/README.txt | 2 + .../org.eclipse.core.resources.prefs | 2 + .../.settings/org.eclipse.jdt.core.prefs | 7 + .../.settings/org.eclipse.m2e.core.prefs | 4 + .../META-INF/MANIFEST.MF | 15 + .../MODULE_LICENSE_EPL | 0 .../org.eclipse.andmore.traceviewuilib/NOTICE | 190 ++ .../org.eclipse.andmore.traceviewuilib/README | 11 + .../build.gradle | 23 + .../build.properties | 4 + .../etc/traceview | 108 + .../etc/traceview.bat | 65 + .../plugin.properties | 4 + .../pom.xml | 37 + .../main/java/com/android/traceview/Call.java | 177 ++ .../android/traceview/ColorController.java | 113 + .../com/android/traceview/DmTraceReader.java | 754 ++++++ .../com/android/traceview/MainWindow.java | 300 +++ .../com/android/traceview/MethodData.java | 513 ++++ .../com/android/traceview/ProfileData.java | 88 + .../com/android/traceview/ProfileNode.java | 51 + .../android/traceview/ProfileProvider.java | 475 ++++ .../com/android/traceview/ProfileSelf.java | 39 + .../com/android/traceview/ProfileView.java | 332 +++ .../android/traceview/PropertiesDialog.java | 104 + .../java/com/android/traceview/Selection.java | 70 + .../traceview/SelectionController.java | 35 + .../com/android/traceview/ThreadData.java | 170 ++ .../com/android/traceview/TickScaler.java | 148 ++ .../java/com/android/traceview/TimeBase.java | 71 + .../com/android/traceview/TimeLineView.java | 2154 ++++++++++++++++ .../com/android/traceview/TraceAction.java | 31 + .../com/android/traceview/TraceReader.java | 79 + .../com/android/traceview/TraceUnits.java | 93 + .../src/main/resources/icons/sort_down.png | Bin 0 -> 102 bytes .../src/main/resources/icons/sort_up.png | Bin 0 -> 105 bytes .../main/resources/icons/traceview-128.png | Bin 0 -> 17131 bytes .../traceview.iml | 19 + .../.classpath | 7 + .../.gitignore | 2 + .../.project | 34 + .../org.eclipse.core.resources.prefs | 2 + .../.settings/org.eclipse.jdt.core.prefs | 7 + .../.settings/org.eclipse.m2e.core.prefs | 4 + .../META-INF/MANIFEST.MF | 17 + .../MODULE_LICENSE_APACHE2 | 0 .../NOTICE | 190 ++ .../build.gradle | 26 + .../build.properties | 4 + .../etc/uiautomatorviewer | 104 + .../etc/uiautomatorviewer.bat | 66 + .../plugin.properties | 4 + .../pom.xml | 37 + .../com/android/uiautomator/DebugBridge.java | 86 + .../com/android/uiautomator/OpenDialog.java | 225 ++ .../uiautomator/UiAutomatorHelper.java | 209 ++ .../android/uiautomator/UiAutomatorModel.java | 170 ++ .../android/uiautomator/UiAutomatorView.java | 608 +++++ .../uiautomator/UiAutomatorViewer.java | 116 + .../uiautomator/actions/ExpandAllAction.java | 42 + .../uiautomator/actions/ImageHelper.java | 48 + .../uiautomator/actions/OpenFilesAction.java | 83 + .../actions/SaveScreenShotAction.java | 92 + .../uiautomator/actions/ScreenshotAction.java | 182 ++ .../uiautomator/actions/ToggleNafAction.java | 46 + .../uiautomator/tree/AttributePair.java | 26 + .../uiautomator/tree/BasicTreeNode.java | 114 + .../tree/BasicTreeNodeContentProvider.java | 63 + .../uiautomator/tree/RootWindowNode.java | 52 + .../tree/UiHierarchyXmlLoader.java | 155 ++ .../com/android/uiautomator/tree/UiNode.java | 123 + .../src/main/java/images/delete.png | Bin 0 -> 1445 bytes .../src/main/java/images/expandall.png | Bin 0 -> 268 bytes .../src/main/java/images/next.png | Bin 0 -> 926 bytes .../src/main/java/images/open-folder.png | Bin 0 -> 383 bytes .../src/main/java/images/prev.png | Bin 0 -> 920 bytes .../src/main/java/images/save.png | Bin 0 -> 981 bytes .../src/main/java/images/screenshot.png | Bin 0 -> 1226 bytes .../main/java/images/screenshotcompressed.png | Bin 0 -> 590 bytes .../src/main/java/images/warning.png | Bin 0 -> 147 bytes andmore-swt/pom.xml | 41 + andmore.target/.gitignore | 1 + andmore.target/.project | 17 + .../.settings/org.eclipse.m2e.core.prefs | 4 + andmore.target/andmore.target.target | 97 + andmore.target/pom.xml | 10 + .../org.eclipse.andmore/andmore.target | 55 - pom.xml | 655 ++--- 771 files changed, 129344 insertions(+), 362 deletions(-) create mode 100644 andmore-swt/.gitignore create mode 100644 andmore-swt/.project create mode 100644 andmore-swt/.settings/org.eclipse.core.resources.prefs create mode 100644 andmore-swt/.settings/org.eclipse.m2e.core.prefs create mode 100644 andmore-swt/features/org.eclipse.andmore.ddmuilib/.gitignore create mode 100644 andmore-swt/features/org.eclipse.andmore.ddmuilib/.project create mode 100644 andmore-swt/features/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.core.resources.prefs create mode 100644 andmore-swt/features/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.m2e.core.prefs create mode 100644 andmore-swt/features/org.eclipse.andmore.ddmuilib/build.properties create mode 100644 andmore-swt/features/org.eclipse.andmore.ddmuilib/feature.properties create mode 100644 andmore-swt/features/org.eclipse.andmore.ddmuilib/feature.xml create mode 100644 andmore-swt/features/org.eclipse.andmore.ddmuilib/findbugsExclusion.xml create mode 100644 andmore-swt/features/org.eclipse.andmore.ddmuilib/pom.xml create mode 100644 andmore-swt/features/org.eclipse.andmore.ddmuilib/sourceTemplateFeature/.gitignore create mode 100644 andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/.gitignore create mode 100644 andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/.project create mode 100644 andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.core.resources.prefs create mode 100644 andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.m2e.core.prefs create mode 100644 andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/build.properties create mode 100644 andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/feature.properties create mode 100644 andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/feature.xml create mode 100644 andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/findbugsExclusion.xml create mode 100644 andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/pom.xml create mode 100644 andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/sourceTemplateFeature/.gitignore create mode 100644 andmore-swt/features/org.eclipse.andmore.sdkstats/.gitignore create mode 100644 andmore-swt/features/org.eclipse.andmore.sdkstats/.project create mode 100644 andmore-swt/features/org.eclipse.andmore.sdkstats/.settings/org.eclipse.core.resources.prefs create mode 100644 andmore-swt/features/org.eclipse.andmore.sdkstats/.settings/org.eclipse.m2e.core.prefs create mode 100644 andmore-swt/features/org.eclipse.andmore.sdkstats/build.properties create mode 100644 andmore-swt/features/org.eclipse.andmore.sdkstats/feature.properties create mode 100644 andmore-swt/features/org.eclipse.andmore.sdkstats/feature.xml create mode 100644 andmore-swt/features/org.eclipse.andmore.sdkstats/findbugsExclusion.xml create mode 100644 andmore-swt/features/org.eclipse.andmore.sdkstats/pom.xml create mode 100644 andmore-swt/features/org.eclipse.andmore.sdkstats/sourceTemplateFeature/.gitignore create mode 100644 andmore-swt/features/org.eclipse.andmore.sdkuilib/.gitignore create mode 100644 andmore-swt/features/org.eclipse.andmore.sdkuilib/.project create mode 100644 andmore-swt/features/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.core.resources.prefs create mode 100644 andmore-swt/features/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.m2e.core.prefs create mode 100644 andmore-swt/features/org.eclipse.andmore.sdkuilib/build.properties create mode 100644 andmore-swt/features/org.eclipse.andmore.sdkuilib/feature.properties create mode 100644 andmore-swt/features/org.eclipse.andmore.sdkuilib/feature.xml create mode 100644 andmore-swt/features/org.eclipse.andmore.sdkuilib/findbugsExclusion.xml create mode 100644 andmore-swt/features/org.eclipse.andmore.sdkuilib/pom.xml create mode 100644 andmore-swt/features/org.eclipse.andmore.sdkuilib/sourceTemplateFeature/.gitignore create mode 100644 andmore-swt/features/org.eclipse.andmore.swt/.gitignore create mode 100644 andmore-swt/features/org.eclipse.andmore.swt/.project create mode 100644 andmore-swt/features/org.eclipse.andmore.swt/.settings/org.eclipse.core.resources.prefs create mode 100644 andmore-swt/features/org.eclipse.andmore.swt/.settings/org.eclipse.m2e.core.prefs create mode 100644 andmore-swt/features/org.eclipse.andmore.swt/build.properties create mode 100644 andmore-swt/features/org.eclipse.andmore.swt/feature.properties create mode 100644 andmore-swt/features/org.eclipse.andmore.swt/feature.xml create mode 100644 andmore-swt/features/org.eclipse.andmore.swt/findbugsExclusion.xml create mode 100644 andmore-swt/features/org.eclipse.andmore.swt/pom.xml create mode 100644 andmore-swt/features/org.eclipse.andmore.swt/sourceTemplateFeature/.gitignore create mode 100644 andmore-swt/features/org.eclipse.andmore.swtmenubar/.gitignore create mode 100644 andmore-swt/features/org.eclipse.andmore.swtmenubar/.project create mode 100644 andmore-swt/features/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.core.resources.prefs create mode 100644 andmore-swt/features/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.m2e.core.prefs create mode 100644 andmore-swt/features/org.eclipse.andmore.swtmenubar/build.properties create mode 100644 andmore-swt/features/org.eclipse.andmore.swtmenubar/feature.properties create mode 100644 andmore-swt/features/org.eclipse.andmore.swtmenubar/feature.xml create mode 100644 andmore-swt/features/org.eclipse.andmore.swtmenubar/findbugsExclusion.xml create mode 100644 andmore-swt/features/org.eclipse.andmore.swtmenubar/pom.xml create mode 100644 andmore-swt/features/org.eclipse.andmore.swtmenubar/sourceTemplateFeature/.gitignore create mode 100644 andmore-swt/features/org.eclipse.andmore.traceviewuilib/.gitignore create mode 100644 andmore-swt/features/org.eclipse.andmore.traceviewuilib/.project create mode 100644 andmore-swt/features/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.core.resources.prefs create mode 100644 andmore-swt/features/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.m2e.core.prefs create mode 100644 andmore-swt/features/org.eclipse.andmore.traceviewuilib/build.properties create mode 100644 andmore-swt/features/org.eclipse.andmore.traceviewuilib/feature.properties create mode 100644 andmore-swt/features/org.eclipse.andmore.traceviewuilib/feature.xml create mode 100644 andmore-swt/features/org.eclipse.andmore.traceviewuilib/findbugsExclusion.xml create mode 100644 andmore-swt/features/org.eclipse.andmore.traceviewuilib/pom.xml create mode 100644 andmore-swt/features/org.eclipse.andmore.traceviewuilib/sourceTemplateFeature/.gitignore create mode 100644 andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/.gitignore create mode 100644 andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/.project create mode 100644 andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.core.resources.prefs create mode 100644 andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.m2e.core.prefs create mode 100644 andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/build.properties create mode 100644 andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/feature.properties create mode 100644 andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/feature.xml create mode 100644 andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/findbugsExclusion.xml create mode 100644 andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/pom.xml create mode 100644 andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/sourceTemplateFeature/.gitignore create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/.classpath create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/.gitignore create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/.project create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.core.resources.prefs create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.jdt.core.prefs create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.m2e.core.prefs create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/META-INF/MANIFEST.MF create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/NOTICE create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/README create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/build.gradle create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/build.properties create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/ddmuilib.iml create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/plugin.properties create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/pom.xml create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/AbstractBufferFindTarget.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/Addr2Line.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/AllocationPanel.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/BackgroundThread.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/BaseHeapPanel.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ClientDisplayPanel.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/DdmUiPreferences.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/DevicePanel.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/EmulatorControlPanel.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/FindDialog.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/HeapPanel.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/IFindTarget.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ITableFocusListener.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ImageLoader.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/InfoPanel.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/NativeHeapPanel.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/Panel.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ScreenShotDialog.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/SelectionDependentPanel.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/StackTracePanel.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/SyncProgressHelper.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/SyncProgressMonitor.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/SysinfoPanel.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/TableHelper.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/TablePanel.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ThreadPanel.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/actions/ICommonAction.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/actions/ToolItemAction.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/annotation/UiThread.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/annotation/WorkerThread.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/console/DdmConsole.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/console/IDdmConsole.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceContentProvider.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceExplorer.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/explorer/FileLabelProvider.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/handler/BaseFileHandler.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/handler/MethodProfilingHandler.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeDiffAllocationInfo.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapDataImporter.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapDiffSnapshot.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapLabelProvider.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapPanel.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapProviderByAllocations.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapProviderByLibrary.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapSnapshot.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeLibraryAllocationInfo.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeStackContentProvider.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeStackLabelProvider.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeSymbolResolverTask.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/CoordinateControls.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/GpxParser.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/KmlParser.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/LocationPoint.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/TrackContentProvider.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/TrackLabelProvider.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/TrackPoint.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/WayPoint.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/WayPointContentProvider.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/WayPointLabelProvider.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/BugReportImporter.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayFilteredLog.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayGraph.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayLog.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySync.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySyncHistogram.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySyncPerf.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventDisplay.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventDisplayOptions.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventLogImporter.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventLogPanel.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventValueSelector.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/OccurrenceRenderer.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/SyncCommon.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/EditFilterDialog.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/ILogCatBufferChangeListener.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/ILogCatMessageSelectionListener.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterContentProvider.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterData.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterLabelProvider.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterSettingsDialog.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterSettingsSerializer.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatMessageList.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatPanel.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiver.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiverFactory.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatStackTraceParser.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogColors.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogFilter.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogPanel.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/net/NetworkPanel.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/screenrecord/ScreenRecorderAction.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/screenrecord/ScreenRecorderOptionsDialog.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/vmtrace/VmTraceOptionsDialog.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/add.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/android.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/backward.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/capture.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/clear.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/d.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/debug-attach.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/debug-error.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/debug-wait.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/delete.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/device.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/diff.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/displayfilters.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/down.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/e.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/edit.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/empty.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/emulator.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/file.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/folder.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/forward.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/gc.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/groupby.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/halt.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/heap.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/hprof.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/i.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/importBug.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/load.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/pause.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/play.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/pull.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/push.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/save.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/scroll_lock.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/sort_down.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/sort_up.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/thread.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/tracing_start.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/tracing_stop.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/up.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/v.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/w.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/warning.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/zygote.png create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/BugReportParserTest.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/heap/NativeHeapDataImporterTest.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/logcat/LogCatFilterSettingsSerializerTest.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/logcat/LogCatStackTraceParserTest.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/logcat/RollingBufferFindTest.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.classpath create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.gitignore create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.project create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.settings/README.txt create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.core.resources.prefs create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.jdt.core.prefs create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.m2e.core.prefs create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/META-INF/MANIFEST.MF create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/NOTICE create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/build.gradle create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/build.properties create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/hierarchyviewer2lib.iml create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/plugin.properties create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/pom.xml create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/HierarchyViewerDirector.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/CapturePSDAction.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DisplayViewAction.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DumpDisplayListAction.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DumpThemeAction.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/EvaluateContrastAction.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/ImageAction.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/InspectScreenshotAction.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/InvalidateAction.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/LoadOverlayAction.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/LoadViewHierarchyAction.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/PixelPerfectAutoRefreshAction.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/PixelPerfectEnabledAction.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/ProfileNodesAction.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshPixelPerfectAction.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshPixelPerfectTreeAction.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshViewAction.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshWindowsAction.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RequestLayoutAction.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SavePixelPerfectAction.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SaveTreeViewAction.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SelectedNodeEnabledAction.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/TreeViewEnabledAction.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/AbstractHvDevice.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/DdmViewDebugDevice.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/DeviceBridge.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/DeviceConnection.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/HvDeviceFactory.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/IHvDevice.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/ViewServerDevice.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/WindowUpdater.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/DeviceSelectionModel.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/EvaluateContrastModel.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/PixelPerfectModel.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/ThemeModel.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/TreeViewModel.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/ViewNode.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/Window.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/CaptureDisplay.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DevicePropertyEditingSupport.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DeviceSelector.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DumpThemeDisplay.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/EvaluateContrastDisplay.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/InvokeMethodPrompt.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/LayoutViewer.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfect.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectControls.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectLoupe.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectPixelPanel.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectTree.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PropertyViewer.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeView.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeViewControls.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeViewOverview.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/DrawableViewNode.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/PsdFile.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/TreeColumnResizer.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/auto-refresh.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/capture-psd.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/device-view-selected.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/device-view.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/display.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/filtered.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/green.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/inspect-screenshot.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/invalidate.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/load-all-views.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/load-overlay.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/load-view-hierarchy.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/not-selected.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/on-black.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/on-white.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/picker.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/pixel-perfect-view-selected.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/pixel-perfect-view.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/profile.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/red.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/refresh-windows.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/request-layout.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/save.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/sdk-hierarchyviewer-128.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/sdk-hierarchyviewer-16.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/selected-filtered-small.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/selected-filtered.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/selected-small.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/selected.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/show-extras.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/show-overlay.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/tree-view-selected.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/tree-view.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/yellow.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/java/com/android/hierarchyviewerlib/models/EvaluateContrastModelTest.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/java/com/android/hierarchyviewerlib/ui/PropertyViewerTest.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/resources/images/all_black.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/resources/images/black_on_white.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/resources/images/dark_on_light_greens.png create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/resources/images/white_on_black.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkstats/.classpath create mode 100644 andmore-swt/org.eclipse.andmore.sdkstats/.gitignore create mode 100644 andmore-swt/org.eclipse.andmore.sdkstats/.project create mode 100644 andmore-swt/org.eclipse.andmore.sdkstats/.settings/README.txt create mode 100644 andmore-swt/org.eclipse.andmore.sdkstats/.settings/org.eclipse.core.resources.prefs create mode 100644 andmore-swt/org.eclipse.andmore.sdkstats/.settings/org.eclipse.jdt.core.prefs create mode 100644 andmore-swt/org.eclipse.andmore.sdkstats/.settings/org.eclipse.m2e.core.prefs create mode 100644 andmore-swt/org.eclipse.andmore.sdkstats/META-INF/MANIFEST.MF create mode 100644 andmore-swt/org.eclipse.andmore.sdkstats/NOTICE create mode 100644 andmore-swt/org.eclipse.andmore.sdkstats/README create mode 100644 andmore-swt/org.eclipse.andmore.sdkstats/build.gradle create mode 100644 andmore-swt/org.eclipse.andmore.sdkstats/build.properties create mode 100644 andmore-swt/org.eclipse.andmore.sdkstats/plugin.properties create mode 100644 andmore-swt/org.eclipse.andmore.sdkstats/pom.xml create mode 100644 andmore-swt/org.eclipse.andmore.sdkstats/sdkstats.iml create mode 100644 andmore-swt/org.eclipse.andmore.sdkstats/src/main/java/com/android/sdkstats/DdmsPreferenceStore.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkstats/src/main/java/com/android/sdkstats/SdkStatsPermissionDialog.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkstats/src/main/java/com/android/sdkstats/SdkStatsService.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkstats/src/test/java/com/android/sdkstats/SdkStatsServiceTest.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/.classpath create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/.gitignore create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/.project create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.core.resources.prefs create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.jdt.core.prefs create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.m2e.core.prefs create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/META-INF/MANIFEST.MF create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/MODULE_LICENSE_APACHE2 create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/NOTICE create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/build.properties create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/libs/commons-compress-1.8.1.jar create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/plugin.properties create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/pom.xml create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/AndroidTargetHash.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/AndroidVersion.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/BuildToolInfo.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/IAndroidTarget.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/ISystemImage.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/LegacyAndroidVersion.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/SdkManager.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/SdkVersionInfo.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/SystemImage.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/androidTarget/AddOnTarget.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/androidTarget/MissingTarget.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/androidTarget/OptionalLibraryImpl.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/androidTarget/PlatformTarget.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/avd/AvdInfo.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/avd/AvdManager.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/avd/HardwareProperties.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/BuildConfig.template create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/BuildConfigGenerator.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/DebugKeyProvider.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/KeystoreHelper.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/SignedJarBuilder.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/SymbolLoader.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/SymbolWriter.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/project/IPropertySource.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/project/ProjectCreator.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/project/ProjectProperties.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/project/ProjectPropertiesWorkingCopy.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/AdbWrapper.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/AddonsListFetcher.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/CanceledByUserException.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/DownloadCache.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/ITask.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/ITaskFactory.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/ITaskMonitor.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/LocalSdkParser.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/NullTaskMonitor.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/SdkStats.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/UrlOpener.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/UserCredentials.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/ArchFilter.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/Archive.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/ArchiveInstaller.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/ArchiveReplacement.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/BitSize.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/ChecksumType.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/HostOs.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/LegacyArch.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/LegacyOs.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/AddonPackage.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/BrokenPackage.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/BuildToolPackage.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/DocPackage.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/ExtraPackage.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/FullRevisionPackage.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IAndroidVersionProvider.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IExactApiLevelDependency.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IFullRevisionProvider.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/ILayoutlibVersion.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IMinApiLevelDependency.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IMinPlatformToolsDependency.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IMinToolsDependency.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IPlatformDependency.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/LayoutlibVersionMixin.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/MajorRevisionPackage.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/MinToolsMixin.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/MinToolsPackage.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/NoPreviewRevisionPackage.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/Package.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/PackageParserUtils.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/PlatformPackage.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/PlatformToolPackage.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/SamplePackage.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/SourcePackage.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/SystemImagePackage.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/ToolPackage.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkAddonSource.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkRepoSource.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSource.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSourceCategory.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSourceProperties.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSources.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSysImgSource.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/ArchiveInfo.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/ISettingsPage.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/IUpdaterData.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/PackageLoader.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/PkgItem.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/SdkUpdaterLogic.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/SdkUpdaterNoWindow.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/SettingsController.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/UpdaterData.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/io/IFileOp.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/io/LegacyFileOp.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/AddonManifestIniProps.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/FullRevision.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/IDescription.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/IListDescription.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/ISdkChangeListener.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/License.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/MajorRevision.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/NoPreviewRevision.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/PkgProps.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/PreciseRevision.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/README.txt create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/RepoConstants.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/RepoXsdUtil.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkAddonConstants.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkAddonsListConstants.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkRepoConstants.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkStatsConstants.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkSysImgConstants.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IPkgCapabilities.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IPkgDesc.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IPkgDescAddon.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IPkgDescExtra.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IdDisplay.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/PkgDesc.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/PkgDescAddon.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/PkgDescExtra.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/PkgType.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalAddonPkgInfo.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalAddonSysImgPkgInfo.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalBuildToolPkgInfo.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalDirInfo.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalDocPkgInfo.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalExtraPkgInfo.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalNdkPkgInfo.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalPkgInfo.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalPlatformPkgInfo.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalPlatformToolPkgInfo.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalSamplePkgInfo.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalSdk.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalSourcePkgInfo.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalSysImgPkgInfo.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalToolPkgInfo.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/PackageParserUtils.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-01.xsd create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-02.xsd create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-03.xsd create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-04.xsd create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-05.xsd create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-06.xsd create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-07.xsd create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addons-list-1.xsd create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addons-list-2.xsd create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-01.xsd create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-02.xsd create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-03.xsd create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-04.xsd create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-05.xsd create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-06.xsd create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-07.xsd create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-08.xsd create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-09.xsd create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-10.xsd create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-11.xsd create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-stats-1.xsd create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-sys-img-01.xsd create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-sys-img-02.xsd create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-sys-img-03.xsd create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/util/CommandLineParser.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/util/FormatUtils.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/util/LineUtil.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/AboutDialog.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ISdkUpdaterWindow.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ISwtUpdaterData.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/MenuBarWrapper.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SdkUpdaterChooserDialog.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SettingsDialog.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SwtUpdaterData.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/UpdaterBaseDialog.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PackagesDiffLogic.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgCategory.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgCategoryApi.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgCategorySource.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgContentProvider.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/SdkLogAdapter.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/SwtPackageLoader.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/ImageFactory.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/accept_icon16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/addon_pkg_16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/android_icon_128.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/android_icon_16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/archive_icon16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/broken_16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/broken_pkg_16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/buildtool_pkg_16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/doc_pkg_16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/error_icon_16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/extra_pkg_16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/incompat_icon16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/log_off_16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/log_on_16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/nopkg_icon_16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkg_incompat_16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkg_installed_16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkg_new_16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkg_update_16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkgcat_16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkgcat_other_16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/platform_pkg_16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/platformtool_pkg_16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/reject_icon16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/sample_pkg_16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/sdkman_logo_128.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/source_pkg_16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/status_ok_16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/stop_disabled_16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/stop_enabled_16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/sysimg_pkg_16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/tag_android-tv_16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/tag_android-tv_32.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/tag_android-wear_16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/tag_android-wear_32.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/tag_default_16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/tag_default_32.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/tool_pkg_16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/unknown_icon16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/warning_icon16.png create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AddonSitesDialog.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AdtUpdateDialog.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AvdManagerPage.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AvdManagerWindowImpl1.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/DeviceManagerPage.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/LogWindow.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPage.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPageIcons.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPageImpl.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PkgTreeColumnViewerLabelProvider.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/SdkUpdaterWindowImpl2.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/ShellSizeAndPos.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ILogUiProvider.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/IProgressUiProvider.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTask.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTaskDialog.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTaskFactory.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressView.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressViewFactory.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/TaskMonitorImpl.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationDialog.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationPresenter.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationSwtView.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdDetailsDialog.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdSelector.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdStartDialog.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/DeviceCreationDialog.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/HardwarePropertyChooser.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/IMessageBoxLogger.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/ImgDisabledButton.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/LegacyAvdEditDialog.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/MessageBoxLog.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/ResolutionChooserDialog.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/SdkTargetSelector.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/ToggleButton.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/repository/AvdManagerWindow.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/repository/SdkUpdaterWindow.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/AuthenticationDialog.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/GridDataBuilder.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/GridDialog.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/GridLayoutBuilder.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/SwtBaseDialog.java create mode 100644 andmore-swt/org.eclipse.andmore.swt/.classpath create mode 100644 andmore-swt/org.eclipse.andmore.swt/.gitignore create mode 100644 andmore-swt/org.eclipse.andmore.swt/.project create mode 100644 andmore-swt/org.eclipse.andmore.swt/.settings/org.eclipse.core.resources.prefs create mode 100644 andmore-swt/org.eclipse.andmore.swt/.settings/org.eclipse.jdt.core.prefs create mode 100644 andmore-swt/org.eclipse.andmore.swt/.settings/org.eclipse.m2e.core.prefs create mode 100644 andmore-swt/org.eclipse.andmore.swt/META-INF/MANIFEST.MF create mode 100644 andmore-swt/org.eclipse.andmore.swt/about.html create mode 100644 andmore-swt/org.eclipse.andmore.swt/build.properties create mode 100644 andmore-swt/org.eclipse.andmore.swt/findbugsExclusion.xml create mode 100644 andmore-swt/org.eclipse.andmore.swt/plugin.properties create mode 100644 andmore-swt/org.eclipse.andmore.swt/plugin.xml create mode 100644 andmore-swt/org.eclipse.andmore.swt/pom.xml create mode 100644 andmore-swt/org.eclipse.andmore.swt/src/org/eclipse/andmore/base/InstallDetails.java create mode 100644 andmore-swt/org.eclipse.andmore.swtmenubar/.classpath create mode 100644 andmore-swt/org.eclipse.andmore.swtmenubar/.gitignore create mode 100644 andmore-swt/org.eclipse.andmore.swtmenubar/.project create mode 100644 andmore-swt/org.eclipse.andmore.swtmenubar/.settings/README.txt create mode 100644 andmore-swt/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.core.resources.prefs create mode 100644 andmore-swt/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.jdt.core.prefs create mode 100644 andmore-swt/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.m2e.core.prefs create mode 100644 andmore-swt/org.eclipse.andmore.swtmenubar/META-INF/MANIFEST.MF create mode 100644 andmore-swt/org.eclipse.andmore.swtmenubar/MODULE_LICENSE_EPL create mode 100644 andmore-swt/org.eclipse.andmore.swtmenubar/NOTICE create mode 100644 andmore-swt/org.eclipse.andmore.swtmenubar/README create mode 100644 andmore-swt/org.eclipse.andmore.swtmenubar/build.gradle create mode 100644 andmore-swt/org.eclipse.andmore.swtmenubar/build.properties create mode 100644 andmore-swt/org.eclipse.andmore.swtmenubar/plugin.properties create mode 100644 andmore-swt/org.eclipse.andmore.swtmenubar/pom.xml create mode 100644 andmore-swt/org.eclipse.andmore.swtmenubar/src/main-darwin/java/com/android/menubar/internal/MenuBarEnhancerCocoa.java create mode 100644 andmore-swt/org.eclipse.andmore.swtmenubar/src/main/java/com/android/menubar/IMenuBarCallback.java create mode 100644 andmore-swt/org.eclipse.andmore.swtmenubar/src/main/java/com/android/menubar/IMenuBarEnhancer.java create mode 100644 andmore-swt/org.eclipse.andmore.swtmenubar/src/main/java/com/android/menubar/MenuBarEnhancer.java create mode 100644 andmore-swt/org.eclipse.andmore.swtmenubar/src/main/java/com/android/menubar/MenuBarEnhancer37.java create mode 100644 andmore-swt/org.eclipse.andmore.swtmenubar/swtmenubar.iml create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/.classpath create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/.gitignore create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/.project create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/.settings/README.txt create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.core.resources.prefs create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.jdt.core.prefs create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.m2e.core.prefs create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/META-INF/MANIFEST.MF create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/MODULE_LICENSE_EPL create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/NOTICE create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/README create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/build.gradle create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/build.properties create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/etc/traceview create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/etc/traceview.bat create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/plugin.properties create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/pom.xml create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/Call.java create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ColorController.java create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/DmTraceReader.java create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/MainWindow.java create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/MethodData.java create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileData.java create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileNode.java create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileProvider.java create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileSelf.java create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileView.java create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/PropertiesDialog.java create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/Selection.java create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/SelectionController.java create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ThreadData.java create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TickScaler.java create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TimeBase.java create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TimeLineView.java create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TraceAction.java create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TraceReader.java create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TraceUnits.java create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/resources/icons/sort_down.png create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/resources/icons/sort_up.png create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/resources/icons/traceview-128.png create mode 100644 andmore-swt/org.eclipse.andmore.traceviewuilib/traceview.iml create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/.classpath create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/.gitignore create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/.project create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.core.resources.prefs create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.jdt.core.prefs create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.m2e.core.prefs create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/META-INF/MANIFEST.MF create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/MODULE_LICENSE_APACHE2 create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/NOTICE create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/build.gradle create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/build.properties create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/etc/uiautomatorviewer create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/etc/uiautomatorviewer.bat create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/plugin.properties create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/pom.xml create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/DebugBridge.java create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/OpenDialog.java create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorHelper.java create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorModel.java create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorView.java create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorViewer.java create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ExpandAllAction.java create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ImageHelper.java create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/OpenFilesAction.java create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/SaveScreenShotAction.java create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ScreenshotAction.java create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ToggleNafAction.java create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/AttributePair.java create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/BasicTreeNode.java create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/BasicTreeNodeContentProvider.java create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/RootWindowNode.java create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/UiHierarchyXmlLoader.java create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/UiNode.java create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/delete.png create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/expandall.png create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/next.png create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/open-folder.png create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/prev.png create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/save.png create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/screenshot.png create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/screenshotcompressed.png create mode 100644 andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/warning.png create mode 100644 andmore-swt/pom.xml create mode 100644 andmore.target/.gitignore create mode 100644 andmore.target/.project create mode 100644 andmore.target/.settings/org.eclipse.m2e.core.prefs create mode 100644 andmore.target/andmore.target.target create mode 100644 andmore.target/pom.xml delete mode 100644 android-core/plugins/org.eclipse.andmore/andmore.target diff --git a/andmore-swt/.gitignore b/andmore-swt/.gitignore new file mode 100644 index 00000000..d16920c5 --- /dev/null +++ b/andmore-swt/.gitignore @@ -0,0 +1,14 @@ +*~ +*.bak +*.pyc +Thumbs.db +*.class +*.DS_Store +.gradle +/build +/out +/repo +.idea/workspace.xml +.idea/dictionaries/tnorbye.xml +bin + diff --git a/andmore-swt/.project b/andmore-swt/.project new file mode 100644 index 00000000..3d52c9a2 --- /dev/null +++ b/andmore-swt/.project @@ -0,0 +1,17 @@ + + + andmore-parent + + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + + diff --git a/andmore-swt/.settings/org.eclipse.core.resources.prefs b/andmore-swt/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..4824b802 --- /dev/null +++ b/andmore-swt/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/andmore-swt/.settings/org.eclipse.m2e.core.prefs b/andmore-swt/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore-swt/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore-swt/features/org.eclipse.andmore.ddmuilib/.gitignore b/andmore-swt/features/org.eclipse.andmore.ddmuilib/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.ddmuilib/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/andmore-swt/features/org.eclipse.andmore.ddmuilib/.project b/andmore-swt/features/org.eclipse.andmore.ddmuilib/.project new file mode 100644 index 00000000..555fa175 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.ddmuilib/.project @@ -0,0 +1,23 @@ + + + org.eclipse.andmore.ddmuilib.feature + + + + + + org.eclipse.pde.FeatureBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.FeatureNature + + diff --git a/andmore-swt/features/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.core.resources.prefs b/andmore-swt/features/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..4824b802 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/andmore-swt/features/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.m2e.core.prefs b/andmore-swt/features/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore-swt/features/org.eclipse.andmore.ddmuilib/build.properties b/andmore-swt/features/org.eclipse.andmore.ddmuilib/build.properties new file mode 100644 index 00000000..3104d6d7 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.ddmuilib/build.properties @@ -0,0 +1,2 @@ +bin.includes = feature.xml,\ + feature.properties diff --git a/andmore-swt/features/org.eclipse.andmore.ddmuilib/feature.properties b/andmore-swt/features/org.eclipse.andmore.ddmuilib/feature.properties new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/features/org.eclipse.andmore.ddmuilib/feature.xml b/andmore-swt/features/org.eclipse.andmore.ddmuilib/feature.xml new file mode 100644 index 00000000..ee4da604 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.ddmuilib/feature.xml @@ -0,0 +1,46 @@ + + + + + Android UI Dalvik Debug Monitor Service + + + + Copyright (C) 2007-2011 The Android Open Source Project + + + + %license + + + + + + + + + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.ddmuilib/findbugsExclusion.xml b/andmore-swt/features/org.eclipse.andmore.ddmuilib/findbugsExclusion.xml new file mode 100644 index 00000000..7443a30f --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.ddmuilib/findbugsExclusion.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.ddmuilib/pom.xml b/andmore-swt/features/org.eclipse.andmore.ddmuilib/pom.xml new file mode 100644 index 00000000..46037543 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.ddmuilib/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + org.eclipse.andmore.ddmuilib.feature + eclipse-feature + ddmuilib + + + ../pom.xml + org.eclipse.andmore + swt-droid-parent + 0.5.2-SNAPSHOT + + + + + org.eclipse.tycho.extras + tycho-source-feature-plugin + + + source-feature + package + + source-feature + + + + + + org.eclipse.tycho + tycho-p2-plugin + ${tycho-version} + + + attach-p2-metadata + package + + p2-metadata + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.ddmuilib/sourceTemplateFeature/.gitignore b/andmore-swt/features/org.eclipse.andmore.ddmuilib/sourceTemplateFeature/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/.gitignore b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/.project b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/.project new file mode 100644 index 00000000..7e0e0583 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/.project @@ -0,0 +1,23 @@ + + + org.eclipse.andmore.hierarchyviewer2lib.feature + + + + + + org.eclipse.pde.FeatureBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.FeatureNature + + diff --git a/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.core.resources.prefs b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..4824b802 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.m2e.core.prefs b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/build.properties b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/build.properties new file mode 100644 index 00000000..3104d6d7 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/build.properties @@ -0,0 +1,2 @@ +bin.includes = feature.xml,\ + feature.properties diff --git a/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/feature.properties b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/feature.properties new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/feature.xml b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/feature.xml new file mode 100644 index 00000000..56959b91 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/feature.xml @@ -0,0 +1,53 @@ + + + + + Android UI Hierarchy Viewer2 library + + + + Copyright (C) 2007-2011 The Android Open Source Project + + + + %license + + + + + + + + + + + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/findbugsExclusion.xml b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/findbugsExclusion.xml new file mode 100644 index 00000000..7443a30f --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/findbugsExclusion.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/pom.xml b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/pom.xml new file mode 100644 index 00000000..cfb8cc1e --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + org.eclipse.andmore.hierarchyviewer2lib.feature + eclipse-feature + hierarchyviewer2lib + + + ../pom.xml + org.eclipse.andmore + swt-droid-parent + 0.5.2-SNAPSHOT + + + + + org.eclipse.tycho.extras + tycho-source-feature-plugin + + + source-feature + package + + source-feature + + + + + + org.eclipse.tycho + tycho-p2-plugin + ${tycho-version} + + + attach-p2-metadata + package + + p2-metadata + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/sourceTemplateFeature/.gitignore b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/sourceTemplateFeature/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/features/org.eclipse.andmore.sdkstats/.gitignore b/andmore-swt/features/org.eclipse.andmore.sdkstats/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.sdkstats/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/andmore-swt/features/org.eclipse.andmore.sdkstats/.project b/andmore-swt/features/org.eclipse.andmore.sdkstats/.project new file mode 100644 index 00000000..5d7e617d --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.sdkstats/.project @@ -0,0 +1,23 @@ + + + org.eclipse.andmore.sdkstats.feature + + + + + + org.eclipse.pde.FeatureBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.FeatureNature + + diff --git a/andmore-swt/features/org.eclipse.andmore.sdkstats/.settings/org.eclipse.core.resources.prefs b/andmore-swt/features/org.eclipse.andmore.sdkstats/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..4824b802 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.sdkstats/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/andmore-swt/features/org.eclipse.andmore.sdkstats/.settings/org.eclipse.m2e.core.prefs b/andmore-swt/features/org.eclipse.andmore.sdkstats/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.sdkstats/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore-swt/features/org.eclipse.andmore.sdkstats/build.properties b/andmore-swt/features/org.eclipse.andmore.sdkstats/build.properties new file mode 100644 index 00000000..3104d6d7 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.sdkstats/build.properties @@ -0,0 +1,2 @@ +bin.includes = feature.xml,\ + feature.properties diff --git a/andmore-swt/features/org.eclipse.andmore.sdkstats/feature.properties b/andmore-swt/features/org.eclipse.andmore.sdkstats/feature.properties new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/features/org.eclipse.andmore.sdkstats/feature.xml b/andmore-swt/features/org.eclipse.andmore.sdkstats/feature.xml new file mode 100644 index 00000000..e05be3a7 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.sdkstats/feature.xml @@ -0,0 +1,46 @@ + + + + + Android UI SDK Statistics + + + + Copyright (C) 2007-2011 The Android Open Source Project + + + + %license + + + + + + + + + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.sdkstats/findbugsExclusion.xml b/andmore-swt/features/org.eclipse.andmore.sdkstats/findbugsExclusion.xml new file mode 100644 index 00000000..7443a30f --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.sdkstats/findbugsExclusion.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.sdkstats/pom.xml b/andmore-swt/features/org.eclipse.andmore.sdkstats/pom.xml new file mode 100644 index 00000000..6c3e500b --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.sdkstats/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + org.eclipse.andmore.sdkstats.feature + eclipse-feature + sdkstats + + + ../pom.xml + org.eclipse.andmore + swt-droid-parent + 0.5.2-SNAPSHOT + + + + + org.eclipse.tycho.extras + tycho-source-feature-plugin + + + source-feature + package + + source-feature + + + + + + org.eclipse.tycho + tycho-p2-plugin + ${tycho-version} + + + attach-p2-metadata + package + + p2-metadata + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.sdkstats/sourceTemplateFeature/.gitignore b/andmore-swt/features/org.eclipse.andmore.sdkstats/sourceTemplateFeature/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/features/org.eclipse.andmore.sdkuilib/.gitignore b/andmore-swt/features/org.eclipse.andmore.sdkuilib/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.sdkuilib/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/andmore-swt/features/org.eclipse.andmore.sdkuilib/.project b/andmore-swt/features/org.eclipse.andmore.sdkuilib/.project new file mode 100644 index 00000000..909e7c2c --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.sdkuilib/.project @@ -0,0 +1,23 @@ + + + org.eclipse.andmore.sdkuilib.feature + + + + + + org.eclipse.pde.FeatureBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.FeatureNature + + diff --git a/andmore-swt/features/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.core.resources.prefs b/andmore-swt/features/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..4824b802 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/andmore-swt/features/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.m2e.core.prefs b/andmore-swt/features/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore-swt/features/org.eclipse.andmore.sdkuilib/build.properties b/andmore-swt/features/org.eclipse.andmore.sdkuilib/build.properties new file mode 100644 index 00000000..3104d6d7 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.sdkuilib/build.properties @@ -0,0 +1,2 @@ +bin.includes = feature.xml,\ + feature.properties diff --git a/andmore-swt/features/org.eclipse.andmore.sdkuilib/feature.properties b/andmore-swt/features/org.eclipse.andmore.sdkuilib/feature.properties new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/features/org.eclipse.andmore.sdkuilib/feature.xml b/andmore-swt/features/org.eclipse.andmore.sdkuilib/feature.xml new file mode 100644 index 00000000..2c1fe365 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.sdkuilib/feature.xml @@ -0,0 +1,46 @@ + + + + + Android UI SDK LIbrary + + + + Copyright (C) 2007-2011 The Android Open Source Project + + + + %license + + + + + + + + + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.sdkuilib/findbugsExclusion.xml b/andmore-swt/features/org.eclipse.andmore.sdkuilib/findbugsExclusion.xml new file mode 100644 index 00000000..7443a30f --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.sdkuilib/findbugsExclusion.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.sdkuilib/pom.xml b/andmore-swt/features/org.eclipse.andmore.sdkuilib/pom.xml new file mode 100644 index 00000000..a0b56f88 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.sdkuilib/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + org.eclipse.andmore.sdkuilib.feature + eclipse-feature + sdkuilib + + + ../pom.xml + org.eclipse.andmore + swt-droid-parent + 0.5.2-SNAPSHOT + + + + + org.eclipse.tycho.extras + tycho-source-feature-plugin + + + source-feature + package + + source-feature + + + + + + org.eclipse.tycho + tycho-p2-plugin + ${tycho-version} + + + attach-p2-metadata + package + + p2-metadata + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.sdkuilib/sourceTemplateFeature/.gitignore b/andmore-swt/features/org.eclipse.andmore.sdkuilib/sourceTemplateFeature/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/features/org.eclipse.andmore.swt/.gitignore b/andmore-swt/features/org.eclipse.andmore.swt/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.swt/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/andmore-swt/features/org.eclipse.andmore.swt/.project b/andmore-swt/features/org.eclipse.andmore.swt/.project new file mode 100644 index 00000000..57952847 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.swt/.project @@ -0,0 +1,23 @@ + + + org.eclipse.andmore.swt.feature + + + + + + org.eclipse.pde.FeatureBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.FeatureNature + + diff --git a/andmore-swt/features/org.eclipse.andmore.swt/.settings/org.eclipse.core.resources.prefs b/andmore-swt/features/org.eclipse.andmore.swt/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..4824b802 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.swt/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/andmore-swt/features/org.eclipse.andmore.swt/.settings/org.eclipse.m2e.core.prefs b/andmore-swt/features/org.eclipse.andmore.swt/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.swt/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore-swt/features/org.eclipse.andmore.swt/build.properties b/andmore-swt/features/org.eclipse.andmore.swt/build.properties new file mode 100644 index 00000000..3104d6d7 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.swt/build.properties @@ -0,0 +1,2 @@ +bin.includes = feature.xml,\ + feature.properties diff --git a/andmore-swt/features/org.eclipse.andmore.swt/feature.properties b/andmore-swt/features/org.eclipse.andmore.swt/feature.properties new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/features/org.eclipse.andmore.swt/feature.xml b/andmore-swt/features/org.eclipse.andmore.swt/feature.xml new file mode 100644 index 00000000..498e1b52 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.swt/feature.xml @@ -0,0 +1,40 @@ + + + + + Android UI Automator Viewer + + + + Copyright (C) 2007-2011 The Android Open Source Project + + + + %license + + + + + + + + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.swt/findbugsExclusion.xml b/andmore-swt/features/org.eclipse.andmore.swt/findbugsExclusion.xml new file mode 100644 index 00000000..7443a30f --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.swt/findbugsExclusion.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.swt/pom.xml b/andmore-swt/features/org.eclipse.andmore.swt/pom.xml new file mode 100644 index 00000000..7c9ed59b --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.swt/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + org.eclipse.andmore.swt.feature + eclipse-feature + swt base + + + ../pom.xml + org.eclipse.andmore + swt-droid-parent + 0.5.2-SNAPSHOT + + + + + org.eclipse.tycho.extras + tycho-source-feature-plugin + + + source-feature + package + + source-feature + + + + + + org.eclipse.tycho + tycho-p2-plugin + ${tycho-version} + + + attach-p2-metadata + package + + p2-metadata + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.swt/sourceTemplateFeature/.gitignore b/andmore-swt/features/org.eclipse.andmore.swt/sourceTemplateFeature/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/features/org.eclipse.andmore.swtmenubar/.gitignore b/andmore-swt/features/org.eclipse.andmore.swtmenubar/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.swtmenubar/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/andmore-swt/features/org.eclipse.andmore.swtmenubar/.project b/andmore-swt/features/org.eclipse.andmore.swtmenubar/.project new file mode 100644 index 00000000..19e83123 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.swtmenubar/.project @@ -0,0 +1,23 @@ + + + org.eclipse.andmore.swtmenubar.feature + + + + + + org.eclipse.pde.FeatureBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.FeatureNature + + diff --git a/andmore-swt/features/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.core.resources.prefs b/andmore-swt/features/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..4824b802 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/andmore-swt/features/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.m2e.core.prefs b/andmore-swt/features/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore-swt/features/org.eclipse.andmore.swtmenubar/build.properties b/andmore-swt/features/org.eclipse.andmore.swtmenubar/build.properties new file mode 100644 index 00000000..3104d6d7 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.swtmenubar/build.properties @@ -0,0 +1,2 @@ +bin.includes = feature.xml,\ + feature.properties diff --git a/andmore-swt/features/org.eclipse.andmore.swtmenubar/feature.properties b/andmore-swt/features/org.eclipse.andmore.swtmenubar/feature.properties new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/features/org.eclipse.andmore.swtmenubar/feature.xml b/andmore-swt/features/org.eclipse.andmore.swtmenubar/feature.xml new file mode 100644 index 00000000..c20b80a2 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.swtmenubar/feature.xml @@ -0,0 +1,39 @@ + + + + + Android UI Menubar + + + + Copyright (C) 2007-2011 The Android Open Source Project + + + + %license + + + + + + + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.swtmenubar/findbugsExclusion.xml b/andmore-swt/features/org.eclipse.andmore.swtmenubar/findbugsExclusion.xml new file mode 100644 index 00000000..7443a30f --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.swtmenubar/findbugsExclusion.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.swtmenubar/pom.xml b/andmore-swt/features/org.eclipse.andmore.swtmenubar/pom.xml new file mode 100644 index 00000000..c291652c --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.swtmenubar/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + org.eclipse.andmore.swtmenubar.feature + eclipse-feature + swtmenubar + + + ../pom.xml + org.eclipse.andmore + swt-droid-parent + 0.5.2-SNAPSHOT + + + + + org.eclipse.tycho.extras + tycho-source-feature-plugin + + + source-feature + package + + source-feature + + + + + + org.eclipse.tycho + tycho-p2-plugin + ${tycho-version} + + + attach-p2-metadata + package + + p2-metadata + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.swtmenubar/sourceTemplateFeature/.gitignore b/andmore-swt/features/org.eclipse.andmore.swtmenubar/sourceTemplateFeature/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/features/org.eclipse.andmore.traceviewuilib/.gitignore b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/andmore-swt/features/org.eclipse.andmore.traceviewuilib/.project b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/.project new file mode 100644 index 00000000..3e780074 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/.project @@ -0,0 +1,23 @@ + + + org.eclipse.andmore.traceviewuilib.feature + + + + + + org.eclipse.pde.FeatureBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.FeatureNature + + diff --git a/andmore-swt/features/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.core.resources.prefs b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..4824b802 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/andmore-swt/features/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.m2e.core.prefs b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore-swt/features/org.eclipse.andmore.traceviewuilib/build.properties b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/build.properties new file mode 100644 index 00000000..3104d6d7 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/build.properties @@ -0,0 +1,2 @@ +bin.includes = feature.xml,\ + feature.properties diff --git a/andmore-swt/features/org.eclipse.andmore.traceviewuilib/feature.properties b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/feature.properties new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/features/org.eclipse.andmore.traceviewuilib/feature.xml b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/feature.xml new file mode 100644 index 00000000..fbb0e73a --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/feature.xml @@ -0,0 +1,46 @@ + + + + + Android UI Traceview library + + + + Copyright (C) 2007-2011 The Android Open Source Project + + + + %license + + + + + + + + + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.traceviewuilib/findbugsExclusion.xml b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/findbugsExclusion.xml new file mode 100644 index 00000000..7443a30f --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/findbugsExclusion.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.traceviewuilib/pom.xml b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/pom.xml new file mode 100644 index 00000000..40391cff --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + org.eclipse.andmore.traceviewuilib.feature + eclipse-feature + traceviewuilib + + + ../pom.xml + org.eclipse.andmore + swt-droid-parent + 0.5.2-SNAPSHOT + + + + + org.eclipse.tycho.extras + tycho-source-feature-plugin + + + source-feature + package + + source-feature + + + + + + org.eclipse.tycho + tycho-p2-plugin + ${tycho-version} + + + attach-p2-metadata + package + + p2-metadata + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.traceviewuilib/sourceTemplateFeature/.gitignore b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/sourceTemplateFeature/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/.gitignore b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/.project b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/.project new file mode 100644 index 00000000..e133b00c --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/.project @@ -0,0 +1,23 @@ + + + org.eclipse.andmore.uiautomatorviewer.feature + + + + + + org.eclipse.pde.FeatureBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.FeatureNature + + diff --git a/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.core.resources.prefs b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..4824b802 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.m2e.core.prefs b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/build.properties b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/build.properties new file mode 100644 index 00000000..3104d6d7 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/build.properties @@ -0,0 +1,2 @@ +bin.includes = feature.xml,\ + feature.properties diff --git a/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/feature.properties b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/feature.properties new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/feature.xml b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/feature.xml new file mode 100644 index 00000000..105107eb --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/feature.xml @@ -0,0 +1,46 @@ + + + + + Android UI Automator Viewer + + + + Copyright (C) 2007-2011 The Android Open Source Project + + + + %license + + + + + + + + + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/findbugsExclusion.xml b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/findbugsExclusion.xml new file mode 100644 index 00000000..7443a30f --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/findbugsExclusion.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/pom.xml b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/pom.xml new file mode 100644 index 00000000..8c566cf8 --- /dev/null +++ b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + org.eclipse.andmore.uiautomatorviewer.feature + eclipse-feature + uiautomatorviewer + + + ../pom.xml + org.eclipse.andmore + swt-droid-parent + 0.5.2-SNAPSHOT + + + + + org.eclipse.tycho.extras + tycho-source-feature-plugin + + + source-feature + package + + source-feature + + + + + + org.eclipse.tycho + tycho-p2-plugin + ${tycho-version} + + + attach-p2-metadata + package + + p2-metadata + + + + + + + + diff --git a/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/sourceTemplateFeature/.gitignore b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/sourceTemplateFeature/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/.classpath b/andmore-swt/org.eclipse.andmore.ddmuilib/.classpath new file mode 100644 index 00000000..bb504db2 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/.classpath @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/.gitignore b/andmore-swt/org.eclipse.andmore.ddmuilib/.gitignore new file mode 100644 index 00000000..bb9ea140 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/.gitignore @@ -0,0 +1,3 @@ +/target/ +/bin/ +/libs/ diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/.project b/andmore-swt/org.eclipse.andmore.ddmuilib/.project new file mode 100644 index 00000000..b8fbe579 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/.project @@ -0,0 +1,34 @@ + + + org.eclipse.andmore.ddmuilib + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.core.resources.prefs b/andmore-swt/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..662c7dfc --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,3 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 +encoding/test-src=UTF-8 diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.jdt.core.prefs b/andmore-swt/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..529ef073 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,13 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.m2e.core.prefs b/andmore-swt/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/META-INF/MANIFEST.MF b/andmore-swt/org.eclipse.andmore.ddmuilib/META-INF/MANIFEST.MF new file mode 100644 index 00000000..412e670c --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/META-INF/MANIFEST.MF @@ -0,0 +1,31 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Localization: plugin +Bundle-Name: %Bundle-Name +Bundle-SymbolicName: org.eclipse.andmore.ddmuilib;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Vendor: %Bundle-Vendor +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Require-Bundle: org.eclipse.core.commands;bundle-version="3.8.1", + org.eclipse.equinox.common;bundle-version="3.8.0", + org.eclipse.jface;bundle-version="3.12.2", + org.eclipse.andmore.swt +Export-Package: com.android.ddmuilib, + com.android.ddmuilib.actions, + com.android.ddmuilib.annotation, + com.android.ddmuilib.console, + com.android.ddmuilib.explorer, + com.android.ddmuilib.handler, + com.android.ddmuilib.heap, + com.android.ddmuilib.location, + com.android.ddmuilib.log.event, + com.android.ddmuilib.logcat, + com.android.ddmuilib.net, + com.android.ddmuilib.screenrecord, + com.android.ddmuilib.vmtrace, + images +Bundle-ClassPath: ., + libs/chart_swt-1.0.13.jar, + libs/jcommon-1.0.24.jar, + libs/jfreechart-1.0.19.jar, + libs/jfreechart-swt-1.0.jar diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/NOTICE b/andmore-swt/org.eclipse.andmore.ddmuilib/NOTICE new file mode 100644 index 00000000..70c54220 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/NOTICE @@ -0,0 +1,190 @@ + + Copyright (c) 2005-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. + + 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. + + + 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 + diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/README b/andmore-swt/org.eclipse.andmore.ddmuilib/README new file mode 100644 index 00000000..fdfa66e7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/README @@ -0,0 +1,14 @@ +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 files +available at prebuild//swt. + +You also need a user library called ANDROID_JFREECHART containing the jar files +available at prebuild/common/jfreechart. diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/build.gradle b/andmore-swt/org.eclipse.andmore.ddmuilib/build.gradle new file mode 100644 index 00000000..6063a7f9 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/build.gradle @@ -0,0 +1,15 @@ +group = 'com.android.tools.ddms' +archivesBaseName = 'ddmuilib' + +dependencies { + compile project(':base:ddmlib') + compile 'jfree:jfreechart:1.0.9' + compile 'jfree:jfreechart-swt:1.0.9' + + testCompile 'junit:junit:3.8.1' +} + +sourceSets { + main.resources.srcDir 'src/main/java' + test.resources.srcDir 'src/test/java' +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/build.properties b/andmore-swt/org.eclipse.andmore.ddmuilib/build.properties new file mode 100644 index 00000000..f4f7f312 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/build.properties @@ -0,0 +1,7 @@ +source.. = src/,\ + src/main/java/ +output.. = bin/ +bin.includes = META-INF/,\ + .,\ + libs/,\ + plugin.properties diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/ddmuilib.iml b/andmore-swt/org.eclipse.andmore.ddmuilib/ddmuilib.iml new file mode 100644 index 00000000..8f694139 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/ddmuilib.iml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/plugin.properties b/andmore-swt/org.eclipse.andmore.ddmuilib/plugin.properties new file mode 100644 index 00000000..30cbe713 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/plugin.properties @@ -0,0 +1,4 @@ +#Properties file for com.android.ide.eclipse.ddmuilib +Bundle-Name = Dalvik Debug Monitor Service Library +Bundle-Vendor=Eclipse Andmore Project +category.name = Android diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/pom.xml b/andmore-swt/org.eclipse.andmore.ddmuilib/pom.xml new file mode 100644 index 00000000..3a905784 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/pom.xml @@ -0,0 +1,158 @@ + + + 4.0.0 + + org.eclipse.andmore.ddmuilib + eclipse-plugin + ddmuilib + + ../pom.xml + org.eclipse.andmore + swt-droid-parent + 0.5.2-SNAPSHOT + + + + + org.jfree + jfreechart + 1.0.19 + + + org.jfree + jfreechart-swt + 1.0 + + + org.jfree + chart_swt + 1.0.13 + + + org.jfree + jcommon + 1.0.24 + + + + + + junit + junit + 4.12 + test + + + + + ${basedir}/test-src + + + org.eclipse.tycho + tycho-maven-plugin + true + + + org.apache.maven.plugins + maven-surefire-plugin + 2.12.4 + + + test + test + + + + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + compiletests + test-compile + + testCompile + + + ${project.java.version} + ${project.java.version} + ${project.java.version} + ${project.java.version} + ${project.build.sourceEncoding} + true + false + -Xlint:none + -Xlint:none + + + + + + org.eclipse.tycho + tycho-source-plugin + ${tycho-version} + + + plugin-source + + plugin-source + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.0.2 + + + copy + initialize + + copy + + + + + org.jfree + jfreechart + 1.0.19 + jar + + + org.jfree + jfreechart-swt + 1.0 + jar + + + org.jfree + chart_swt + 1.0.13 + jar + + + org.jfree + jcommon + 1.0.24 + jar + + + ${project.basedir}/libs + false + false + true + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/AbstractBufferFindTarget.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/AbstractBufferFindTarget.java new file mode 100644 index 00000000..9b1bb624 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/AbstractBufferFindTarget.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2012 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 java.util.regex.Pattern; + +/** + * {@link AbstractBufferFindTarget} implements methods to find items inside a buffer. It takes + * care of the logic to search backwards/forwards in the buffer, wrapping around when necessary. + * The actual contents of the buffer should be provided by the classes that extend this. + */ +public abstract class AbstractBufferFindTarget implements IFindTarget { + private int mCurrentSearchIndex; + + // Single element cache of the last search regex + private Pattern mLastSearchPattern; + private String mLastSearchText; + + @Override + public boolean findAndSelect(String text, boolean isNewSearch, boolean searchForward) { + boolean found = false; + int maxIndex = getItemCount(); + + synchronized (this) { + // Find starting index for this search + if (isNewSearch) { + // for new searches, start from an appropriate place as provided by the delegate + mCurrentSearchIndex = getStartingIndex(); + } else { + // for ongoing searches (finding next match for the same term), continue from + // the current result index + mCurrentSearchIndex = getNext(mCurrentSearchIndex, searchForward, maxIndex); + } + + // Create a regex pattern based on the search term. + Pattern pattern; + if (text.equals(mLastSearchText)) { + pattern = mLastSearchPattern; + } else { + pattern = Pattern.compile(text, Pattern.CASE_INSENSITIVE); + mLastSearchPattern = pattern; + mLastSearchText = text; + } + + // Iterate through the list of items. The search ends if we have gone through + // all items once. + int index = mCurrentSearchIndex; + do { + String msgText = getItem(mCurrentSearchIndex); + if (msgText != null && pattern.matcher(msgText).find()) { + found = true; + break; + } + + mCurrentSearchIndex = getNext(mCurrentSearchIndex, searchForward, maxIndex); + } while (index != mCurrentSearchIndex); // loop through entire contents once + } + + if (found) { + selectAndReveal(mCurrentSearchIndex); + } + + return found; + } + + /** Indicate that the log buffer has scrolled by certain number of elements */ + public void scrollBy(int delta) { + synchronized (this) { + if (mCurrentSearchIndex > 0) { + mCurrentSearchIndex = Math.max(0, mCurrentSearchIndex - delta); + } + } + } + + private int getNext(int index, boolean searchForward, int max) { + // increment or decrement index + index = searchForward ? index + 1 : index - 1; + + // take care of underflow + if (index == -1) { + index = max - 1; + } + + // ..and overflow + if (index == max) { + index = 0; + } + + return index; + } + + /** Obtain the number of items in the buffer */ + public abstract int getItemCount(); + + /** Obtain the item at given index */ + public abstract String getItem(int index); + + /** Select and reveal the item at given index */ + public abstract void selectAndReveal(int index); + + /** Obtain the index from which search should begin */ + public abstract int getStartingIndex(); +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/Addr2Line.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/Addr2Line.java new file mode 100644 index 00000000..39accfa9 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/Addr2Line.java @@ -0,0 +1,373 @@ +/* + * 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.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ddmlib.Log; +import com.android.ddmlib.NativeLibraryMapInfo; +import com.android.ddmlib.NativeStackCallInfo; + +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; + +/** + * 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 { + private static final String ANDROID_SYMBOLS_ENVVAR = "ANDROID_SYMBOLS"; + + private static final String LIBRARY_NOT_FOUND_MESSAGE_FORMAT = + "Unable to locate library %s on disk. Addresses mapping to this library " + + "will not be resolved. In order to fix this, set the the library search path " + + "in the UI, or set the environment variable " + ANDROID_SYMBOLS_ENVVAR + "."; + + /** + * 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 NativeLibraryMapInfo mLibrary; + + /** addr2line command to execute */ + private String mAddr2LineCmd; + + /** 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; + + private static final String DEFAULT_LIBRARY_SYMBOLS_FOLDER; + static { + String symbols = System.getenv(ANDROID_SYMBOLS_ENVVAR); + if (symbols == null) { + DEFAULT_LIBRARY_SYMBOLS_FOLDER = DdmUiPreferences.getSymbolDirectory(); + } else { + DEFAULT_LIBRARY_SYMBOLS_FOLDER = symbols; + } + } + + private static List mLibrarySearchPaths = new ArrayList(); + + /** + * Set the search path where libraries should be found. + * @param path search path to use, can be a colon separated list of paths if multiple folders + * should be searched + */ + public static void setSearchPath(String path) { + mLibrarySearchPaths.clear(); + mLibrarySearchPaths.addAll(Arrays.asList(path.split(":"))); + } + + /** + * Returns the instance of an Addr2Line process for the specified library + * and abi. + *
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. + * @param abi indicates which underlying addr2line command to use. + * @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(@NonNull final NativeLibraryMapInfo library, @Nullable String abi) { + String libName = library.getLibraryName(); + + // synchronize around the hashmap object + if (libName != null) { + synchronized (sProcessCache) { + // look for an existing process + Addr2Line process = sProcessCache.get(libName); + + // if we don't find one, we create it + if (process == null) { + process = new Addr2Line(library, abi); + + // then we start it + boolean status = process.start(); + + if (status) { + // if starting the process worked, then we add it to the + // list. + sProcessCache.put(libName, 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 and abi. The library should be present + * in the search path as provided by ANDROID_SYMBOLS, ANDROID_OUT/symbols, or in the user + * provided search path. + * + * @param library the library in which to look for address. + * @param abi indicates which underlying addr2line command to use. + */ + private Addr2Line(@NonNull final NativeLibraryMapInfo library, @Nullable String abi) { + mLibrary = library; + + // Set the addr2line command based on the abi. + if (abi == null || abi.startsWith("32")) { + Log.d("ddm-Addr2Line", "Using 32 bit addr2line command"); + mAddr2LineCmd = System.getenv("ANDROID_ADDR2LINE"); + if (mAddr2LineCmd == null) { + mAddr2LineCmd = DdmUiPreferences.getAddr2Line(); + } + } else { + Log.d("ddm-Addr2Line", "Using 64 bit addr2line command"); + mAddr2LineCmd = System.getenv("ANDROID_ADDR2LINE64"); + if (mAddr2LineCmd == null) { + mAddr2LineCmd = DdmUiPreferences.getAddr2Line64(); + } + } + } + + /** + * Search for the library in the library search path and obtain the full path to where it + * is found. + * @return fully resolved path to the library if found in search path, null otherwise + */ + private String getLibraryPath(String library) { + // first check the symbols folder + String path = DEFAULT_LIBRARY_SYMBOLS_FOLDER + library; + if (new File(path).exists()) { + return path; + } + + for (String p : mLibrarySearchPaths) { + // try appending the full path on device + String fullPath = p + "/" + library; + if (new File(fullPath).exists()) { + return fullPath; + } + + // try appending basename(library) + fullPath = p + "/" + new File(library).getName(); + if (new File(fullPath).exists()) { + return fullPath; + } + } + + return null; + } + + /** + * 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. + + // build the command line + String[] command = new String[5]; + command[0] = mAddr2LineCmd; + command[1] = "-C"; + command[2] = "-f"; + command[3] = "-e"; + + String fullPath = getLibraryPath(mLibrary.getLibraryName()); + if (fullPath == null) { + String msg = String.format(LIBRARY_NOT_FOUND_MESSAGE_FORMAT, mLibrary.getLibraryName()); + Log.e("ddm-Addr2Line", msg); + return false; + } + + command[4] = fullPath; + + 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", + mAddr2LineCmd, 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) { + long offset = addr - mLibrary.getStartAddress(); + + // 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(offset, 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(addr, mLibrary.getLibraryName(), 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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/AllocationPanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/AllocationPanel.java new file mode 100644 index 00000000..04a40405 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/AllocationPanel.java @@ -0,0 +1,657 @@ +/* + * 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.AllocationInfo.AllocationSorter; +import com.android.ddmlib.AllocationInfo.SortMode; +import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; +import com.android.ddmlib.Client; +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.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +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 org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.Text; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Locale; + +/** + * Base class for our information panels. + */ +public class AllocationPanel extends TablePanel { + + private final static String PREFS_ALLOC_COL_NUMBER = "allocPanel.Col00"; //$NON-NLS-1$ + 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_COLUMN = "allocPanel.stack.col0"; //$NON-NLS-1$ + + private Composite mAllocationBase; + private Table mAllocationTable; + private TableViewer mAllocationViewer; + + private StackTracePanel mStackTracePanel; + private Table mStackTraceTable; + private Button mEnableButton; + private Button mRequestButton; + private Button mTraceFilterCheck; + + private final AllocationSorter mSorter = new AllocationSorter(); + private TableColumn mSortColumn; + private Image mSortUpImg; + private Image mSortDownImg; + private String mFilterText = null; + + /** + * 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 class AllocationContentProvider implements IStructuredContentProvider { + @Override + public Object[] getElements(Object inputElement) { + if (inputElement instanceof Client) { + AllocationInfo[] allocs = ((Client)inputElement).getClientData().getAllocations(); + if (allocs != null) { + if (mFilterText != null && mFilterText.length() > 0) { + allocs = getFilteredAllocations(allocs, mFilterText); + } + Arrays.sort(allocs, mSorter); + return allocs; + } + } + + return new Object[0]; + } + + @Override + public void dispose() { + // pass + } + + @Override + 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 { + + @Override + public Image getColumnImage(Object element, int columnIndex) { + return null; + } + + @Override + public String getColumnText(Object element, int columnIndex) { + if (element instanceof AllocationInfo) { + AllocationInfo alloc = (AllocationInfo)element; + switch (columnIndex) { + case 0: + return Integer.toString(alloc.getAllocNumber()); + case 1: + return Integer.toString(alloc.getSize()); + case 2: + return alloc.getAllocatedClass(); + case 3: + return Short.toString(alloc.getThreadId()); + case 4: + return alloc.getFirstTraceClassName(); + case 5: + return alloc.getFirstTraceMethodName(); + } + } + + return null; + } + + @Override + public void addListener(ILabelProviderListener listener) { + // pass + } + + @Override + public void dispose() { + // pass + } + + @Override + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + @Override + public void removeListener(ILabelProviderListener listener) { + // pass + } + } + + /** + * Create our control(s). + */ + @Override + protected Control createControl(Composite parent) { + final IPreferenceStore store = DdmUiPreferences.getStore(); + + Display display = parent.getDisplay(); + + // get some images + mSortUpImg = ImageLoader.getDdmUiLibLoader().loadImage("sort_up.png", display); + mSortDownImg = ImageLoader.getDdmUiLibLoader().loadImage("sort_down.png", display); + + // 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(6, 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); + + GridData gridData; + + Composite spacer = new Composite(topParent, SWT.NONE); + spacer.setLayoutData(gridData = new GridData(GridData.FILL_HORIZONTAL)); + + new Label(topParent, SWT.NONE).setText("Filter:"); + + final Text filterText = new Text(topParent, SWT.BORDER); + filterText.setLayoutData(gridData = new GridData(GridData.FILL_HORIZONTAL)); + gridData.widthHint = 200; + + filterText.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent arg0) { + mFilterText = filterText.getText().trim(); + mAllocationViewer.refresh(); + } + }); + + mTraceFilterCheck = new Button(topParent, SWT.CHECK); + mTraceFilterCheck.setText("Inc. trace"); + mTraceFilterCheck.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + mAllocationViewer.refresh(); + } + }); + + mAllocationTable = new Table(topParent, SWT.MULTI | SWT.FULL_SELECTION); + mAllocationTable.setLayoutData(gridData = new GridData(GridData.FILL_BOTH)); + gridData.horizontalSpan = 6; + mAllocationTable.setHeaderVisible(true); + mAllocationTable.setLinesVisible(true); + + final TableColumn numberCol = TableHelper.createTableColumn( + mAllocationTable, + "Alloc Order", + SWT.RIGHT, + "Alloc Order", //$NON-NLS-1$ + PREFS_ALLOC_COL_NUMBER, store); + numberCol.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + setSortColumn(numberCol, SortMode.NUMBER); + } + }); + + final TableColumn sizeCol = TableHelper.createTableColumn( + mAllocationTable, + "Allocation Size", + SWT.RIGHT, + "888", //$NON-NLS-1$ + PREFS_ALLOC_COL_SIZE, store); + sizeCol.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + setSortColumn(sizeCol, SortMode.SIZE); + } + }); + + final TableColumn classCol = TableHelper.createTableColumn( + mAllocationTable, + "Allocated Class", + SWT.LEFT, + "Allocated Class", //$NON-NLS-1$ + PREFS_ALLOC_COL_CLASS, store); + classCol.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + setSortColumn(classCol, SortMode.CLASS); + } + }); + + final TableColumn threadCol = TableHelper.createTableColumn( + mAllocationTable, + "Thread Id", + SWT.LEFT, + "999", //$NON-NLS-1$ + PREFS_ALLOC_COL_THREAD, store); + threadCol.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + setSortColumn(threadCol, SortMode.THREAD); + } + }); + + final TableColumn inClassCol = TableHelper.createTableColumn( + mAllocationTable, + "Allocated in", + SWT.LEFT, + "utime", //$NON-NLS-1$ + PREFS_ALLOC_COL_TRACE_CLASS, store); + inClassCol.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + setSortColumn(inClassCol, SortMode.IN_CLASS); + } + }); + + final TableColumn inMethodCol = TableHelper.createTableColumn( + mAllocationTable, + "Allocated in", + SWT.LEFT, + "utime", //$NON-NLS-1$ + PREFS_ALLOC_COL_TRACE_METHOD, store); + inMethodCol.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + setSortColumn(inMethodCol, SortMode.IN_METHOD); + } + }); + + // init the default sort colum + switch (mSorter.getSortMode()) { + case SIZE: + mSortColumn = sizeCol; + break; + case CLASS: + mSortColumn = classCol; + break; + case THREAD: + mSortColumn = threadCol; + break; + case IN_CLASS: + mSortColumn = inClassCol; + break; + case IN_METHOD: + mSortColumn = inMethodCol; + break; + case ALLOCATION_SITE : + break; + case NUMBER : + break; + default : + break; + } + + mSortColumn.setImage(mSorter.isDescending() ? mSortDownImg : mSortUpImg); + + mAllocationViewer = new TableViewer(mAllocationTable); + mAllocationViewer.setContentProvider(new AllocationContentProvider()); + mAllocationViewer.setLabelProvider(new AllocationLabelProvider()); + + mAllocationViewer.addSelectionChangedListener(new ISelectionChangedListener() { + @Override + 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_COLUMN, 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() { + @Override + 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; + } + + @Override + public void dispose() { + mSortUpImg.dispose(); + mSortDownImg.dispose(); + super.dispose(); + } + + /** + * 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) + */ + @Override + public void clientChanged(final Client client, int changeMask) { + if (client == getCurrentClient()) { + if ((changeMask & Client.CHANGE_HEAP_ALLOCATIONS) != 0) { + try { + mAllocationTable.getDisplay().asyncExec(new Runnable() { + @Override + 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() { + @Override + 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"); + } + } + + private void setSortColumn(final TableColumn column, SortMode sortMode) { + // set the new sort mode + mSorter.setSortMode(sortMode); + + mAllocationTable.setRedraw(false); + + // remove image from previous sort colum + if (mSortColumn != column) { + mSortColumn.setImage(null); + } + + mSortColumn = column; + if (mSorter.isDescending()) { + mSortColumn.setImage(mSortDownImg); + } else { + mSortColumn.setImage(mSortUpImg); + } + + mAllocationTable.setRedraw(true); + mAllocationViewer.refresh(); + } + + private AllocationInfo[] getFilteredAllocations(AllocationInfo[] allocations, + String filterText) { + ArrayList results = new ArrayList(); + // Using default locale here such that the locale-specific c + Locale locale = Locale.getDefault(); + filterText = filterText.toLowerCase(locale); + boolean fullTrace = mTraceFilterCheck.getSelection(); + + for (AllocationInfo info : allocations) { + if (info.filter(filterText, fullTrace, locale)) { + results.add(info); + } + } + + return results.toArray(new AllocationInfo[results.size()]); + } + +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/BackgroundThread.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/BackgroundThread.java new file mode 100644 index 00000000..5518a002 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/BaseHeapPanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/BaseHeapPanel.java new file mode 100644 index 00000000..c8a60f8d --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ClientDisplayPanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ClientDisplayPanel.java new file mode 100644 index 00000000..39ea342d --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/DdmUiPreferences.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/DdmUiPreferences.java new file mode 100644 index 00000000..fe519606 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/DdmUiPreferences.java @@ -0,0 +1,88 @@ +/* + * 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 sAddr2LineLocation64 =""; //$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; + } + + public static String getSymbolDirectory() { + return sSymbolLocation; + } + + public static void setSymbolsLocation(String location) { + sSymbolLocation = location; + } + + public static String getAddr2Line() { + return sAddr2LineLocation; + } + + public static void setAddr2LineLocation(String location) { + sAddr2LineLocation = location; + } + + public static String getAddr2Line64() { + return sAddr2LineLocation64; + } + + public static void setAddr2LineLocation64(String location) { + sAddr2LineLocation64 = location; + } + + public static String getTraceview() { + return sTraceviewLocation; + } + + public static void setTraceviewLocation(String location) { + sTraceviewLocation = location; + } + + +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/DevicePanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/DevicePanel.java new file mode 100644 index 00000000..992e0c03 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/DevicePanel.java @@ -0,0 +1,829 @@ +/* + * 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.annotations.NonNull; +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; +import com.android.ddmlib.AndroidDebugBridge.IDebugBridgeChangeListener; +import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; +import com.android.ddmlib.Client; +import com.android.ddmlib.ClientData; +import com.android.ddmlib.ClientData.DebuggerStatus; +import com.android.ddmlib.DdmPreferences; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.IDevice.DeviceState; +import com.android.ddmuilib.vmtrace.VmTraceOptionsDialog; +import com.google.common.base.Throwables; + +import org.eclipse.jface.dialogs.MessageDialog; +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.jface.window.Window; +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.io.IOException; +import java.util.ArrayList; +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +/** + * 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 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 { + @Override + public Object[] getChildren(Object parentElement) { + if (parentElement instanceof IDevice) { + return ((IDevice)parentElement).getClients(); + } + return new Object[0]; + } + + @Override + public Object getParent(Object element) { + if (element instanceof Client) { + return ((Client)element).getDevice(); + } + return null; + } + + @Override + public boolean hasChildren(Object element) { + if (element instanceof IDevice) { + return ((IDevice)element).hasClients(); + } + + // Clients never have children. + return false; + } + + @Override + public Object[] getElements(Object inputElement) { + if (inputElement instanceof AndroidDebugBridge) { + return ((AndroidDebugBridge)inputElement).getDevices(); + } + return new Object[0]; + } + + @Override + public void dispose() { + // pass + } + + @Override + 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 { + @Override + 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; + } + + @Override + public String getColumnText(Object element, int columnIndex) { + if (element instanceof IDevice) { + IDevice device = (IDevice)element; + switch (columnIndex) { + case DEVICE_COL_SERIAL: + return device.getName(); + 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) { + if (cd.isValidUserId() && cd.getUserId() != 0) { + return String.format(Locale.US, "%s (%d)", name, cd.getUserId()); + } else { + 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; + } + + @Override + public void addListener(ILabelProviderListener listener) { + // pass + } + + @Override + public void dispose() { + // pass + } + + @Override + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + @Override + 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 advancedPortSupport if true the device panel will add support for selected client port + * and display the ports in the ui. + */ + public DevicePanel(boolean advancedPortSupport) { + 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()); + + 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) { + return; + } + + try { + toggleMethodProfiling(mCurrentClient); + } catch (IOException e) { + MessageDialog.openError(mTree.getShell(), "Method Profiling", + "Unexpected I/O error while starting/stopping profiling: " + + Throwables.getRootCause(e).getMessage()); + } + } + + private void toggleMethodProfiling(@NonNull Client client) throws IOException { + ClientData cd = mCurrentClient.getClientData(); + if (cd.getMethodProfilingStatus() == ClientData.MethodProfilingStatus.TRACER_ON) { + mCurrentClient.stopMethodTracer(); + } else if (cd.getMethodProfilingStatus() == ClientData.MethodProfilingStatus.SAMPLER_ON) { + mCurrentClient.stopSamplingProfiler(); + } else { + boolean supportsSampling = cd.hasFeature(ClientData.FEATURE_SAMPLING_PROFILER); + + // default to tracing + boolean shouldUseTracing = true; + int samplingIntervalMicros = 1; + + // if client supports sampling, then ask the user to choose the method + if (supportsSampling) { + VmTraceOptionsDialog dialog = new VmTraceOptionsDialog(mTree.getShell()); + if (dialog.open() == Window.CANCEL) { + return; + } + shouldUseTracing = dialog.shouldUseTracing(); + if (!shouldUseTracing) { + samplingIntervalMicros = dialog.getSamplingIntervalMicros(); + } + } + + if (shouldUseTracing) { + mCurrentClient.startMethodTracer(); + } else { + mCurrentClient.startSamplingProfiler(samplingIntervalMicros, TimeUnit.MICROSECONDS); + } + } + } + + 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. + */ + @Override + public void bridgeChanged(final AndroidDebugBridge bridge) { + if (mTree.isDisposed() == false) { + exec(new Runnable() { + @Override + 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) + */ + @Override + public void deviceConnected(IDevice device) { + exec(new Runnable() { + @Override + 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) + */ + @Override + 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,int) + */ + @Override + 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() { + @Override + 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) + */ + @Override + public void clientChanged(final Client client, final int changeMask) { + exec(new Runnable() { + @Override + 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) { + ImageLoader loader = ImageLoader.getDdmUiLibLoader(); + + if (mDeviceImage == null) { + mDeviceImage = loader.loadImage(display, "device.png", //$NON-NLS-1$ + ICON_WIDTH, ICON_WIDTH, + display.getSystemColor(SWT.COLOR_RED)); + } + if (mEmulatorImage == null) { + mEmulatorImage = loader.loadImage(display, + "emulator.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$ + display.getSystemColor(SWT.COLOR_BLUE)); + } + if (mThreadImage == null) { + mThreadImage = loader.loadImage(display, ICON_THREAD, + ICON_WIDTH, ICON_WIDTH, + display.getSystemColor(SWT.COLOR_YELLOW)); + } + if (mHeapImage == null) { + mHeapImage = loader.loadImage(display, ICON_HEAP, + ICON_WIDTH, ICON_WIDTH, + display.getSystemColor(SWT.COLOR_BLUE)); + } + if (mWaitingImage == null) { + mWaitingImage = loader.loadImage(display, + "debug-wait.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$ + display.getSystemColor(SWT.COLOR_RED)); + } + if (mDebuggerImage == null) { + mDebuggerImage = loader.loadImage(display, + "debug-attach.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$ + display.getSystemColor(SWT.COLOR_GREEN)); + } + if (mDebugErrorImage == null) { + mDebugErrorImage = loader.loadImage(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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/EmulatorControlPanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/EmulatorControlPanel.java new file mode 100644 index 00000000..ce6f084d --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/EmulatorControlPanel.java @@ -0,0 +1,1463 @@ +/* + * 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.EmulatorConsole; +import com.android.ddmlib.EmulatorConsole.GsmMode; +import com.android.ddmlib.EmulatorConsole.GsmStatus; +import com.android.ddmlib.EmulatorConsole.NetworkStatus; +import com.android.ddmlib.IDevice; +import com.android.ddmuilib.location.CoordinateControls; +import com.android.ddmuilib.location.GpxParser; +import com.android.ddmuilib.location.GpxParser.Track; +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 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 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() { + } + + /** + * 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() { + @Override + 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-2$ + + // replace remaining stand alone \n + message = message.replaceAll("\n", "\\\\n"); //$NON-NLS-1$ //$NON-NLS-2$ + + // replace remaining stand alone \r + message = message.replaceAll("\r", "\\\\n"); //$NON-NLS-1$ //$NON-NLS-2$ + + 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); + + ImageLoader loader = ImageLoader.getDdmUiLibLoader(); + mPlayImage = loader.loadImage("play.png", mParent.getDisplay()); //$NON-NLS-1$ + mPauseImage = loader.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() { + @Override + 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() { + @Override + 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; + + ImageLoader loader = ImageLoader.getDdmUiLibLoader(); + mGpxBackwardButton = new Button(mGpxPlayControls, SWT.TOGGLE | SWT.FLAT); + mGpxBackwardButton.setImage(loader.loadImage("backward.png", mParent.getDisplay())); //$NON-NLS-1$ + mGpxBackwardButton.setSelection(false); + mGpxBackwardButton.addSelectionListener(mDirectionButtonAdapter); + mGpxForwardButton = new Button(mGpxPlayControls, SWT.TOGGLE | SWT.FLAT); + mGpxForwardButton.setImage(loader.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() { + @Override + 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; + + ImageLoader loader = ImageLoader.getDdmUiLibLoader(); + mKmlBackwardButton = new Button(mKmlPlayControls, SWT.TOGGLE | SWT.FLAT); + mKmlBackwardButton.setImage(loader.loadImage("backward.png", mParent.getDisplay())); //$NON-NLS-1$ + mKmlBackwardButton.setSelection(false); + mKmlBackwardButton.addSelectionListener(mDirectionButtonAdapter); + mKmlForwardButton = new Button(mKmlPlayControls, SWT.TOGGLE | SWT.FLAT); + mKmlForwardButton.setImage(loader.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() { + @Override + 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() { + @Override + 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() { + @Override + 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() { + @Override + 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() { + @Override + 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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/FindDialog.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/FindDialog.java new file mode 100644 index 00000000..3f2d7c31 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/FindDialog.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2012 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.dialogs.Dialog; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +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.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +/** + * {@link FindDialog} provides a text box where users can enter text that should be + * searched for in the target editor/view. The buttons "Find Previous" and "Find Next" + * allow users to search forwards/backwards. This dialog simply provides a front end for the user + * and the actual task of searching is delegated to the {@link IFindTarget}. + */ +public class FindDialog extends Dialog { + private Label mStatusLabel; + private Button mFindNext; + private Button mFindPrevious; + private final IFindTarget mTarget; + private Text mSearchText; + private String mPreviousSearchText; + private final int mDefaultButtonId; + + /** Id of the "Find Next" button */ + public static final int FIND_NEXT_ID = IDialogConstants.CLIENT_ID; + + /** Id of the "Find Previous button */ + public static final int FIND_PREVIOUS_ID = IDialogConstants.CLIENT_ID + 1; + + public FindDialog(Shell shell, IFindTarget target) { + this(shell, target, FIND_PREVIOUS_ID); + } + + /** + * Construct a find dialog. + * @param shell shell to use + * @param target delegate to be invoked on user action + * @param defaultButtonId one of {@code #FIND_NEXT_ID} or {@code #FIND_PREVIOUS_ID}. + */ + public FindDialog(Shell shell, IFindTarget target, int defaultButtonId) { + super(shell); + + mTarget = target; + mDefaultButtonId = defaultButtonId; + + setShellStyle((getShellStyle() & ~SWT.APPLICATION_MODAL) | SWT.MODELESS); + setBlockOnOpen(true); + } + + @Override + protected Control createDialogArea(Composite parent) { + Composite panel = new Composite(parent, SWT.NONE); + panel.setLayout(new GridLayout(2, false)); + panel.setLayoutData(new GridData(GridData.FILL_BOTH)); + + Label lblMessage = new Label(panel, SWT.NONE); + lblMessage.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1)); + lblMessage.setText("Find:"); + + mSearchText = new Text(panel, SWT.BORDER); + mSearchText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + mSearchText.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent e) { + boolean hasText = !mSearchText.getText().trim().isEmpty(); + mFindNext.setEnabled(hasText); + mFindPrevious.setEnabled(hasText); + } + }); + + mStatusLabel = new Label(panel, SWT.NONE); + mStatusLabel.setForeground(getShell().getDisplay().getSystemColor(SWT.COLOR_DARK_RED)); + GridData gd = new GridData(); + gd.horizontalSpan = 2; + gd.grabExcessHorizontalSpace = true; + mStatusLabel.setLayoutData(gd); + + return panel; + } + + @Override + protected void createButtonsForButtonBar(Composite parent) { + createButton(parent, IDialogConstants.CLOSE_ID, IDialogConstants.CLOSE_LABEL, false); + + mFindNext = createButton(parent, FIND_NEXT_ID, "Find Next", + mDefaultButtonId == FIND_NEXT_ID); + mFindPrevious = createButton(parent, FIND_PREVIOUS_ID, "Find Previous", + mDefaultButtonId != FIND_NEXT_ID); + mFindNext.setEnabled(false); + mFindPrevious.setEnabled(false); + } + + @Override + protected void buttonPressed(int buttonId) { + if (buttonId == IDialogConstants.CLOSE_ID) { + close(); + return; + } + + if (buttonId == FIND_PREVIOUS_ID || buttonId == FIND_NEXT_ID) { + if (mTarget != null) { + String searchText = mSearchText.getText(); + boolean newSearch = !searchText.equals(mPreviousSearchText); + mPreviousSearchText = searchText; + boolean searchForward = buttonId == FIND_NEXT_ID; + + boolean hasMatches = mTarget.findAndSelect(searchText, newSearch, searchForward); + if (!hasMatches) { + mStatusLabel.setText("String not found"); + mStatusLabel.pack(); + } else { + mStatusLabel.setText(""); + } + } + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/HeapPanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/HeapPanel.java new file mode 100644 index 00000000..8900570c --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/HeapPanel.java @@ -0,0 +1,1307 @@ +/* + * 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.IClientChangeListener; +import com.android.ddmlib.Client; +import com.android.ddmlib.ClientData; +import com.android.ddmlib.HeapSegment.HeapSegmentElement; +import com.android.ddmlib.Log; + +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.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) + */ + @Override + 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() { + @Override + 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(ImageLoader.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(ImageLoader.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"); + + // make sure there is always one empty item so that one table row is always displayed. + TableItem item = new TableItem(tab, SWT.NONE); + item.setText(""); + + 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() { + @Override + 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(); + + int numRows = 0; + if (cd != null) { + synchronized (cd) { + Iterator iter = cd.getVmHeapIds(); + + while (iter.hasNext()) { + numRows++; + Integer id = iter.next(); + ClientData.HeapInfo info = cd.getVmHeapInfo(id); + if (info == null) { + continue; + } + + TableItem item = new TableItem(mHeapSummary, SWT.NONE); + item.setText(0, id.toString()); + + item.setText(1, prettyByteCount(info.sizeInBytes)); + item.setText(2, prettyByteCount(info.bytesAllocated)); + item.setText(3, prettyByteCount(info.sizeInBytes - info.bytesAllocated)); + item.setText(4, fractionalPercent(info.bytesAllocated, info.sizeInBytes)); + item.setText(5, addCommasToNumber(info.objectsAllocated)); + } + } + } + + if (numRows == 0) { + // make sure there is always one empty item so that one table row is always displayed. + TableItem item = new TableItem(mHeapSummary, SWT.NONE); + item.setText(""); + } + + 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); + } + + @Override + 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(((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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/IFindTarget.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/IFindTarget.java new file mode 100644 index 00000000..a5dfef44 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/IFindTarget.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2012 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; + +public interface IFindTarget { + boolean findAndSelect(String text, boolean isNewSearch, boolean searchForward); +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ITableFocusListener.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ITableFocusListener.java new file mode 100644 index 00000000..9ad4c0e3 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ImageLoader.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ImageLoader.java new file mode 100644 index 00000000..a6e31b40 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ImageLoader.java @@ -0,0 +1,206 @@ +/* + * 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.jface.resource.ImageDescriptor; +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; + +import java.io.InputStream; +import java.net.URL; +import java.util.HashMap; + +/** + * Class to load images stored in a jar file. + * All images are loaded from /images/filename + * + * Because Java requires to know the jar file in which to load the image from, a class is required + * when getting the instance. Instances are cached and associated to the class passed to + * {@link #getLoader(Class)}. + * + * {@link #getDdmUiLibLoader()} use {@link ImageLoader#getClass()} as the class. This is to be used + * to load images from ddmuilib. + * + * Loaded images are stored so that 2 calls with the same filename will return the same object. + * This also means that {@link Image} object returned by the loader should never be disposed. + * + */ +public class ImageLoader { + + private static final String PATH = "/images/"; //$NON-NLS-1$ + + private final HashMap mLoadedImages = new HashMap(); + private static final HashMap, ImageLoader> mInstances = + new HashMap, ImageLoader>(); + private final Class mClass; + + /** + * Private constructor, creating an instance associated with a class. + * The class is used to identify which jar file the images are loaded from. + */ + private ImageLoader(Class theClass) { + if (theClass == null) { + theClass = ImageLoader.class; + } + mClass = theClass; + } + + /** + * Returns the {@link ImageLoader} instance to load images from ddmuilib.jar + */ + public static ImageLoader getDdmUiLibLoader() { + return getLoader(null); + } + + /** + * Returns an {@link ImageLoader} to load images based on a given class. + * + * The loader will load images from the jar from which the class was loaded. using + * {@link Class#getResource(String)} and {@link Class#getResourceAsStream(String)}. + * + * Since all images are loaded using the path /images/filename, any class from the + * jar will work. However since the loader is cached and reused when the query provides the same + * class instance, and since the loader will also cache the loaded images, it is recommended + * to always use the same class for a given Jar file. + * + */ + public static ImageLoader getLoader(Class theClass) { + ImageLoader instance = mInstances.get(theClass); + if (instance == null) { + instance = new ImageLoader(theClass); + mInstances.put(theClass, instance); + } + + return instance; + } + + /** + * Disposes all images for all instances. + * This should only be called when the program exits. + */ + public static void dispose() { + for (ImageLoader loader : mInstances.values()) { + loader.doDispose(); + } + } + + private synchronized void doDispose() { + for (Image image : mLoadedImages.values()) { + image.dispose(); + } + + mLoadedImages.clear(); + } + + /** + * Returns an {@link ImageDescriptor} for a given filename. + * + * This searches for an image located at /images/filename. + * + * @param filename the filename of the image to load. + */ + public ImageDescriptor loadDescriptor(String filename) { + URL url = mClass.getResource(PATH + filename); + // TODO cache in a map + return ImageDescriptor.createFromURL(url); + } + + /** + * Returns an {@link Image} for a given filename. + * + * This searches for an image located at /images/filename. + * + * @param filename the filename of the image to load. + * @param display the Display object + */ + public synchronized Image loadImage(String filename, Display display) { + Image img = mLoadedImages.get(filename); + if (img == null) { + String tmp = PATH + filename; + InputStream imageStream = mClass.getResourceAsStream(tmp); + + if (imageStream != null) { + img = new Image(display, imageStream); + mLoadedImages.put(filename, img); + } + + if (img == null) { + throw new RuntimeException("Failed to load " + tmp); + } + } + + return img; + } + + /** + * 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 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 Image loadImage(Display display, String fileName, int width, int height, + Color phColor) { + + Image img = 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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/InfoPanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/InfoPanel.java new file mode 100644 index 00000000..49520947 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/InfoPanel.java @@ -0,0 +1,211 @@ +/* + * 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.IClientChangeListener; +import com.android.ddmlib.Client; +import com.android.ddmlib.ClientData; + +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:", + "ABI Flavor:", + "JVM Flags:", + }; + 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; + private static final int ENT_ABI_FLAVOR = 6; + private static final int ENT_JVM_FLAGS = 7; + + /** + * 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) + */ + @Override + 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() { + @Override + 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"); + } + + item = mTable.getItem(ENT_ABI_FLAVOR); + String abi = cd.getAbi(); + item.setText(1, abi == null ? "" : abi); + + item = mTable.getItem(ENT_JVM_FLAGS); + String jvmFlags = cd.getJvmFlags(); + item.setText(1, jvmFlags == null ? "" : jvmFlags); + } + + mCol2.pack(); + + //Log.i("ddms", "InfoPanel: changed " + client); + } + + @Override + protected void setTableFocusListener() { + addTableToFocusListener(mTable); + } +} + diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/NativeHeapPanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/NativeHeapPanel.java new file mode 100644 index 00000000..684399b5 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/NativeHeapPanel.java @@ -0,0 +1,1648 @@ +/* + * 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.IClientChangeListener; +import com.android.ddmlib.Client; +import com.android.ddmlib.ClientData; +import com.android.ddmlib.HeapSegment.HeapSegmentElement; +import com.android.ddmlib.Log; +import com.android.ddmlib.NativeAllocationInfo; +import com.android.ddmlib.NativeLibraryMapInfo; +import com.android.ddmlib.NativeStackCallInfo; +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; +import java.util.List; + +/** + * 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 List list = info.getStackCallAddresses(); + final int size = list.size(); + + ArrayList resolvedStackCall = + new ArrayList(); + + for (int i = 0; i < size; i++) { + long addr = list.get(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, mClientData.getAbi()); + if (process != null) { + // remove the base of the library address + NativeStackCallInfo info = process.getAddress(addr); + if (info != null) { + return info; + } + } + } + + return new NativeStackCallInfo(addr, + library != null ? library.getLibraryName() : null, + Long.toHexString(addr), + ""); + } + + private NativeLibraryMapInfo getLibraryFor(long addr) { + for (NativeLibraryMapInfo info : mClientData.getMappedNativeLibraries()) { + 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() { + @Override + 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() { + @Override + 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() { + @Override + 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) + */ + @Override + 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() { + @Override + 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() { + if (mStackCallThread != null) { + mStackCallThread.quit(); + } + + 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 + List addresses = mi.getStackCallAddresses(); + List resolvedStackCall = mi.getResolvedStackCall(); + + if (resolvedStackCall == null) { + return; + } + + for (int i = 0 ; i < resolvedStackCall.size(); i++) { + if (addresses.get(i) == null || addresses.get(i).longValue() == 0) { + continue; + } + + long addr = addresses.get(i).longValue(); + NativeStackCallInfo source = resolvedStackCall.get(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() { + @Override + 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(); + if (index >= 0 && index < mAllocationTable.getItemCount()) { + 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 + if (index1 != -1 && index2 != -1) { + 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() { + @Override + 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() { + @Override + 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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/Panel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/Panel.java new file mode 100644 index 00000000..dadde285 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ScreenShotDialog.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ScreenShotDialog.java new file mode 100644 index 00000000..fa49cc0e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ScreenShotDialog.java @@ -0,0 +1,352 @@ +/* + * 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.AdbCommandRejectedException; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.Log; +import com.android.ddmlib.RawImage; +import com.android.ddmlib.TimeoutException; + +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.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.File; +import java.io.IOException; +import java.util.Calendar; + + +/** + * 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; + + /** Number of 90 degree rotations applied to the current image */ + private int mRotateCount = 0; + + /** + * 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); + // RawImage only allows us to rotate the image 90 degrees at the time, + // so to preserve the current rotation we must call getRotated() + // the same number of times the user has done it manually. + // TODO: improve the RawImage class. + if (mRawImage != null) { + for (int i = 0; i < mRotateCount; i++) { + mRawImage = mRawImage.getRotated(); + } + updateImageDisplay(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) { + mRotateCount = (mRotateCount + 1) % 4; + 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(ImageLoader.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 = ImageLoader.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; + } catch (TimeoutException e) { + Log.w("ddms", "Unable to get frame buffer: timeout "); + return null; + } catch (AdbCommandRejectedException e) { + Log.w("ddms", "Unable to get frame buffer: " + e.getMessage()); + return null; + } + } + + /* + * Prompt the user to save the image to disk. + */ + private void saveImage(Shell shell) { + FileDialog dlg = new FileDialog(shell, SWT.SAVE); + + Calendar now = Calendar.getInstance(); + String fileName = String.format("device-%tF-%tH%tM%tS.png", + now, now, now, now); + + dlg.setText("Save image..."); + dlg.setFileName(fileName); + + String lastDir = DdmUiPreferences.getStore().getString("lastImageSaveDir"); + if (lastDir.length() == 0) { + lastDir = DdmUiPreferences.getStore().getString("imageSaveDir"); + } + dlg.setFilterPath(lastDir); + dlg.setFilterNames(new String[] { + "PNG Files (*.png)" + }); + dlg.setFilterExtensions(new String[] { + "*.png" //$NON-NLS-1$ + }); + + fileName = dlg.open(); + if (fileName != null) { + // FileDialog.getFilterPath() does NOT always return the current + // directory of the FileDialog; on the Mac it sometimes just returns + // the value the dialog was initialized with. It does however return + // the full path as its return value, so just pick the path from + // there. + if (!fileName.endsWith(".png")) { + fileName = fileName + ".png"; + } + + String saveDir = new File(fileName).getParent(); + if (saveDir != null) { + DdmUiPreferences.getStore().setValue("lastImageSaveDir", saveDir); + } + + Log.d("ddms", "Saving image to " + fileName); + ImageData imageData = mImageLabel.getImage().getImageData(); + + try { + org.eclipse.swt.graphics.ImageLoader loader = + new org.eclipse.swt.graphics.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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/SelectionDependentPanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/SelectionDependentPanel.java new file mode 100644 index 00000000..2c7b17f6 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/StackTracePanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/StackTracePanel.java new file mode 100644 index 00000000..6b96e42b --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/StackTracePanel.java @@ -0,0 +1,223 @@ +/* + * 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, 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 { + @Override + 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]; + } + + @Override + public void dispose() { + // pass + } + + @Override + 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 { + + @Override + public Image getColumnImage(Object element, int columnIndex) { + return null; + } + + @Override + public String getColumnText(Object element, int columnIndex) { + if (element instanceof StackTraceElement && columnIndex == 0) { + StackTraceElement traceElement = (StackTraceElement) element; + return " at " + traceElement.toString(); + } + return null; + } + + @Override + public void addListener(ILabelProviderListener listener) { + // pass + } + + @Override + public void dispose() { + // pass + } + + @Override + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + @Override + 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_column + * @param store + */ + public Table createPanel(Composite parent, String prefs_stack_column, + IPreferenceStore store) { + + mStackTraceTable = new Table(parent, SWT.MULTI | SWT.FULL_SELECTION); + mStackTraceTable.setHeaderVisible(false); + mStackTraceTable.setLinesVisible(false); + + TableHelper.createTableColumn( + mStackTraceTable, + "Info", + SWT.LEFT, + "SomeLongClassName.method(android/somepackage/someotherpackage/somefile.java:99999)", //$NON-NLS-1$ + prefs_stack_column, store); + + mStackTraceViewer = new TableViewer(mStackTraceTable); + mStackTraceViewer.setContentProvider(new StackTraceContentProvider()); + mStackTraceViewer.setLabelProvider(new StackTraceLabelProvider()); + + mStackTraceViewer.addDoubleClickListener(new IDoubleClickListener() { + @Override + 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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/SyncProgressHelper.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/SyncProgressHelper.java new file mode 100644 index 00000000..ce3f2045 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/SyncProgressHelper.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2010 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.SyncException; +import com.android.ddmlib.SyncService; +import com.android.ddmlib.SyncService.ISyncProgressMonitor; +import com.android.ddmlib.TimeoutException; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jface.dialogs.ProgressMonitorDialog; +import org.eclipse.jface.operation.IRunnableWithProgress; +import org.eclipse.swt.widgets.Shell; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; + +/** + * Helper class to run a Sync in a {@link ProgressMonitorDialog}. + */ +public class SyncProgressHelper { + + /** + * a runnable class run with an {@link ISyncProgressMonitor}. + */ + public interface SyncRunnable { + /** Runs the sync action */ + void run(ISyncProgressMonitor monitor) throws SyncException, IOException, TimeoutException; + /** close the {@link SyncService} */ + void close(); + } + + /** + * Runs a {@link SyncRunnable} in a {@link ProgressMonitorDialog}. + * @param runnable The {@link SyncRunnable} to run. + * @param progressMessage the message to display in the progress dialog + * @param parentShell the parent shell for the progress dialog. + * + * @throws InvocationTargetException + * @throws InterruptedException + * @throws SyncException if an error happens during the push of the package on the device. + * @throws IOException + * @throws TimeoutException + */ + public static void run(final SyncRunnable runnable, final String progressMessage, + final Shell parentShell) + throws InvocationTargetException, InterruptedException, SyncException, IOException, + TimeoutException { + + final Exception[] result = new Exception[1]; + new ProgressMonitorDialog(parentShell).run(true, true, new IRunnableWithProgress() { + @Override + public void run(IProgressMonitor monitor) { + try { + runnable.run(new SyncProgressMonitor(monitor, progressMessage)); + } catch (Exception e) { + result[0] = e; + } finally { + runnable.close(); + } + } + }); + + if (result[0] instanceof SyncException) { + SyncException se = (SyncException)result[0]; + if (se.wasCanceled()) { + // no need to throw this + return; + } + throw se; + } + + // just do some casting so that the method declaration matches what's thrown. + if (result[0] instanceof TimeoutException) { + throw (TimeoutException)result[0]; + } + + if (result[0] instanceof IOException) { + throw (IOException)result[0]; + } + + if (result[0] instanceof RuntimeException) { + throw (RuntimeException)result[0]; + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/SyncProgressMonitor.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/SyncProgressMonitor.java new file mode 100644 index 00000000..4fd13c40 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/SyncProgressMonitor.java @@ -0,0 +1,60 @@ +/* + * 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; + } + + @Override + public void start(int totalWork) { + mMonitor.beginTask(mName, totalWork); + } + + @Override + public void stop() { + mMonitor.done(); + } + + @Override + public void advance(int work) { + mMonitor.worked(work); + } + + @Override + public boolean isCanceled() { + return mMonitor.isCanceled(); + } + + @Override + public void startSubTask(String name) { + mMonitor.subTask(name); + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/SysinfoPanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/SysinfoPanel.java new file mode 100644 index 00000000..48f9af07 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/SysinfoPanel.java @@ -0,0 +1,931 @@ +/* + * 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.annotations.concurrency.GuardedBy; +import com.android.ddmlib.AdbCommandRejectedException; +import com.android.ddmlib.Client; +import com.android.ddmlib.ClientData; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.IShellOutputReceiver; +import com.android.ddmlib.Log; +import com.android.ddmlib.NullOutputReceiver; +import com.android.ddmlib.ShellCommandUnresponsiveException; +import com.android.ddmlib.TimeoutException; +import com.android.ddmuilib.SysinfoPanel.BugReportParser.GfxProfileData; +import com.google.common.base.Splitter; +import com.google.common.collect.Lists; + +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.StackLayout; +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.Display; +import org.eclipse.swt.widgets.Label; +import org.jfree.chart.ChartFactory; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.data.category.DefaultCategoryDataset; +import org.jfree.data.general.DefaultPieDataset; +import org.jfree.chart.swt.ChartComposite; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicBoolean; +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 { + + // UI components + private Label mLabel; + private Button mFetchButton; + private Combo mDisplayMode; + + private DefaultPieDataset mDataset; + private DefaultCategoryDataset mBarDataSet; + + private StackLayout mStackLayout; + private Composite mChartComposite; + private Composite mPieChartComposite; + private Composite mStackedBarComposite; + + // Selects the current display: MODE_CPU, etc. + private int mMode = 0; + private String mGfxPackageName; + + private static final Object RECEIVER_LOCK = new Object(); + @GuardedBy("RECEIVER_LOCK") + private ShellOutputReceiver mLastOutputReceiver; + + private static final int MODE_CPU = 0; + private static final int MODE_MEMINFO = 1; + private static final int MODE_GFXINFO = 2; + + // argument to dumpsys; section in the bugreport holding the data + private static final String DUMP_COMMAND[] = { + "dumpsys cpuinfo", + "cat /proc/meminfo ; procrank", + "dumpsys gfxinfo", + }; + + private static final String CAPTIONS[] = { + "CPU load", + "Memory usage", + "Frame Render Time", + }; + + /** Shell property that controls whether graphics profiling is enabled or not. */ + private static final String PROP_GFX_PROFILING = "debug.hwui.profile"; //$NON-NLS-1$ + + /** + * Generates the dataset to display. + * + * @param file The bugreport file to process. + */ + private void generateDataset(File file) { + if (file == null) { + return; + } + try { + BufferedReader br = getBugreportReader(file); + if (mMode == MODE_CPU) { + readCpuDataset(br); + } else if (mMode == MODE_MEMINFO) { + readMeminfoDataset(br); + } else if (mMode == MODE_GFXINFO) { + readGfxInfoDataset(br); + } + br.close(); + } 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() { + clearDataSet(); + + if (mMode == MODE_GFXINFO) { + boolean en = isGfxProfilingEnabled(); + if (!en) { + if (enableGfxProfiling()) { + MessageDialog.openInformation(Display.getCurrent().getActiveShell(), + "DDMS", + "Graphics profiling was enabled on the device.\n" + + "It may be necessary to relaunch your application to see profile information."); + } else { + MessageDialog.openError(Display.getCurrent().getActiveShell(), + "DDMS", + "Unexpected error enabling graphics profiling on device.\n"); + return; + } + } + } + + final String command = getDumpsysCommand(mMode); + if (command == null) { + return; + } + + Thread t = new Thread(new Runnable() { + @Override + public void run() { + try { + String header = null; + if (mMode == MODE_MEMINFO) { + // Hack to add bugreport-style section header for meminfo + header = "------ MEMORY INFO ------\n"; + } + + IShellOutputReceiver receiver = initShellOutputBuffer(header); + getCurrentDevice().executeShellCommand(command, receiver); + } catch (IOException e) { + Log.e("DDMS", e); + } catch (TimeoutException e) { + Log.e("DDMS", e); + } catch (AdbCommandRejectedException e) { + Log.e("DDMS", e); + } catch (ShellCommandUnresponsiveException e) { + Log.e("DDMS", e); + } + } + }, "Sysinfo Output Collector"); + t.start(); + } + + private boolean isGfxProfilingEnabled() { + IDevice device = getCurrentDevice(); + if (device == null) { + return false; + } + + String prop; + try { + Future future = device.getSystemProperty(PROP_GFX_PROFILING); + prop = future.get(); + return prop != null ? Boolean.valueOf(prop) : false; + } catch (Exception e) { + return false; + } + } + + private boolean enableGfxProfiling() { + IDevice device = getCurrentDevice(); + if (device == null) { + return false; + } + + try { + device.executeShellCommand("setprop " + PROP_GFX_PROFILING + " true", + new NullOutputReceiver()); + } catch (Exception e) { + return false; + } + + return true; + } + + private String getDumpsysCommand(int mode) { + if (mode == MODE_GFXINFO) { + Client c = getCurrentClient(); + if (c == null) { + return null; + } + + ClientData cd = c.getClientData(); + if (cd == null) { + return null; + } + + mGfxPackageName = cd.getClientDescription(); + if (mGfxPackageName == null) { + return null; + } + + return "dumpsys gfxinfo " + mGfxPackageName; + } else if (mode < DUMP_COMMAND.length) { + return DUMP_COMMAND[mode]; + } + + return null; + } + + /** + * Initializes temporary output file for executeShellCommand(). + * + * @throws IOException on file error + */ + IShellOutputReceiver initShellOutputBuffer(String header) throws IOException { + File f = File.createTempFile("ddmsfile", ".txt"); + f.deleteOnExit(); + + synchronized (RECEIVER_LOCK) { + if (mLastOutputReceiver != null) { + mLastOutputReceiver.cancel(); + } + + mLastOutputReceiver = new ShellOutputReceiver(f, header); + } + return mLastOutputReceiver; + } + + /** + * 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 (getCurrentDevice() != null) { + loadFromDevice(); + } + } + }); + + 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)); + + mChartComposite = new Composite(top, SWT.NONE); + mChartComposite.setLayoutData(new GridData(GridData.FILL_BOTH)); + mStackLayout = new StackLayout(); + mChartComposite.setLayout(mStackLayout); + + mPieChartComposite = createPieChartComposite(mChartComposite); + mStackedBarComposite = createStackedBarComposite(mChartComposite); + + mStackLayout.topControl = mPieChartComposite; + + return top; + } + + private Composite createStackedBarComposite(Composite chartComposite) { + mBarDataSet = new DefaultCategoryDataset(); + JFreeChart chart = ChartFactory.createStackedBarChart("Per Frame Rendering Time", + "Frame #", "Time (ms)", mBarDataSet, PlotOrientation.VERTICAL, + true /* legend */, true /* tooltips */, false /* urls */); + + ChartComposite c = newChartComposite(chart, chartComposite); + c.setLayoutData(new GridData(GridData.FILL_BOTH)); + return c; + } + + private Composite createPieChartComposite(Composite chartComposite) { + mDataset = new DefaultPieDataset(); + JFreeChart chart = ChartFactory.createPieChart("", mDataset, false + /* legend */, true/* tooltips */, false /* urls */); + + ChartComposite c = newChartComposite(chart, chartComposite); + c.setLayoutData(new GridData(GridData.FILL_BOTH)); + return c; + } + + private ChartComposite newChartComposite(JFreeChart chart, Composite parent) { + return new ChartComposite(parent, + 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); + } + + @Override + 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 { + return new BufferedReader(new FileReader(file)); + } + + /** + * 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; + } + + public static final class BugReportParser { + public static final class DataValue { + final String name; + final double value; + + public DataValue(String n, double v) { + name = n; + value = v; + } + }; + + /** Components of the time it takes to draw a single frame. */ + public static final class GfxProfileData { + /** draw time (time spent building display lists) in ms */ + final double draw; + + /** process time (time spent by Android's 2D renderer to execute display lists) (ms) */ + final double process; + + /** execute time (time spent to send frame to the compositor) in ms */ + final double execute; + + public GfxProfileData(double draw, double process, double execute) { + this.draw = draw; + this.process = process; + this.execute = execute; + } + } + + public static List parseGfxInfo(BufferedReader br) throws IOException { + Pattern headerPattern = Pattern.compile("\\s+Draw\\s+Process\\s+Execute"); + + String line = null; + while ((line = br.readLine()) != null) { + Matcher m = headerPattern.matcher(line); + if (m.find()) { + break; + } + } + + if (line == null) { + return Collections.emptyList(); + } + + // parse something like: " 0.85 1.10 0.61\n", 3 doubles basically + Pattern dataPattern = + Pattern.compile("(\\d*\\.\\d+)\\s+(\\d*\\.\\d+)\\s+(\\d*\\.\\d+)"); + + List data = new ArrayList(128); + while ((line = br.readLine()) != null) { + Matcher m = dataPattern.matcher(line); + if (!m.find()) { + break; + } + + double draw = safeParseDouble(m.group(1)); + double process = safeParseDouble(m.group(2)); + double execute = safeParseDouble(m.group(3)); + + data.add(new GfxProfileData(draw, process, execute)); + } + + return data; + } + + /** + * Processes wakelock information from bugreport. Updates mDataset with the + * new data. + * + * @param br Reader providing the content + * @throws IOException if error reading file + */ + public static List readWakelockDataset(BufferedReader br) throws IOException { + List results = new ArrayList(); + + 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.; + results.add(new DataValue(m.group(1), value)); + total -= value; + } else { + m = totalPattern.matcher(line); + if (m.find()) { + total += parseTimeMs(m.group(1)) / 1000.; + } + } + } + } + if (total > 0) { + results.add(new DataValue("Unlocked", total)); + } + + return results; + } + + /** + * Processes alarm information from bugreport. Updates mDataset with the new + * data. + * + * @param br Reader providing the content + * @throws IOException if error reading file + */ + public static List readAlarmDataset(BufferedReader br) throws IOException { + List results = new ArrayList(); + 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); + results.add(new DataValue(name, count)); + } + } + + return results; + } + + /** + * Processes cpu load information from bugreport. Updates mDataset with the + * new data. + * + * @param br Reader providing the content + * @throws IOException if error reading file + */ + public static List readCpuDataset(BufferedReader br) throws IOException { + List results = new ArrayList(); + Pattern pattern1 = Pattern.compile("(\\S+): (\\S+)% = (.+)% user . (.+)% kernel"); + Pattern pattern2 = Pattern.compile("(\\S+)% (\\S+): (.+)% user . (.+)% kernel"); + + while (true) { + String line = br.readLine(); + if (line == null) { + break; + } + line = line.trim(); + + if (line.startsWith("Load:")) { + continue; + } + + String name = ""; + double user = 0, kernel = 0, both = 0; + boolean found = false; + + // try pattern1 + Matcher m = pattern1.matcher(line); + if (m.find()) { + found = true; + name = m.group(1); + both = safeParseLong(m.group(2)); + user = safeParseLong(m.group(3)); + kernel = safeParseLong(m.group(4)); + } + + // try pattern2 + m = pattern2.matcher(line); + if (m.find()) { + found = true; + name = m.group(2); + both = safeParseDouble(m.group(1)); + user = safeParseDouble(m.group(3)); + kernel = safeParseDouble(m.group(4)); + } + + if (!found) { + continue; + } + + if ("TOTAL".equals(name)) { + if (both < 100) { + results.add(new DataValue("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) { + results.add(new DataValue(name + " (user)", user)); + } + if (kernel > 0) { + results.add(new DataValue(name + " (kernel)" , both - user)); + } + if (user == 0 && kernel == 0 && both > 0) { + results.add(new DataValue(name, both)); + } + } + + } + + return results; + } + + private static long safeParseLong(String s) { + try { + return Long.parseLong(s); + } catch (NumberFormatException e) { + return 0; + } + } + + private static double safeParseDouble(String s) { + try { + return Double.parseDouble(s); + } catch (NumberFormatException e) { + return 0; + } + } + + /** + * Processes meminfo information from bugreport. Updates mDataset with the + * new data. + * + * @param br Reader providing the content + * @throws IOException if error reading file + */ + public static List readMeminfoDataset(BufferedReader br) throws IOException { + List results = new ArrayList(); + Pattern valuePattern = Pattern.compile("(\\d+) kB"); + long total = 0; + long other = 0; + + // Scan meminfo + String line = null; + while ((line = br.readLine()) != null) { + if (line.contains("----")) { + continue; + } + + 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")) { + results.add(new DataValue("Free", kb)); + total -= kb; + } else if (line.startsWith("Slab")) { + results.add(new DataValue("Slab", kb)); + total -= kb; + } else if (line.startsWith("PageTables")) { + results.add(new DataValue("PageTables", kb)); + total -= kb; + } else if (line.startsWith("Buffers") && kb > 0) { + results.add(new DataValue("Buffers", kb)); + total -= kb; + } else if (line.startsWith("Inactive")) { + results.add(new DataValue("Inactive", kb)); + total -= kb; + } else if (line.startsWith("MemFree")) { + results.add(new DataValue("Free", kb)); + total -= kb; + } + } else { + break; + } + } + + List procRankResults = readProcRankDataset(br, line); + for (DataValue procRank : procRankResults) { + if (procRank.value > 2000) { // only show processes using > 2000K in memory + results.add(procRank); + } else { + other += procRank.value; + } + + total -= procRank.value; + } + + if (other > 0) { + results.add(new DataValue("Other", other)); + } + + // The Pss calculation is not necessarily accurate as accounting memory to + // a process is not accurate. So only if there really is unaccounted for memory do we + // add it to the pie. + if (total > 0) { + results.add(new DataValue("Unknown", total)); + } + + return results; + } + + static List readProcRankDataset(BufferedReader br, String header) + throws IOException { + List results = new ArrayList(); + + if (header == null || !header.contains("PID")) { + return results; + } + + Splitter PROCRANK_SPLITTER = Splitter.on(' ').omitEmptyStrings().trimResults(); + List fields = Lists.newArrayList(PROCRANK_SPLITTER.split(header)); + int pssIndex = fields.indexOf("Pss"); + int cmdIndex = fields.indexOf("cmdline"); + + if (pssIndex == -1 || cmdIndex == -1) { + return results; + } + + String line; + while ((line = br.readLine()) != null) { + // Extract pss field from procrank output + fields = Lists.newArrayList(PROCRANK_SPLITTER.split(line)); + + if (fields.size() < cmdIndex) { + break; + } + + String cmdline = fields.get(cmdIndex).replace("/system/bin/", ""); + String pssInK = fields.get(pssIndex); + if (pssInK.endsWith("K")) { + pssInK = pssInK.substring(0, pssInK.length() - 1); + } + long pss = safeParseLong(pssInK); + results.add(new DataValue(cmdline, pss)); + } + + return results; + } + + /** + * Processes sync information from bugreport. Updates mDataset with the new + * data. + * + * @param br Reader providing the content + * @throws IOException if error reading file + */ + public static List readSyncDataset(BufferedReader br) throws IOException { + List results = new ArrayList(); + + 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]); + results.add(new DataValue(authority, dur)); + } else if (duration.length() == 3) { + long dur = Long.parseLong(durParts[0]) * 3600 + + Long.parseLong(durParts[1]) * 60 + Long + .parseLong(durParts[2]); + results.add(new DataValue(authority, dur)); + } + } + } + + return results; + } + } + + private void readCpuDataset(BufferedReader br) throws IOException { + updatePieDataSet(BugReportParser.readCpuDataset(br), ""); + } + + private void readMeminfoDataset(BufferedReader br) throws IOException { + updatePieDataSet(BugReportParser.readMeminfoDataset(br), "PSS in kB"); + } + + private void readGfxInfoDataset(BufferedReader br) throws IOException { + updateBarChartDataSet(BugReportParser.parseGfxInfo(br), + mGfxPackageName == null ? "" : mGfxPackageName); + } + + private void clearDataSet() { + mLabel.setText(""); + mDataset.clear(); + mBarDataSet.clear(); + } + + private void updatePieDataSet(final List data, final String label) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + mLabel.setText(label); + mStackLayout.topControl = mPieChartComposite; + mChartComposite.layout(); + + for (BugReportParser.DataValue d : data) { + mDataset.setValue(d.name, d.value); + } + } + }); + } + + private void updateBarChartDataSet(final List gfxProfileData, + final String label) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + mLabel.setText(label); + mStackLayout.topControl = mStackedBarComposite; + mChartComposite.layout(); + + for (int i = 0; i < gfxProfileData.size(); i++) { + GfxProfileData d = gfxProfileData.get(i); + String frameNumber = Integer.toString(i); + + mBarDataSet.addValue(d.draw, "Draw", frameNumber); + mBarDataSet.addValue(d.process, "Process", frameNumber); + mBarDataSet.addValue(d.execute, "Execute", frameNumber); + } + } + }); + } + + private class ShellOutputReceiver implements IShellOutputReceiver { + private final OutputStream mStream; + private final File mFile; + private AtomicBoolean mCancelled = new AtomicBoolean(); + + public ShellOutputReceiver(File f, String header) { + mFile = f; + try { + mStream = new FileOutputStream(f); + } catch (FileNotFoundException e) { + throw new IllegalArgumentException(e); + } + + if (header != null) { + byte[] data = header.getBytes(); + addOutput(data, 0, data.length); + } + } + + @Override + public void addOutput(byte[] data, int offset, int length) { + try { + mStream.write(data, offset, length); + } catch (IOException e) { + Log.e("DDMS", e); + } + } + + @Override + public void flush() { + try { + mStream.close(); + } catch (IOException e) { + Log.e("DDMS", e); + } + + if (!isCancelled()) { + generateDataset(mFile); + } + } + + @Override + public boolean isCancelled() { + return mCancelled.get(); + } + + public void cancel() { + mCancelled.set(true); + } + + public File getDataFile() { + return mFile; + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/TableHelper.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/TableHelper.java new file mode 100644 index 00000000..5449a348 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/TableHelper.java @@ -0,0 +1,209 @@ +/* + * 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() { + @Override + public void controlMoved(ControlEvent e) { + } + + @Override + 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() { + @Override + public void controlMoved(ControlEvent e) { + } + + @Override + 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() { + @Override + public void controlMoved(ControlEvent e) { + } + + @Override + 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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/TablePanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/TablePanel.java new file mode 100644 index 00000000..ce6867fa --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/TablePanel.java @@ -0,0 +1,132 @@ +/* + * 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 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() { + @Override + 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() }); + } + } + + @Override + public void selectAll() { + table.selectAll(); + } + }; + + // add the focus listener on the table to notify the global listener + table.addFocusListener(new FocusListener() { + @Override + public void focusGained(FocusEvent e) { + mGlobalListener.focusGained(activator); + } + + @Override + 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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ThreadPanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ThreadPanel.java new file mode 100644 index 00000000..ea94602a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ThreadPanel.java @@ -0,0 +1,573 @@ +/* + * 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.IClientChangeListener; +import com.android.ddmlib.Client; +import com.android.ddmlib.ThreadInfo; + +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_COLUMN = "threadPanel.stack.col0"; //$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", "Runnable", "TimedWait", "Monitor", + "Wait", "Initializing", "Starting", "Native", "VmWait", + "Suspended" + }; + + /** + * Content Provider to display the threads of a client. + * Expected input is a {@link Client} object. + */ + private static class ThreadContentProvider implements IStructuredContentProvider { + @Override + public Object[] getElements(Object inputElement) { + if (inputElement instanceof Client) { + return ((Client)inputElement).getClientData().getThreads(); + } + + return new Object[0]; + } + + @Override + public void dispose() { + // pass + } + + @Override + 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 { + + @Override + public Image getColumnImage(Object element, int columnIndex) { + return null; + } + + @Override + 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; + } + + @Override + public void addListener(ILabelProviderListener listener) { + // pass + } + + @Override + public void dispose() { + // pass + } + + @Override + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + @Override + 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() { + @Override + public void selectionChanged(SelectionChangedEvent event) { + requestThreadStackTrace(getThreadSelection(event.getSelection())); + } + }); + mThreadViewer.addDoubleClickListener(new IDoubleClickListener() { + @Override + public void doubleClick(DoubleClickEvent event) { + requestThreadStackTrace(getThreadSelection(event.getSelection())); + } + }); + + // 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) { + requestThreadStackTrace(getThreadSelection(null)); + } + }); + + mStackTraceTimeLabel = new Label(mStackTraceBase, SWT.NONE); + mStackTraceTimeLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mStackTracePanel = new StackTracePanel(); + mStackTraceTable = mStackTracePanel.createPanel(mStackTraceBase, PREFS_STACK_COLUMN, 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() { + @Override + 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) + */ + @Override + 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() { + @Override + 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() { + @Override + 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(); + } + + private void requestThreadStackTrace(ThreadInfo selectedThread) { + if (selectedThread != null) { + Client client = (Client) mThreadViewer.getInput(); + if (client != null) { + client.requestThreadStackTrace(selectedThread.getThreadId()); + } + } + } + + /** + * 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() { + @Override + 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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/actions/ICommonAction.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/actions/ICommonAction.java new file mode 100644 index 00000000..7d65c48e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/actions/ToolItemAction.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/actions/ToolItemAction.java new file mode 100644 index 00000000..6747d032 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/actions/ToolItemAction.java @@ -0,0 +1,71 @@ +/* + * 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) + */ + @Override + 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) + */ + @Override + 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) + */ + @Override + public void setRunnable(final Runnable runnable) { + item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + runnable.run(); + } + }); + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/annotation/UiThread.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/annotation/UiThread.java new file mode 100644 index 00000000..32b61b22 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/annotation/WorkerThread.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/annotation/WorkerThread.java new file mode 100644 index 00000000..fe01c3fb --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/console/DdmConsole.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/console/DdmConsole.java new file mode 100644 index 00000000..7bc7a5ce --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/console/IDdmConsole.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/console/IDdmConsole.java new file mode 100644 index 00000000..cf1cf640 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceContentProvider.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceContentProvider.java new file mode 100644 index 00000000..1d47959f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceContentProvider.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.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() { + @Override + 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() { + @Override + 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); + } + } + }); + } + } + } + + @Override + 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() { + @Override + 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) + */ + @Override + 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) + */ + @Override + 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) + */ + @Override + 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) + */ + @Override + 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() + */ + @Override + public void dispose() { + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.IContentProvider#inputChanged(org.eclipse.jface.viewers.Viewer, java.lang.Object, java.lang.Object) + */ + @Override + 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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceExplorer.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceExplorer.java new file mode 100644 index 00000000..cc602c4c --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceExplorer.java @@ -0,0 +1,922 @@ +/* + * 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.AdbCommandRejectedException; +import com.android.ddmlib.DdmConstants; +import com.android.ddmlib.FileListingService; +import com.android.ddmlib.FileListingService.FileEntry; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.IShellOutputReceiver; +import com.android.ddmlib.ShellCommandUnresponsiveException; +import com.android.ddmlib.SyncException; +import com.android.ddmlib.SyncService; +import com.android.ddmlib.SyncService.ISyncProgressMonitor; +import com.android.ddmlib.TimeoutException; +import com.android.ddmuilib.DdmUiPreferences; +import com.android.ddmuilib.ImageLoader; +import com.android.ddmuilib.Panel; +import com.android.ddmuilib.SyncProgressHelper; +import com.android.ddmuilib.SyncProgressHelper.SyncRunnable; +import com.android.ddmuilib.TableHelper; +import com.android.ddmuilib.actions.ICommonAction; +import com.android.ddmuilib.console.DdmConsole; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.jface.dialogs.ErrorDialog; +import org.eclipse.jface.dialogs.IInputValidator; +import org.eclipse.jface.dialogs.InputDialog; +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.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 ICommonAction mCreateNewFolderAction; + + private Image mFileImage; + private Image mFolderImage; + private Image mPackageImage; + private Image mOtherImage; + + private IDevice mCurrentDevice; + + private String mDefaultSave; + + public DeviceExplorer() { + } + + /** + * Sets custom images for the device explorer. If none are set then defaults are used. + * This can be useful to set platform-specific explorer icons. + * + * This should be called before {@link #createControl(Composite)}. + * + * @param fileImage the icon to represent a file. + * @param folderImage the icon to represent a folder. + * @param packageImage the icon to represent an apk. + * @param otherImage the icon to represent other types of files. + */ + public void setCustomImages(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 + * @param createNewFolderAction + */ + public void setActions(ICommonAction pushAction, ICommonAction pullAction, + ICommonAction deleteAction, ICommonAction createNewFolderAction) { + mPushAction = pushAction; + mPullAction = pullAction; + mDeleteAction = deleteAction; + mCreateNewFolderAction = createNewFolderAction; + } + + /** + * 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()); + + ImageLoader loader = ImageLoader.getDdmUiLibLoader(); + if (mFileImage == null) { + mFileImage = loader.loadImage("file.png", mParent.getDisplay()); + } + if (mFolderImage == null) { + mFolderImage = loader.loadImage("folder.png", mParent.getDisplay()); + } + if (mPackageImage == null) { + mPackageImage = loader.loadImage("android.png", mParent.getDisplay()); + } + if (mOtherImage == null) { + // TODO: find a default image for other. + } + + 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() { + @Override + public void selectionChanged(SelectionChangedEvent event) { + ISelection sel = event.getSelection(); + if (sel.isEmpty()) { + mPullAction.setEnabled(false); + mPushAction.setEnabled(false); + mDeleteAction.setEnabled(false); + mCreateNewFolderAction.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) { + FileEntry entry = (FileEntry) element; + setDeleteEnabledState(entry); + mCreateNewFolderAction.setEnabled(entry.isDirectory()); + } else { + mDeleteAction.setEnabled(false); + } + } + } + } + }); + + // add support for double click + mTreeViewer.addDoubleClickListener(new IDoubleClickListener() { + @Override + 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() { + @Override + 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, DdmConstants.DOT_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(); + sync.pullFile(keyEntry, keyFile.getAbsolutePath(), monitor); + sync.pullFile(dataEntry, dataFile.getAbsolutePath(), monitor); + + // 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; + } catch (SyncException e) { + if (e.wasCanceled() == false) { + DdmConsole.printErrorToConsole(String.format( + "Failed to pull %1$s: %2$s", keyEntry.getName(), e.getMessage())); + return; + } + } catch (TimeoutException e) { + DdmConsole.printErrorToConsole(String.format( + "Failed to pull %1$s: timeout", keyEntry.getName())); + } catch (AdbCommandRejectedException e) { + DdmConsole.printErrorToConsole(String.format( + "Failed to pull %1$s: %2$s", keyEntry.getName(), e.getMessage())); + } + } + + /** + * 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() { + @Override + public void addOutput(byte[] data, int offset, int length) { + // pass + // TODO get output to display errors if any. + } + + @Override + public void flush() { + mTreeViewer.refresh(parentEntry); + } + + @Override + 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. + } catch (TimeoutException e) { + // adb failed somehow, we do nothing. We should be displaying the error from the output + // of the shell command. + } catch (AdbCommandRejectedException e) { + // adb failed somehow, we do nothing. We should be displaying the error from the output + // of the shell command. + } catch (ShellCommandUnresponsiveException e) { + // adb failed somehow, we do nothing. We should be displaying the error from the output + // of the shell command. + } + + } + + public void createNewFolderInSelection() { + TreeItem[] items = mTree.getSelection(); + + if (items.length != 1) { + return; + } + + final FileEntry entry = (FileEntry) items[0].getData(); + + if (entry.isDirectory()) { + InputDialog inputDialog = new InputDialog(mTree.getShell(), "New Folder", + "Please enter the new folder name", "New Folder", new IInputValidator() { + @Override + public String isValid(String newText) { + if ((newText != null) && (newText.length() > 0) + && (newText.trim().length() > 0) + && (newText.indexOf('/') == -1) + && (newText.indexOf('\\') == -1)) { + return null; + } else { + return "Invalid name"; + } + } + }); + inputDialog.open(); + String value = inputDialog.getValue(); + + if (value != null) { + // create the mkdir command + String command = "mkdir " + entry.getFullEscapedPath() //$NON-NLS-1$ + + FileListingService.FILE_SEPARATOR + FileEntry.escape(value); + + try { + mCurrentDevice.executeShellCommand(command, new IShellOutputReceiver() { + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public void flush() { + mTreeViewer.refresh(entry); + } + + @Override + public void addOutput(byte[] data, int offset, int length) { + String errorMessage; + if (data != null) { + errorMessage = new String(data); + } else { + errorMessage = ""; + } + Status status = new Status(IStatus.ERROR, + "DeviceExplorer", 0, errorMessage, null); //$NON-NLS-1$ + ErrorDialog.openError(mTree.getShell(), "New Folder Error", + "New Folder Error", status); + } + }); + } catch (TimeoutException e) { + // adb failed somehow, we do nothing. We should be + // displaying the error from the output of the shell + // command. + } catch (AdbCommandRejectedException e) { + // adb failed somehow, we do nothing. We should be + // displaying the error from the output of the shell + // command. + } catch (ShellCommandUnresponsiveException e) { + // adb failed somehow, we do nothing. We should be + // displaying the error from the output of the shell + // command. + } 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() { + @Override + 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() { + @Override + 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()]); + + SyncProgressHelper.run(new SyncRunnable() { + @Override + public void run(ISyncProgressMonitor monitor) + throws SyncException, IOException, TimeoutException { + sync.pull(entryArray, localDirectory, monitor); + } + + @Override + public void close() { + sync.close(); + } + }, "Pulling file(s) from the device", mParent.getShell()); + } + } catch (SyncException e) { + if (e.wasCanceled() == false) { + DdmConsole.printErrorToConsole(String.format( + "Failed to pull selection: %1$s", e.getMessage())); + } + } 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) { + SyncProgressHelper.run(new SyncRunnable() { + @Override + public void run(ISyncProgressMonitor monitor) + throws SyncException, IOException, TimeoutException { + sync.pullFile(remote, local, monitor); + } + + @Override + public void close() { + sync.close(); + } + }, String.format("Pulling %1$s from the device", remote.getName()), + mParent.getShell()); + } + } catch (SyncException e) { + if (e.wasCanceled() == false) { + DdmConsole.printErrorToConsole(String.format( + "Failed to pull selection: %1$s", e.getMessage())); + } + } 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) { + SyncProgressHelper.run(new SyncRunnable() { + @Override + public void run(ISyncProgressMonitor monitor) + throws SyncException, IOException, TimeoutException { + sync.push(localFiles, remoteDirectory, monitor); + } + + @Override + public void close() { + sync.close(); + } + }, "Pushing file(s) to the device", mParent.getShell()); + } + } catch (SyncException e) { + if (e.wasCanceled() == false) { + DdmConsole.printErrorToConsole(String.format( + "Failed to push selection: %1$s", e.getMessage())); + } + } 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) { + // get the file name + String[] segs = local.split(Pattern.quote(File.separator)); + String name = segs[segs.length-1]; + final String remoteFile = remoteDirectory + FileListingService.FILE_SEPARATOR + + name; + + SyncProgressHelper.run(new SyncRunnable() { + @Override + public void run(ISyncProgressMonitor monitor) + throws SyncException, IOException, TimeoutException { + sync.pushFile(local, remoteFile, monitor); + } + + @Override + public void close() { + sync.close(); + } + }, String.format("Pushing %1$s to the device.", name), mParent.getShell()); + } + } catch (SyncException e) { + if (e.wasCanceled() == false) { + DdmConsole.printErrorToConsole(String.format( + "Failed to push selection: %1$s", e.getMessage())); + } + } 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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/explorer/FileLabelProvider.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/explorer/FileLabelProvider.java new file mode 100644 index 00000000..36a51c81 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/explorer/FileLabelProvider.java @@ -0,0 +1,160 @@ +/* + * 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) + */ + @Override + public Image getImage(Object element) { + return null; + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.ILabelProvider#getText(java.lang.Object) + */ + @Override + public String getText(Object element) { + return null; + } + + @Override + 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; + } + + @Override + 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) + */ + @Override + public void addListener(ILabelProviderListener listener) { + // we don't need listeners. + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.IBaseLabelProvider#dispose() + */ + @Override + public void dispose() { + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.IBaseLabelProvider#isLabelProperty(java.lang.Object, java.lang.String) + */ + @Override + public boolean isLabelProperty(Object element, String property) { + return false; + } + + /* (non-Javadoc) + * @see org.eclipse.jface.viewers.IBaseLabelProvider#removeListener(org.eclipse.jface.viewers.ILabelProviderListener) + */ + @Override + public void removeListener(ILabelProviderListener listener) { + // we don't need listeners + } + +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/handler/BaseFileHandler.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/handler/BaseFileHandler.java new file mode 100644 index 00000000..1e715c76 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/handler/BaseFileHandler.java @@ -0,0 +1,184 @@ +/* + * 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.ClientData.IHprofDumpHandler; +import com.android.ddmlib.ClientData.IMethodProfilingHandler; +import com.android.ddmlib.SyncException; +import com.android.ddmlib.SyncService; +import com.android.ddmlib.SyncService.ISyncProgressMonitor; +import com.android.ddmlib.TimeoutException; +import com.android.ddmuilib.SyncProgressHelper; +import com.android.ddmuilib.SyncProgressHelper.SyncRunnable; + +import org.eclipse.jface.dialogs.MessageDialog; +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 + * @throws SyncException if an error happens during the push of the package on the device. + * @throws IOException + */ + protected void promptAndPull(final SyncService sync, + String localFileName, final String remoteFilePath, String title) + throws InvocationTargetException, InterruptedException, SyncException, TimeoutException, + IOException { + FileDialog fileDialog = new FileDialog(mParentShell, SWT.SAVE); + + fileDialog.setText(title); + fileDialog.setFileName(localFileName); + + final String localFilePath = fileDialog.open(); + if (localFilePath != null) { + SyncProgressHelper.run(new SyncRunnable() { + @Override + public void run(ISyncProgressMonitor monitor) throws SyncException, IOException, + TimeoutException { + sync.pullFile(remoteFilePath, localFilePath, monitor); + } + + @Override + public void close() { + sync.close(); + } + }, + String.format("Pulling %1$s from the device", remoteFilePath), mParentShell); + } + } + + /** + * 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; + } + + /** + * 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() { + @Override + 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, String extension) throws IOException { + File f = File.createTempFile("ddms", extension); + 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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/handler/MethodProfilingHandler.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/handler/MethodProfilingHandler.java new file mode 100644 index 00000000..f9985070 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/handler/MethodProfilingHandler.java @@ -0,0 +1,195 @@ +/* + * 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.ClientData.IMethodProfilingHandler; +import com.android.ddmlib.DdmConstants; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.Log; +import com.android.ddmlib.SyncException; +import com.android.ddmlib.SyncService; +import com.android.ddmlib.SyncService.ISyncProgressMonitor; +import com.android.ddmlib.TimeoutException; +import com.android.ddmuilib.DdmUiPreferences; +import com.android.ddmuilib.SyncProgressHelper; +import com.android.ddmuilib.SyncProgressHelper.SyncRunnable; +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"; + } + + @Override + 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" : ""); + } + + @Override + 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" : ""); + } + + @Override + public void onSuccess(final String remoteFilePath, final Client client) { + mParentShell.getDisplay().asyncExec(new Runnable() { + @Override + 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()); + } + } + + }); + } + + @Override + public void onSuccess(byte[] data, final Client client) { + try { + File tempFile = saveTempFile(data, DdmConstants.DOT_TRACE); + open(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(final SyncService sync, final String remoteFilePath) + throws InvocationTargetException, InterruptedException, IOException { + // get a temp file + File temp = File.createTempFile("android", DdmConstants.DOT_TRACE); //$NON-NLS-1$ + final String tempPath = temp.getAbsolutePath(); + + // pull the file + try { + SyncProgressHelper.run(new SyncRunnable() { + @Override + public void run(ISyncProgressMonitor monitor) + throws SyncException, IOException, TimeoutException { + sync.pullFile(remoteFilePath, tempPath, monitor); + } + + @Override + public void close() { + sync.close(); + } + }, + String.format("Pulling %1$s from the device", remoteFilePath), mParentShell); + + // open the temp file in traceview + open(tempPath); + } catch (SyncException e) { + if (e.wasCanceled() == false) { + displayErrorFromUiThread("Unable to download trace file:\n\n%1$s", e.getMessage()); + } + } catch (TimeoutException e) { + displayErrorFromUiThread("Unable to download trace file:\n\ntimeout"); + } + } + + protected void open(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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeDiffAllocationInfo.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeDiffAllocationInfo.java new file mode 100644 index 00000000..e19931e8 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeDiffAllocationInfo.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2013 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.heap; + +import com.android.ddmlib.NativeAllocationInfo; +import com.android.ddmlib.NativeStackCallInfo; + +import java.util.List; + +/** + * {@link NativeDiffAllocationInfo} stores the difference in the allocation + * counts between two allocations with the same stack trace. + * + * Since only the allocation counts are different, it delegates all other functionality to + * one of the allocations and just maintains the allocation count. + */ +public class NativeDiffAllocationInfo extends NativeAllocationInfo { + private final NativeAllocationInfo info; + + public NativeDiffAllocationInfo(NativeAllocationInfo cur, NativeAllocationInfo prev) { + super(cur.getSize(), getNewAllocations(cur, prev)); + info = cur; + } + + private static int getNewAllocations(NativeAllocationInfo n1, NativeAllocationInfo n2) { + return n1.getAllocationCount() - n2.getAllocationCount(); + } + + @Override + public boolean isStackCallResolved() { + return info.isStackCallResolved(); + } + + @Override + public List getStackCallAddresses() { + return info.getStackCallAddresses(); + } + + @Override + public synchronized List getResolvedStackCall() { + return info.getResolvedStackCall(); + } + + @Override + public synchronized NativeStackCallInfo getRelevantStackCallInfo() { + return info.getRelevantStackCallInfo(); + } + + @Override + public boolean isZygoteChild() { + return info.isZygoteChild(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapDataImporter.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapDataImporter.java new file mode 100644 index 00000000..e54997a3 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapDataImporter.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2011 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.heap; + +import com.android.ddmlib.NativeAllocationInfo; +import com.android.ddmlib.NativeStackCallInfo; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jface.operation.IRunnableWithProgress; + +import java.io.IOException; +import java.io.LineNumberReader; +import java.io.Reader; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.InputMismatchException; +import java.util.List; +import java.util.Scanner; +import java.util.regex.Pattern; + +public class NativeHeapDataImporter implements IRunnableWithProgress { + private final LineNumberReader mReader; + private int mStartLineNumber; + private int mEndLineNumber; + + private NativeHeapSnapshot mSnapshot; + + public NativeHeapDataImporter(Reader stream) { + mReader = new LineNumberReader(stream); + mReader.setLineNumber(1); // start numbering at 1 + } + + @Override + public void run(IProgressMonitor monitor) + throws InvocationTargetException, InterruptedException { + monitor.beginTask("Importing Heap Data", IProgressMonitor.UNKNOWN); + + List allocations = new ArrayList(); + try { + while (true) { + String line; + StringBuilder sb = new StringBuilder(); + + // read in a sequence of lines corresponding to a single NativeAllocationInfo + mStartLineNumber = mReader.getLineNumber(); + while ((line = mReader.readLine()) != null) { + if (line.trim().length() == 0) { + // each block of allocations end with an empty line + break; + } + + sb.append(line); + sb.append('\n'); + } + mEndLineNumber = mReader.getLineNumber(); + + // parse those lines into a NativeAllocationInfo object + String allocationBlock = sb.toString(); + if (allocationBlock.trim().length() > 0) { + allocations.add(getNativeAllocation(allocationBlock)); + } + + if (line == null) { // EOF + break; + } + } + } catch (Exception e) { + if (e.getMessage() == null) { + e = new RuntimeException(genericErrorMessage("Unexpected Parse error")); + } + throw new InvocationTargetException(e); + } finally { + try { + mReader.close(); + } catch (IOException e) { + // we can ignore this exception + } + monitor.done(); + } + + mSnapshot = new NativeHeapSnapshot(allocations); + } + + /** Parse a single native allocation dump. This is the complement of + * {@link NativeAllocationInfo#toString()}. + * + * An allocation is of the following form: + * Allocations: 1 + * Size: 344748 + * Total Size: 344748 + * BeginStackTrace: + * 40069bd8 /lib/libc_malloc_leak.so --- get_backtrace --- /libc/bionic/malloc_leak.c:258 + * 40069dd8 /lib/libc_malloc_leak.so --- leak_calloc --- /libc/bionic/malloc_leak.c:576 + * 40069bd8 /lib/libc_malloc_leak.so --- 40069bd8 --- + * 40069dd8 /lib/libc_malloc_leak.so --- 40069dd8 --- + * EndStackTrace + * Note that in the above stack trace, the last two lines are examples where the address + * was not resolved. + * + * @param block a string of lines corresponding to a single {@code NativeAllocationInfo} + * @return parse the input and return the corresponding {@link NativeAllocationInfo} + * @throws InputMismatchException if there are any parse errors + */ + private NativeAllocationInfo getNativeAllocation(String block) { + Scanner sc = new Scanner(block); + + try { + String kw = sc.next(); + if (!NativeAllocationInfo.ALLOCATIONS_KW.equals(kw)) { + throw new InputMismatchException( + expectedKeywordErrorMessage(NativeAllocationInfo.ALLOCATIONS_KW, kw)); + } + + int allocations = sc.nextInt(); + + kw = sc.next(); + if (!NativeAllocationInfo.SIZE_KW.equals(kw)) { + throw new InputMismatchException( + expectedKeywordErrorMessage(NativeAllocationInfo.SIZE_KW, kw)); + } + + int size = sc.nextInt(); + + kw = sc.next(); + if (!NativeAllocationInfo.TOTAL_SIZE_KW.equals(kw)) { + throw new InputMismatchException( + expectedKeywordErrorMessage(NativeAllocationInfo.TOTAL_SIZE_KW, kw)); + } + + int totalSize = sc.nextInt(); + if (totalSize != size * allocations) { + throw new InputMismatchException( + genericErrorMessage("Total Size does not match size * # of allocations")); + } + + NativeAllocationInfo info = new NativeAllocationInfo(size, allocations); + + kw = sc.next(); + if (!NativeAllocationInfo.BEGIN_STACKTRACE_KW.equals(kw)) { + throw new InputMismatchException( + expectedKeywordErrorMessage(NativeAllocationInfo.BEGIN_STACKTRACE_KW, kw)); + } + + List stackInfo = new ArrayList(); + Pattern endTracePattern = Pattern.compile(NativeAllocationInfo.END_STACKTRACE_KW); + + + while (true) { + long address = sc.nextLong(16); + info.addStackCallAddress(address); + + String library = sc.next(); + sc.next(); // ignore "---" + String method = scanTillSeparator(sc, "---"); + + String filename = ""; + if (!isUnresolved(method, address)) { + filename = sc.next(); + } + + stackInfo.add(new NativeStackCallInfo(address, library, method, filename)); + + if (sc.hasNext(endTracePattern)) { + break; + } + } + + info.setResolvedStackCall(stackInfo); + return info; + } finally { + sc.close(); + } + } + + private String scanTillSeparator(Scanner sc, String separator) { + StringBuilder sb = new StringBuilder(); + + while (true) { + String token = sc.next(); + if (token.equals(separator)) { + break; + } + + sb.append(token); + + // We do not know the exact delimiter that was skipped over, but we know + // that there was atleast 1 whitespace. Add a single whitespace character + // to account for this. + sb.append(' '); + } + + return sb.toString().trim(); + } + + private boolean isUnresolved(String method, long address) { + // a method is unresolved if it is just the hex representation of the address + return Long.toString(address, 16).equals(method); + } + + private String genericErrorMessage(String message) { + return String.format("%1$s between lines %2$d and %3$d", + message, mStartLineNumber, mEndLineNumber); + } + + private String expectedKeywordErrorMessage(String expected, String actual) { + return String.format("Expected keyword '%1$s', saw '%2$s' between lines %3$d to %4$d.", + expected, actual, mStartLineNumber, mEndLineNumber); + } + + public NativeHeapSnapshot getImportedSnapshot() { + return mSnapshot; + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapDiffSnapshot.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapDiffSnapshot.java new file mode 100644 index 00000000..dfc8b7c1 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapDiffSnapshot.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2011 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.heap; + +import com.android.ddmlib.NativeAllocationInfo; +import com.google.common.collect.Sets; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Models a heap snapshot that is the difference between two snapshots. + */ +public class NativeHeapDiffSnapshot extends NativeHeapSnapshot { + private long mCommonAllocationsTotalMemory; + + public NativeHeapDiffSnapshot(NativeHeapSnapshot newSnapshot, NativeHeapSnapshot oldSnapshot) { + // The diff snapshots behaves like a snapshot that only contains the new allocations + // not present in the old snapshot + super(getNewAllocations(newSnapshot, oldSnapshot)); + + Set commonAllocations = + new HashSet(oldSnapshot.getAllocations()); + commonAllocations.retainAll(newSnapshot.getAllocations()); + + // Memory common between the old and new snapshots + mCommonAllocationsTotalMemory = getTotalMemory(commonAllocations); + } + + private static List getNewAllocations(NativeHeapSnapshot newSnapshot, + NativeHeapSnapshot oldSnapshot) { + Set allocations = + new HashSet(newSnapshot.getAllocations()); + + // compute new allocations + allocations.removeAll(oldSnapshot.getAllocations()); + + // Account for allocations with the same stack trace that were + // present in the older set of allocations. + // e.g. A particular stack trace might have had 3 allocations in snapshot 1, + // and 2 more in snapshot 2. We only want to show the new allocations (just the 2 from + // snapshot 2). However, the way the allocations are stored, in snapshot 2, we'll see + // 5 allocations at the stack trace. We need to subtract out the 3 from the first allocation + Set onlyInPrevious = + new HashSet(oldSnapshot.getAllocations()); + Set newAllocations = + Sets.newHashSetWithExpectedSize(allocations.size()); + + onlyInPrevious.removeAll(newSnapshot.getAllocations()); + for (NativeAllocationInfo current : allocations) { + NativeAllocationInfo old = getOldAllocationWithSameStack(current, onlyInPrevious); + if (old == null) { + newAllocations.add(current); + } else if (current.getAllocationCount() > old.getAllocationCount()) { + newAllocations.add(new NativeDiffAllocationInfo(current, old)); + } + } + + return new ArrayList(newAllocations); + } + + private static NativeAllocationInfo getOldAllocationWithSameStack( + NativeAllocationInfo info, + Set allocations) { + for (NativeAllocationInfo a : allocations) { + if (info.getSize() == a.getSize() && info.stackEquals(a)) { + return a; + } + } + + return null; + } + + @Override + public String getFormattedMemorySize() { + // for a diff snapshot, we report the following string for display: + // xxx bytes new allocation + yyy bytes retained from previous allocation + // = zzz bytes total + + long newAllocations = getTotalSize(); + return String.format("%s bytes new + %s bytes retained = %s bytes total", + formatMemorySize(newAllocations), + formatMemorySize(mCommonAllocationsTotalMemory), + formatMemorySize(newAllocations + mCommonAllocationsTotalMemory)); + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapLabelProvider.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapLabelProvider.java new file mode 100644 index 00000000..3c3845eb --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapLabelProvider.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2011 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.heap; + +import com.android.ddmlib.NativeAllocationInfo; +import com.android.ddmlib.NativeStackCallInfo; + +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.jface.viewers.LabelProvider; +import org.eclipse.swt.graphics.Image; + +/** + * A Label Provider for the Native Heap TreeViewer in {@link NativeHeapPanel}. + */ +public class NativeHeapLabelProvider extends LabelProvider implements ITableLabelProvider { + private long mTotalSize; + + @Override + public Image getColumnImage(Object arg0, int arg1) { + return null; + } + + @Override + public String getColumnText(Object element, int index) { + if (element instanceof NativeAllocationInfo) { + return getColumnTextForNativeAllocation((NativeAllocationInfo) element, index); + } + + if (element instanceof NativeLibraryAllocationInfo) { + return getColumnTextForNativeLibrary((NativeLibraryAllocationInfo) element, index); + } + + return null; + } + + private String getColumnTextForNativeAllocation(NativeAllocationInfo info, int index) { + NativeStackCallInfo stackInfo = info.getRelevantStackCallInfo(); + + switch (index) { + case 0: + return stackInfo == null ? stackResolutionStatus(info) : stackInfo.getLibraryName(); + case 1: + return Integer.toString(info.getSize() * info.getAllocationCount()); + case 2: + return getPercentageString(info.getSize() * info.getAllocationCount(), mTotalSize); + case 3: + String prefix = ""; + if (!info.isZygoteChild()) { + prefix = "Z "; + } + return prefix + Integer.toString(info.getAllocationCount()); + case 4: + return Integer.toString(info.getSize()); + case 5: + return stackInfo == null ? stackResolutionStatus(info) : stackInfo.getMethodName(); + default: + return null; + } + } + + private String getColumnTextForNativeLibrary(NativeLibraryAllocationInfo info, int index) { + switch (index) { + case 0: + return info.getLibraryName(); + case 1: + return Long.toString(info.getTotalSize()); + case 2: + return getPercentageString(info.getTotalSize(), mTotalSize); + default: + return null; + } + } + + private String getPercentageString(long size, long total) { + if (total == 0) { + return ""; + } + + return String.format("%.1f%%", (float)(size * 100)/(float)total); + } + + private String stackResolutionStatus(NativeAllocationInfo info) { + if (info.isStackCallResolved()) { + return "?"; // resolved and unknown + } else { + return "Resolving..."; // still resolving... + } + } + + /** + * Set the total size of the heap dump for use in percentage calculations. + * This value should be set whenever the input to the tree changes so that the percentages + * are computed correctly. + */ + public void setTotalSize(long totalSize) { + mTotalSize = totalSize; + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapPanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapPanel.java new file mode 100644 index 00000000..c845fc13 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapPanel.java @@ -0,0 +1,1137 @@ +/* + * Copyright (C) 2011 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.heap; + +import com.android.ddmlib.Client; +import com.android.ddmlib.Log; +import com.android.ddmlib.NativeAllocationInfo; +import com.android.ddmlib.NativeLibraryMapInfo; +import com.android.ddmlib.NativeStackCallInfo; +import com.android.ddmuilib.Addr2Line; +import com.android.ddmuilib.BaseHeapPanel; +import com.android.ddmuilib.ITableFocusListener; +import com.android.ddmuilib.ITableFocusListener.IFocusedTableActivator; +import com.android.ddmuilib.ImageLoader; +import com.android.ddmuilib.TableHelper; + +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.dialogs.ProgressMonitorDialog; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.swt.SWT; +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.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.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.Shell; +import org.eclipse.swt.widgets.Text; +import org.eclipse.swt.widgets.ToolBar; +import org.eclipse.swt.widgets.ToolItem; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeItem; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.Reader; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** Panel to display native heap information. */ +public class NativeHeapPanel extends BaseHeapPanel { + private static final boolean USE_OLD_RESOLVER; + static { + String useOldResolver = System.getenv("ANDROID_DDMS_OLD_SYMRESOLVER"); + USE_OLD_RESOLVER = useOldResolver != null && useOldResolver.equalsIgnoreCase("true"); + } + private final int MAX_DISPLAYED_ERROR_ITEMS = 5; + + private static final String TOOLTIP_EXPORT_DATA = "Export Heap Data"; + private static final String TOOLTIP_ZYGOTE_ALLOCATIONS = "Show Zygote Allocations"; + private static final String TOOLTIP_DIFFS_ONLY = "Only show new allocations not present in previous snapshot"; + private static final String TOOLTIP_GROUPBY = "Group allocations by library."; + + private static final String EXPORT_DATA_IMAGE = "save.png"; + private static final String ZYGOTE_IMAGE = "zygote.png"; + private static final String DIFFS_ONLY_IMAGE = "diff.png"; + private static final String GROUPBY_IMAGE = "groupby.png"; + + private static final String SNAPSHOT_HEAP_BUTTON_TEXT = "Snapshot Current Native Heap Usage"; + private static final String LOAD_HEAP_DATA_BUTTON_TEXT = "Import Heap Data"; + private static final String SYMBOL_SEARCH_PATH_LABEL_TEXT = "Symbol Search Path:"; + private static final String SYMBOL_SEARCH_PATH_TEXT_MESSAGE = + "List of colon separated paths to search for symbol debug information. See tooltip for examples."; + private static final String SYMBOL_SEARCH_PATH_TOOLTIP_TEXT = + "Colon separated paths that contain unstripped libraries with debug symbols.\n" + + "e.g.: /out/target/product/generic/symbols/system/lib:/path/to/my/app/obj/local/armeabi"; + + private static final String PREFS_SHOW_DIFFS_ONLY = "nativeheap.show.diffs.only"; + private static final String PREFS_SHOW_ZYGOTE_ALLOCATIONS = "nativeheap.show.zygote"; + private static final String PREFS_GROUP_BY_LIBRARY = "nativeheap.grouby.library"; + private static final String PREFS_SYMBOL_SEARCH_PATH = "nativeheap.search.path"; + private static final String PREFS_SASH_HEIGHT_PERCENT = "nativeheap.sash.percent"; + private static final String PREFS_LAST_IMPORTED_HEAPPATH = "nativeheap.last.import.path"; + private IPreferenceStore mPrefStore; + + private List mNativeHeapSnapshots; + + // Maintain the differences between a snapshot and its predecessor. + // mDiffSnapshots[i] = mNativeHeapSnapshots[i] - mNativeHeapSnapshots[i-1] + // The zeroth entry is null since there is no predecessor. + // The list is filled lazily on demand. + private List mDiffSnapshots; + + private Map> mImportedSnapshotsPerPid; + + private Button mSnapshotHeapButton; + private Button mLoadHeapDataButton; + private Text mSymbolSearchPathText; + private Combo mSnapshotIndexCombo; + private Label mMemoryAllocatedText; + + private TreeViewer mDetailsTreeViewer; + private TreeViewer mStackTraceTreeViewer; + private NativeHeapProviderByAllocations mContentProviderByAllocations; + private NativeHeapProviderByLibrary mContentProviderByLibrary; + private NativeHeapLabelProvider mDetailsTreeLabelProvider; + + private ToolItem mGroupByButton; + private ToolItem mDiffsOnlyButton; + private ToolItem mShowZygoteAllocationsButton; + private ToolItem mExportHeapDataButton; + + public NativeHeapPanel(IPreferenceStore prefStore) { + mPrefStore = prefStore; + mPrefStore.setDefault(PREFS_SASH_HEIGHT_PERCENT, 75); + mPrefStore.setDefault(PREFS_SYMBOL_SEARCH_PATH, ""); + mPrefStore.setDefault(PREFS_GROUP_BY_LIBRARY, false); + mPrefStore.setDefault(PREFS_SHOW_ZYGOTE_ALLOCATIONS, true); + mPrefStore.setDefault(PREFS_SHOW_DIFFS_ONLY, false); + + mNativeHeapSnapshots = new ArrayList(); + mDiffSnapshots = new ArrayList(); + mImportedSnapshotsPerPid = new HashMap>(); + } + + /** {@inheritDoc} */ + @Override + public void clientChanged(final Client client, int changeMask) { + if (client != getCurrentClient()) { + return; + } + + if ((changeMask & Client.CHANGE_NATIVE_HEAP_DATA) != Client.CHANGE_NATIVE_HEAP_DATA) { + return; + } + + List allocations = client.getClientData().getNativeAllocationList(); + if (allocations.size() == 0) { + return; + } + + // We need to clone this list since getClientData().getNativeAllocationList() clobbers + // the list on future updates + final List nativeAllocations = shallowCloneList(allocations); + + addNativeHeapSnapshot(new NativeHeapSnapshot(nativeAllocations)); + updateDisplay(); + + // Attempt to resolve symbols in a separate thread. + // The UI should be refreshed once the symbols have been resolved. + if (USE_OLD_RESOLVER) { + Thread t = new Thread(new SymbolResolverTask(nativeAllocations, + client.getClientData().getMappedNativeLibraries())); + t.setName("Address to Symbol Resolver"); + t.start(); + } else { + Display.getDefault().asyncExec(new Runnable() { + @Override + public void run() { + resolveSymbols(); + mDetailsTreeViewer.refresh(); + mStackTraceTreeViewer.refresh(); + } + + public void resolveSymbols() { + Shell shell = Display.getDefault().getActiveShell(); + ProgressMonitorDialog d = new ProgressMonitorDialog(shell); + + NativeSymbolResolverTask resolver = new NativeSymbolResolverTask( + nativeAllocations, + client.getClientData().getMappedNativeLibraries(), + mSymbolSearchPathText.getText(), + client.getClientData().getAbi()); + + try { + d.run(true, true, resolver); + } catch (InvocationTargetException e) { + MessageDialog.openError(shell, + "Error Resolving Symbols", + e.getCause().getMessage()); + return; + } catch (InterruptedException e) { + return; + } + + MessageDialog.openInformation(shell, "Symbol Resolution Status", + getResolutionStatusMessage(resolver)); + } + }); + } + } + + private String getResolutionStatusMessage(NativeSymbolResolverTask resolver) { + StringBuilder sb = new StringBuilder(); + sb.append("Symbol Resolution Complete.\n\n"); + + // show addresses that were not mapped + Set unmappedAddresses = resolver.getUnmappedAddresses(); + if (unmappedAddresses.size() > 0) { + sb.append(String.format("Unmapped addresses (%d): ", + unmappedAddresses.size())); + sb.append(getSampleForDisplay(unmappedAddresses)); + sb.append('\n'); + } + + // show libraries that were not present on disk + Set notFoundLibraries = resolver.getNotFoundLibraries(); + if (notFoundLibraries.size() > 0) { + sb.append(String.format("Libraries not found on disk (%d): ", + notFoundLibraries.size())); + sb.append(getSampleForDisplay(notFoundLibraries)); + sb.append('\n'); + } + + // show addresses that were mapped but not resolved + Set unresolvableAddresses = resolver.getUnresolvableAddresses(); + if (unresolvableAddresses.size() > 0) { + sb.append(String.format("Unresolved addresses (%d): ", + unresolvableAddresses.size())); + sb.append(getSampleForDisplay(unresolvableAddresses)); + sb.append('\n'); + } + + if (resolver.getAddr2LineErrorMessage() != null) { + sb.append("Error launching addr2line: "); + sb.append(resolver.getAddr2LineErrorMessage()); + } + + return sb.toString(); + } + + /** + * Get the string representation for a collection of items. + * If there are more items than {@link #MAX_DISPLAYED_ERROR_ITEMS}, then only the first + * {@link #MAX_DISPLAYED_ERROR_ITEMS} items are taken into account, + * and an ellipsis is added at the end. + */ + private String getSampleForDisplay(Collection items) { + StringBuilder sb = new StringBuilder(); + + int c = 1; + Iterator it = items.iterator(); + while (it.hasNext()) { + Object item = it.next(); + if (item instanceof Long) { + sb.append(String.format("0x%x", item)); + } else { + sb.append(item); + } + + if (c == MAX_DISPLAYED_ERROR_ITEMS && it.hasNext()) { + sb.append(", ..."); + break; + } else if (it.hasNext()) { + sb.append(", "); + } + + c++; + } + return sb.toString(); + } + + private void addNativeHeapSnapshot(NativeHeapSnapshot snapshot) { + mNativeHeapSnapshots.add(snapshot); + + // The diff snapshots are filled in lazily on demand. + // But the list needs to be the same size as mNativeHeapSnapshots, so we add a null. + mDiffSnapshots.add(null); + } + + private List shallowCloneList(List allocations) { + List clonedList = + new ArrayList(allocations.size()); + + for (NativeAllocationInfo i : allocations) { + clonedList.add(i); + } + + return clonedList; + } + + @Override + public void deviceSelected() { + // pass + } + + @Override + public void clientSelected() { + Client c = getCurrentClient(); + + if (c == null) { + // if there is no client selected, then we disable the buttons but leave the + // display as is so that whatever snapshots are displayed continue to stay + // visible to the user. + mSnapshotHeapButton.setEnabled(false); + mLoadHeapDataButton.setEnabled(false); + return; + } + + mNativeHeapSnapshots = new ArrayList(); + mDiffSnapshots = new ArrayList(); + + mSnapshotHeapButton.setEnabled(true); + mLoadHeapDataButton.setEnabled(true); + + List importedSnapshots = mImportedSnapshotsPerPid.get( + c.getClientData().getPid()); + if (importedSnapshots != null) { + for (NativeHeapSnapshot n : importedSnapshots) { + addNativeHeapSnapshot(n); + } + } + + List allocations = c.getClientData().getNativeAllocationList(); + allocations = shallowCloneList(allocations); + + if (allocations.size() > 0) { + addNativeHeapSnapshot(new NativeHeapSnapshot(allocations)); + } + + updateDisplay(); + } + + private void updateDisplay() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + updateSnapshotIndexCombo(); + updateToolbars(); + + int lastSnapshotIndex = mNativeHeapSnapshots.size() - 1; + displaySnapshot(lastSnapshotIndex); + displayStackTraceForSelection(); + } + }); + } + + private void displaySelectedSnapshot() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + int idx = mSnapshotIndexCombo.getSelectionIndex(); + displaySnapshot(idx); + } + }); + } + + private void displaySnapshot(int index) { + if (index < 0 || mNativeHeapSnapshots.size() == 0) { + mDetailsTreeViewer.setInput(null); + mMemoryAllocatedText.setText(""); + return; + } + + assert index < mNativeHeapSnapshots.size() : "Invalid snapshot index"; + + NativeHeapSnapshot snapshot = mNativeHeapSnapshots.get(index); + if (mDiffsOnlyButton.getSelection() && index > 0) { + snapshot = getDiffSnapshot(index); + } + + mMemoryAllocatedText.setText(snapshot.getFormattedMemorySize()); + mMemoryAllocatedText.pack(); + + mDetailsTreeLabelProvider.setTotalSize(snapshot.getTotalSize()); + mDetailsTreeViewer.setInput(snapshot); + mDetailsTreeViewer.refresh(); + } + + /** Obtain the diff of snapshot[index] & snapshot[index-1] */ + private NativeHeapSnapshot getDiffSnapshot(int index) { + // if it was already computed, simply return that + NativeHeapSnapshot diffSnapshot = mDiffSnapshots.get(index); + if (diffSnapshot != null) { + return diffSnapshot; + } + + // compute the diff + NativeHeapSnapshot cur = mNativeHeapSnapshots.get(index); + NativeHeapSnapshot prev = mNativeHeapSnapshots.get(index - 1); + diffSnapshot = new NativeHeapDiffSnapshot(cur, prev); + + // cache for future use + mDiffSnapshots.set(index, diffSnapshot); + + return diffSnapshot; + } + + private void updateDisplayGrouping() { + boolean groupByLibrary = mGroupByButton.getSelection(); + mPrefStore.setValue(PREFS_GROUP_BY_LIBRARY, groupByLibrary); + + if (groupByLibrary) { + mDetailsTreeViewer.setContentProvider(mContentProviderByLibrary); + } else { + mDetailsTreeViewer.setContentProvider(mContentProviderByAllocations); + } + } + + private void updateDisplayForZygotes() { + boolean displayZygoteMemory = mShowZygoteAllocationsButton.getSelection(); + mPrefStore.setValue(PREFS_SHOW_ZYGOTE_ALLOCATIONS, displayZygoteMemory); + + // inform the content providers of the zygote display setting + mContentProviderByLibrary.displayZygoteMemory(displayZygoteMemory); + mContentProviderByAllocations.displayZygoteMemory(displayZygoteMemory); + + // refresh the UI + mDetailsTreeViewer.refresh(); + } + + private void updateSnapshotIndexCombo() { + List items = new ArrayList(); + + int numSnapshots = mNativeHeapSnapshots.size(); + for (int i = 0; i < numSnapshots; i++) { + // offset indices by 1 so that users see index starting at 1 rather than 0 + items.add("Snapshot " + (i + 1)); + } + + mSnapshotIndexCombo.setItems(items.toArray(new String[items.size()])); + + if (numSnapshots > 0) { + mSnapshotIndexCombo.setEnabled(true); + mSnapshotIndexCombo.select(numSnapshots - 1); + } else { + mSnapshotIndexCombo.setEnabled(false); + } + } + + private void updateToolbars() { + int numSnapshots = mNativeHeapSnapshots.size(); + mExportHeapDataButton.setEnabled(numSnapshots > 0); + } + + @Override + protected Control createControl(Composite parent) { + Composite c = new Composite(parent, SWT.NONE); + c.setLayout(new GridLayout(1, false)); + c.setLayoutData(new GridData(GridData.FILL_BOTH)); + + createControlsSection(c); + createDetailsSection(c); + + // Initialize widget state based on whether a client + // is selected or not. + clientSelected(); + + return c; + } + + private void createControlsSection(Composite parent) { + Composite c = new Composite(parent, SWT.NONE); + c.setLayout(new GridLayout(3, false)); + c.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + createGetHeapDataSection(c); + + Label l = new Label(c, SWT.SEPARATOR | SWT.VERTICAL); + l.setLayoutData(new GridData(GridData.FILL_VERTICAL)); + + createDisplaySection(c); + } + + private void createGetHeapDataSection(Composite parent) { + Composite c = new Composite(parent, SWT.NONE); + c.setLayout(new GridLayout(1, false)); + + createTakeHeapSnapshotButton(c); + + Label l = new Label(c, SWT.SEPARATOR | SWT.HORIZONTAL); + l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + createLoadHeapDataButton(c); + } + + private void createTakeHeapSnapshotButton(Composite parent) { + mSnapshotHeapButton = new Button(parent, SWT.BORDER | SWT.PUSH); + mSnapshotHeapButton.setText(SNAPSHOT_HEAP_BUTTON_TEXT); + mSnapshotHeapButton.setLayoutData(new GridData()); + + // disable by default, enabled only when a client is selected + mSnapshotHeapButton.setEnabled(false); + + mSnapshotHeapButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent evt) { + snapshotHeap(); + } + }); + } + + private void snapshotHeap() { + Client c = getCurrentClient(); + assert c != null : "Snapshot Heap could not have been enabled w/o a selected client."; + + // send an async request + c.requestNativeHeapInformation(); + } + + private void createLoadHeapDataButton(Composite parent) { + mLoadHeapDataButton = new Button(parent, SWT.BORDER | SWT.PUSH); + mLoadHeapDataButton.setText(LOAD_HEAP_DATA_BUTTON_TEXT); + mLoadHeapDataButton.setLayoutData(new GridData()); + + // disable by default, enabled only when a client is selected + mLoadHeapDataButton.setEnabled(false); + + mLoadHeapDataButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent evt) { + loadHeapDataFromFile(); + } + }); + } + + private void loadHeapDataFromFile() { + // pop up a file dialog and get the file to load + final String path = getHeapDumpToImport(); + if (path == null) { + return; + } + + Reader reader = null; + try { + reader = new FileReader(path); + } catch (FileNotFoundException e) { + // cannot occur since user input was via a FileDialog + } + + Shell shell = Display.getDefault().getActiveShell(); + ProgressMonitorDialog d = new ProgressMonitorDialog(shell); + + NativeHeapDataImporter importer = new NativeHeapDataImporter(reader); + try { + d.run(true, true, importer); + } catch (InvocationTargetException e) { + // exception while parsing, display error to user and then return + MessageDialog.openError(shell, + "Error Importing Heap Data", + e.getCause().getMessage()); + return; + } catch (InterruptedException e) { + // operation cancelled by user, simply return + return; + } + + NativeHeapSnapshot snapshot = importer.getImportedSnapshot(); + + addToImportedSnapshots(snapshot); // save imported snapshot for future use + addNativeHeapSnapshot(snapshot); // add to currently displayed snapshots as well + + updateDisplay(); + } + + private void addToImportedSnapshots(NativeHeapSnapshot snapshot) { + Client c = getCurrentClient(); + + if (c == null) { + return; + } + + Integer pid = c.getClientData().getPid(); + List importedSnapshots = mImportedSnapshotsPerPid.get(pid); + if (importedSnapshots == null) { + importedSnapshots = new ArrayList(); + } + + importedSnapshots.add(snapshot); + mImportedSnapshotsPerPid.put(pid, importedSnapshots); + } + + private String getHeapDumpToImport() { + FileDialog fileDialog = new FileDialog(Display.getDefault().getActiveShell(), + SWT.OPEN); + + fileDialog.setText("Import Heap Dump"); + fileDialog.setFilterExtensions(new String[] {"*.txt"}); + fileDialog.setFilterPath(mPrefStore.getString(PREFS_LAST_IMPORTED_HEAPPATH)); + + String selectedFile = fileDialog.open(); + if (selectedFile != null) { + // save the path to restore in future dialog open + mPrefStore.setValue(PREFS_LAST_IMPORTED_HEAPPATH, new File(selectedFile).getParent()); + } + return selectedFile; + } + + private void createDisplaySection(Composite parent) { + Composite c = new Composite(parent, SWT.NONE); + c.setLayout(new GridLayout(2, false)); + c.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + // Create: Display: __________________ + createLabel(c, "Display:"); + mSnapshotIndexCombo = new Combo(c, SWT.READ_ONLY); + mSnapshotIndexCombo.setItems(new String[] {"No heap snapshots available."}); + mSnapshotIndexCombo.setEnabled(false); + mSnapshotIndexCombo.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + displaySelectedSnapshot(); + } + }); + + // Create: Memory Allocated (bytes): _________________ + createLabel(c, "Memory Allocated:"); + mMemoryAllocatedText = new Label(c, SWT.NONE); + GridData gd = new GridData(); + gd.widthHint = 100; + mMemoryAllocatedText.setLayoutData(gd); + + // Create: Search Path: __________________ + createLabel(c, SYMBOL_SEARCH_PATH_LABEL_TEXT); + mSymbolSearchPathText = new Text(c, SWT.BORDER); + mSymbolSearchPathText.setMessage(SYMBOL_SEARCH_PATH_TEXT_MESSAGE); + mSymbolSearchPathText.setToolTipText(SYMBOL_SEARCH_PATH_TOOLTIP_TEXT); + mSymbolSearchPathText.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent arg0) { + String path = mSymbolSearchPathText.getText(); + updateSearchPath(path); + mPrefStore.setValue(PREFS_SYMBOL_SEARCH_PATH, path); + } + }); + mSymbolSearchPathText.setText(mPrefStore.getString(PREFS_SYMBOL_SEARCH_PATH)); + mSymbolSearchPathText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + } + + private void updateSearchPath(String path) { + Addr2Line.setSearchPath(path); + } + + private void createLabel(Composite parent, String text) { + Label l = new Label(parent, SWT.NONE); + l.setText(text); + GridData gd = new GridData(); + gd.horizontalAlignment = SWT.RIGHT; + l.setLayoutData(gd); + } + + /** + * Create the details section displaying the details table and the stack trace + * corresponding to the selection. + * + * The details is laid out like so: + * Details Toolbar + * Details Table + * ------------sash--- + * Stack Trace Label + * Stack Trace Text + * There is a sash in between the two sections, and we need to save/restore the sash + * preferences. Using FormLayout seems like the easiest solution here, but the layout + * code looks ugly as a result. + */ + private void createDetailsSection(Composite parent) { + final Composite c = new Composite(parent, SWT.NONE); + c.setLayout(new FormLayout()); + c.setLayoutData(new GridData(GridData.FILL_BOTH)); + + ToolBar detailsToolBar = new ToolBar(c, SWT.FLAT | SWT.BORDER); + initializeDetailsToolBar(detailsToolBar); + + Tree detailsTree = new Tree(c, SWT.VIRTUAL | SWT.BORDER | SWT.MULTI); + initializeDetailsTree(detailsTree); + + final Sash sash = new Sash(c, SWT.HORIZONTAL | SWT.BORDER); + + Label stackTraceLabel = new Label(c, SWT.NONE); + stackTraceLabel.setText("Stack Trace:"); + + Tree stackTraceTree = new Tree(c, SWT.BORDER | SWT.MULTI); + initializeStackTraceTree(stackTraceTree); + + // layout the widgets created above + FormData data = new FormData(); + data.top = new FormAttachment(0, 0); + data.left = new FormAttachment(0, 0); + data.right = new FormAttachment(100, 0); + detailsToolBar.setLayoutData(data); + + data = new FormData(); + data.top = new FormAttachment(detailsToolBar, 0); + data.bottom = new FormAttachment(sash, 0); + data.left = new FormAttachment(0, 0); + data.right = new FormAttachment(100, 0); + detailsTree.setLayoutData(data); + + final FormData sashData = new FormData(); + sashData.top = new FormAttachment(mPrefStore.getInt(PREFS_SASH_HEIGHT_PERCENT), 0); + 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.left = new FormAttachment(0, 0); + data.right = new FormAttachment(100, 0); + stackTraceLabel.setLayoutData(data); + + data = new FormData(); + data.top = new FormAttachment(stackTraceLabel, 0); + data.left = new FormAttachment(0, 0); + data.bottom = new FormAttachment(100, 0); + data.right = new FormAttachment(100, 0); + stackTraceTree.setLayoutData(data); + + sash.addListener(SWT.Selection, new Listener() { + @Override + public void handleEvent(Event e) { + Rectangle sashRect = sash.getBounds(); + Rectangle panelRect = c.getClientArea(); + int sashPercent = sashRect.y * 100 / panelRect.height; + mPrefStore.setValue(PREFS_SASH_HEIGHT_PERCENT, sashPercent); + + sashData.top = new FormAttachment(0, e.y); + c.layout(); + } + }); + } + + private void initializeDetailsToolBar(ToolBar toolbar) { + mGroupByButton = new ToolItem(toolbar, SWT.CHECK); + mGroupByButton.setImage(ImageLoader.getDdmUiLibLoader().loadImage(GROUPBY_IMAGE, + toolbar.getDisplay())); + mGroupByButton.setToolTipText(TOOLTIP_GROUPBY); + mGroupByButton.setSelection(mPrefStore.getBoolean(PREFS_GROUP_BY_LIBRARY)); + mGroupByButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + updateDisplayGrouping(); + } + }); + + mDiffsOnlyButton = new ToolItem(toolbar, SWT.CHECK); + mDiffsOnlyButton.setImage(ImageLoader.getDdmUiLibLoader().loadImage(DIFFS_ONLY_IMAGE, + toolbar.getDisplay())); + mDiffsOnlyButton.setToolTipText(TOOLTIP_DIFFS_ONLY); + mDiffsOnlyButton.setSelection(mPrefStore.getBoolean(PREFS_SHOW_DIFFS_ONLY)); + mDiffsOnlyButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + // simply refresh the display, as the display logic takes care of + // the current state of the diffs only checkbox. + int idx = mSnapshotIndexCombo.getSelectionIndex(); + displaySnapshot(idx); + } + }); + + mShowZygoteAllocationsButton = new ToolItem(toolbar, SWT.CHECK); + mShowZygoteAllocationsButton.setImage(ImageLoader.getDdmUiLibLoader().loadImage( + ZYGOTE_IMAGE, toolbar.getDisplay())); + mShowZygoteAllocationsButton.setToolTipText(TOOLTIP_ZYGOTE_ALLOCATIONS); + mShowZygoteAllocationsButton.setSelection( + mPrefStore.getBoolean(PREFS_SHOW_ZYGOTE_ALLOCATIONS)); + mShowZygoteAllocationsButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + updateDisplayForZygotes(); + } + }); + + mExportHeapDataButton = new ToolItem(toolbar, SWT.PUSH); + mExportHeapDataButton.setImage(ImageLoader.getDdmUiLibLoader().loadImage( + EXPORT_DATA_IMAGE, toolbar.getDisplay())); + mExportHeapDataButton.setToolTipText(TOOLTIP_EXPORT_DATA); + mExportHeapDataButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + exportSnapshot(); + } + }); + } + + /** Export currently displayed snapshot to a file */ + private void exportSnapshot() { + int idx = mSnapshotIndexCombo.getSelectionIndex(); + String snapshotName = mSnapshotIndexCombo.getItem(idx); + + FileDialog fileDialog = new FileDialog(Display.getDefault().getActiveShell(), + SWT.SAVE); + + fileDialog.setText("Save " + snapshotName); + fileDialog.setFileName("allocations.txt"); + + final String fileName = fileDialog.open(); + if (fileName == null) { + return; + } + + final NativeHeapSnapshot snapshot = mNativeHeapSnapshots.get(idx); + Thread t = new Thread(new Runnable() { + @Override + public void run() { + PrintWriter out; + try { + out = new PrintWriter(new BufferedWriter(new FileWriter(fileName))); + } catch (IOException e) { + displayErrorMessage(e.getMessage()); + return; + } + + for (NativeAllocationInfo alloc : snapshot.getAllocations()) { + out.println(alloc.toString()); + } + out.close(); + } + + private void displayErrorMessage(final String message) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + MessageDialog.openError(Display.getDefault().getActiveShell(), + "Failed to export heap data", message); + } + }); + } + }); + t.setName("Saving Heap Data to File..."); + t.start(); + } + + private void initializeDetailsTree(Tree tree) { + tree.setHeaderVisible(true); + tree.setLinesVisible(true); + + List properties = Arrays.asList("Library", + "Total", + "Percentage", + "Count", + "Size", + "Method"); + + List sampleValues = Arrays.asList("/path/in/device/to/system/library.so", + "123456789", + " 100%", + "123456789", + "123456789", + "PossiblyLongDemangledMethodName"); + + // right align numeric values + List swtFlags = Arrays.asList(SWT.LEFT, + SWT.RIGHT, + SWT.RIGHT, + SWT.RIGHT, + SWT.RIGHT, + SWT.LEFT); + + for (int i = 0; i < properties.size(); i++) { + String p = properties.get(i); + String v = sampleValues.get(i); + int flags = swtFlags.get(i); + TableHelper.createTreeColumn(tree, p, flags, v, getPref("details", p), mPrefStore); + } + + mDetailsTreeViewer = new TreeViewer(tree); + + mDetailsTreeViewer.setUseHashlookup(true); + + boolean displayZygotes = mPrefStore.getBoolean(PREFS_SHOW_ZYGOTE_ALLOCATIONS); + mContentProviderByAllocations = new NativeHeapProviderByAllocations(mDetailsTreeViewer, + displayZygotes); + mContentProviderByLibrary = new NativeHeapProviderByLibrary(mDetailsTreeViewer, + displayZygotes); + if (mPrefStore.getBoolean(PREFS_GROUP_BY_LIBRARY)) { + mDetailsTreeViewer.setContentProvider(mContentProviderByLibrary); + } else { + mDetailsTreeViewer.setContentProvider(mContentProviderByAllocations); + } + + mDetailsTreeLabelProvider = new NativeHeapLabelProvider(); + mDetailsTreeViewer.setLabelProvider(mDetailsTreeLabelProvider); + + mDetailsTreeViewer.setInput(null); + + tree.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent event) { + displayStackTraceForSelection(); + } + }); + } + + private void initializeStackTraceTree(Tree tree) { + tree.setHeaderVisible(true); + tree.setLinesVisible(true); + + List properties = Arrays.asList("Address", + "Library", + "Method", + "File", + "Line"); + + List sampleValues = Arrays.asList("0x1234_5678", + "/path/in/device/to/system/library.so", + "PossiblyLongDemangledMethodName", + "/android/out/prefix/in/home/directory/to/path/in/device/to/system/library.so", + "2000"); + + for (int i = 0; i < properties.size(); i++) { + String p = properties.get(i); + String v = sampleValues.get(i); + TableHelper.createTreeColumn(tree, p, SWT.LEFT, v, getPref("stack", p), mPrefStore); + } + + mStackTraceTreeViewer = new TreeViewer(tree); + + mStackTraceTreeViewer.setContentProvider(new NativeStackContentProvider()); + mStackTraceTreeViewer.setLabelProvider(new NativeStackLabelProvider()); + + mStackTraceTreeViewer.setInput(null); + } + + private void displayStackTraceForSelection() { + TreeItem []items = mDetailsTreeViewer.getTree().getSelection(); + if (items.length == 0) { + mStackTraceTreeViewer.setInput(null); + return; + } + + Object data = items[0].getData(); + if (!(data instanceof NativeAllocationInfo)) { + mStackTraceTreeViewer.setInput(null); + return; + } + + NativeAllocationInfo info = (NativeAllocationInfo) data; + if (info.isStackCallResolved()) { + mStackTraceTreeViewer.setInput(info.getResolvedStackCall()); + } else { + mStackTraceTreeViewer.setInput(info.getStackCallAddresses()); + } + } + + private String getPref(String prefix, String s) { + return "nativeheap.tree." + prefix + "." + s; + } + + @Override + public void setFocus() { + } + + private ITableFocusListener mTableFocusListener; + + @Override + public void setTableFocusListener(ITableFocusListener listener) { + mTableFocusListener = listener; + + final Tree heapSitesTree = mDetailsTreeViewer.getTree(); + final IFocusedTableActivator heapSitesActivator = new IFocusedTableActivator() { + @Override + public void copy(Clipboard clipboard) { + TreeItem[] items = heapSitesTree.getSelection(); + copyToClipboard(items, clipboard); + } + + @Override + public void selectAll() { + heapSitesTree.selectAll(); + } + }; + + heapSitesTree.addFocusListener(new FocusListener() { + @Override + public void focusLost(FocusEvent arg0) { + mTableFocusListener.focusLost(heapSitesActivator); + } + + @Override + public void focusGained(FocusEvent arg0) { + mTableFocusListener.focusGained(heapSitesActivator); + } + }); + + final Tree stackTraceTree = mStackTraceTreeViewer.getTree(); + final IFocusedTableActivator stackTraceActivator = new IFocusedTableActivator() { + @Override + public void copy(Clipboard clipboard) { + TreeItem[] items = stackTraceTree.getSelection(); + copyToClipboard(items, clipboard); + } + + @Override + public void selectAll() { + stackTraceTree.selectAll(); + } + }; + + stackTraceTree.addFocusListener(new FocusListener() { + @Override + public void focusLost(FocusEvent arg0) { + mTableFocusListener.focusLost(stackTraceActivator); + } + + @Override + public void focusGained(FocusEvent arg0) { + mTableFocusListener.focusGained(stackTraceActivator); + } + }); + } + + private void copyToClipboard(TreeItem[] items, Clipboard clipboard) { + StringBuilder sb = new StringBuilder(); + + for (TreeItem item : items) { + Object data = item.getData(); + if (data != null) { + sb.append(data.toString()); + sb.append('\n'); + } + } + + String content = sb.toString(); + if (content.length() > 0) { + clipboard.setContents( + new Object[] {sb.toString()}, + new Transfer[] {TextTransfer.getInstance()} + ); + } + } + + private class SymbolResolverTask implements Runnable { + private List mCallSites; + private List mMappedLibraries; + private Map mResolvedSymbolCache; + + public SymbolResolverTask(List callSites, + List mappedLibraries) { + mCallSites = callSites; + mMappedLibraries = mappedLibraries; + + mResolvedSymbolCache = new HashMap(); + } + + @Override + public void run() { + for (NativeAllocationInfo callSite : mCallSites) { + if (callSite.isStackCallResolved()) { + continue; + } + + List addresses = callSite.getStackCallAddresses(); + List resolvedStackInfo = + new ArrayList(addresses.size()); + + for (Long address : addresses) { + NativeStackCallInfo info = mResolvedSymbolCache.get(address); + + if (info != null) { + resolvedStackInfo.add(info); + } else { + info = resolveAddress(address); + resolvedStackInfo.add(info); + mResolvedSymbolCache.put(address, info); + } + } + + callSite.setResolvedStackCall(resolvedStackInfo); + } + + Display.getDefault().asyncExec(new Runnable() { + @Override + public void run() { + mDetailsTreeViewer.refresh(); + mStackTraceTreeViewer.refresh(); + } + }); + } + + private NativeStackCallInfo resolveAddress(long addr) { + NativeLibraryMapInfo library = getLibraryFor(addr); + + if (library != null) { + Client c = getCurrentClient(); + Addr2Line process = Addr2Line.getProcess(library, c.getClientData().getAbi()); + if (process != null) { + NativeStackCallInfo info = process.getAddress(addr); + if (info != null) { + return info; + } + } + } + + return new NativeStackCallInfo(addr, + library != null ? library.getLibraryName() : null, + Long.toHexString(addr), + ""); + } + + private NativeLibraryMapInfo getLibraryFor(long addr) { + for (NativeLibraryMapInfo info : mMappedLibraries) { + if (info.isWithinLibrary(addr)) { + return info; + } + } + + Log.d("ddm-nativeheap", "Failed finding Library for " + Long.toHexString(addr)); + return null; + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapProviderByAllocations.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapProviderByAllocations.java new file mode 100644 index 00000000..40757283 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapProviderByAllocations.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2011 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.heap; + +import com.android.ddmlib.NativeAllocationInfo; + +import org.eclipse.jface.viewers.ILazyTreeContentProvider; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.Viewer; + +import java.util.List; + +/** + * Content Provider for the native heap tree viewer in {@link NativeHeapPanel}. + * It expects a {@link NativeHeapSnapshot} as input, and provides the list of allocations + * in the heap dump as content to the UI. + */ +public final class NativeHeapProviderByAllocations implements ILazyTreeContentProvider { + private TreeViewer mViewer; + private boolean mDisplayZygoteMemory; + private NativeHeapSnapshot mNativeHeapDump; + + public NativeHeapProviderByAllocations(TreeViewer viewer, boolean displayZygotes) { + mViewer = viewer; + mDisplayZygoteMemory = displayZygotes; + } + + @Override + public void dispose() { + } + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + mNativeHeapDump = (NativeHeapSnapshot) newInput; + } + + @Override + public Object getParent(Object arg0) { + return null; + } + + @Override + public void updateChildCount(Object element, int currentChildCount) { + int childCount = 0; + + if (element == mNativeHeapDump) { // root element + childCount = getAllocations().size(); + } + + mViewer.setChildCount(element, childCount); + } + + @Override + public void updateElement(Object parent, int index) { + Object item = null; + + if (parent == mNativeHeapDump) { // root element + item = getAllocations().get(index); + } + + mViewer.replace(parent, index, item); + mViewer.setChildCount(item, 0); + } + + public void displayZygoteMemory(boolean en) { + mDisplayZygoteMemory = en; + } + + private List getAllocations() { + if (mDisplayZygoteMemory) { + return mNativeHeapDump.getAllocations(); + } else { + return mNativeHeapDump.getNonZygoteAllocations(); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapProviderByLibrary.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapProviderByLibrary.java new file mode 100644 index 00000000..8d2c7ff5 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapProviderByLibrary.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2011 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.heap; + +import org.eclipse.jface.viewers.ILazyTreeContentProvider; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.Viewer; + +import java.util.List; + +/** + * Content Provider for the native heap tree viewer in {@link NativeHeapPanel}. + * It expects input of type {@link NativeHeapSnapshot}, and provides heap allocations + * grouped by library to the UI. + */ +public class NativeHeapProviderByLibrary implements ILazyTreeContentProvider { + private TreeViewer mViewer; + private boolean mDisplayZygoteMemory; + + public NativeHeapProviderByLibrary(TreeViewer viewer, boolean displayZygotes) { + mViewer = viewer; + mDisplayZygoteMemory = displayZygotes; + } + + @Override + public void dispose() { + } + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + } + + @Override + public Object getParent(Object element) { + return null; + } + + @Override + public void updateChildCount(Object element, int currentChildCount) { + int childCount = 0; + + if (element instanceof NativeHeapSnapshot) { + NativeHeapSnapshot snapshot = (NativeHeapSnapshot) element; + childCount = getLibraryAllocations(snapshot).size(); + } else if (element instanceof NativeLibraryAllocationInfo) { + NativeLibraryAllocationInfo info = (NativeLibraryAllocationInfo) element; + childCount = info.getAllocations().size(); + } + + mViewer.setChildCount(element, childCount); + } + + @Override + public void updateElement(Object parent, int index) { + Object item = null; + int childCount = 0; + + if (parent instanceof NativeHeapSnapshot) { // root element + NativeHeapSnapshot snapshot = (NativeHeapSnapshot) parent; + item = getLibraryAllocations(snapshot).get(index); + childCount = ((NativeLibraryAllocationInfo) item).getAllocations().size(); + } else if (parent instanceof NativeLibraryAllocationInfo) { + item = ((NativeLibraryAllocationInfo) parent).getAllocations().get(index); + } + + mViewer.replace(parent, index, item); + mViewer.setChildCount(item, childCount); + } + + public void displayZygoteMemory(boolean en) { + mDisplayZygoteMemory = en; + } + + private List getLibraryAllocations(NativeHeapSnapshot snapshot) { + if (mDisplayZygoteMemory) { + return snapshot.getAllocationsByLibrary(); + } else { + return snapshot.getNonZygoteAllocationsByLibrary(); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapSnapshot.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapSnapshot.java new file mode 100644 index 00000000..6956fcc9 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapSnapshot.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2011 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.heap; + +import com.android.ddmlib.NativeAllocationInfo; + +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * A Native Heap Snapshot models a single heap dump. + * + * It primarily consists of a list of {@link NativeAllocationInfo} objects. From this list, + * other objects of interest to the UI are computed and cached for future use. + */ +public class NativeHeapSnapshot { + private static final NumberFormat NUMBER_FORMATTER = NumberFormat.getInstance(); + + private List mHeapAllocations; + private List mHeapAllocationsByLibrary; + + private List mNonZygoteHeapAllocations; + private List mNonZygoteHeapAllocationsByLibrary; + + private long mTotalSize; + + public NativeHeapSnapshot(List heapAllocations) { + mHeapAllocations = heapAllocations; + + // precompute the total size as this is always needed. + mTotalSize = getTotalMemory(heapAllocations); + } + + protected long getTotalMemory(Collection heapSnapshot) { + long total = 0; + + for (NativeAllocationInfo info : heapSnapshot) { + total += info.getAllocationCount() * info.getSize(); + } + + return total; + } + + public List getAllocations() { + return mHeapAllocations; + } + + public List getAllocationsByLibrary() { + if (mHeapAllocationsByLibrary != null) { + return mHeapAllocationsByLibrary; + } + + List heapAllocations = + NativeLibraryAllocationInfo.constructFrom(mHeapAllocations); + + // cache for future uses only if it is fully resolved. + if (isFullyResolved(heapAllocations)) { + mHeapAllocationsByLibrary = heapAllocations; + } + + return heapAllocations; + } + + private boolean isFullyResolved(List heapAllocations) { + for (NativeLibraryAllocationInfo info : heapAllocations) { + if (info.getLibraryName().equals(NativeLibraryAllocationInfo.UNRESOLVED_LIBRARY_NAME)) { + return false; + } + } + + return true; + } + + public long getTotalSize() { + return mTotalSize; + } + + public String getFormattedMemorySize() { + return String.format("%s bytes", formatMemorySize(getTotalSize())); + } + + protected String formatMemorySize(long memSize) { + return NUMBER_FORMATTER.format(memSize); + } + + public List getNonZygoteAllocations() { + if (mNonZygoteHeapAllocations != null) { + return mNonZygoteHeapAllocations; + } + + // filter out all zygote allocations + mNonZygoteHeapAllocations = new ArrayList(); + for (NativeAllocationInfo info : mHeapAllocations) { + if (info.isZygoteChild()) { + mNonZygoteHeapAllocations.add(info); + } + } + + return mNonZygoteHeapAllocations; + } + + public List getNonZygoteAllocationsByLibrary() { + if (mNonZygoteHeapAllocationsByLibrary != null) { + return mNonZygoteHeapAllocationsByLibrary; + } + + List heapAllocations = + NativeLibraryAllocationInfo.constructFrom(getNonZygoteAllocations()); + + // cache for future uses only if it is fully resolved. + if (isFullyResolved(heapAllocations)) { + mNonZygoteHeapAllocationsByLibrary = heapAllocations; + } + + return heapAllocations; + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeLibraryAllocationInfo.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeLibraryAllocationInfo.java new file mode 100644 index 00000000..4925f9ae --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeLibraryAllocationInfo.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2011 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.heap; + +import com.android.ddmlib.NativeAllocationInfo; +import com.android.ddmlib.NativeStackCallInfo; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A heap dump representation where each call site is associated with its source library. + */ +public final class NativeLibraryAllocationInfo { + /** Library name to use when grouping before symbol resolution is complete. */ + public static final String UNRESOLVED_LIBRARY_NAME = "Resolving.."; + + /** Any call site that cannot be resolved to a specific library goes under this name. */ + private static final String UNKNOWN_LIBRARY_NAME = "unknown"; + + private final String mLibraryName; + private final List mHeapAllocations; + private int mTotalSize; + + private NativeLibraryAllocationInfo(String libraryName) { + mLibraryName = libraryName; + mHeapAllocations = new ArrayList(); + } + + private void addAllocation(NativeAllocationInfo info) { + mHeapAllocations.add(info); + } + + private void updateTotalSize() { + mTotalSize = 0; + for (NativeAllocationInfo i : mHeapAllocations) { + mTotalSize += i.getAllocationCount() * i.getSize(); + } + } + + public String getLibraryName() { + return mLibraryName; + } + + public long getTotalSize() { + return mTotalSize; + } + + public List getAllocations() { + return mHeapAllocations; + } + + /** + * Factory method to create a list of {@link NativeLibraryAllocationInfo} objects, + * given the list of {@link NativeAllocationInfo} objects. + * + * If the {@link NativeAllocationInfo} objects do not have their symbols resolved, + * then they are grouped under the library {@link #UNRESOLVED_LIBRARY_NAME}. If they do + * have their symbols resolved, but map to an unknown library, then they are grouped under + * the library {@link #UNKNOWN_LIBRARY_NAME}. + */ + public static List constructFrom( + List allocations) { + if (allocations == null) { + return null; + } + + Map allocationsByLibrary = + new HashMap(); + + // go through each native allocation and assign it to the appropriate library + for (NativeAllocationInfo info : allocations) { + String libName = UNRESOLVED_LIBRARY_NAME; + + if (info.isStackCallResolved()) { + NativeStackCallInfo relevantStackCall = info.getRelevantStackCallInfo(); + if (relevantStackCall != null) { + libName = relevantStackCall.getLibraryName(); + } else { + libName = UNKNOWN_LIBRARY_NAME; + } + } + + addtoLibrary(allocationsByLibrary, libName, info); + } + + List libraryAllocations = + new ArrayList(allocationsByLibrary.values()); + + // now update some summary statistics for each library + for (NativeLibraryAllocationInfo l : libraryAllocations) { + l.updateTotalSize(); + } + + // finally, sort by total size + Collections.sort(libraryAllocations, new Comparator() { + @Override + public int compare(NativeLibraryAllocationInfo o1, + NativeLibraryAllocationInfo o2) { + return (int) (o2.getTotalSize() - o1.getTotalSize()); + } + }); + + return libraryAllocations; + } + + private static void addtoLibrary(Map libraryAllocations, + String libName, NativeAllocationInfo info) { + NativeLibraryAllocationInfo libAllocationInfo = libraryAllocations.get(libName); + if (libAllocationInfo == null) { + libAllocationInfo = new NativeLibraryAllocationInfo(libName); + libraryAllocations.put(libName, libAllocationInfo); + } + + libAllocationInfo.addAllocation(info); + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeStackContentProvider.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeStackContentProvider.java new file mode 100644 index 00000000..79e32029 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeStackContentProvider.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2011 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.heap; + +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.Viewer; + +import java.util.List; + +public class NativeStackContentProvider implements ITreeContentProvider { + @Override + public Object[] getElements(Object arg0) { + return getChildren(arg0); + } + + @Override + public void dispose() { + } + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + } + + @Override + public Object[] getChildren(Object parentElement) { + if (parentElement instanceof List) { + return ((List) parentElement).toArray(); + } + + return null; + } + + @Override + public Object getParent(Object element) { + return null; + } + + @Override + public boolean hasChildren(Object element) { + return false; + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeStackLabelProvider.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeStackLabelProvider.java new file mode 100644 index 00000000..5574a6e2 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeStackLabelProvider.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2011 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.heap; + +import com.android.ddmlib.NativeStackCallInfo; + +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.jface.viewers.LabelProvider; +import org.eclipse.swt.graphics.Image; + +public class NativeStackLabelProvider extends LabelProvider implements ITableLabelProvider { + @Override + public Image getColumnImage(Object arg0, int arg1) { + return null; + } + + @Override + public String getColumnText(Object element, int index) { + if (element instanceof NativeStackCallInfo) { + return getResolvedStackTraceColumnText((NativeStackCallInfo) element, index); + } + + if (element instanceof Long) { + // if the addresses have not been resolved, then just display the + // addresses alone + return getStackAddressColumnText((Long) element, index); + } + + return null; + } + + public String getResolvedStackTraceColumnText(NativeStackCallInfo info, int index) { + switch (index) { + case 0: + return String.format("0x%08x", info.getAddress()); + case 1: + return info.getLibraryName(); + case 2: + return info.getMethodName(); + case 3: + return info.getSourceFile(); + case 4: + int l = info.getLineNumber(); + return l == -1 ? "" : Integer.toString(l); + } + + return null; + } + + private String getStackAddressColumnText(Long address, int index) { + if (index == 0) { + return String.format("0x%08x", address); + } + + return null; + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeSymbolResolverTask.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeSymbolResolverTask.java new file mode 100644 index 00000000..7e4fe0ee --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeSymbolResolverTask.java @@ -0,0 +1,443 @@ +/* + * Copyright (C) 2011 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.heap; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ddmlib.NativeAllocationInfo; +import com.android.ddmlib.NativeLibraryMapInfo; +import com.android.ddmlib.NativeStackCallInfo; +import com.android.ddmuilib.DdmUiPreferences; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jface.operation.IRunnableWithProgress; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.RandomAccessFile; +import java.io.OutputStreamWriter; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +/** + * A symbol resolver task that can resolve a set of addresses to their corresponding + * source method name + file name:line number. + * + * It first identifies the library that contains the address, and then runs addr2line on + * the library to get the symbol name + source location. + */ +public class NativeSymbolResolverTask implements IRunnableWithProgress { + private static final String ADDR2LINE; + private static final String ADDR2LINE64; + private static final String DEFAULT_SYMBOLS_FOLDER; + + private static final int ELF_CLASS32 = 1; + private static final int ELF_CLASS64 = 2; + private static final int ELF_DATA2LSB = 1; + private static final int ELF_PT_LOAD = 1; + + static { + String addr2lineEnv = System.getenv("ANDROID_ADDR2LINE"); + String addr2line64Env = System.getenv("ANDROID_ADDR2LINE64"); + ADDR2LINE = addr2lineEnv != null ? addr2lineEnv : DdmUiPreferences.getAddr2Line(); + ADDR2LINE64 = addr2line64Env != null ? addr2line64Env : DdmUiPreferences.getAddr2Line64(); + + String symbols = System.getenv("ANDROID_SYMBOLS"); + DEFAULT_SYMBOLS_FOLDER = symbols != null ? symbols : DdmUiPreferences.getSymbolDirectory(); + } + + private List mCallSites; + private List mMappedLibraries; + private List mSymbolSearchFolders; + + /** All unresolved addresses from all the callsites. */ + private SortedSet mUnresolvedAddresses; + + /** Set of all addresses that could were not resolved at the end of the resolution process. */ + private Set mUnresolvableAddresses; + + /** Map of library -> [unresolved addresses mapping to this library]. */ + private Map> mUnresolvedAddressesPerLibrary; + + /** Addresses that could not be mapped to a library, should be mostly empty. */ + private Set mUnmappedAddresses; + + /** Cache of the resolution for every unresolved address. */ + private Map mAddressResolution; + + /** List of libraries that were not located on disk. */ + private Set mNotFoundLibraries; + private String mAddr2LineErrorMessage = null; + + /** The addr2line command to use to resolve addresses. */ + private String mAddr2LineCmd; + + public NativeSymbolResolverTask(List callSites, + List mappedLibraries, + @NonNull String symbolSearchPath, + @Nullable String abi) { + mCallSites = callSites; + mMappedLibraries = mappedLibraries; + mSymbolSearchFolders = new ArrayList(); + mSymbolSearchFolders.add(DEFAULT_SYMBOLS_FOLDER); + mSymbolSearchFolders.addAll(Arrays.asList(symbolSearchPath.split(":"))); + + mUnresolvedAddresses = new TreeSet(); + mUnresolvableAddresses = new HashSet(); + mUnresolvedAddressesPerLibrary = new HashMap>(); + mUnmappedAddresses = new HashSet(); + mAddressResolution = new HashMap(); + mNotFoundLibraries = new HashSet(); + + if (abi == null || abi.startsWith("32")) { + mAddr2LineCmd = ADDR2LINE; + } else { + mAddr2LineCmd = ADDR2LINE64; + } + } + + @Override + public void run(IProgressMonitor monitor) + throws InvocationTargetException, InterruptedException { + monitor.beginTask("Resolving symbols", IProgressMonitor.UNKNOWN); + + collectAllUnresolvedAddresses(); + checkCancellation(monitor); + + mapUnresolvedAddressesToLibrary(); + checkCancellation(monitor); + + resolveLibraryAddresses(monitor); + checkCancellation(monitor); + + resolveCallSites(mCallSites); + + monitor.done(); + } + + private void collectAllUnresolvedAddresses() { + for (NativeAllocationInfo callSite : mCallSites) { + mUnresolvedAddresses.addAll(callSite.getStackCallAddresses()); + } + } + + private void mapUnresolvedAddressesToLibrary() { + Set mappedAddresses = new HashSet(); + + for (NativeLibraryMapInfo lib : mMappedLibraries) { + SortedSet addressesInLibrary = mUnresolvedAddresses.subSet(lib.getStartAddress(), + lib.getEndAddress() + 1); + if (addressesInLibrary.size() > 0) { + mUnresolvedAddressesPerLibrary.put(lib, addressesInLibrary); + mappedAddresses.addAll(addressesInLibrary); + } + } + + // unmapped addresses = unresolved addresses - mapped addresses + mUnmappedAddresses.addAll(mUnresolvedAddresses); + mUnmappedAddresses.removeAll(mappedAddresses); + } + + private void resolveLibraryAddresses(IProgressMonitor monitor) throws InterruptedException { + for (NativeLibraryMapInfo lib : mUnresolvedAddressesPerLibrary.keySet()) { + String libPath = getLibraryLocation(lib); + Set addressesToResolve = mUnresolvedAddressesPerLibrary.get(lib); + + if (libPath == null) { + mNotFoundLibraries.add(lib.getLibraryName()); + markAddressesNotResolvable(addressesToResolve, lib); + } else { + monitor.subTask(String.format("Resolving addresses mapped to %s.", libPath)); + resolveAddresses(lib, libPath, addressesToResolve); + } + + checkCancellation(monitor); + } + } + + private long unsigned(byte value, long shift) { + return ((long) value & 0xFF) << shift; + } + + private short elfGetHalfWord(RandomAccessFile file, long offset) throws IOException { + byte[] buf = new byte[2]; + file.seek(offset); + file.readFully(buf, 0, 2); + return (short) (unsigned(buf[0], 0) | unsigned(buf[1], 8)); + } + + private int elfGetWord(RandomAccessFile file, long offset) throws IOException { + byte[] buf = new byte[4]; + file.seek(offset); + file.readFully(buf, 0, 4); + return (int) (unsigned(buf[0], 0) | unsigned(buf[1], 8) | + unsigned(buf[2], 16) | unsigned(buf[3], 24)); + } + + private long elfGetDoubleWord(RandomAccessFile file, long offset) throws IOException { + byte[] buf = new byte[8]; + file.seek(offset); + file.readFully(buf, 0, 8); + return unsigned(buf[0], 0) | unsigned(buf[1], 8) | + unsigned(buf[2], 16) | unsigned(buf[3], 24) | + unsigned(buf[4], 32) | unsigned(buf[5], 40) | + unsigned(buf[6], 48) | unsigned(buf[7], 56); + } + + private long getLoadBase(String libPath) { + RandomAccessFile file; + try { + file = new RandomAccessFile(libPath, "r"); + } catch (FileNotFoundException e) { + return 0; + } + byte[] buffer = new byte[8]; + try { + file.readFully(buffer, 0, 6); + } catch (IOException e) { + return 0; + } + if (buffer[0] != 0x7f || buffer[1] != 'E' || buffer[2] != 'L' || + buffer[3] != 'F' || buffer[5] != ELF_DATA2LSB) { + return 0; + } + + boolean elf32; + long elfPhdrSize; + long ePhnumOffset; + long ePhoffOffset; + long pTypeOffset; + long pOffsetOffset; + long pVaddrOffset; + if (buffer[4] == ELF_CLASS32) { + // ELFCLASS32 + elf32 = true; + elfPhdrSize = 32; + + ePhnumOffset = 44; + ePhoffOffset = 28; + pTypeOffset = 0; + pOffsetOffset = 4; + pVaddrOffset = 8; + } else if (buffer[4] == ELF_CLASS64) { + // ELFCLASS64 + elf32 = false; + elfPhdrSize = 56; + + ePhnumOffset = 56; + ePhoffOffset = 32; + pTypeOffset = 0; + pOffsetOffset = 8; + pVaddrOffset = 16; + } else { + // Unknown class type. + return 0; + } + + try { + int ePhnum = elfGetHalfWord(file, ePhnumOffset); + long offset; + if (elf32) { + offset = elfGetWord(file, ePhoffOffset); + } else { + offset = elfGetDoubleWord(file, ePhoffOffset); + } + for (int i = 0; i < ePhnum; i++) { + int pType = elfGetWord(file, offset + pTypeOffset); + + long pOffset; + if (elf32) { + pOffset = elfGetWord(file, offset + pOffsetOffset); + } else { + pOffset = elfGetDoubleWord(file, offset + pOffsetOffset); + } + // Assume all offsets are zero. + if (pType == ELF_PT_LOAD && pOffset == 0) { + long pVaddr; + if (elf32) { + pVaddr = elfGetWord(file, offset + pVaddrOffset); + } else { + pVaddr = elfGetDoubleWord(file, offset + pVaddrOffset); + } + return pVaddr; + } + offset += elfPhdrSize; + } + } catch (IOException e) { + return 0; + } + return 0; + } + + private void resolveAddresses(NativeLibraryMapInfo lib, String libPath, + Set addressesToResolve) { + Process addr2line; + try { + addr2line = new ProcessBuilder(mAddr2LineCmd, + "-C", // demangle + "-f", // display function names in addition to file:number + "-e", libPath).start(); + } catch (IOException e) { + // Since the library path is known to be valid, the only reason for an exception + // is that addr2line was not found. We just save the message in this case. + mAddr2LineErrorMessage = e.getMessage(); + markAddressesNotResolvable(addressesToResolve, lib); + return; + } + + BufferedReader resultReader = new BufferedReader(new InputStreamReader( + addr2line.getInputStream())); + BufferedWriter addressWriter = new BufferedWriter(new OutputStreamWriter( + addr2line.getOutputStream())); + + long libStartAddress = isExecutable(lib) ? 0 : lib.getStartAddress(); + long libLoadBase = isExecutable(lib) ? 0 : getLoadBase(libPath); + try { + for (Long addr : addressesToResolve) { + long offset = addr - libStartAddress + libLoadBase; + addressWriter.write(Long.toHexString(offset)); + addressWriter.newLine(); + addressWriter.flush(); + String method = resultReader.readLine(); + String sourceFile = resultReader.readLine(); + + mAddressResolution.put(addr, + new NativeStackCallInfo(addr, + lib.getLibraryName(), + method, + sourceFile)); + } + } catch (IOException e) { + // if there is any error, then mark the addresses not already resolved + // as unresolvable. + for (Long addr : addressesToResolve) { + if (mAddressResolution.get(addr) == null) { + markAddressNotResolvable(lib, addr); + } + } + } + + try { + resultReader.close(); + addressWriter.close(); + } catch (IOException e) { + // we can ignore these exceptions + } + + addr2line.destroy(); + } + + private boolean isExecutable(NativeLibraryMapInfo object) { + // TODO: Use a tool like readelf or nm to determine whether this object is a library + // or an executable. + // For now, we'll just assume that any object present in the bin folder is an executable. + String devicePath = object.getLibraryName(); + return devicePath.contains("/bin/"); + } + + private void markAddressesNotResolvable(Set addressesToResolve, + NativeLibraryMapInfo lib) { + for (Long addr : addressesToResolve) { + markAddressNotResolvable(lib, addr); + } + } + + private void markAddressNotResolvable(NativeLibraryMapInfo lib, Long addr) { + mAddressResolution.put(addr, + new NativeStackCallInfo(addr, + lib.getLibraryName(), + Long.toHexString(addr), + "")); + mUnresolvableAddresses.add(addr); + } + + /** + * Locate on local disk the debug library w/ symbols corresponding to the + * library on the device. It searches for this library in the symbol path. + * @return absolute path if found, null otherwise + */ + private String getLibraryLocation(NativeLibraryMapInfo lib) { + String pathOnDevice = lib.getLibraryName(); + String libName = new File(pathOnDevice).getName(); + + for (String p : mSymbolSearchFolders) { + // try appending the full path on device + String fullPath = p + File.separator + pathOnDevice; + if (new File(fullPath).exists()) { + return fullPath; + } + + // try appending basename(library) + fullPath = p + File.separator + libName; + if (new File(fullPath).exists()) { + return fullPath; + } + } + + return null; + } + + private void resolveCallSites(List callSites) { + for (NativeAllocationInfo callSite : callSites) { + List stackInfo = new ArrayList(); + + for (Long addr : callSite.getStackCallAddresses()) { + NativeStackCallInfo info = mAddressResolution.get(addr); + + if (info != null) { + stackInfo.add(info); + } + } + + callSite.setResolvedStackCall(stackInfo); + } + } + + private void checkCancellation(IProgressMonitor monitor) throws InterruptedException { + if (monitor.isCanceled()) { + throw new InterruptedException(); + } + } + + public String getAddr2LineErrorMessage() { + return mAddr2LineErrorMessage; + } + + public Set getUnmappedAddresses() { + return mUnmappedAddresses; + } + + public Set getUnresolvableAddresses() { + return mUnresolvableAddresses; + } + + public Set getNotFoundLibraries() { + return mNotFoundLibraries; + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/CoordinateControls.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/CoordinateControls.java new file mode 100644 index 00000000..f6c59b0a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/CoordinateControls.java @@ -0,0 +1,249 @@ +/* + * 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; + +import java.text.DecimalFormat; +import java.text.ParseException; + +/** + * 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; + private final DecimalFormat mDecimalFormat = new DecimalFormat(); + + /** 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() { + @Override + public void modifyText(ModifyEvent event) { + if (mManualTextChange > 0) { + return; + } + try { + mValue = getValueFromSexagesimalControls(); + setValueIntoDecimalControl(mValue); + mValueValidity = true; + } catch (ParseException 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() { + @Override + public void modifyText(ModifyEvent event) { + if (mManualTextChange > 0) { + return; + } + try { + mValue = mDecimalFormat.parse(mDecimalText.getText()).doubleValue(); + setValueIntoSexagesimalControl(mValue); + mValueValidity = true; + } catch (ParseException 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 ParseException { + double degrees = mDecimalFormat.parse(mSexagesimalDegreeText.getText()).doubleValue(); + double minutes = mDecimalFormat.parse(mSexagesimalMinuteText.getText()).doubleValue(); + double seconds = mDecimalFormat.parse(mSexagesimalSecondText.getText()).doubleValue(); + + 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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/GpxParser.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/GpxParser.java new file mode 100644 index 00000000..bb4da6c6 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/KmlParser.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/KmlParser.java new file mode 100644 index 00000000..f04200fe --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/LocationPoint.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/LocationPoint.java new file mode 100644 index 00000000..d4f15f01 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/TrackContentProvider.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/TrackContentProvider.java new file mode 100644 index 00000000..dd2f10ce --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/TrackContentProvider.java @@ -0,0 +1,48 @@ +/* + * 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 { + + @Override + public Object[] getElements(Object inputElement) { + if (inputElement instanceof Track[]) { + return (Track[])inputElement; + } + + return new Object[0]; + } + + @Override + public void dispose() { + // pass + } + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/TrackLabelProvider.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/TrackLabelProvider.java new file mode 100644 index 00000000..c1ce106e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/TrackLabelProvider.java @@ -0,0 +1,87 @@ +/* + * 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 { + + @Override + public Image getColumnImage(Object element, int columnIndex) { + return null; + } + + @Override + 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; + } + + @Override + public void addListener(ILabelProviderListener listener) { + // pass + } + + @Override + public void dispose() { + // pass + } + + @Override + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + @Override + public void removeListener(ILabelProviderListener listener) { + // pass + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/TrackPoint.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/TrackPoint.java new file mode 100644 index 00000000..ef8a3170 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/WayPoint.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/WayPoint.java new file mode 100644 index 00000000..527d522f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/WayPointContentProvider.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/WayPointContentProvider.java new file mode 100644 index 00000000..b60062fb --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/WayPointContentProvider.java @@ -0,0 +1,46 @@ +/* + * 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 { + + @Override + public Object[] getElements(Object inputElement) { + if (inputElement instanceof WayPoint[]) { + return (WayPoint[])inputElement; + } + + return new Object[0]; + } + + @Override + public void dispose() { + // pass + } + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/WayPointLabelProvider.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/WayPointLabelProvider.java new file mode 100644 index 00000000..a8a75aa3 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/location/WayPointLabelProvider.java @@ -0,0 +1,79 @@ +/* + * 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 { + + @Override + public Image getColumnImage(Object element, int columnIndex) { + return null; + } + + @Override + 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; + } + + @Override + public void addListener(ILabelProviderListener listener) { + // pass + } + + @Override + public void dispose() { + // pass + } + + @Override + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + @Override + public void removeListener(ILabelProviderListener listener) { + // pass + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/BugReportImporter.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/BugReportImporter.java new file mode 100644 index 00000000..55e64d04 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/BugReportImporter.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.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) { + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException ignore) { + } + } + } + } + + 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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayFilteredLog.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayFilteredLog.java new file mode 100644 index 00000000..5b7f40db --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayGraph.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayGraph.java new file mode 100644 index 00000000..e6d6509e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayLog.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayLog.java new file mode 100644 index 00000000..89a145f6 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplayLog.java @@ -0,0 +1,381 @@ +/* + * 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 = 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 = 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() { + @Override + 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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySync.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySync.java new file mode 100644 index 00000000..216c6069 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySync.java @@ -0,0 +1,305 @@ +/* + * 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]; + + @SuppressWarnings("unchecked") + List mTooltipsSyncTmp[] = new List[NUM_AUTHS]; + mTooltipsSync = mTooltipsSyncTmp; + + 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 = 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)"); + } + scanner.close(); + 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 = 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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySyncHistogram.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySyncHistogram.java new file mode 100644 index 00000000..12cd9490 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySyncHistogram.java @@ -0,0 +1,181 @@ +/* + * 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]; + + @SuppressWarnings("unchecked") + Map mTimePeriodMapTmp[] = new HashMap[NUM_AUTHS + 1]; + mTimePeriodMap = mTimePeriodMapTmp; + + 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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySyncPerf.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySyncPerf.java new file mode 100644 index 00000000..e573dba3 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/DisplaySyncPerf.java @@ -0,0 +1,227 @@ +/* + * 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(); + + @SuppressWarnings("unchecked") + List[] mTooltipsTmp = new List[NUM_SERIES]; + mTooltips = mTooltipsTmp; + + 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 = 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 = 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 (NumberFormatException e) { + // This can happen when parsing events from froyo+ where the event with id 52000 + // as a completely different format. For now, skip this event if this happens. + } 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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventDisplay.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventDisplay.java new file mode 100644 index 00000000..8ed1ce58 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventDisplay.java @@ -0,0 +1,975 @@ +/* + * 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) { + throw new InvalidParameterException(); + } + + mPidFilterList = pids; + } + + ArrayList getPidFilterList() { + return mPidFilterList; + } + + void addPidFiler(int pid) { + if (mPidFiltering == false) { + throw 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() { + @Override + 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() { + @Override + 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() { + @Override + 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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventDisplayOptions.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventDisplayOptions.java new file mode 100644 index 00000000..33cdc0e9 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventDisplayOptions.java @@ -0,0 +1,961 @@ +/* + * 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.ImageLoader; +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 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(Shell parent) { + super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL); + } + + /** + * 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() { + @Override + 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; + + ImageLoader loader = ImageLoader.getDdmUiLibLoader(); + mEventDisplayNewButton = new Button(bottomControls, SWT.PUSH | SWT.FLAT); + mEventDisplayNewButton.setImage(loader.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(loader.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(loader.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(loader.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() { + @Override + 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() { + @Override + 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() { + @Override + 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() { + @Override + 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() { + @Override + 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() { + @Override + 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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventLogImporter.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventLogImporter.java new file mode 100644 index 00000000..3211283e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventLogImporter.java @@ -0,0 +1,95 @@ +/* + * 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) { + } finally { + if (tagReader != null) { + try { + tagReader.close(); + } catch (IOException ignore) { + } + } + if (eventReader != null) { + try { + eventReader.close(); + } catch (IOException ignore) { + } + } + } + } + + 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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventLogPanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventLogPanel.java new file mode 100644 index 00000000..682971b5 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventLogPanel.java @@ -0,0 +1,938 @@ +/* + * 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.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 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() { + super(); + 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() { + @Override + public void run() { + openOptionPanel(); + } + }); + + mClearAction = clearAction; + mClearAction.setRunnable(new Runnable() { + @Override + public void run() { + clearLog(); + } + }); + + mSaveAction = saveAction; + mSaveAction.setRunnable(new Runnable() { + @Override + 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() { + @Override + 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() { + @Override + 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(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) + */ + @Override + 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() { + @Override + 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() { + @Override + 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) + */ + @Override + @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() { + @Override + 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) + */ + @Override + 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() { + @Override + 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. + } + } + + @Override + @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); + try { + int count; + while ((count = fis.read(buffer)) != -1) { + logReceiver.parseNewData(buffer, 0, count); + } + } finally { + fis.close(); + } + } + + @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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventValueSelector.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventValueSelector.java new file mode 100644 index 00000000..6361d6b0 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventValueSelector.java @@ -0,0 +1,630 @@ +/* + * 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.CompareMethod; +import com.android.ddmlib.log.EventContainer.EventValueType; +import com.android.ddmlib.log.EventLogParser; +import com.android.ddmlib.log.EventValueDescription; +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() { + @Override + 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() { + @Override + 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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/OccurrenceRenderer.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/OccurrenceRenderer.java new file mode 100644 index 00000000..f3d8ece9 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/SyncCommon.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/SyncCommon.java new file mode 100644 index 00000000..729122e0 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/SyncCommon.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.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) || + "com.android.calendar".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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/EditFilterDialog.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/EditFilterDialog.java new file mode 100644 index 00000000..1b3da726 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/EditFilterDialog.java @@ -0,0 +1,397 @@ +/* + * 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.ImageLoader; + +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 = 260; + + private static final String IMAGE_WARNING = "warning.png"; //$NON-NLS-1$ + private static final String IMAGE_EMPTY = "empty.png"; //$NON-NLS-1$ + + private Shell mParent; + + private Shell mShell; + + private boolean mOk = false; + + /** + * 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 mNameWarning; + private Label mTagWarning; + private Label mPidWarning; + + public EditFilterDialog(Shell parent) { + super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL); + } + + public EditFilterDialog(Shell shell, LogFilter filter) { + this(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() { + @Override + 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(3, 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() { + @Override + public void modifyText(ModifyEvent e) { + mName = filterNameText.getText().trim(); + validate(); + } + }); + + mNameWarning = new Label(nameComposite, SWT.NONE); + mNameWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_EMPTY, + mShell.getDisplay())); + + // 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); + } + } + + tagText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + tagText.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent e) { + mTag = tagText.getText().trim(); + validate(); + } + }); + + mTagWarning = new Label(main, SWT.NONE); + mTagWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_EMPTY, + mShell.getDisplay())); + + 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() { + @Override + public void modifyText(ModifyEvent e) { + mPid = pidText.getText().trim(); + validate(); + } + }); + + mPidWarning = new Label(main, SWT.NONE); + mPidWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_EMPTY, + 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); + GridData 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() { + + boolean result = true; + + // then we check it only contains digits. + if (mPid != null) { + if (mPid.matches("[0-9]*") == false) { //$NON-NLS-1$ + mPidWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage( + IMAGE_WARNING, + mShell.getDisplay())); + mPidWarning.setToolTipText("PID must be a number"); //$NON-NLS-1$ + result = false; + } else { + mPidWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage( + IMAGE_EMPTY, + mShell.getDisplay())); + mPidWarning.setToolTipText(null); + } + } + + // then we check it not contains character | or : + if (mTag != null) { + if (mTag.matches(".*[:|].*") == true) { //$NON-NLS-1$ + mTagWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage( + IMAGE_WARNING, + mShell.getDisplay())); + mTagWarning.setToolTipText("Tag cannot contain | or :"); //$NON-NLS-1$ + result = false; + } else { + mTagWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage( + IMAGE_EMPTY, + mShell.getDisplay())); + mTagWarning.setToolTipText(null); + } + } + + // then we check it not contains character | or : + if (mName != null && mName.length() > 0) { + if (mName.matches(".*[:|].*") == true) { //$NON-NLS-1$ + mNameWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage( + IMAGE_WARNING, + mShell.getDisplay())); + mNameWarning.setToolTipText("Name cannot contain | or :"); //$NON-NLS-1$ + result = false; + } else { + mNameWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage( + IMAGE_EMPTY, + mShell.getDisplay())); + mNameWarning.setToolTipText(null); + } + } else { + result = false; + } + + mOkButton.setEnabled(result); + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/ILogCatBufferChangeListener.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/ILogCatBufferChangeListener.java new file mode 100644 index 00000000..db586cca --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/ILogCatBufferChangeListener.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2011 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.logcat.LogCatMessage; + +import java.util.List; + +/** + * Listeners interested in changes in the logcat buffer should implement this interface. + */ +public interface ILogCatBufferChangeListener { + /** + * Called when the logcat buffer changes. + * @param addedMessages list of messages that were added to the logcat buffer + * @param deletedMessages list of messages that were removed from the logcat buffer + */ + void bufferChanged(List addedMessages, List deletedMessages); +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/ILogCatMessageSelectionListener.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/ILogCatMessageSelectionListener.java new file mode 100644 index 00000000..60c6d054 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/ILogCatMessageSelectionListener.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2011 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.logcat.LogCatMessage; + +/** + * Classes interested in listening to user selection of logcat + * messages should implement this interface. + */ +public interface ILogCatMessageSelectionListener { + void messageDoubleClicked(LogCatMessage m); +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterContentProvider.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterContentProvider.java new file mode 100644 index 00000000..329c2029 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterContentProvider.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2011 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.logcat.LogCatFilter; + +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.Viewer; + +import java.util.List; + +/** + * A JFace content provider for logcat filter list, used in {@link LogCatPanel}. + */ +public final class LogCatFilterContentProvider implements IStructuredContentProvider { + @Override + public void dispose() { + } + + @Override + public void inputChanged(Viewer arg0, Object arg1, Object arg2) { + } + + /** + * Obtain the list of filters currently in use. + * @param model list of {@link LogCatFilter}'s + * @return array of {@link LogCatFilter} objects, or null. + */ + @Override + public Object[] getElements(Object model) { + return ((List) model).toArray(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterData.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterData.java new file mode 100644 index 00000000..cb34eeb4 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterData.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2013 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.logcat.LogCatFilter; +import com.android.ddmlib.logcat.LogCatMessage; + +import java.util.List; + +public class LogCatFilterData { + private final LogCatFilter mFilter; + + /** Indicates the number of messages that match this filter, but have not + * yet been read by the user. This is really metadata about this filter + * necessary for the UI. If we ever end up needing to store more metadata, + * then it is probably better to move it out into a separate class. */ + private int mUnreadCount; + + /** Indicates that this filter is transient, and should not be persisted + * across Eclipse sessions. */ + private boolean mTransient; + + public LogCatFilterData(LogCatFilter f) { + mFilter = f; + + // By default, all filters are persistent. Transient filters should explicitly + // mark it so by calling setTransient. + mTransient = false; + } + + /** + * Update the unread count based on new messages received. The unread count + * is incremented by the count of messages in the received list that will be + * accepted by this filter. + * @param newMessages list of new messages. + */ + public void updateUnreadCount(List newMessages) { + for (LogCatMessage m : newMessages) { + if (mFilter.matches(m)) { + mUnreadCount++; + } + } + } + + /** + * Reset count of unread messages. + */ + public void resetUnreadCount() { + mUnreadCount = 0; + } + + /** + * Get current value for the unread message counter. + */ + public int getUnreadCount() { + return mUnreadCount; + } + + /** Make this filter transient: It will not be persisted across sessions. */ + public void setTransient() { + mTransient = true; + } + + public boolean isTransient() { + return mTransient; + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterLabelProvider.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterLabelProvider.java new file mode 100644 index 00000000..4aa6d80a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterLabelProvider.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2011 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.logcat.LogCatFilter; + +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.jface.viewers.LabelProvider; +import org.eclipse.swt.graphics.Image; + +import java.util.Map; + +/** + * A JFace label provider for the LogCat filters. It expects elements of type + * {@link LogCatFilter}. + */ +public final class LogCatFilterLabelProvider extends LabelProvider implements ITableLabelProvider { + private Map mFilterData; + + public LogCatFilterLabelProvider(Map filterData) { + mFilterData = filterData; + } + + @Override + public Image getColumnImage(Object arg0, int arg1) { + return null; + } + + /** + * Implements {@link ITableLabelProvider#getColumnText(Object, int)}. + * @param element an instance of {@link LogCatFilter} + * @param index index of the column + * @return text to use in the column + */ + @Override + public String getColumnText(Object element, int index) { + if (!(element instanceof LogCatFilter)) { + return null; + } + + LogCatFilter f = (LogCatFilter) element; + LogCatFilterData fd = mFilterData.get(f); + + if (fd != null && fd.getUnreadCount() > 0) { + return String.format("%s (%d)", f.getName(), fd.getUnreadCount()); + } else { + return f.getName(); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterSettingsDialog.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterSettingsDialog.java new file mode 100644 index 00000000..8b8238cc --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterSettingsDialog.java @@ -0,0 +1,327 @@ +/* + * Copyright (C) 2011 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.LogLevel; + +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.dialogs.TitleAreaDialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +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.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/** + * Dialog used to create or edit settings for a logcat filter. + */ +public final class LogCatFilterSettingsDialog extends TitleAreaDialog { + private static final String TITLE = "Logcat Message Filter Settings"; + private static final String DEFAULT_MESSAGE = + "Filter logcat messages by the source's tag, pid or minimum log level.\n" + + "Empty fields will match all messages."; + + private String mFilterName; + private String mTag; + private String mText; + private String mPid; + private String mAppName; + private String mLogLevel; + + private Text mFilterNameText; + private Text mTagFilterText; + private Text mTextFilterText; + private Text mPidFilterText; + private Text mAppNameFilterText; + private Combo mLogLevelCombo; + private Button mOkButton; + + /** + * Construct the filter settings dialog with default values for all fields. + * @param parentShell . + */ + public LogCatFilterSettingsDialog(Shell parentShell) { + super(parentShell); + setDefaults("", "", "", "", "", LogLevel.VERBOSE); + } + + /** + * Set the default values to show when the dialog is opened. + * @param filterName name for the filter. + * @param tag value for filter by tag + * @param text value for filter by text + * @param pid value for filter by pid + * @param appName value for filter by app name + * @param level value for filter by log level + */ + public void setDefaults(String filterName, String tag, String text, String pid, String appName, + LogLevel level) { + mFilterName = filterName; + mTag = tag; + mText = text; + mPid = pid; + mAppName = appName; + mLogLevel = level.getStringValue(); + } + + @Override + protected Control createDialogArea(Composite shell) { + setTitle(TITLE); + setMessage(DEFAULT_MESSAGE); + + Composite parent = (Composite) super.createDialogArea(shell); + Composite c = new Composite(parent, SWT.BORDER); + c.setLayout(new GridLayout(2, false)); + c.setLayoutData(new GridData(GridData.FILL_BOTH)); + + createLabel(c, "Filter Name:"); + mFilterNameText = new Text(c, SWT.BORDER); + mFilterNameText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mFilterNameText.setText(mFilterName); + + createSeparator(c); + + createLabel(c, "by Log Tag:"); + mTagFilterText = new Text(c, SWT.BORDER); + mTagFilterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mTagFilterText.setText(mTag); + + createLabel(c, "by Log Message:"); + mTextFilterText = new Text(c, SWT.BORDER); + mTextFilterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mTextFilterText.setText(mText); + + createLabel(c, "by PID:"); + mPidFilterText = new Text(c, SWT.BORDER); + mPidFilterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mPidFilterText.setText(mPid); + + createLabel(c, "by Application Name:"); + mAppNameFilterText = new Text(c, SWT.BORDER); + mAppNameFilterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mAppNameFilterText.setText(mAppName); + + createLabel(c, "by Log Level:"); + mLogLevelCombo = new Combo(c, SWT.READ_ONLY | SWT.DROP_DOWN); + mLogLevelCombo.setItems(getLogLevels().toArray(new String[0])); + mLogLevelCombo.select(getLogLevels().indexOf(mLogLevel)); + + /* call validateDialog() whenever user modifies any text field */ + ModifyListener m = new ModifyListener() { + @Override + public void modifyText(ModifyEvent arg0) { + DialogStatus status = validateDialog(); + mOkButton.setEnabled(status.valid); + setErrorMessage(status.message); + } + }; + mFilterNameText.addModifyListener(m); + mTagFilterText.addModifyListener(m); + mTextFilterText.addModifyListener(m); + mPidFilterText.addModifyListener(m); + mAppNameFilterText.addModifyListener(m); + + return c; + } + + + @Override + protected void createButtonsForButtonBar(Composite parent) { + super.createButtonsForButtonBar(parent); + + mOkButton = getButton(IDialogConstants.OK_ID); + + DialogStatus status = validateDialog(); + mOkButton.setEnabled(status.valid); + } + + /** + * A tuple that specifies whether the current state of the inputs + * on the dialog is valid or not. If it is not valid, the message + * field stores the reason why it isn't. + */ + private static final class DialogStatus { + final boolean valid; + final String message; + + private DialogStatus(boolean isValid, String errMessage) { + valid = isValid; + message = errMessage; + } + } + + private DialogStatus validateDialog() { + /* check that there is some name for the filter */ + if (mFilterNameText.getText().trim().equals("")) { + return new DialogStatus(false, + "Please provide a name for this filter."); + } + + /* if a pid is provided, it should be a +ve integer */ + String pidText = mPidFilterText.getText().trim(); + if (pidText.trim().length() > 0) { + int pid = 0; + try { + pid = Integer.parseInt(pidText); + } catch (NumberFormatException e) { + return new DialogStatus(false, + "PID should be a positive integer."); + } + + if (pid < 0) { + return new DialogStatus(false, + "PID should be a positive integer."); + } + } + + /* tag field must use a valid regex pattern */ + String tagText = mTagFilterText.getText().trim(); + if (tagText.trim().length() > 0) { + try { + Pattern.compile(tagText); + } catch (PatternSyntaxException e) { + return new DialogStatus(false, + "Invalid regex used in tag field: " + e.getMessage()); + } + } + + /* text field must use a valid regex pattern */ + String messageText = mTextFilterText.getText().trim(); + if (messageText.trim().length() > 0) { + try { + Pattern.compile(messageText); + } catch (PatternSyntaxException e) { + return new DialogStatus(false, + "Invalid regex used in text field: " + e.getMessage()); + } + } + + /* app name field must use a valid regex pattern */ + String appNameText = mAppNameFilterText.getText().trim(); + if (appNameText.trim().length() > 0) { + try { + Pattern.compile(appNameText); + } catch (PatternSyntaxException e) { + return new DialogStatus(false, + "Invalid regex used in application name field: " + e.getMessage()); + } + } + + return new DialogStatus(true, null); + } + + private void createSeparator(Composite c) { + Label l = new Label(c, SWT.SEPARATOR | SWT.HORIZONTAL); + GridData gd = new GridData(GridData.FILL_HORIZONTAL); + gd.horizontalSpan = 2; + l.setLayoutData(gd); + } + + private void createLabel(Composite c, String text) { + Label l = new Label(c, SWT.NONE); + l.setText(text); + GridData gd = new GridData(); + gd.horizontalAlignment = SWT.RIGHT; + l.setLayoutData(gd); + } + + @Override + protected void okPressed() { + /* save values from the widgets before the shell is closed. */ + mFilterName = mFilterNameText.getText(); + mTag = mTagFilterText.getText(); + mText = mTextFilterText.getText(); + mLogLevel = mLogLevelCombo.getText(); + mPid = mPidFilterText.getText(); + mAppName = mAppNameFilterText.getText(); + + super.okPressed(); + } + + /** + * Obtain the name for this filter. + * @return user provided filter name, maybe empty. + */ + public String getFilterName() { + return mFilterName; + } + + /** + * Obtain the tag regex to filter by. + * @return user provided tag regex, maybe empty. + */ + public String getTag() { + return mTag; + } + + /** + * Obtain the text regex to filter by. + * @return user provided tag regex, maybe empty. + */ + public String getText() { + return mText; + } + + /** + * Obtain user provided PID to filter by. + * @return user provided pid, maybe empty. + */ + public String getPid() { + return mPid; + } + + /** + * Obtain user provided application name to filter by. + * @return user provided app name regex, maybe empty + */ + public String getAppName() { + return mAppName; + } + + /** + * Obtain log level to filter by. + * @return log level string. + */ + public String getLogLevel() { + return mLogLevel; + } + + /** + * Obtain the string representation of all supported log levels. + * @return an array of strings, each representing a certain log level. + */ + public static List getLogLevels() { + List logLevels = new ArrayList(); + + for (LogLevel l : LogLevel.values()) { + logLevels.add(l.getStringValue()); + } + + return logLevels; + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterSettingsSerializer.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterSettingsSerializer.java new file mode 100644 index 00000000..d65e7f87 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatFilterSettingsSerializer.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2011 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.LogLevel; +import com.android.ddmlib.logcat.LogCatFilter; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Class to help save/restore user created filters. + * + * Users can create multiple filters in the logcat view. These filters could have regexes + * in their settings. All of the user created filters are saved into a single Eclipse + * preference. This class helps in generating the string to be saved given a list of + * {@link LogCatFilter}'s, and also does the reverse of creating the list of filters + * given the encoded string. + */ +public final class LogCatFilterSettingsSerializer { + private static final char SINGLE_QUOTE = '\''; + private static final char ESCAPE_CHAR = '\\'; + + private static final String ATTR_DELIM = ", "; + private static final String KW_DELIM = ": "; + + private static final String KW_NAME = "name"; + private static final String KW_TAG = "tag"; + private static final String KW_TEXT = "text"; + private static final String KW_PID = "pid"; + private static final String KW_APP = "app"; + private static final String KW_LOGLEVEL = "level"; + + /** + * Encode the settings from a list of {@link LogCatFilter}'s into a string for saving to + * the preference store. See + * {@link LogCatFilterSettingsSerializer#decodeFromPreferenceString(String)} for the + * reverse operation. + * @param filters list of filters to save. + * @param filterData mapping from filter to per filter UI data + * @return an encoded string that can be saved in Eclipse preference store. The encoded string + * is of a list of key:'value' pairs. + */ + public String encodeToPreferenceString(List filters, + Map filterData) { + StringBuffer sb = new StringBuffer(); + + for (LogCatFilter f : filters) { + LogCatFilterData fd = filterData.get(f); + if (fd != null && fd.isTransient()) { + // do not persist transient filters + continue; + } + + sb.append(KW_NAME); sb.append(KW_DELIM); sb.append(quoteString(f.getName())); + sb.append(ATTR_DELIM); + sb.append(KW_TAG); sb.append(KW_DELIM); sb.append(quoteString(f.getTag())); + sb.append(ATTR_DELIM); + sb.append(KW_TEXT); sb.append(KW_DELIM); sb.append(quoteString(f.getText())); + sb.append(ATTR_DELIM); + sb.append(KW_PID); sb.append(KW_DELIM); sb.append(quoteString(f.getPid())); + sb.append(ATTR_DELIM); + sb.append(KW_APP); sb.append(KW_DELIM); sb.append(quoteString(f.getAppName())); + sb.append(ATTR_DELIM); + sb.append(KW_LOGLEVEL); sb.append(KW_DELIM); + sb.append(quoteString(f.getLogLevel().getStringValue())); + sb.append(ATTR_DELIM); + } + return sb.toString(); + } + + /** + * Decode an encoded string representing the settings of a list of logcat + * filters into a list of {@link LogCatFilter}'s. + * @param pref encoded preference string + * @return a list of {@link LogCatFilter} + */ + public List decodeFromPreferenceString(String pref) { + List fs = new ArrayList(); + + /* first split the string into a list of key, value pairs */ + List kv = getKeyValues(pref); + if (kv.size() == 0) { + return fs; + } + + /* construct filter settings from the key value pairs */ + int index = 0; + while (index < kv.size()) { + String name = ""; + String tag = ""; + String pid = ""; + String app = ""; + String text = ""; + LogLevel level = LogLevel.VERBOSE; + + assert kv.get(index).equals(KW_NAME); + name = kv.get(index + 1); + + index += 2; + while (index < kv.size() && !kv.get(index).equals(KW_NAME)) { + String key = kv.get(index); + String value = kv.get(index + 1); + index += 2; + + if (key.equals(KW_TAG)) { + tag = value; + } else if (key.equals(KW_TEXT)) { + text = value; + } else if (key.equals(KW_PID)) { + pid = value; + } else if (key.equals(KW_APP)) { + app = value; + } else if (key.equals(KW_LOGLEVEL)) { + level = LogLevel.getByString(value); + } + } + + fs.add(new LogCatFilter(name, tag, text, pid, app, level)); + } + + return fs; + } + + private List getKeyValues(String pref) { + List kv = new ArrayList(); + int index = 0; + while (index < pref.length()) { + String kw = getKeyword(pref.substring(index)); + if (kw == null) { + break; + } + index += kw.length() + KW_DELIM.length(); + + String value = getNextString(pref.substring(index)); + index += value.length() + ATTR_DELIM.length(); + + value = unquoteString(value); + + kv.add(kw); + kv.add(value); + } + + return kv; + } + + /** + * Enclose a string in quotes, escaping all the quotes within the string. + */ + private String quoteString(String s) { + return SINGLE_QUOTE + s.replace(Character.toString(SINGLE_QUOTE), "\\'") + + SINGLE_QUOTE; + } + + /** + * Recover original string from its escaped version created using + * {@link LogCatFilterSettingsSerializer#quoteString(String)}. + */ + private String unquoteString(String s) { + s = s.substring(1, s.length() - 1); /* remove start and end QUOTES */ + return s.replace("\\'", Character.toString(SINGLE_QUOTE)); + } + + private String getKeyword(String pref) { + int kwlen = pref.indexOf(KW_DELIM); + if (kwlen == -1) { + return null; + } + + return pref.substring(0, kwlen); + } + + /** + * Get the next quoted string from the input stream of characters. + */ + private String getNextString(String s) { + assert s.charAt(0) == SINGLE_QUOTE; + + StringBuffer sb = new StringBuffer(); + + int index = 0; + while (index < s.length()) { + sb.append(s.charAt(index)); + + if (index > 0 + && s.charAt(index) == SINGLE_QUOTE // current char is a single quote + && s.charAt(index - 1) != ESCAPE_CHAR) { // prev char wasn't a backslash + /* break if an unescaped SINGLE QUOTE (end of string) is seen */ + break; + } + + index++; + } + + return sb.toString(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatMessageList.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatMessageList.java new file mode 100644 index 00000000..eaaaa90c --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatMessageList.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2011 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.logcat.LogCatMessage; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; + +/** + * Container for a list of log messages. The list of messages are + * maintained in a circular buffer (FIFO). + */ +public final class LogCatMessageList { + /** Preference key for size of the FIFO. */ + public static final String MAX_MESSAGES_PREFKEY = + "logcat.messagelist.max.size"; + + /** Default value for max # of messages. */ + public static final int MAX_MESSAGES_DEFAULT = 5000; + + private int mFifoSize; + private BlockingQueue mQ; + + /** + * Construct an empty message list. + * @param maxMessages capacity of the circular buffer + */ + public LogCatMessageList(int maxMessages) { + mFifoSize = maxMessages; + + mQ = new ArrayBlockingQueue(mFifoSize); + } + + /** + * Resize the message list. + * @param n new size for the list + */ + public synchronized void resize(int n) { + mFifoSize = n; + + if (mFifoSize > mQ.size()) { + /* if resizing to a bigger fifo, we can copy over all elements from the current mQ */ + mQ = new ArrayBlockingQueue(mFifoSize, true, mQ); + } else { + /* for a smaller fifo, copy over the last n entries */ + LogCatMessage[] curMessages = mQ.toArray(new LogCatMessage[mQ.size()]); + mQ = new ArrayBlockingQueue(mFifoSize); + for (int i = curMessages.length - mFifoSize; i < curMessages.length; i++) { + mQ.offer(curMessages[i]); + } + } + } + + /** + * Append a message to the list. If the list is full, the first + * message will be popped off of it. + * @param m log to be inserted + */ + public synchronized void appendMessages(final List messages) { + ensureSpace(messages.size()); + for (LogCatMessage m: messages) { + mQ.offer(m); + } + } + + /** + * Ensure that there is sufficient space for given number of messages. + * @return list of messages that were deleted to create additional space. + */ + public synchronized List ensureSpace(int messageCount) { + List l = new ArrayList(messageCount); + + while (mQ.remainingCapacity() < messageCount) { + l.add(mQ.poll()); + } + + return l; + } + + /** + * Returns the number of additional elements that this queue can + * ideally (in the absence of memory or resource constraints) + * accept without blocking. + * @return the remaining capacity + */ + public synchronized int remainingCapacity() { + return mQ.remainingCapacity(); + } + + /** Clear all messages in the list. */ + public synchronized void clear() { + mQ.clear(); + } + + /** Obtain a copy of the message list. */ + public synchronized List getAllMessages() { + return new ArrayList(mQ); + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatPanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatPanel.java new file mode 100644 index 00000000..61c07dfd --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatPanel.java @@ -0,0 +1,1607 @@ +/* + * Copyright (C) 2011 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.DdmConstants; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.Log.LogLevel; +import com.android.ddmlib.logcat.LogCatFilter; +import com.android.ddmlib.logcat.LogCatMessage; +import com.android.ddmuilib.AbstractBufferFindTarget; +import com.android.ddmuilib.FindDialog; +import com.android.ddmuilib.ITableFocusListener; +import com.android.ddmuilib.ITableFocusListener.IFocusedTableActivator; +import com.android.ddmuilib.ImageLoader; +import com.android.ddmuilib.SelectionDependentPanel; +import com.android.ddmuilib.TableHelper; + +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.MenuManager; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.preference.PreferenceConverter; +import org.eclipse.jface.util.IPropertyChangeListener; +import org.eclipse.jface.util.PropertyChangeEvent; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.SashForm; +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.dnd.TextTransfer; +import org.eclipse.swt.dnd.Transfer; +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.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.events.SelectionListener; +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.Point; +import org.eclipse.swt.graphics.RGB; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +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.Menu; +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 org.eclipse.swt.widgets.Text; +import org.eclipse.swt.widgets.ToolBar; +import org.eclipse.swt.widgets.ToolItem; + +import java.io.BufferedWriter; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/** + * LogCatPanel displays a table listing the logcat messages. + */ +public final class LogCatPanel extends SelectionDependentPanel + implements ILogCatBufferChangeListener { + /** Preference key to use for storing list of logcat filters. */ + public static final String LOGCAT_FILTERS_LIST = "logcat.view.filters.list"; + + /** Preference key to use for storing font settings. */ + public static final String LOGCAT_VIEW_FONT_PREFKEY = "logcat.view.font"; + + /** Preference key to use for deciding whether to automatically en/disable scroll lock. */ + public static final String AUTO_SCROLL_LOCK_PREFKEY = "logcat.view.auto-scroll-lock"; + + // Preference keys for message colors based on severity level + private static final String MSG_COLOR_PREFKEY_PREFIX = "logcat.msg.color."; + public static final String VERBOSE_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "verbose"; //$NON-NLS-1$ + public static final String DEBUG_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "debug"; //$NON-NLS-1$ + public static final String INFO_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "info"; //$NON-NLS-1$ + public static final String WARN_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "warn"; //$NON-NLS-1$ + public static final String ERROR_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "error"; //$NON-NLS-1$ + public static final String ASSERT_COLOR_PREFKEY = MSG_COLOR_PREFKEY_PREFIX + "assert"; //$NON-NLS-1$ + + // Use a monospace font family + private static final String FONT_FAMILY = + DdmConstants.CURRENT_PLATFORM == DdmConstants.PLATFORM_DARWIN ? "Monaco":"Courier New"; + + // Use the default system font size + private static final FontData DEFAULT_LOGCAT_FONTDATA; + static { + int h = Display.getDefault().getSystemFont().getFontData()[0].getHeight(); + DEFAULT_LOGCAT_FONTDATA = new FontData(FONT_FAMILY, h, SWT.NORMAL); + } + + private static final String LOGCAT_VIEW_COLSIZE_PREFKEY_PREFIX = "logcat.view.colsize."; + private static final String DISPLAY_FILTERS_COLUMN_PREFKEY = "logcat.view.display.filters"; + + /** Default message to show in the message search field. */ + private static final String DEFAULT_SEARCH_MESSAGE = + "Search for messages. Accepts Java regexes. " + + "Prefix with pid:, app:, tag: or text: to limit scope."; + + /** Tooltip to show in the message search field. */ + private static final String DEFAULT_SEARCH_TOOLTIP = + "Example search patterns:\n" + + " sqlite (search for sqlite in text field)\n" + + " app:browser (search for messages generated by the browser application)"; + + private static final String IMAGE_ADD_FILTER = "add.png"; //$NON-NLS-1$ + private static final String IMAGE_DELETE_FILTER = "delete.png"; //$NON-NLS-1$ + private static final String IMAGE_EDIT_FILTER = "edit.png"; //$NON-NLS-1$ + private static final String IMAGE_SAVE_LOG_TO_FILE = "save.png"; //$NON-NLS-1$ + private static final String IMAGE_CLEAR_LOG = "clear.png"; //$NON-NLS-1$ + private static final String IMAGE_DISPLAY_FILTERS = "displayfilters.png"; //$NON-NLS-1$ + private static final String IMAGE_SCROLL_LOCK = "scroll_lock.png"; //$NON-NLS-1$ + + private static final int[] WEIGHTS_SHOW_FILTERS = new int[] {15, 85}; + private static final int[] WEIGHTS_LOGCAT_ONLY = new int[] {0, 100}; + + /** Index of the default filter in the saved filters column. */ + private static final int DEFAULT_FILTER_INDEX = 0; + + /* Text colors for the filter box */ + private static final Color VALID_FILTER_REGEX_COLOR = + Display.getDefault().getSystemColor(SWT.COLOR_BLACK); + private static final Color INVALID_FILTER_REGEX_COLOR = + Display.getDefault().getSystemColor(SWT.COLOR_RED); + + private LogCatReceiver mReceiver; + private IPreferenceStore mPrefStore; + + private List mLogCatFilters; + private Map mLogCatFilterData; + private int mCurrentSelectedFilterIndex; + + private ToolItem mNewFilterToolItem; + private ToolItem mDeleteFilterToolItem; + private ToolItem mEditFilterToolItem; + private TableViewer mFiltersTableViewer; + + private Combo mLiveFilterLevelCombo; + private Text mLiveFilterText; + + private List mCurrentFilters = Collections.emptyList(); + + private Table mTable; + + private boolean mShouldScrollToLatestLog = true; + private ToolItem mScrollLockCheckBox; + private boolean mAutoScrollLock; + + // Lock under which the vertical scroll bar listener should be added + private final Object mScrollBarSelectionListenerLock = new Object(); + private SelectionListener mScrollBarSelectionListener; + private boolean mScrollBarListenerSet = false; + + private String mLogFileExportFolder; + + private Font mFont; + private int mWrapWidthInChars; + + private Color mVerboseColor; + private Color mDebugColor; + private Color mInfoColor; + private Color mWarnColor; + private Color mErrorColor; + private Color mAssertColor; + + private SashForm mSash; + + // messages added since last refresh, synchronized on mLogBuffer + private List mLogBuffer; + + // # of messages deleted since last refresh, synchronized on mLogBuffer + private int mDeletedLogCount; + + /** + * Construct a logcat panel. + * @param prefStore preference store where UI preferences will be saved + */ + public LogCatPanel(IPreferenceStore prefStore) { + mPrefStore = prefStore; + mLogBuffer = new ArrayList(LogCatMessageList.MAX_MESSAGES_DEFAULT); + + initializeFilters(); + + setupDefaultPreferences(); + initializePreferenceUpdateListeners(); + + mFont = getFontFromPrefStore(); + loadMessageColorPreferences(); + mAutoScrollLock = mPrefStore.getBoolean(AUTO_SCROLL_LOCK_PREFKEY); + } + + private void loadMessageColorPreferences() { + if (mVerboseColor != null) { + disposeMessageColors(); + } + + mVerboseColor = getColorFromPrefStore(VERBOSE_COLOR_PREFKEY); + mDebugColor = getColorFromPrefStore(DEBUG_COLOR_PREFKEY); + mInfoColor = getColorFromPrefStore(INFO_COLOR_PREFKEY); + mWarnColor = getColorFromPrefStore(WARN_COLOR_PREFKEY); + mErrorColor = getColorFromPrefStore(ERROR_COLOR_PREFKEY); + mAssertColor = getColorFromPrefStore(ASSERT_COLOR_PREFKEY); + } + + private void initializeFilters() { + mLogCatFilters = new ArrayList(); + mLogCatFilterData = new ConcurrentHashMap(); + + /* add default filter matching all messages */ + String tag = ""; + String text = ""; + String pid = ""; + String app = ""; + LogCatFilter defaultFilter = new LogCatFilter("All messages (no filters)", + tag, text, pid, app, LogLevel.VERBOSE); + + mLogCatFilters.add(defaultFilter); + mLogCatFilterData.put(defaultFilter, new LogCatFilterData(defaultFilter)); + + /* restore saved filters from prefStore */ + List savedFilters = getSavedFilters(); + for (LogCatFilter f: savedFilters) { + mLogCatFilters.add(f); + mLogCatFilterData.put(f, new LogCatFilterData(f)); + } + } + + private void setupDefaultPreferences() { + PreferenceConverter.setDefault(mPrefStore, LogCatPanel.LOGCAT_VIEW_FONT_PREFKEY, + DEFAULT_LOGCAT_FONTDATA); + mPrefStore.setDefault(LogCatMessageList.MAX_MESSAGES_PREFKEY, + LogCatMessageList.MAX_MESSAGES_DEFAULT); + mPrefStore.setDefault(DISPLAY_FILTERS_COLUMN_PREFKEY, true); + mPrefStore.setDefault(AUTO_SCROLL_LOCK_PREFKEY, true); + + /* Default Colors for different log levels. */ + PreferenceConverter.setDefault(mPrefStore, LogCatPanel.VERBOSE_COLOR_PREFKEY, + new RGB(0, 0, 0)); + PreferenceConverter.setDefault(mPrefStore, LogCatPanel.DEBUG_COLOR_PREFKEY, + new RGB(0, 0, 127)); + PreferenceConverter.setDefault(mPrefStore, LogCatPanel.INFO_COLOR_PREFKEY, + new RGB(0, 127, 0)); + PreferenceConverter.setDefault(mPrefStore, LogCatPanel.WARN_COLOR_PREFKEY, + new RGB(255, 127, 0)); + PreferenceConverter.setDefault(mPrefStore, LogCatPanel.ERROR_COLOR_PREFKEY, + new RGB(255, 0, 0)); + PreferenceConverter.setDefault(mPrefStore, LogCatPanel.ASSERT_COLOR_PREFKEY, + new RGB(255, 0, 0)); + } + + private void initializePreferenceUpdateListeners() { + mPrefStore.addPropertyChangeListener(new IPropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent event) { + String changedProperty = event.getProperty(); + if (changedProperty.equals(LogCatPanel.LOGCAT_VIEW_FONT_PREFKEY)) { + if (mFont != null) { + mFont.dispose(); + } + mFont = getFontFromPrefStore(); + recomputeWrapWidth(); + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + for (TableItem it: mTable.getItems()) { + it.setFont(mFont); + } + } + }); + } else if (changedProperty.startsWith(MSG_COLOR_PREFKEY_PREFIX)) { + loadMessageColorPreferences(); + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + Color c = mVerboseColor; + for (TableItem it: mTable.getItems()) { + Object data = it.getData(); + if (data instanceof LogCatMessage) { + c = getForegroundColor((LogCatMessage) data); + } + it.setForeground(c); + } + } + }); + } else if (changedProperty.equals(LogCatMessageList.MAX_MESSAGES_PREFKEY)) { + mReceiver.resizeFifo(mPrefStore.getInt( + LogCatMessageList.MAX_MESSAGES_PREFKEY)); + reloadLogBuffer(); + } else if (changedProperty.equals(AUTO_SCROLL_LOCK_PREFKEY)) { + mAutoScrollLock = mPrefStore.getBoolean(AUTO_SCROLL_LOCK_PREFKEY); + } + } + }); + } + + private void saveFilterPreferences() { + LogCatFilterSettingsSerializer serializer = new LogCatFilterSettingsSerializer(); + + /* save all filter settings except the first one which is the default */ + String e = serializer.encodeToPreferenceString( + mLogCatFilters.subList(1, mLogCatFilters.size()), mLogCatFilterData); + mPrefStore.setValue(LOGCAT_FILTERS_LIST, e); + } + + private List getSavedFilters() { + LogCatFilterSettingsSerializer serializer = new LogCatFilterSettingsSerializer(); + String e = mPrefStore.getString(LOGCAT_FILTERS_LIST); + return serializer.decodeFromPreferenceString(e); + } + + @Override + public void deviceSelected() { + IDevice device = getCurrentDevice(); + if (device == null) { + // If the device is not working properly, getCurrentDevice() could return null. + // In such a case, we don't launch logcat, nor switch the display. + return; + } + + if (mReceiver != null) { + // Don't need to listen to new logcat messages from previous device anymore. + mReceiver.removeMessageReceivedEventListener(this); + + // When switching between devices, existing filter match count should be reset. + for (LogCatFilter f : mLogCatFilters) { + LogCatFilterData fd = mLogCatFilterData.get(f); + fd.resetUnreadCount(); + } + } + + mReceiver = LogCatReceiverFactory.INSTANCE.newReceiver(device, mPrefStore); + mReceiver.addMessageReceivedEventListener(this); + reloadLogBuffer(); + + // Always scroll to last line whenever the selected device changes. + // Run this in a separate async thread to give the table some time to update after the + // setInput above. + Display.getDefault().asyncExec(new Runnable() { + @Override + public void run() { + scrollToLatestLog(); + } + }); + } + + @Override + public void clientSelected() { + } + + @Override + protected void postCreation() { + } + + @Override + protected Control createControl(Composite parent) { + GridLayout layout = new GridLayout(1, false); + parent.setLayout(layout); + + createViews(parent); + setupDefaults(); + + return null; + } + + private void createViews(Composite parent) { + mSash = createSash(parent); + + createListOfFilters(mSash); + createLogTableView(mSash); + + boolean showFilters = mPrefStore.getBoolean(DISPLAY_FILTERS_COLUMN_PREFKEY); + updateFiltersColumn(showFilters); + } + + private SashForm createSash(Composite parent) { + SashForm sash = new SashForm(parent, SWT.HORIZONTAL); + sash.setLayoutData(new GridData(GridData.FILL_BOTH)); + return sash; + } + + private void createListOfFilters(SashForm sash) { + Composite c = new Composite(sash, SWT.BORDER); + GridLayout layout = new GridLayout(2, false); + c.setLayout(layout); + c.setLayoutData(new GridData(GridData.FILL_BOTH)); + + createFiltersToolbar(c); + createFiltersTable(c); + } + + private void createFiltersToolbar(Composite parent) { + Label l = new Label(parent, SWT.NONE); + l.setText("Saved Filters"); + GridData gd = new GridData(); + gd.horizontalAlignment = SWT.LEFT; + l.setLayoutData(gd); + + ToolBar t = new ToolBar(parent, SWT.FLAT); + gd = new GridData(); + gd.horizontalAlignment = SWT.RIGHT; + t.setLayoutData(gd); + + /* new filter */ + mNewFilterToolItem = new ToolItem(t, SWT.PUSH); + mNewFilterToolItem.setImage( + ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_ADD_FILTER, t.getDisplay())); + mNewFilterToolItem.setToolTipText("Add a new logcat filter"); + mNewFilterToolItem.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + addNewFilter(); + } + }); + + /* delete filter */ + mDeleteFilterToolItem = new ToolItem(t, SWT.PUSH); + mDeleteFilterToolItem.setImage( + ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_DELETE_FILTER, t.getDisplay())); + mDeleteFilterToolItem.setToolTipText("Delete selected logcat filter"); + mDeleteFilterToolItem.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + deleteSelectedFilter(); + } + }); + + /* edit filter */ + mEditFilterToolItem = new ToolItem(t, SWT.PUSH); + mEditFilterToolItem.setImage( + ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_EDIT_FILTER, t.getDisplay())); + mEditFilterToolItem.setToolTipText("Edit selected logcat filter"); + mEditFilterToolItem.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + editSelectedFilter(); + } + }); + } + + private void addNewFilter(String defaultTag, String defaultText, String defaultPid, + String defaultAppName, LogLevel defaultLevel) { + LogCatFilterSettingsDialog d = new LogCatFilterSettingsDialog( + Display.getCurrent().getActiveShell()); + d.setDefaults("", defaultTag, defaultText, defaultPid, defaultAppName, defaultLevel); + if (d.open() != Window.OK) { + return; + } + + LogCatFilter f = new LogCatFilter(d.getFilterName().trim(), + d.getTag().trim(), + d.getText().trim(), + d.getPid().trim(), + d.getAppName().trim(), + LogLevel.getByString(d.getLogLevel())); + + mLogCatFilters.add(f); + mLogCatFilterData.put(f, new LogCatFilterData(f)); + mFiltersTableViewer.refresh(); + + /* select the newly added entry */ + int idx = mLogCatFilters.size() - 1; + mFiltersTableViewer.getTable().setSelection(idx); + + filterSelectionChanged(); + saveFilterPreferences(); + } + + private void addNewFilter() { + addNewFilter("", "", "", "", LogLevel.VERBOSE); + } + + private void deleteSelectedFilter() { + int selectedIndex = mFiltersTableViewer.getTable().getSelectionIndex(); + if (selectedIndex <= 0) { + /* return if no selected filter, or the default filter was selected (0th). */ + return; + } + + LogCatFilter f = mLogCatFilters.get(selectedIndex); + mLogCatFilters.remove(selectedIndex); + mLogCatFilterData.remove(f); + + mFiltersTableViewer.refresh(); + mFiltersTableViewer.getTable().setSelection(selectedIndex - 1); + + filterSelectionChanged(); + saveFilterPreferences(); + } + + private void editSelectedFilter() { + int selectedIndex = mFiltersTableViewer.getTable().getSelectionIndex(); + if (selectedIndex < 0) { + return; + } + + LogCatFilter curFilter = mLogCatFilters.get(selectedIndex); + + LogCatFilterSettingsDialog dialog = new LogCatFilterSettingsDialog( + Display.getCurrent().getActiveShell()); + dialog.setDefaults(curFilter.getName(), curFilter.getTag(), curFilter.getText(), + curFilter.getPid(), curFilter.getAppName(), curFilter.getLogLevel()); + if (dialog.open() != Window.OK) { + return; + } + + LogCatFilter f = new LogCatFilter(dialog.getFilterName(), + dialog.getTag(), + dialog.getText(), + dialog.getPid(), + dialog.getAppName(), + LogLevel.getByString(dialog.getLogLevel())); + mLogCatFilters.set(selectedIndex, f); + mFiltersTableViewer.refresh(); + + mFiltersTableViewer.getTable().setSelection(selectedIndex); + filterSelectionChanged(); + saveFilterPreferences(); + } + + /** + * Select the transient filter for the specified application. If no such filter + * exists, then create one and then select that. This method should be called from + * the UI thread. + * @param appName application name to filter by + */ + public void selectTransientAppFilter(String appName) { + assert mTable.getDisplay().getThread() == Thread.currentThread(); + + LogCatFilter f = findTransientAppFilter(appName); + if (f == null) { + f = createTransientAppFilter(appName); + mLogCatFilters.add(f); + + LogCatFilterData fd = new LogCatFilterData(f); + fd.setTransient(); + mLogCatFilterData.put(f, fd); + } + + selectFilterAt(mLogCatFilters.indexOf(f)); + } + + private LogCatFilter findTransientAppFilter(String appName) { + for (LogCatFilter f : mLogCatFilters) { + LogCatFilterData fd = mLogCatFilterData.get(f); + if (fd != null && fd.isTransient() && f.getAppName().equals(appName)) { + return f; + } + } + return null; + } + + private LogCatFilter createTransientAppFilter(String appName) { + LogCatFilter f = new LogCatFilter(appName + " (Session Filter)", + "", + "", + "", + appName, + LogLevel.VERBOSE); + return f; + } + + private void selectFilterAt(final int index) { + mFiltersTableViewer.refresh(); + + if (index != mFiltersTableViewer.getTable().getSelectionIndex()) { + mFiltersTableViewer.getTable().setSelection(index); + filterSelectionChanged(); + } + } + + private void createFiltersTable(Composite parent) { + final Table table = new Table(parent, SWT.FULL_SELECTION); + + GridData gd = new GridData(GridData.FILL_BOTH); + gd.horizontalSpan = 2; + table.setLayoutData(gd); + + mFiltersTableViewer = new TableViewer(table); + mFiltersTableViewer.setContentProvider(new LogCatFilterContentProvider()); + mFiltersTableViewer.setLabelProvider(new LogCatFilterLabelProvider(mLogCatFilterData)); + mFiltersTableViewer.setInput(mLogCatFilters); + + mFiltersTableViewer.getTable().addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent event) { + filterSelectionChanged(); + } + + @Override + public void widgetDefaultSelected(SelectionEvent arg0) { + editSelectedFilter(); + } + }); + } + + private void createLogTableView(SashForm sash) { + Composite c = new Composite(sash, SWT.NONE); + c.setLayout(new GridLayout()); + c.setLayoutData(new GridData(GridData.FILL_BOTH)); + + createLiveFilters(c); + createLogcatViewTable(c); + } + + /** Create the search bar at the top of the logcat messages table. */ + private void createLiveFilters(Composite parent) { + Composite c = new Composite(parent, SWT.NONE); + c.setLayout(new GridLayout(3, false)); + c.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mLiveFilterText = new Text(c, SWT.BORDER | SWT.SEARCH); + mLiveFilterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mLiveFilterText.setMessage(DEFAULT_SEARCH_MESSAGE); + mLiveFilterText.setToolTipText(DEFAULT_SEARCH_TOOLTIP); + mLiveFilterText.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent arg0) { + updateFilterTextColor(); + updateAppliedFilters(); + } + }); + + mLiveFilterLevelCombo = new Combo(c, SWT.READ_ONLY | SWT.DROP_DOWN); + mLiveFilterLevelCombo.setItems( + LogCatFilterSettingsDialog.getLogLevels().toArray(new String[0])); + mLiveFilterLevelCombo.select(0); + mLiveFilterLevelCombo.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + updateAppliedFilters(); + } + }); + + ToolBar toolBar = new ToolBar(c, SWT.FLAT); + + ToolItem saveToLog = new ToolItem(toolBar, SWT.PUSH); + saveToLog.setImage(ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_SAVE_LOG_TO_FILE, + toolBar.getDisplay())); + saveToLog.setToolTipText("Export Selected Items To Text File.."); + saveToLog.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + saveLogToFile(); + } + }); + + ToolItem clearLog = new ToolItem(toolBar, SWT.PUSH); + clearLog.setImage( + ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_CLEAR_LOG, toolBar.getDisplay())); + clearLog.setToolTipText("Clear Log"); + clearLog.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + if (mReceiver != null) { + mReceiver.clearMessages(); + refreshLogCatTable(); + resetUnreadCountForAllFilters(); + + // the filters view is not cleared unless the filters are re-applied. + updateAppliedFilters(); + } + } + }); + + final ToolItem showFiltersColumn = new ToolItem(toolBar, SWT.CHECK); + showFiltersColumn.setImage( + ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_DISPLAY_FILTERS, + toolBar.getDisplay())); + showFiltersColumn.setSelection(mPrefStore.getBoolean(DISPLAY_FILTERS_COLUMN_PREFKEY)); + showFiltersColumn.setToolTipText("Display Saved Filters View"); + showFiltersColumn.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent event) { + boolean showFilters = showFiltersColumn.getSelection(); + mPrefStore.setValue(DISPLAY_FILTERS_COLUMN_PREFKEY, showFilters); + updateFiltersColumn(showFilters); + } + }); + + mScrollLockCheckBox = new ToolItem(toolBar, SWT.CHECK); + mScrollLockCheckBox.setImage( + ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_SCROLL_LOCK, + toolBar.getDisplay())); + mScrollLockCheckBox.setSelection(true); + mScrollLockCheckBox.setToolTipText("Scroll Lock"); + mScrollLockCheckBox.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent event) { + boolean scrollLock = mScrollLockCheckBox.getSelection(); + setScrollToLatestLog(scrollLock); + } + }); + } + + /** Sets the foreground color of filter text based on whether the regex is valid. */ + private void updateFilterTextColor() { + String text = mLiveFilterText.getText(); + Color c; + try { + Pattern.compile(text.trim()); + c = VALID_FILTER_REGEX_COLOR; + } catch (PatternSyntaxException e) { + c = INVALID_FILTER_REGEX_COLOR; + } + mLiveFilterText.setForeground(c); + } + + private void updateFiltersColumn(boolean showFilters) { + if (showFilters) { + mSash.setWeights(WEIGHTS_SHOW_FILTERS); + } else { + mSash.setWeights(WEIGHTS_LOGCAT_ONLY); + } + } + + /** + * Save logcat messages selected in the table to a file. + */ + private void saveLogToFile() { + /* show dialog box and get target file name */ + final String fName = getLogFileTargetLocation(); + if (fName == null) { + return; + } + + /* obtain list of selected messages */ + final List selectedMessages = getSelectedLogCatMessages(); + + /* save messages to file in a different (non UI) thread */ + Thread t = new Thread(new Runnable() { + @Override + public void run() { + BufferedWriter w = null; + try { + w = new BufferedWriter(new FileWriter(fName)); + for (LogCatMessage m : selectedMessages) { + w.append(m.toString()); + w.newLine(); + } + } catch (final IOException e) { + Display.getDefault().asyncExec(new Runnable() { + @Override + public void run() { + MessageDialog.openError(Display.getCurrent().getActiveShell(), + "Unable to export selection to file.", + "Unexpected error while saving selected messages to file: " + + e.getMessage()); + } + }); + } finally { + if (w != null) { + try { + w.close(); + } catch (IOException e) { + // ignore + } + } + } + } + }); + t.setName("Saving selected items to logfile.."); + t.start(); + } + + /** + * Display a {@link FileDialog} to the user and obtain the location for the log file. + * @return path to target file, null if user canceled the dialog + */ + private String getLogFileTargetLocation() { + FileDialog fd = new FileDialog(Display.getCurrent().getActiveShell(), SWT.SAVE); + + fd.setText("Save Log.."); + fd.setFileName("log.txt"); + + if (mLogFileExportFolder == null) { + mLogFileExportFolder = System.getProperty("user.home"); + } + fd.setFilterPath(mLogFileExportFolder); + + fd.setFilterNames(new String[] { + "Text Files (*.txt)" + }); + fd.setFilterExtensions(new String[] { + "*.txt" + }); + + String fName = fd.open(); + if (fName != null) { + mLogFileExportFolder = fd.getFilterPath(); /* save path to restore on future calls */ + } + + return fName; + } + + private List getSelectedLogCatMessages() { + int[] indices = mTable.getSelectionIndices(); + Arrays.sort(indices); /* Table.getSelectionIndices() does not specify an order */ + + List selectedMessages = new ArrayList(indices.length); + for (int i : indices) { + Object data = mTable.getItem(i).getData(); + if (data instanceof LogCatMessage) { + selectedMessages.add((LogCatMessage) data); + } + } + + return selectedMessages; + } + + private List applyCurrentFilters(List msgList) { + List filteredItems = new ArrayList(msgList.size()); + + for (LogCatMessage msg: msgList) { + if (isMessageAccepted(msg, mCurrentFilters)) { + filteredItems.add(msg); + } + } + + return filteredItems; + } + + private boolean isMessageAccepted(LogCatMessage msg, List filters) { + for (LogCatFilter f : filters) { + if (!f.matches(msg)) { + // not accepted by this filter + return false; + } + } + + // accepted by all filters + return true; + } + + private void createLogcatViewTable(Composite parent) { + mTable = new Table(parent, SWT.FULL_SELECTION | SWT.MULTI); + + mTable.setLayoutData(new GridData(GridData.FILL_BOTH)); + mTable.getHorizontalBar().setVisible(true); + + /** Columns to show in the table. */ + String[] properties = { + "Level", + "Time", + "PID", + "TID", + "Application", + "Tag", + "Text", + }; + + /** The sampleText for each column is used to determine the default widths + * for each column. The contents do not matter, only their lengths are needed. */ + String[] sampleText = { + " ", + " 00-00 00:00:00.0000 ", + " 0000", + " 0000", + " com.android.launcher", + " SampleTagText", + " Log Message field should be pretty long by default. As long as possible for correct display on Mac.", + }; + + for (int i = 0; i < properties.length; i++) { + TableHelper.createTableColumn(mTable, + properties[i], /* Column title */ + SWT.LEFT, /* Column Style */ + sampleText[i], /* String to compute default col width */ + getColPreferenceKey(properties[i]), /* Preference Store key for this column */ + mPrefStore); + } + + // don't zebra stripe the table: When the buffer is full, and scroll lock is on, having + // zebra striping means that the background could keep changing depending on the number + // of new messages added to the bottom of the log. + mTable.setLinesVisible(false); + mTable.setHeaderVisible(true); + + // Set the row height to be sufficient enough to display the current font. + // This is not strictly necessary, except that on WinXP, the rows showed up clipped. So + // we explicitly set it to be sure. + mTable.addListener(SWT.MeasureItem, new Listener() { + @Override + public void handleEvent(Event event) { + event.height = event.gc.getFontMetrics().getHeight(); + } + }); + + // Update the label provider whenever the text column's width changes + TableColumn textColumn = mTable.getColumn(properties.length - 1); + textColumn.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent event) { + recomputeWrapWidth(); + } + }); + + addRightClickMenu(mTable); + initDoubleClickListener(); + recomputeWrapWidth(); + + mTable.addDisposeListener(new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent arg0) { + dispose(); + } + }); + + final ScrollBar vbar = mTable.getVerticalBar(); + mScrollBarSelectionListener = new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + if (!mAutoScrollLock) { + return; + } + + // thumb + selection < max => bar is not at the bottom. + // We subtract an arbitrary amount (thumbSize/2) from this difference to allow + // for cases like half a line being displayed at the end from affecting this + // calculation. The thumbSize/2 number seems to work experimentally across + // Linux/Mac & Windows, but might possibly need tweaking. + int diff = vbar.getThumb() + vbar.getSelection() - vbar.getMaximum(); + boolean isAtBottom = Math.abs(diff) < vbar.getThumb() / 2; + + if (isAtBottom != mShouldScrollToLatestLog) { + setScrollToLatestLog(isAtBottom); + mScrollLockCheckBox.setSelection(isAtBottom); + } + } + }; + startScrollBarMonitor(vbar); + + // Explicitly set the values to use for the scroll bar. In particular, we want these values + // to have a high enough accuracy that even small movements of the scroll bar have an + // effect on the selection. The auto scroll lock detection assumes that the scroll bar is + // at the bottom iff selection + thumb == max. + final int MAX = 10000; + final int THUMB = 10; + vbar.setValues(MAX - THUMB, // selection + 0, // min + MAX, // max + THUMB, // thumb + 1, // increment + THUMB); // page increment + } + + private void startScrollBarMonitor(ScrollBar vbar) { + synchronized (mScrollBarSelectionListenerLock) { + if (!mScrollBarListenerSet) { + mScrollBarListenerSet = true; + vbar.addSelectionListener(mScrollBarSelectionListener); + } + } + } + + private void stopScrollBarMonitor(ScrollBar vbar) { + synchronized (mScrollBarSelectionListenerLock) { + if (mScrollBarListenerSet) { + mScrollBarListenerSet = false; + vbar.removeSelectionListener(mScrollBarSelectionListener); + } + } + } + + /** Setup menu to be displayed when right clicking a log message. */ + private void addRightClickMenu(final Table table) { + // This action will pop up a create filter dialog pre-populated with current selection + final Action filterAction = new Action("Filter similar messages...") { + @Override + public void run() { + List selectedMessages = getSelectedLogCatMessages(); + if (selectedMessages.size() == 0) { + addNewFilter(); + } else { + LogCatMessage m = selectedMessages.get(0); + addNewFilter(m.getTag(), m.getMessage(), Integer.toString(m.getPid()), m.getAppName(), + m.getLogLevel()); + } + } + }; + + final Action findAction = new Action("Find...") { + @Override + public void run() { + showFindDialog(); + }; + }; + + final MenuManager mgr = new MenuManager(); + mgr.add(filterAction); + mgr.add(findAction); + final Menu menu = mgr.createContextMenu(table); + + table.addListener(SWT.MenuDetect, new Listener() { + @Override + public void handleEvent(Event event) { + Point pt = table.getDisplay().map(null, table, new Point(event.x, event.y)); + Rectangle clientArea = table.getClientArea(); + + // The click location is in the header if it is between + // clientArea.y and clientArea.y + header height + boolean header = pt.y > clientArea.y + && pt.y < (clientArea.y + table.getHeaderHeight()); + + // Show the menu only if it is not inside the header + table.setMenu(header ? null : menu); + } + }); + } + + public void recomputeWrapWidth() { + if (mTable == null || mTable.isDisposed()) { + return; + } + + // get width of the last column (log message) + TableColumn tc = mTable.getColumn(mTable.getColumnCount() - 1); + int colWidth = tc.getWidth(); + + // get font width + GC gc = new GC(tc.getParent()); + gc.setFont(mFont); + int avgCharWidth = gc.getFontMetrics().getAverageCharWidth(); + gc.dispose(); + + int MIN_CHARS_PER_LINE = 50; // show atleast these many chars per line + mWrapWidthInChars = Math.max(colWidth/avgCharWidth, MIN_CHARS_PER_LINE); + + int OFFSET_AT_END_OF_LINE = 10; // leave some space at the end of the line + mWrapWidthInChars -= OFFSET_AT_END_OF_LINE; + } + + private void setScrollToLatestLog(boolean scroll) { + mShouldScrollToLatestLog = scroll; + if (scroll) { + scrollToLatestLog(); + } + } + + private String getColPreferenceKey(String field) { + return LOGCAT_VIEW_COLSIZE_PREFKEY_PREFIX + field; + } + + private Font getFontFromPrefStore() { + FontData fd = PreferenceConverter.getFontData(mPrefStore, + LogCatPanel.LOGCAT_VIEW_FONT_PREFKEY); + return new Font(Display.getDefault(), fd); + } + + private Color getColorFromPrefStore(String key) { + RGB rgb = PreferenceConverter.getColor(mPrefStore, key); + return new Color(Display.getDefault(), rgb); + } + + private void setupDefaults() { + int defaultFilterIndex = 0; + mFiltersTableViewer.getTable().setSelection(defaultFilterIndex); + + filterSelectionChanged(); + } + + /** + * Perform all necessary updates whenever a filter is selected (by user or programmatically). + */ + private void filterSelectionChanged() { + int idx = mFiltersTableViewer.getTable().getSelectionIndex(); + if (idx == -1) { + /* One of the filters should always be selected. + * On Linux, there is no way to deselect an item. + * On Mac, clicking inside the list view, but not an any item will result + * in all items being deselected. In such a case, we simply reselect the + * first entry. */ + idx = 0; + mFiltersTableViewer.getTable().setSelection(idx); + } + + mCurrentSelectedFilterIndex = idx; + + resetUnreadCountForAllFilters(); + updateFiltersToolBar(); + updateAppliedFilters(); + } + + private void resetUnreadCountForAllFilters() { + for (LogCatFilterData fd: mLogCatFilterData.values()) { + fd.resetUnreadCount(); + } + refreshFiltersTable(); + } + + private void updateFiltersToolBar() { + /* The default filter at index 0 can neither be edited, nor removed. */ + boolean en = mCurrentSelectedFilterIndex != DEFAULT_FILTER_INDEX; + mEditFilterToolItem.setEnabled(en); + mDeleteFilterToolItem.setEnabled(en); + } + + private void updateAppliedFilters() { + mCurrentFilters = getFiltersToApply(); + reloadLogBuffer(); + } + + private List getFiltersToApply() { + /* list of filters to apply = saved filter + live filters */ + List filters = new ArrayList(); + + if (mCurrentSelectedFilterIndex != DEFAULT_FILTER_INDEX) { + filters.add(getSelectedSavedFilter()); + } + + filters.addAll(getCurrentLiveFilters()); + return filters; + } + + private List getCurrentLiveFilters() { + return LogCatFilter.fromString( + mLiveFilterText.getText(), /* current query */ + LogLevel.getByString(mLiveFilterLevelCombo.getText())); /* current log level */ + } + + private LogCatFilter getSelectedSavedFilter() { + return mLogCatFilters.get(mCurrentSelectedFilterIndex); + } + + @Override + public void setFocus() { + } + + @Override + public void bufferChanged(List addedMessages, + List deletedMessages) { + updateUnreadCount(addedMessages); + refreshFiltersTable(); + + synchronized (mLogBuffer) { + addedMessages = applyCurrentFilters(addedMessages); + deletedMessages = applyCurrentFilters(deletedMessages); + + mLogBuffer.addAll(addedMessages); + mDeletedLogCount += deletedMessages.size(); + } + + refreshLogCatTable(); + } + + private void reloadLogBuffer() { + mTable.removeAll(); + + synchronized (mLogBuffer) { + mLogBuffer.clear(); + mDeletedLogCount = 0; + } + + if (mReceiver == null || mReceiver.getMessages() == null) { + return; + } + + List addedMessages = mReceiver.getMessages().getAllMessages(); + List deletedMessages = Collections.emptyList(); + bufferChanged(addedMessages, deletedMessages); + } + + /** + * When new messages are received, and they match a saved filter, update + * the unread count associated with that filter. + * @param receivedMessages list of new messages received + */ + private void updateUnreadCount(List receivedMessages) { + for (int i = 0; i < mLogCatFilters.size(); i++) { + if (i == mCurrentSelectedFilterIndex) { + /* no need to update unread count for currently selected filter */ + continue; + } + LogCatFilter f = mLogCatFilters.get(i); + LogCatFilterData fd = mLogCatFilterData.get(f); + fd.updateUnreadCount(receivedMessages); + } + } + + private void refreshFiltersTable() { + Display.getDefault().asyncExec(new Runnable() { + @Override + public void run() { + if (mFiltersTableViewer.getTable().isDisposed()) { + return; + } + mFiltersTableViewer.refresh(); + } + }); + } + + /** Task currently submitted to {@link Display#asyncExec} to be run in UI thread. */ + private LogCatTableRefresherTask mCurrentRefresher; + + /** + * Refresh the logcat table asynchronously from the UI thread. + * This method adds a new async refresh only if there are no pending refreshes for the table. + * Doing so eliminates redundant refresh threads from being queued up to be run on the + * display thread. + */ + private void refreshLogCatTable() { + synchronized (this) { + if (mCurrentRefresher == null) { + mCurrentRefresher = new LogCatTableRefresherTask(); + Display.getDefault().asyncExec(mCurrentRefresher); + } + } + } + + /** + * The {@link LogCatTableRefresherTask} takes care of refreshing the table with the + * new log messages that have been received. Since the log behaves like a circular buffer, + * the first step is to remove items from the top of the table (if necessary). This step + * is complicated by the fact that a single log message may span multiple rows if the message + * was wrapped. Once the deleted items are removed, the new messages are added to the bottom + * of the table. If scroll lock is enabled, the item that was original visible is made visible + * again, if not, the last item is made visible. + */ + private class LogCatTableRefresherTask implements Runnable { + @Override + public void run() { + if (mTable.isDisposed()) { + return; + } + synchronized (LogCatPanel.this) { + mCurrentRefresher = null; + } + + // Current topIndex so that it can be restored if scroll locked. + int topIndex = mTable.getTopIndex(); + + mTable.setRedraw(false); + + // the scroll bar should only listen to user generated scroll events, not the + // scroll events that happen due to the addition of logs + stopScrollBarMonitor(mTable.getVerticalBar()); + + // Obtain the list of new messages, and the number of deleted messages. + List newMessages; + int deletedMessageCount; + synchronized (mLogBuffer) { + newMessages = new ArrayList(mLogBuffer); + mLogBuffer.clear(); + + deletedMessageCount = mDeletedLogCount; + mDeletedLogCount = 0; + + mFindTarget.scrollBy(deletedMessageCount); + } + + int originalItemCount = mTable.getItemCount(); + + // Remove entries from the start of the table if they were removed in the log buffer + // This is complicated by the fact that a single message may span multiple TableItems + // if it was word-wrapped. + deletedMessageCount -= removeFromTable(mTable, deletedMessageCount); + + // Compute number of table items that were deleted from the table. + int deletedItemCount = originalItemCount - mTable.getItemCount(); + + // If there are more messages to delete (after deleting messages from the table), + // then delete them from the start of the newly added messages list + if (deletedMessageCount > 0) { + assert deletedMessageCount < newMessages.size(); + for (int i = 0; i < deletedMessageCount; i++) { + newMessages.remove(0); + } + } + + // Add the remaining messages to the table. + for (LogCatMessage m: newMessages) { + List wrappedMessageList = wrapMessage(m.getMessage(), mWrapWidthInChars); + Color c = getForegroundColor(m); + for (int i = 0; i < wrappedMessageList.size(); i++) { + TableItem item = new TableItem(mTable, SWT.NONE); + + if (i == 0) { + // Only set the message data in the first item. This allows code that + // examines the table item data (such as copy selection) to distinguish + // between real messages versus lines that are really just wrapped + // content from the previous message. + item.setData(m); + + item.setText(new String[] { + Character.toString(m.getLogLevel().getPriorityLetter()), + m.getTimestamp().toString(), + Integer.toString(m.getPid()), + Integer.toString(m.getTid()), + m.getAppName(), + m.getTag(), + wrappedMessageList.get(i) + }); + } else { + item.setText(new String[] { + "", "", "", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + "", "", "", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + wrappedMessageList.get(i) + }); + } + item.setForeground(c); + item.setFont(mFont); + } + } + + if (mShouldScrollToLatestLog) { + scrollToLatestLog(); + } else { + // If scroll locked, show the same item that was original visible in the table. + int index = Math.max(topIndex - deletedItemCount, 0); + mTable.setTopIndex(index); + } + + mTable.setRedraw(true); + + // re-enable listening to scroll bar events, but do so in a separate thread to make + // sure that the current task (LogCatRefresherTask) has completed first + Display.getDefault().asyncExec(new Runnable() { + @Override + public void run() { + if (!mTable.isDisposed()) { + startScrollBarMonitor(mTable.getVerticalBar()); + } + } + }); + } + + /** + * Removes given number of messages from the table, starting at the top of the table. + * Note that the number of messages deleted is not equal to the number of rows + * deleted since a single message could span multiple rows. This method first calculates + * the number of rows that correspond to the number of messages to delete, and then + * removes all those rows. + * @param table table from which messages should be removed + * @param msgCount number of messages to be removed + * @return number of messages that were actually removed + */ + private int removeFromTable(Table table, int msgCount) { + int deletedMessageCount = 0; // # of messages that have been deleted + int lastItemToDelete = 0; // index of the last item that should be deleted + + while (deletedMessageCount < msgCount && lastItemToDelete < table.getItemCount()) { + // only rows that begin a message have their item data set + TableItem item = table.getItem(lastItemToDelete); + if (item.getData() != null) { + deletedMessageCount++; + } + + lastItemToDelete++; + } + + // If there are any table items left over at the end that are wrapped over from the + // previous message, mark them for deletion as well. + if (lastItemToDelete < table.getItemCount() + && table.getItem(lastItemToDelete).getData() == null) { + lastItemToDelete++; + } + + table.remove(0, lastItemToDelete - 1); + + return deletedMessageCount; + } + } + + /** Scroll to the last line. */ + private void scrollToLatestLog() { + if (!mTable.isDisposed()) { + mTable.setTopIndex(mTable.getItemCount() - 1); + } + } + + /** + * Splits the message into multiple lines if the message length exceeds given width. + * If the message was split, then a wrap character \u23ce is appended to the end of all + * lines but the last one. + */ + private List wrapMessage(String msg, int wrapWidth) { + if (msg.length() < wrapWidth) { + return Collections.singletonList(msg); + } + + List wrappedMessages = new ArrayList(); + + int offset = 0; + int len = msg.length(); + + while (len > 0) { + int copylen = Math.min(wrapWidth, len); + String s = msg.substring(offset, offset + copylen); + + offset += copylen; + len -= copylen; + + if (len > 0) { // if there are more lines following, then append a wrap marker + s += " \u23ce"; //$NON-NLS-1$ + } + + wrappedMessages.add(s); + } + + return wrappedMessages; + } + + private Color getForegroundColor(LogCatMessage m) { + LogLevel l = m.getLogLevel(); + + if (l.equals(LogLevel.VERBOSE)) { + return mVerboseColor; + } else if (l.equals(LogLevel.INFO)) { + return mInfoColor; + } else if (l.equals(LogLevel.DEBUG)) { + return mDebugColor; + } else if (l.equals(LogLevel.ERROR)) { + return mErrorColor; + } else if (l.equals(LogLevel.WARN)) { + return mWarnColor; + } else if (l.equals(LogLevel.ASSERT)) { + return mAssertColor; + } + + return mVerboseColor; + } + + private List mMessageSelectionListeners; + + private void initDoubleClickListener() { + mMessageSelectionListeners = new ArrayList(1); + + mTable.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetDefaultSelected(SelectionEvent arg0) { + List selectedMessages = getSelectedLogCatMessages(); + if (selectedMessages.size() == 0) { + return; + } + + for (ILogCatMessageSelectionListener l : mMessageSelectionListeners) { + l.messageDoubleClicked(selectedMessages.get(0)); + } + } + }); + } + + public void addLogCatMessageSelectionListener(ILogCatMessageSelectionListener l) { + mMessageSelectionListeners.add(l); + } + + private ITableFocusListener mTableFocusListener; + + /** + * Specify the listener to be called when the logcat view gets focus. This interface is + * required by DDMS to hook up the menu items for Copy and Select All. + * @param listener listener to be notified when logcat view is in focus + */ + public void setTableFocusListener(ITableFocusListener listener) { + mTableFocusListener = listener; + + final IFocusedTableActivator activator = new IFocusedTableActivator() { + @Override + public void copy(Clipboard clipboard) { + copySelectionToClipboard(clipboard); + } + + @Override + public void selectAll() { + mTable.selectAll(); + } + }; + + mTable.addFocusListener(new FocusListener() { + @Override + public void focusGained(FocusEvent e) { + mTableFocusListener.focusGained(activator); + } + + @Override + public void focusLost(FocusEvent e) { + mTableFocusListener.focusLost(activator); + } + }); + } + + /** Copy all selected messages to clipboard. */ + public void copySelectionToClipboard(Clipboard clipboard) { + StringBuilder sb = new StringBuilder(); + + for (LogCatMessage m : getSelectedLogCatMessages()) { + sb.append(m.toString()); + sb.append('\n'); + } + + if (sb.length() > 0) { + clipboard.setContents( + new Object[] {sb.toString()}, + new Transfer[] {TextTransfer.getInstance()} + ); + } + } + + /** Select all items in the logcat table. */ + public void selectAll() { + mTable.selectAll(); + } + + private void dispose() { + if (mFont != null && !mFont.isDisposed()) { + mFont.dispose(); + } + + if (mVerboseColor != null && !mVerboseColor.isDisposed()) { + disposeMessageColors(); + } + } + + private void disposeMessageColors() { + mVerboseColor.dispose(); + mDebugColor.dispose(); + mInfoColor.dispose(); + mWarnColor.dispose(); + mErrorColor.dispose(); + mAssertColor.dispose(); + } + + private class LogcatFindTarget extends AbstractBufferFindTarget { + @Override + public void selectAndReveal(int index) { + mTable.deselectAll(); + mTable.select(index); + mTable.showSelection(); + } + + @Override + public int getItemCount() { + return mTable.getItemCount(); + } + + @Override + public String getItem(int index) { + Object data = mTable.getItem(index).getData(); + if (data != null) { + return data.toString(); + } + + return null; + } + + @Override + public int getStartingIndex() { + // start searches from current selection if present, otherwise from the tail end + // of the buffer + int s = mTable.getSelectionIndex(); + if (s != -1) { + return s; + } else { + return getItemCount() - 1; + } + }; + }; + + private FindDialog mFindDialog; + private LogcatFindTarget mFindTarget = new LogcatFindTarget(); + public void showFindDialog() { + if (mFindDialog != null) { + // if the dialog is already displayed + return; + } + + mFindDialog = new FindDialog(Display.getDefault().getActiveShell(), mFindTarget); + mFindDialog.open(); // blocks until find dialog is closed + mFindDialog = null; + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiver.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiver.java new file mode 100644 index 00000000..2aceafc4 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiver.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2011 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.LogLevel; +import com.android.ddmlib.logcat.LogCatListener; +import com.android.ddmlib.logcat.LogCatMessage; +import com.android.ddmlib.logcat.LogCatReceiverTask; + +import org.eclipse.jface.preference.IPreferenceStore; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * A class to monitor a device for logcat messages. It stores the received + * log messages in a circular buffer. + */ +public final class LogCatReceiver implements LogCatListener { + private static LogCatMessage DEVICE_DISCONNECTED_MESSAGE = + new LogCatMessage(LogLevel.ERROR, "Device disconnected"); + + private LogCatMessageList mLogMessages; + private IDevice mCurrentDevice; + private LogCatReceiverTask mLogCatReceiverTask; + private Set mLogCatMessageListeners; + private IPreferenceStore mPrefStore; + + /** + * Construct a LogCat message receiver for provided device. This will launch a + * logcat command on the device, and monitor the output of that command in + * a separate thread. All logcat messages are then stored in a circular + * buffer, which can be retrieved using {@link LogCatReceiver#getMessages()}. + * @param device device to monitor for logcat messages + * @param prefStore + */ + public LogCatReceiver(IDevice device, IPreferenceStore prefStore) { + mCurrentDevice = device; + mPrefStore = prefStore; + + mLogCatMessageListeners = new HashSet(); + mLogMessages = new LogCatMessageList(getFifoSize()); + + startReceiverThread(); + } + + /** + * Stop receiving messages from currently active device. + */ + public void stop() { + if (mLogCatReceiverTask != null) { + /* stop the current logcat command */ + mLogCatReceiverTask.removeLogCatListener(this); + mLogCatReceiverTask.stop(); + mLogCatReceiverTask = null; + + // add a message to the log indicating that the device has been disconnected. + log(Collections.singletonList(DEVICE_DISCONNECTED_MESSAGE)); + } + + mCurrentDevice = null; + } + + private int getFifoSize() { + int n = mPrefStore.getInt(LogCatMessageList.MAX_MESSAGES_PREFKEY); + return n == 0 ? LogCatMessageList.MAX_MESSAGES_DEFAULT : n; + } + + private void startReceiverThread() { + if (mCurrentDevice == null) { + return; + } + + mLogCatReceiverTask = new LogCatReceiverTask(mCurrentDevice); + mLogCatReceiverTask.addLogCatListener(this); + + Thread t = new Thread(mLogCatReceiverTask); + t.setName("LogCat output receiver for " + mCurrentDevice.getSerialNumber()); + t.start(); + } + + @Override + public void log(List newMessages) { + List deletedMessages; + synchronized (mLogMessages) { + deletedMessages = mLogMessages.ensureSpace(newMessages.size()); + mLogMessages.appendMessages(newMessages); + } + sendLogChangedEvent(newMessages, deletedMessages); + } + + /** + * Get the list of logcat messages received from currently active device. + * @return list of messages if currently listening, null otherwise + */ + public LogCatMessageList getMessages() { + return mLogMessages; + } + + /** + * Clear the list of messages received from the currently active device. + */ + public void clearMessages() { + mLogMessages.clear(); + } + + /** + * Add to list of message event listeners. + * @param l listener to notified when messages are received from the device + */ + public void addMessageReceivedEventListener(ILogCatBufferChangeListener l) { + mLogCatMessageListeners.add(l); + } + + public void removeMessageReceivedEventListener(ILogCatBufferChangeListener l) { + mLogCatMessageListeners.remove(l); + } + + private void sendLogChangedEvent(List addedMessages, + List deletedMessages) { + for (ILogCatBufferChangeListener l : mLogCatMessageListeners) { + l.bufferChanged(addedMessages, deletedMessages); + } + } + + /** + * Resize the internal FIFO. + * @param size new size + */ + public void resizeFifo(int size) { + mLogMessages.resize(size); + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiverFactory.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiverFactory.java new file mode 100644 index 00000000..bf440eaa --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatReceiverFactory.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2011 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.AndroidDebugBridge; +import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; +import com.android.ddmlib.IDevice; + +import org.eclipse.jface.preference.IPreferenceStore; + +import java.util.HashMap; +import java.util.Map; + +/** + * A factory for {@link LogCatReceiver} objects. Its primary objective is to cache + * constructed {@link LogCatReceiver}'s per device and hand them back when requested. + */ +public class LogCatReceiverFactory { + /** Singleton instance. */ + public static final LogCatReceiverFactory INSTANCE = new LogCatReceiverFactory(); + + private Map mReceiverCache = new HashMap(); + + /** Private constructor: cannot instantiate. */ + private LogCatReceiverFactory() { + AndroidDebugBridge.addDeviceChangeListener(new IDeviceChangeListener() { + @Override + public void deviceDisconnected(final IDevice device) { + // The deviceDisconnected() is called from DDMS code that holds + // multiple locks regarding list of clients, etc. + // It so happens that #newReceiver() below adds a clientChangeListener + // which requires those locks as well. So if we call + // #removeReceiverFor from a DDMS/Monitor thread, we could end up + // in a deadlock. As a result, we spawn a separate thread that + // doesn't hold any of the DDMS locks to remove the receiver. + Thread t = new Thread(new Runnable() { + @Override + public void run() { + removeReceiverFor(device); } + }, "Remove logcat receiver for " + device.getSerialNumber()); + t.start(); + } + + @Override + public void deviceConnected(IDevice device) { + } + + @Override + public void deviceChanged(IDevice device, int changeMask) { + } + }); + } + + /** + * Remove existing logcat receivers. This method should not be called from a DDMS thread + * context that might be holding locks. Doing so could result in a deadlock with the following + * two threads locked up:

    + *
  • {@link #removeReceiverFor(IDevice)} waiting to lock {@link LogCatReceiverFactory}, + * while holding a DDMS monitor internal lock.
  • + *
  • {@link #newReceiver(IDevice, IPreferenceStore)} holding {@link LogCatReceiverFactory} + * while attempting to obtain a DDMS monitor lock.
  • + *
+ */ + private synchronized void removeReceiverFor(IDevice device) { + LogCatReceiver r = mReceiverCache.get(device.getSerialNumber()); + if (r != null) { + r.stop(); + mReceiverCache.remove(device.getSerialNumber()); + } + } + + public synchronized LogCatReceiver newReceiver(IDevice device, IPreferenceStore prefs) { + LogCatReceiver r = mReceiverCache.get(device.getSerialNumber()); + if (r != null) { + return r; + } + + r = new LogCatReceiver(device, prefs); + mReceiverCache.put(device.getSerialNumber(), r); + return r; + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatStackTraceParser.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatStackTraceParser.java new file mode 100644 index 00000000..f37484f9 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatStackTraceParser.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2011 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 java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Helper class that can determine if a string matches the exception + * stack trace pattern, and if so, can provide the java source file + * and line where the exception occured. + */ +public final class LogCatStackTraceParser { + /** Regex to match a stack trace line. E.g.: + * at com.foo.Class.method(FileName.extension:10) + * extension is typically java, but can be anything (java/groovy/scala/..). + */ + private static final String EXCEPTION_LINE_REGEX = + "\\s*at\\ (.*)\\((.*)\\..*\\:(\\d+)\\)"; //$NON-NLS-1$ + + private static final Pattern EXCEPTION_LINE_PATTERN = + Pattern.compile(EXCEPTION_LINE_REGEX); + + /** + * Identify if a input line matches the expected pattern + * for a stack trace from an exception. + */ + public boolean isValidExceptionTrace(String line) { + return EXCEPTION_LINE_PATTERN.matcher(line).find(); + } + + /** + * Get fully qualified method name that threw the exception. + * @param line line from the stack trace, must have been validated with + * {@link LogCatStackTraceParser#isValidExceptionTrace(String)} before calling this method. + * @return fully qualified method name + */ + public String getMethodName(String line) { + Matcher m = EXCEPTION_LINE_PATTERN.matcher(line); + m.find(); + return m.group(1); + } + + /** + * Get source file name where exception was generated. Input line must be first validated with + * {@link LogCatStackTraceParser#isValidExceptionTrace(String)}. + */ + public String getFileName(String line) { + Matcher m = EXCEPTION_LINE_PATTERN.matcher(line); + m.find(); + return m.group(2); + } + + /** + * Get line number where exception was generated. Input line must be first validated with + * {@link LogCatStackTraceParser#isValidExceptionTrace(String)}. + */ + public int getLineNumber(String line) { + Matcher m = EXCEPTION_LINE_PATTERN.matcher(line); + m.find(); + try { + return Integer.parseInt(m.group(3)); + } catch (NumberFormatException e) { + return 0; + } + } + +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogColors.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogColors.java new file mode 100644 index 00000000..56bd94c4 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogFilter.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogFilter.java new file mode 100644 index 00000000..6b15ad1f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogFilter.java @@ -0,0 +1,556 @@ +/* + * 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); + } + mRemovedMessageCount = 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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogPanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogPanel.java new file mode 100644 index 00000000..c4c6f8ab --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogPanel.java @@ -0,0 +1,1626 @@ +/* + * 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.AdbCommandRejectedException; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.Log; +import com.android.ddmlib.Log.LogLevel; +import com.android.ddmlib.MultiLineReceiver; +import com.android.ddmlib.ShellCommandUnresponsiveException; +import com.android.ddmlib.TimeoutException; +import com.android.ddmuilib.DdmUiPreferences; +import com.android.ddmuilib.ITableFocusListener; +import com.android.ddmuilib.ITableFocusListener.IFocusedTableActivator; +import com.android.ddmuilib.SelectionDependentPanel; +import com.android.ddmuilib.TableHelper; +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; + + private String mDefaultLogSave; + + private int mColumnMode = COLUMN_MODE_MANUAL; + private Font mDisplayFont; + + private ITableFocusListener mGlobalListener; + + private LogCatViewInterface mLogCatViewInterface = null; + + /** 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); + } + } + + @Override + 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; + } + + @Override + 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() { + @Override + public void run() { + mTabItem.setText(name); + } + }); + + // we're done with this ps. + mDone = true; + return; + } + } + } + + } + + /** + * Interface implemented by the LogCatView in Eclipse for particular action on double-click. + */ + public interface LogCatViewInterface { + public void onDoubleClick(); + } + + /** + * Create the log view with some default parameters + * @param colors The display color object + * @param filterStorage the storage for user defined filters. + * @param mode The filtering mode + */ + public LogPanel(LogColors colors, + ILogFilterStorageManager filterStorage, int mode) { + 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() { + @Override + 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, 0 /*timeout*/); //$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(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( + 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. + FileWriter writer = null; + try { + 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; + } finally { + if (writer != null) { + try { + writer.close(); + } catch (IOException e) { + // ignore + } + } + } + } + } + + 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() { + @Override + public void copy(Clipboard clipboard) { + copyTable(clipboard, table); + } + + @Override + public void selectAll() { + table.selectAll(); + } + }; + + // add the focus listener on the table to notify the global listener + table.addFocusListener(new FocusListener() { + @Override + public void focusGained(FocusEvent e) { + mGlobalListener.focusGained(activator); + } + + @Override + 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); + t.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetDefaultSelected(SelectionEvent e) { + if (mLogCatViewInterface != null) { + mLogCatViewInterface.onDoubleClick(); + } + } + }); + + 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() { + @Override + public void controlMoved(ControlEvent e) { + } + + @Override + 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() { + @Override + 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() { + @Override + 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() { + @Override + 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; + + // increment the next usable slot index + mBufferEnd = (mBufferEnd + 1) % STRING_BUFFER_LENGTH; + + // check we aren't overwriting start + if (mBufferEnd == mBufferStart) { + mBufferStart = (mBufferStart + 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) { + // Ignore + } catch (TimeoutException e) { + // Ignore + } catch (AdbCommandRejectedException e) { + // Ignore + } catch (ShellCommandUnresponsiveException e) { + // Ignore + } + } + }.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; + } + + public void setLogCatViewInterface(LogCatViewInterface i) { + mLogCatViewInterface = i; + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/net/NetworkPanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/net/NetworkPanel.java new file mode 100644 index 00000000..65ce49f8 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/net/NetworkPanel.java @@ -0,0 +1,1125 @@ +/* + * Copyright (C) 2012 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.net; + +import com.android.ddmlib.AdbCommandRejectedException; +import com.android.ddmlib.Client; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.MultiLineReceiver; +import com.android.ddmlib.ShellCommandUnresponsiveException; +import com.android.ddmlib.TimeoutException; +import com.android.ddmuilib.DdmUiPreferences; +import com.android.ddmuilib.TableHelper; +import com.android.ddmuilib.TablePanel; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.jface.dialogs.ErrorDialog; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.IStructuredContentProvider; +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.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.FormAttachment; +import org.eclipse.swt.layout.FormData; +import org.eclipse.swt.layout.FormLayout; +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.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Table; +import org.jfree.chart.ChartFactory; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.axis.AxisLocation; +import org.jfree.chart.axis.NumberAxis; +import org.jfree.chart.axis.ValueAxis; +import org.jfree.chart.plot.DatasetRenderingOrder; +import org.jfree.chart.plot.ValueMarker; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.xy.StackedXYAreaRenderer2; +import org.jfree.chart.renderer.xy.XYAreaRenderer; +import org.jfree.data.DefaultKeyedValues2D; +import org.jfree.data.time.Millisecond; +import org.jfree.data.time.TimePeriod; +import org.jfree.data.time.TimeSeries; +import org.jfree.data.time.TimeSeriesCollection; +import org.jfree.data.xy.AbstractIntervalXYDataset; +import org.jfree.data.xy.TableXYDataset; +import org.jfree.experimental.chart.swt.ChartComposite; +import org.jfree.ui.RectangleAnchor; +import org.jfree.ui.TextAnchor; + +import java.io.IOException; +import java.text.DecimalFormat; +import java.text.FieldPosition; +import java.text.NumberFormat; +import java.text.ParsePosition; +import java.util.ArrayList; +import java.util.Date; +import java.util.Formatter; +import java.util.Iterator; + +/** + * Displays live network statistics for currently selected {@link Client}. + */ +public class NetworkPanel extends TablePanel { + + // TODO: enable view of packets and bytes/packet + // TODO: add sash to resize chart and table + // TODO: let user edit tags to be meaningful + + /** Amount of historical data to display. */ + private static final long HISTORY_MILLIS = 30 * 1000; + + private final static String PREFS_NETWORK_COL_TITLE = "networkPanel.title"; + private final static String PREFS_NETWORK_COL_RX_BYTES = "networkPanel.rxBytes"; + private final static String PREFS_NETWORK_COL_RX_PACKETS = "networkPanel.rxPackets"; + private final static String PREFS_NETWORK_COL_TX_BYTES = "networkPanel.txBytes"; + private final static String PREFS_NETWORK_COL_TX_PACKETS = "networkPanel.txPackets"; + + /** Path to network statistics on remote device. */ + private static final String PROC_XT_QTAGUID = "/proc/net/xt_qtaguid/stats"; + + private static final java.awt.Color TOTAL_COLOR = java.awt.Color.GRAY; + + /** Colors used for tag series data. */ + private static final java.awt.Color[] SERIES_COLORS = new java.awt.Color[] { + java.awt.Color.decode("0x2bc4c1"), // teal + java.awt.Color.decode("0xD50F25"), // red + java.awt.Color.decode("0x3369E8"), // blue + java.awt.Color.decode("0xEEB211"), // orange + java.awt.Color.decode("0x00bd2e"), // green + java.awt.Color.decode("0xae26ae"), // purple + }; + + private Display mDisplay; + + private Composite mPanel; + + /** Header panel with configuration options. */ + private Composite mHeader; + + private Label mSpeedLabel; + private Combo mSpeedCombo; + + /** Current sleep between each sample, from {@link #mSpeedCombo}. */ + private long mSpeedMillis; + + private Button mRunningButton; + private Button mResetButton; + + /** Chart of recent network activity. */ + private JFreeChart mChart; + private ChartComposite mChartComposite; + + private ValueAxis mDomainAxis; + + /** Data for total traffic (tag 0x0). */ + private TimeSeriesCollection mTotalCollection; + private TimeSeries mRxTotalSeries; + private TimeSeries mTxTotalSeries; + + /** Data for detailed tagged traffic. */ + private LiveTimeTableXYDataset mRxDetailDataset; + private LiveTimeTableXYDataset mTxDetailDataset; + + private XYAreaRenderer mTotalRenderer; + private StackedXYAreaRenderer2 mRenderer; + + /** Table showing summary of network activity. */ + private Table mTable; + private TableViewer mTableViewer; + + /** UID of currently selected {@link Client}. */ + private int mActiveUid = -1; + + /** List of traffic flows being actively tracked. */ + private ArrayList mTrackedItems = new ArrayList(); + + private SampleThread mSampleThread; + + private class SampleThread extends Thread { + private volatile boolean mFinish; + + public void finish() { + mFinish = true; + interrupt(); + } + + @Override + public void run() { + while (!mFinish && !mDisplay.isDisposed()) { + performSample(); + + try { + Thread.sleep(mSpeedMillis); + } catch (InterruptedException e) { + // ignored + } + } + } + } + + /** Last snapshot taken by {@link #performSample()}. */ + private NetworkSnapshot mLastSnapshot; + + @Override + protected Control createControl(Composite parent) { + mDisplay = parent.getDisplay(); + + mPanel = new Composite(parent, SWT.NONE); + + final FormLayout formLayout = new FormLayout(); + mPanel.setLayout(formLayout); + + createHeader(); + createChart(); + createTable(); + + return mPanel; + } + + /** + * Create header panel with configuration options. + */ + private void createHeader() { + + mHeader = new Composite(mPanel, SWT.NONE); + final RowLayout layout = new RowLayout(); + layout.center = true; + mHeader.setLayout(layout); + + mSpeedLabel = new Label(mHeader, SWT.NONE); + mSpeedLabel.setText("Speed:"); + mSpeedCombo = new Combo(mHeader, SWT.PUSH); + mSpeedCombo.add("Fast (100ms)"); + mSpeedCombo.add("Medium (250ms)"); + mSpeedCombo.add("Slow (500ms)"); + mSpeedCombo.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + updateSpeed(); + } + }); + + mSpeedCombo.select(1); + updateSpeed(); + + mRunningButton = new Button(mHeader, SWT.PUSH); + mRunningButton.setText("Start"); + mRunningButton.setEnabled(false); + mRunningButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + final boolean alreadyRunning = mSampleThread != null; + updateRunning(!alreadyRunning); + } + }); + + mResetButton = new Button(mHeader, SWT.PUSH); + mResetButton.setText("Reset"); + mResetButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + clearTrackedItems(); + } + }); + + final FormData data = new FormData(); + data.top = new FormAttachment(0); + data.left = new FormAttachment(0); + data.right = new FormAttachment(100); + mHeader.setLayoutData(data); + } + + /** + * Create chart of recent network activity. + */ + private void createChart() { + + mChart = ChartFactory.createTimeSeriesChart(null, null, null, null, false, false, false); + + // create backing datasets and series + mRxTotalSeries = new TimeSeries("RX total"); + mTxTotalSeries = new TimeSeries("TX total"); + + mRxTotalSeries.setMaximumItemAge(HISTORY_MILLIS); + mTxTotalSeries.setMaximumItemAge(HISTORY_MILLIS); + + mTotalCollection = new TimeSeriesCollection(); + mTotalCollection.addSeries(mRxTotalSeries); + mTotalCollection.addSeries(mTxTotalSeries); + + mRxDetailDataset = new LiveTimeTableXYDataset(); + mTxDetailDataset = new LiveTimeTableXYDataset(); + + mTotalRenderer = new XYAreaRenderer(XYAreaRenderer.AREA); + mRenderer = new StackedXYAreaRenderer2(); + + final XYPlot xyPlot = mChart.getXYPlot(); + + xyPlot.setDatasetRenderingOrder(DatasetRenderingOrder.FORWARD); + + xyPlot.setDataset(0, mTotalCollection); + xyPlot.setDataset(1, mRxDetailDataset); + xyPlot.setDataset(2, mTxDetailDataset); + xyPlot.setRenderer(0, mTotalRenderer); + xyPlot.setRenderer(1, mRenderer); + xyPlot.setRenderer(2, mRenderer); + + // we control domain axis manually when taking samples + mDomainAxis = xyPlot.getDomainAxis(); + mDomainAxis.setAutoRange(false); + + final NumberAxis axis = new NumberAxis(); + axis.setNumberFormatOverride(new BytesFormat(true)); + axis.setAutoRangeMinimumSize(50); + xyPlot.setRangeAxis(axis); + xyPlot.setRangeAxisLocation(AxisLocation.BOTTOM_OR_RIGHT); + + // draw thick line to separate RX versus TX traffic + xyPlot.addRangeMarker( + new ValueMarker(0, java.awt.Color.BLACK, new java.awt.BasicStroke(2))); + + // label to indicate that positive axis is RX traffic + final ValueMarker rxMarker = new ValueMarker(0); + rxMarker.setStroke(new java.awt.BasicStroke(0)); + rxMarker.setLabel("RX"); + rxMarker.setLabelFont(rxMarker.getLabelFont().deriveFont(30f)); + rxMarker.setLabelPaint(java.awt.Color.LIGHT_GRAY); + rxMarker.setLabelAnchor(RectangleAnchor.TOP_RIGHT); + rxMarker.setLabelTextAnchor(TextAnchor.BOTTOM_RIGHT); + xyPlot.addRangeMarker(rxMarker); + + // label to indicate that negative axis is TX traffic + final ValueMarker txMarker = new ValueMarker(0); + txMarker.setStroke(new java.awt.BasicStroke(0)); + txMarker.setLabel("TX"); + txMarker.setLabelFont(txMarker.getLabelFont().deriveFont(30f)); + txMarker.setLabelPaint(java.awt.Color.LIGHT_GRAY); + txMarker.setLabelAnchor(RectangleAnchor.BOTTOM_RIGHT); + txMarker.setLabelTextAnchor(TextAnchor.TOP_RIGHT); + xyPlot.addRangeMarker(txMarker); + + mChartComposite = new ChartComposite(mPanel, SWT.BORDER, mChart, + ChartComposite.DEFAULT_WIDTH, ChartComposite.DEFAULT_HEIGHT, + ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH, + ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT, 4096, 4096, true, true, true, true, + false, true); + + final FormData data = new FormData(); + data.top = new FormAttachment(mHeader); + data.left = new FormAttachment(0); + data.bottom = new FormAttachment(70); + data.right = new FormAttachment(100); + mChartComposite.setLayoutData(data); + } + + /** + * Create table showing summary of network activity. + */ + private void createTable() { + mTable = new Table(mPanel, SWT.BORDER | SWT.MULTI | SWT.FULL_SELECTION); + + final FormData data = new FormData(); + data.top = new FormAttachment(mChartComposite); + data.left = new FormAttachment(mChartComposite, 0, SWT.CENTER); + data.bottom = new FormAttachment(100); + mTable.setLayoutData(data); + + mTable.setHeaderVisible(true); + mTable.setLinesVisible(true); + + final IPreferenceStore store = DdmUiPreferences.getStore(); + + TableHelper.createTableColumn(mTable, "", SWT.CENTER, buildSampleText(2), null, null); + TableHelper.createTableColumn( + mTable, "Tag", SWT.LEFT, buildSampleText(32), PREFS_NETWORK_COL_TITLE, store); + TableHelper.createTableColumn(mTable, "RX bytes", SWT.RIGHT, buildSampleText(12), + PREFS_NETWORK_COL_RX_BYTES, store); + TableHelper.createTableColumn(mTable, "RX packets", SWT.RIGHT, buildSampleText(12), + PREFS_NETWORK_COL_RX_PACKETS, store); + TableHelper.createTableColumn(mTable, "TX bytes", SWT.RIGHT, buildSampleText(12), + PREFS_NETWORK_COL_TX_BYTES, store); + TableHelper.createTableColumn(mTable, "TX packets", SWT.RIGHT, buildSampleText(12), + PREFS_NETWORK_COL_TX_PACKETS, store); + + mTableViewer = new TableViewer(mTable); + mTableViewer.setContentProvider(new ContentProvider()); + mTableViewer.setLabelProvider(new LabelProvider()); + } + + /** + * Update {@link #mSpeedMillis} to match {@link #mSpeedCombo} selection. + */ + private void updateSpeed() { + switch (mSpeedCombo.getSelectionIndex()) { + case 0: + mSpeedMillis = 100; + break; + case 1: + mSpeedMillis = 250; + break; + case 2: + mSpeedMillis = 500; + break; + } + } + + /** + * Update if {@link SampleThread} should be actively running. Will create + * new thread or finish existing thread to match requested state. + */ + private void updateRunning(boolean shouldRun) { + final boolean alreadyRunning = mSampleThread != null; + if (alreadyRunning && !shouldRun) { + mSampleThread.finish(); + mSampleThread = null; + + mRunningButton.setText("Start"); + mHeader.pack(); + } else if (!alreadyRunning && shouldRun) { + mSampleThread = new SampleThread(); + mSampleThread.start(); + + mRunningButton.setText("Stop"); + mHeader.pack(); + } + } + + @Override + public void setFocus() { + mPanel.setFocus(); + } + + private static java.awt.Color nextSeriesColor(int index) { + return SERIES_COLORS[index % SERIES_COLORS.length]; + } + + /** + * Find a {@link TrackedItem} that matches the requested UID and tag, or + * create one if none exists. + */ + public TrackedItem findOrCreateTrackedItem(int uid, int tag) { + // try searching for existing item + for (TrackedItem item : mTrackedItems) { + if (item.uid == uid && item.tag == tag) { + return item; + } + } + + // nothing found; create new item + final TrackedItem item = new TrackedItem(uid, tag); + if (item.isTotal()) { + item.color = TOTAL_COLOR; + item.label = "Total"; + } else { + final int size = mTrackedItems.size(); + item.color = nextSeriesColor(size); + Formatter formatter = new Formatter(); + item.label = "0x" + formatter.format("%08x", tag); + formatter.close(); + } + + // create color chip to display as legend in table + item.colorImage = new Image(mDisplay, 20, 20); + final GC gc = new GC(item.colorImage); + gc.setBackground(new org.eclipse.swt.graphics.Color(mDisplay, item.color + .getRed(), item.color.getGreen(), item.color.getBlue())); + gc.fillRectangle(item.colorImage.getBounds()); + gc.dispose(); + + mTrackedItems.add(item); + return item; + } + + /** + * Clear all {@link TrackedItem} and chart history. + */ + public void clearTrackedItems() { + mRxTotalSeries.clear(); + mTxTotalSeries.clear(); + + mRxDetailDataset.clear(); + mTxDetailDataset.clear(); + + mTrackedItems.clear(); + mTableViewer.setInput(mTrackedItems); + } + + /** + * Update the {@link #mRenderer} colors to match {@link TrackedItem#color}. + */ + private void updateSeriesPaint() { + for (TrackedItem item : mTrackedItems) { + final int seriesIndex = mRxDetailDataset.getColumnIndex(item.label); + if (seriesIndex >= 0) { + mRenderer.setSeriesPaint(seriesIndex, item.color); + mRenderer.setSeriesFillPaint(seriesIndex, item.color); + } + } + + // series data is always the same color + final int count = mTotalCollection.getSeriesCount(); + for (int i = 0; i < count; i++) { + mTotalRenderer.setSeriesPaint(i, TOTAL_COLOR); + mTotalRenderer.setSeriesFillPaint(i, TOTAL_COLOR); + } + } + + /** + * Traffic flow being actively tracked, uniquely defined by UID and tag. Can + * record {@link NetworkSnapshot} deltas into {@link TimeSeries} for + * charting, and into summary statistics for {@link Table} display. + */ + private class TrackedItem { + public final int uid; + public final int tag; + + public java.awt.Color color; + public Image colorImage; + + public String label; + public long rxBytes; + public long rxPackets; + public long txBytes; + public long txPackets; + + public TrackedItem(int uid, int tag) { + this.uid = uid; + this.tag = tag; + } + + public boolean isTotal() { + return tag == 0x0; + } + + /** + * Record the given {@link NetworkSnapshot} delta, updating + * {@link TimeSeries} and summary statistics. + * + * @param time Timestamp when delta was observed. + * @param deltaMillis Time duration covered by delta, in milliseconds. + */ + public void recordDelta(Millisecond time, long deltaMillis, NetworkSnapshot.Entry delta) { + final long rxBytesPerSecond = (delta.rxBytes * 1000) / deltaMillis; + final long txBytesPerSecond = (delta.txBytes * 1000) / deltaMillis; + + // record values under correct series + if (isTotal()) { + mRxTotalSeries.addOrUpdate(time, rxBytesPerSecond); + mTxTotalSeries.addOrUpdate(time, -txBytesPerSecond); + } else { + mRxDetailDataset.addValue(rxBytesPerSecond, time, label); + mTxDetailDataset.addValue(-txBytesPerSecond, time, label); + } + + rxBytes += delta.rxBytes; + rxPackets += delta.rxPackets; + txBytes += delta.txBytes; + txPackets += delta.txPackets; + } + } + + @Override + public void deviceSelected() { + // treat as client selection to update enabled states + clientSelected(); + } + + @Override + public void clientSelected() { + mActiveUid = -1; + + final Client client = getCurrentClient(); + if (client != null) { + final int pid = client.getClientData().getPid(); + try { + // map PID to UID from device + final UidParser uidParser = new UidParser(); + getCurrentDevice().executeShellCommand("cat /proc/" + pid + "/status", uidParser); + mActiveUid = uidParser.uid; + } catch (TimeoutException e) { + e.printStackTrace(); + } catch (AdbCommandRejectedException e) { + e.printStackTrace(); + } catch (ShellCommandUnresponsiveException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + clearTrackedItems(); + updateRunning(false); + + final boolean validUid = mActiveUid != -1; + mRunningButton.setEnabled(validUid); + } + + @Override + public void clientChanged(Client client, int changeMask) { + // ignored + } + + /** + * Take a snapshot from {@link #getCurrentDevice()}, recording any delta + * network traffic to {@link TrackedItem}. + */ + public void performSample() { + final IDevice device = getCurrentDevice(); + if (device == null) return; + + try { + final NetworkSnapshotParser parser = new NetworkSnapshotParser(); + device.executeShellCommand("cat " + PROC_XT_QTAGUID, parser); + + if (parser.isError()) { + mDisplay.asyncExec(new Runnable() { + @Override + public void run() { + updateRunning(false); + + final String title = "Problem reading stats"; + final String message = "Problem reading xt_qtaguid network " + + "statistics from selected device."; + Status status = new Status(IStatus.ERROR, "NetworkPanel", 0, message, null); + ErrorDialog.openError(mPanel.getShell(), title, title, status); + } + }); + + return; + } + + final NetworkSnapshot snapshot = parser.getParsedSnapshot(); + + // use first snapshot as baseline + if (mLastSnapshot == null) { + mLastSnapshot = snapshot; + return; + } + + final NetworkSnapshot delta = NetworkSnapshot.subtract(snapshot, mLastSnapshot); + mLastSnapshot = snapshot; + + // perform delta updates over on UI thread + if (!mDisplay.isDisposed()) { + mDisplay.syncExec(new UpdateDeltaRunnable(delta, snapshot.timestamp)); + } + + } catch (TimeoutException e) { + e.printStackTrace(); + } catch (AdbCommandRejectedException e) { + e.printStackTrace(); + } catch (ShellCommandUnresponsiveException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * Task that updates UI with given {@link NetworkSnapshot} delta. + */ + private class UpdateDeltaRunnable implements Runnable { + private final NetworkSnapshot mDelta; + private final long mEndTime; + + public UpdateDeltaRunnable(NetworkSnapshot delta, long endTime) { + mDelta = delta; + mEndTime = endTime; + } + + @Override + public void run() { + if (mDisplay.isDisposed()) return; + + final Millisecond time = new Millisecond(new Date(mEndTime)); + for (NetworkSnapshot.Entry entry : mDelta) { + if (mActiveUid != entry.uid) continue; + + final TrackedItem item = findOrCreateTrackedItem(entry.uid, entry.tag); + item.recordDelta(time, mDelta.timestamp, entry); + } + + // remove any historical detail data + final long beforeMillis = mEndTime - HISTORY_MILLIS; + mRxDetailDataset.removeBefore(beforeMillis); + mTxDetailDataset.removeBefore(beforeMillis); + + // trigger refresh from bulk changes above + mRxDetailDataset.fireDatasetChanged(); + mTxDetailDataset.fireDatasetChanged(); + + // update axis to show latest 30 second time period + mDomainAxis.setRange(mEndTime - HISTORY_MILLIS, mEndTime); + + updateSeriesPaint(); + + // kick table viewer to update + mTableViewer.setInput(mTrackedItems); + } + } + + /** + * Parser that extracts UID from remote {@code /proc/pid/status} file. + */ + private static class UidParser extends MultiLineReceiver { + public int uid = -1; + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public void processNewLines(String[] lines) { + for (String line : lines) { + if (line.startsWith("Uid:")) { + // we care about the "real" UID + final String[] cols = line.split("\t"); + uid = Integer.parseInt(cols[1]); + } + } + } + } + + /** + * Parser that populates {@link NetworkSnapshot} based on contents of remote + * {@link NetworkPanel#PROC_XT_QTAGUID} file. + */ + private static class NetworkSnapshotParser extends MultiLineReceiver { + private NetworkSnapshot mSnapshot; + + public NetworkSnapshotParser() { + mSnapshot = new NetworkSnapshot(System.currentTimeMillis()); + } + + public boolean isError() { + return mSnapshot == null; + } + + public NetworkSnapshot getParsedSnapshot() { + return mSnapshot; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public void processNewLines(String[] lines) { + for (String line : lines) { + if (line.endsWith("No such file or directory")) { + mSnapshot = null; + return; + } + + // ignore header line + if (line.startsWith("idx")) { + continue; + } + + final String[] cols = line.split(" "); + if (cols.length < 9) continue; + + // iface and set are currently ignored, which groups those + // entries together. + final NetworkSnapshot.Entry entry = new NetworkSnapshot.Entry(); + + entry.iface = null; //cols[1]; + entry.uid = Integer.parseInt(cols[3]); + entry.set = -1; //Integer.parseInt(cols[4]); + entry.tag = kernelToTag(cols[2]); + entry.rxBytes = Long.parseLong(cols[5]); + entry.rxPackets = Long.parseLong(cols[6]); + entry.txBytes = Long.parseLong(cols[7]); + entry.txPackets = Long.parseLong(cols[8]); + + mSnapshot.combine(entry); + } + } + + /** + * Convert {@code /proc/} tag format to {@link Integer}. Assumes incoming + * format like {@code 0x7fffffff00000000}. + * Matches code in android.server.NetworkManagementSocketTagger + */ + public static int kernelToTag(String string) { + int length = string.length(); + if (length > 10) { + return Long.decode(string.substring(0, length - 8)).intValue(); + } else { + return 0; + } + } + } + + /** + * Parsed snapshot of {@link NetworkPanel#PROC_XT_QTAGUID} at specific time. + */ + private static class NetworkSnapshot implements Iterable { + private ArrayList mStats = new ArrayList(); + + public final long timestamp; + + /** Single parsed statistics row. */ + public static class Entry { + public String iface; + public int uid; + public int set; + public int tag; + public long rxBytes; + public long rxPackets; + public long txBytes; + public long txPackets; + + public boolean isEmpty() { + return rxBytes == 0 && rxPackets == 0 && txBytes == 0 && txPackets == 0; + } + } + + public NetworkSnapshot(long timestamp) { + this.timestamp = timestamp; + } + + public void clear() { + mStats.clear(); + } + + /** + * Combine the given {@link Entry} with any existing {@link Entry}, or + * insert if none exists. + */ + public void combine(Entry entry) { + final Entry existing = findEntry(entry.iface, entry.uid, entry.set, entry.tag); + if (existing != null) { + existing.rxBytes += entry.rxBytes; + existing.rxPackets += entry.rxPackets; + existing.txBytes += entry.txBytes; + existing.txPackets += entry.txPackets; + } else { + mStats.add(entry); + } + } + + @Override + public Iterator iterator() { + return mStats.iterator(); + } + + public Entry findEntry(String iface, int uid, int set, int tag) { + for (Entry entry : mStats) { + if (entry.uid == uid && entry.set == set && entry.tag == tag + && equal(entry.iface, iface)) { + return entry; + } + } + return null; + } + + /** + * Subtract the two given {@link NetworkSnapshot} objects, returning the + * delta between them. + */ + public static NetworkSnapshot subtract(NetworkSnapshot left, NetworkSnapshot right) { + final NetworkSnapshot result = new NetworkSnapshot(left.timestamp - right.timestamp); + + // for each row on left, subtract value from right side + for (Entry leftEntry : left) { + final Entry rightEntry = right.findEntry( + leftEntry.iface, leftEntry.uid, leftEntry.set, leftEntry.tag); + if (rightEntry == null) continue; + + final Entry resultEntry = new Entry(); + resultEntry.iface = leftEntry.iface; + resultEntry.uid = leftEntry.uid; + resultEntry.set = leftEntry.set; + resultEntry.tag = leftEntry.tag; + resultEntry.rxBytes = leftEntry.rxBytes - rightEntry.rxBytes; + resultEntry.rxPackets = leftEntry.rxPackets - rightEntry.rxPackets; + resultEntry.txBytes = leftEntry.txBytes - rightEntry.txBytes; + resultEntry.txPackets = leftEntry.txPackets - rightEntry.txPackets; + + result.combine(resultEntry); + } + + return result; + } + } + + /** + * Provider of {@link #mTrackedItems}. + */ + private class ContentProvider implements IStructuredContentProvider { + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } + + @Override + public void dispose() { + // pass + } + + @Override + public Object[] getElements(Object inputElement) { + return mTrackedItems.toArray(); + } + } + + /** + * Provider of labels for {@Link TrackedItem} values. + */ + private static class LabelProvider implements ITableLabelProvider { + private final DecimalFormat mFormat = new DecimalFormat("#,###"); + + @Override + public Image getColumnImage(Object element, int columnIndex) { + if (element instanceof TrackedItem) { + final TrackedItem item = (TrackedItem) element; + switch (columnIndex) { + case 0: + return item.colorImage; + } + } + return null; + } + + @Override + public String getColumnText(Object element, int columnIndex) { + if (element instanceof TrackedItem) { + final TrackedItem item = (TrackedItem) element; + switch (columnIndex) { + case 0: + return null; + case 1: + return item.label; + case 2: + return mFormat.format(item.rxBytes); + case 3: + return mFormat.format(item.rxPackets); + case 4: + return mFormat.format(item.txBytes); + case 5: + return mFormat.format(item.txPackets); + } + } + return null; + } + + @Override + public void addListener(ILabelProviderListener listener) { + // pass + } + + @Override + public void dispose() { + // pass + } + + @Override + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + @Override + public void removeListener(ILabelProviderListener listener) { + // pass + } + } + + /** + * Format that displays simplified byte units for when given values are + * large enough. + */ + private static class BytesFormat extends NumberFormat { + private final String[] mUnits; + private final DecimalFormat mFormat = new DecimalFormat("#.#"); + + public BytesFormat(boolean perSecond) { + if (perSecond) { + mUnits = new String[] { "B/s", "KB/s", "MB/s" }; + } else { + mUnits = new String[] { "B", "KB", "MB" }; + } + } + + @Override + public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) { + double value = Math.abs(number); + + int i = 0; + while (value > 1024 && i < mUnits.length - 1) { + value /= 1024; + i++; + } + + toAppendTo.append(mFormat.format(value)); + toAppendTo.append(mUnits[i]); + + return toAppendTo; + } + + @Override + public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) { + return format((long) number, toAppendTo, pos); + } + + @Override + public Number parse(String source, ParsePosition parsePosition) { + return null; + } + } + + public static boolean equal(Object a, Object b) { + return a == b || (a != null && a.equals(b)); + } + + /** + * Build stub string of requested length, usually for measurement. + */ + private static String buildSampleText(int length) { + final StringBuilder builder = new StringBuilder(length); + for (int i = 0; i < length; i++) { + builder.append("X"); + } + return builder.toString(); + } + + /** + * Dataset that contains live measurements. Exposes + * {@link #removeBefore(long)} to efficiently remove old data, and enables + * batched {@link #fireDatasetChanged()} events. + */ + public static class LiveTimeTableXYDataset extends AbstractIntervalXYDataset implements + TableXYDataset { + private DefaultKeyedValues2D mValues = new DefaultKeyedValues2D(true); + + /** + * Caller is responsible for triggering {@link #fireDatasetChanged()}. + */ + public void addValue(Number value, TimePeriod rowKey, String columnKey) { + mValues.addValue(value, rowKey, columnKey); + } + + /** + * Caller is responsible for triggering {@link #fireDatasetChanged()}. + */ + public void removeBefore(long beforeMillis) { + while(mValues.getRowCount() > 0) { + final TimePeriod period = (TimePeriod) mValues.getRowKey(0); + if (period.getEnd().getTime() < beforeMillis) { + mValues.removeRow(0); + } else { + break; + } + } + } + + public int getColumnIndex(String key) { + return mValues.getColumnIndex(key); + } + + public void clear() { + mValues.clear(); + fireDatasetChanged(); + } + + @Override + public void fireDatasetChanged() { + super.fireDatasetChanged(); + } + + @Override + public int getItemCount() { + return mValues.getRowCount(); + } + + @Override + public int getItemCount(int series) { + return mValues.getRowCount(); + } + + @Override + public int getSeriesCount() { + return mValues.getColumnCount(); + } + + @Override + public Comparable getSeriesKey(int series) { + return mValues.getColumnKey(series); + } + + @Override + public double getXValue(int series, int item) { + final TimePeriod period = (TimePeriod) mValues.getRowKey(item); + return period.getStart().getTime(); + } + + @Override + public double getStartXValue(int series, int item) { + return getXValue(series, item); + } + + @Override + public double getEndXValue(int series, int item) { + return getXValue(series, item); + } + + @Override + public Number getX(int series, int item) { + return getXValue(series, item); + } + + @Override + public Number getStartX(int series, int item) { + return getXValue(series, item); + } + + @Override + public Number getEndX(int series, int item) { + return getXValue(series, item); + } + + @Override + public Number getY(int series, int item) { + return mValues.getValue(item, series); + } + + @Override + public Number getStartY(int series, int item) { + return getY(series, item); + } + + @Override + public Number getEndY(int series, int item) { + return getY(series, item); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/screenrecord/ScreenRecorderAction.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/screenrecord/ScreenRecorderAction.java new file mode 100644 index 00000000..c4161dcc --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/screenrecord/ScreenRecorderAction.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2013 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.screenrecord; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ddmlib.CollectingOutputReceiver; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.ScreenRecorderOptions; + +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.jface.window.Window; +import org.eclipse.swt.widgets.Shell; + +import java.lang.reflect.InvocationTargetException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class ScreenRecorderAction { + private static final String TITLE = "Screen Recorder"; + private static final String REMOTE_PATH = "/sdcard/ddmsrec.mp4"; + + private final Shell mParentShell; + private final IDevice mDevice; + + public ScreenRecorderAction(Shell parent, IDevice device) { + mParentShell = parent; + mDevice = device; + } + + public void performAction() { + ScreenRecorderOptionsDialog optionsDialog = new ScreenRecorderOptionsDialog(mParentShell); + if (optionsDialog.open() == Window.CANCEL) { + return; + } + + final ScreenRecorderOptions options = new ScreenRecorderOptions.Builder() + .setBitRate(optionsDialog.getBitRate()) + .setSize(optionsDialog.getWidth(), optionsDialog.getHeight()) + .build(); + + final CountDownLatch latch = new CountDownLatch(1); + final CollectingOutputReceiver receiver = new CollectingOutputReceiver(latch); + + new Thread(new Runnable() { + @Override + public void run() { + try { + mDevice.startScreenRecorder(REMOTE_PATH, options, receiver); + } catch (Exception e) { + showError("Unexpected error while launching screenrecorder", e); + latch.countDown(); + } + } + }, "Screen Recorder").start(); + + try { + new ProgressMonitorDialog(mParentShell).run(true, true, new IRunnableWithProgress() { + @Override + public void run(IProgressMonitor monitor) + throws InvocationTargetException, InterruptedException { + int timeInSecond = 0; + monitor.beginTask("Recording...", IProgressMonitor.UNKNOWN); + + while (true) { + // Wait for a second to see if the command has completed + if (latch.await(1, TimeUnit.SECONDS)) { + break; + } + + // update recording time in second + monitor.subTask(String.format("Recording...%d seconds elapsed", timeInSecond++)); + + // If not, check if user has cancelled + if (monitor.isCanceled()) { + receiver.cancel(); + + monitor.subTask("Stopping..."); + + // wait for an additional second to make sure that the command + // completed and screenrecorder finishes writing the output + latch.await(1, TimeUnit.SECONDS); + break; + } + } + } + }); + } catch (InvocationTargetException e) { + showError("Unexpected error while recording: ", e.getTargetException()); + return; + } catch (InterruptedException ignored) { + } + + try { + mDevice.pullFile(REMOTE_PATH, optionsDialog.getDestination().getAbsolutePath()); + } catch (Exception e) { + showError("Unexpected error while copying video recording from device", e); + } + + MessageDialog.openInformation(mParentShell, TITLE, "Screen recording saved at " + + optionsDialog.getDestination().getAbsolutePath()); + } + + private void showError(@NonNull final String message, @Nullable final Throwable e) { + mParentShell.getDisplay().asyncExec(new Runnable() { + @Override + public void run() { + String msg = message; + if (e != null) { + msg += e.getLocalizedMessage() != null ? ": " + e.getLocalizedMessage() : ""; + } + MessageDialog.openError(mParentShell, TITLE, msg); + } + }); + + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/screenrecord/ScreenRecorderOptionsDialog.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/screenrecord/ScreenRecorderOptionsDialog.java new file mode 100644 index 00000000..ccca3df1 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/screenrecord/ScreenRecorderOptionsDialog.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2013 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.screenrecord; + +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.dialogs.TitleAreaDialog; +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.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.FileDialog; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +import java.io.File; +import java.util.Calendar; + +public class ScreenRecorderOptionsDialog extends TitleAreaDialog { + private static final int DEFAULT_BITRATE_MBPS = 4; + + private static String sLastSavedFolder = System.getProperty("user.home"); + private static String sLastFileName = suggestFileName(); + + private static int sBitRateMbps = DEFAULT_BITRATE_MBPS; + private static int sWidth = 0; + private static int sHeight = 0; + + private Text mBitRateText; + private Text mWidthText; + private Text mHeightText; + private Text mDestinationText; + + public ScreenRecorderOptionsDialog(Shell parentShell) { + super(parentShell); + setShellStyle(getShellStyle() | SWT.RESIZE); + } + + @Override + protected Control createDialogArea(Composite shell) { + setTitle("Screen Recorder Options"); + setMessage("Provide screen recorder options. Leave empty to use defaults."); + + Composite parent = (Composite) super.createDialogArea(shell); + Composite c = new Composite(parent, SWT.BORDER); + c.setLayout(new GridLayout(3, false)); + c.setLayoutData(new GridData(GridData.FILL_BOTH)); + + createLabel(c, "Bit Rate (in Mbps)"); + mBitRateText = new Text(c, SWT.BORDER); + mBitRateText.setText(Integer.toString(sBitRateMbps)); + mBitRateText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + createLabel(c, ""); // empty label for 3rd column + + createLabel(c, "Video width (in px, defaults to screen width)"); + mWidthText = new Text(c, SWT.BORDER); + mWidthText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + if (sWidth > 0) { + mWidthText.setText(Integer.toString(sWidth)); + } + createLabel(c, ""); // empty label for 3rd column + + createLabel(c, "Video height (in px, defaults to screen height)"); + mHeightText = new Text(c, SWT.BORDER); + mHeightText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + if (sHeight > 0) { + mHeightText.setText(Integer.toString(sHeight)); + } + createLabel(c, ""); // empty label for 3rd column + + ModifyListener m = new ModifyListener() { + @Override + public void modifyText(ModifyEvent modifyEvent) { + validateAndUpdateState(); + } + }; + mBitRateText.addModifyListener(m); + mWidthText.addModifyListener(m); + mHeightText.addModifyListener(m); + + createLabel(c, "Save Video as: "); + mDestinationText = new Text(c, SWT.BORDER); + mDestinationText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mDestinationText.setText(getFilePath()); + + Button browseButton = new Button(c, SWT.PUSH); + browseButton.setText("Browse"); + browseButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent selectionEvent) { + FileDialog dlg = new FileDialog(getShell(), SWT.SAVE); + + dlg.setText("Save Video..."); + dlg.setFileName(sLastFileName != null ? sLastFileName : suggestFileName()); + if (sLastSavedFolder != null) { + dlg.setFilterPath(sLastSavedFolder); + } + dlg.setFilterNames(new String[] { "MP4 files (*.mp4)" }); + dlg.setFilterExtensions(new String[] { "*.mp4" }); + + String filePath = dlg.open(); + if (filePath != null) { + if (!filePath.endsWith(".mp4")) { + filePath += ".mp4"; + } + + mDestinationText.setText(filePath); + validateAndUpdateState(); + } + } + }); + + return c; + } + + private static String getFilePath() { + return sLastSavedFolder + File.separatorChar + sLastFileName; + } + + private static String suggestFileName() { + Calendar now = Calendar.getInstance(); + return String.format("device-%tF-%tH%tM%tS.mp4", now, now, now, now); + } + + private void createLabel(Composite c, String text) { + Label l = new Label(c, SWT.NONE); + l.setText(text); + GridData gd = new GridData(); + gd.horizontalAlignment = SWT.RIGHT; + l.setLayoutData(gd); + } + + private void validateAndUpdateState() { + int intValue; + + if ((intValue = validateInteger(mBitRateText.getText().trim(), + "Bit Rate has to be an integer")) < 0) { + return; + } + sBitRateMbps = intValue > 0 ? intValue : DEFAULT_BITRATE_MBPS; + + if ((intValue = validateInteger(mWidthText.getText().trim(), + "Recorded video resolution width has to be a valid integer.")) < 0) { + return; + } + if (intValue % 16 != 0) { + setErrorMessage("Width must be a multiple of 16"); + setOkButtonEnabled(false); + return; + } + sWidth = intValue; + + if ((intValue = validateInteger(mHeightText.getText().trim(), + "Recorded video resolution height has to be a valid integer.")) < 0) { + return; + } + if (intValue % 16 != 0) { + setErrorMessage("Height must be a multiple of 16"); + setOkButtonEnabled(false); + return; + } + sHeight = intValue; + + String filePath = mDestinationText.getText(); + File f = new File(filePath); + if (!f.getParentFile().isDirectory()) { + setErrorMessage("The path '" + f.getParentFile().getAbsolutePath() + + "' is not a valid directory."); + setOkButtonEnabled(false); + return; + } + sLastFileName = f.getName(); + sLastSavedFolder = f.getParentFile().getAbsolutePath(); + + setErrorMessage(null); + setOkButtonEnabled(true); + } + + private int validateInteger(String s, String errorMessage) { + if (!s.isEmpty()) { + try { + return Integer.parseInt(s); + } catch (NumberFormatException e) { + setErrorMessage(errorMessage); + setOkButtonEnabled(false); + return -1; + } + } + + return 0; + } + + private void setOkButtonEnabled(boolean en) { + getButton(IDialogConstants.OK_ID).setEnabled(en); + } + + public int getBitRate() { + return sBitRateMbps; + } + + public int getWidth() { + return sWidth; + } + + public int getHeight() { + return sHeight; + } + + public File getDestination() { + return new File(sLastSavedFolder, sLastFileName); + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/vmtrace/VmTraceOptionsDialog.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/vmtrace/VmTraceOptionsDialog.java new file mode 100644 index 00000000..9a65ba18 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/vmtrace/VmTraceOptionsDialog.java @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2013 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.vmtrace; + +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.dialogs.IDialogConstants; +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.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.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +/** Dialog that allows users to select between method tracing or sampler based profiling. */ +public class VmTraceOptionsDialog extends Dialog { + private static final int DEFAULT_SAMPLING_INTERVAL_US = 1000; + + // Static variables that maintain state across invocations of the dialog + private static boolean sTracingEnabled = false; + private static int sSamplingIntervalUs = DEFAULT_SAMPLING_INTERVAL_US; + + public VmTraceOptionsDialog(Shell parentShell) { + super(parentShell); + } + + @Override + protected void configureShell(Shell newShell) { + super.configureShell(newShell); + newShell.setText("Profiling Options"); + } + + @Override + protected Control createDialogArea(Composite shell) { + int horizontalIndent = 30; + + Composite parent = (Composite) super.createDialogArea(shell); + Composite c = new Composite(parent, SWT.NONE); + c.setLayout(new GridLayout(2, false)); + c.setLayoutData(new GridData(GridData.FILL_BOTH)); + + final Button useSamplingButton = new Button(c, SWT.RADIO); + useSamplingButton.setText("Sample based profiling"); + useSamplingButton.setSelection(!sTracingEnabled); + GridData gd = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING, GridData.VERTICAL_ALIGN_CENTER, true, + true, 2, 1); + useSamplingButton.setLayoutData(gd); + + Label l = new Label(c, SWT.NONE); + l.setText("Sample based profiling works by interrupting the VM at a given frequency and \n" + + "collecting the call stacks at that time. The overhead is proportional to the \n" + + "sampling frequency."); + gd = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING, GridData.VERTICAL_ALIGN_CENTER, true, + true, 2, 1); + gd.horizontalIndent = horizontalIndent; + l.setLayoutData(gd); + + l = new Label(c, SWT.NONE); + l.setText("Sampling frequency (microseconds): "); + gd = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING, GridData.VERTICAL_ALIGN_END, + false, true); + gd.horizontalIndent = horizontalIndent; + l.setLayoutData(gd); + + final Text samplingIntervalTextField = new Text(c, SWT.BORDER); + gd = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING, GridData.VERTICAL_ALIGN_CENTER, true, + true); + gd.widthHint = 100; + samplingIntervalTextField.setLayoutData(gd); + samplingIntervalTextField.setEnabled(!sTracingEnabled); + samplingIntervalTextField.setText(Integer.toString(sSamplingIntervalUs)); + samplingIntervalTextField.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent modifyEvent) { + int v = getIntegerValue(samplingIntervalTextField.getText()); + getButton(IDialogConstants.OK_ID).setEnabled(v > 0); + sSamplingIntervalUs = v > 0 ? v : DEFAULT_SAMPLING_INTERVAL_US; + } + + private int getIntegerValue(String text) { + try { + return Integer.parseInt(text); + } catch (NumberFormatException e) { + return -1; + } + } + }); + + final Button useTracingButton = new Button(c, SWT.RADIO); + useTracingButton.setText("Trace based profiling"); + useTracingButton.setSelection(sTracingEnabled); + gd = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING, + GridData.VERTICAL_ALIGN_CENTER, true, true, 2, 1); + useTracingButton.setLayoutData(gd); + + l = new Label(c, SWT.NONE); + l.setText("Trace based profiling works by tracing the entry and exit of every method.\n" + + "This captures the execution of all methods, no matter how small, and hence\n" + + "has a high overhead."); + gd = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING, GridData.VERTICAL_ALIGN_CENTER, true, + true, 2, 1); + gd.horizontalIndent = horizontalIndent; + l.setLayoutData(gd); + + SelectionAdapter selectionAdapter = new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent event) { + sTracingEnabled = useTracingButton.getSelection(); + samplingIntervalTextField.setEnabled(!sTracingEnabled); + } + }; + useTracingButton.addSelectionListener(selectionAdapter); + useSamplingButton.addSelectionListener(selectionAdapter); + + return c; + } + + public boolean shouldUseTracing() { + return sTracingEnabled; + } + + public int getSamplingIntervalMicros() { + return sSamplingIntervalUs; + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/add.png b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/add.png new file mode 100644 index 0000000000000000000000000000000000000000..eefc2ca300a00e1d80a61c8ddf21416d8e4215db GIT binary patch literal 146 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 literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/backward.png b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/capture.png b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/capture.png new file mode 100644 index 0000000000000000000000000000000000000000..da5c10be437316a8c2bd4c099fe7ff82e821f497 GIT binary patch literal 691 zcmV;k0!;mhP)njzcdaRU4tfb%H7b4C zgN5~!7KDo=vZo$mMG+Jgh%I{&3Ry_(L84$WGLi^<(9I-jY|`8xCv*F@UBBBscWz_} z(t&f&x#yhU@7#O8^C?|tE?^_^4jtgAu>s+BprHqdJ_dGwkbA>MV3}V9JFfQuEDC9f zOpvTg;BcPc0{Ev&kpK35D9i`4l0+0KbcGVrd2+cHJGK=S0(ZI&L0O4`vxorvl*(h` z*g^~~S?E*s(#NhIC^xR&;h|6%iU^e$twMx3?sSxWv38%`2}g$yV-KD{&F8`D>fpR` z5&qHB01>3}ysCpweSADPQ&nB(oaH7si}&)Q$ND+@x|y^2PFYWJLw1?TPT_DAqrM^R>buGv>40hjw(?JKyWhc(%oH&BC4|gUwt8eD3J8K< zB@F?;4c!Av@V=Obp|%a)iI?D;jBuPB24U5BT0<(Gk)I_pnEF@DhPJTkfDm9*=2sCB@Vbv|_3etjr>l|89kyoIHq<5gJ zkPxA%#VY+=5wbof8WC2p+k3gSwF$aN(^v-I&mj8pO(JM55-q$s_v&h z&p{b{9^{^TBXW=`8JgkMk*b8&wb$IP@+kkEwL8!ATb_|M;hX&~1je5I!z=%VHI@Il Z`3*g%EuWWcPjUbN002ovPDHLkV1fkdG=u;E literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/clear.png b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/d.png b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/debug-attach.png b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/debug-error.png b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/delete.png b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/device.png b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/diff.png b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/diff.png new file mode 100644 index 0000000000000000000000000000000000000000..bdd9e5c1f3948e995d9e168be8a7f4884d9f31a8 GIT binary patch literal 213 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`EX7WqAsj$Z!;#Vf2?p zUk71ECym(^Ktah8*NBqf{Irtt#G+J&^73-M%)IR4&* z_GTS{lZ-ki=6I>9F7nCVU*7Mx(Sb+eM+5`IiFtCC+otqQ1e(s^>FVdQ&MBb@09c|x AiU0rr literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/displayfilters.png b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/displayfilters.png new file mode 100644 index 0000000000000000000000000000000000000000..d110c2cfb3afe6247caca41c445791da0916bd4d GIT binary patch literal 242 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`EX7WqAsj$Z!;#Vf2?p zUk71ECym(^Ktah8*NBqf{Irtt#G+J&^73-M%)IR41WO<`e#Cm8MF*NS2g8Wf4$?%Y*WxI|AEzEcJqyt1#_!Io!fKx eT_e}mKjw?Oq^#d?VRi)2Y6eeNKbLh*2~7YJ*H0?| literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/down.png b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/e.png b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/file.png b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/forward.png b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/gc.png b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/groupby.png b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/groupby.png new file mode 100644 index 0000000000000000000000000000000000000000..250b98276257e7c1564982c7672618b1433a4ecb GIT binary patch literal 413 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`EX7WqAsj$Z!;#Vf2?p zUk71ECym(^Ktah8*NBqf{Irtt#G+J&^73-M%)IR4v*T<*4r;2-x!*$HnLE-<{fsm{>Tk+X+k(%j<@x6btY zwJ=HcgYpE6lR=Yqac$)LQgVs^YwY!K+1Aw`YX2=|JadP&^woj)inihZfwzHoN2 z+_pB4x=?|W%$bvBx3kUwTDAGdw35BqcCmpI!l&|j#@?u^sFZS9%OJ*-;P&sTL*yyr z@>7o|b(Lyb7Nk5<@n$r!%VuC>c=5Y8*KV5l1-Jc@yKCRfcx=2G7?cd2u6{1-oD!M< D2WYBY literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/halt.png b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/hprof.png b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/pause.png b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/save.png b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/scroll_lock.png b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/scroll_lock.png new file mode 100644 index 0000000000000000000000000000000000000000..5d26689b7f6ec1ee4d4890f39ee4048f0caefe74 GIT binary patch literal 291 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`EX7WqAsj$Z!;#Vf2?p zUk71ECym(^Ktah8*NBqf{Irtt#G+J&^73-M%)IR4?c4Xyf1v+i?^K3KW(W8pmfo{EKB-UhedFf0(%U-kM=(#%s5o*>tH*K({h@y85}Sb4q9e06^Jo-T(jq literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/sort_down.png b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/sort_down.png new file mode 100644 index 0000000000000000000000000000000000000000..2d4ccc1add771d4d8f9d941013bcc6c5746b3207 GIT binary patch literal 102 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4>3X_2hDb;zCrB6^;Mn@po>AQ3 zkVw;&he14H&1(c^CzVu+bUUg&Hj;H!oNK{w*Ois2_5PXVK#dHZu6{1-oD!Mf48F;!lhDb;zCrE@mu)koc(8DAV zSTTd^!KVVY0OmH$0Otc)30#GmO_3bI?2j)`W@89@%*rG(>wOkbFN3G6pUXO@geCy* CXdEH{ literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/thread.png b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/tracing_start.png b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/tracing_stop.png b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/v.png b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/w.png b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/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~YhyYtLgB32?p zUk71ECym(^Ktah8*NBqf{Irtt#G+J&^73-M%)IR4%e|d}0^I8-+&Xp7xPmV~B{?-!u_?mifLlw(Q(l|Ki0os}cIKGr z?|#JRV|b?Ke*e$UK`s(W(ajklXBC#39NyCYv#(>jRaeKYk6kZ5o>vRr8fSla$pWiG ztbQLhP0HLCR`lk9+UoNgw@-TCsJ`R$2ivmiSym4;<&0{&3peI5@996z6Q8^#a0P?> z1I@$XE39tk9BhwB`5|<>*4c0W@;O0Lk0p;+7Ces-3)|Jx@qMf5qZj<{S1LW^Ep#=u kymdKI;Vst0IeX6F8}}l literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/BugReportParserTest.java b/andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/BugReportParserTest.java new file mode 100644 index 00000000..2977390e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/BugReportParserTest.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2012 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.SysinfoPanel.BugReportParser; +import com.android.ddmuilib.SysinfoPanel.BugReportParser.DataValue; +import com.android.ddmuilib.SysinfoPanel.BugReportParser.GfxProfileData; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.util.List; + +import org.junit.Test; +import static org.junit.Assert.*; + +public class BugReportParserTest { + @Test + public void testParseEclairCpuDataSet() throws IOException { + String cpuInfo = + "Currently running services:\n" + + " cpuinfo\n" + + " ----------------------------------------------------------------------------\n" + + " DUMP OF SERVICE cpuinfo:\n" + + " Load: 0.53 / 0.11 / 0.04\n" + + " CPU usage from 33406ms to 28224ms ago:\n" + + " system_server: 56% = 42% user + 13% kernel / faults: 6724 minor 9 major\n" + + " bootanimation: 1% = 0% user + 0% kernel\n" + + " zygote: 0% = 0% user + 0% kernel / faults: 146 minor\n" + + " TOTAL: 98% = 67% user + 30% kernel;\n"; + BufferedReader br = new BufferedReader(new StringReader(cpuInfo)); + List data = BugReportParser.readCpuDataset(br); + + assertEquals(4, data.size()); + assertEquals("system_server (user)", data.get(0).name); + assertEquals("Idle", data.get(3).name); + } + + @Test + public void testParseJbCpuDataSet() throws IOException { + String cpuInfo = + "Load: 1.0 / 1.02 / 0.97\n" + + "CPU usage from 96307ms to 36303ms ago:\n" + + " 0.4% 675/system_server: 0.3% user + 0.1% kernel / faults: 198 minor\n" + + " 0.1% 173/mpdecision: 0% user + 0.1% kernel\n" + + " 0% 2856/kworker/0:2: 0% user + 0% kernel\n" + + " 0% 3128/kworker/0:0: 0% user + 0% kernel\n" + + "0.3% TOTAL: 0.1% user + 0% kernel + 0% iowait\n"; + BufferedReader br = new BufferedReader(new StringReader(cpuInfo)); + List data = BugReportParser.readCpuDataset(br); + + assertEquals(4, data.size()); + assertEquals("675/system_server (user)", data.get(0).name); + assertEquals("Idle", data.get(3).name); + } + + @Test + public void testParseProcRankEclair() throws IOException { + String memInfo = + " 51 39408K 37908K 18731K 14936K system_server\n" + + " 96 27432K 27432K 9501K 6816K android.process.acore\n" + + " 27 248K 248K 83K 76K /system/bin/debuggerd\n"; + BufferedReader br = new BufferedReader(new StringReader(memInfo)); + List data = BugReportParser.readProcRankDataset(br, + " PID Vss Rss Pss Uss cmdline\n"); + + assertEquals(3, data.size()); + assertEquals("debuggerd", data.get(2).name); + if (data.get(0).value - 18731 > 0.0002) { + fail("Unexpected PSS Value " + data.get(0).value); + } + } + + @Test + public void testParseProcRankJb() throws IOException { + String memInfo = + " 675 101120K 100928K 63452K 52624K system_server\n" + + "10170 82100K 82012K 58246K 53580K com.android.chrome:sandboxed_process0\n" + + " 8742 27296K 27224K 6849K 5620K com.google.android.apps.walletnfcrel\n" + + " ------ ------ ------\n" + + " 480598K 394172K TOTAL\n" + + "\n" + + "RAM: 1916984K total, 886404K free, 72036K buffers, 482544K cached, 456K shmem, 34864K slab\n"; + BufferedReader br = new BufferedReader(new StringReader(memInfo)); + List data = BugReportParser.readProcRankDataset(br, + " PID Vss Rss Pss Uss cmdline\n"); + + assertEquals(3, data.size()); + } + + @Test + public void testParseMeminfoEclair() throws IOException { + String memInfo = + "------ MEMORY INFO ------\n" + + "MemTotal: 516528 kB\n" + + "MemFree: 401036 kB\n" + + "Buffers: 0 kB\n" + + " PID Vss Rss Pss Uss cmdline\n" + + " 51 39408K 37908K 18731K 14936K system_server\n" + + " 96 27432K 27432K 9501K 6816K android.process.acore\n" + + " 297 23348K 23348K 5245K 2276K com.android.gallery\n"; + BufferedReader br = new BufferedReader(new StringReader(memInfo)); + List data = BugReportParser.readMeminfoDataset(br); + assertEquals(5, data.size()); + + assertEquals("Free", data.get(0).name); + } + + @Test + public void testParseMeminfoJb() throws IOException { + + String memInfo = // note: This dataset does not have all entries, so the totals will be off + "------ MEMORY INFO ------\n" + + "MemTotal: 1916984 kB\n" + + "MemFree: 888048 kB\n" + + "Buffers: 72036 kB\n" + + " PID Vss Rss Pss Uss cmdline\n" + + " 675 101120K 100928K 63452K 52624K system_server\n" + + "10170 82100K 82012K 58246K 53580K com.android.chrome:sandboxed_process0\n" + + " 8742 27296K 27224K 6849K 5620K com.google.android.apps.walletnfcrel\n" + + " ------ ------ ------\n" + + " 480598K 394172K TOTAL\n" + + "\n" + + "RAM: 1916984K total, 886404K free, 72036K buffers, 482544K cached, 456K shmem, 34864K slab\n"; + + BufferedReader br = new BufferedReader(new StringReader(memInfo)); + List data = BugReportParser.readMeminfoDataset(br); + + assertEquals(6, data.size()); + } + + @Test + public void testParseGfxInfo() throws IOException { + String gfxinfo = + "Applications Graphics Acceleration Info:\n" + + "Uptime: 78455570 Realtime: 78455565\n" + + "\n" + + "** Graphics info for pid 20517 [com.android.launcher] **\n" + + "\n" + + "Recent DisplayList operations\n" + + " DrawDisplayList\n" + + " \n" + + " RestoreToCount\n" + + "\n" + + "Caches:\n" + + "Current memory usage / total memory usage (bytes):\n" + + " TextureCache 4663920 / 25165824\n" + + " \n" + + " FontRenderer 0 262144 / 262144\n" + + "Other:\n" + + " FboCache 2 / 16\n" + + " PatchCache 9 / 512\n" + + "Total memory usage:\n" + + " 13274756 bytes, 12.66 MB\n" + + "\n" + + "Profile data in ms:\n" + + "\n" + + " com.android.launcher/com.android.launcher2.Launcher/android.view.ViewRootImpl@4265d918\n" + + " Draw Process Execute\n" + + " 0.85 1.10 0.61\n" + + " 54.45 0.85 0.52\n" + + " 1.04 2.17 0.73\n" + + " 0.15 0.46 1.01\n" + + "\n" + + "View hierarchy:\n" + + "\n" + + " com.android.launcher/com.android.launcher2.Launcher/android.view.ViewRootImpl@4265d918\n" + + " 276 views, 27.16 kB of display lists, 228 frames rendered\n" + + "\n" + + "\n" + + "Total ViewRootImpl: 1\n" + + "Total Views: 276\n" + + "Total DisplayList: 27.16 kB\n"; + + BufferedReader br = new BufferedReader(new StringReader(gfxinfo)); + List gfxProfile = BugReportParser.parseGfxInfo(br); + + assertEquals(4, gfxProfile.size()); + assertEquals(0.85, gfxProfile.get(0).draw, 0.01); + assertEquals(1.01, gfxProfile.get(3).execute, 0.01); + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/heap/NativeHeapDataImporterTest.java b/andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/heap/NativeHeapDataImporterTest.java new file mode 100644 index 00000000..ea193eba --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/heap/NativeHeapDataImporterTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2011 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.heap; + +import com.android.ddmlib.NativeAllocationInfo; +import com.android.ddmlib.NativeStackCallInfo; + +import org.eclipse.core.runtime.NullProgressMonitor; + +import java.io.StringReader; +import java.lang.reflect.InvocationTargetException; +import java.util.List; + +import org.junit.Test; +import static org.junit.Assert.*; + +public class NativeHeapDataImporterTest { + private static final String BASIC_TEXT = + "Allocations: 1\n" + + "Size: 524292\n" + + "TotalSize: 524292\n" + + "BeginStacktrace:\n" + + " 40170bd8 /libc_malloc_leak.so --- getbacktrace --- /b/malloc_leak.c:258\n" + + " 400910d6 /lib/libc.so --- ca110c --- /bionic/malloc_debug_common.c:227\n" + + " 5dd6abfe /lib/libcgdrv.so --- 5dd6abfe ---\n" + + " 5dd98a8e /lib/libcgdrv.so --- 5dd98a8e ---\n" + + "EndStacktrace\n"; + + private NativeHeapDataImporter mImporter; + + @Test + public void testImportValidAllocation() { + mImporter = createImporter(BASIC_TEXT); + try { + mImporter.run(new NullProgressMonitor()); + } catch (InvocationTargetException e) { + fail("Unexpected exception while parsing text: " + e.getTargetException().getMessage()); + } catch (InterruptedException e) { + fail("Tests are not interrupted!"); + } + + NativeHeapSnapshot snapshot = mImporter.getImportedSnapshot(); + assertNotNull(snapshot); + + // check whether all details have been parsed correctly + assertEquals(1, snapshot.getAllocations().size()); + + NativeAllocationInfo info = snapshot.getAllocations().get(0); + + assertEquals(1, info.getAllocationCount()); + assertEquals(524292, info.getSize()); + assertEquals(true, info.isStackCallResolved()); + + List stack = info.getResolvedStackCall(); + assertEquals(4, stack.size()); + } + + private NativeHeapDataImporter createImporter(String contentsToParse) { + StringReader r = new StringReader(contentsToParse); + return new NativeHeapDataImporter(r); + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/logcat/LogCatFilterSettingsSerializerTest.java b/andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/logcat/LogCatFilterSettingsSerializerTest.java new file mode 100644 index 00000000..5cc756c1 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/logcat/LogCatFilterSettingsSerializerTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2011 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.LogLevel; +import com.android.ddmlib.logcat.LogCatFilter; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + +import org.junit.Test; +import static org.junit.Assert.*; + +public class LogCatFilterSettingsSerializerTest { + /* test that decode(encode(f)) = f */ + public void testSerializer() { + LogCatFilter fs = new LogCatFilter( + "TestFilter", //$NON-NLS-1$ + "Tag'.*Regex", //$NON-NLS-1$ + "regexForTextField..''", //$NON-NLS-1$ + "123", //$NON-NLS-1$ + "TestAppName.*", //$NON-NLS-1$ + LogLevel.ERROR); + + LogCatFilterSettingsSerializer serializer = new LogCatFilterSettingsSerializer(); + String s = serializer.encodeToPreferenceString(Arrays.asList(fs), + new HashMap()); + List decodedFiltersList = serializer.decodeFromPreferenceString(s); + + assertEquals(1, decodedFiltersList.size()); + + LogCatFilter dfs = decodedFiltersList.get(0); + assertEquals(fs.getName(), dfs.getName()); + assertEquals(fs.getTag(), dfs.getTag()); + assertEquals(fs.getText(), dfs.getText()); + assertEquals(fs.getPid(), dfs.getPid()); + assertEquals(fs.getAppName(), dfs.getAppName()); + assertEquals(fs.getLogLevel(), dfs.getLogLevel()); + } + + /* test that transient filters are not persisted */ + @Test + public void testTransientFilters() { + LogCatFilter fs = new LogCatFilter( + "TestFilter", //$NON-NLS-1$ + "Tag'.*Regex", //$NON-NLS-1$ + "regexForTextField..''", //$NON-NLS-1$ + "123", //$NON-NLS-1$ + "TestAppName.*", //$NON-NLS-1$ + LogLevel.ERROR); + LogCatFilterData fd = new LogCatFilterData(fs); + fd.setTransient(); + HashMap fdMap = + new HashMap(); + fdMap.put(fs, fd); + + LogCatFilterSettingsSerializer serializer = new LogCatFilterSettingsSerializer(); + String s = serializer.encodeToPreferenceString(Arrays.asList(fs), fdMap); + List decodedFiltersList = serializer.decodeFromPreferenceString(s); + + assertEquals(0, decodedFiltersList.size()); + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/logcat/LogCatStackTraceParserTest.java b/andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/logcat/LogCatStackTraceParserTest.java new file mode 100644 index 00000000..b9a588fd --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/logcat/LogCatStackTraceParserTest.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2011 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.junit.Test; +import static org.junit.Assert.*; + +import org.junit.Before; + +public class LogCatStackTraceParserTest { + private LogCatStackTraceParser mTranslator; + + private static final String SAMPLE_METHOD = "com.foo.Class.method"; //$NON-NLS-1$ + private static final String SAMPLE_FNAME = "FileName"; //$NON-NLS-1$ + private static final int SAMPLE_LINENUM = 20; + private static final String SAMPLE_TRACE = + String.format(" at %s(%s.groovy:%d)", //$NON-NLS-1$ + SAMPLE_METHOD, SAMPLE_FNAME, SAMPLE_LINENUM); + + @Before + public void setUp() throws Exception { + mTranslator = new LogCatStackTraceParser(); + } + + @Test + public void testIsValidExceptionTrace() { + assertTrue(mTranslator.isValidExceptionTrace(SAMPLE_TRACE)); + assertFalse(mTranslator.isValidExceptionTrace( + "java.lang.RuntimeException: message")); //$NON-NLS-1$ + assertFalse(mTranslator.isValidExceptionTrace( + "at com.foo.test(Ins.java:unknown)")); //$NON-NLS-1$ + } + + @Test + public void testGetMethodName() { + assertEquals(SAMPLE_METHOD, mTranslator.getMethodName(SAMPLE_TRACE)); + } + + @Test + public void testGetFileName() { + assertEquals(SAMPLE_FNAME, mTranslator.getFileName(SAMPLE_TRACE)); + } + + @Test + public void testGetLineNumber() { + assertEquals(SAMPLE_LINENUM, mTranslator.getLineNumber(SAMPLE_TRACE)); + } +} diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/logcat/RollingBufferFindTest.java b/andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/logcat/RollingBufferFindTest.java new file mode 100644 index 00000000..75e1df84 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/test-src/com/android/ddmuilib/logcat/RollingBufferFindTest.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2012 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.AbstractBufferFindTarget; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; +import static org.junit.Assert.*; + +public class RollingBufferFindTest { + public class FindTarget extends AbstractBufferFindTarget { + private int mSelectedItem = -1; + private int mItemReadCount = 0; + private List mItems = Arrays.asList( + "abc", + "def", + "abc", + null, + "xyz" + ); + + @Override + public int getItemCount() { + return mItems.size(); + } + + @Override + public String getItem(int index) { + mItemReadCount++; + return mItems.get(index); + } + + @Override + public void selectAndReveal(int index) { + mSelectedItem = index; + } + + @Override + public int getStartingIndex() { + return mItems.size() - 1; + } + } + FindTarget mFindTarget = new FindTarget(); + + @Test + public void testMultipleMatch() { + mFindTarget.mSelectedItem = -1; + + String text = "abc"; + int lastIndex = mFindTarget.mItems.lastIndexOf(text); + int firstIndex = mFindTarget.mItems.indexOf(text); + + // the first time we search through the buffer we should hit the item at lastIndex + assertTrue(mFindTarget.findAndSelect(text, true, false)); + assertEquals(lastIndex, mFindTarget.mSelectedItem); + + // subsequent search should hit the item at first index + assertTrue(mFindTarget.findAndSelect(text, false, false)); + assertEquals(firstIndex, mFindTarget.mSelectedItem); + + // search again should roll over and hit the last index + assertTrue(mFindTarget.findAndSelect(text, false, false)); + assertEquals(lastIndex, mFindTarget.mSelectedItem); + } + + @Test + public void testMissingItem() { + mFindTarget.mSelectedItem = -1; + mFindTarget.mItemReadCount = 0; + + // should not match + assertFalse(mFindTarget.findAndSelect("nonexistent", true, false)); + + // no item should be selected + assertEquals(-1, mFindTarget.mSelectedItem); + + // but all items should have been read in once + assertEquals(mFindTarget.getItemCount(), mFindTarget.mItemReadCount); + } + + @Test + public void testSearchDirection() { + String text = "abc"; + int lastIndex = mFindTarget.mItems.lastIndexOf(text); + int firstIndex = mFindTarget.mItems.indexOf(text); + + // the first time we search through the buffer we should hit the "abc" from the last + assertTrue(mFindTarget.findAndSelect(text, true, false)); + assertEquals(lastIndex, mFindTarget.mSelectedItem); + + // searching forward from there should also hit the first index + assertTrue(mFindTarget.findAndSelect(text, false, true)); + assertEquals(firstIndex, mFindTarget.mSelectedItem); + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.classpath b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.classpath new file mode 100644 index 00000000..00782ce2 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.gitignore b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.gitignore new file mode 100644 index 00000000..0f630157 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.gitignore @@ -0,0 +1,2 @@ +/target/ +/bin/ diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.project b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.project new file mode 100644 index 00000000..2be391f8 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.project @@ -0,0 +1,34 @@ + + + org.eclipse.andmore.hierarchyviewer2lib + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.settings/README.txt b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.settings/README.txt new file mode 100644 index 00000000..2945bee0 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.settings/README.txt @@ -0,0 +1,2 @@ +Copy this in eclipse project as a .settings folder at the root. +This ensure proper compilation compliance and warning/error levels. \ No newline at end of file diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.core.resources.prefs b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..4824b802 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.jdt.core.prefs b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..295926d9 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,7 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.m2e.core.prefs b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/META-INF/MANIFEST.MF b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/META-INF/MANIFEST.MF new file mode 100644 index 00000000..8e2eace2 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/META-INF/MANIFEST.MF @@ -0,0 +1,20 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Localization: plugin +Bundle-Name: %Bundle-Name +Bundle-SymbolicName: org.eclipse.andmore.hierarchyviewer2lib;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Vendor: %Bundle-Vendor +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Require-Bundle: org.eclipse.core.commands;bundle-version="3.8.1", + org.eclipse.equinox.common;bundle-version="3.8.0", + org.eclipse.jface;bundle-version="3.12.2", + org.eclipse.andmore.swt, + org.eclipse.andmore.swtmenubar, + org.eclipse.andmore.ddmuilib +Export-Package: com.android.hierarchyviewerlib, + com.android.hierarchyviewerlib.actions, + com.android.hierarchyviewerlib.device, + com.android.hierarchyviewerlib.models, + com.android.hierarchyviewerlib.ui +Bundle-ClassPath: . diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/NOTICE b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/NOTICE new file mode 100644 index 00000000..70c54220 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/NOTICE @@ -0,0 +1,190 @@ + + Copyright (c) 2005-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. + + 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. + + + 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 + diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/build.gradle b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/build.gradle new file mode 100644 index 00000000..55308716 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/build.gradle @@ -0,0 +1,16 @@ +group = 'com.android.tools' +archivesBaseName = 'hierarchyviewer2lib' + +dependencies { + compile project(':base:ddmlib') + compile project(':swt:ddmuilib') + + compile 'com.google.guava:guava:15.0' + + testCompile 'junit:junit:3.8.1' +} + +sourceSets { + main.resources.srcDir 'src/main/java' + test.resources.srcDir 'src/test/java' +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/build.properties b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/build.properties new file mode 100644 index 00000000..ecc09051 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/build.properties @@ -0,0 +1,4 @@ +source.. = src/main/java/ +output.. = bin/ +bin.includes = META-INF/,\ + . diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/hierarchyviewer2lib.iml b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/hierarchyviewer2lib.iml new file mode 100644 index 00000000..206f6712 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/hierarchyviewer2lib.iml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/plugin.properties b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/plugin.properties new file mode 100644 index 00000000..16ab8145 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/plugin.properties @@ -0,0 +1,4 @@ +#Properties file for com.android.ide.eclipse.hierarchyviewer2lib +Bundle-Vendor = Eclipse Andmore +Bundle-Name = UI Hierarchy Viewer 2 library +category.name = Android diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/pom.xml b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/pom.xml new file mode 100644 index 00000000..b7c2527b --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + ../pom.xml + org.eclipse.andmore + swt-droid-parent + 0.5.2-SNAPSHOT + + org.eclipse.andmore.hierarchyviewer2lib + eclipse-plugin + hierarchyviewer2lib + + + + org.eclipse.tycho + tycho-maven-plugin + true + + + org.eclipse.tycho + tycho-source-plugin + ${tycho-version} + + + plugin-source + + plugin-source + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/HierarchyViewerDirector.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/HierarchyViewerDirector.java new file mode 100644 index 00000000..ac4b5bb0 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/HierarchyViewerDirector.java @@ -0,0 +1,791 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib; + +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.Log; +import com.android.hierarchyviewerlib.device.DeviceBridge; +import com.android.hierarchyviewerlib.device.HvDeviceFactory; +import com.android.hierarchyviewerlib.device.IHvDevice; +import com.android.hierarchyviewerlib.device.WindowUpdater; +import com.android.hierarchyviewerlib.device.WindowUpdater.IWindowChangeListener; +import com.android.hierarchyviewerlib.models.DeviceSelectionModel; +import com.android.hierarchyviewerlib.models.ThemeModel; +import com.android.hierarchyviewerlib.models.PixelPerfectModel; +import com.android.hierarchyviewerlib.models.TreeViewModel; +import com.android.hierarchyviewerlib.models.ViewNode; +import com.android.hierarchyviewerlib.models.Window; +import com.android.hierarchyviewerlib.ui.CaptureDisplay; +import com.android.hierarchyviewerlib.ui.DumpThemeDisplay; +import com.android.hierarchyviewerlib.ui.EvaluateContrastDisplay; +import com.android.hierarchyviewerlib.ui.TreeView; +import com.android.hierarchyviewerlib.ui.util.DrawableViewNode; +import com.android.hierarchyviewerlib.ui.util.PsdFile; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.ImageLoader; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Shell; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; + +/** + * This is the class where most of the logic resides. + */ +public abstract class HierarchyViewerDirector implements IDeviceChangeListener, + IWindowChangeListener { + private static final boolean sIsUsingDdmProtocol; + static { + String sHvProtoEnvVar = System.getenv("ANDROID_HVPROTO"); //$NON-NLS-1$ + sIsUsingDdmProtocol = "ddm".equalsIgnoreCase(sHvProtoEnvVar); + } + + protected static HierarchyViewerDirector sDirector; + + public static final String TAG = "hierarchyviewer"; + + private int mPixelPerfectRefreshesInProgress = 0; + + private Timer mPixelPerfectRefreshTimer = new Timer(); + + private boolean mAutoRefresh = false; + + public static final int DEFAULT_PIXEL_PERFECT_AUTOREFRESH_INTERVAL = 5; + + private int mPixelPerfectAutoRefreshInterval = DEFAULT_PIXEL_PERFECT_AUTOREFRESH_INTERVAL; + + private PixelPerfectAutoRefreshTask mCurrentAutoRefreshTask; + + private String mFilterText = ""; //$NON-NLS-1$ + + private static final Object mDevicesLock = new Object(); + private Map mDevices = new HashMap(10); + + public static boolean isUsingDdmProtocol() { + return sIsUsingDdmProtocol; + } + + public void terminate() { + WindowUpdater.terminate(); + mPixelPerfectRefreshTimer.cancel(); + } + + public abstract String getAdbLocation(); + + public static HierarchyViewerDirector getDirector() { + return sDirector; + } + + /** + * Init the DeviceBridge with an existing {@link AndroidDebugBridge}. + * @param bridge the bridge object to use + */ + public void acquireBridge(AndroidDebugBridge bridge) { + DeviceBridge.acquireBridge(bridge); + } + + /** + * Creates an {@link AndroidDebugBridge} connected to adb at the given location. + * + * If a bridge is already running, this disconnects it and creates a new one. + * + * @param adbLocation the location to adb. + */ + public void initDebugBridge() { + DeviceBridge.initDebugBridge(getAdbLocation()); + } + + public void stopDebugBridge() { + DeviceBridge.terminate(); + } + + public void populateDeviceSelectionModel() { + IDevice[] devices = DeviceBridge.getDevices(); + for (IDevice device : devices) { + deviceConnected(device); + } + } + + public void startListenForDevices() { + DeviceBridge.startListenForDevices(this); + } + + public void stopListenForDevices() { + DeviceBridge.stopListenForDevices(this); + } + + public abstract void executeInBackground(String taskName, Runnable task); + + @Override + public void deviceConnected(final IDevice device) { + executeInBackground("Connecting device", new Runnable() { + @Override + public void run() { + if (!device.isOnline()) { + return; + } + + IHvDevice hvDevice; + synchronized (mDevicesLock) { + hvDevice = mDevices.get(device); + if (hvDevice == null) { + hvDevice = HvDeviceFactory.create(device); + hvDevice.initializeViewDebug(); + hvDevice.addWindowChangeListener(getDirector()); + mDevices.put(device, hvDevice); + } else { + // attempt re-initializing view server if device state has changed + hvDevice.initializeViewDebug(); + } + } + + DeviceSelectionModel.getModel().addDevice(hvDevice); + focusChanged(device); + } + }); + } + + @Override + public void deviceDisconnected(final IDevice device) { + executeInBackground("Disconnecting device", new Runnable() { + @Override + public void run() { + IHvDevice hvDevice; + synchronized (mDevicesLock) { + hvDevice = mDevices.get(device); + if (hvDevice != null) { + mDevices.remove(device); + } + } + + if (hvDevice == null) { + return; + } + + hvDevice.terminateViewDebug(); + hvDevice.removeWindowChangeListener(getDirector()); + DeviceSelectionModel.getModel().removeDevice(hvDevice); + if (PixelPerfectModel.getModel().getDevice() == device) { + PixelPerfectModel.getModel().setData(null, null, null); + } + Window treeViewWindow = TreeViewModel.getModel().getWindow(); + if (treeViewWindow != null && treeViewWindow.getDevice() == device) { + TreeViewModel.getModel().setData(null, null); + mFilterText = ""; //$NON-NLS-1$ + } + } + }); + } + + @Override + public void windowsChanged(final IDevice device) { + executeInBackground("Refreshing windows", new Runnable() { + @Override + public void run() { + IHvDevice hvDevice = getHvDevice(device); + hvDevice.reloadWindows(); + DeviceSelectionModel.getModel().updateDevice(hvDevice); + } + }); + } + + @Override + public void focusChanged(final IDevice device) { + executeInBackground("Updating focus", new Runnable() { + @Override + public void run() { + IHvDevice hvDevice = getHvDevice(device); + int focusedWindow = hvDevice.getFocusedWindow(); + DeviceSelectionModel.getModel().updateFocusedWindow(hvDevice, focusedWindow); + } + }); + } + + @Override + public void deviceChanged(IDevice device, int changeMask) { + if ((changeMask & IDevice.CHANGE_STATE) != 0 && device.isOnline()) { + deviceConnected(device); + } + } + + public void refreshPixelPerfect() { + final IDevice device = PixelPerfectModel.getModel().getDevice(); + if (device != null) { + // Some interesting logic here. We don't want to refresh the pixel + // perfect view 1000 times in a row if the focus keeps changing. We + // just + // want it to refresh following the last focus change. + boolean proceed = false; + synchronized (this) { + if (mPixelPerfectRefreshesInProgress <= 1) { + proceed = true; + mPixelPerfectRefreshesInProgress++; + } + } + if (proceed) { + executeInBackground("Refreshing pixel perfect screenshot", new Runnable() { + @Override + public void run() { + Image screenshotImage = getScreenshotImage(getHvDevice(device)); + if (screenshotImage != null) { + PixelPerfectModel.getModel().setImage(screenshotImage); + } + synchronized (HierarchyViewerDirector.this) { + mPixelPerfectRefreshesInProgress--; + } + } + + }); + } + } + } + + public void refreshPixelPerfectTree() { + final IDevice device = PixelPerfectModel.getModel().getDevice(); + if (device != null) { + executeInBackground("Refreshing pixel perfect tree", new Runnable() { + @Override + public void run() { + IHvDevice hvDevice = getHvDevice(device); + ViewNode viewNode = + hvDevice.loadWindowData(Window.getFocusedWindow(hvDevice)); + if (viewNode != null) { + PixelPerfectModel.getModel().setTree(viewNode); + } + } + + }); + } + } + + public void loadPixelPerfectData(final IHvDevice hvDevice) { + executeInBackground("Loading pixel perfect data", new Runnable() { + @Override + public void run() { + Image screenshotImage = getScreenshotImage(hvDevice); + if (screenshotImage != null) { + ViewNode viewNode = + hvDevice.loadWindowData(Window.getFocusedWindow(hvDevice)); + if (viewNode != null) { + PixelPerfectModel.getModel().setData(hvDevice.getDevice(), + screenshotImage, viewNode); + } + } + } + }); + } + + private IHvDevice getHvDevice(IDevice device) { + synchronized (mDevicesLock) { + return mDevices.get(device); + } + } + + private Image getScreenshotImage(IHvDevice hvDevice) { + return (hvDevice == null) ? null : hvDevice.getScreenshotImage(); + } + + public void loadViewTreeData(final Window window) { + executeInBackground("Loading view hierarchy", new Runnable() { + @Override + public void run() { + mFilterText = ""; //$NON-NLS-1$ + + IHvDevice hvDevice = window.getHvDevice(); + ViewNode viewNode = hvDevice.loadWindowData(window); + if (viewNode != null) { + viewNode.setViewCount(); + TreeViewModel.getModel().setData(window, viewNode); + } + } + }); + } + + public void loadOverlay(final Shell shell) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + FileDialog fileDialog = new FileDialog(shell, SWT.OPEN); + fileDialog.setFilterExtensions(new String[] { + "*.jpg;*.jpeg;*.png;*.gif;*.bmp" //$NON-NLS-1$ + }); + fileDialog.setFilterNames(new String[] { + "Image (*.jpg, *.jpeg, *.png, *.gif, *.bmp)" + }); + fileDialog.setText("Choose an overlay image"); + String fileName = fileDialog.open(); + if (fileName != null) { + try { + Image image = new Image(Display.getDefault(), fileName); + PixelPerfectModel.getModel().setOverlayImage(image); + } catch (SWTException e) { + Log.e(TAG, "Unable to load image from " + fileName); + } + } + } + }); + } + + public void showCapture(final Shell shell, final ViewNode viewNode) { + executeInBackground("Capturing node", new Runnable() { + @Override + public void run() { + final Image image = loadCapture(viewNode); + if (image != null) { + + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + CaptureDisplay.show(shell, viewNode, image); + } + }); + } + } + }); + } + + public void showEvaluateContrast(final Shell shell) { + executeInBackground("Capturing node and evaluating contrast", new Runnable() { + @Override + public void run() { + mFilterText = ""; //$NON-NLS-1$ + Window window = TreeViewModel.getModel().getWindow(); + IHvDevice hvDevice = window.getHvDevice(); + final ViewNode viewNode = hvDevice.loadWindowData(window); + if (viewNode != null) { + viewNode.setViewCount(); + TreeViewModel.getModel().setData(window, viewNode); + } + + final Image image = loadCapture(viewNode); + if (image != null && viewNode != null) { + + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + EvaluateContrastDisplay.show(shell, viewNode, image); + } + }); + } + } + }); + } + + public void showDumpTheme(final Shell shell) { + executeInBackground("Capturing node and dumping theme", new Runnable() { + @Override + public void run() { + ViewNode viewNode; + Window window = TreeViewModel.getModel().getWindow(); + IHvDevice hvDevice = window.getHvDevice(); + + DrawableViewNode tree = TreeViewModel.getModel().getTree(); + if (tree == null) { + viewNode = hvDevice.loadWindowData(window); + } else { + viewNode = tree.viewNode; + } + + final ThemeModel model = hvDevice.dumpTheme(viewNode); + if (model != null) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + DumpThemeDisplay.show(shell, model); + } + }); + } else { + Log.e(TAG, "Unable to dump theme."); + } + } + }); + } + + public Image loadCapture(ViewNode viewNode) { + IHvDevice hvDevice = viewNode.window.getHvDevice(); + final Image image = hvDevice.loadCapture(viewNode.window, viewNode); + if (image != null) { + viewNode.image = image; + + // Force the layout viewer to redraw. + TreeViewModel.getModel().notifySelectionChanged(); + } + return image; + } + + public void loadCaptureInBackground(final ViewNode viewNode) { + executeInBackground("Capturing node", new Runnable() { + @Override + public void run() { + loadCapture(viewNode); + } + }); + } + + public void showCapture(Shell shell) { + DrawableViewNode viewNode = TreeViewModel.getModel().getSelection(); + if (viewNode != null) { + showCapture(shell, viewNode.viewNode); + } + } + + public void refreshWindows() { + executeInBackground("Refreshing windows", new Runnable() { + @Override + public void run() { + IHvDevice[] hvDevicesA = DeviceSelectionModel.getModel().getDevices(); + IDevice[] devicesA = new IDevice[hvDevicesA.length]; + for (int i = 0; i < hvDevicesA.length; i++) { + devicesA[i] = hvDevicesA[i].getDevice(); + } + IDevice[] devicesB = DeviceBridge.getDevices(); + HashSet deviceSet = new HashSet(); + for (int i = 0; i < devicesB.length; i++) { + deviceSet.add(devicesB[i]); + } + for (int i = 0; i < devicesA.length; i++) { + if (deviceSet.contains(devicesA[i])) { + windowsChanged(devicesA[i]); + deviceSet.remove(devicesA[i]); + } else { + deviceDisconnected(devicesA[i]); + } + } + for (IDevice device : deviceSet) { + deviceConnected(device); + } + } + }); + } + + public void loadViewHierarchy() { + Window window = DeviceSelectionModel.getModel().getSelectedWindow(); + if (window != null) { + loadViewTreeData(window); + } + } + + public void inspectScreenshot() { + IHvDevice device = DeviceSelectionModel.getModel().getSelectedDevice(); + if (device != null) { + loadPixelPerfectData(device); + } + } + + public void saveTreeView(final Shell shell) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + final DrawableViewNode viewNode = TreeViewModel.getModel().getTree(); + if (viewNode != null) { + FileDialog fileDialog = new FileDialog(shell, SWT.SAVE); + fileDialog.setFilterExtensions(new String[] { + "*.png" //$NON-NLS-1$ + }); + fileDialog.setFilterNames(new String[] { + "Portable Network Graphics File (*.png)" + }); + fileDialog.setText("Choose where to save the tree image"); + final String fileName = fileDialog.open(); + if (fileName != null) { + executeInBackground("Saving tree view", new Runnable() { + @Override + public void run() { + Image image = TreeView.paintToImage(viewNode); + ImageLoader imageLoader = new ImageLoader(); + imageLoader.data = new ImageData[] { + image.getImageData() + }; + String extensionedFileName = fileName; + if (!extensionedFileName.toLowerCase().endsWith(".png")) { //$NON-NLS-1$ + extensionedFileName += ".png"; //$NON-NLS-1$ + } + try { + imageLoader.save(extensionedFileName, SWT.IMAGE_PNG); + } catch (SWTException e) { + Log.e(TAG, "Unable to save tree view as a PNG image at " + + fileName); + } + image.dispose(); + } + }); + } + } + } + }); + } + + public void savePixelPerfect(final Shell shell) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + Image untouchableImage = PixelPerfectModel.getModel().getImage(); + if (untouchableImage != null) { + final ImageData imageData = untouchableImage.getImageData(); + FileDialog fileDialog = new FileDialog(shell, SWT.SAVE); + fileDialog.setFilterExtensions(new String[] { + "*.png" //$NON-NLS-1$ + }); + fileDialog.setFilterNames(new String[] { + "Portable Network Graphics File (*.png)" + }); + fileDialog.setText("Choose where to save the screenshot"); + final String fileName = fileDialog.open(); + if (fileName != null) { + executeInBackground("Saving pixel perfect", new Runnable() { + @Override + public void run() { + ImageLoader imageLoader = new ImageLoader(); + imageLoader.data = new ImageData[] { + imageData + }; + String extensionedFileName = fileName; + if (!extensionedFileName.toLowerCase().endsWith(".png")) { //$NON-NLS-1$ + extensionedFileName += ".png"; //$NON-NLS-1$ + } + try { + imageLoader.save(extensionedFileName, SWT.IMAGE_PNG); + } catch (SWTException e) { + Log.e(TAG, "Unable to save tree view as a PNG image at " + + fileName); + } + } + }); + } + } + } + }); + } + + public void capturePSD(final Shell shell) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + final Window window = TreeViewModel.getModel().getWindow(); + if (window != null) { + FileDialog fileDialog = new FileDialog(shell, SWT.SAVE); + fileDialog.setFilterExtensions(new String[] { + "*.psd" //$NON-NLS-1$ + }); + fileDialog.setFilterNames(new String[] { + "Photoshop Document (*.psd)" + }); + fileDialog.setText("Choose where to save the window layers"); + final String fileName = fileDialog.open(); + if (fileName != null) { + executeInBackground("Saving window layers", new Runnable() { + @Override + public void run() { + IHvDevice hvDevice = getHvDevice(window.getDevice()); + PsdFile psdFile = hvDevice.captureLayers(window); + if (psdFile != null) { + String extensionedFileName = fileName; + if (!extensionedFileName.toLowerCase().endsWith(".psd")) { //$NON-NLS-1$ + extensionedFileName += ".psd"; //$NON-NLS-1$ + } + try { + psdFile.write(new FileOutputStream(extensionedFileName)); + } catch (FileNotFoundException e) { + Log.e(TAG, "Unable to write to file " + fileName); + } + } + } + }); + } + } + } + }); + } + + public void reloadViewHierarchy() { + Window window = TreeViewModel.getModel().getWindow(); + if (window != null) { + loadViewTreeData(window); + } + } + + public void invalidateCurrentNode() { + final DrawableViewNode selectedNode = TreeViewModel.getModel().getSelection(); + if (selectedNode != null) { + executeInBackground("Invalidating view", new Runnable() { + @Override + public void run() { + IHvDevice hvDevice = getHvDevice(selectedNode.viewNode.window.getDevice()); + hvDevice.invalidateView(selectedNode.viewNode); + } + }); + } + } + + public void relayoutCurrentNode() { + final DrawableViewNode selectedNode = TreeViewModel.getModel().getSelection(); + if (selectedNode != null) { + executeInBackground("Request layout", new Runnable() { + @Override + public void run() { + IHvDevice hvDevice = getHvDevice(selectedNode.viewNode.window.getDevice()); + hvDevice.requestLayout(selectedNode.viewNode); + } + }); + } + } + + public void dumpDisplayListForCurrentNode() { + final DrawableViewNode selectedNode = TreeViewModel.getModel().getSelection(); + if (selectedNode != null) { + executeInBackground("Dump displaylist", new Runnable() { + @Override + public void run() { + IHvDevice hvDevice = getHvDevice(selectedNode.viewNode.window.getDevice()); + hvDevice.outputDisplayList(selectedNode.viewNode); + } + }); + } + } + + public void profileCurrentNode() { + final DrawableViewNode selectedNode = TreeViewModel.getModel().getSelection(); + if (selectedNode != null) { + executeInBackground("Profile Node", new Runnable() { + @Override + public void run() { + IHvDevice hvDevice = getHvDevice(selectedNode.viewNode.window.getDevice()); + hvDevice.loadProfileData(selectedNode.viewNode.window, selectedNode.viewNode); + // Force the layout viewer to redraw. + TreeViewModel.getModel().notifySelectionChanged(); + } + }); + } + } + + public void invokeMethodOnSelectedView(final String method, final List args) { + final DrawableViewNode selectedNode = TreeViewModel.getModel().getSelection(); + if (selectedNode != null) { + executeInBackground("Invoke View Method", new Runnable() { + @Override + public void run() { + IHvDevice hvDevice = getHvDevice(selectedNode.viewNode.window.getDevice()); + hvDevice.invokeViewMethod(selectedNode.viewNode.window, selectedNode.viewNode, + method, args); + } + }); + } + } + + public void loadAllViews() { + executeInBackground("Loading all views", new Runnable() { + @Override + public void run() { + DrawableViewNode tree = TreeViewModel.getModel().getTree(); + if (tree != null) { + loadViewRecursive(tree.viewNode); + // Force the layout viewer to redraw. + TreeViewModel.getModel().notifySelectionChanged(); + } + } + }); + } + + private void loadViewRecursive(ViewNode viewNode) { + IHvDevice hvDevice = getHvDevice(viewNode.window.getDevice()); + Image image = hvDevice.loadCapture(viewNode.window, viewNode); + if (image == null) { + return; + } + viewNode.image = image; + final int N = viewNode.children.size(); + for (int i = 0; i < N; i++) { + loadViewRecursive(viewNode.children.get(i)); + } + } + + public void filterNodes(String filterText) { + this.mFilterText = filterText; + DrawableViewNode tree = TreeViewModel.getModel().getTree(); + if (tree != null) { + tree.viewNode.filter(filterText); + // Force redraw + TreeViewModel.getModel().notifySelectionChanged(); + } + } + + public String getFilterText() { + return mFilterText; + } + + private static class PixelPerfectAutoRefreshTask extends TimerTask { + @Override + public void run() { + HierarchyViewerDirector.getDirector().refreshPixelPerfect(); + } + }; + + public void setPixelPerfectAutoRefresh(boolean value) { + synchronized (mPixelPerfectRefreshTimer) { + if (value == mAutoRefresh) { + return; + } + mAutoRefresh = value; + if (mAutoRefresh) { + mCurrentAutoRefreshTask = new PixelPerfectAutoRefreshTask(); + mPixelPerfectRefreshTimer.schedule(mCurrentAutoRefreshTask, + mPixelPerfectAutoRefreshInterval * 1000, + mPixelPerfectAutoRefreshInterval * 1000); + } else { + mCurrentAutoRefreshTask.cancel(); + mCurrentAutoRefreshTask = null; + } + } + } + + public void setPixelPerfectAutoRefreshInterval(int value) { + synchronized (mPixelPerfectRefreshTimer) { + if (mPixelPerfectAutoRefreshInterval == value) { + return; + } + mPixelPerfectAutoRefreshInterval = value; + if (mAutoRefresh) { + mCurrentAutoRefreshTask.cancel(); + long timeLeft = + Math.max(0, mPixelPerfectAutoRefreshInterval + * 1000 + - (System.currentTimeMillis() - mCurrentAutoRefreshTask + .scheduledExecutionTime())); + mCurrentAutoRefreshTask = new PixelPerfectAutoRefreshTask(); + mPixelPerfectRefreshTimer.schedule(mCurrentAutoRefreshTask, timeLeft, + mPixelPerfectAutoRefreshInterval * 1000); + } + } + } + + public int getPixelPerfectAutoRefreshInverval() { + return mPixelPerfectAutoRefreshInterval; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/CapturePSDAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/CapturePSDAction.java new file mode 100644 index 00000000..4331c7d1 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/CapturePSDAction.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.actions; + +import com.android.ddmuilib.ImageLoader; +import com.android.hierarchyviewerlib.HierarchyViewerDirector; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +public class CapturePSDAction extends TreeViewEnabledAction implements ImageAction { + + private static CapturePSDAction sAction; + + private Image mImage; + + private Shell mShell; + + private CapturePSDAction(Shell shell) { + super("&Capture Layers"); + this.mShell = shell; + setAccelerator(SWT.MOD1 + 'C'); + ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); + mImage = imageLoader.loadImage("capture-psd.png", Display.getDefault()); //$NON-NLS-1$ + setImageDescriptor(ImageDescriptor.createFromImage(mImage)); + setToolTipText("Capture the window layers as a photoshop document"); + } + + public static CapturePSDAction getAction(Shell shell) { + if (sAction == null) { + sAction = new CapturePSDAction(shell); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().capturePSD(mShell); + } + + @Override + public Image getImage() { + return mImage; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DisplayViewAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DisplayViewAction.java new file mode 100644 index 00000000..00976583 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DisplayViewAction.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.actions; + +import com.android.ddmuilib.ImageLoader; +import com.android.hierarchyviewerlib.HierarchyViewerDirector; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +public class DisplayViewAction extends SelectedNodeEnabledAction implements ImageAction { + + private static DisplayViewAction sAction; + + private Image mImage; + + private Shell mShell; + + private DisplayViewAction(Shell shell) { + super("&Display View"); + this.mShell = shell; + setAccelerator(SWT.MOD1 + 'D'); + ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); + mImage = imageLoader.loadImage("display.png", Display.getDefault()); //$NON-NLS-1$ + setImageDescriptor(ImageDescriptor.createFromImage(mImage)); + setToolTipText("Display the selected view image in a separate window"); + } + + public static DisplayViewAction getAction(Shell shell) { + if (sAction == null) { + sAction = new DisplayViewAction(shell); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().showCapture(mShell); + } + + @Override + public Image getImage() { + return mImage; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DumpDisplayListAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DumpDisplayListAction.java new file mode 100644 index 00000000..2dc42b18 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DumpDisplayListAction.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2011 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.hierarchyviewerlib.actions; + +import com.android.ddmuilib.ImageLoader; +import com.android.hierarchyviewerlib.HierarchyViewerDirector; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; + +public class DumpDisplayListAction extends SelectedNodeEnabledAction implements ImageAction { + + private static DumpDisplayListAction sAction; + + private Image mImage; + + private DumpDisplayListAction() { + super("Dump DisplayList"); + ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); + mImage = imageLoader.loadImage("load-view-hierarchy.png", Display.getDefault()); //$NON-NLS-1$ + setImageDescriptor(ImageDescriptor.createFromImage(mImage)); + setToolTipText("Request the view to output its displaylist to logcat"); + } + + public static DumpDisplayListAction getAction() { + if (sAction == null) { + sAction = new DumpDisplayListAction(); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().dumpDisplayListForCurrentNode(); + } + + @Override + public Image getImage() { + return mImage; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DumpThemeAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DumpThemeAction.java new file mode 100644 index 00000000..9465cf98 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DumpThemeAction.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2014 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.hierarchyviewerlib.actions; + +import com.android.hierarchyviewerlib.HierarchyViewerDirector; + +import org.eclipse.jface.action.Action; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Shell; + +public class DumpThemeAction extends Action implements ImageAction { + + private static DumpThemeAction sAction; + + private Image mImage; + + private Shell mShell; + + private DumpThemeAction(Shell shell) { + super("&Dump Theme"); + mShell = shell; + setAccelerator(SWT.MOD1 + 'D'); + setToolTipText("Dumping the resources in this View's Theme."); + // TODO: Get icon for Button + } + + public static DumpThemeAction getAction(Shell shell) { + if (sAction == null) { + sAction = new DumpThemeAction(shell); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().showDumpTheme(mShell); + } + + @Override + public Image getImage() { + return mImage; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/EvaluateContrastAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/EvaluateContrastAction.java new file mode 100644 index 00000000..1815bb9f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/EvaluateContrastAction.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2014 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.hierarchyviewerlib.actions; + +import com.android.ddmuilib.ImageLoader; +import com.android.hierarchyviewerlib.HierarchyViewerDirector; + +import org.eclipse.jface.action.Action; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +public class EvaluateContrastAction extends Action implements ImageAction { + + private static EvaluateContrastAction sAction; + + private Image mImage; + + private Shell mShell; + + private EvaluateContrastAction(Shell shell) { + super("&Evaluate Contrast"); + mShell = shell; + setAccelerator(SWT.MOD1 + 'D'); + setToolTipText("Evaluate the contrast ratio of this view."); + // TODO: Get icon for Button + } + + public static EvaluateContrastAction getAction(Shell shell) { + if (sAction == null) { + sAction = new EvaluateContrastAction(shell); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().showEvaluateContrast(mShell); + } + + @Override + public Image getImage() { + return mImage; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/ImageAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/ImageAction.java new file mode 100644 index 00000000..6aa2858b --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/ImageAction.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.actions; + +import org.eclipse.swt.graphics.Image; + +public interface ImageAction { + public Image getImage(); + + public String getText(); + + public String getToolTipText(); +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/InspectScreenshotAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/InspectScreenshotAction.java new file mode 100644 index 00000000..4ef1313a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/InspectScreenshotAction.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.actions; + +import com.android.ddmuilib.ImageLoader; +import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import com.android.hierarchyviewerlib.device.IHvDevice; +import com.android.hierarchyviewerlib.models.DeviceSelectionModel; +import com.android.hierarchyviewerlib.models.DeviceSelectionModel.IWindowChangeListener; +import com.android.hierarchyviewerlib.models.Window; + +import org.eclipse.jface.action.Action; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; + +public class InspectScreenshotAction extends Action implements ImageAction, IWindowChangeListener { + + private static InspectScreenshotAction sAction; + + private Image mImage; + + private InspectScreenshotAction() { + super("Inspect &Screenshot"); + setAccelerator(SWT.MOD1 + 'S'); + ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); + mImage = imageLoader.loadImage("inspect-screenshot.png", Display.getDefault()); //$NON-NLS-1$ + setImageDescriptor(ImageDescriptor.createFromImage(mImage)); + setToolTipText("Inspect a screenshot in the pixel perfect view"); + setEnabled( + DeviceSelectionModel.getModel().getSelectedDevice() != null); + DeviceSelectionModel.getModel().addWindowChangeListener(this); + } + + public static InspectScreenshotAction getAction() { + if (sAction == null) { + sAction = new InspectScreenshotAction(); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().inspectScreenshot(); + } + + @Override + public Image getImage() { + return mImage; + } + + @Override + public void deviceChanged(IHvDevice device) { + // pass + } + + @Override + public void deviceConnected(IHvDevice device) { + // pass + } + + @Override + public void deviceDisconnected(IHvDevice device) { + // pass + } + + @Override + public void focusChanged(IHvDevice device) { + // pass + } + + @Override + public void selectionChanged(final IHvDevice device, final Window window) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + InspectScreenshotAction.getAction().setEnabled(device != null); + } + }); + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/InvalidateAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/InvalidateAction.java new file mode 100644 index 00000000..d3de05e9 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/InvalidateAction.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.actions; + +import com.android.ddmuilib.ImageLoader; +import com.android.hierarchyviewerlib.HierarchyViewerDirector; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; + +public class InvalidateAction extends SelectedNodeEnabledAction implements ImageAction { + + private static InvalidateAction sAction; + + private Image mImage; + + private InvalidateAction() { + super("&Invalidate Layout"); + setAccelerator(SWT.MOD1 + 'I'); + ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); + mImage = imageLoader.loadImage("invalidate.png", Display.getDefault()); //$NON-NLS-1$ + setImageDescriptor(ImageDescriptor.createFromImage(mImage)); + setToolTipText("Invalidate the layout for the current window"); + } + + public static InvalidateAction getAction() { + if (sAction == null) { + sAction = new InvalidateAction(); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().invalidateCurrentNode(); + } + + @Override + public Image getImage() { + return mImage; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/LoadOverlayAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/LoadOverlayAction.java new file mode 100644 index 00000000..4d25d657 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/LoadOverlayAction.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.actions; + +import com.android.ddmuilib.ImageLoader; +import com.android.hierarchyviewerlib.HierarchyViewerDirector; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +public class LoadOverlayAction extends PixelPerfectEnabledAction implements ImageAction { + + private static LoadOverlayAction sAction; + + private Image mImage; + + private Shell mShell; + + private LoadOverlayAction(Shell shell) { + super("Load &Overlay"); + this.mShell = shell; + setAccelerator(SWT.MOD1 + 'O'); + ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); + mImage = imageLoader.loadImage("load-overlay.png", Display.getDefault()); //$NON-NLS-1$ + setImageDescriptor(ImageDescriptor.createFromImage(mImage)); + setToolTipText("Load an image to overlay the screenshot"); + } + + public static LoadOverlayAction getAction(Shell shell) { + if (sAction == null) { + sAction = new LoadOverlayAction(shell); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().loadOverlay(mShell); + } + + @Override + public Image getImage() { + return mImage; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/LoadViewHierarchyAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/LoadViewHierarchyAction.java new file mode 100644 index 00000000..81a159c1 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/LoadViewHierarchyAction.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.actions; + +import com.android.ddmuilib.ImageLoader; +import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import com.android.hierarchyviewerlib.device.IHvDevice; +import com.android.hierarchyviewerlib.models.DeviceSelectionModel; +import com.android.hierarchyviewerlib.models.DeviceSelectionModel.IWindowChangeListener; +import com.android.hierarchyviewerlib.models.Window; + +import org.eclipse.jface.action.Action; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; + +public class LoadViewHierarchyAction extends Action implements ImageAction, IWindowChangeListener { + + private static LoadViewHierarchyAction sAction; + + private Image mImage; + + private LoadViewHierarchyAction() { + super("Load View &Hierarchy"); + setAccelerator(SWT.MOD1 + 'H'); + ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); + mImage = imageLoader.loadImage("load-view-hierarchy.png", Display.getDefault()); //$NON-NLS-1$ + setImageDescriptor(ImageDescriptor.createFromImage(mImage)); + setToolTipText("Load the view hierarchy into the tree view"); + setEnabled( + DeviceSelectionModel.getModel().getSelectedWindow() != null); + DeviceSelectionModel.getModel().addWindowChangeListener(this); + } + + public static LoadViewHierarchyAction getAction() { + if (sAction == null) { + sAction = new LoadViewHierarchyAction(); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().loadViewHierarchy(); + } + + @Override + public Image getImage() { + return mImage; + } + + @Override + public void deviceChanged(IHvDevice device) { + // pass + } + + @Override + public void deviceConnected(IHvDevice device) { + // pass + } + + @Override + public void deviceDisconnected(IHvDevice device) { + // pass + } + + @Override + public void focusChanged(IHvDevice device) { + // pass + } + + @Override + public void selectionChanged(final IHvDevice device, final Window window) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + LoadViewHierarchyAction.getAction().setEnabled(window != null); + } + }); + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/PixelPerfectAutoRefreshAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/PixelPerfectAutoRefreshAction.java new file mode 100644 index 00000000..8f337c06 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/PixelPerfectAutoRefreshAction.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.actions; + +import com.android.ddmuilib.ImageLoader; +import com.android.hierarchyviewerlib.HierarchyViewerDirector; + +import org.eclipse.jface.action.Action; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; + +public class PixelPerfectAutoRefreshAction extends PixelPerfectEnabledAction implements ImageAction { + + private static PixelPerfectAutoRefreshAction sAction; + + private Image mImage; + + private PixelPerfectAutoRefreshAction() { + super("Auto &Refresh", Action.AS_CHECK_BOX); + setAccelerator(SWT.MOD1 + 'R'); + ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); + mImage = imageLoader.loadImage("auto-refresh.png", Display.getDefault()); //$NON-NLS-1$ + setImageDescriptor(ImageDescriptor.createFromImage(mImage)); + setToolTipText("Automatically refresh the screenshot"); + } + + public static PixelPerfectAutoRefreshAction getAction() { + if (sAction == null) { + sAction = new PixelPerfectAutoRefreshAction(); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().setPixelPerfectAutoRefresh(sAction.isChecked()); + } + + @Override + public Image getImage() { + return mImage; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/PixelPerfectEnabledAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/PixelPerfectEnabledAction.java new file mode 100644 index 00000000..b5d22345 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/PixelPerfectEnabledAction.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.actions; + +import com.android.hierarchyviewerlib.models.PixelPerfectModel; +import com.android.hierarchyviewerlib.models.PixelPerfectModel.IImageChangeListener; + +import org.eclipse.jface.action.Action; +import org.eclipse.swt.widgets.Display; + +public class PixelPerfectEnabledAction extends Action implements IImageChangeListener { + public PixelPerfectEnabledAction(String name) { + super(name); + setEnabled(PixelPerfectModel.getModel().getImage() != null); + PixelPerfectModel.getModel().addImageChangeListener(this); + } + + public PixelPerfectEnabledAction(String name, int type) { + super(name, type); + setEnabled(PixelPerfectModel.getModel().getImage() != null); + PixelPerfectModel.getModel().addImageChangeListener(this); + } + + @Override + public void crosshairMoved() { + // pass + } + + @Override + public void imageChanged() { + // + } + + @Override + public void imageLoaded() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + setEnabled(PixelPerfectModel.getModel().getImage() != null); + } + }); + } + + @Override + public void overlayChanged() { + // pass + } + + @Override + public void overlayTransparencyChanged() { + // pass + } + + @Override + public void selectionChanged() { + // pass + } + + @Override + public void treeChanged() { + // pass + } + + @Override + public void zoomChanged() { + // pass + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/ProfileNodesAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/ProfileNodesAction.java new file mode 100644 index 00000000..79b08e2a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/ProfileNodesAction.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2013 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.hierarchyviewerlib.actions; + +import com.android.ddmuilib.ImageLoader; +import com.android.hierarchyviewerlib.HierarchyViewerDirector; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; + +public class ProfileNodesAction extends SelectedNodeEnabledAction implements ImageAction { + private static ProfileNodesAction sAction; + + private Image mImage; + + public ProfileNodesAction() { + super("Profile Node"); + ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); + mImage = imageLoader.loadImage("profile.png", Display.getDefault()); //$NON-NLS-1$ + setImageDescriptor(ImageDescriptor.createFromImage(mImage)); + setToolTipText("Obtain layout times for tree rooted at selected node"); + } + + public static ProfileNodesAction getAction() { + if (sAction == null) { + sAction = new ProfileNodesAction(); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().profileCurrentNode(); + } + + @Override + public Image getImage() { + return mImage; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshPixelPerfectAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshPixelPerfectAction.java new file mode 100644 index 00000000..0c022857 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshPixelPerfectAction.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.actions; + +import com.android.ddmuilib.ImageLoader; +import com.android.hierarchyviewerlib.HierarchyViewerDirector; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; + +public class RefreshPixelPerfectAction extends PixelPerfectEnabledAction implements ImageAction { + + private static RefreshPixelPerfectAction sAction; + + private Image mImage; + + private RefreshPixelPerfectAction() { + super("&Refresh Screenshot"); + setAccelerator(SWT.F5); + ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); + mImage = imageLoader.loadImage("refresh-windows.png", Display.getDefault()); //$NON-NLS-1$ + setImageDescriptor(ImageDescriptor.createFromImage(mImage)); + setToolTipText("Refresh the screenshot"); + } + + public static RefreshPixelPerfectAction getAction() { + if (sAction == null) { + sAction = new RefreshPixelPerfectAction(); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().refreshPixelPerfect(); + } + + @Override + public Image getImage() { + return mImage; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshPixelPerfectTreeAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshPixelPerfectTreeAction.java new file mode 100644 index 00000000..1823f480 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshPixelPerfectTreeAction.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.actions; + +import com.android.ddmuilib.ImageLoader; +import com.android.hierarchyviewerlib.HierarchyViewerDirector; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; + +public class RefreshPixelPerfectTreeAction extends PixelPerfectEnabledAction implements ImageAction { + + private static RefreshPixelPerfectTreeAction sAction; + + private Image mImage; + + private RefreshPixelPerfectTreeAction() { + super("Refresh &Tree"); + setAccelerator(SWT.MOD1 + 'T'); + ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); + mImage = imageLoader.loadImage("load-view-hierarchy.png", Display.getDefault()); //$NON-NLS-1$ + setImageDescriptor(ImageDescriptor.createFromImage(mImage)); + setToolTipText("Refresh the tree"); + } + + public static RefreshPixelPerfectTreeAction getAction() { + if (sAction == null) { + sAction = new RefreshPixelPerfectTreeAction(); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().refreshPixelPerfectTree(); + } + + @Override + public Image getImage() { + return mImage; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshViewAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshViewAction.java new file mode 100644 index 00000000..e9b35e5a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshViewAction.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.actions; + +import com.android.ddmuilib.ImageLoader; +import com.android.hierarchyviewerlib.HierarchyViewerDirector; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; + +public class RefreshViewAction extends TreeViewEnabledAction implements ImageAction { + + private static RefreshViewAction sAction; + + private Image mImage; + + private RefreshViewAction() { + super("Load View &Hierarchy"); + setAccelerator(SWT.MOD1 + 'H'); + ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); + mImage = imageLoader.loadImage("load-view-hierarchy.png", Display.getDefault()); //$NON-NLS-1$ + setImageDescriptor(ImageDescriptor.createFromImage(mImage)); + setToolTipText("Reload the view hierarchy"); + } + + public static RefreshViewAction getAction() { + if (sAction == null) { + sAction = new RefreshViewAction(); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().reloadViewHierarchy(); + } + + @Override + public Image getImage() { + return mImage; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshWindowsAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshWindowsAction.java new file mode 100644 index 00000000..ae6754da --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshWindowsAction.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.actions; + +import com.android.ddmuilib.ImageLoader; +import com.android.hierarchyviewerlib.HierarchyViewerDirector; + +import org.eclipse.jface.action.Action; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; + +public class RefreshWindowsAction extends Action implements ImageAction { + + private static RefreshWindowsAction sAction; + + private Image mImage; + + private RefreshWindowsAction() { + super("&Refresh"); + setAccelerator(SWT.F5); + ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); + mImage = imageLoader.loadImage("refresh-windows.png", Display.getDefault()); //$NON-NLS-1$ + setImageDescriptor(ImageDescriptor.createFromImage(mImage)); + setToolTipText("Refresh the list of devices"); + } + + public static RefreshWindowsAction getAction() { + if (sAction == null) { + sAction = new RefreshWindowsAction(); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().refreshWindows(); + } + + @Override + public Image getImage() { + return mImage; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RequestLayoutAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RequestLayoutAction.java new file mode 100644 index 00000000..7621a7c5 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RequestLayoutAction.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.actions; + +import com.android.ddmuilib.ImageLoader; +import com.android.hierarchyviewerlib.HierarchyViewerDirector; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; + +public class RequestLayoutAction extends SelectedNodeEnabledAction implements ImageAction { + + private static RequestLayoutAction sAction; + + private Image mImage; + + private RequestLayoutAction() { + super("Request &Layout"); + setAccelerator(SWT.MOD1 + 'L'); + ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); + mImage = imageLoader.loadImage("request-layout.png", Display.getDefault()); //$NON-NLS-1$ + setImageDescriptor(ImageDescriptor.createFromImage(mImage)); + setToolTipText("Request the view to lay out"); + } + + public static RequestLayoutAction getAction() { + if (sAction == null) { + sAction = new RequestLayoutAction(); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().relayoutCurrentNode(); + } + + @Override + public Image getImage() { + return mImage; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SavePixelPerfectAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SavePixelPerfectAction.java new file mode 100644 index 00000000..555ac5b4 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SavePixelPerfectAction.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.actions; + +import com.android.ddmuilib.ImageLoader; +import com.android.hierarchyviewerlib.HierarchyViewerDirector; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +public class SavePixelPerfectAction extends PixelPerfectEnabledAction implements ImageAction { + + private static SavePixelPerfectAction sAction; + + private Image mImage; + + private Shell mShell; + + private SavePixelPerfectAction(Shell shell) { + super("&Save as PNG"); + this.mShell = shell; + setAccelerator(SWT.MOD1 + 'S'); + ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); + mImage = imageLoader.loadImage("save.png", Display.getDefault()); //$NON-NLS-1$ + setImageDescriptor(ImageDescriptor.createFromImage(mImage)); + setToolTipText("Save the screenshot as a PNG image"); + } + + public static SavePixelPerfectAction getAction(Shell shell) { + if (sAction == null) { + sAction = new SavePixelPerfectAction(shell); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().savePixelPerfect(mShell); + } + + @Override + public Image getImage() { + return mImage; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SaveTreeViewAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SaveTreeViewAction.java new file mode 100644 index 00000000..daefd309 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SaveTreeViewAction.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.actions; + +import com.android.ddmuilib.ImageLoader; +import com.android.hierarchyviewerlib.HierarchyViewerDirector; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +public class SaveTreeViewAction extends TreeViewEnabledAction implements ImageAction { + + private static SaveTreeViewAction sAction; + + private Image mImage; + + private Shell mShell; + + private SaveTreeViewAction(Shell shell) { + super("&Save as PNG"); + this.mShell = shell; + setAccelerator(SWT.MOD1 + 'S'); + ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); + mImage = imageLoader.loadImage("save.png", Display.getDefault()); //$NON-NLS-1$ + setImageDescriptor(ImageDescriptor.createFromImage(mImage)); + setToolTipText("Save the tree view as a PNG image"); + } + + public static SaveTreeViewAction getAction(Shell shell) { + if (sAction == null) { + sAction = new SaveTreeViewAction(shell); + } + return sAction; + } + + @Override + public void run() { + HierarchyViewerDirector.getDirector().saveTreeView(mShell); + } + + @Override + public Image getImage() { + return mImage; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SelectedNodeEnabledAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SelectedNodeEnabledAction.java new file mode 100644 index 00000000..80471d27 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SelectedNodeEnabledAction.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.actions; + +import com.android.hierarchyviewerlib.models.TreeViewModel; +import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener; + +import org.eclipse.jface.action.Action; +import org.eclipse.swt.widgets.Display; + +public class SelectedNodeEnabledAction extends Action implements ITreeChangeListener { + public SelectedNodeEnabledAction(String name) { + super(name); + setEnabled(TreeViewModel.getModel().getTree() != null + && TreeViewModel.getModel().getSelection() != null); + TreeViewModel.getModel().addTreeChangeListener(this); + } + + @Override + public void selectionChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + setEnabled(TreeViewModel.getModel().getTree() != null + && TreeViewModel.getModel().getSelection() != null); + } + }); + } + + @Override + public void treeChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + setEnabled(TreeViewModel.getModel().getTree() != null + && TreeViewModel.getModel().getSelection() != null); + } + }); + } + + @Override + public void viewportChanged() { + } + + @Override + public void zoomChanged() { + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/TreeViewEnabledAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/TreeViewEnabledAction.java new file mode 100644 index 00000000..f2cb57b1 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/TreeViewEnabledAction.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.actions; + +import com.android.hierarchyviewerlib.models.TreeViewModel; +import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener; + +import org.eclipse.jface.action.Action; +import org.eclipse.swt.widgets.Display; + +public class TreeViewEnabledAction extends Action implements ITreeChangeListener { + public TreeViewEnabledAction(String name) { + super(name); + setEnabled(TreeViewModel.getModel().getTree() != null); + TreeViewModel.getModel().addTreeChangeListener(this); + } + + @Override + public void selectionChanged() { + // pass + } + + @Override + public void treeChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + setEnabled(TreeViewModel.getModel().getTree() != null); + } + }); + } + + @Override + public void viewportChanged() { + } + + @Override + public void zoomChanged() { + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/AbstractHvDevice.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/AbstractHvDevice.java new file mode 100644 index 00000000..495b3d37 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/AbstractHvDevice.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2013 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.hierarchyviewerlib.device; + +import com.android.ddmlib.AdbCommandRejectedException; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.Log; +import com.android.ddmlib.RawImage; +import com.android.ddmlib.TimeoutException; + +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.PaletteData; +import org.eclipse.swt.widgets.Display; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicReference; + +public abstract class AbstractHvDevice implements IHvDevice { + private static final String TAG = "HierarchyViewer"; + + @Override + public Image getScreenshotImage() { + IDevice device = getDevice(); + final AtomicReference imageRef = new AtomicReference(); + + try { + final RawImage screenshot = device.getScreenshot(); + if (screenshot == null) { + return null; + } + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + ImageData imageData = + new ImageData(screenshot.width, screenshot.height, screenshot.bpp, + new PaletteData(screenshot.getRedMask(), screenshot + .getGreenMask(), screenshot.getBlueMask()), 1, + screenshot.data); + imageRef.set(new Image(Display.getDefault(), imageData)); + } + }); + return imageRef.get(); + } catch (IOException e) { + Log.e(TAG, "Unable to load screenshot from device " + device.getName()); + } catch (TimeoutException e) { + Log.e(TAG, "Timeout loading screenshot from device " + device.getName()); + } catch (AdbCommandRejectedException e) { + Log.e(TAG, "Adb rejected command to load screenshot from device " + device.getName()); + } + return null; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/DdmViewDebugDevice.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/DdmViewDebugDevice.java new file mode 100644 index 00000000..01e09739 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/DdmViewDebugDevice.java @@ -0,0 +1,444 @@ +/* + * Copyright (C) 2013 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.hierarchyviewerlib.device; + +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; +import com.android.ddmlib.Client; +import com.android.ddmlib.ClientData; +import com.android.ddmlib.HandleViewDebug; +import com.android.ddmlib.HandleViewDebug.ViewDumpHandler; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.Log; +import com.android.hierarchyviewerlib.device.WindowUpdater.IWindowChangeListener; +import com.android.hierarchyviewerlib.models.ThemeModel; +import com.android.hierarchyviewerlib.models.ViewNode; +import com.android.hierarchyviewerlib.models.Window; +import com.android.hierarchyviewerlib.ui.util.PsdFile; + +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.StringReader; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +public class DdmViewDebugDevice extends AbstractHvDevice implements IDeviceChangeListener { + private static final String TAG = "DdmViewDebugDevice"; + + private final IDevice mDevice; + private Map> mViewRootsPerClient = new HashMap>(40); + + public DdmViewDebugDevice(IDevice device) { + mDevice = device; + } + + @Override + public boolean initializeViewDebug() { + AndroidDebugBridge.addDeviceChangeListener(this); + return reloadWindows(); + } + + private static class ListViewRootsHandler extends ViewDumpHandler { + private List mViewRoots = Collections.synchronizedList(new ArrayList(10)); + + public ListViewRootsHandler() { + super(HandleViewDebug.CHUNK_VULW); + } + + @Override + protected void handleViewDebugResult(ByteBuffer data) { + int nWindows = data.getInt(); + + for (int i = 0; i < nWindows; i++) { + int len = data.getInt(); + mViewRoots.add(getString(data, len)); + } + } + + public List getViewRoots(long timeout, TimeUnit unit) { + waitForResult(timeout, unit); + return mViewRoots; + } + } + + private static class CaptureByteArrayHandler extends ViewDumpHandler { + public CaptureByteArrayHandler(int type) { + super(type); + } + + private AtomicReference mData = new AtomicReference(); + + @Override + protected void handleViewDebugResult(ByteBuffer data) { + byte[] b = new byte[data.remaining()]; + data.get(b); + mData.set(b); + + } + + public byte[] getData(long timeout, TimeUnit unit) { + waitForResult(timeout, unit); + return mData.get(); + } + } + + private static class CaptureLayersHandler extends ViewDumpHandler { + private AtomicReference mPsd = new AtomicReference(); + + public CaptureLayersHandler() { + super(HandleViewDebug.CHUNK_VURT); + } + + @Override + protected void handleViewDebugResult(ByteBuffer data) { + byte[] b = new byte[data.remaining()]; + data.get(b); + DataInputStream dis = new DataInputStream(new ByteArrayInputStream(b)); + try { + mPsd.set(DeviceBridge.parsePsd(dis)); + } catch (IOException e) { + Log.e(TAG, e); + } + } + + public PsdFile getPsdFile(long timeout, TimeUnit unit) { + waitForResult(timeout, unit); + return mPsd.get(); + } + } + + @Override + public boolean reloadWindows() { + mViewRootsPerClient = new HashMap>(40); + + for (Client c : mDevice.getClients()) { + ClientData cd = c.getClientData(); + if (cd != null && cd.hasFeature(ClientData.FEATURE_VIEW_HIERARCHY)) { + ListViewRootsHandler handler = new ListViewRootsHandler(); + + try { + HandleViewDebug.listViewRoots(c, handler); + } catch (IOException e) { + Log.i(TAG, "No connection to client: " + cd.getClientDescription()); + continue; + } + + List viewRoots = new ArrayList( + handler.getViewRoots(200, TimeUnit.MILLISECONDS)); + mViewRootsPerClient.put(c, viewRoots); + } + } + + return true; + } + + @Override + public void terminateViewDebug() { + // nothing to terminate + } + + @Override + public boolean isViewDebugEnabled() { + return true; + } + + @Override + public boolean supportsDisplayListDump() { + return true; + } + + @Override + public Window[] getWindows() { + List windows = new ArrayList(10); + + for (Client c: mViewRootsPerClient.keySet()) { + for (String viewRoot: mViewRootsPerClient.get(c)) { + windows.add(new Window(this, viewRoot, c)); + } + } + + return windows.toArray(new Window[windows.size()]); + } + + @Override + public int getFocusedWindow() { + // TODO: add support for identifying view in focus + return -1; + } + + @Override + public IDevice getDevice() { + return mDevice; + } + + @Override + public ViewNode loadWindowData(Window window) { + Client c = window.getClient(); + if (c == null) { + return null; + } + + String viewRoot = window.getTitle(); + CaptureByteArrayHandler handler = new CaptureByteArrayHandler(HandleViewDebug.CHUNK_VURT); + try { + HandleViewDebug.dumpViewHierarchy(c, viewRoot, + false /* skipChildren */, + true /* includeProperties */, + handler); + } catch (IOException e) { + Log.e(TAG, e); + return null; + } + + byte[] data = handler.getData(20, TimeUnit.SECONDS); + if (data == null) { + return null; + } + + String viewHierarchy = new String(data, Charset.forName("UTF-8")); + return DeviceBridge.parseViewHierarchy(new BufferedReader(new StringReader(viewHierarchy)), + window); + } + + @Override + public void loadProfileData(Window window, ViewNode viewNode) { + Client c = window.getClient(); + if (c == null) { + return; + } + + String viewRoot = window.getTitle(); + CaptureByteArrayHandler handler = new CaptureByteArrayHandler(HandleViewDebug.CHUNK_VUOP); + try { + HandleViewDebug.profileView(c, viewRoot, viewNode.toString(), handler); + } catch (IOException e) { + Log.e(TAG, e); + return; + } + + byte[] data = handler.getData(30, TimeUnit.SECONDS); + if (data == null) { + Log.e(TAG, "Timed out waiting for profile data"); + return; + } + + try { + boolean success = DeviceBridge.loadProfileDataRecursive(viewNode, + new BufferedReader(new StringReader(new String(data)))); + if (success) { + viewNode.setProfileRatings(); + } + } catch (IOException e) { + Log.e(TAG, e); + return; + } + } + + @Override + public Image loadCapture(Window window, ViewNode viewNode) { + Client c = window.getClient(); + if (c == null) { + return null; + } + + String viewRoot = window.getTitle(); + CaptureByteArrayHandler handler = new CaptureByteArrayHandler(HandleViewDebug.CHUNK_VUOP); + + try { + HandleViewDebug.captureView(c, viewRoot, viewNode.toString(), handler); + } catch (IOException e) { + Log.e(TAG, e); + return null; + } + + byte[] data = handler.getData(10, TimeUnit.SECONDS); + return (data == null) ? null : + new Image(Display.getDefault(), new ByteArrayInputStream(data)); + } + + @Override + public PsdFile captureLayers(Window window) { + Client c = window.getClient(); + if (c == null) { + return null; + } + + String viewRoot = window.getTitle(); + CaptureLayersHandler handler = new CaptureLayersHandler(); + try { + HandleViewDebug.captureLayers(c, viewRoot, handler); + } catch (IOException e) { + Log.e(TAG, e); + return null; + } + + return handler.getPsdFile(20, TimeUnit.SECONDS); + } + + @Override + public void invalidateView(ViewNode viewNode) { + Window window = viewNode.window; + Client c = window.getClient(); + if (c == null) { + return; + } + + String viewRoot = window.getTitle(); + try { + HandleViewDebug.invalidateView(c, viewRoot, viewNode.toString()); + } catch (IOException e) { + Log.e(TAG, e); + } + } + + @Override + public void requestLayout(ViewNode viewNode) { + Window window = viewNode.window; + Client c = window.getClient(); + if (c == null) { + return; + } + + String viewRoot = window.getTitle(); + try { + HandleViewDebug.requestLayout(c, viewRoot, viewNode.toString()); + } catch (IOException e) { + Log.e(TAG, e); + } + } + + @Override + public void outputDisplayList(ViewNode viewNode) { + Window window = viewNode.window; + Client c = window.getClient(); + if (c == null) { + return; + } + + String viewRoot = window.getTitle(); + try { + HandleViewDebug.dumpDisplayList(c, viewRoot, viewNode.toString()); + } catch (IOException e) { + Log.e(TAG, e); + } + } + + @Override + public ThemeModel dumpTheme(ViewNode viewNode) { + Window window = viewNode.window; + Client c = window.getClient(); + if (c == null) { + return null; + } + + String viewRoot = window.getTitle(); + CaptureByteArrayHandler handler = new CaptureByteArrayHandler(HandleViewDebug.CHUNK_VURT); + try { + HandleViewDebug.dumpTheme(c, viewRoot, handler); + } catch (IOException e) { + Log.e(TAG, e); + return null; + } + + byte[] data = handler.getData(20, TimeUnit.SECONDS); + if (data == null) { + return null; + } + + String themeDump = new String(data, Charset.forName("UTF-8")); + return DeviceBridge.parseThemeDump(new BufferedReader(new StringReader(themeDump))); + } + + @Override + public void addWindowChangeListener(IWindowChangeListener l) { + // TODO: add support for listening to view root changes + } + + @Override + public void removeWindowChangeListener(IWindowChangeListener l) { + // TODO: add support for listening to view root changes + } + + @Override + public void deviceConnected(IDevice device) { + // pass + } + + @Override + public void deviceDisconnected(IDevice device) { + // pass + } + + @Override + public void deviceChanged(IDevice device, int changeMask) { + if ((changeMask & IDevice.CHANGE_CLIENT_LIST) != 0) { + reloadWindows(); + } + } + + @Override + public boolean isViewUpdateEnabled() { + return true; + } + + @Override + public void invokeViewMethod(Window window, ViewNode viewNode, String method, + List args) { + Client c = window.getClient(); + if (c == null) { + return; + } + + String viewRoot = window.getTitle(); + try { + HandleViewDebug.invokeMethod(c, viewRoot, viewNode.toString(), method, args.toArray()); + } catch (IOException e) { + Log.e(TAG, e); + } + } + + @Override + public boolean setLayoutParameter(Window window, ViewNode viewNode, String property, + int value) { + Client c = window.getClient(); + if (c == null) { + return false; + } + + String viewRoot = window.getTitle(); + try { + HandleViewDebug.setLayoutParameter(c, viewRoot, viewNode.toString(), property, value); + } catch (IOException e) { + Log.e(TAG, e); + return false; + } + + return true; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/DeviceBridge.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/DeviceBridge.java new file mode 100644 index 00000000..0deed842 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/DeviceBridge.java @@ -0,0 +1,744 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.device; + +import com.android.ddmlib.AdbCommandRejectedException; +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.Log; +import com.android.ddmlib.MultiLineReceiver; +import com.android.ddmlib.ShellCommandUnresponsiveException; +import com.android.ddmlib.TimeoutException; +import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import com.android.hierarchyviewerlib.models.ThemeModel; +import com.android.hierarchyviewerlib.models.ViewNode; +import com.android.hierarchyviewerlib.models.Window; +import com.android.hierarchyviewerlib.ui.util.PsdFile; + +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; + +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.image.BufferedImage; +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.imageio.ImageIO; + +/** + * A bridge to the device. + */ +public class DeviceBridge { + + public static final String TAG = "hierarchyviewer"; + + private static final int DEFAULT_SERVER_PORT = 4939; + + // These codes must match the auto-generated codes in IWindowManager.java + // See IWindowManager.aidl as well + private static final int SERVICE_CODE_START_SERVER = 1; + + private static final int SERVICE_CODE_STOP_SERVER = 2; + + private static final int SERVICE_CODE_IS_SERVER_RUNNING = 3; + + private static AndroidDebugBridge sBridge; + + private static final HashMap sDevicePortMap = new HashMap(); + + private static final HashMap sViewServerInfo = + new HashMap(); + + private static int sNextLocalPort = DEFAULT_SERVER_PORT; + + public static class ViewServerInfo { + public final int protocolVersion; + + public final int serverVersion; + + ViewServerInfo(int serverVersion, int protocolVersion) { + this.protocolVersion = protocolVersion; + this.serverVersion = serverVersion; + } + } + + /** + * Init the DeviceBridge with an existing {@link AndroidDebugBridge}. + * @param bridge the bridge object to use + */ + public static void acquireBridge(AndroidDebugBridge bridge) { + sBridge = bridge; + } + + /** + * Creates an {@link AndroidDebugBridge} connected to adb at the given location. + * + * If a bridge is already running, this disconnects it and creates a new one. + * + * @param adbLocation the location to adb. + */ + public static void initDebugBridge(String adbLocation) { + if (sBridge == null) { + /* debugger support required only if hv is using ddm protocol */ + AndroidDebugBridge.init(HierarchyViewerDirector.isUsingDdmProtocol()); + } + if (sBridge == null || !sBridge.isConnected()) { + sBridge = AndroidDebugBridge.createBridge(adbLocation, true); + } + } + + /** Disconnects the current {@link AndroidDebugBridge}. */ + public static void terminate() { + AndroidDebugBridge.terminate(); + } + + public static IDevice[] getDevices() { + if (sBridge == null) { + return new IDevice[0]; + } + return sBridge.getDevices(); + } + + /* + * This adds a listener to the debug bridge. The listener is notified of + * connecting/disconnecting devices, devices coming online, etc. + */ + public static void startListenForDevices(AndroidDebugBridge.IDeviceChangeListener listener) { + AndroidDebugBridge.addDeviceChangeListener(listener); + } + + public static void stopListenForDevices(AndroidDebugBridge.IDeviceChangeListener listener) { + AndroidDebugBridge.removeDeviceChangeListener(listener); + } + + /** + * Sets up a just-connected device to work with the view server. + *

+ * This starts a port forwarding between a local port and a port on the + * device. + * + * @param device + */ + public static void setupDeviceForward(IDevice device) { + synchronized (sDevicePortMap) { + if (device.getState() == IDevice.DeviceState.ONLINE) { + int localPort = sNextLocalPort++; + try { + device.createForward(localPort, DEFAULT_SERVER_PORT); + sDevicePortMap.put(device, localPort); + } catch (TimeoutException e) { + Log.e(TAG, "Timeout setting up port forwarding for " + device); + } catch (AdbCommandRejectedException e) { + Log.e(TAG, String.format("Adb rejected forward command for device %1$s: %2$s", + device, e.getMessage())); + } catch (IOException e) { + Log.e(TAG, String.format("Failed to create forward for device %1$s: %2$s", + device, e.getMessage())); + } + } + } + } + + public static void removeDeviceForward(IDevice device) { + synchronized (sDevicePortMap) { + final Integer localPort = sDevicePortMap.get(device); + if (localPort != null) { + try { + device.removeForward(localPort, DEFAULT_SERVER_PORT); + sDevicePortMap.remove(device); + } catch (TimeoutException e) { + Log.e(TAG, "Timeout removing port forwarding for " + device); + } catch (AdbCommandRejectedException e) { + // In this case, we want to fail silently. + } catch (IOException e) { + Log.e(TAG, String.format("Failed to remove forward for device %1$s: %2$s", + device, e.getMessage())); + } + } + } + } + + public static int getDeviceLocalPort(IDevice device) { + synchronized (sDevicePortMap) { + Integer port = sDevicePortMap.get(device); + if (port != null) { + return port; + } + + Log.e(TAG, "Missing forwarded port for " + device.getSerialNumber()); + return -1; + } + + } + + public static boolean isViewServerRunning(IDevice device) { + final boolean[] result = new boolean[1]; + try { + if (device.isOnline()) { + device.executeShellCommand(buildIsServerRunningShellCommand(), + new BooleanResultReader(result)); + if (!result[0]) { + ViewServerInfo serverInfo = loadViewServerInfo(device); + if (serverInfo != null && serverInfo.protocolVersion > 2) { + result[0] = true; + } + } + } + } catch (TimeoutException e) { + Log.e(TAG, "Timeout checking status of view server on device " + device); + } catch (IOException e) { + Log.e(TAG, "Unable to check status of view server on device " + device); + } catch (AdbCommandRejectedException e) { + Log.e(TAG, "Adb rejected command to check status of view server on device " + device); + } catch (ShellCommandUnresponsiveException e) { + Log.e(TAG, "Unable to execute command to check status of view server on device " + + device); + } + return result[0]; + } + + public static boolean startViewServer(IDevice device) { + return startViewServer(device, DEFAULT_SERVER_PORT); + } + + public static boolean startViewServer(IDevice device, int port) { + final boolean[] result = new boolean[1]; + try { + if (device.isOnline()) { + device.executeShellCommand(buildStartServerShellCommand(port), + new BooleanResultReader(result)); + } + } catch (TimeoutException e) { + Log.e(TAG, "Timeout starting view server on device " + device); + } catch (IOException e) { + Log.e(TAG, "Unable to start view server on device " + device); + } catch (AdbCommandRejectedException e) { + Log.e(TAG, "Adb rejected command to start view server on device " + device); + } catch (ShellCommandUnresponsiveException e) { + Log.e(TAG, "Unable to execute command to start view server on device " + device); + } + return result[0]; + } + + public static boolean stopViewServer(IDevice device) { + final boolean[] result = new boolean[1]; + try { + if (device.isOnline()) { + device.executeShellCommand(buildStopServerShellCommand(), new BooleanResultReader( + result)); + } + } catch (TimeoutException e) { + Log.e(TAG, "Timeout stopping view server on device " + device); + } catch (IOException e) { + Log.e(TAG, "Unable to stop view server on device " + device); + } catch (AdbCommandRejectedException e) { + Log.e(TAG, "Adb rejected command to stop view server on device " + device); + } catch (ShellCommandUnresponsiveException e) { + Log.e(TAG, "Unable to execute command to stop view server on device " + device); + } + return result[0]; + } + + private static String buildStartServerShellCommand(int port) { + return String.format("service call window %d i32 %d", SERVICE_CODE_START_SERVER, port); //$NON-NLS-1$ + } + + private static String buildStopServerShellCommand() { + return String.format("service call window %d", SERVICE_CODE_STOP_SERVER); //$NON-NLS-1$ + } + + private static String buildIsServerRunningShellCommand() { + return String.format("service call window %d", SERVICE_CODE_IS_SERVER_RUNNING); //$NON-NLS-1$ + } + + private static class BooleanResultReader extends MultiLineReceiver { + private final boolean[] mResult; + + public BooleanResultReader(boolean[] result) { + mResult = result; + } + + @Override + public void processNewLines(String[] strings) { + if (strings.length > 0) { + Pattern pattern = Pattern.compile(".*?\\([0-9]{8} ([0-9]{8}).*"); //$NON-NLS-1$ + Matcher matcher = pattern.matcher(strings[0]); + if (matcher.matches()) { + if (Integer.parseInt(matcher.group(1)) == 1) { + mResult[0] = true; + } + } + } + } + + @Override + public boolean isCancelled() { + return false; + } + } + + public static ViewServerInfo loadViewServerInfo(IDevice device) { + int server = -1; + int protocol = -1; + DeviceConnection connection = null; + try { + connection = new DeviceConnection(device); + connection.sendCommand("SERVER"); //$NON-NLS-1$ + String line = connection.getInputStream().readLine(); + if (line != null) { + server = Integer.parseInt(line); + } + } catch (Exception e) { + Log.e(TAG, "Unable to get view server version from device " + device); + } finally { + if (connection != null) { + connection.close(); + } + } + connection = null; + try { + connection = new DeviceConnection(device); + connection.sendCommand("PROTOCOL"); //$NON-NLS-1$ + String line = connection.getInputStream().readLine(); + if (line != null) { + protocol = Integer.parseInt(line); + } + } catch (Exception e) { + Log.e(TAG, "Unable to get view server protocol version from device " + device); + } finally { + if (connection != null) { + connection.close(); + } + } + if (server == -1 || protocol == -1) { + return null; + } + ViewServerInfo returnValue = new ViewServerInfo(server, protocol); + synchronized (sViewServerInfo) { + sViewServerInfo.put(device, returnValue); + } + return returnValue; + } + + public static ViewServerInfo getViewServerInfo(IDevice device) { + synchronized (sViewServerInfo) { + return sViewServerInfo.get(device); + } + } + + public static void removeViewServerInfo(IDevice device) { + synchronized (sViewServerInfo) { + sViewServerInfo.remove(device); + } + } + + /* + * This loads the list of windows from the specified device. The format is: + * hashCode1 title1 hashCode2 title2 ... hashCodeN titleN DONE. + */ + public static Window[] loadWindows(IHvDevice hvDevice, IDevice device) { + ArrayList windows = new ArrayList(); + DeviceConnection connection = null; + ViewServerInfo serverInfo = getViewServerInfo(device); + try { + connection = new DeviceConnection(device); + connection.sendCommand("LIST"); //$NON-NLS-1$ + BufferedReader in = connection.getInputStream(); + String line; + while ((line = in.readLine()) != null) { + if ("DONE.".equalsIgnoreCase(line)) { //$NON-NLS-1$ + break; + } + + int index = line.indexOf(' '); + if (index != -1) { + String windowId = line.substring(0, index); + + int id; + if (serverInfo.serverVersion > 2) { + id = (int) Long.parseLong(windowId, 16); + } else { + id = Integer.parseInt(windowId, 16); + } + + Window w = new Window(hvDevice, line.substring(index + 1), id); + windows.add(w); + } + } + // Automatic refreshing of windows was added in protocol version 3. + // Before, the user needed to specify explicitly that he wants to + // get the focused window, which was done using a special type of + // window with hash code -1. + if (serverInfo.protocolVersion < 3) { + windows.add(Window.getFocusedWindow(hvDevice)); + } + } catch (Exception e) { + Log.e(TAG, "Unable to load the window list from device " + device); + } finally { + if (connection != null) { + connection.close(); + } + } + // The server returns the list of windows from the window at the bottom + // to the top. We want the reverse order to put the top window on top of + // the list. + Window[] returnValue = new Window[windows.size()]; + for (int i = windows.size() - 1; i >= 0; i--) { + returnValue[returnValue.length - i - 1] = windows.get(i); + } + return returnValue; + } + + /* + * This gets the hash code of the window that has focus. Only works with + * protocol version 3 and above. + */ + public static int getFocusedWindow(IDevice device) { + DeviceConnection connection = null; + try { + connection = new DeviceConnection(device); + connection.sendCommand("GET_FOCUS"); //$NON-NLS-1$ + String line = connection.getInputStream().readLine(); + if (line == null || line.length() == 0) { + return -1; + } + return (int) Long.parseLong(line.substring(0, line.indexOf(' ')), 16); + } catch (Exception e) { + Log.e(TAG, "Unable to get the focused window from device " + device); + } finally { + if (connection != null) { + connection.close(); + } + } + return -1; + } + + public static ViewNode loadWindowData(Window window) { + DeviceConnection connection = null; + try { + connection = new DeviceConnection(window.getDevice()); + connection.sendCommand("DUMP " + window.encode()); //$NON-NLS-1$ + BufferedReader in = connection.getInputStream(); + ViewNode currentNode = parseViewHierarchy(in, window); + ViewServerInfo serverInfo = getViewServerInfo(window.getDevice()); + if (serverInfo != null) { + currentNode.protocolVersion = serverInfo.protocolVersion; + } + return currentNode; + } catch (Exception e) { + Log.e(TAG, "Unable to load window data for window " + window.getTitle() + " on device " + + window.getDevice()); + Log.e(TAG, e.getMessage()); + } finally { + if (connection != null) { + connection.close(); + } + } + return null; + } + + public static ViewNode parseViewHierarchy(BufferedReader in, Window window) { + ViewNode currentNode = null; + int currentDepth = -1; + String line; + try { + while ((line = in.readLine()) != null) { + if ("DONE.".equalsIgnoreCase(line)) { + break; + } + int depth = 0; + while (line.charAt(depth) == ' ') { + depth++; + } + while (depth <= currentDepth) { + if (currentNode != null) { + currentNode = currentNode.parent; + } + currentDepth--; + } + currentNode = new ViewNode(window, currentNode, line.substring(depth)); + currentDepth = depth; + } + } catch (IOException e) { + Log.e(TAG, "Error reading view hierarchy stream: " + e.getMessage()); + return null; + } + if (currentNode == null) { + return null; + } + while (currentNode.parent != null) { + currentNode = currentNode.parent; + } + + return currentNode; + } + + public static boolean loadProfileData(Window window, ViewNode viewNode) { + DeviceConnection connection = null; + try { + connection = new DeviceConnection(window.getDevice()); + connection.sendCommand("PROFILE " + window.encode() + " " + viewNode.toString()); //$NON-NLS-1$ + BufferedReader in = connection.getInputStream(); + int protocol; + synchronized (sViewServerInfo) { + protocol = sViewServerInfo.get(window.getDevice()).protocolVersion; + } + if (protocol < 3) { + return loadProfileData(viewNode, in); + } else { + boolean ret = loadProfileDataRecursive(viewNode, in); + if (ret) { + viewNode.setProfileRatings(); + } + return ret; + } + } catch (Exception e) { + Log.e(TAG, "Unable to load profiling data for window " + window.getTitle() + + " on device " + window.getDevice()); + } finally { + if (connection != null) { + connection.close(); + } + } + return false; + } + + private static boolean loadProfileData(ViewNode node, BufferedReader in) throws IOException { + String line; + if ((line = in.readLine()) == null || line.equalsIgnoreCase("-1 -1 -1") //$NON-NLS-1$ + || line.equalsIgnoreCase("DONE.")) { //$NON-NLS-1$ + return false; + } + String[] data = line.split(" "); + node.measureTime = (Long.parseLong(data[0]) / 1000.0) / 1000.0; + node.layoutTime = (Long.parseLong(data[1]) / 1000.0) / 1000.0; + node.drawTime = (Long.parseLong(data[2]) / 1000.0) / 1000.0; + return true; + } + + public static boolean loadProfileDataRecursive(ViewNode node, BufferedReader in) + throws IOException { + if (!loadProfileData(node, in)) { + return false; + } + for (int i = 0; i < node.children.size(); i++) { + if (!loadProfileDataRecursive(node.children.get(i), in)) { + return false; + } + } + return true; + } + + public static Image loadCapture(Window window, ViewNode viewNode) { + DeviceConnection connection = null; + try { + connection = new DeviceConnection(window.getDevice()); + connection.getSocket().setSoTimeout(5000); + connection.sendCommand("CAPTURE " + window.encode() + " " + viewNode.toString()); //$NON-NLS-1$ + return new Image(Display.getDefault(), connection.getSocket().getInputStream()); + } catch (Exception e) { + Log.e(TAG, "Unable to capture data for node " + viewNode + " in window " + + window.getTitle() + " on device " + window.getDevice()); + } finally { + if (connection != null) { + connection.close(); + } + } + return null; + } + + public static PsdFile captureLayers(Window window) { + DeviceConnection connection = null; + DataInputStream in = null; + + try { + connection = new DeviceConnection(window.getDevice()); + connection.sendCommand("CAPTURE_LAYERS " + window.encode()); //$NON-NLS-1$ + + in = + new DataInputStream(new BufferedInputStream(connection.getSocket() + .getInputStream())); + + return parsePsd(in); + } catch (IOException e) { + Log.e(TAG, "Unable to capture layers for window " + window.getTitle() + " on device " + + window.getDevice()); + } finally { + if (in != null) { + try { + in.close(); + } catch (Exception ex) { + } + } + + if (connection != null) { + connection.close(); + } + } + + return null; + } + + public static PsdFile parsePsd(DataInputStream in) throws IOException { + int width = in.readInt(); + int height = in.readInt(); + + PsdFile psd = new PsdFile(width, height); + + while (readLayer(in, psd)) { + } + + return psd; + } + + private static boolean readLayer(DataInputStream in, PsdFile psd) { + try { + if (in.read() == 2) { + return false; + } + String name = in.readUTF(); + boolean visible = in.read() == 1; + int x = in.readInt(); + int y = in.readInt(); + int dataSize = in.readInt(); + + byte[] data = new byte[dataSize]; + int read = 0; + while (read < dataSize) { + read += in.read(data, read, dataSize - read); + } + + ByteArrayInputStream arrayIn = new ByteArrayInputStream(data); + BufferedImage chunk = ImageIO.read(arrayIn); + + // Ensure the image is in the right format + BufferedImage image = + new BufferedImage(chunk.getWidth(), chunk.getHeight(), + BufferedImage.TYPE_INT_ARGB); + Graphics2D g = image.createGraphics(); + g.drawImage(chunk, null, 0, 0); + g.dispose(); + + psd.addLayer(name, image, new Point(x, y), visible); + + return true; + } catch (Exception e) { + return false; + } + } + + public static void invalidateView(ViewNode viewNode) { + DeviceConnection connection = null; + try { + connection = new DeviceConnection(viewNode.window.getDevice()); + connection.sendCommand("INVALIDATE " + viewNode.window.encode() + " " + viewNode); //$NON-NLS-1$ + } catch (Exception e) { + Log.e(TAG, "Unable to invalidate view " + viewNode + " in window " + viewNode.window + + " on device " + viewNode.window.getDevice()); + } finally { + if (connection != null) { + connection.close(); + } + } + } + + public static void requestLayout(ViewNode viewNode) { + DeviceConnection connection = null; + try { + connection = new DeviceConnection(viewNode.window.getDevice()); + connection.sendCommand("REQUEST_LAYOUT " + viewNode.window.encode() + " " + viewNode); //$NON-NLS-1$ + } catch (Exception e) { + Log.e(TAG, "Unable to request layout for node " + viewNode + " in window " + + viewNode.window + " on device " + viewNode.window.getDevice()); + } finally { + if (connection != null) { + connection.close(); + } + } + } + + public static void outputDisplayList(ViewNode viewNode) { + DeviceConnection connection = null; + try { + connection = new DeviceConnection(viewNode.window.getDevice()); + connection.sendCommand("OUTPUT_DISPLAYLIST " + + viewNode.window.encode() + " " + viewNode); //$NON-NLS-1$ + } catch (Exception e) { + Log.e(TAG, "Unable to dump displaylist for node " + viewNode + " in window " + + viewNode.window + " on device " + viewNode.window.getDevice()); + } finally { + if (connection != null) { + connection.close(); + } + } + } + + public static ThemeModel dumpTheme(ViewNode viewNode) { + DeviceConnection connection = null; + ThemeModel model = null; + + try { + connection = new DeviceConnection(viewNode.window.getDevice()); + connection.sendCommand("DUMP_THEME " + viewNode.window.encode() + + " " + viewNode); //$NON-NLS-1$ + + BufferedReader in = connection.getInputStream(); + model = parseThemeDump(in); + } catch (Exception e) { + Log.e(TAG, "Unable to dump theme for node " + viewNode + " in window " + + viewNode.window + " on device " + viewNode.window.getDevice()); + return null; + } finally { + if (connection != null) { + connection.close(); + } + } + return model; + } + + public static ThemeModel parseThemeDump(BufferedReader in) { + ThemeModel model = new ThemeModel(); + String resourceName; + String resourceValue; + + try { + while ((resourceName = in.readLine()) != null) { + if ("DONE.".equalsIgnoreCase(resourceName)) { + break; + } + if ((resourceValue = in.readLine()) == null) { + return null; + } + model.add(resourceName, resourceValue); + } + } catch (IOException e) { + Log.e(TAG, "Error reading theme dump: " + e.getMessage()); + return null; + } + + return model; + } + +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/DeviceConnection.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/DeviceConnection.java new file mode 100644 index 00000000..c8abc673 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/DeviceConnection.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.device; + +import com.android.ddmlib.IDevice; +import com.google.common.base.Charsets; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.channels.SocketChannel; + +/** + * This class is used for connecting to a device in debug mode running the view + * server. + */ +public class DeviceConnection { + + // Now a socket channel, since socket channels are friendly with interrupts. + private SocketChannel mSocketChannel; + + private BufferedReader mIn; + + private BufferedWriter mOut; + + public DeviceConnection(IDevice device) throws IOException { + mSocketChannel = SocketChannel.open(); + int port = DeviceBridge.getDeviceLocalPort(device); + + if (port == -1) { + throw new IOException(); + } + + mSocketChannel.connect(new InetSocketAddress("127.0.0.1", port)); //$NON-NLS-1$ + mSocketChannel.socket().setSoTimeout(40000); + } + + public BufferedReader getInputStream() throws IOException { + if (mIn == null) { + mIn = new BufferedReader(new InputStreamReader( + mSocketChannel.socket().getInputStream(), Charsets.UTF_8)); + } + return mIn; + } + + public BufferedWriter getOutputStream() throws IOException { + if (mOut == null) { + mOut = new BufferedWriter(new OutputStreamWriter( + mSocketChannel.socket().getOutputStream(), Charsets.UTF_8)); + } + return mOut; + } + + public Socket getSocket() { + return mSocketChannel.socket(); + } + + public void sendCommand(String command) throws IOException { + BufferedWriter out = getOutputStream(); + out.write(command); + out.newLine(); + out.flush(); + } + + public void close() { + try { + if (mIn != null) { + mIn.close(); + } + } catch (IOException e) { + } + try { + if (mOut != null) { + mOut.close(); + } + } catch (IOException e) { + } + try { + mSocketChannel.close(); + } catch (IOException e) { + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/HvDeviceFactory.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/HvDeviceFactory.java new file mode 100644 index 00000000..4b0e638b --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/HvDeviceFactory.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2013 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.hierarchyviewerlib.device; + +import com.android.ddmlib.Client; +import com.android.ddmlib.ClientData; +import com.android.ddmlib.IDevice; +import com.android.hierarchyviewerlib.HierarchyViewerDirector; + +public class HvDeviceFactory { + public static IHvDevice create(IDevice device) { + // default to old mechanism until the new one is fully tested + if (!HierarchyViewerDirector.isUsingDdmProtocol()) { + return new ViewServerDevice(device); + } + + // Wait for a few seconds after the device has been connected to + // allow all the clients to be initialized. Specifically, we need to wait + // until the client data is filled with the list of features supported + // by the client. + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + // ignore + } + + boolean ddmViewHierarchy = false; + + // see if any of the clients on the device support view hierarchy via DDMS + for (Client c : device.getClients()) { + ClientData cd = c.getClientData(); + if (cd != null && cd.hasFeature(ClientData.FEATURE_VIEW_HIERARCHY)) { + ddmViewHierarchy = true; + break; + } + } + + return ddmViewHierarchy ? new DdmViewDebugDevice(device) : new ViewServerDevice(device); + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/IHvDevice.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/IHvDevice.java new file mode 100644 index 00000000..7b1c1bd0 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/IHvDevice.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2013 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.hierarchyviewerlib.device; + +import com.android.ddmlib.IDevice; +import com.android.hierarchyviewerlib.device.WindowUpdater.IWindowChangeListener; +import com.android.hierarchyviewerlib.models.ThemeModel; +import com.android.hierarchyviewerlib.models.ViewNode; +import com.android.hierarchyviewerlib.models.Window; +import com.android.hierarchyviewerlib.ui.util.PsdFile; + +import org.eclipse.swt.graphics.Image; + +import java.util.List; + +/** Represents a device that can perform view debug operations. */ +public interface IHvDevice { + /** + * Initializes view debugging on the device. + * @return true if the on device component was successfully initialized + */ + boolean initializeViewDebug(); + boolean reloadWindows(); + + void terminateViewDebug(); + boolean isViewDebugEnabled(); + boolean supportsDisplayListDump(); + + Window[] getWindows(); + int getFocusedWindow(); + + IDevice getDevice(); + + Image getScreenshotImage(); + ViewNode loadWindowData(Window window); + void loadProfileData(Window window, ViewNode viewNode); + Image loadCapture(Window window, ViewNode viewNode); + PsdFile captureLayers(Window window); + void invalidateView(ViewNode viewNode); + void requestLayout(ViewNode viewNode); + void outputDisplayList(ViewNode viewNode); + ThemeModel dumpTheme(ViewNode viewNode); + + boolean isViewUpdateEnabled(); + void invokeViewMethod(Window window, ViewNode viewNode, String method, List args); + boolean setLayoutParameter(Window window, ViewNode viewNode, String property, int value); + + void addWindowChangeListener(IWindowChangeListener l); + void removeWindowChangeListener(IWindowChangeListener l); +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/ViewServerDevice.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/ViewServerDevice.java new file mode 100644 index 00000000..157734b2 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/ViewServerDevice.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2013 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.hierarchyviewerlib.device; + +import com.android.ddmlib.IDevice; +import com.android.ddmlib.Log; +import com.android.hierarchyviewerlib.device.DeviceBridge.ViewServerInfo; +import com.android.hierarchyviewerlib.device.WindowUpdater.IWindowChangeListener; +import com.android.hierarchyviewerlib.models.ThemeModel; +import com.android.hierarchyviewerlib.models.ViewNode; +import com.android.hierarchyviewerlib.models.Window; +import com.android.hierarchyviewerlib.ui.util.PsdFile; + +import org.eclipse.swt.graphics.Image; + +import java.util.List; + +public class ViewServerDevice extends AbstractHvDevice { + static final String TAG = "ViewServerDevice"; + + final IDevice mDevice; + private ViewServerInfo mViewServerInfo; + private Window[] mWindows; + + public ViewServerDevice(IDevice device) { + mDevice = device; + } + + @Override + public boolean initializeViewDebug() { + if (!mDevice.isOnline()) { + return false; + } + + DeviceBridge.setupDeviceForward(mDevice); + + return reloadWindows(); + } + + @Override + public boolean reloadWindows() { + if (!DeviceBridge.isViewServerRunning(mDevice)) { + if (!DeviceBridge.startViewServer(mDevice)) { + Log.e(TAG, "Unable to debug device: " + mDevice.getName()); + DeviceBridge.removeDeviceForward(mDevice); + return false; + } + } + + mViewServerInfo = DeviceBridge.loadViewServerInfo(mDevice); + if (mViewServerInfo == null) { + return false; + } + + mWindows = DeviceBridge.loadWindows(this, mDevice); + return true; + } + + @Override + public boolean supportsDisplayListDump() { + return mViewServerInfo != null && mViewServerInfo.protocolVersion >= 4; + } + + @Override + public void terminateViewDebug() { + DeviceBridge.removeDeviceForward(mDevice); + DeviceBridge.removeViewServerInfo(mDevice); + } + + @Override + public boolean isViewDebugEnabled() { + return mViewServerInfo != null; + } + + @Override + public Window[] getWindows() { + return mWindows; + } + + @Override + public int getFocusedWindow() { + return DeviceBridge.getFocusedWindow(mDevice); + } + + @Override + public IDevice getDevice() { + return mDevice; + } + + @Override + public ViewNode loadWindowData(Window window) { + return DeviceBridge.loadWindowData(window); + } + + @Override + public void loadProfileData(Window window, ViewNode viewNode) { + DeviceBridge.loadProfileData(window, viewNode); + } + + @Override + public Image loadCapture(Window window, ViewNode viewNode) { + return DeviceBridge.loadCapture(window, viewNode); + } + + @Override + public PsdFile captureLayers(Window window) { + return DeviceBridge.captureLayers(window); + } + + @Override + public void invalidateView(ViewNode viewNode) { + DeviceBridge.invalidateView(viewNode); + } + + @Override + public void requestLayout(ViewNode viewNode) { + DeviceBridge.requestLayout(viewNode); + } + + @Override + public void outputDisplayList(ViewNode viewNode) { + DeviceBridge.outputDisplayList(viewNode); + } + + @Override + public ThemeModel dumpTheme(ViewNode viewNode) { + return DeviceBridge.dumpTheme(viewNode); + } + + @Override + public void addWindowChangeListener(IWindowChangeListener l) { + if (mViewServerInfo != null && mViewServerInfo.protocolVersion >= 3) { + WindowUpdater.startListenForWindowChanges(l, mDevice); + } + } + + @Override + public void removeWindowChangeListener(IWindowChangeListener l) { + if (mViewServerInfo != null && mViewServerInfo.protocolVersion >= 3) { + WindowUpdater.stopListenForWindowChanges(l, mDevice); + } + } + + @Override + public boolean isViewUpdateEnabled() { + return false; + } + + @Override + public void invokeViewMethod(Window window, ViewNode viewNode, String method, + List args) { + // not supported + } + + @Override + public boolean setLayoutParameter(Window window, ViewNode viewNode, String property, + int value) { + // not supported + return false; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/WindowUpdater.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/WindowUpdater.java new file mode 100644 index 00000000..484e5275 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/device/WindowUpdater.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.device; + +import com.android.ddmlib.IDevice; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; + +/** + * This class handles automatic updating of the list of windows in the device + * selector for device with protocol version 3 or above of the view server. It + * connects to the devices, keeps the connection open and listens for messages. + * It notifies all it's listeners of changes. + */ +public class WindowUpdater { + private static HashMap> sWindowChangeListeners = + new HashMap>(); + + private static HashMap sListeningThreads = new HashMap(); + + public static interface IWindowChangeListener { + public void windowsChanged(IDevice device); + + public void focusChanged(IDevice device); + } + + public static void terminate() { + synchronized (sListeningThreads) { + for (IDevice device : sListeningThreads.keySet()) { + sListeningThreads.get(device).interrupt(); + + } + } + } + + public static void startListenForWindowChanges(IWindowChangeListener listener, IDevice device) { + synchronized (sWindowChangeListeners) { + // In this case, a listening thread already exists, so we don't need + // to create another one. + if (sWindowChangeListeners.containsKey(device)) { + sWindowChangeListeners.get(device).add(listener); + return; + } + ArrayList listeners = new ArrayList(); + listeners.add(listener); + sWindowChangeListeners.put(device, listeners); + } + // Start listening + Thread listeningThread = new Thread(new WindowChangeMonitor(device)); + synchronized (sListeningThreads) { + sListeningThreads.put(device, listeningThread); + } + listeningThread.start(); + } + + public static void stopListenForWindowChanges(IWindowChangeListener listener, IDevice device) { + synchronized (sWindowChangeListeners) { + ArrayList listeners = sWindowChangeListeners.get(device); + if (listeners == null) { + return; + } + listeners.remove(listener); + // There are more listeners, so don't stop the listening thread. + if (listeners.size() != 0) { + return; + } + sWindowChangeListeners.remove(device); + } + // Everybody left, so the party's over! + Thread listeningThread; + synchronized (sListeningThreads) { + listeningThread = sListeningThreads.get(device); + sListeningThreads.remove(device); + } + listeningThread.interrupt(); + } + + private static IWindowChangeListener[] getWindowChangeListenersAsArray(IDevice device) { + IWindowChangeListener[] listeners; + synchronized (sWindowChangeListeners) { + ArrayList windowChangeListenerList = + sWindowChangeListeners.get(device); + if (windowChangeListenerList == null) { + return null; + } + listeners = + windowChangeListenerList + .toArray(new IWindowChangeListener[windowChangeListenerList.size()]); + } + return listeners; + } + + public static void notifyWindowsChanged(IDevice device) { + IWindowChangeListener[] listeners = getWindowChangeListenersAsArray(device); + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].windowsChanged(device); + } + } + } + + public static void notifyFocusChanged(IDevice device) { + IWindowChangeListener[] listeners = getWindowChangeListenersAsArray(device); + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].focusChanged(device); + } + } + } + + private static class WindowChangeMonitor implements Runnable { + private IDevice device; + + public WindowChangeMonitor(IDevice device) { + this.device = device; + } + + @Override + public void run() { + while (!Thread.currentThread().isInterrupted()) { + DeviceConnection connection = null; + try { + connection = new DeviceConnection(device); + connection.sendCommand("AUTOLIST"); + String line; + while (!Thread.currentThread().isInterrupted() + && (line = connection.getInputStream().readLine()) != null) { + if (line.equalsIgnoreCase("LIST UPDATE")) { + notifyWindowsChanged(device); + } else if (line.equalsIgnoreCase("FOCUS UPDATE")) { + notifyFocusChanged(device); + } + } + + } catch (IOException e) { + } finally { + if (connection != null) { + connection.close(); + } + } + } + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/DeviceSelectionModel.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/DeviceSelectionModel.java new file mode 100644 index 00000000..f40dc47c --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/DeviceSelectionModel.java @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.models; + +import com.android.hierarchyviewerlib.device.IHvDevice; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * This class stores the list of windows for each connected device. It notifies + * listeners of any changes as well as knows which window is currently selected + * in the device selector. + */ +public class DeviceSelectionModel { + private final Map mDeviceMap = new HashMap(10); + private final Map mFocusedWindowHashes = + new HashMap(20); + + private final ArrayList mWindowChangeListeners = + new ArrayList(); + + private IHvDevice mSelectedDevice; + + private Window mSelectedWindow; + + private static DeviceSelectionModel sModel; + + private static class DeviceInfo { + Window[] windows; + + private DeviceInfo(Window[] windows) { + this.windows = windows; + } + } + public static DeviceSelectionModel getModel() { + if (sModel == null) { + sModel = new DeviceSelectionModel(); + } + return sModel; + } + + public void addDevice(IHvDevice hvDevice) { + synchronized (mDeviceMap) { + DeviceInfo info = new DeviceInfo(hvDevice.getWindows()); + mDeviceMap.put(hvDevice, info); + } + + notifyDeviceConnected(hvDevice); + } + + public void removeDevice(IHvDevice hvDevice) { + boolean selectionChanged = false; + synchronized (mDeviceMap) { + mDeviceMap.remove(hvDevice); + mFocusedWindowHashes.remove(hvDevice); + if (mSelectedDevice == hvDevice) { + mSelectedDevice = null; + mSelectedWindow = null; + selectionChanged = true; + } + } + notifyDeviceDisconnected(hvDevice); + if (selectionChanged) { + notifySelectionChanged(mSelectedDevice, mSelectedWindow); + } + } + + public void updateDevice(IHvDevice hvDevice) { + boolean selectionChanged = false; + synchronized (mDeviceMap) { + Window[] windows = hvDevice.getWindows(); + mDeviceMap.put(hvDevice, new DeviceInfo(windows)); + + // If the selected window no longer exists, we clear the selection. + if (mSelectedDevice == hvDevice && mSelectedWindow != null) { + boolean windowStillExists = false; + for (int i = 0; i < windows.length && !windowStillExists; i++) { + if (windows[i].equals(mSelectedWindow)) { + windowStillExists = true; + } + } + if (!windowStillExists) { + mSelectedDevice = null; + mSelectedWindow = null; + selectionChanged = true; + } + } + } + + notifyDeviceChanged(hvDevice); + if (selectionChanged) { + notifySelectionChanged(mSelectedDevice, mSelectedWindow); + } + } + + /* + * Change which window has focus and notify the listeners. + */ + public void updateFocusedWindow(IHvDevice device, int focusedWindow) { + Integer oldValue = null; + synchronized (mDeviceMap) { + oldValue = mFocusedWindowHashes.put(device, new Integer(focusedWindow)); + } + // Only notify if the values are different. It would be cool if Java + // containers accepted basic types like int. + if (oldValue == null || (oldValue != null && oldValue.intValue() != focusedWindow)) { + notifyFocusChanged(device); + } + } + + public static interface IWindowChangeListener { + public void deviceConnected(IHvDevice device); + + public void deviceChanged(IHvDevice device); + + public void deviceDisconnected(IHvDevice device); + + public void focusChanged(IHvDevice device); + + public void selectionChanged(IHvDevice device, Window window); + } + + private IWindowChangeListener[] getWindowChangeListenerList() { + IWindowChangeListener[] listeners = null; + synchronized (mWindowChangeListeners) { + if (mWindowChangeListeners.size() == 0) { + return null; + } + listeners = + mWindowChangeListeners.toArray(new IWindowChangeListener[mWindowChangeListeners + .size()]); + } + return listeners; + } + + private void notifyDeviceConnected(IHvDevice device) { + IWindowChangeListener[] listeners = getWindowChangeListenerList(); + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].deviceConnected(device); + } + } + } + + private void notifyDeviceChanged(IHvDevice device) { + IWindowChangeListener[] listeners = getWindowChangeListenerList(); + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].deviceChanged(device); + } + } + } + + private void notifyDeviceDisconnected(IHvDevice device) { + IWindowChangeListener[] listeners = getWindowChangeListenerList(); + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].deviceDisconnected(device); + } + } + } + + private void notifyFocusChanged(IHvDevice device) { + IWindowChangeListener[] listeners = getWindowChangeListenerList(); + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].focusChanged(device); + } + } + } + + private void notifySelectionChanged(IHvDevice device, Window window) { + IWindowChangeListener[] listeners = getWindowChangeListenerList(); + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].selectionChanged(device, window); + } + } + } + + public void addWindowChangeListener(IWindowChangeListener listener) { + synchronized (mWindowChangeListeners) { + mWindowChangeListeners.add(listener); + } + } + + public void removeWindowChangeListener(IWindowChangeListener listener) { + synchronized (mWindowChangeListeners) { + mWindowChangeListeners.remove(listener); + } + } + + public IHvDevice[] getDevices() { + synchronized (mDeviceMap) { + Set devices = mDeviceMap.keySet(); + return devices.toArray(new IHvDevice[devices.size()]); + } + } + + public Window[] getWindows(IHvDevice device) { + synchronized (mDeviceMap) { + DeviceInfo info = mDeviceMap.get(device); + if (info != null) { + return info.windows; + } + } + + return null; + } + + // Returns the window that currently has focus or -1. Note that this means + // that a window with hashcode -1 gets highlighted. If you remember, this is + // the infamous + public int getFocusedWindow(IHvDevice device) { + synchronized (mDeviceMap) { + Integer focusedWindow = mFocusedWindowHashes.get(device); + if (focusedWindow == null) { + return -1; + } + return focusedWindow.intValue(); + } + } + + public void setSelection(IHvDevice device, Window window) { + synchronized (mDeviceMap) { + mSelectedDevice = device; + mSelectedWindow = window; + } + notifySelectionChanged(device, window); + } + + public IHvDevice getSelectedDevice() { + synchronized (mDeviceMap) { + return mSelectedDevice; + } + } + + public Window getSelectedWindow() { + synchronized (mDeviceMap) { + return mSelectedWindow; + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/EvaluateContrastModel.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/EvaluateContrastModel.java new file mode 100644 index 00000000..247a5412 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/EvaluateContrastModel.java @@ -0,0 +1,390 @@ +/* + * Copyright (C) 2014 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.hierarchyviewerlib.models; + +import com.android.annotations.Nullable; +import com.google.common.collect.Lists; + +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; + +import java.awt.Color; +import java.awt.Rectangle; +import java.lang.Math; +import java.lang.Integer; +import java.lang.String; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map.Entry; + +/** + *

+ * This class uses the Web Content Accessibility Guidelines (WCAG) 2.0 (http://www.w3.org/TR/WCAG20) + * to evaluate the contrast ratio between the text and background colors of a view. + *

+ *

+ * Given an image of a view and the bounds of where the view is within the image (x, y, width, + * height), this class will extract the luminance values (measure of brightness) of the entire view + * to try and determine the text and background color. If known, the constructor accepts text size + * and text color to provide a more accurate result. + *

+ *

+ * The {@link #calculateLuminance(int)} method calculates the luminance value of an {@code int} + * representation of a {@link Color}. We use two of these values, the text and background + * luminances, to determine the contrast ratio.

+ *

+ *

+ * "Sufficient contrast" is defined as having a contrast ratio of 4.5:1 in general, except for: + *

    + *
  • Large text (>= 18 points, or >= 14 points and bold), + * which can have a contrast ratio of only 3:1.
  • + *
  • Inactive components or pure decorations.
  • + *
  • Text that is part of a logo or brand name.
  • + *
+ */ +public class EvaluateContrastModel { + + public enum ContrastResult { + PASS, + FAIL, + INDETERMINATE + } + + public static final String CONTRAST_RATIO_FORMAT = "%.2f:1"; + public static final double CONTRAST_RATIO_NORMAL_TEXT = 4.5; + public static final double CONTRAST_RATIO_LARGE_TEXT = 3.0; + public static final int NORMAL_TEXT_SZ_PTS = 18; + public static final int NORMAL_TEXT_BOLD_SZ_PTS = 14; + + public static final String NOT_APPLICABLE = "N/A"; + + private static final double MAX_RGB_VALUE = 255.0; + + private ImageData mImageData; + /** The bounds of the view within the image */ + private Rectangle mViewBounds; + + /** Maps an int representation of a {@link Color} to its luminance value. */ + private HashMap mLuminanceMap; + /** Keeps track of how many times a luminance value occurs in this view. */ + private HashMap mLuminanceHistogram; + private final List mBackgroundColors; + private final List mForegroundColors; + private double mBackgroundLuminance; + private double mForegroundLuminance; + + private double mContrastRatio; + private Integer mTextColor; + private Double mTextSize; + private boolean mIsBold; + + /** + *

+ * Constructs an EvaluateContrastModel to extract and process properties from the image related + * to contrast and luminance. + *

+ *

+ * NOTE: Invoking this constructor performs image processing tasks, which are relatively + * heavywheight. + *

+ * + * @param image Screenshot of the view. + * @param textColor Color of the text. If null, we will estimate the color of the text. + * @param textSize Size of the text. If null, we may have an indeterminate result where it + * passes only one of the tests. + * @param x Starting x-coordinate of the view in the image. + * @param y Starting y-coordinate of the view in the image. + * @param width The width of the view in the image. + * @param height The height of the view in the image. + * @param isBold True if we know the text is bold, false otherwise. + */ + public EvaluateContrastModel(Image image, @Nullable Integer textColor, + @Nullable Double textSize, int x, int y, int width, int height, boolean isBold) { + mImageData = image.getImageData(); + mTextColor = textColor; + mTextSize = textSize; + mViewBounds = new Rectangle(x, y, width, height); + mIsBold = isBold; + + mBackgroundColors = new LinkedList(); + mForegroundColors = new LinkedList(); + mLuminanceMap = new HashMap(); + mLuminanceHistogram = new HashMap(); + + processSwatch(); + } + + /** + * Formula derived from http://gmazzocato.altervista.org/colorwheel/algo.php. + * More information can be found at http://www.w3.org/TR/WCAG20/relative-luminance.xml. + */ + public static double calculateLuminance(int color) { + Color colorObj = new Color(color); + float[] sRGB = new float[4]; + colorObj.getRGBComponents(sRGB); + + final double[] lumRGB = new double[4]; + for (int i = 0; i < sRGB.length; ++i) { + lumRGB[i] = (sRGB[i] <= 0.03928d) ? sRGB[i] / 12.92d + : Math.pow(((sRGB[i] + 0.055d) / 1.055d), 2.4d); + } + + return 0.2126d * lumRGB[0] + 0.7152d * lumRGB[1] + 0.0722d * lumRGB[2]; + } + + public static double calculateContrastRatio(double lum1, double lum2) { + if ((lum1 < 0.0d) || (lum2 < 0.0d)) { + throw new IllegalArgumentException("Luminance values may not be negative."); + } + + return (Math.max(lum1, lum2) + 0.05d) / (Math.min(lum1, lum2) + 0.05d); + } + + public static String intToHexString(int color) { + return String.format("#%06X", (0xFFFFFF & color)); + } + + private void processSwatch() { + processLuminanceData(); + extractFgBgData(); + + double textLuminance = mTextColor == null ? mForegroundLuminance : + calculateLuminance(calculateTextColor(mTextColor, mBackgroundColors.get(0))); + // Two-decimal digits of precision for the contrast ratio + mContrastRatio = Math.round(calculateContrastRatio( + textLuminance, mBackgroundLuminance) * 100.0d) / 100.0d; + } + + private void processLuminanceData() { + for (int x = mViewBounds.x; x < mViewBounds.width; ++x) { + for (int y = mViewBounds.y; y < mViewBounds.height; ++y) { + final int color = mImageData.getPixel(x, y); + final double luminance = calculateLuminance(color); + if (!mLuminanceMap.containsKey(color)) { + mLuminanceMap.put(color, luminance); + } + + if (!mLuminanceHistogram.containsKey(luminance)) { + mLuminanceHistogram.put(luminance, 0); + } + + mLuminanceHistogram.put(luminance, mLuminanceHistogram.get(luminance) + 1); + } + } + } + + private void extractFgBgData() { + if (mLuminanceMap.isEmpty()) { + // An empty luminance map indicates we've encountered a 0px area + // image. It has no luminance. + mBackgroundLuminance = mForegroundLuminance = 0; + mBackgroundColors.add(0); + mForegroundColors.add(0); + } else if (mLuminanceMap.size() == 1) { + // Deal with views that only contain a single color + mBackgroundLuminance = mForegroundLuminance = mLuminanceHistogram.keySet().iterator() + .next(); + final int singleColor = mLuminanceMap.keySet().iterator().next(); + mForegroundColors.add(singleColor); + mBackgroundColors.add(singleColor); + } else { + // Sort all luminance values seen from low to high + final ArrayList> colorsByLuminance = + Lists.newArrayList(mLuminanceMap.entrySet()); + Collections.sort(colorsByLuminance, new Comparator>() { + @Override + public int compare(Entry lhs, Entry rhs) { + return Double.compare(lhs.getValue(), rhs.getValue()); + } + }); + + // Sort luminance values seen by frequency in the image + final ArrayList> luminanceByFrequency = + Lists.newArrayList(mLuminanceHistogram.entrySet()); + Collections.sort(luminanceByFrequency, new Comparator>() { + @Override + public int compare(Entry lhs, Entry rhs) { + return lhs.getValue() - rhs.getValue(); + } + }); + + // Find the average luminance value within the set of luminances for + // purposes of splitting luminance values into high-luminance and + // low-luminance buckets. This is explicitly not a weighted average. + double luminanceSum = 0; + for (Entry luminanceCount : luminanceByFrequency) { + luminanceSum += luminanceCount.getKey(); + } + + final double averageLuminance = luminanceSum / luminanceByFrequency.size(); + + // Select the highest and lowest luminance values that contribute to + // most number of pixels in the image -- our background and + // foreground colors. + double lowLuminanceContributor = 0.0d; + for (int i = luminanceByFrequency.size() - 1; i >= 0; --i) { + final double luminanceValue = luminanceByFrequency.get(i).getKey(); + if (luminanceValue < averageLuminance) { + lowLuminanceContributor = luminanceValue; + break; + } + } + + double highLuminanceContributor = 1.0d; + for (int i = luminanceByFrequency.size() - 1; i >= 0; --i) { + final double luminanceValue = luminanceByFrequency.get(i).getKey(); + if (luminanceValue >= averageLuminance) { + highLuminanceContributor = luminanceValue; + break; + } + } + + // Background luminance is that which occurs more frequently + if (mLuminanceHistogram.get(highLuminanceContributor) + > mLuminanceHistogram.get(lowLuminanceContributor)) { + mBackgroundLuminance = highLuminanceContributor; + mForegroundLuminance = lowLuminanceContributor; + } else { + mBackgroundLuminance = lowLuminanceContributor; + mForegroundLuminance = highLuminanceContributor; + } + + // Determine the contributing colors for those luminance values + // TODO: Optimize (find an alternative to reiterating through whole image) + for (Entry colorLuminance : mLuminanceMap.entrySet()) { + if (colorLuminance.getValue() == mBackgroundLuminance) { + mBackgroundColors.add(colorLuminance.getKey()); + } + + if (colorLuminance.getValue() == mForegroundLuminance) { + mForegroundColors.add(colorLuminance.getKey()); + } + } + } + } + + /** + * Calculates a more accurate text color for how the text in the view appears by using its alpha + * value to determine how much it needs to be blended into its background color. + * + * @param textColor Text color. + * @param backgroundColor Background color. + * @return Calculated text color. + */ + private int calculateTextColor(int textColor, int backgroundColor) { + Color text = new Color(textColor, true); + Color background = new Color(backgroundColor, true); + + int alpha = text.getAlpha(); + double alphaPercentage = alpha / MAX_RGB_VALUE; + double alphaCompliment = 1 - alphaPercentage; + + int red = (int) (alphaPercentage * text.getRed() + alphaCompliment * background.getRed()); + int green = (int) (alphaPercentage * text.getGreen() + + alphaCompliment * background.getGreen()); + int blue = (int) (alphaPercentage * text.getBlue() + + alphaCompliment * background.getBlue()); + + Color rgb = new Color(red, green, blue, (int) MAX_RGB_VALUE); + mTextColor = rgb.getRGB(); + + return mTextColor; + } + + public ContrastResult getContrastResult() { + ContrastResult normalTest = getContrastResultForNormalText(); + ContrastResult largeTest = getContrastResultForLargeText(); + + if (normalTest == largeTest) { + return normalTest; + } else if (mTextSize == null) { + return ContrastResult.INDETERMINATE; + } else if (mTextSize >= NORMAL_TEXT_BOLD_SZ_PTS && mIsBold || + mTextSize > NORMAL_TEXT_SZ_PTS) { + return largeTest; + } else { + return normalTest; + } + } + + public ContrastResult getContrastResultForLargeText() { + return mContrastRatio >= CONTRAST_RATIO_LARGE_TEXT ? + ContrastResult.PASS : ContrastResult.FAIL; + } + + public ContrastResult getContrastResultForNormalText() { + if (mIsBold && mTextSize != null && mTextSize >= NORMAL_TEXT_BOLD_SZ_PTS) { + return getContrastResultForLargeText(); + } + return mContrastRatio >= CONTRAST_RATIO_NORMAL_TEXT ? + ContrastResult.PASS : ContrastResult.FAIL; + } + + public double getContrastRatio() { + return mContrastRatio; + } + + public double getBackgroundLuminance() { + return mBackgroundLuminance; + } + + public String getTextSize() { + if (mTextSize == null ){ + return NOT_APPLICABLE; + } + return Double.toString(mTextSize); + } + + public int getTextColor() { + Integer textColor; + + if (mTextColor != null) { + textColor = mTextColor; + } else { + // assumes that the foreground color is the luminance value that occurs the least + // frequently; which is also the best estimate we have for text color. + textColor = mForegroundColors.get(0); + } + + return textColor.intValue(); + } + + public String getTextColorHex() { + return intToHexString(getTextColor()); + } + + public int getBackgroundColor() { + return mBackgroundColors.get(0); + } + + public String getBackgroundColorHex() { + return intToHexString(mBackgroundColors.get(0)); + } + + public boolean isIndeterminate() { + return mTextSize == null && getContrastResult() == ContrastResult.INDETERMINATE; + } + + public boolean isBold() { + return mIsBold; + } + +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/PixelPerfectModel.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/PixelPerfectModel.java new file mode 100644 index 00000000..f054b0f1 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/PixelPerfectModel.java @@ -0,0 +1,360 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.models; + +import com.android.ddmlib.IDevice; + +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.widgets.Display; + +import java.util.ArrayList; + +public class PixelPerfectModel { + + public static final int MIN_ZOOM = 2; + + public static final int MAX_ZOOM = 24; + + public static final int DEFAULT_ZOOM = 8; + + public static final int DEFAULT_OVERLAY_TRANSPARENCY_PERCENTAGE = 50; + + private IDevice mDevice; + + private Image mImage; + + private Point mCrosshairLocation; + + private ViewNode mViewNode; + + private ViewNode mSelectedNode; + + private int mZoom; + + private final ArrayList mImageChangeListeners = + new ArrayList(); + + private Image mOverlayImage; + + private double mOverlayTransparency = DEFAULT_OVERLAY_TRANSPARENCY_PERCENTAGE / 100.0; + + private static PixelPerfectModel sModel; + + public static PixelPerfectModel getModel() { + if (sModel == null) { + sModel = new PixelPerfectModel(); + } + return sModel; + } + + public void setData(final IDevice device, final Image image, final ViewNode viewNode) { + final Image toDispose = this.mImage; + final Image toDispose2 = this.mOverlayImage; + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + synchronized (PixelPerfectModel.this) { + PixelPerfectModel.this.mDevice = device; + PixelPerfectModel.this.mImage = image; + PixelPerfectModel.this.mViewNode = viewNode; + if (image != null) { + PixelPerfectModel.this.mCrosshairLocation = + new Point(image.getBounds().width / 2, image.getBounds().height / 2); + } else { + PixelPerfectModel.this.mCrosshairLocation = null; + } + mOverlayImage = null; + PixelPerfectModel.this.mSelectedNode = null; + mZoom = DEFAULT_ZOOM; + } + } + }); + notifyImageLoaded(); + if (toDispose != null) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + toDispose.dispose(); + } + }); + } + if (toDispose2 != null) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + toDispose2.dispose(); + } + }); + } + + } + + public void setCrosshairLocation(int x, int y) { + synchronized (this) { + mCrosshairLocation = new Point(x, y); + } + notifyCrosshairMoved(); + } + + public void setSelected(ViewNode selected) { + synchronized (this) { + this.mSelectedNode = selected; + } + notifySelectionChanged(); + } + + public void setTree(final ViewNode viewNode) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + synchronized (PixelPerfectModel.this) { + PixelPerfectModel.this.mViewNode = viewNode; + PixelPerfectModel.this.mSelectedNode = null; + } + } + }); + notifyTreeChanged(); + } + + public void setImage(final Image image) { + final Image toDispose = this.mImage; + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + synchronized (PixelPerfectModel.this) { + PixelPerfectModel.this.mImage = image; + } + } + }); + notifyImageChanged(); + if (toDispose != null) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + toDispose.dispose(); + } + }); + } + } + + public void setZoom(int newZoom) { + synchronized (this) { + if (newZoom < MIN_ZOOM) { + newZoom = MIN_ZOOM; + } + if (newZoom > MAX_ZOOM) { + newZoom = MAX_ZOOM; + } + mZoom = newZoom; + } + notifyZoomChanged(); + } + + public void setOverlayImage(final Image overlayImage) { + final Image toDispose = this.mOverlayImage; + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + synchronized (PixelPerfectModel.this) { + PixelPerfectModel.this.mOverlayImage = overlayImage; + } + } + }); + notifyOverlayChanged(); + if (toDispose != null) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + toDispose.dispose(); + } + }); + } + } + + public void setOverlayTransparency(double value) { + synchronized (this) { + value = Math.max(value, 0); + value = Math.min(value, 1); + mOverlayTransparency = value; + } + notifyOverlayTransparencyChanged(); + } + + public ViewNode getViewNode() { + synchronized (this) { + return mViewNode; + } + } + + public Point getCrosshairLocation() { + synchronized (this) { + return mCrosshairLocation; + } + } + + public Image getImage() { + synchronized (this) { + return mImage; + } + } + + public ViewNode getSelected() { + synchronized (this) { + return mSelectedNode; + } + } + + public IDevice getDevice() { + synchronized (this) { + return mDevice; + } + } + + public int getZoom() { + synchronized (this) { + return mZoom; + } + } + + public Image getOverlayImage() { + synchronized (this) { + return mOverlayImage; + } + } + + public double getOverlayTransparency() { + synchronized (this) { + return mOverlayTransparency; + } + } + + public static interface IImageChangeListener { + public void imageLoaded(); + + public void imageChanged(); + + public void crosshairMoved(); + + public void selectionChanged(); + + public void treeChanged(); + + public void zoomChanged(); + + public void overlayChanged(); + + public void overlayTransparencyChanged(); + } + + private IImageChangeListener[] getImageChangeListenerList() { + IImageChangeListener[] listeners = null; + synchronized (mImageChangeListeners) { + if (mImageChangeListeners.size() == 0) { + return null; + } + listeners = + mImageChangeListeners.toArray(new IImageChangeListener[mImageChangeListeners + .size()]); + } + return listeners; + } + + public void notifyImageLoaded() { + IImageChangeListener[] listeners = getImageChangeListenerList(); + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].imageLoaded(); + } + } + } + + public void notifyImageChanged() { + IImageChangeListener[] listeners = getImageChangeListenerList(); + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].imageChanged(); + } + } + } + + public void notifyCrosshairMoved() { + IImageChangeListener[] listeners = getImageChangeListenerList(); + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].crosshairMoved(); + } + } + } + + public void notifySelectionChanged() { + IImageChangeListener[] listeners = getImageChangeListenerList(); + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].selectionChanged(); + } + } + } + + public void notifyTreeChanged() { + IImageChangeListener[] listeners = getImageChangeListenerList(); + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].treeChanged(); + } + } + } + + public void notifyZoomChanged() { + IImageChangeListener[] listeners = getImageChangeListenerList(); + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].zoomChanged(); + } + } + } + + public void notifyOverlayChanged() { + IImageChangeListener[] listeners = getImageChangeListenerList(); + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].overlayChanged(); + } + } + } + + public void notifyOverlayTransparencyChanged() { + IImageChangeListener[] listeners = getImageChangeListenerList(); + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].overlayTransparencyChanged(); + } + } + } + + public void addImageChangeListener(IImageChangeListener listener) { + synchronized (mImageChangeListeners) { + mImageChangeListeners.add(listener); + } + } + + public void removeImageChangeListener(IImageChangeListener listener) { + synchronized (mImageChangeListeners) { + mImageChangeListeners.remove(listener); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/ThemeModel.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/ThemeModel.java new file mode 100644 index 00000000..4e0972ff --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/ThemeModel.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2014 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.hierarchyviewerlib.models; + +import com.android.annotations.NonNull; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class manages the resource data that was dumped from a View's Theme. + */ +public class ThemeModel { + + private List data; + + public ThemeModel() { + data = new ArrayList(); + } + + public void add(@NonNull String name, @NonNull String value) { + data.add(new ThemeModelData(name, value)); + } + + public List getData() { + return data; + } + + public class ThemeModelData { + private String mName; + private String mValue; + + public ThemeModelData(@NonNull String name, @NonNull String value) { + mName = name; + mValue = value; + } + + public String getName() { + return mName; + } + + public String getValue() { + return mValue; + } + } +} \ No newline at end of file diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/TreeViewModel.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/TreeViewModel.java new file mode 100644 index 00000000..0eb96c95 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/TreeViewModel.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.models; + +import com.android.hierarchyviewerlib.ui.util.DrawableViewNode; +import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Point; +import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Rectangle; + +import java.util.ArrayList; + +public class TreeViewModel { + public static final double MAX_ZOOM = 2; + + public static final double MIN_ZOOM = 0.2; + + private Window mWindow; + + private DrawableViewNode mTree; + + private DrawableViewNode mSelectedNode; + + private Rectangle mViewport; + + private double mZoom; + + private final ArrayList mTreeChangeListeners = + new ArrayList(); + + private static TreeViewModel sModel; + + public static TreeViewModel getModel() { + if (sModel == null) { + sModel = new TreeViewModel(); + } + return sModel; + } + + public void setData(Window window, ViewNode viewNode) { + synchronized (this) { + if (mTree != null) { + mTree.viewNode.dispose(); + } + this.mWindow = window; + if (viewNode == null) { + mTree = null; + } else { + mTree = new DrawableViewNode(viewNode); + mTree.setLeft(); + mTree.placeRoot(); + } + mViewport = null; + mZoom = 1; + mSelectedNode = null; + } + notifyTreeChanged(); + } + + public void setSelection(DrawableViewNode selectedNode) { + synchronized (this) { + this.mSelectedNode = selectedNode; + } + notifySelectionChanged(); + } + + public void setViewport(Rectangle viewport) { + synchronized (this) { + this.mViewport = viewport; + } + notifyViewportChanged(); + } + + public void setZoom(double newZoom) { + Point zoomPoint = null; + synchronized (this) { + if (mTree != null && mViewport != null) { + zoomPoint = + new Point(mViewport.x + mViewport.width / 2, mViewport.y + mViewport.height / 2); + } + } + zoomOnPoint(newZoom, zoomPoint); + } + + public void zoomOnPoint(double newZoom, Point zoomPoint) { + synchronized (this) { + if (mTree != null && this.mViewport != null) { + if (newZoom < MIN_ZOOM) { + newZoom = MIN_ZOOM; + } + if (newZoom > MAX_ZOOM) { + newZoom = MAX_ZOOM; + } + mViewport.x = zoomPoint.x - (zoomPoint.x - mViewport.x) * mZoom / newZoom; + mViewport.y = zoomPoint.y - (zoomPoint.y - mViewport.y) * mZoom / newZoom; + mViewport.width = mViewport.width * mZoom / newZoom; + mViewport.height = mViewport.height * mZoom / newZoom; + mZoom = newZoom; + } + } + notifyZoomChanged(); + } + + public DrawableViewNode getTree() { + synchronized (this) { + return mTree; + } + } + + public Window getWindow() { + synchronized (this) { + return mWindow; + } + } + + public Rectangle getViewport() { + synchronized (this) { + return mViewport; + } + } + + public double getZoom() { + synchronized (this) { + return mZoom; + } + } + + public DrawableViewNode getSelection() { + synchronized (this) { + return mSelectedNode; + } + } + + public static interface ITreeChangeListener { + public void treeChanged(); + + public void selectionChanged(); + + public void viewportChanged(); + + public void zoomChanged(); + } + + private ITreeChangeListener[] getTreeChangeListenerList() { + ITreeChangeListener[] listeners = null; + synchronized (mTreeChangeListeners) { + if (mTreeChangeListeners.size() == 0) { + return null; + } + listeners = + mTreeChangeListeners.toArray(new ITreeChangeListener[mTreeChangeListeners.size()]); + } + return listeners; + } + + public void notifyTreeChanged() { + ITreeChangeListener[] listeners = getTreeChangeListenerList(); + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].treeChanged(); + } + } + } + + public void notifySelectionChanged() { + ITreeChangeListener[] listeners = getTreeChangeListenerList(); + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].selectionChanged(); + } + } + } + + public void notifyViewportChanged() { + ITreeChangeListener[] listeners = getTreeChangeListenerList(); + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].viewportChanged(); + } + } + } + + public void notifyZoomChanged() { + ITreeChangeListener[] listeners = getTreeChangeListenerList(); + if (listeners != null) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].zoomChanged(); + } + } + } + + public void addTreeChangeListener(ITreeChangeListener listener) { + synchronized (mTreeChangeListeners) { + mTreeChangeListeners.add(listener); + } + } + + public void removeTreeChangeListener(ITreeChangeListener listener) { + synchronized (mTreeChangeListeners) { + mTreeChangeListeners.remove(listener); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/ViewNode.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/ViewNode.java new file mode 100644 index 00000000..cb00888a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/ViewNode.java @@ -0,0 +1,369 @@ +/* + * 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.hierarchyviewerlib.models; + +import org.eclipse.swt.graphics.Image; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +public class ViewNode { + + public static enum ProfileRating { + RED, YELLOW, GREEN, NONE + }; + + private static final double RED_THRESHOLD = 0.8; + + private static final double YELLOW_THRESHOLD = 0.5; + + public static final String MISCELLANIOUS = "miscellaneous"; + + public String id; + + public String name; + + public String hashCode; + + public List properties = new ArrayList(); + + public Map namedProperties = new HashMap(); + + public ViewNode parent; + + public List children = new ArrayList(); + + public int left; + + public int top; + + public int width; + + public int height; + + public int scrollX; + + public int scrollY; + + public int paddingLeft; + + public int paddingRight; + + public int paddingTop; + + public int paddingBottom; + + public int marginLeft; + + public int marginRight; + + public int marginTop; + + public int marginBottom; + + public int baseline; + + public boolean willNotDraw; + + public boolean hasMargins; + + public boolean hasFocus; + + public int index; + + public double measureTime; + + public double layoutTime; + + public double drawTime; + + public ProfileRating measureRating = ProfileRating.NONE; + + public ProfileRating layoutRating = ProfileRating.NONE; + + public ProfileRating drawRating = ProfileRating.NONE; + + public Set categories = new TreeSet(); + + public Window window; + + public Image image; + + public int imageReferences = 1; + + public int viewCount; + + public boolean filtered; + + public int protocolVersion; + + public ViewNode(Window window, ViewNode parent, String data) { + this.window = window; + this.parent = parent; + index = this.parent == null ? 0 : this.parent.children.size(); + if (this.parent != null) { + this.parent.children.add(this); + } + int delimIndex = data.indexOf('@'); + if (delimIndex < 0) { + throw new IllegalArgumentException("Invalid format for ViewNode, missing @: " + data); + } + name = data.substring(0, delimIndex); + data = data.substring(delimIndex + 1); + delimIndex = data.indexOf(' '); + hashCode = data.substring(0, delimIndex); + + if (data.length() > delimIndex + 1) { + loadProperties(data.substring(delimIndex + 1).trim()); + } else { + // defaults in case properties are not available + id = "unknown"; + width = height = 10; + } + + measureTime = -1; + layoutTime = -1; + drawTime = -1; + } + + public void dispose() { + final int N = children.size(); + for (int i = 0; i < N; i++) { + children.get(i).dispose(); + } + dereferenceImage(); + } + + public void referenceImage() { + imageReferences++; + } + + public void dereferenceImage() { + imageReferences--; + if (image != null && imageReferences == 0) { + image.dispose(); + } + } + + private void loadProperties(String data) { + int start = 0; + boolean stop; + do { + int index = data.indexOf('=', start); + ViewNode.Property property = new ViewNode.Property(); + property.name = data.substring(start, index); + + int index2 = data.indexOf(',', index + 1); + int length = Integer.parseInt(data.substring(index + 1, index2)); + start = index2 + 1 + length; + property.value = data.substring(index2 + 1, index2 + 1 + length); + + properties.add(property); + namedProperties.put(property.name, property); + + stop = start >= data.length(); + if (!stop) { + start += 1; + } + } while (!stop); + + Collections.sort(properties, new Comparator() { + @Override + public int compare(ViewNode.Property source, ViewNode.Property destination) { + return source.name.compareTo(destination.name); + } + }); + + id = namedProperties.get("mID").value; //$NON-NLS-1$ + + left = + namedProperties.containsKey("mLeft") ? getInt("mLeft", 0) : getInt("layout:mLeft", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + 0); + top = namedProperties.containsKey("mTop") ? getInt("mTop", 0) : getInt("layout:mTop", 0); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + width = + namedProperties.containsKey("getWidth()") ? getInt("getWidth()", 0) : getInt( //$NON-NLS-1$ //$NON-NLS-2$ + "layout:getWidth()", 0); //$NON-NLS-1$ + height = + namedProperties.containsKey("getHeight()") ? getInt("getHeight()", 0) : getInt( //$NON-NLS-1$ //$NON-NLS-2$ + "layout:getHeight()", 0); //$NON-NLS-1$ + scrollX = + namedProperties.containsKey("mScrollX") ? getInt("mScrollX", 0) : getInt( //$NON-NLS-1$ //$NON-NLS-2$ + "scrolling:mScrollX", 0); //$NON-NLS-1$ + scrollY = + namedProperties.containsKey("mScrollY") ? getInt("mScrollY", 0) : getInt( //$NON-NLS-1$ //$NON-NLS-2$ + "scrolling:mScrollY", 0); //$NON-NLS-1$ + paddingLeft = + namedProperties.containsKey("mPaddingLeft") ? getInt("mPaddingLeft", 0) : getInt( //$NON-NLS-1$ //$NON-NLS-2$ + "padding:mPaddingLeft", 0); //$NON-NLS-1$ + paddingRight = + namedProperties.containsKey("mPaddingRight") ? getInt("mPaddingRight", 0) : getInt( //$NON-NLS-1$ //$NON-NLS-2$ + "padding:mPaddingRight", 0); //$NON-NLS-1$ + paddingTop = + namedProperties.containsKey("mPaddingTop") ? getInt("mPaddingTop", 0) : getInt( //$NON-NLS-1$ //$NON-NLS-2$ + "padding:mPaddingTop", 0); //$NON-NLS-1$ + paddingBottom = + namedProperties.containsKey("mPaddingBottom") ? getInt("mPaddingBottom", 0) //$NON-NLS-1$ //$NON-NLS-2$ + : getInt("padding:mPaddingBottom", 0); //$NON-NLS-1$ + marginLeft = + namedProperties.containsKey("layout_leftMargin") ? getInt("layout_leftMargin", //$NON-NLS-1$ //$NON-NLS-2$ + Integer.MIN_VALUE) : getInt("layout:layout_leftMargin", Integer.MIN_VALUE); //$NON-NLS-1$ + marginRight = + namedProperties.containsKey("layout_rightMargin") ? getInt("layout_rightMargin", //$NON-NLS-1$ //$NON-NLS-2$ + Integer.MIN_VALUE) : getInt("layout:layout_rightMargin", Integer.MIN_VALUE); //$NON-NLS-1$ + marginTop = + namedProperties.containsKey("layout_topMargin") ? getInt("layout_topMargin", //$NON-NLS-1$ //$NON-NLS-2$ + Integer.MIN_VALUE) : getInt("layout:layout_topMargin", Integer.MIN_VALUE); //$NON-NLS-1$ + marginBottom = + namedProperties.containsKey("layout_bottomMargin") ? getInt("layout_bottomMargin", //$NON-NLS-1$ //$NON-NLS-2$ + Integer.MIN_VALUE) + : getInt("layout:layout_bottomMargin", Integer.MIN_VALUE); //$NON-NLS-1$ + baseline = + namedProperties.containsKey("getBaseline()") ? getInt("getBaseline()", 0) : getInt( //$NON-NLS-1$ //$NON-NLS-2$ + "layout:getBaseline()", 0); //$NON-NLS-1$ + willNotDraw = + namedProperties.containsKey("willNotDraw()") ? getBoolean("willNotDraw()", false) //$NON-NLS-1$ //$NON-NLS-2$ + : getBoolean("drawing:willNotDraw()", false); //$NON-NLS-1$ + hasFocus = + namedProperties.containsKey("hasFocus()") ? getBoolean("hasFocus()", false) //$NON-NLS-1$ //$NON-NLS-2$ + : getBoolean("focus:hasFocus()", false); //$NON-NLS-1$ + + hasMargins = + marginLeft != Integer.MIN_VALUE && marginRight != Integer.MIN_VALUE + && marginTop != Integer.MIN_VALUE && marginBottom != Integer.MIN_VALUE; + + for (String name : namedProperties.keySet()) { + int index = name.indexOf(':'); + if (index != -1) { + categories.add(name.substring(0, index)); + } + } + if (categories.size() != 0) { + categories.add(MISCELLANIOUS); + } + } + + public void setProfileRatings() { + final int N = children.size(); + if (N > 1) { + double totalMeasure = 0; + double totalLayout = 0; + double totalDraw = 0; + for (int i = 0; i < N; i++) { + ViewNode child = children.get(i); + totalMeasure += child.measureTime; + totalLayout += child.layoutTime; + totalDraw += child.drawTime; + } + for (int i = 0; i < N; i++) { + ViewNode child = children.get(i); + if (child.measureTime / totalMeasure >= RED_THRESHOLD) { + child.measureRating = ProfileRating.RED; + } else if (child.measureTime / totalMeasure >= YELLOW_THRESHOLD) { + child.measureRating = ProfileRating.YELLOW; + } else { + child.measureRating = ProfileRating.GREEN; + } + if (child.layoutTime / totalLayout >= RED_THRESHOLD) { + child.layoutRating = ProfileRating.RED; + } else if (child.layoutTime / totalLayout >= YELLOW_THRESHOLD) { + child.layoutRating = ProfileRating.YELLOW; + } else { + child.layoutRating = ProfileRating.GREEN; + } + if (child.drawTime / totalDraw >= RED_THRESHOLD) { + child.drawRating = ProfileRating.RED; + } else if (child.drawTime / totalDraw >= YELLOW_THRESHOLD) { + child.drawRating = ProfileRating.YELLOW; + } else { + child.drawRating = ProfileRating.GREEN; + } + } + } + for (int i = 0; i < N; i++) { + children.get(i).setProfileRatings(); + } + } + + public void setViewCount() { + viewCount = 1; + final int N = children.size(); + for (int i = 0; i < N; i++) { + ViewNode child = children.get(i); + child.setViewCount(); + viewCount += child.viewCount; + } + } + + public void filter(String text) { + int dotIndex = name.lastIndexOf('.'); + String shortName = (dotIndex == -1) ? name : name.substring(dotIndex + 1); + filtered = + !text.equals("") //$NON-NLS-1$ + && (shortName.toLowerCase().contains(text.toLowerCase()) || (!id + .equals("NO_ID") && id.toLowerCase().contains(text.toLowerCase()))); //$NON-NLS-1$ + final int N = children.size(); + for (int i = 0; i < N; i++) { + children.get(i).filter(text); + } + } + + private boolean getBoolean(String name, boolean defaultValue) { + Property p = namedProperties.get(name); + if (p != null) { + try { + return Boolean.parseBoolean(p.value); + } catch (NumberFormatException e) { + return defaultValue; + } + } + return defaultValue; + } + + private int getInt(String name, int defaultValue) { + Property p = namedProperties.get(name); + if (p != null) { + try { + return Integer.parseInt(p.value); + } catch (NumberFormatException e) { + return defaultValue; + } + } + return defaultValue; + } + + @Override + public String toString() { + return name + "@" + hashCode; //$NON-NLS-1$ + } + + public static class Property { + public String name; + + public String value; + + @Override + public String toString() { + return name + '=' + value; + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/Window.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/Window.java new file mode 100644 index 00000000..a0a12917 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/models/Window.java @@ -0,0 +1,117 @@ +/* + * 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.hierarchyviewerlib.models; + +import com.android.ddmlib.Client; +import com.android.ddmlib.IDevice; +import com.android.hierarchyviewerlib.device.IHvDevice; + +/** + * Used for storing a window from the window manager service on the device. + * These are the windows that the device selector shows. + */ +public class Window { + private final String mTitle; + private final int mHashCode; + private final IHvDevice mHvDevice; + private final Client mClient; + + public Window(IHvDevice device, String title, int hashCode) { + mHvDevice = device; + mTitle = title; + mHashCode = hashCode; + mClient = null; + } + + public Window(IHvDevice device, String title, Client c) { + mHvDevice = device; + mTitle = title; + mClient = c; + mHashCode = c.hashCode(); + } + + public String getTitle() { + return mTitle; + } + + public int getHashCode() { + return mHashCode; + } + + public String encode() { + return Integer.toHexString(mHashCode); + } + + @Override + public String toString() { + return mTitle; + } + + public IHvDevice getHvDevice() { + return mHvDevice; + } + + public IDevice getDevice() { + return mHvDevice.getDevice(); + } + + public Client getClient() { + return mClient; + } + + public static Window getFocusedWindow(IHvDevice device) { + return new Window(device, "", -1); + } + + /* + * After each refresh of the windows in the device selector, the windows are + * different instances and automatically reselecting the same window doesn't + * work in the device selector unless the equals method is defined here. + */ + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + + Window other = (Window) obj; + if (mHvDevice == null) { + if (other.mHvDevice != null) + return false; + } else if (!mHvDevice.getDevice().getSerialNumber().equals( + other.mHvDevice.getDevice().getSerialNumber())) + return false; + + if (mHashCode != other.mHashCode) + return false; + + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + ((mHvDevice == null) ? 0 : mHvDevice.getDevice().getSerialNumber().hashCode()); + result = prime * result + mHashCode; + return result; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/CaptureDisplay.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/CaptureDisplay.java new file mode 100644 index 00000000..a3953fd3 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/CaptureDisplay.java @@ -0,0 +1,218 @@ +/* + * 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.hierarchyviewerlib.ui; + +import com.android.ddmuilib.ImageLoader; +import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import com.android.hierarchyviewerlib.models.ViewNode; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.PaintEvent; +import org.eclipse.swt.events.PaintListener; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.events.ShellAdapter; +import org.eclipse.swt.events.ShellEvent; +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.layout.RowLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +public class CaptureDisplay { + private static Shell sShell; + + private static Canvas sCanvas; + + private static Image sImage; + + private static ViewNode sViewNode; + + private static Composite sButtonBar; + + private static Button sOnWhite; + + private static Button sOnBlack; + + private static Button sShowExtras; + + public static void show(Shell parentShell, ViewNode viewNode, Image image) { + if (sShell == null) { + createShell(); + } + if (sShell.isVisible() && CaptureDisplay.sViewNode != null) { + CaptureDisplay.sViewNode.dereferenceImage(); + } + CaptureDisplay.sImage = image; + CaptureDisplay.sViewNode = viewNode; + viewNode.referenceImage(); + sShell.setText(viewNode.name); + + boolean shellVisible = sShell.isVisible(); + if (!shellVisible) { + sShell.setSize(0, 0); + } + Rectangle bounds = + sShell.computeTrim(0, 0, Math.max(sButtonBar.getBounds().width, + image.getBounds().width), sButtonBar.getBounds().height + + image.getBounds().height + 5); + sShell.setSize(bounds.width, bounds.height); + if (!shellVisible) { + sShell.setLocation(parentShell.getBounds().x + + (parentShell.getBounds().width - bounds.width) / 2, parentShell.getBounds().y + + (parentShell.getBounds().height - bounds.height) / 2); + } + sShell.open(); + if (shellVisible) { + sCanvas.redraw(); + } + } + + private static void createShell() { + sShell = new Shell(Display.getDefault(), SWT.CLOSE | SWT.TITLE); + GridLayout gridLayout = new GridLayout(); + gridLayout.marginWidth = 0; + gridLayout.marginHeight = 0; + sShell.setLayout(gridLayout); + + sButtonBar = new Composite(sShell, SWT.NONE); + RowLayout rowLayout = new RowLayout(SWT.HORIZONTAL); + rowLayout.pack = true; + rowLayout.center = true; + sButtonBar.setLayout(rowLayout); + Composite buttons = new Composite(sButtonBar, SWT.NONE); + buttons.setLayout(new FillLayout()); + + sOnWhite = new Button(buttons, SWT.TOGGLE); + sOnWhite.setText("On White"); + sOnBlack = new Button(buttons, SWT.TOGGLE); + sOnBlack.setText("On Black"); + sOnBlack.setSelection(true); + sOnWhite.addSelectionListener(sWhiteSelectionListener); + sOnBlack.addSelectionListener(sBlackSelectionListener); + + sShowExtras = new Button(sButtonBar, SWT.CHECK); + sShowExtras.setText("Show Extras"); + sShowExtras.addSelectionListener(sExtrasSelectionListener); + + sCanvas = new Canvas(sShell, SWT.NONE); + sCanvas.setLayoutData(new GridData(GridData.FILL_BOTH)); + sCanvas.addPaintListener(sPaintListener); + + sShell.addShellListener(sShellListener); + + ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); + Image image = imageLoader.loadImage("display.png", Display.getDefault()); //$NON-NLS-1$ + sShell.setImage(image); + } + + private static PaintListener sPaintListener = new PaintListener() { + + @Override + public void paintControl(PaintEvent e) { + if (sOnWhite.getSelection()) { + e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); + } else { + e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); + } + e.gc.fillRectangle(0, 0, sCanvas.getBounds().width, sCanvas.getBounds().height); + if (sImage != null) { + int width = sImage.getBounds().width; + int height = sImage.getBounds().height; + int x = (sCanvas.getBounds().width - width) / 2; + int y = (sCanvas.getBounds().height - height) / 2; + e.gc.drawImage(sImage, x, y); + if (sShowExtras.getSelection()) { + if ((sViewNode.paddingLeft | sViewNode.paddingRight | sViewNode.paddingTop | sViewNode.paddingBottom) != 0) { + e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLUE)); + e.gc.drawRectangle(x + sViewNode.paddingLeft, y + sViewNode.paddingTop, width + - sViewNode.paddingLeft - sViewNode.paddingRight - 1, height + - sViewNode.paddingTop - sViewNode.paddingBottom - 1); + } + if (sViewNode.hasMargins) { + e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_GREEN)); + e.gc.drawRectangle(x - sViewNode.marginLeft, y - sViewNode.marginTop, width + + sViewNode.marginLeft + sViewNode.marginRight - 1, height + + sViewNode.marginTop + sViewNode.marginBottom - 1); + } + if (sViewNode.baseline != -1) { + e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_RED)); + e.gc.drawLine(x, y + sViewNode.baseline, x + width - 1, sViewNode.baseline); + } + } + } + } + }; + + private static ShellAdapter sShellListener = new ShellAdapter() { + @Override + public void shellClosed(ShellEvent e) { + e.doit = false; + sShell.setVisible(false); + if (sViewNode != null) { + sViewNode.dereferenceImage(); + } + } + + }; + + private static SelectionListener sWhiteSelectionListener = new SelectionListener() { + @Override + public void widgetDefaultSelected(SelectionEvent e) { + // pass + } + + @Override + public void widgetSelected(SelectionEvent e) { + sOnWhite.setSelection(true); + sOnBlack.setSelection(false); + sCanvas.redraw(); + } + }; + + private static SelectionListener sBlackSelectionListener = new SelectionListener() { + @Override + public void widgetDefaultSelected(SelectionEvent e) { + // pass + } + + @Override + public void widgetSelected(SelectionEvent e) { + sOnBlack.setSelection(true); + sOnWhite.setSelection(false); + sCanvas.redraw(); + } + }; + + private static SelectionListener sExtrasSelectionListener = new SelectionListener() { + @Override + public void widgetDefaultSelected(SelectionEvent e) { + // pass + } + + @Override + public void widgetSelected(SelectionEvent e) { + sCanvas.redraw(); + } + }; +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DevicePropertyEditingSupport.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DevicePropertyEditingSupport.java new file mode 100644 index 00000000..991c0373 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DevicePropertyEditingSupport.java @@ -0,0 +1,321 @@ +/* + * Copyright (C) 2013 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.hierarchyviewerlib.ui; + +import com.android.SdkConstants; +import com.android.hierarchyviewerlib.device.IHvDevice; +import com.android.hierarchyviewerlib.models.ViewNode; +import com.android.hierarchyviewerlib.models.ViewNode.Property; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableSet; + +import java.text.ParseException; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.text.NumberFormat; + +public class DevicePropertyEditingSupport { + public enum PropertyType { + INTEGER, + INTEGER_OR_CONSTANT, + ENUM, + }; + + private static final List sDevicePropertyEditors = Arrays.asList( + new LayoutPropertyEditor(), + new PaddingPropertyEditor() + ); + + public boolean canEdit(Property p) { + return getPropertyEditorFor(p) != null; + } + + private IDevicePropertyEditor getPropertyEditorFor(Property p) { + for (IDevicePropertyEditor pe: sDevicePropertyEditors) { + if (pe.canEdit(p)) { + return pe; + } + } + + return null; + } + + public PropertyType getPropertyType(Property p) { + return getPropertyEditorFor(p).getType(p); + } + + public String[] getPropertyRange(Property p) { + return getPropertyEditorFor(p).getPropertyRange(p); + } + + public boolean setValue(Collection properties, Property p, Object newValue, + ViewNode viewNode, IHvDevice device) { + return getPropertyEditorFor(p).setValue(properties, p, newValue, viewNode, device); + } + + private static String stripCategoryPrefix(String name) { + return name.substring(name.indexOf(':') + 1); + } + + private interface IDevicePropertyEditor { + boolean canEdit(Property p); + PropertyType getType(Property p); + String[] getPropertyRange(Property p); + boolean setValue(Collection properties, Property p, Object newValue, + ViewNode viewNode, IHvDevice device); + } + + private static class LayoutPropertyEditor implements IDevicePropertyEditor { + private static final Set sLayoutPropertiesWithStringValues = + ImmutableSet.of(SdkConstants.ATTR_LAYOUT_WIDTH, + SdkConstants.ATTR_LAYOUT_HEIGHT, + SdkConstants.ATTR_LAYOUT_GRAVITY); + + private static final int MATCH_PARENT = -1; + private static final int FILL_PARENT = -1; + private static final int WRAP_CONTENT = -2; + + private enum LayoutGravity { + top(0x30), + bottom(0x50), + left(0x03), + right(0x05), + center_vertical(0x10), + fill_vertical(0x70), + center_horizontal(0x01), + fill_horizontal(0x07), + center(0x11), + fill(0x77), + clip_vertical(0x80), + clip_horizontal(0x08), + start(0x00800003), + end(0x00800005); + + private final int mValue; + + private LayoutGravity(int v) { + mValue = v; + } + } + + /** + * Returns true if this is a layout property with either a known string value, or an + * integer value. + */ + @Override + public boolean canEdit(Property p) { + String name = stripCategoryPrefix(p.name); + if (!name.startsWith(SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX)) { + return false; + } + + if (sLayoutPropertiesWithStringValues.contains(name)) { + return true; + } + + try { + parseLocalizedInt(p.value); + return true; + } catch (ParseException e) { + return false; + } + } + + @Override + public PropertyType getType(Property p) { + String name = stripCategoryPrefix(p.name); + if (sLayoutPropertiesWithStringValues.contains(name)) { + return PropertyType.INTEGER_OR_CONSTANT; + } else { + return PropertyType.INTEGER; + } + } + + @Override + public String[] getPropertyRange(Property p) { + return new String[0]; + } + + @Override + public boolean setValue(Collection properties, Property p, Object newValue, + ViewNode viewNode, IHvDevice device) { + String name = stripCategoryPrefix(p.name); + + // nothing to do if same as current value + if (p.value.equals(newValue)) { + return false; + } + + int value = -1; + String textValue = null; + + if (SdkConstants.ATTR_LAYOUT_GRAVITY.equals(name)) { + value = 0; + StringBuilder sb = new StringBuilder(20); + for (String attr: Splitter.on('|').split((String) newValue)) { + LayoutGravity g; + try { + g = LayoutGravity.valueOf(attr); + } catch (IllegalArgumentException e) { + // ignore this gravity attribute + continue; + } + + value |= g.mValue; + + if (sb.length() > 0) { + sb.append('|'); + } + sb.append(g.name()); + } + textValue = sb.toString(); + } else if (SdkConstants.ATTR_LAYOUT_HEIGHT.equals(name) + || SdkConstants.ATTR_LAYOUT_WIDTH.equals(name)) { + // newValue is of type string, but its contents may be a named constant or a integer + String s = (String) newValue; + if (s.equalsIgnoreCase(SdkConstants.VALUE_MATCH_PARENT)) { + textValue = SdkConstants.VALUE_MATCH_PARENT; + value = MATCH_PARENT; + } else if (s.equalsIgnoreCase(SdkConstants.VALUE_FILL_PARENT)) { + textValue = SdkConstants.VALUE_FILL_PARENT; + value = FILL_PARENT; + } else if (s.equalsIgnoreCase(SdkConstants.VALUE_WRAP_CONTENT)) { + textValue = SdkConstants.VALUE_WRAP_CONTENT; + value = WRAP_CONTENT; + } + } + + if (textValue == null) { + try { + value = Integer.parseInt((String) newValue); + } catch (NumberFormatException e) { + return false; + } + } + + // attempt to set the value on the device + name = name.substring(SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX.length()); + if (device.setLayoutParameter(viewNode.window, viewNode, name, value)) { + p.value = textValue != null ? textValue : (String) newValue; + } + + return true; + } + } + + private static class PaddingPropertyEditor implements IDevicePropertyEditor { + // These names should match the field names used for padding in the Framework's View class + private static final String PADDING_LEFT = "mPaddingLeft"; //$NON-NLS-1$ + private static final String PADDING_RIGHT = "mPaddingRight"; //$NON-NLS-1$ + private static final String PADDING_TOP = "mPaddingTop"; //$NON-NLS-1$ + private static final String PADDING_BOTTOM = "mPaddingBottom"; //$NON-NLS-1$ + + private static final Set sPaddingProperties = ImmutableSet.of( + PADDING_LEFT, PADDING_RIGHT, PADDING_TOP, PADDING_BOTTOM); + + @Override + public boolean canEdit(Property p) { + return sPaddingProperties.contains(stripCategoryPrefix(p.name)); + } + + @Override + public PropertyType getType(Property p) { + return PropertyType.INTEGER; + } + + @Override + public String[] getPropertyRange(Property p) { + return new String[0]; + } + + /** + * Set padding: Since the only view method is setPadding(l, t, r, b), we need access + * to all 4 padding's to update any particular one. + */ + @Override + public boolean setValue(Collection properties, Property prop, Object newValue, + ViewNode viewNode, IHvDevice device) { + int v; + try { + v = Integer.parseInt((String) newValue); + } catch (NumberFormatException e) { + return false; + } + + int pLeft = 0; + int pRight = 0; + int pTop = 0; + int pBottom = 0; + + String propName = stripCategoryPrefix(prop.name); + for (Property p: properties) { + String name = stripCategoryPrefix(p.name); + if (!sPaddingProperties.contains(name)) { + continue; + } + + if (name.equals(PADDING_LEFT)) { + pLeft = propName.equals(PADDING_LEFT) ? + v : parseLocalizedInt(p.value, 0); + } else if (name.equals(PADDING_RIGHT)) { + pRight = propName.equals(PADDING_RIGHT) ? + v : parseLocalizedInt(p.value, 0); + } else if (name.equals(PADDING_TOP)) { + pTop = propName.equals(PADDING_TOP) ? + v : parseLocalizedInt(p.value, 0); + } else if (name.equals(PADDING_BOTTOM)) { + pBottom = propName.equals(PADDING_BOTTOM) ? + v : parseLocalizedInt(p.value, 0); + } + } + + // invoke setPadding() on the device + device.invokeViewMethod(viewNode.window, viewNode, "setPadding", Arrays.asList( + Integer.valueOf(pLeft), + Integer.valueOf(pTop), + Integer.valueOf(pRight), + Integer.valueOf(pBottom) + )); + + // update the value set in the property (to avoid reading all properties back from + // the device) + prop.value = Integer.toString(v); + return true; + } + } + + public static int parseLocalizedInt(String string) + throws ParseException + { + if (string.isEmpty()) { + return 0; + } + return NumberFormat.getIntegerInstance().parse(string).intValue(); + } + + public static int parseLocalizedInt(String string, int defaultValue) + { + try + { + return parseLocalizedInt(string); + } + catch (ParseException e) {} + return defaultValue; + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DeviceSelector.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DeviceSelector.java new file mode 100644 index 00000000..567f5dd6 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DeviceSelector.java @@ -0,0 +1,342 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.ui; + +import com.android.ddmuilib.ImageLoader; +import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import com.android.hierarchyviewerlib.device.IHvDevice; +import com.android.hierarchyviewerlib.models.DeviceSelectionModel; +import com.android.hierarchyviewerlib.models.DeviceSelectionModel.IWindowChangeListener; +import com.android.hierarchyviewerlib.models.Window; + +import org.eclipse.jface.viewers.IFontProvider; +import org.eclipse.jface.viewers.ILabelProvider; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.ITreeContentProvider; +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.events.ControlAdapter; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.ControlListener; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeItem; + +public class DeviceSelector extends Composite implements IWindowChangeListener, SelectionListener { + private TreeViewer mTreeViewer; + + private Tree mTree; + + private DeviceSelectionModel mModel; + + private Font mBoldFont; + + private Image mDeviceImage; + + private Image mEmulatorImage; + + private final static int ICON_WIDTH = 16; + + private boolean mDoTreeViewStuff; + + private boolean mDoPixelPerfectStuff; + + private class ContentProvider implements ITreeContentProvider, ILabelProvider, IFontProvider { + @Override + public Object[] getChildren(Object parentElement) { + if (parentElement instanceof IHvDevice && mDoTreeViewStuff) { + Window[] list = mModel.getWindows((IHvDevice) parentElement); + if (list != null) { + return list; + } + } + return new Object[0]; + } + + @Override + public Object getParent(Object element) { + if (element instanceof Window) { + return ((Window) element).getDevice(); + } + return null; + } + + @Override + public boolean hasChildren(Object element) { + if (element instanceof IHvDevice && mDoTreeViewStuff) { + Window[] list = mModel.getWindows((IHvDevice) element); + if (list != null) { + return list.length != 0; + } + } + return false; + } + + @Override + public Object[] getElements(Object inputElement) { + if (inputElement instanceof DeviceSelectionModel) { + return mModel.getDevices(); + } + return new Object[0]; + } + + @Override + public void dispose() { + // pass + } + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } + + @Override + public Image getImage(Object element) { + if (element instanceof IHvDevice) { + if (((IHvDevice) element).getDevice().isEmulator()) { + return mEmulatorImage; + } + return mDeviceImage; + } + return null; + } + + @Override + public String getText(Object element) { + if (element instanceof IHvDevice) { + return ((IHvDevice) element).getDevice().getName(); + } else if (element instanceof Window) { + return ((Window) element).getTitle(); + } + return null; + } + + @Override + public Font getFont(Object element) { + if (element instanceof Window) { + int focusedWindow = mModel.getFocusedWindow(((Window) element).getHvDevice()); + if (focusedWindow == ((Window) element).getHashCode()) { + return mBoldFont; + } + } + return null; + } + + @Override + public void addListener(ILabelProviderListener listener) { + // pass + } + + @Override + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + @Override + public void removeListener(ILabelProviderListener listener) { + // pass + } + } + + public DeviceSelector(Composite parent, boolean doTreeViewStuff, boolean doPixelPerfectStuff) { + super(parent, SWT.NONE); + this.mDoTreeViewStuff = doTreeViewStuff; + this.mDoPixelPerfectStuff = doPixelPerfectStuff; + setLayout(new FillLayout()); + mTreeViewer = new TreeViewer(this, SWT.SINGLE); + mTreeViewer.setAutoExpandLevel(TreeViewer.ALL_LEVELS); + + mTree = mTreeViewer.getTree(); + mTree.setLinesVisible(true); + mTree.addSelectionListener(this); + + addDisposeListener(mDisposeListener); + + loadResources(); + + mModel = DeviceSelectionModel.getModel(); + ContentProvider contentProvider = new ContentProvider(); + mTreeViewer.setContentProvider(contentProvider); + mTreeViewer.setLabelProvider(contentProvider); + mModel.addWindowChangeListener(this); + mTreeViewer.setInput(mModel); + + addControlListener(mControlListener); + } + + public void loadResources() { + Display display = Display.getDefault(); + Font systemFont = display.getSystemFont(); + FontData[] fontData = systemFont.getFontData(); + FontData[] newFontData = new FontData[fontData.length]; + for (int i = 0; i < fontData.length; i++) { + newFontData[i] = + new FontData(fontData[i].getName(), fontData[i].getHeight(), fontData[i] + .getStyle() + | SWT.BOLD); + } + mBoldFont = new Font(Display.getDefault(), newFontData); + + ImageLoader loader = ImageLoader.getDdmUiLibLoader(); + mDeviceImage = + loader.loadImage(display, "device.png", ICON_WIDTH, ICON_WIDTH, display //$NON-NLS-1$ + .getSystemColor(SWT.COLOR_RED)); + + mEmulatorImage = + loader.loadImage(display, "emulator.png", ICON_WIDTH, ICON_WIDTH, display //$NON-NLS-1$ + .getSystemColor(SWT.COLOR_BLUE)); + } + + private DisposeListener mDisposeListener = new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + mModel.removeWindowChangeListener(DeviceSelector.this); + mBoldFont.dispose(); + } + }; + + // If the window gets too small, hide the data, otherwise SWT throws an + // ERROR. + + private ControlListener mControlListener = new ControlAdapter() { + private boolean noInput = false; + + @Override + public void controlResized(ControlEvent e) { + if (getBounds().height <= 38) { + mTreeViewer.setInput(null); + noInput = true; + } else if (noInput) { + mTreeViewer.setInput(mModel); + noInput = false; + } + } + }; + + @Override + public boolean setFocus() { + return mTree.setFocus(); + } + + public void setMode(boolean doTreeViewStuff, boolean doPixelPerfectStuff) { + if (this.mDoTreeViewStuff != doTreeViewStuff + || this.mDoPixelPerfectStuff != doPixelPerfectStuff) { + final boolean expandAll = !this.mDoTreeViewStuff && doTreeViewStuff; + this.mDoTreeViewStuff = doTreeViewStuff; + this.mDoPixelPerfectStuff = doPixelPerfectStuff; + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + mTreeViewer.refresh(); + if (expandAll) { + mTreeViewer.expandAll(); + } + } + }); + } + } + + @Override + public void deviceConnected(final IHvDevice device) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + mTreeViewer.refresh(); + mTreeViewer.setExpandedState(device, true); + } + }); + } + + @Override + public void deviceChanged(final IHvDevice device) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + TreeSelection selection = (TreeSelection) mTreeViewer.getSelection(); + mTreeViewer.refresh(device); + if (selection.getFirstElement() instanceof Window + && ((Window) selection.getFirstElement()).getDevice() == device) { + mTreeViewer.setSelection(selection, true); + } + } + }); + } + + @Override + public void deviceDisconnected(final IHvDevice device) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + mTreeViewer.refresh(); + } + }); + } + + @Override + public void focusChanged(final IHvDevice device) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + TreeSelection selection = (TreeSelection) mTreeViewer.getSelection(); + mTreeViewer.refresh(device); + if (selection.getFirstElement() instanceof Window + && ((Window) selection.getFirstElement()).getDevice() == device) { + mTreeViewer.setSelection(selection, true); + } + } + }); + } + + @Override + public void selectionChanged(IHvDevice device, Window window) { + // pass + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + Object selection = ((TreeItem) e.item).getData(); + if (selection instanceof IHvDevice && mDoPixelPerfectStuff) { + HierarchyViewerDirector.getDirector().loadPixelPerfectData((IHvDevice) selection); + } else if (selection instanceof Window && mDoTreeViewStuff) { + HierarchyViewerDirector.getDirector().loadViewTreeData((Window) selection); + } + } + + @Override + public void widgetSelected(SelectionEvent e) { + TreeItem item = (TreeItem) e.item; + if (item == null) return; + Object selection = item.getData(); + if (selection instanceof IHvDevice) { + mModel.setSelection((IHvDevice) selection, null); + } else if (selection instanceof Window) { + mModel.setSelection(((Window) selection).getHvDevice(), (Window) selection); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DumpThemeDisplay.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DumpThemeDisplay.java new file mode 100644 index 00000000..d105c2b0 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DumpThemeDisplay.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2014 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.hierarchyviewerlib.ui; + +import com.android.annotations.NonNull; +import com.android.hierarchyviewerlib.models.ThemeModel; + +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.ShellAdapter; +import org.eclipse.swt.events.ShellEvent; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +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.util.List; + + +public class DumpThemeDisplay { + private static final int DEFAULT_HEIGHT = 600; // px + private static final int NUM_COLUMNS = 2; + + private static Shell sShell; + private static ThemeModel sModel; + private static Text sSearchText; + private static Table sTable; + + public static void show(Shell parentShell, ThemeModel model) { + if (sShell == null) { + buildContents(); + } else { + sSearchText.setText(""); + sTable.removeAll(); + } + + sModel = model; + addTableItems("", sModel.getData()); + + // configure size and placement + sShell.setLocation(parentShell.getBounds().x, parentShell.getBounds().y); + for (int i = 0; i < NUM_COLUMNS; ++i) { + sTable.getColumn(i).pack(); + } + sTable.setLayoutData(GridDataFactory.swtDefaults().hint( + sTable.computeSize(SWT.DEFAULT, SWT.DEFAULT).x, DEFAULT_HEIGHT).create()); + sShell.pack(); + sShell.open(); + } + + private static void addTableItem(String name, String value) { + TableItem row = new TableItem(sTable, SWT.NONE); + row.setText(0, name); + row.setText(1, value); + } + + private static String sanitize(@NonNull String text) { + return text.toLowerCase().trim(); + } + + private static void addTableItems(String searchText, + List list) { + for (ThemeModel.ThemeModelData data : list) { + searchText = sanitize(searchText); + + if ("".equals(searchText)) { + addTableItem(data.getName(), data.getValue()); + } else { + if (sanitize(data.getName()).contains(searchText) + || sanitize(data.getValue()).contains(searchText)) { + addTableItem(data.getName(), data.getValue()); + } + } + } + } + + private static void buildContents() { + sShell = new Shell(Display.getDefault(), SWT.CLOSE | SWT.TITLE); + sShell.setText("Dump Theme"); + sShell.addShellListener(sShellListener); + sShell.setLayout(new GridLayout()); + + sSearchText = new Text(sShell, SWT.SINGLE | SWT.BORDER); + sSearchText.setMessage("Enter text to search list"); + sSearchText.addModifyListener(sModifyListener); + + sTable = new Table(sShell, SWT.BORDER | SWT.FULL_SELECTION); + sTable.setHeaderVisible(true); + sTable.setLinesVisible(true); + + String[] headers = { "Resource Name", "Resource Value" }; + for (int i = 0; i < headers.length; ++i) { + TableColumn column = new TableColumn(sTable, SWT.NONE); + column.setText(headers[i]); + } + } + + private static ModifyListener sModifyListener = new ModifyListener() { + @Override + public void modifyText(ModifyEvent modifyEvent) { + String searchText = sanitize(sSearchText.getText()); + sTable.removeAll(); + addTableItems(searchText, sModel.getData()); + } + }; + + private static ShellAdapter sShellListener = new ShellAdapter() { + @Override + public void shellClosed(ShellEvent e) { + e.doit = false; + sShell.setVisible(false); + } + }; +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/EvaluateContrastDisplay.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/EvaluateContrastDisplay.java new file mode 100644 index 00000000..84170ed5 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/EvaluateContrastDisplay.java @@ -0,0 +1,542 @@ +/* + * Copyright (C) 2014 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.hierarchyviewerlib.ui; + +import com.android.ddmuilib.ImageLoader; +import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import com.android.hierarchyviewerlib.models.EvaluateContrastModel; +import com.android.hierarchyviewerlib.models.ViewNode; +import com.android.hierarchyviewerlib.models.EvaluateContrastModel.ContrastResult; +import com.android.hierarchyviewerlib.models.ViewNode.Property; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.ScrolledComposite; +import org.eclipse.swt.events.PaintEvent; +import org.eclipse.swt.events.PaintListener; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.events.ShellAdapter; +import org.eclipse.swt.events.ShellEvent; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.graphics.RGB; +import org.eclipse.swt.layout.FillLayout; +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.Canvas; +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.ScrollBar; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeItem; + +import java.lang.Math; +import java.lang.Override; +import java.lang.StringBuilder; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.HashMap; + +public class EvaluateContrastDisplay { + private static final int DEFAULT_HEIGHT = 600; // px + private static final int MARGIN = 30; // px + private static final int PALLETE_IMAGE_SIZE = 16; // px + private static final int IMAGE_WIDTH = 800; // px + private static final int RESULTS_PANEL_WIDTH = 300; // px + private static final int MAX_NUM_CHARACTERS = 35; + + private static final String ABBREVIATE_SUFFIX = "...\""; + + private static Shell sShell; + private static Canvas sCanvas; + private static Composite sResultsPanel; + private static Tree sResultsTree; + + private static Image sImage; + private static Point sImageOffset; + private static ScrollBar sImageScrollBar; + private static int sImageWidth; + private static int sImageHeight; + + private static Image sYellowImage; + private static Image sRedImage; + private static Image sGreenImage; + + private static ViewNode sSelectedNode; + + private static org.eclipse.swt.graphics.Color sBorderColorPass; + private static org.eclipse.swt.graphics.Color sBorderColorFail; + private static org.eclipse.swt.graphics.Color sBorderColorIndeterminate; + private static org.eclipse.swt.graphics.Color sBorderColorCurrentlySelected; + + private static HashMap sRectangleForViewNode; + private static HashMap sBorderColorForViewNode; + private static HashMap sViewNodeForModel; + private static HashMap sImageForColor; + private static HashMap sViewNodeForTreeItem; + + private static double sScaleFactor; + + static { + sImageForColor = new HashMap(); + sViewNodeForTreeItem = new HashMap(); + + ImageLoader loader = ImageLoader.getLoader(EvaluateContrastDisplay.class); + sYellowImage = loader.loadImage("yellow.png", Display.getDefault()); + sRedImage = loader.loadImage("red.png", Display.getDefault()); + sGreenImage = loader.loadImage("green.png", Display.getDefault()); + + sRectangleForViewNode = new HashMap(); + sBorderColorForViewNode = new HashMap(); + sViewNodeForModel = new HashMap(); + } + + private static org.eclipse.swt.graphics.Color getBorderColorPass() { + if (sBorderColorPass == null) { + sBorderColorPass = /** green */ + new org.eclipse.swt.graphics.Color(Display.getDefault(), new RGB(0, 255, 0)); + } + return sBorderColorPass; + } + + private static org.eclipse.swt.graphics.Color getBorderColorFail() { + if (sBorderColorFail == null) { + sBorderColorFail = /** red */ + new org.eclipse.swt.graphics.Color(Display.getDefault(), new RGB(255, 0, 0)); + } + return sBorderColorFail; + } + + private static org.eclipse.swt.graphics.Color getBorderColorIndeterminate() { + if (sBorderColorIndeterminate == null) { + sBorderColorIndeterminate = /** yellow */ + new org.eclipse.swt.graphics.Color(Display.getDefault(), new RGB(255, 255, 0)); + } + return sBorderColorIndeterminate; + } + + private static org.eclipse.swt.graphics.Color getBorderColorCurrentlySelected() { + if (sBorderColorCurrentlySelected == null) { + sBorderColorCurrentlySelected = /** blue */ + new org.eclipse.swt.graphics.Color(Display.getDefault(), new RGB(0, 0, 255)); + } + return sBorderColorCurrentlySelected; + } + + private static void clear(boolean shellIsNull) { + sRectangleForViewNode.clear(); + sBorderColorForViewNode.clear(); + sViewNodeForModel.clear(); + + if (!shellIsNull) { + sImage.dispose(); + for (Image image : sImageForColor.values()) { + image.dispose(); + } + + sImageForColor.clear(); + sViewNodeForTreeItem.clear(); + for (Control item : sShell.getChildren()) { + item.dispose(); + } + } + + if (sBorderColorPass != null) { + sBorderColorPass.dispose(); + sBorderColorPass = null; + } + if (sBorderColorFail != null) { + sBorderColorFail.dispose(); + sBorderColorFail = null; + } + if (sBorderColorIndeterminate != null) { + sBorderColorIndeterminate.dispose(); + sBorderColorIndeterminate = null; + } + if (sBorderColorCurrentlySelected != null) { + sBorderColorCurrentlySelected.dispose(); + sBorderColorCurrentlySelected = null; + } + } + + private static Image scaleImage(Image image, int width, int height) { + Image scaled = new Image(Display.getDefault(), width, height); + GC gc = new GC(scaled); + gc.setInterpolation(SWT.HIGH); + gc.setAntialias(SWT.ON); + gc.drawImage(image, 0, 0, image.getBounds().width, image.getBounds().height, 0, 0, + width, height); + image.dispose(); + gc.dispose(); + return scaled; + } + + public static void show(Shell parentShell, ViewNode rootNode, Image image) { + clear(sShell == null); + + sScaleFactor = Math.min(IMAGE_WIDTH / (double) image.getBounds().width, 1.0); + sImage = scaleImage(image, IMAGE_WIDTH, + (int) Math.round(image.getBounds().height * sScaleFactor)); + sImageWidth = sImage.getBounds().width; + sImageHeight = sImage.getBounds().height; + + if (sShell == null) { + sShell = new Shell(Display.getDefault(), SWT.CLOSE | SWT.TITLE); + sShell.setText("Evaluate Contrast"); + sShell.addShellListener(sShellListener); + sShell.setLayout(new GridLayout(2, false)); + } + buildContents(sShell); + processEvaluatableChildViews(rootNode); + + sShell.setLocation(parentShell.getBounds().x, parentShell.getBounds().y); + sShell.setSize(IMAGE_WIDTH + RESULTS_PANEL_WIDTH + MARGIN, DEFAULT_HEIGHT + (MARGIN * 2)); + sImageScrollBar.setMaximum(sImage.getBounds().height); + sImageScrollBar.setThumb(DEFAULT_HEIGHT); + sShell.open(); + sShell.layout(); + } + + private static void buildContents(Composite shell) { + buildResultsPanel(); + buildImagePanel(shell); + } + + private static void buildResultsPanel() { + sResultsPanel = new Composite(sShell, SWT.NONE); + sResultsPanel.setLayout(new FillLayout()); + GridData gridData = new GridData(SWT.BEGINNING, SWT.BEGINNING, false, true); + sResultsPanel.setLayoutData(gridData); + + ScrolledComposite scrolledComposite = new ScrolledComposite(sResultsPanel, SWT.VERTICAL); + sResultsTree = new Tree(scrolledComposite, SWT.NONE); + sResultsTree.setLinesVisible(true); + scrolledComposite.setContent(sResultsTree); + sResultsTree.setSize(RESULTS_PANEL_WIDTH, DEFAULT_HEIGHT); + + sResultsTree.addListener(SWT.PaintItem, new Listener() { + @Override + public void handleEvent(Event event) { + TreeItem item = (TreeItem) event.item; + Image image = (Image) item.getData(); + if (image != null) { + int x = event.x + event.width; + int itemHeight = sResultsTree.getItemHeight(); + int imageHeight = image.getBounds().height; + int y = event.y + (itemHeight - imageHeight) / 2; + event.gc.drawImage(image, x, y); + } + } + }); + + Listener listener = new Listener() { + @Override + public void handleEvent(Event e) { + TreeItem treeItem = (TreeItem) e.item; + if (treeItem.getItemCount() == 0) { + do { + treeItem = treeItem.getParentItem(); + } while (treeItem.getParentItem() != null); + } + + ViewNode node = sViewNodeForTreeItem.get(treeItem); + if (sSelectedNode != node) { + sSelectedNode = sViewNodeForTreeItem.get(treeItem); + sCanvas.redraw(); + } + } + }; + sResultsTree.addListener(SWT.Selection, listener); + sResultsTree.addListener(SWT.DefaultSelection, listener); + } + + private static void buildImagePanel(Composite parent) { + sImageOffset = new Point(0, 0); + sCanvas = new Canvas(parent, SWT.V_SCROLL | SWT.NO_BACKGROUND | SWT.NO_REDRAW_RESIZE); + sCanvas.addPaintListener(new PaintListener() { + + @Override + public void paintControl(PaintEvent e) { + GC gc = e.gc; + gc.drawImage(sImage, sImageOffset.x, sImageOffset.y); + + for (ViewNode viewNode : sRectangleForViewNode.keySet()) { + Rectangle rectangle = sRectangleForViewNode.get(viewNode); + if (sSelectedNode == viewNode) { + e.gc.setForeground(getBorderColorCurrentlySelected()); + } else { + e.gc.setForeground(sBorderColorForViewNode.get(viewNode)); + } + e.gc.drawRectangle(Math.max(0, sImageOffset.x + rectangle.x - 1), + sImageOffset.y + rectangle.y - 1, + rectangle.width - 1, + rectangle.height - 1); + } + + Rectangle rect = sImage.getBounds(); + Rectangle client = sCanvas.getClientArea(); + int marginWidth = client.width - rect.width; + if (marginWidth > 0) { + gc.fillRectangle(rect.width, 0, marginWidth, client.height); + } + int marginHeight = client.height - rect.height; + if (marginHeight > 0) { + gc.fillRectangle(0, rect.height, client.width, marginHeight); + } + } + }); + + sImageScrollBar = sCanvas.getVerticalBar(); + sImageScrollBar.addListener(SWT.Selection, new Listener() { + @Override + public void handleEvent(Event e) { + int offset = sImageScrollBar.getSelection(); + Rectangle imageBounds = sImage.getBounds(); + sImageOffset.y = -offset; + + int y = -offset - sImageOffset.y; + sCanvas.scroll(0, y, 0, 0, imageBounds.width, imageBounds.height, false); + sCanvas.redraw(); + } + }); + + GridData gridData = new GridData(SWT.BEGINNING, SWT.BEGINNING, true, true); + gridData.widthHint = IMAGE_WIDTH + MARGIN; + gridData.heightHint = DEFAULT_HEIGHT; + sCanvas.setLayoutData(gridData); + } + + private static void processEvaluatableChildViews(ViewNode root){ + List children = getEvaluatableChildViews(root); + + for (final ViewNode child : children) { + calculateRectangleForViewNode(child); + EvaluateContrastModel evaluateContrastModel = evaluateContrastForView(child); + if (evaluateContrastModel != null) { + calculateBorderColorForViewNode(child, evaluateContrastModel.getContrastResult()); + buildTreeItem(evaluateContrastModel, child); + sViewNodeForModel.put(child, evaluateContrastModel); + } else { + sRectangleForViewNode.remove(child); + } + } + } + + private static void buildTreeItem(EvaluateContrastModel model, final ViewNode child) { + int dotIndex = child.name.lastIndexOf('.'); + String shortName = (dotIndex == -1) ? child.name : child.name.substring(dotIndex + 1); + String text = shortName + ": \"" + child.namedProperties.get("text:mText").value + "\""; + + TreeItem item = new TreeItem(sResultsTree, SWT.NONE); + item.setText(transformText(text, MAX_NUM_CHARACTERS)); + item.setImage(getResultImage(model.getContrastResult())); + sViewNodeForTreeItem.put(item, child); + buildTreeItemsForModel(model, item); + } + + private static Image buildImageForColor(int color) { + Image image = sImageForColor.get(color); + + if (image == null) { + image = new Image(Display.getDefault(), PALLETE_IMAGE_SIZE, PALLETE_IMAGE_SIZE); + GC gc = new GC(image); + + org.eclipse.swt.graphics.Color swtColor = awtColortoSwtColor(new java.awt.Color(color)); + gc.setBackground(swtColor); + swtColor.dispose(); + gc.fillRectangle(0, 0, PALLETE_IMAGE_SIZE, PALLETE_IMAGE_SIZE); + + swtColor = awtColortoSwtColor(java.awt.Color.BLACK); + gc.setForeground(swtColor); + swtColor.dispose(); + gc.drawRectangle(0, 0, PALLETE_IMAGE_SIZE - 1, PALLETE_IMAGE_SIZE - 1); + gc.dispose(); + + sImageForColor.put(color, image); + } + + return image; + } + + public static org.eclipse.swt.graphics.Color awtColortoSwtColor(java.awt.Color color) { + return new org.eclipse.swt.graphics.Color(Display.getDefault(), + color.getRed(), color.getGreen(), color.getBlue()); + } + + private static void buildTreeItemsForModel(EvaluateContrastModel model, TreeItem parent) { + TreeItem item = new TreeItem(parent, SWT.NONE); + item.setText("Text color: " + model.getTextColorHex()); + item.setData(buildImageForColor(model.getTextColor())); + + item = new TreeItem(parent, SWT.NONE); + item.setText("Background color: " + model.getBackgroundColorHex()); + item.setData(buildImageForColor(model.getBackgroundColor())); + + new TreeItem(parent, SWT.NONE).setText("Text size: " + model.getTextSize()); + + new TreeItem(parent, SWT.NONE).setText("Contrast ratio: " + String.format( + EvaluateContrastModel.CONTRAST_RATIO_FORMAT, model.getContrastRatio())); + + if (!model.isIndeterminate()) { + new TreeItem(parent, SWT.NONE).setText("Test: " + model.getContrastResult().name()); + } else { + item = new TreeItem(parent, SWT.NONE); + item.setText("Normal Text Test: " + model.getContrastResultForNormalText().name()); + item.setImage(getResultImage(model.getContrastResultForNormalText())); + item = new TreeItem(parent, SWT.NONE); + item.setText("Large Text Test: " + model.getContrastResultForLargeText().name()); + item.setImage(getResultImage(model.getContrastResultForLargeText())); + } + } + + private static List getEvaluatableChildViews(ViewNode root) { + List children = new ArrayList(); + + children.add(root); + for (int i = 0; i < children.size(); ++i) { + ViewNode node = children.get(i); + List temp = node.children; + for (ViewNode child: temp) { + if (!children.contains(child)) { + children.add(child); + } + } + } + + List evalutableChildren = new ArrayList(); + for (final ViewNode child : children) { + if (child.namedProperties.get("text:mText") != null) { + evalutableChildren.add(child); + } + } + + return evalutableChildren; + } + + private static void calculateBorderColorForViewNode(ViewNode node, ContrastResult result) { + org.eclipse.swt.graphics.Color borderColor; + + switch (result) { + case PASS: + borderColor = getBorderColorPass(); + break; + case FAIL: + borderColor = getBorderColorFail(); + break; + case INDETERMINATE: + default: + borderColor = getBorderColorIndeterminate(); + } + + sBorderColorForViewNode.put(node, borderColor); + } + + private static Image getResultImage(ContrastResult result) { + switch (result) { + case PASS: + return sGreenImage; + case FAIL: + return sRedImage; + default: + return sYellowImage; + } + } + + private static String transformText(String text, int maxNumCharacters) { + if (text.length() == maxNumCharacters) { + return text; + } else if (text.length() < maxNumCharacters) { + char[] filler = new char[maxNumCharacters - text.length()]; + Arrays.fill(filler,' '); + return text + new String(filler); + } + + StringBuilder abbreviatedText = new StringBuilder(); + abbreviatedText.append(text.substring(0, maxNumCharacters - ABBREVIATE_SUFFIX.length())); + abbreviatedText.append(ABBREVIATE_SUFFIX); + return abbreviatedText.toString(); + } + + private static void calculateRectangleForViewNode(ViewNode viewNode) { + int leftShift = 0; + int topShift = 0; + int nodeLeft = (int) Math.round(viewNode.left * sScaleFactor); + int nodeTop = (int) Math.round(viewNode.top * sScaleFactor); + int nodeWidth = (int) Math.round(viewNode.width * sScaleFactor); + int nodeHeight = (int) Math.round(viewNode.height * sScaleFactor); + ViewNode current = viewNode; + + while (current.parent != null) { + leftShift += (int) Math.round( + sScaleFactor * (current.parent.left - current.parent.scrollX)); + topShift += (int) Math.round( + sScaleFactor * (current.parent.top - current.parent.scrollY)); + current = current.parent; + } + + sRectangleForViewNode.put(viewNode, new Rectangle(leftShift + nodeLeft, + topShift + nodeTop, nodeWidth, nodeHeight)); + } + + private static EvaluateContrastModel evaluateContrastForView(ViewNode node) { + Map namedProperties = node.namedProperties; + Property textColorProperty = namedProperties.get("text:mCurTextColor"); + Integer textColor = textColorProperty == null ? null : + Integer.valueOf(textColorProperty.value); + Property textSizeProperty = namedProperties.get("text:getScaledTextSize()"); + Double textSize = textSizeProperty == null ? null : Double.valueOf(textSizeProperty.value); + Rectangle rectangle = sRectangleForViewNode.get(node); + Property boldProperty = namedProperties.get("text:getTypefaceStyle()"); + boolean isBold = boldProperty != null && boldProperty.value.equals("BOLD"); + + // TODO: also remove views that are covered by other views + if (rectangle.x < 0 || rectangle.x > sImageWidth || + rectangle.y < 0 || rectangle.y > sImageHeight || + rectangle.width == 0 || rectangle.height == 0) { + // not viewable in screenshot, therefore can't parse background color + return null; + } + + int x = Math.max(0, rectangle.x); + int y = Math.max(0, rectangle.y); + int width = Math.min(sImageWidth, rectangle.x + rectangle.width); + int height = Math.min(sImageHeight, rectangle.y + rectangle.height); + + return new EvaluateContrastModel( + sImage, textColor, textSize, x, y, width, height, isBold); + } + + private static ShellAdapter sShellListener = new ShellAdapter() { + @Override + public void shellClosed(ShellEvent e) { + e.doit = false; + sShell.setVisible(false); + clear(sShell == null); + } + }; +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/InvokeMethodPrompt.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/InvokeMethodPrompt.java new file mode 100644 index 00000000..41c2de4d --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/InvokeMethodPrompt.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2013 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.hierarchyviewerlib.ui; + +import com.android.ddmlib.Log; +import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import com.android.hierarchyviewerlib.device.IHvDevice; +import com.android.hierarchyviewerlib.models.TreeViewModel; +import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener; +import com.android.hierarchyviewerlib.models.ViewNode; +import com.android.hierarchyviewerlib.ui.util.DrawableViewNode; +import com.google.common.base.CharMatcher; +import com.google.common.base.Splitter; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.KeyEvent; +import org.eclipse.swt.events.KeyListener; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Text; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class InvokeMethodPrompt extends Composite implements ITreeChangeListener { + private TreeViewModel mModel; + private DrawableViewNode mSelectedNode; + private Text mText; + private static final Splitter CMD_SPLITTER = Splitter.on(CharMatcher.anyOf(", ")) + .trimResults().omitEmptyStrings(); + + public InvokeMethodPrompt(Composite parent) { + super(parent, SWT.NONE); + setLayout(new FillLayout()); + + mText = new Text(this, SWT.BORDER); + mText.addKeyListener(new KeyListener() { + @Override + public void keyReleased(KeyEvent ke) { + } + + @Override + public void keyPressed(KeyEvent ke) { + onKeyPress(ke); + } + }); + + mModel = TreeViewModel.getModel(); + mModel.addTreeChangeListener(this); + } + + private void onKeyPress(KeyEvent ke) { + if (ke.keyCode == SWT.CR) { + String cmd = mText.getText().trim(); + if (!cmd.isEmpty()) { + invokeViewMethod(cmd); + } + mText.setText(""); + } + } + + private void invokeViewMethod(String cmd) { + Iterator segmentIterator = CMD_SPLITTER.split(cmd).iterator(); + + String method = null; + if (segmentIterator.hasNext()) { + method = segmentIterator.next(); + } else { + return; + } + + List args = new ArrayList(10); + while (segmentIterator.hasNext()) { + String arg = segmentIterator.next(); + + // check for boolean + if (arg.equalsIgnoreCase("true")) { + args.add(Boolean.TRUE); + continue; + } else if (arg.equalsIgnoreCase("false")) { + args.add(Boolean.FALSE); + continue; + } + + // see if last character gives a clue regarding the argument type + char typeSpecifier = Character.toUpperCase(arg.charAt(arg.length() - 1)); + try { + switch (typeSpecifier) { + case 'L': + args.add(Long.valueOf(arg.substring(0, arg.length()))); + break; + case 'D': + args.add(Double.valueOf(arg.substring(0, arg.length()))); + break; + case 'F': + args.add(Float.valueOf(arg.substring(0, arg.length()))); + break; + case 'S': + args.add(Short.valueOf(arg.substring(0, arg.length()))); + break; + case 'B': + args.add(Byte.valueOf(arg.substring(0, arg.length()))); + break; + default: // default to integer + args.add(Integer.valueOf(arg)); + break; + } + } catch (NumberFormatException e) { + Log.e("hv", "Unable to parse method argument: " + arg); + return; + } + } + + HierarchyViewerDirector.getDirector().invokeMethodOnSelectedView(method, args); + } + + @Override + public void selectionChanged() { + mSelectedNode = mModel.getSelection(); + refresh(); + } + + private boolean isViewUpdateEnabled(ViewNode viewNode) { + IHvDevice device = viewNode.window.getHvDevice(); + return device != null && device.isViewUpdateEnabled(); + } + + private void refresh() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + mText.setEnabled(mSelectedNode != null + && isViewUpdateEnabled(mSelectedNode.viewNode)); + } + }); + } + + @Override + public void treeChanged() { + selectionChanged(); + } + + @Override + public void viewportChanged() { + } + + @Override + public void zoomChanged() { + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/LayoutViewer.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/LayoutViewer.java new file mode 100644 index 00000000..d3278feb --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/LayoutViewer.java @@ -0,0 +1,372 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.ui; + +import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import com.android.hierarchyviewerlib.models.TreeViewModel; +import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener; +import com.android.hierarchyviewerlib.ui.util.DrawableViewNode; +import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Point; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.MouseListener; +import org.eclipse.swt.events.PaintEvent; +import org.eclipse.swt.events.PaintListener; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.graphics.Transform; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Listener; + +import java.util.ArrayList; + +public class LayoutViewer extends Canvas implements ITreeChangeListener { + + private TreeViewModel mModel; + + private DrawableViewNode mTree; + + private DrawableViewNode mSelectedNode; + + private Transform mTransform; + + private Transform mInverse; + + private double mScale; + + private boolean mShowExtras = false; + + private boolean mOnBlack = true; + + public LayoutViewer(Composite parent) { + super(parent, SWT.NONE); + mModel = TreeViewModel.getModel(); + mModel.addTreeChangeListener(this); + + addDisposeListener(mDisposeListener); + addPaintListener(mPaintListener); + addListener(SWT.Resize, mResizeListener); + addMouseListener(mMouseListener); + + mTransform = new Transform(Display.getDefault()); + mInverse = new Transform(Display.getDefault()); + + treeChanged(); + } + + public void setShowExtras(boolean show) { + mShowExtras = show; + doRedraw(); + } + + public void setOnBlack(boolean value) { + mOnBlack = value; + doRedraw(); + } + + public boolean getOnBlack() { + return mOnBlack; + } + + private DisposeListener mDisposeListener = new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + mModel.removeTreeChangeListener(LayoutViewer.this); + mTransform.dispose(); + mInverse.dispose(); + if (mSelectedNode != null) { + mSelectedNode.viewNode.dereferenceImage(); + } + } + }; + + private Listener mResizeListener = new Listener() { + @Override + public void handleEvent(Event e) { + synchronized (this) { + setTransform(); + } + } + }; + + private MouseListener mMouseListener = new MouseListener() { + + @Override + public void mouseDoubleClick(MouseEvent e) { + if (mSelectedNode != null) { + HierarchyViewerDirector.getDirector() + .showCapture(getShell(), mSelectedNode.viewNode); + } + } + + @Override + public void mouseDown(MouseEvent e) { + boolean selectionChanged = false; + DrawableViewNode newSelection = null; + synchronized (LayoutViewer.this) { + if (mTree != null) { + float[] pt = { + e.x, e.y + }; + mInverse.transform(pt); + newSelection = + updateSelection(mTree, pt[0], pt[1], 0, 0, 0, 0, mTree.viewNode.width, + mTree.viewNode.height); + if (mSelectedNode != newSelection) { + selectionChanged = true; + } + } + } + if (selectionChanged) { + mModel.setSelection(newSelection); + } + } + + @Override + public void mouseUp(MouseEvent e) { + // pass + } + }; + + private DrawableViewNode updateSelection(DrawableViewNode node, float x, float y, int left, + int top, int clipX, int clipY, int clipWidth, int clipHeight) { + if (!node.treeDrawn) { + return null; + } + // Update the clip + int x1 = Math.max(left, clipX); + int x2 = Math.min(left + node.viewNode.width, clipX + clipWidth); + int y1 = Math.max(top, clipY); + int y2 = Math.min(top + node.viewNode.height, clipY + clipHeight); + clipX = x1; + clipY = y1; + clipWidth = x2 - x1; + clipHeight = y2 - y1; + if (x < clipX || x > clipX + clipWidth || y < clipY || y > clipY + clipHeight) { + return null; + } + final int N = node.children.size(); + for (int i = N - 1; i >= 0; i--) { + DrawableViewNode child = node.children.get(i); + DrawableViewNode ret = + updateSelection(child, x, y, + left + child.viewNode.left - node.viewNode.scrollX, top + + child.viewNode.top - node.viewNode.scrollY, clipX, clipY, + clipWidth, clipHeight); + if (ret != null) { + return ret; + } + } + return node; + } + + private PaintListener mPaintListener = new PaintListener() { + @Override + public void paintControl(PaintEvent e) { + synchronized (LayoutViewer.this) { + if (mOnBlack) { + e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); + } else { + e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); + } + e.gc.fillRectangle(0, 0, getBounds().width, getBounds().height); + if (mTree != null) { + e.gc.setLineWidth((int) Math.ceil(0.3 / mScale)); + e.gc.setTransform(mTransform); + if (mOnBlack) { + e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); + } else { + e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); + } + Rectangle parentClipping = e.gc.getClipping(); + e.gc.setClipping(0, 0, mTree.viewNode.width + (int) Math.ceil(0.3 / mScale), + mTree.viewNode.height + (int) Math.ceil(0.3 / mScale)); + paintRecursive(e.gc, mTree, 0, 0, true); + + if (mSelectedNode != null) { + e.gc.setClipping(parentClipping); + + // w00t, let's be nice and display the whole path in + // light red and the selected node in dark red. + ArrayList rightLeftDistances = new ArrayList(); + int left = 0; + int top = 0; + DrawableViewNode currentNode = mSelectedNode; + while (currentNode != mTree) { + left += currentNode.viewNode.left; + top += currentNode.viewNode.top; + currentNode = currentNode.parent; + left -= currentNode.viewNode.scrollX; + top -= currentNode.viewNode.scrollY; + rightLeftDistances.add(new Point(left, top)); + } + e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_DARK_RED)); + currentNode = mSelectedNode.parent; + final int N = rightLeftDistances.size(); + for (int i = 0; i < N; i++) { + e.gc.drawRectangle((int) (left - rightLeftDistances.get(i).x), + (int) (top - rightLeftDistances.get(i).y), + currentNode.viewNode.width, currentNode.viewNode.height); + currentNode = currentNode.parent; + } + + if (mShowExtras && mSelectedNode.viewNode.image != null) { + e.gc.drawImage(mSelectedNode.viewNode.image, left, top); + if (mOnBlack) { + e.gc.setForeground(Display.getDefault().getSystemColor( + SWT.COLOR_WHITE)); + } else { + e.gc.setForeground(Display.getDefault().getSystemColor( + SWT.COLOR_BLACK)); + } + paintRecursive(e.gc, mSelectedNode, left, top, true); + + } + + e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_RED)); + e.gc.setLineWidth((int) Math.ceil(2 / mScale)); + e.gc.drawRectangle(left, top, mSelectedNode.viewNode.width, + mSelectedNode.viewNode.height); + } + } + } + } + }; + + private void paintRecursive(GC gc, DrawableViewNode node, int left, int top, boolean root) { + if (!node.treeDrawn) { + return; + } + // Don't shift the root + if (!root) { + left += node.viewNode.left; + top += node.viewNode.top; + } + Rectangle parentClipping = gc.getClipping(); + int x1 = Math.max(parentClipping.x, left); + int x2 = + Math.min(parentClipping.x + parentClipping.width, left + node.viewNode.width + + (int) Math.ceil(0.3 / mScale)); + int y1 = Math.max(parentClipping.y, top); + int y2 = + Math.min(parentClipping.y + parentClipping.height, top + node.viewNode.height + + (int) Math.ceil(0.3 / mScale)); + + // Clipping is weird... You set it to -5 and it comes out 17 or + // something. + if (x2 <= x1 || y2 <= y1) { + return; + } + gc.setClipping(x1, y1, x2 - x1, y2 - y1); + final int N = node.children.size(); + for (int i = 0; i < N; i++) { + paintRecursive(gc, node.children.get(i), left - node.viewNode.scrollX, top + - node.viewNode.scrollY, false); + } + gc.setClipping(parentClipping); + if (!node.viewNode.willNotDraw) { + gc.drawRectangle(left, top, node.viewNode.width, node.viewNode.height); + } + + } + + private void doRedraw() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + redraw(); + } + }); + } + + private void setTransform() { + if (mTree != null) { + Rectangle bounds = getBounds(); + int leftRightPadding = bounds.width <= 30 ? 0 : 5; + int topBottomPadding = bounds.height <= 30 ? 0 : 5; + mScale = + Math.min(1.0 * (bounds.width - leftRightPadding * 2) / mTree.viewNode.width, 1.0 + * (bounds.height - topBottomPadding * 2) / mTree.viewNode.height); + int scaledWidth = (int) Math.ceil(mTree.viewNode.width * mScale); + int scaledHeight = (int) Math.ceil(mTree.viewNode.height * mScale); + + mTransform.identity(); + mInverse.identity(); + mTransform.translate((bounds.width - scaledWidth) / 2.0f, + (bounds.height - scaledHeight) / 2.0f); + mInverse.translate((bounds.width - scaledWidth) / 2.0f, + (bounds.height - scaledHeight) / 2.0f); + mTransform.scale((float) mScale, (float) mScale); + mInverse.scale((float) mScale, (float) mScale); + if (bounds.width != 0 && bounds.height != 0) { + mInverse.invert(); + } + } + } + + @Override + public void selectionChanged() { + synchronized (this) { + if (mSelectedNode != null) { + mSelectedNode.viewNode.dereferenceImage(); + } + mSelectedNode = mModel.getSelection(); + if (mSelectedNode != null) { + mSelectedNode.viewNode.referenceImage(); + } + } + doRedraw(); + } + + // Note the syncExec and then synchronized... It avoids deadlock + @Override + public void treeChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + synchronized (this) { + if (mSelectedNode != null) { + mSelectedNode.viewNode.dereferenceImage(); + } + mTree = mModel.getTree(); + mSelectedNode = mModel.getSelection(); + if (mSelectedNode != null) { + mSelectedNode.viewNode.referenceImage(); + } + setTransform(); + } + } + }); + doRedraw(); + } + + @Override + public void viewportChanged() { + // pass + } + + @Override + public void zoomChanged() { + // pass + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfect.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfect.java new file mode 100644 index 00000000..3973fed4 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfect.java @@ -0,0 +1,392 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.ui; + +import com.android.hierarchyviewerlib.models.PixelPerfectModel; +import com.android.hierarchyviewerlib.models.ViewNode; +import com.android.hierarchyviewerlib.models.PixelPerfectModel.IImageChangeListener; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.ScrolledComposite; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.KeyEvent; +import org.eclipse.swt.events.KeyListener; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.MouseListener; +import org.eclipse.swt.events.MouseMoveListener; +import org.eclipse.swt.events.PaintEvent; +import org.eclipse.swt.events.PaintListener; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.RGB; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; + +public class PixelPerfect extends ScrolledComposite implements IImageChangeListener { + private Canvas mCanvas; + + private PixelPerfectModel mModel; + + private Image mImage; + + private Color mCrosshairColor; + + private Color mMarginColor; + + private Color mBorderColor; + + private Color mPaddingColor; + + private int mWidth; + + private int mHeight; + + private Point mCrosshairLocation; + + private ViewNode mSelectedNode; + + private Image mOverlayImage; + + private double mOverlayTransparency; + + public PixelPerfect(Composite parent) { + super(parent, SWT.H_SCROLL | SWT.V_SCROLL); + mCanvas = new Canvas(this, SWT.NONE); + setContent(mCanvas); + setExpandHorizontal(true); + setExpandVertical(true); + mModel = PixelPerfectModel.getModel(); + mModel.addImageChangeListener(this); + + mCanvas.addPaintListener(mPaintListener); + mCanvas.addMouseListener(mMouseListener); + mCanvas.addMouseMoveListener(mMouseMoveListener); + mCanvas.addKeyListener(mKeyListener); + + addDisposeListener(mDisposeListener); + + mCrosshairColor = new Color(Display.getDefault(), new RGB(0, 255, 255)); + mBorderColor = new Color(Display.getDefault(), new RGB(255, 0, 0)); + mMarginColor = new Color(Display.getDefault(), new RGB(0, 255, 0)); + mPaddingColor = new Color(Display.getDefault(), new RGB(0, 0, 255)); + + imageLoaded(); + } + + private DisposeListener mDisposeListener = new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + mModel.removeImageChangeListener(PixelPerfect.this); + mCrosshairColor.dispose(); + mBorderColor.dispose(); + mPaddingColor.dispose(); + } + }; + + @Override + public boolean setFocus() { + return mCanvas.setFocus(); + } + + private MouseListener mMouseListener = new MouseListener() { + + @Override + public void mouseDoubleClick(MouseEvent e) { + // pass + } + + @Override + public void mouseDown(MouseEvent e) { + handleMouseEvent(e); + } + + @Override + public void mouseUp(MouseEvent e) { + handleMouseEvent(e); + } + + }; + + private MouseMoveListener mMouseMoveListener = new MouseMoveListener() { + @Override + public void mouseMove(MouseEvent e) { + if (e.stateMask != 0) { + handleMouseEvent(e); + } + } + }; + + private void handleMouseEvent(MouseEvent e) { + synchronized (PixelPerfect.this) { + if (mImage == null) { + return; + } + int leftOffset = mCanvas.getSize().x / 2 - mWidth / 2; + int topOffset = mCanvas.getSize().y / 2 - mHeight / 2; + e.x -= leftOffset; + e.y -= topOffset; + e.x = Math.max(e.x, 0); + e.x = Math.min(e.x, mWidth - 1); + e.y = Math.max(e.y, 0); + e.y = Math.min(e.y, mHeight - 1); + } + mModel.setCrosshairLocation(e.x, e.y); + } + + private KeyListener mKeyListener = new KeyListener() { + + @Override + public void keyPressed(KeyEvent e) { + boolean crosshairMoved = false; + synchronized (PixelPerfect.this) { + if (mImage != null) { + switch (e.keyCode) { + case SWT.ARROW_UP: + if (mCrosshairLocation.y != 0) { + mCrosshairLocation.y--; + crosshairMoved = true; + } + break; + case SWT.ARROW_DOWN: + if (mCrosshairLocation.y != mHeight - 1) { + mCrosshairLocation.y++; + crosshairMoved = true; + } + break; + case SWT.ARROW_LEFT: + if (mCrosshairLocation.x != 0) { + mCrosshairLocation.x--; + crosshairMoved = true; + } + break; + case SWT.ARROW_RIGHT: + if (mCrosshairLocation.x != mWidth - 1) { + mCrosshairLocation.x++; + crosshairMoved = true; + } + break; + } + } + } + if (crosshairMoved) { + mModel.setCrosshairLocation(mCrosshairLocation.x, mCrosshairLocation.y); + } + } + + @Override + public void keyReleased(KeyEvent e) { + // pass + } + + }; + + private PaintListener mPaintListener = new PaintListener() { + @Override + public void paintControl(PaintEvent e) { + synchronized (PixelPerfect.this) { + e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); + e.gc.fillRectangle(0, 0, mCanvas.getSize().x, mCanvas.getSize().y); + if (mImage != null) { + // Let's be cool and put it in the center... + int leftOffset = mCanvas.getSize().x / 2 - mWidth / 2; + int topOffset = mCanvas.getSize().y / 2 - mHeight / 2; + e.gc.drawImage(mImage, leftOffset, topOffset); + if (mOverlayImage != null) { + e.gc.setAlpha((int) (mOverlayTransparency * 255)); + int overlayTopOffset = + mCanvas.getSize().y / 2 + mHeight / 2 + - mOverlayImage.getBounds().height; + e.gc.drawImage(mOverlayImage, leftOffset, overlayTopOffset); + e.gc.setAlpha(255); + } + + if (mSelectedNode != null) { + // If the screen is in landscape mode, the + // coordinates are backwards. + int leftShift = 0; + int topShift = 0; + int nodeLeft = mSelectedNode.left; + int nodeTop = mSelectedNode.top; + int nodeWidth = mSelectedNode.width; + int nodeHeight = mSelectedNode.height; + int nodeMarginLeft = mSelectedNode.marginLeft; + int nodeMarginTop = mSelectedNode.marginTop; + int nodeMarginRight = mSelectedNode.marginRight; + int nodeMarginBottom = mSelectedNode.marginBottom; + int nodePadLeft = mSelectedNode.paddingLeft; + int nodePadTop = mSelectedNode.paddingTop; + int nodePadRight = mSelectedNode.paddingRight; + int nodePadBottom = mSelectedNode.paddingBottom; + ViewNode cur = mSelectedNode; + while (cur.parent != null) { + leftShift += cur.parent.left - cur.parent.scrollX; + topShift += cur.parent.top - cur.parent.scrollY; + cur = cur.parent; + } + + // Everything is sideways. + if (cur.width > cur.height) { + e.gc.setForeground(mPaddingColor); + e.gc.drawRectangle(leftOffset + mWidth - nodeTop - topShift - nodeHeight + + nodePadBottom, + topOffset + leftShift + nodeLeft + nodePadLeft, nodeHeight + - nodePadBottom - nodePadTop, nodeWidth - nodePadRight + - nodePadLeft); + e.gc.setForeground(mMarginColor); + e.gc.drawRectangle(leftOffset + mWidth - nodeTop - topShift - nodeHeight + - nodeMarginBottom, topOffset + leftShift + nodeLeft + - nodeMarginLeft, + nodeHeight + nodeMarginBottom + nodeMarginTop, nodeWidth + + nodeMarginRight + nodeMarginLeft); + e.gc.setForeground(mBorderColor); + e.gc.drawRectangle( + leftOffset + mWidth - nodeTop - topShift - nodeHeight, topOffset + + leftShift + nodeLeft, nodeHeight, nodeWidth); + } else { + e.gc.setForeground(mPaddingColor); + e.gc.drawRectangle(leftOffset + leftShift + nodeLeft + nodePadLeft, + topOffset + topShift + nodeTop + nodePadTop, nodeWidth + - nodePadRight - nodePadLeft, nodeHeight + - nodePadBottom - nodePadTop); + e.gc.setForeground(mMarginColor); + e.gc.drawRectangle(leftOffset + leftShift + nodeLeft - nodeMarginLeft, + topOffset + topShift + nodeTop - nodeMarginTop, nodeWidth + + nodeMarginRight + nodeMarginLeft, nodeHeight + + nodeMarginBottom + nodeMarginTop); + e.gc.setForeground(mBorderColor); + e.gc.drawRectangle(leftOffset + leftShift + nodeLeft, topOffset + + topShift + nodeTop, nodeWidth, nodeHeight); + } + } + if (mCrosshairLocation != null) { + e.gc.setForeground(mCrosshairColor); + e.gc.drawLine(leftOffset, topOffset + mCrosshairLocation.y, leftOffset + + mWidth - 1, topOffset + mCrosshairLocation.y); + e.gc.drawLine(leftOffset + mCrosshairLocation.x, topOffset, leftOffset + + mCrosshairLocation.x, topOffset + mHeight - 1); + } + } + } + } + }; + + private void doRedraw() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + mCanvas.redraw(); + } + }); + } + + private void loadImage() { + mImage = mModel.getImage(); + if (mImage != null) { + mWidth = mImage.getBounds().width; + mHeight = mImage.getBounds().height; + } else { + mWidth = 0; + mHeight = 0; + } + setMinSize(mWidth, mHeight); + } + + @Override + public void imageLoaded() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + synchronized (this) { + loadImage(); + mCrosshairLocation = mModel.getCrosshairLocation(); + mSelectedNode = mModel.getSelected(); + mOverlayImage = mModel.getOverlayImage(); + mOverlayTransparency = mModel.getOverlayTransparency(); + } + } + }); + doRedraw(); + } + + @Override + public void imageChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + synchronized (this) { + loadImage(); + } + } + }); + doRedraw(); + } + + @Override + public void crosshairMoved() { + synchronized (this) { + mCrosshairLocation = mModel.getCrosshairLocation(); + } + doRedraw(); + } + + @Override + public void selectionChanged() { + synchronized (this) { + mSelectedNode = mModel.getSelected(); + } + doRedraw(); + } + + // Note the syncExec and then synchronized... It avoids deadlock + @Override + public void treeChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + synchronized (this) { + mSelectedNode = mModel.getSelected(); + } + } + }); + doRedraw(); + } + + @Override + public void zoomChanged() { + // pass + } + + @Override + public void overlayChanged() { + synchronized (this) { + mOverlayImage = mModel.getOverlayImage(); + mOverlayTransparency = mModel.getOverlayTransparency(); + } + doRedraw(); + } + + @Override + public void overlayTransparencyChanged() { + synchronized (this) { + mOverlayTransparency = mModel.getOverlayTransparency(); + } + doRedraw(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectControls.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectControls.java new file mode 100644 index 00000000..70e523b7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectControls.java @@ -0,0 +1,296 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.ui; + +import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import com.android.hierarchyviewerlib.models.PixelPerfectModel; +import com.android.hierarchyviewerlib.models.PixelPerfectModel.IImageChangeListener; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.FormAttachment; +import org.eclipse.swt.layout.FormData; +import org.eclipse.swt.layout.FormLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Slider; + +public class PixelPerfectControls extends Composite implements IImageChangeListener { + + private Slider mOverlaySlider; + + private Slider mZoomSlider; + + private Slider mAutoRefreshSlider; + + public PixelPerfectControls(Composite parent) { + super(parent, SWT.NONE); + setLayout(new FormLayout()); + + Label overlayTransparencyRight = new Label(this, SWT.NONE); + overlayTransparencyRight.setText("100%"); + FormData overlayTransparencyRightData = new FormData(); + overlayTransparencyRightData.right = new FormAttachment(100, -2); + overlayTransparencyRightData.top = new FormAttachment(0, 2); + overlayTransparencyRight.setLayoutData(overlayTransparencyRightData); + + Label refreshRight = new Label(this, SWT.NONE); + refreshRight.setText("40s"); + FormData refreshRightData = new FormData(); + refreshRightData.right = new FormAttachment(100, -2); + refreshRightData.top = new FormAttachment(overlayTransparencyRight, 2); + refreshRightData.left = new FormAttachment(overlayTransparencyRight, 0, SWT.LEFT); + refreshRight.setLayoutData(refreshRightData); + + Label zoomRight = new Label(this, SWT.NONE); + zoomRight.setText("24x"); + FormData zoomRightData = new FormData(); + zoomRightData.right = new FormAttachment(100, -2); + zoomRightData.top = new FormAttachment(refreshRight, 2); + zoomRightData.left = new FormAttachment(overlayTransparencyRight, 0, SWT.LEFT); + zoomRight.setLayoutData(zoomRightData); + + Label overlayTransparency = new Label(this, SWT.NONE); + Label refresh = new Label(this, SWT.NONE); + + overlayTransparency.setText("Overlay:"); + FormData overlayTransparencyData = new FormData(); + overlayTransparencyData.left = new FormAttachment(0, 2); + overlayTransparencyData.top = new FormAttachment(0, 2); + overlayTransparencyData.right = new FormAttachment(refresh, 0, SWT.RIGHT); + overlayTransparency.setLayoutData(overlayTransparencyData); + + refresh.setText("Refresh Rate:"); + FormData refreshData = new FormData(); + refreshData.top = new FormAttachment(overlayTransparency, 2); + refreshData.left = new FormAttachment(0, 2); + refresh.setLayoutData(refreshData); + + Label zoom = new Label(this, SWT.NONE); + zoom.setText("Zoom:"); + FormData zoomData = new FormData(); + zoomData.right = new FormAttachment(refresh, 0, SWT.RIGHT); + zoomData.top = new FormAttachment(refresh, 2); + zoomData.left = new FormAttachment(0, 2); + zoom.setLayoutData(zoomData); + + Label overlayTransparencyLeft = new Label(this, SWT.RIGHT); + overlayTransparencyLeft.setText("0%"); + FormData overlayTransparencyLeftData = new FormData(); + overlayTransparencyLeftData.top = new FormAttachment(0, 2); + overlayTransparencyLeftData.left = new FormAttachment(overlayTransparency, 2); + overlayTransparencyLeft.setLayoutData(overlayTransparencyLeftData); + + Label refreshLeft = new Label(this, SWT.RIGHT); + refreshLeft.setText("1s"); + FormData refreshLeftData = new FormData(); + refreshLeftData.top = new FormAttachment(overlayTransparencyLeft, 2); + refreshLeftData.left = new FormAttachment(refresh, 2); + refreshLeft.setLayoutData(refreshLeftData); + + Label zoomLeft = new Label(this, SWT.RIGHT); + zoomLeft.setText("2x"); + FormData zoomLeftData = new FormData(); + zoomLeftData.top = new FormAttachment(refreshLeft, 2); + zoomLeftData.left = new FormAttachment(zoom, 2); + zoomLeft.setLayoutData(zoomLeftData); + + mOverlaySlider = new Slider(this, SWT.HORIZONTAL); + mOverlaySlider.setMinimum(0); + mOverlaySlider.setMaximum(101); + mOverlaySlider.setThumb(1); + mOverlaySlider.setSelection((int) Math.round(PixelPerfectModel.getModel() + .getOverlayTransparency() * 100)); + + Image overlayImage = PixelPerfectModel.getModel().getOverlayImage(); + mOverlaySlider.setEnabled(overlayImage != null); + FormData overlaySliderData = new FormData(); + overlaySliderData.right = new FormAttachment(overlayTransparencyRight, -4); + overlaySliderData.top = new FormAttachment(0, 2); + overlaySliderData.left = new FormAttachment(overlayTransparencyLeft, 4); + mOverlaySlider.setLayoutData(overlaySliderData); + + mOverlaySlider.addSelectionListener(overlaySliderSelectionListener); + + mAutoRefreshSlider = new Slider(this, SWT.HORIZONTAL); + mAutoRefreshSlider.setMinimum(1); + mAutoRefreshSlider.setMaximum(41); + mAutoRefreshSlider.setThumb(1); + mAutoRefreshSlider.setSelection(HierarchyViewerDirector.getDirector() + .getPixelPerfectAutoRefreshInverval()); + FormData refreshSliderData = new FormData(); + refreshSliderData.right = new FormAttachment(overlayTransparencyRight, -4); + refreshSliderData.top = new FormAttachment(overlayTransparencyRight, 2); + refreshSliderData.left = new FormAttachment(mOverlaySlider, 0, SWT.LEFT); + mAutoRefreshSlider.setLayoutData(refreshSliderData); + + mAutoRefreshSlider.addSelectionListener(mRefreshSliderSelectionListener); + + mZoomSlider = new Slider(this, SWT.HORIZONTAL); + mZoomSlider.setMinimum(2); + mZoomSlider.setMaximum(25); + mZoomSlider.setThumb(1); + mZoomSlider.setSelection(PixelPerfectModel.getModel().getZoom()); + FormData zoomSliderData = new FormData(); + zoomSliderData.right = new FormAttachment(overlayTransparencyRight, -4); + zoomSliderData.top = new FormAttachment(refreshRight, 2); + zoomSliderData.left = new FormAttachment(mOverlaySlider, 0, SWT.LEFT); + mZoomSlider.setLayoutData(zoomSliderData); + + mZoomSlider.addSelectionListener(mZoomSliderSelectionListener); + + addDisposeListener(mDisposeListener); + + PixelPerfectModel.getModel().addImageChangeListener(this); + } + + private DisposeListener mDisposeListener = new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + PixelPerfectModel.getModel().removeImageChangeListener(PixelPerfectControls.this); + } + }; + + private SelectionListener overlaySliderSelectionListener = new SelectionListener() { + private int oldValue; + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + // pass + } + + @Override + public void widgetSelected(SelectionEvent e) { + int newValue = mOverlaySlider.getSelection(); + if (oldValue != newValue) { + PixelPerfectModel.getModel().removeImageChangeListener(PixelPerfectControls.this); + PixelPerfectModel.getModel().setOverlayTransparency(newValue / 100.0); + PixelPerfectModel.getModel().addImageChangeListener(PixelPerfectControls.this); + oldValue = newValue; + } + } + }; + + private SelectionListener mRefreshSliderSelectionListener = new SelectionListener() { + private int oldValue; + + @Override + public void widgetDefaultSelected(final SelectionEvent e) { + // pass + } + + @Override + public void widgetSelected(SelectionEvent e) { + int newValue = mAutoRefreshSlider.getSelection(); + if (oldValue != newValue) { + HierarchyViewerDirector.getDirector().setPixelPerfectAutoRefreshInterval(newValue); + } + } + }; + + private SelectionListener mZoomSliderSelectionListener = new SelectionListener() { + private int oldValue; + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + // pass + } + + @Override + public void widgetSelected(SelectionEvent e) { + int newValue = mZoomSlider.getSelection(); + if (oldValue != newValue) { + PixelPerfectModel.getModel().removeImageChangeListener(PixelPerfectControls.this); + PixelPerfectModel.getModel().setZoom(newValue); + PixelPerfectModel.getModel().addImageChangeListener(PixelPerfectControls.this); + oldValue = newValue; + } + } + }; + + @Override + public void crosshairMoved() { + // pass + } + + @Override + public void treeChanged() { + // pass + } + + @Override + public void imageChanged() { + // pass + } + + @Override + public void imageLoaded() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + Image overlayImage = PixelPerfectModel.getModel().getOverlayImage(); + mOverlaySlider.setEnabled(overlayImage != null); + if (PixelPerfectModel.getModel().getImage() == null) { + } else { + mZoomSlider.setSelection(PixelPerfectModel.getModel().getZoom()); + } + } + }); + } + + @Override + public void overlayChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + Image overlayImage = PixelPerfectModel.getModel().getOverlayImage(); + mOverlaySlider.setEnabled(overlayImage != null); + } + }); + } + + @Override + public void overlayTransparencyChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + mOverlaySlider.setSelection((int) (PixelPerfectModel.getModel() + .getOverlayTransparency() * 100)); + } + }); + } + + @Override + public void selectionChanged() { + // pass + } + + @Override + public void zoomChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + mZoomSlider.setSelection(PixelPerfectModel.getModel().getZoom()); + } + }); + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectLoupe.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectLoupe.java new file mode 100644 index 00000000..ee7cf483 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectLoupe.java @@ -0,0 +1,391 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.ui; + +import com.android.hierarchyviewerlib.models.PixelPerfectModel; +import com.android.hierarchyviewerlib.models.PixelPerfectModel.IImageChangeListener; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.KeyEvent; +import org.eclipse.swt.events.KeyListener; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.MouseListener; +import org.eclipse.swt.events.MouseWheelListener; +import org.eclipse.swt.events.PaintEvent; +import org.eclipse.swt.events.PaintListener; +import org.eclipse.swt.graphics.Color; +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.graphics.Rectangle; +import org.eclipse.swt.graphics.Transform; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; + +public class PixelPerfectLoupe extends Canvas implements IImageChangeListener { + private PixelPerfectModel mModel; + + private Image mImage; + + private Image mGrid; + + private Color mCrosshairColor; + + private int mWidth; + + private int mHeight; + + private Point mCrosshairLocation; + + private int mZoom; + + private Transform mTransform; + + private int mCanvasWidth; + + private int mCanvasHeight; + + private Image mOverlayImage; + + private double mOverlayTransparency; + + private boolean mShowOverlay = false; + + public PixelPerfectLoupe(Composite parent) { + super(parent, SWT.NONE); + mModel = PixelPerfectModel.getModel(); + mModel.addImageChangeListener(this); + + addPaintListener(mPaintListener); + addMouseListener(mMouseListener); + addMouseWheelListener(mMouseWheelListener); + addDisposeListener(mDisposeListener); + addKeyListener(mKeyListener); + + mCrosshairColor = new Color(Display.getDefault(), new RGB(255, 94, 254)); + + mTransform = new Transform(Display.getDefault()); + + imageLoaded(); + } + + public void setShowOverlay(boolean value) { + synchronized (this) { + mShowOverlay = value; + } + doRedraw(); + } + + private DisposeListener mDisposeListener = new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + mModel.removeImageChangeListener(PixelPerfectLoupe.this); + mCrosshairColor.dispose(); + mTransform.dispose(); + if (mGrid != null) { + mGrid.dispose(); + } + } + }; + + private MouseListener mMouseListener = new MouseListener() { + + @Override + public void mouseDoubleClick(MouseEvent e) { + // pass + } + + @Override + public void mouseDown(MouseEvent e) { + handleMouseEvent(e); + } + + @Override + public void mouseUp(MouseEvent e) { + // + } + + }; + + private MouseWheelListener mMouseWheelListener = new MouseWheelListener() { + @Override + public void mouseScrolled(MouseEvent e) { + int newZoom = -1; + synchronized (PixelPerfectLoupe.this) { + if (mImage != null && mCrosshairLocation != null) { + if (e.count > 0) { + newZoom = mZoom + 1; + } else { + newZoom = mZoom - 1; + } + } + } + if (newZoom != -1) { + mModel.setZoom(newZoom); + } + } + }; + + private void handleMouseEvent(MouseEvent e) { + int newX = -1; + int newY = -1; + synchronized (PixelPerfectLoupe.this) { + if (mImage == null) { + return; + } + int zoomedX = -mCrosshairLocation.x * mZoom - mZoom / 2 + getBounds().width / 2; + int zoomedY = -mCrosshairLocation.y * mZoom - mZoom / 2 + getBounds().height / 2; + int x = (e.x - zoomedX) / mZoom; + int y = (e.y - zoomedY) / mZoom; + if (x >= 0 && x < mWidth && y >= 0 && y < mHeight) { + newX = x; + newY = y; + } + } + if (newX != -1) { + mModel.setCrosshairLocation(newX, newY); + } + } + + private KeyListener mKeyListener = new KeyListener() { + + @Override + public void keyPressed(KeyEvent e) { + boolean crosshairMoved = false; + synchronized (PixelPerfectLoupe.this) { + if (mImage != null) { + switch (e.keyCode) { + case SWT.ARROW_UP: + if (mCrosshairLocation.y != 0) { + mCrosshairLocation.y--; + crosshairMoved = true; + } + break; + case SWT.ARROW_DOWN: + if (mCrosshairLocation.y != mHeight - 1) { + mCrosshairLocation.y++; + crosshairMoved = true; + } + break; + case SWT.ARROW_LEFT: + if (mCrosshairLocation.x != 0) { + mCrosshairLocation.x--; + crosshairMoved = true; + } + break; + case SWT.ARROW_RIGHT: + if (mCrosshairLocation.x != mWidth - 1) { + mCrosshairLocation.x++; + crosshairMoved = true; + } + break; + } + } + } + if (crosshairMoved) { + mModel.setCrosshairLocation(mCrosshairLocation.x, mCrosshairLocation.y); + } + } + + @Override + public void keyReleased(KeyEvent e) { + // pass + } + + }; + + private PaintListener mPaintListener = new PaintListener() { + @Override + public void paintControl(PaintEvent e) { + synchronized (PixelPerfectLoupe.this) { + e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); + e.gc.fillRectangle(0, 0, getSize().x, getSize().y); + if (mImage != null && mCrosshairLocation != null) { + int zoomedX = -mCrosshairLocation.x * mZoom - mZoom / 2 + getBounds().width / 2; + int zoomedY = -mCrosshairLocation.y * mZoom - mZoom / 2 + getBounds().height / 2; + mTransform.translate(zoomedX, zoomedY); + mTransform.scale(mZoom, mZoom); + e.gc.setInterpolation(SWT.NONE); + e.gc.setTransform(mTransform); + e.gc.drawImage(mImage, 0, 0); + if (mShowOverlay && mOverlayImage != null) { + e.gc.setAlpha((int) (mOverlayTransparency * 255)); + e.gc.drawImage(mOverlayImage, 0, mHeight - mOverlayImage.getBounds().height); + e.gc.setAlpha(255); + } + + mTransform.identity(); + e.gc.setTransform(mTransform); + + // If the size of the canvas has changed, we need to make + // another grid. + if (mGrid != null + && (mCanvasWidth != getBounds().width || mCanvasHeight != getBounds().height)) { + mGrid.dispose(); + mGrid = null; + } + mCanvasWidth = getBounds().width; + mCanvasHeight = getBounds().height; + if (mGrid == null) { + // Make a transparent image; + ImageData imageData = + new ImageData(mCanvasWidth + mZoom + 1, mCanvasHeight + mZoom + 1, 1, + new PaletteData(new RGB[] { + new RGB(0, 0, 0) + })); + imageData.transparentPixel = 0; + + // Draw the grid. + mGrid = new Image(Display.getDefault(), imageData); + GC gc = new GC(mGrid); + gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); + for (int x = 0; x <= mCanvasWidth + mZoom; x += mZoom) { + gc.drawLine(x, 0, x, mCanvasHeight + mZoom); + } + for (int y = 0; y <= mCanvasHeight + mZoom; y += mZoom) { + gc.drawLine(0, y, mCanvasWidth + mZoom, y); + } + gc.dispose(); + } + + e.gc.setClipping(new Rectangle(zoomedX, zoomedY, mWidth * mZoom + 1, mHeight + * mZoom + 1)); + e.gc.setAlpha(76); + e.gc.drawImage(mGrid, (mCanvasWidth / 2 - mZoom / 2) % mZoom - mZoom, + (mCanvasHeight / 2 - mZoom / 2) % mZoom - mZoom); + e.gc.setAlpha(255); + + e.gc.setForeground(mCrosshairColor); + e.gc.drawLine(0, mCanvasHeight / 2, mCanvasWidth - 1, mCanvasHeight / 2); + e.gc.drawLine(mCanvasWidth / 2, 0, mCanvasWidth / 2, mCanvasHeight - 1); + } + } + } + }; + + private void doRedraw() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + redraw(); + } + }); + } + + private void loadImage() { + mImage = mModel.getImage(); + if (mImage != null) { + mWidth = mImage.getBounds().width; + mHeight = mImage.getBounds().height; + } else { + mWidth = 0; + mHeight = 0; + } + } + + // Note the syncExec and then synchronized... It avoids deadlock + @Override + public void imageLoaded() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + synchronized (this) { + loadImage(); + mCrosshairLocation = mModel.getCrosshairLocation(); + mZoom = mModel.getZoom(); + mOverlayImage = mModel.getOverlayImage(); + mOverlayTransparency = mModel.getOverlayTransparency(); + } + } + }); + doRedraw(); + } + + @Override + public void imageChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + synchronized (this) { + loadImage(); + } + } + }); + doRedraw(); + } + + @Override + public void crosshairMoved() { + synchronized (this) { + mCrosshairLocation = mModel.getCrosshairLocation(); + } + doRedraw(); + } + + @Override + public void selectionChanged() { + // pass + } + + @Override + public void treeChanged() { + // pass + } + + @Override + public void zoomChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + synchronized (this) { + if (mGrid != null) { + // To notify that the zoom level has changed, we get rid + // of the + // grid. + mGrid.dispose(); + mGrid = null; + } + mZoom = mModel.getZoom(); + } + } + }); + doRedraw(); + } + + @Override + public void overlayChanged() { + synchronized (this) { + mOverlayImage = mModel.getOverlayImage(); + mOverlayTransparency = mModel.getOverlayTransparency(); + } + doRedraw(); + } + + @Override + public void overlayTransparencyChanged() { + synchronized (this) { + mOverlayTransparency = mModel.getOverlayTransparency(); + } + doRedraw(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectPixelPanel.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectPixelPanel.java new file mode 100644 index 00000000..01088edf --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectPixelPanel.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.ui; + +import com.android.hierarchyviewerlib.models.PixelPerfectModel; +import com.android.hierarchyviewerlib.models.PixelPerfectModel.IImageChangeListener; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.PaintEvent; +import org.eclipse.swt.events.PaintListener; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.RGB; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; + +public class PixelPerfectPixelPanel extends Canvas implements IImageChangeListener { + private PixelPerfectModel mModel; + + private Image mImage; + + private Image mOverlayImage; + + private Point mCrosshairLocation; + + public static final int PREFERRED_WIDTH = 180; + + public static final int PREFERRED_HEIGHT = 52; + + public PixelPerfectPixelPanel(Composite parent) { + super(parent, SWT.NONE); + mModel = PixelPerfectModel.getModel(); + mModel.addImageChangeListener(this); + + addPaintListener(mPaintListener); + addDisposeListener(mDisposeListener); + + imageLoaded(); + } + + @Override + public Point computeSize(int wHint, int hHint, boolean changed) { + int height = PREFERRED_HEIGHT; + int width = (wHint == SWT.DEFAULT) ? PREFERRED_WIDTH : wHint; + return new Point(width, height); + } + + private DisposeListener mDisposeListener = new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + mModel.removeImageChangeListener(PixelPerfectPixelPanel.this); + } + }; + + private PaintListener mPaintListener = new PaintListener() { + @Override + public void paintControl(PaintEvent e) { + synchronized (PixelPerfectPixelPanel.this) { + e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); + e.gc.fillRectangle(0, 0, getBounds().width, getBounds().height); + if (mImage != null) { + RGB pixel = + mImage.getImageData().palette.getRGB(mImage.getImageData().getPixel( + mCrosshairLocation.x, mCrosshairLocation.y)); + Color rgbColor = new Color(Display.getDefault(), pixel); + e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); + e.gc.setBackground(rgbColor); + e.gc.drawRectangle(4, 4, 60, 30); + e.gc.fillRectangle(5, 5, 59, 29); + rgbColor.dispose(); + e.gc.drawText("#" + + Integer + .toHexString( + (1 << 24) + (pixel.red << 16) + (pixel.green << 8) + + pixel.blue).substring(1), 4, 35, true); + e.gc.drawText("R:", 80, 4, true); + e.gc.drawText("G:", 80, 20, true); + e.gc.drawText("B:", 80, 35, true); + e.gc.drawText(Integer.toString(pixel.red), 97, 4, true); + e.gc.drawText(Integer.toString(pixel.green), 97, 20, true); + e.gc.drawText(Integer.toString(pixel.blue), 97, 35, true); + e.gc.drawText("X:", 132, 4, true); + e.gc.drawText("Y:", 132, 20, true); + e.gc.drawText(Integer.toString(mCrosshairLocation.x) + " px", 149, 4, true); + e.gc.drawText(Integer.toString(mCrosshairLocation.y) + " px", 149, 20, true); + + if (mOverlayImage != null) { + int xInOverlay = mCrosshairLocation.x; + int yInOverlay = + mCrosshairLocation.y + - (mImage.getBounds().height - mOverlayImage.getBounds().height); + if (xInOverlay >= 0 && yInOverlay >= 0 + && xInOverlay < mOverlayImage.getBounds().width + && yInOverlay < mOverlayImage.getBounds().height) { + pixel = + mOverlayImage.getImageData().palette.getRGB(mOverlayImage + .getImageData().getPixel(xInOverlay, yInOverlay)); + rgbColor = new Color(Display.getDefault(), pixel); + e.gc + .setForeground(Display.getDefault().getSystemColor( + SWT.COLOR_WHITE)); + e.gc.setBackground(rgbColor); + e.gc.drawRectangle(204, 4, 60, 30); + e.gc.fillRectangle(205, 5, 59, 29); + rgbColor.dispose(); + e.gc.drawText("#" + + Integer.toHexString( + (1 << 24) + (pixel.red << 16) + (pixel.green << 8) + + pixel.blue).substring(1), 204, 35, true); + e.gc.drawText("R:", 280, 4, true); + e.gc.drawText("G:", 280, 20, true); + e.gc.drawText("B:", 280, 35, true); + e.gc.drawText(Integer.toString(pixel.red), 297, 4, true); + e.gc.drawText(Integer.toString(pixel.green), 297, 20, true); + e.gc.drawText(Integer.toString(pixel.blue), 297, 35, true); + } + } + } + } + } + }; + + private void doRedraw() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + redraw(); + } + }); + } + + @Override + public void crosshairMoved() { + synchronized (this) { + mCrosshairLocation = mModel.getCrosshairLocation(); + } + doRedraw(); + } + + @Override + public void imageChanged() { + synchronized (this) { + mImage = mModel.getImage(); + } + doRedraw(); + } + + @Override + public void imageLoaded() { + synchronized (this) { + mImage = mModel.getImage(); + mCrosshairLocation = mModel.getCrosshairLocation(); + mOverlayImage = mModel.getOverlayImage(); + } + doRedraw(); + } + + @Override + public void overlayChanged() { + synchronized (this) { + mOverlayImage = mModel.getOverlayImage(); + } + doRedraw(); + } + + @Override + public void overlayTransparencyChanged() { + // pass + } + + @Override + public void selectionChanged() { + // pass + } + + @Override + public void treeChanged() { + // pass + } + + @Override + public void zoomChanged() { + // pass + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectTree.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectTree.java new file mode 100644 index 00000000..769fc013 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectTree.java @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.ui; + +import com.android.ddmuilib.ImageLoader; +import com.android.hierarchyviewerlib.models.PixelPerfectModel; +import com.android.hierarchyviewerlib.models.ViewNode; +import com.android.hierarchyviewerlib.models.PixelPerfectModel.IImageChangeListener; + +import org.eclipse.jface.viewers.ILabelProvider; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.ITreeContentProvider; +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.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Tree; + +import java.util.List; + +public class PixelPerfectTree extends Composite implements IImageChangeListener, SelectionListener { + + private TreeViewer mTreeViewer; + + private Tree mTree; + + private PixelPerfectModel mModel; + + private Image mFolderImage; + + private Image mFileImage; + + private class ContentProvider implements ITreeContentProvider, ILabelProvider { + @Override + public Object[] getChildren(Object element) { + if (element instanceof ViewNode) { + List children = ((ViewNode) element).children; + return children.toArray(new ViewNode[children.size()]); + } + return null; + } + + @Override + public Object getParent(Object element) { + if (element instanceof ViewNode) { + return ((ViewNode) element).parent; + } + return null; + } + + @Override + public boolean hasChildren(Object element) { + if (element instanceof ViewNode) { + return ((ViewNode) element).children.size() != 0; + } + return false; + } + + @Override + public Object[] getElements(Object element) { + if (element instanceof PixelPerfectModel) { + ViewNode viewNode = ((PixelPerfectModel) element).getViewNode(); + if (viewNode == null) { + return new Object[0]; + } + return new Object[] { + viewNode + }; + } + return new Object[0]; + } + + @Override + public void dispose() { + // pass + } + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } + + @Override + public Image getImage(Object element) { + if (element instanceof ViewNode) { + if (hasChildren(element)) { + return mFolderImage; + } + return mFileImage; + } + return null; + } + + @Override + public String getText(Object element) { + if (element instanceof ViewNode) { + return ((ViewNode) element).name; + } + return null; + } + + @Override + public void addListener(ILabelProviderListener listener) { + // pass + } + + @Override + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + @Override + public void removeListener(ILabelProviderListener listener) { + // pass + } + } + + public PixelPerfectTree(Composite parent) { + super(parent, SWT.NONE); + setLayout(new FillLayout()); + mTreeViewer = new TreeViewer(this, SWT.SINGLE); + mTreeViewer.setAutoExpandLevel(TreeViewer.ALL_LEVELS); + + mTree = mTreeViewer.getTree(); + mTree.addSelectionListener(this); + + loadResources(); + + addDisposeListener(mDisposeListener); + + mModel = PixelPerfectModel.getModel(); + ContentProvider contentProvider = new ContentProvider(); + mTreeViewer.setContentProvider(contentProvider); + mTreeViewer.setLabelProvider(contentProvider); + mTreeViewer.setInput(mModel); + mModel.addImageChangeListener(this); + + } + + private void loadResources() { + ImageLoader loader = ImageLoader.getDdmUiLibLoader(); + mFileImage = loader.loadImage("file.png", Display.getDefault()); + mFolderImage = loader.loadImage("folder.png", Display.getDefault()); + } + + private DisposeListener mDisposeListener = new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + mModel.removeImageChangeListener(PixelPerfectTree.this); + } + }; + + @Override + public boolean setFocus() { + return mTree.setFocus(); + } + + @Override + public void imageLoaded() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + mTreeViewer.refresh(); + mTreeViewer.expandAll(); + } + }); + } + + @Override + public void imageChanged() { + // pass + } + + @Override + public void crosshairMoved() { + // pass + } + + @Override + public void selectionChanged() { + // pass + } + + @Override + public void treeChanged() { + imageLoaded(); + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + // pass + } + + @Override + public void widgetSelected(SelectionEvent e) { + // To combat phantom selection... + if (((TreeSelection) mTreeViewer.getSelection()).isEmpty()) { + mModel.setSelected(null); + } else { + mModel.setSelected((ViewNode) e.item.getData()); + } + } + + @Override + public void zoomChanged() { + // pass + } + + @Override + public void overlayChanged() { + // pass + } + + @Override + public void overlayTransparencyChanged() { + // pass + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PropertyViewer.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PropertyViewer.java new file mode 100644 index 00000000..bf0b6541 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PropertyViewer.java @@ -0,0 +1,420 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.ui; + +import com.android.annotations.NonNull; +import com.android.annotations.VisibleForTesting; +import com.android.ddmuilib.ImageLoader; +import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import com.android.hierarchyviewerlib.device.IHvDevice; +import com.android.hierarchyviewerlib.models.TreeViewModel; +import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener; +import com.android.hierarchyviewerlib.models.ViewNode; +import com.android.hierarchyviewerlib.models.ViewNode.Property; +import com.android.hierarchyviewerlib.ui.DevicePropertyEditingSupport.PropertyType; +import com.android.hierarchyviewerlib.ui.util.DrawableViewNode; +import com.android.hierarchyviewerlib.ui.util.TreeColumnResizer; + +import org.eclipse.jface.viewers.CellEditor; +import org.eclipse.jface.viewers.ColumnViewer; +import org.eclipse.jface.viewers.ComboBoxCellEditor; +import org.eclipse.jface.viewers.EditingSupport; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.TextCellEditor; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.TreeViewerColumn; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ControlAdapter; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.ControlListener; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeColumn; + +import java.util.ArrayList; +import java.util.Collection; + +public class PropertyViewer extends Composite implements ITreeChangeListener { + private static final String PROPERTY_GET_PREFIX = "get"; + private static final String EMPTY_ARGUMENT_LIST = "()"; + + private TreeViewModel mModel; + + private TreeViewer mTreeViewer; + private Tree mTree; + private TreeViewerColumn mValueColumn; + private PropertyValueEditingSupport mPropertyValueEditingSupport; + + private Image mImage; + + private DrawableViewNode mSelectedNode; + + @VisibleForTesting + static @NonNull String parseColumnTextName(@NonNull String name) { + int start = 0; + int end = name.length(); + + int index = name.indexOf(':'); + if (index != -1) { + start = index + 1; + } + + index = name.indexOf(PROPERTY_GET_PREFIX); + int prefixOffset = start + PROPERTY_GET_PREFIX.length(); + if (index == start && prefixOffset < end + && Character.isUpperCase(name.charAt(prefixOffset))) { + start = prefixOffset; + } + + if (name.endsWith(EMPTY_ARGUMENT_LIST)) { + end -= EMPTY_ARGUMENT_LIST.length(); + } + + if (start < end && !Character.isLowerCase(name.charAt(start))) { + return Character.toLowerCase(name.charAt(start)) + + (start + 1 < end ? name.substring(start + 1, end) : ""); + } + + return name.substring(start, end); + } + + private class ContentProvider implements ITreeContentProvider, ITableLabelProvider { + + @Override + public Object[] getChildren(Object parentElement) { + synchronized (PropertyViewer.this) { + if (mSelectedNode != null && parentElement instanceof String) { + String category = (String) parentElement; + ArrayList returnValue = new ArrayList(); + for (Property property : mSelectedNode.viewNode.properties) { + if (category.equals(ViewNode.MISCELLANIOUS)) { + if (property.name.indexOf(':') == -1) { + returnValue.add(property); + } + } else { + if (property.name.startsWith(((String) parentElement) + ":")) { + returnValue.add(property); + } + } + } + return returnValue.toArray(new Property[returnValue.size()]); + } + return new Object[0]; + } + } + + @Override + public Object getParent(Object element) { + synchronized (PropertyViewer.this) { + if (mSelectedNode != null && element instanceof Property) { + if (mSelectedNode.viewNode.categories.size() == 0) { + return null; + } + String name = ((Property) element).name; + int index = name.indexOf(':'); + if (index == -1) { + return ViewNode.MISCELLANIOUS; + } + return name.substring(0, index); + } + return null; + } + } + + @Override + public boolean hasChildren(Object element) { + synchronized (PropertyViewer.this) { + if (mSelectedNode != null && element instanceof String) { + String category = (String) element; + for (String name : mSelectedNode.viewNode.namedProperties.keySet()) { + if (category.equals(ViewNode.MISCELLANIOUS)) { + if (name.indexOf(':') == -1) { + return true; + } + } else { + if (name.startsWith(((String) element) + ":")) { + return true; + } + } + } + } + return false; + } + } + + @Override + public Object[] getElements(Object inputElement) { + synchronized (PropertyViewer.this) { + if (mSelectedNode != null && inputElement instanceof TreeViewModel) { + if (mSelectedNode.viewNode.categories.size() == 0) { + return mSelectedNode.viewNode.properties + .toArray(new Property[mSelectedNode.viewNode.properties.size()]); + } else { + return mSelectedNode.viewNode.categories + .toArray(new String[mSelectedNode.viewNode.categories.size()]); + } + } + return new Object[0]; + } + } + + @Override + public void dispose() { + // pass + } + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } + + @Override + public Image getColumnImage(Object element, int column) { + if (mSelectedNode == null) { + return null; + } + if (column == 1 && mPropertyValueEditingSupport.canEdit(element)) { + return mImage; + } + + return null; + } + + @Override + public String getColumnText(Object element, int column) { + synchronized (PropertyViewer.this) { + if (mSelectedNode != null) { + if (element instanceof String && column == 0) { + String category = (String) element; + return Character.toUpperCase(category.charAt(0)) + category.substring(1); + } else if (element instanceof Property) { + if (column == 0) { + return parseColumnTextName(((Property) element).name); + } else if (column == 1) { + return ((Property) element).value; + } + } + } + return ""; + } + } + + @Override + public void addListener(ILabelProviderListener listener) { + // pass + } + + @Override + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + @Override + public void removeListener(ILabelProviderListener listener) { + // pass + } + } + + private class PropertyValueEditingSupport extends EditingSupport { + private DevicePropertyEditingSupport mDevicePropertyEditingSupport = + new DevicePropertyEditingSupport(); + + public PropertyValueEditingSupport(ColumnViewer viewer) { + super(viewer); + } + + @Override + protected boolean canEdit(Object element) { + if (mSelectedNode == null) { + return false; + } + + return element instanceof Property + && mSelectedNode.viewNode.window.getHvDevice().isViewUpdateEnabled() + && mDevicePropertyEditingSupport.canEdit((Property) element); + } + + @Override + protected CellEditor getCellEditor(Object element) { + Property p = (Property) element; + PropertyType type = mDevicePropertyEditingSupport.getPropertyType(p); + Composite parent = (Composite) getViewer().getControl(); + + switch (type) { + case INTEGER: + case INTEGER_OR_CONSTANT: + return new TextCellEditor(parent); + case ENUM: + String[] items = mDevicePropertyEditingSupport.getPropertyRange(p); + return new ComboBoxCellEditor(parent, items, SWT.READ_ONLY); + } + + return null; + } + + @Override + protected Object getValue(Object element) { + Property p = (Property) element; + PropertyType type = mDevicePropertyEditingSupport.getPropertyType(p); + + if (type == PropertyType.ENUM) { + // for enums, return the index of the current value in the list of possible values + String[] items = mDevicePropertyEditingSupport.getPropertyRange(p); + return Integer.valueOf(indexOf(p.value, items)); + } + + return ((Property) element).value; + } + + private int indexOf(String item, String[] items) { + for (int i = 0; i < items.length; i++) { + if (items[i].equals(item)) { + return i; + } + } + + return -1; + } + + @Override + protected void setValue(Object element, Object newValue) { + Property p = (Property) element; + IHvDevice device = mSelectedNode.viewNode.window.getHvDevice(); + Collection properties = mSelectedNode.viewNode.namedProperties.values(); + if (mDevicePropertyEditingSupport.setValue(properties, p, newValue, + mSelectedNode.viewNode, device)) { + doRefresh(); + } + } + } + + public PropertyViewer(Composite parent) { + super(parent, SWT.NONE); + setLayout(new FillLayout()); + mTreeViewer = new TreeViewer(this, SWT.NONE); + + mTree = mTreeViewer.getTree(); + mTree.setLinesVisible(true); + mTree.setHeaderVisible(true); + + TreeColumn propertyColumn = new TreeColumn(mTree, SWT.NONE); + propertyColumn.setText("Property"); + TreeColumn valueColumn = new TreeColumn(mTree, SWT.NONE); + valueColumn.setText("Value"); + + mValueColumn = new TreeViewerColumn(mTreeViewer, valueColumn); + mPropertyValueEditingSupport = new PropertyValueEditingSupport(mTreeViewer); + mValueColumn.setEditingSupport(mPropertyValueEditingSupport); + + mModel = TreeViewModel.getModel(); + ContentProvider contentProvider = new ContentProvider(); + mTreeViewer.setContentProvider(contentProvider); + mTreeViewer.setLabelProvider(contentProvider); + mTreeViewer.setInput(mModel); + mModel.addTreeChangeListener(this); + + addDisposeListener(mDisposeListener); + + @SuppressWarnings("unused") + TreeColumnResizer resizer = new TreeColumnResizer(this, propertyColumn, valueColumn); + + addControlListener(mControlListener); + + ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); + mImage = imageLoader.loadImage("picker.png", Display.getDefault()); //$NON-NLS-1$ + + treeChanged(); + } + + private DisposeListener mDisposeListener = new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + mModel.removeTreeChangeListener(PropertyViewer.this); + } + }; + + // If the window gets too small, hide the data, otherwise SWT throws an + // ERROR. + + private ControlListener mControlListener = new ControlAdapter() { + private boolean noInput = false; + + private boolean noHeader = false; + + @Override + public void controlResized(ControlEvent e) { + if (getBounds().height <= 20) { + mTree.setHeaderVisible(false); + noHeader = true; + } else if (noHeader) { + mTree.setHeaderVisible(true); + noHeader = false; + } + if (getBounds().height <= 38) { + mTreeViewer.setInput(null); + noInput = true; + } else if (noInput) { + mTreeViewer.setInput(mModel); + noInput = false; + } + } + }; + + @Override + public void selectionChanged() { + synchronized (this) { + mSelectedNode = mModel.getSelection(); + } + doRefresh(); + } + + @Override + public void treeChanged() { + synchronized (this) { + mSelectedNode = mModel.getSelection(); + } + doRefresh(); + } + + @Override + public void viewportChanged() { + // pass + } + + @Override + public void zoomChanged() { + // pass + } + + private void doRefresh() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + mTreeViewer.refresh(); + } + }); + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeView.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeView.java new file mode 100644 index 00000000..c3798cbc --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeView.java @@ -0,0 +1,1096 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.ui; + +import com.android.ddmuilib.ImageLoader; +import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import com.android.hierarchyviewerlib.models.TreeViewModel; +import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener; +import com.android.hierarchyviewerlib.models.ViewNode.ProfileRating; +import com.android.hierarchyviewerlib.ui.util.DrawableViewNode; +import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Point; +import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Rectangle; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.KeyEvent; +import org.eclipse.swt.events.KeyListener; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.MouseListener; +import org.eclipse.swt.events.MouseMoveListener; +import org.eclipse.swt.events.MouseWheelListener; +import org.eclipse.swt.events.PaintEvent; +import org.eclipse.swt.events.PaintListener; +import org.eclipse.swt.events.TraverseEvent; +import org.eclipse.swt.events.TraverseListener; +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.Path; +import org.eclipse.swt.graphics.RGB; +import org.eclipse.swt.graphics.Transform; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Listener; + +import java.text.DecimalFormat; + +public class TreeView extends Canvas implements ITreeChangeListener { + + private TreeViewModel mModel; + + private DrawableViewNode mTree; + + private DrawableViewNode mSelectedNode; + + private Rectangle mViewport; + + private Transform mTransform; + + private Transform mInverse; + + private double mZoom; + + private Point mLastPoint; + + private boolean mAlreadySelectedOnMouseDown; + + private boolean mDoubleClicked; + + private boolean mNodeMoved; + + private DrawableViewNode mDraggedNode; + + public static final int LINE_PADDING = 10; + + public static final float BEZIER_FRACTION = 0.35f; + + private static Image sRedImage; + + private static Image sYellowImage; + + private static Image sGreenImage; + + private static Image sNotSelectedImage; + + private static Image sSelectedImage; + + private static Image sFilteredImage; + + private static Image sFilteredSelectedImage; + + private static Font sSystemFont; + + private Color mBoxColor; + + private Color mTextBackgroundColor; + + private Rectangle mSelectedRectangleLocation; + + private Point mButtonCenter; + + private static final int BUTTON_SIZE = 13; + + private Image mScaledSelectedImage; + + private boolean mButtonClicked; + + private DrawableViewNode mLastDrawnSelectedViewNode; + + // The profile-image box needs to be moved to, + // so add some dragging leeway. + private static final int DRAG_LEEWAY = 220; + + // Profile-image box constants + private static final int RECT_WIDTH = 190; + + private static final int RECT_HEIGHT = 224; + + private static final int BUTTON_RIGHT_OFFSET = 5; + + private static final int BUTTON_TOP_OFFSET = 5; + + private static final int IMAGE_WIDTH = 125; + + private static final int IMAGE_HEIGHT = 120; + + private static final int IMAGE_OFFSET = 6; + + private static final int IMAGE_ROUNDING = 8; + + private static final int RECTANGLE_SIZE = 5; + + private static final int TEXT_SIDE_OFFSET = 8; + + private static final int TEXT_TOP_OFFSET = 4; + + private static final int TEXT_SPACING = 2; + + private static final int TEXT_ROUNDING = 20; + + public TreeView(Composite parent) { + super(parent, SWT.NONE); + + mModel = TreeViewModel.getModel(); + mModel.addTreeChangeListener(this); + + addPaintListener(mPaintListener); + addMouseListener(mMouseListener); + addMouseMoveListener(mMouseMoveListener); + addMouseWheelListener(mMouseWheelListener); + addListener(SWT.Resize, mResizeListener); + addDisposeListener(mDisposeListener); + addKeyListener(mKeyListener); + addTraverseListener(new TraverseListener() { + @Override + public void keyTraversed(TraverseEvent e) { + if (e.detail == SWT.TRAVERSE_TAB_NEXT || e.detail == SWT.TRAVERSE_TAB_PREVIOUS) { + e.doit = true; + } + } + }); + + loadResources(); + + mTransform = new Transform(Display.getDefault()); + mInverse = new Transform(Display.getDefault()); + + loadAllData(); + } + + private void loadResources() { + ImageLoader loader = ImageLoader.getLoader(this.getClass()); + sRedImage = loader.loadImage("red.png", Display.getDefault()); //$NON-NLS-1$ + sYellowImage = loader.loadImage("yellow.png", Display.getDefault()); //$NON-NLS-1$ + sGreenImage = loader.loadImage("green.png", Display.getDefault()); //$NON-NLS-1$ + sNotSelectedImage = loader.loadImage("not-selected.png", Display.getDefault()); //$NON-NLS-1$ + sSelectedImage = loader.loadImage("selected.png", Display.getDefault()); //$NON-NLS-1$ + sFilteredImage = loader.loadImage("filtered.png", Display.getDefault()); //$NON-NLS-1$ + sFilteredSelectedImage = loader.loadImage("selected-filtered.png", Display.getDefault()); //$NON-NLS-1$ + mBoxColor = new Color(Display.getDefault(), new RGB(225, 225, 225)); + mTextBackgroundColor = new Color(Display.getDefault(), new RGB(82, 82, 82)); + if (mScaledSelectedImage != null) { + mScaledSelectedImage.dispose(); + } + sSystemFont = Display.getDefault().getSystemFont(); + } + + private DisposeListener mDisposeListener = new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + mModel.removeTreeChangeListener(TreeView.this); + mTransform.dispose(); + mInverse.dispose(); + mBoxColor.dispose(); + mTextBackgroundColor.dispose(); + if (mTree != null) { + mModel.setViewport(null); + } + } + }; + + private Listener mResizeListener = new Listener() { + @Override + public void handleEvent(Event e) { + synchronized (TreeView.this) { + if (mTree != null && mViewport != null) { + + // Keep the center in the same place. + Point viewCenter = + new Point(mViewport.x + mViewport.width / 2, mViewport.y + mViewport.height + / 2); + mViewport.width = getBounds().width / mZoom; + mViewport.height = getBounds().height / mZoom; + mViewport.x = viewCenter.x - mViewport.width / 2; + mViewport.y = viewCenter.y - mViewport.height / 2; + } + } + if (mViewport != null) { + mModel.setViewport(mViewport); + } + } + }; + + private KeyListener mKeyListener = new KeyListener() { + + @Override + public void keyPressed(KeyEvent e) { + boolean selectionChanged = false; + DrawableViewNode clickedNode = null; + synchronized (TreeView.this) { + if (mTree != null && mViewport != null && mSelectedNode != null) { + switch (e.keyCode) { + case SWT.ARROW_LEFT: + if (mSelectedNode.parent != null) { + mSelectedNode = mSelectedNode.parent; + selectionChanged = true; + } + break; + case SWT.ARROW_UP: + + // On up and down, it is cool to go up and down only + // the leaf nodes. + // It goes well with the layout viewer + DrawableViewNode currentNode = mSelectedNode; + while (currentNode.parent != null && currentNode.viewNode.index == 0) { + currentNode = currentNode.parent; + } + if (currentNode.parent != null) { + selectionChanged = true; + currentNode = + currentNode.parent.children + .get(currentNode.viewNode.index - 1); + while (currentNode.children.size() != 0) { + currentNode = + currentNode.children + .get(currentNode.children.size() - 1); + } + } + if (selectionChanged) { + mSelectedNode = currentNode; + } + break; + case SWT.ARROW_DOWN: + currentNode = mSelectedNode; + while (currentNode.parent != null + && currentNode.viewNode.index + 1 == currentNode.parent.children + .size()) { + currentNode = currentNode.parent; + } + if (currentNode.parent != null) { + selectionChanged = true; + currentNode = + currentNode.parent.children + .get(currentNode.viewNode.index + 1); + while (currentNode.children.size() != 0) { + currentNode = currentNode.children.get(0); + } + } + if (selectionChanged) { + mSelectedNode = currentNode; + } + break; + case SWT.ARROW_RIGHT: + DrawableViewNode rightNode = null; + double mostOverlap = 0; + final int N = mSelectedNode.children.size(); + + // We consider all the children and pick the one + // who's tree overlaps the most. + for (int i = 0; i < N; i++) { + DrawableViewNode child = mSelectedNode.children.get(i); + DrawableViewNode topMostChild = child; + while (topMostChild.children.size() != 0) { + topMostChild = topMostChild.children.get(0); + } + double overlap = + Math.min(DrawableViewNode.NODE_HEIGHT, Math.min( + mSelectedNode.top + DrawableViewNode.NODE_HEIGHT + - topMostChild.top, topMostChild.top + + child.treeHeight - mSelectedNode.top)); + if (overlap > mostOverlap) { + mostOverlap = overlap; + rightNode = child; + } + } + if (rightNode != null) { + mSelectedNode = rightNode; + selectionChanged = true; + } + break; + case SWT.CR: + clickedNode = mSelectedNode; + break; + } + } + } + if (selectionChanged) { + mModel.setSelection(mSelectedNode); + } + if (clickedNode != null) { + HierarchyViewerDirector.getDirector().showCapture(getShell(), clickedNode.viewNode); + } + } + + @Override + public void keyReleased(KeyEvent e) { + } + }; + + private MouseListener mMouseListener = new MouseListener() { + + @Override + public void mouseDoubleClick(MouseEvent e) { + DrawableViewNode clickedNode = null; + synchronized (TreeView.this) { + if (mTree != null && mViewport != null) { + Point pt = transformPoint(e.x, e.y); + clickedNode = mTree.getSelected(pt.x, pt.y); + } + } + if (clickedNode != null) { + HierarchyViewerDirector.getDirector().showCapture(getShell(), clickedNode.viewNode); + mDoubleClicked = true; + } + } + + @Override + public void mouseDown(MouseEvent e) { + boolean selectionChanged = false; + synchronized (TreeView.this) { + if (mTree != null && mViewport != null) { + Point pt = transformPoint(e.x, e.y); + + // Ignore profiling rectangle, except for... + if (mSelectedRectangleLocation != null + && pt.x >= mSelectedRectangleLocation.x + && pt.x < mSelectedRectangleLocation.x + + mSelectedRectangleLocation.width + && pt.y >= mSelectedRectangleLocation.y + && pt.y < mSelectedRectangleLocation.y + + mSelectedRectangleLocation.height) { + + // the small button! + if ((pt.x - mButtonCenter.x) * (pt.x - mButtonCenter.x) + + (pt.y - mButtonCenter.y) * (pt.y - mButtonCenter.y) <= (BUTTON_SIZE * BUTTON_SIZE) / 4) { + mButtonClicked = true; + doRedraw(); + } + return; + } + mDraggedNode = mTree.getSelected(pt.x, pt.y); + + // Update the selection. + if (mDraggedNode != null && mDraggedNode != mSelectedNode) { + mSelectedNode = mDraggedNode; + selectionChanged = true; + mAlreadySelectedOnMouseDown = false; + } else if (mDraggedNode != null) { + mAlreadySelectedOnMouseDown = true; + } + + // Can't drag the root. + if (mDraggedNode == mTree) { + mDraggedNode = null; + } + + if (mDraggedNode != null) { + mLastPoint = pt; + } else { + mLastPoint = new Point(e.x, e.y); + } + mNodeMoved = false; + mDoubleClicked = false; + } + } + if (selectionChanged) { + mModel.setSelection(mSelectedNode); + } + } + + @Override + public void mouseUp(MouseEvent e) { + boolean redraw = false; + boolean redrawButton = false; + boolean viewportChanged = false; + boolean selectionChanged = false; + synchronized (TreeView.this) { + if (mTree != null && mViewport != null && mLastPoint != null) { + if (mDraggedNode == null) { + // The viewport moves. + handleMouseDrag(new Point(e.x, e.y)); + viewportChanged = true; + } else { + // The nodes move. + handleMouseDrag(transformPoint(e.x, e.y)); + } + + // Deselect on the second click... + // This is in the mouse up, because mouse up happens after a + // double click event. + // During a double click, we don't want to deselect. + Point pt = transformPoint(e.x, e.y); + DrawableViewNode mouseUpOn = mTree.getSelected(pt.x, pt.y); + if (mouseUpOn != null && mouseUpOn == mSelectedNode + && mAlreadySelectedOnMouseDown && !mNodeMoved && !mDoubleClicked) { + mSelectedNode = null; + selectionChanged = true; + } + mLastPoint = null; + mDraggedNode = null; + redraw = true; + } + + // Just clicked the button here. + if (mButtonClicked) { + HierarchyViewerDirector.getDirector().showCapture(getShell(), + mSelectedNode.viewNode); + mButtonClicked = false; + redrawButton = true; + } + } + + // Complicated. + if (viewportChanged) { + mModel.setViewport(mViewport); + } else if (redraw) { + mModel.removeTreeChangeListener(TreeView.this); + mModel.notifyViewportChanged(); + if (selectionChanged) { + mModel.setSelection(mSelectedNode); + } + mModel.addTreeChangeListener(TreeView.this); + doRedraw(); + } else if (redrawButton) { + doRedraw(); + } + } + + }; + + private MouseMoveListener mMouseMoveListener = new MouseMoveListener() { + @Override + public void mouseMove(MouseEvent e) { + boolean redraw = false; + boolean viewportChanged = false; + synchronized (TreeView.this) { + if (mTree != null && mViewport != null && mLastPoint != null) { + if (mDraggedNode == null) { + handleMouseDrag(new Point(e.x, e.y)); + viewportChanged = true; + } else { + handleMouseDrag(transformPoint(e.x, e.y)); + } + redraw = true; + } + } + if (viewportChanged) { + mModel.setViewport(mViewport); + } else if (redraw) { + mModel.removeTreeChangeListener(TreeView.this); + mModel.notifyViewportChanged(); + mModel.addTreeChangeListener(TreeView.this); + doRedraw(); + } + } + }; + + private void handleMouseDrag(Point pt) { + + // Case 1: a node is dragged. DrawableViewNode knows how to handle this. + if (mDraggedNode != null) { + if (mLastPoint.y - pt.y != 0) { + mNodeMoved = true; + } + mDraggedNode.move(mLastPoint.y - pt.y); + mLastPoint = pt; + return; + } + + // Case 2: the viewport is dragged. We have to make sure we respect the + // bounds - don't let the user drag way out... + some leeway for the + // profiling box. + double xDif = (mLastPoint.x - pt.x) / mZoom; + double yDif = (mLastPoint.y - pt.y) / mZoom; + + double treeX = mTree.bounds.x - DRAG_LEEWAY; + double treeY = mTree.bounds.y - DRAG_LEEWAY; + double treeWidth = mTree.bounds.width + 2 * DRAG_LEEWAY; + double treeHeight = mTree.bounds.height + 2 * DRAG_LEEWAY; + + if (mViewport.width > treeWidth) { + if (xDif < 0 && mViewport.x + mViewport.width > treeX + treeWidth) { + mViewport.x = Math.max(mViewport.x + xDif, treeX + treeWidth - mViewport.width); + } else if (xDif > 0 && mViewport.x < treeX) { + mViewport.x = Math.min(mViewport.x + xDif, treeX); + } + } else { + if (xDif < 0 && mViewport.x > treeX) { + mViewport.x = Math.max(mViewport.x + xDif, treeX); + } else if (xDif > 0 && mViewport.x + mViewport.width < treeX + treeWidth) { + mViewport.x = Math.min(mViewport.x + xDif, treeX + treeWidth - mViewport.width); + } + } + if (mViewport.height > treeHeight) { + if (yDif < 0 && mViewport.y + mViewport.height > treeY + treeHeight) { + mViewport.y = Math.max(mViewport.y + yDif, treeY + treeHeight - mViewport.height); + } else if (yDif > 0 && mViewport.y < treeY) { + mViewport.y = Math.min(mViewport.y + yDif, treeY); + } + } else { + if (yDif < 0 && mViewport.y > treeY) { + mViewport.y = Math.max(mViewport.y + yDif, treeY); + } else if (yDif > 0 && mViewport.y + mViewport.height < treeY + treeHeight) { + mViewport.y = Math.min(mViewport.y + yDif, treeY + treeHeight - mViewport.height); + } + } + mLastPoint = pt; + } + + private Point transformPoint(double x, double y) { + float[] pt = { + (float) x, (float) y + }; + mInverse.transform(pt); + return new Point(pt[0], pt[1]); + } + + private MouseWheelListener mMouseWheelListener = new MouseWheelListener() { + @Override + public void mouseScrolled(MouseEvent e) { + Point zoomPoint = null; + synchronized (TreeView.this) { + if (mTree != null && mViewport != null) { + mZoom += Math.ceil(e.count / 3.0) * 0.1; + zoomPoint = transformPoint(e.x, e.y); + } + } + if (zoomPoint != null) { + mModel.zoomOnPoint(mZoom, zoomPoint); + } + } + }; + + private PaintListener mPaintListener = new PaintListener() { + @Override + public void paintControl(PaintEvent e) { + synchronized (TreeView.this) { + e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); + e.gc.fillRectangle(0, 0, getBounds().width, getBounds().height); + if (mTree != null && mViewport != null) { + + // Easy stuff! + e.gc.setTransform(mTransform); + e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); + Path connectionPath = new Path(Display.getDefault()); + paintRecursive(e.gc, mTransform, mTree, mSelectedNode, connectionPath); + e.gc.drawPath(connectionPath); + connectionPath.dispose(); + + // Draw the profiling box. + if (mSelectedNode != null) { + + e.gc.setAlpha(200); + + // Draw the little triangle + int x = mSelectedNode.left + DrawableViewNode.NODE_WIDTH / 2; + int y = (int) mSelectedNode.top + 4; + e.gc.setBackground(mBoxColor); + e.gc.fillPolygon(new int[] { + x, y, x - 11, y - 11, x + 11, y - 11 + }); + + // Draw the rectangle and update the location. + y -= 10 + RECT_HEIGHT; + e.gc.fillRoundRectangle(x - RECT_WIDTH / 2, y, RECT_WIDTH, RECT_HEIGHT, 30, + 30); + mSelectedRectangleLocation = + new Rectangle(x - RECT_WIDTH / 2, y, RECT_WIDTH, RECT_HEIGHT); + + e.gc.setAlpha(255); + + // Draw the button + mButtonCenter = + new Point(x - BUTTON_RIGHT_OFFSET + (RECT_WIDTH - BUTTON_SIZE) / 2, + y + BUTTON_TOP_OFFSET + BUTTON_SIZE / 2); + + if (mButtonClicked) { + e.gc + .setBackground(Display.getDefault().getSystemColor( + SWT.COLOR_BLACK)); + } else { + e.gc.setBackground(mTextBackgroundColor); + + } + e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); + + e.gc.fillOval(x + RECT_WIDTH / 2 - BUTTON_RIGHT_OFFSET - BUTTON_SIZE, y + + BUTTON_TOP_OFFSET, BUTTON_SIZE, BUTTON_SIZE); + + e.gc.drawRectangle(x - BUTTON_RIGHT_OFFSET + + (RECT_WIDTH - BUTTON_SIZE - RECTANGLE_SIZE) / 2 - 1, y + + BUTTON_TOP_OFFSET + (BUTTON_SIZE - RECTANGLE_SIZE) / 2, + RECTANGLE_SIZE + 1, RECTANGLE_SIZE); + + y += 15; + + // If there is an image, draw it. + if (mSelectedNode.viewNode.image != null + && mSelectedNode.viewNode.image.getBounds().height != 1 + && mSelectedNode.viewNode.image.getBounds().width != 1) { + + // Scaling the image to the right size takes lots of + // time, so we want to do it only once. + + // If the selection changed, get rid of the old + // image. + if (mLastDrawnSelectedViewNode != mSelectedNode) { + if (mScaledSelectedImage != null) { + mScaledSelectedImage.dispose(); + mScaledSelectedImage = null; + } + mLastDrawnSelectedViewNode = mSelectedNode; + } + + if (mScaledSelectedImage == null) { + double ratio = + 1.0 * mSelectedNode.viewNode.image.getBounds().width + / mSelectedNode.viewNode.image.getBounds().height; + int newWidth, newHeight; + if (ratio > 1.0 * IMAGE_WIDTH / IMAGE_HEIGHT) { + newWidth = + Math.min(IMAGE_WIDTH, mSelectedNode.viewNode.image + .getBounds().width); + newHeight = (int) (newWidth / ratio); + } else { + newHeight = + Math.min(IMAGE_HEIGHT, mSelectedNode.viewNode.image + .getBounds().height); + newWidth = (int) (newHeight * ratio); + } + + // Interesting note... We make the image twice + // the needed size so that there is better + // resolution under zoom. + newWidth = Math.max(newWidth * 2, 1); + newHeight = Math.max(newHeight * 2, 1); + mScaledSelectedImage = + new Image(Display.getDefault(), newWidth, newHeight); + GC gc = new GC(mScaledSelectedImage); + gc.setBackground(mTextBackgroundColor); + gc.fillRectangle(0, 0, newWidth, newHeight); + gc.drawImage(mSelectedNode.viewNode.image, 0, 0, + mSelectedNode.viewNode.image.getBounds().width, + mSelectedNode.viewNode.image.getBounds().height, 0, 0, + newWidth, newHeight); + gc.dispose(); + } + + // Draw the background rectangle + e.gc.setBackground(mTextBackgroundColor); + e.gc.fillRoundRectangle(x - mScaledSelectedImage.getBounds().width / 4 + - IMAGE_OFFSET, y + + (IMAGE_HEIGHT - mScaledSelectedImage.getBounds().height / 2) + / 2 - IMAGE_OFFSET, mScaledSelectedImage.getBounds().width / 2 + + 2 * IMAGE_OFFSET, mScaledSelectedImage.getBounds().height / 2 + + 2 * IMAGE_OFFSET, IMAGE_ROUNDING, IMAGE_ROUNDING); + + // Under max zoom, we want the image to be + // untransformed. So, get back to the identity + // transform. + int imageX = x - mScaledSelectedImage.getBounds().width / 4; + int imageY = + y + + (IMAGE_HEIGHT - mScaledSelectedImage.getBounds().height / 2) + / 2; + + Transform untransformedTransform = new Transform(Display.getDefault()); + e.gc.setTransform(untransformedTransform); + float[] pt = new float[] { + imageX, imageY + }; + mTransform.transform(pt); + e.gc.drawImage(mScaledSelectedImage, 0, 0, mScaledSelectedImage + .getBounds().width, mScaledSelectedImage.getBounds().height, + (int) pt[0], (int) pt[1], (int) (mScaledSelectedImage + .getBounds().width + * mZoom / 2), + (int) (mScaledSelectedImage.getBounds().height * mZoom / 2)); + untransformedTransform.dispose(); + e.gc.setTransform(mTransform); + } + + // Text stuff + + y += IMAGE_HEIGHT; + y += 10; + Font font = getFont(8, false); + e.gc.setFont(font); + + String text = + mSelectedNode.viewNode.viewCount + " view" + + (mSelectedNode.viewNode.viewCount != 1 ? "s" : ""); + DecimalFormat formatter = new DecimalFormat("0.000"); + + String measureText = + "Measure: " + + (mSelectedNode.viewNode.measureTime != -1 ? formatter + .format(mSelectedNode.viewNode.measureTime) + + " ms" : "n/a"); + String layoutText = + "Layout: " + + (mSelectedNode.viewNode.layoutTime != -1 ? formatter + .format(mSelectedNode.viewNode.layoutTime) + + " ms" : "n/a"); + String drawText = + "Draw: " + + (mSelectedNode.viewNode.drawTime != -1 ? formatter + .format(mSelectedNode.viewNode.drawTime) + + " ms" : "n/a"); + + org.eclipse.swt.graphics.Point titleExtent = e.gc.stringExtent(text); + org.eclipse.swt.graphics.Point measureExtent = + e.gc.stringExtent(measureText); + org.eclipse.swt.graphics.Point layoutExtent = e.gc.stringExtent(layoutText); + org.eclipse.swt.graphics.Point drawExtent = e.gc.stringExtent(drawText); + int boxWidth = + Math.max(titleExtent.x, Math.max(measureExtent.x, Math.max( + layoutExtent.x, drawExtent.x))) + + 2 * TEXT_SIDE_OFFSET; + int boxHeight = + titleExtent.y + TEXT_SPACING + measureExtent.y + TEXT_SPACING + + layoutExtent.y + TEXT_SPACING + drawExtent.y + 2 + * TEXT_TOP_OFFSET; + + e.gc.setBackground(mTextBackgroundColor); + e.gc.fillRoundRectangle(x - boxWidth / 2, y, boxWidth, boxHeight, + TEXT_ROUNDING, TEXT_ROUNDING); + + e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); + + y += TEXT_TOP_OFFSET; + + e.gc.drawText(text, x - titleExtent.x / 2, y, true); + + x -= boxWidth / 2; + x += TEXT_SIDE_OFFSET; + + y += titleExtent.y + TEXT_SPACING; + + e.gc.drawText(measureText, x, y, true); + + y += measureExtent.y + TEXT_SPACING; + + e.gc.drawText(layoutText, x, y, true); + + y += layoutExtent.y + TEXT_SPACING; + + e.gc.drawText(drawText, x, y, true); + + font.dispose(); + } else { + mSelectedRectangleLocation = null; + mButtonCenter = null; + } + } + } + } + }; + + private static void paintRecursive(GC gc, Transform transform, DrawableViewNode node, + DrawableViewNode selectedNode, Path connectionPath) { + if (selectedNode == node && node.viewNode.filtered) { + gc.drawImage(sFilteredSelectedImage, node.left, (int) Math.round(node.top)); + } else if (selectedNode == node) { + gc.drawImage(sSelectedImage, node.left, (int) Math.round(node.top)); + } else if (node.viewNode.filtered) { + gc.drawImage(sFilteredImage, node.left, (int) Math.round(node.top)); + } else { + gc.drawImage(sNotSelectedImage, node.left, (int) Math.round(node.top)); + } + + int fontHeight = gc.getFontMetrics().getHeight(); + + // Draw the text... + int contentWidth = + DrawableViewNode.NODE_WIDTH - 2 * DrawableViewNode.CONTENT_LEFT_RIGHT_PADDING; + String name = node.viewNode.name; + int dotIndex = name.lastIndexOf('.'); + if (dotIndex != -1) { + name = name.substring(dotIndex + 1); + } + double x = node.left + DrawableViewNode.CONTENT_LEFT_RIGHT_PADDING; + double y = node.top + DrawableViewNode.CONTENT_TOP_BOTTOM_PADDING; + drawTextInArea(gc, transform, name, x, y, contentWidth, fontHeight, 10, true); + + y += fontHeight + DrawableViewNode.CONTENT_INTER_PADDING; + + drawTextInArea(gc, transform, "@" + node.viewNode.hashCode, x, y, contentWidth, fontHeight, + 8, false); + + y += fontHeight + DrawableViewNode.CONTENT_INTER_PADDING; + if (!node.viewNode.id.equals("NO_ID")) { + drawTextInArea(gc, transform, node.viewNode.id, x, y, contentWidth, fontHeight, 8, + false); + } + + if (node.viewNode.measureRating != ProfileRating.NONE) { + y = + node.top + DrawableViewNode.NODE_HEIGHT + - DrawableViewNode.CONTENT_TOP_BOTTOM_PADDING + - sRedImage.getBounds().height; + x += + (contentWidth - (sRedImage.getBounds().width * 3 + 2 * DrawableViewNode.CONTENT_INTER_PADDING)) / 2; + switch (node.viewNode.measureRating) { + case GREEN: + gc.drawImage(sGreenImage, (int) x, (int) y); + break; + case YELLOW: + gc.drawImage(sYellowImage, (int) x, (int) y); + break; + case RED: + gc.drawImage(sRedImage, (int) x, (int) y); + break; + } + + x += sRedImage.getBounds().width + DrawableViewNode.CONTENT_INTER_PADDING; + switch (node.viewNode.layoutRating) { + case GREEN: + gc.drawImage(sGreenImage, (int) x, (int) y); + break; + case YELLOW: + gc.drawImage(sYellowImage, (int) x, (int) y); + break; + case RED: + gc.drawImage(sRedImage, (int) x, (int) y); + break; + } + + x += sRedImage.getBounds().width + DrawableViewNode.CONTENT_INTER_PADDING; + switch (node.viewNode.drawRating) { + case GREEN: + gc.drawImage(sGreenImage, (int) x, (int) y); + break; + case YELLOW: + gc.drawImage(sYellowImage, (int) x, (int) y); + break; + case RED: + gc.drawImage(sRedImage, (int) x, (int) y); + break; + } + } + + org.eclipse.swt.graphics.Point indexExtent = + gc.stringExtent(Integer.toString(node.viewNode.index)); + x = + node.left + DrawableViewNode.NODE_WIDTH - DrawableViewNode.INDEX_PADDING + - indexExtent.x; + y = + node.top + DrawableViewNode.NODE_HEIGHT - DrawableViewNode.INDEX_PADDING + - indexExtent.y; + gc.drawText(Integer.toString(node.viewNode.index), (int) x, (int) y, SWT.DRAW_TRANSPARENT); + + int N = node.children.size(); + if (N == 0) { + return; + } + float childSpacing = (1.0f * (DrawableViewNode.NODE_HEIGHT - 2 * LINE_PADDING)) / N; + for (int i = 0; i < N; i++) { + DrawableViewNode child = node.children.get(i); + paintRecursive(gc, transform, child, selectedNode, connectionPath); + float x1 = node.left + DrawableViewNode.NODE_WIDTH; + float y1 = (float) node.top + LINE_PADDING + childSpacing * i + childSpacing / 2; + float x2 = child.left; + float y2 = (float) child.top + DrawableViewNode.NODE_HEIGHT / 2.0f; + float cx1 = x1 + BEZIER_FRACTION * DrawableViewNode.PARENT_CHILD_SPACING; + float cy1 = y1; + float cx2 = x2 - BEZIER_FRACTION * DrawableViewNode.PARENT_CHILD_SPACING; + float cy2 = y2; + connectionPath.moveTo(x1, y1); + connectionPath.cubicTo(cx1, cy1, cx2, cy2, x2, y2); + } + } + + private static void drawTextInArea(GC gc, Transform transform, String text, double x, double y, + double width, double height, int fontSize, boolean bold) { + + Font oldFont = gc.getFont(); + + Font newFont = getFont(fontSize, bold); + gc.setFont(newFont); + + org.eclipse.swt.graphics.Point extent = gc.stringExtent(text); + + if (extent.x > width) { + // Oh no... we need to scale it. + double scale = width / extent.x; + float[] transformElements = new float[6]; + transform.getElements(transformElements); + transform.scale((float) scale, (float) scale); + gc.setTransform(transform); + + x /= scale; + y /= scale; + y += (extent.y / scale - extent.y) / 2; + + gc.drawText(text, (int) x, (int) y, SWT.DRAW_TRANSPARENT); + + transform.setElements(transformElements[0], transformElements[1], transformElements[2], + transformElements[3], transformElements[4], transformElements[5]); + gc.setTransform(transform); + } else { + gc.drawText(text, (int) (x + (width - extent.x) / 2), + (int) (y + (height - extent.y) / 2), SWT.DRAW_TRANSPARENT); + } + gc.setFont(oldFont); + newFont.dispose(); + + } + + public static Image paintToImage(DrawableViewNode tree) { + Image image = + new Image(Display.getDefault(), (int) Math.ceil(tree.bounds.width), (int) Math + .ceil(tree.bounds.height)); + + Transform transform = new Transform(Display.getDefault()); + transform.identity(); + transform.translate((float) -tree.bounds.x, (float) -tree.bounds.y); + Path connectionPath = new Path(Display.getDefault()); + GC gc = new GC(image); + + // Can't use Display.getDefault().getSystemColor in a non-UI thread. + Color white = new Color(Display.getDefault(), 255, 255, 255); + Color black = new Color(Display.getDefault(), 0, 0, 0); + gc.setForeground(white); + gc.setBackground(black); + gc.fillRectangle(0, 0, image.getBounds().width, image.getBounds().height); + gc.setTransform(transform); + paintRecursive(gc, transform, tree, null, connectionPath); + gc.drawPath(connectionPath); + gc.dispose(); + connectionPath.dispose(); + white.dispose(); + black.dispose(); + return image; + } + + private static Font getFont(int size, boolean bold) { + FontData[] fontData = sSystemFont.getFontData(); + for (int i = 0; i < fontData.length; i++) { + fontData[i].setHeight(size); + if (bold) { + fontData[i].setStyle(SWT.BOLD); + } + } + return new Font(Display.getDefault(), fontData); + } + + private void doRedraw() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + redraw(); + } + }); + } + + public void loadAllData() { + boolean newViewport = mViewport == null; + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + synchronized (this) { + mTree = mModel.getTree(); + mSelectedNode = mModel.getSelection(); + mViewport = mModel.getViewport(); + mZoom = mModel.getZoom(); + if (mTree != null && mViewport == null) { + mViewport = + new Rectangle(0, mTree.top + DrawableViewNode.NODE_HEIGHT / 2 + - getBounds().height / 2, getBounds().width, + getBounds().height); + } else { + setTransform(); + } + } + } + }); + if (newViewport) { + mModel.setViewport(mViewport); + } + } + + // Fickle behaviour... When a new tree is loaded, the model doesn't know + // about the viewport until it passes through here. + @Override + public void treeChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + synchronized (this) { + mTree = mModel.getTree(); + mSelectedNode = mModel.getSelection(); + if (mTree == null) { + mViewport = null; + } else { + mViewport = + new Rectangle(0, mTree.top + DrawableViewNode.NODE_HEIGHT / 2 + - getBounds().height / 2, getBounds().width, + getBounds().height); + } + } + } + }); + if (mViewport != null) { + mModel.setViewport(mViewport); + } else { + doRedraw(); + } + } + + private void setTransform() { + if (mViewport != null && mTree != null) { + // Set the transform. + mTransform.identity(); + mInverse.identity(); + + mTransform.scale((float) mZoom, (float) mZoom); + mInverse.scale((float) mZoom, (float) mZoom); + mTransform.translate((float) -mViewport.x, (float) -mViewport.y); + mInverse.translate((float) -mViewport.x, (float) -mViewport.y); + mInverse.invert(); + } + } + + // Note the syncExec and then synchronized... It avoids deadlock + @Override + public void viewportChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + synchronized (this) { + mViewport = mModel.getViewport(); + mZoom = mModel.getZoom(); + setTransform(); + } + } + }); + doRedraw(); + } + + @Override + public void zoomChanged() { + viewportChanged(); + } + + @Override + public void selectionChanged() { + synchronized (this) { + mSelectedNode = mModel.getSelection(); + if (mSelectedNode != null && mSelectedNode.viewNode.image == null) { + HierarchyViewerDirector.getDirector() + .loadCaptureInBackground(mSelectedNode.viewNode); + } + } + doRedraw(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeViewControls.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeViewControls.java new file mode 100644 index 00000000..0b4dab44 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeViewControls.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.ui; + +import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import com.android.hierarchyviewerlib.models.TreeViewModel; +import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +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.Label; +import org.eclipse.swt.widgets.Slider; +import org.eclipse.swt.widgets.Text; + +public class TreeViewControls extends Composite implements ITreeChangeListener { + + private Text mFilterText; + + private Slider mZoomSlider; + + public TreeViewControls(Composite parent) { + super(parent, SWT.NONE); + GridLayout layout = new GridLayout(5, false); + layout.marginWidth = layout.marginHeight = 2; + layout.verticalSpacing = layout.horizontalSpacing = 4; + setLayout(layout); + + Label filterLabel = new Label(this, SWT.NONE); + filterLabel.setText("Filter by class or id:"); + filterLabel.setLayoutData(new GridData(GridData.BEGINNING, GridData.CENTER, false, true)); + + mFilterText = new Text(this, SWT.LEFT | SWT.SINGLE); + mFilterText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mFilterText.addModifyListener(mFilterTextModifyListener); + mFilterText.setText(HierarchyViewerDirector.getDirector().getFilterText()); + + Label smallZoomLabel = new Label(this, SWT.NONE); + smallZoomLabel.setText(" 20%"); + smallZoomLabel + .setLayoutData(new GridData(GridData.BEGINNING, GridData.CENTER, false, true)); + + mZoomSlider = new Slider(this, SWT.HORIZONTAL); + GridData zoomSliderGridData = new GridData(GridData.CENTER, GridData.CENTER, false, false); + zoomSliderGridData.widthHint = 190; + mZoomSlider.setLayoutData(zoomSliderGridData); + mZoomSlider.setMinimum((int) (TreeViewModel.MIN_ZOOM * 10)); + mZoomSlider.setMaximum((int) (TreeViewModel.MAX_ZOOM * 10 + 1)); + mZoomSlider.setThumb(1); + mZoomSlider.setSelection((int) Math.round(TreeViewModel.getModel().getZoom() * 10)); + + mZoomSlider.addSelectionListener(mZoomSliderSelectionListener); + + Label largeZoomLabel = new Label(this, SWT.NONE); + largeZoomLabel + .setLayoutData(new GridData(GridData.BEGINNING, GridData.CENTER, false, true)); + largeZoomLabel.setText("200%"); + + addDisposeListener(mDisposeListener); + + TreeViewModel.getModel().addTreeChangeListener(this); + } + + private DisposeListener mDisposeListener = new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + TreeViewModel.getModel().removeTreeChangeListener(TreeViewControls.this); + } + }; + + private SelectionListener mZoomSliderSelectionListener = new SelectionListener() { + private int oldValue; + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + // pass + } + + @Override + public void widgetSelected(SelectionEvent e) { + int newValue = mZoomSlider.getSelection(); + if (oldValue != newValue) { + TreeViewModel.getModel().removeTreeChangeListener(TreeViewControls.this); + TreeViewModel.getModel().setZoom(newValue / 10.0); + TreeViewModel.getModel().addTreeChangeListener(TreeViewControls.this); + oldValue = newValue; + } + } + }; + + private ModifyListener mFilterTextModifyListener = new ModifyListener() { + @Override + public void modifyText(ModifyEvent e) { + HierarchyViewerDirector.getDirector().filterNodes(mFilterText.getText()); + } + }; + + @Override + public void selectionChanged() { + // pass + } + + @Override + public void treeChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + if (TreeViewModel.getModel().getTree() != null) { + mZoomSlider.setSelection((int) Math + .round(TreeViewModel.getModel().getZoom() * 10)); + } + mFilterText.setText(""); //$NON-NLS-1$ + } + }); + } + + @Override + public void viewportChanged() { + // pass + } + + @Override + public void zoomChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + mZoomSlider.setSelection((int) Math.round(TreeViewModel.getModel().getZoom() * 10)); + } + }); + }; +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeViewOverview.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeViewOverview.java new file mode 100644 index 00000000..1bb5594f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeViewOverview.java @@ -0,0 +1,396 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.ui; + +import com.android.ddmuilib.ImageLoader; +import com.android.hierarchyviewerlib.models.TreeViewModel; +import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener; +import com.android.hierarchyviewerlib.ui.util.DrawableViewNode; +import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Point; +import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Rectangle; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.MouseListener; +import org.eclipse.swt.events.MouseMoveListener; +import org.eclipse.swt.events.PaintEvent; +import org.eclipse.swt.events.PaintListener; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Path; +import org.eclipse.swt.graphics.Transform; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Listener; + +public class TreeViewOverview extends Canvas implements ITreeChangeListener { + + private TreeViewModel mModel; + + private DrawableViewNode mTree; + + private Rectangle mViewport; + + private Transform mTransform; + + private Transform mInverse; + + private Rectangle mBounds = new Rectangle(); + + private double mScale; + + private boolean mDragging = false; + + private DrawableViewNode mSelectedNode; + + private static Image sNotSelectedImage; + + private static Image sSelectedImage; + + private static Image sFilteredImage; + + private static Image sFilteredSelectedImage; + + public TreeViewOverview(Composite parent) { + super(parent, SWT.NONE); + + mModel = TreeViewModel.getModel(); + mModel.addTreeChangeListener(this); + + loadResources(); + + addPaintListener(mPaintListener); + addMouseListener(mMouseListener); + addMouseMoveListener(mMouseMoveListener); + addListener(SWT.Resize, mResizeListener); + addDisposeListener(mDisposeListener); + + mTransform = new Transform(Display.getDefault()); + mInverse = new Transform(Display.getDefault()); + + loadAllData(); + } + + private void loadResources() { + ImageLoader loader = ImageLoader.getLoader(this.getClass()); + sNotSelectedImage = loader.loadImage("not-selected.png", Display.getDefault()); //$NON-NLS-1$ + sSelectedImage = loader.loadImage("selected-small.png", Display.getDefault()); //$NON-NLS-1$ + sFilteredImage = loader.loadImage("filtered.png", Display.getDefault()); //$NON-NLS-1$ + sFilteredSelectedImage = + loader.loadImage("selected-filtered-small.png", Display.getDefault()); //$NON-NLS-1$ + } + + private DisposeListener mDisposeListener = new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + mModel.removeTreeChangeListener(TreeViewOverview.this); + mTransform.dispose(); + mInverse.dispose(); + } + }; + + private MouseListener mMouseListener = new MouseListener() { + + @Override + public void mouseDoubleClick(MouseEvent e) { + // pass + } + + @Override + public void mouseDown(MouseEvent e) { + boolean redraw = false; + synchronized (TreeViewOverview.this) { + if (mTree != null && mViewport != null) { + mDragging = true; + redraw = true; + handleMouseEvent(transformPoint(e.x, e.y)); + } + } + if (redraw) { + mModel.removeTreeChangeListener(TreeViewOverview.this); + mModel.setViewport(mViewport); + mModel.addTreeChangeListener(TreeViewOverview.this); + doRedraw(); + } + } + + @Override + public void mouseUp(MouseEvent e) { + boolean redraw = false; + synchronized (TreeViewOverview.this) { + if (mTree != null && mViewport != null) { + mDragging = false; + redraw = true; + handleMouseEvent(transformPoint(e.x, e.y)); + + // Update bounds and transform only on mouse up. That way, + // you don't get confusing behaviour during mouse drag and + // it snaps neatly at the end + setBounds(); + setTransform(); + } + } + if (redraw) { + mModel.removeTreeChangeListener(TreeViewOverview.this); + mModel.setViewport(mViewport); + mModel.addTreeChangeListener(TreeViewOverview.this); + doRedraw(); + } + } + + }; + + private MouseMoveListener mMouseMoveListener = new MouseMoveListener() { + @Override + public void mouseMove(MouseEvent e) { + boolean moved = false; + synchronized (TreeViewOverview.this) { + if (mDragging) { + moved = true; + handleMouseEvent(transformPoint(e.x, e.y)); + } + } + if (moved) { + mModel.removeTreeChangeListener(TreeViewOverview.this); + mModel.setViewport(mViewport); + mModel.addTreeChangeListener(TreeViewOverview.this); + doRedraw(); + } + } + }; + + private void handleMouseEvent(Point pt) { + mViewport.x = pt.x - mViewport.width / 2; + mViewport.y = pt.y - mViewport.height / 2; + if (mViewport.x < mBounds.x) { + mViewport.x = mBounds.x; + } + if (mViewport.y < mBounds.y) { + mViewport.y = mBounds.y; + } + if (mViewport.x + mViewport.width > mBounds.x + mBounds.width) { + mViewport.x = mBounds.x + mBounds.width - mViewport.width; + } + if (mViewport.y + mViewport.height > mBounds.y + mBounds.height) { + mViewport.y = mBounds.y + mBounds.height - mViewport.height; + } + } + + private Point transformPoint(double x, double y) { + float[] pt = { + (float) x, (float) y + }; + mInverse.transform(pt); + return new Point(pt[0], pt[1]); + } + + private Listener mResizeListener = new Listener() { + @Override + public void handleEvent(Event arg0) { + synchronized (TreeViewOverview.this) { + setTransform(); + } + doRedraw(); + } + }; + + private PaintListener mPaintListener = new PaintListener() { + @Override + public void paintControl(PaintEvent e) { + synchronized (TreeViewOverview.this) { + if (mTree != null) { + e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK)); + e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); + e.gc.fillRectangle(0, 0, getBounds().width, getBounds().height); + e.gc.setTransform(mTransform); + e.gc.setLineWidth((int) Math.ceil(0.7 / mScale)); + Path connectionPath = new Path(Display.getDefault()); + paintRecursive(e.gc, mTree, connectionPath); + e.gc.drawPath(connectionPath); + connectionPath.dispose(); + + if (mViewport != null) { + e.gc.setAlpha(50); + e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); + e.gc.fillRectangle((int) mViewport.x, (int) mViewport.y, (int) Math + .ceil(mViewport.width), (int) Math.ceil(mViewport.height)); + + e.gc.setAlpha(255); + e.gc.setForeground(Display.getDefault().getSystemColor( + SWT.COLOR_DARK_GRAY)); + e.gc.setLineWidth((int) Math.ceil(2 / mScale)); + e.gc.drawRectangle((int) mViewport.x, (int) mViewport.y, (int) Math + .ceil(mViewport.width), (int) Math.ceil(mViewport.height)); + } + } + } + } + }; + + private void paintRecursive(GC gc, DrawableViewNode node, Path connectionPath) { + if (mSelectedNode == node && node.viewNode.filtered) { + gc.drawImage(sFilteredSelectedImage, node.left, (int) Math.round(node.top)); + } else if (mSelectedNode == node) { + gc.drawImage(sSelectedImage, node.left, (int) Math.round(node.top)); + } else if (node.viewNode.filtered) { + gc.drawImage(sFilteredImage, node.left, (int) Math.round(node.top)); + } else { + gc.drawImage(sNotSelectedImage, node.left, (int) Math.round(node.top)); + } + int N = node.children.size(); + if (N == 0) { + return; + } + float childSpacing = + (1.0f * (DrawableViewNode.NODE_HEIGHT - 2 * TreeView.LINE_PADDING)) / N; + for (int i = 0; i < N; i++) { + DrawableViewNode child = node.children.get(i); + paintRecursive(gc, child, connectionPath); + float x1 = node.left + DrawableViewNode.NODE_WIDTH; + float y1 = + (float) node.top + TreeView.LINE_PADDING + childSpacing * i + childSpacing / 2; + float x2 = child.left; + float y2 = (float) child.top + DrawableViewNode.NODE_HEIGHT / 2.0f; + float cx1 = x1 + TreeView.BEZIER_FRACTION * DrawableViewNode.PARENT_CHILD_SPACING; + float cy1 = y1; + float cx2 = x2 - TreeView.BEZIER_FRACTION * DrawableViewNode.PARENT_CHILD_SPACING; + float cy2 = y2; + connectionPath.moveTo(x1, y1); + connectionPath.cubicTo(cx1, cy1, cx2, cy2, x2, y2); + } + } + + private void doRedraw() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + redraw(); + } + }); + } + + public void loadAllData() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + synchronized (this) { + mTree = mModel.getTree(); + mSelectedNode = mModel.getSelection(); + mViewport = mModel.getViewport(); + setBounds(); + setTransform(); + } + } + }); + } + + // Note the syncExec and then synchronized... It avoids deadlock + @Override + public void treeChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + synchronized (this) { + mTree = mModel.getTree(); + mSelectedNode = mModel.getSelection(); + mViewport = mModel.getViewport(); + setBounds(); + setTransform(); + } + } + }); + doRedraw(); + } + + private void setBounds() { + if (mViewport != null && mTree != null) { + mBounds.x = Math.min(mViewport.x, mTree.bounds.x); + mBounds.y = Math.min(mViewport.y, mTree.bounds.y); + mBounds.width = + Math.max(mViewport.x + mViewport.width, mTree.bounds.x + mTree.bounds.width) + - mBounds.x; + mBounds.height = + Math.max(mViewport.y + mViewport.height, mTree.bounds.y + mTree.bounds.height) + - mBounds.y; + } else if (mTree != null) { + mBounds.x = mTree.bounds.x; + mBounds.y = mTree.bounds.y; + mBounds.width = mTree.bounds.x + mTree.bounds.width - mBounds.x; + mBounds.height = mTree.bounds.y + mTree.bounds.height - mBounds.y; + } + } + + private void setTransform() { + if (mTree != null) { + + mTransform.identity(); + mInverse.identity(); + final Point size = new Point(); + size.x = getBounds().width; + size.y = getBounds().height; + if (mBounds.width == 0 || mBounds.height == 0 || size.x == 0 || size.y == 0) { + mScale = 1; + } else { + mScale = Math.min(size.x / mBounds.width, size.y / mBounds.height); + } + mTransform.scale((float) mScale, (float) mScale); + mInverse.scale((float) mScale, (float) mScale); + mTransform.translate((float) -mBounds.x, (float) -mBounds.y); + mInverse.translate((float) -mBounds.x, (float) -mBounds.y); + if (size.x / mBounds.width < size.y / mBounds.height) { + mTransform.translate(0, (float) (size.y / mScale - mBounds.height) / 2); + mInverse.translate(0, (float) (size.y / mScale - mBounds.height) / 2); + } else { + mTransform.translate((float) (size.x / mScale - mBounds.width) / 2, 0); + mInverse.translate((float) (size.x / mScale - mBounds.width) / 2, 0); + } + mInverse.invert(); + } + } + + @Override + public void viewportChanged() { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + synchronized (this) { + mViewport = mModel.getViewport(); + setBounds(); + setTransform(); + } + } + }); + doRedraw(); + } + + @Override + public void zoomChanged() { + viewportChanged(); + } + + @Override + public void selectionChanged() { + synchronized (this) { + mSelectedNode = mModel.getSelection(); + } + doRedraw(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/DrawableViewNode.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/DrawableViewNode.java new file mode 100644 index 00000000..3343fdf8 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/DrawableViewNode.java @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.ui.util; + +import com.android.hierarchyviewerlib.models.ViewNode; + +import java.util.ArrayList; + +public class DrawableViewNode { + public ViewNode viewNode; + + public final ArrayList children = new ArrayList(); + + public final static int NODE_HEIGHT = 100; + + public final static int NODE_WIDTH = 180; + + public final static int CONTENT_LEFT_RIGHT_PADDING = 9; + + public final static int CONTENT_TOP_BOTTOM_PADDING = 8; + + public final static int CONTENT_INTER_PADDING = 3; + + public final static int INDEX_PADDING = 7; + + public final static int LEAF_NODE_SPACING = 9; + + public final static int NON_LEAF_NODE_SPACING = 15; + + public final static int PARENT_CHILD_SPACING = 50; + + public final static int PADDING = 30; + + public int treeHeight; + + public int treeWidth; + + public boolean leaf; + + public DrawableViewNode parent; + + public int left; + + public double top; + + public int topSpacing; + + public int bottomSpacing; + + public boolean treeDrawn; + + public static class Rectangle { + public double x, y, width, height; + + public Rectangle() { + + } + + public Rectangle(Rectangle other) { + this.x = other.x; + this.y = other.y; + this.width = other.width; + this.height = other.height; + } + + public Rectangle(double x, double y, double width, double height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + @Override + public String toString() { + return "{" + x + ", " + y + ", " + width + ", " + height + "}"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ + } + + } + + public static class Point { + public double x, y; + + public Point() { + } + + public Point(double x, double y) { + this.x = x; + this.y = y; + } + + @Override + public String toString() { + return "(" + x + ", " + y + ")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + } + + public Rectangle bounds = new Rectangle(); + + public DrawableViewNode(ViewNode viewNode) { + this.viewNode = viewNode; + treeDrawn = !viewNode.willNotDraw; + if (viewNode.children.size() == 0) { + treeHeight = NODE_HEIGHT; + treeWidth = NODE_WIDTH; + leaf = true; + } else { + leaf = false; + int N = viewNode.children.size(); + treeHeight = 0; + treeWidth = 0; + for (int i = 0; i < N; i++) { + DrawableViewNode child = new DrawableViewNode(viewNode.children.get(i)); + children.add(child); + child.parent = this; + treeHeight += child.treeHeight; + treeWidth = Math.max(treeWidth, child.treeWidth); + if (i != 0) { + DrawableViewNode prevChild = children.get(i - 1); + if (prevChild.leaf && child.leaf) { + treeHeight += LEAF_NODE_SPACING; + prevChild.bottomSpacing = LEAF_NODE_SPACING; + child.topSpacing = LEAF_NODE_SPACING; + } else { + treeHeight += NON_LEAF_NODE_SPACING; + prevChild.bottomSpacing = NON_LEAF_NODE_SPACING; + child.topSpacing = NON_LEAF_NODE_SPACING; + } + } + treeDrawn |= child.treeDrawn; + } + treeWidth += NODE_WIDTH + PARENT_CHILD_SPACING; + } + } + + public void setLeft() { + if (parent == null) { + left = PADDING; + bounds.x = 0; + bounds.width = treeWidth + 2 * PADDING; + } else { + left = parent.left + NODE_WIDTH + PARENT_CHILD_SPACING; + } + int N = children.size(); + for (int i = 0; i < N; i++) { + children.get(i).setLeft(); + } + } + + public void placeRoot() { + top = PADDING + (treeHeight - NODE_HEIGHT) / 2.0; + double currentTop = PADDING; + int N = children.size(); + for (int i = 0; i < N; i++) { + DrawableViewNode child = children.get(i); + child.place(currentTop, top - currentTop); + currentTop += child.treeHeight + child.bottomSpacing; + } + bounds.y = 0; + bounds.height = treeHeight + 2 * PADDING; + } + + private void place(double treeTop, double rootDistance) { + if (treeHeight <= rootDistance) { + top = treeTop + treeHeight - NODE_HEIGHT; + } else if (rootDistance <= -NODE_HEIGHT) { + top = treeTop; + } else { + if (children.size() == 0) { + top = treeTop; + } else { + top = + rootDistance + treeTop - NODE_HEIGHT + (2.0 * NODE_HEIGHT) + / (treeHeight + NODE_HEIGHT) * (treeHeight - rootDistance); + } + } + int N = children.size(); + double currentTop = treeTop; + for (int i = 0; i < N; i++) { + DrawableViewNode child = children.get(i); + child.place(currentTop, rootDistance); + currentTop += child.treeHeight + child.bottomSpacing; + rootDistance -= child.treeHeight + child.bottomSpacing; + } + } + + public DrawableViewNode getSelected(double x, double y) { + if (x >= left && x < left + NODE_WIDTH && y >= top && y <= top + NODE_HEIGHT) { + return this; + } + int N = children.size(); + for (int i = 0; i < N; i++) { + DrawableViewNode selected = children.get(i).getSelected(x, y); + if (selected != null) { + return selected; + } + } + return null; + } + + /* + * Moves the node the specified distance up. + */ + public void move(double distance) { + top -= distance; + + // Get the root + DrawableViewNode root = this; + while (root.parent != null) { + root = root.parent; + } + + // Figure out the new tree top. + double treeTop; + if (top + NODE_HEIGHT <= root.top) { + treeTop = top + NODE_HEIGHT - treeHeight; + } else if (top >= root.top + NODE_HEIGHT) { + treeTop = top; + } else { + if (leaf) { + treeTop = top; + } else { + double distanceRatio = 1 - (root.top + NODE_HEIGHT - top) / (2.0 * NODE_HEIGHT); + treeTop = root.top - treeHeight + distanceRatio * (treeHeight + NODE_HEIGHT); + } + } + // Go up the tree and figure out the tree top. + DrawableViewNode node = this; + while (node.parent != null) { + int index = node.viewNode.index; + for (int i = 0; i < index; i++) { + DrawableViewNode sibling = node.parent.children.get(i); + treeTop -= sibling.treeHeight + sibling.bottomSpacing; + } + node = node.parent; + } + + // Update the bounds. + root.bounds.y = Math.min(root.top - PADDING, treeTop - PADDING); + root.bounds.height = + Math.max(treeTop + root.treeHeight + PADDING, root.top + NODE_HEIGHT + PADDING) + - root.bounds.y; + // Place all the children of the root + double currentTop = treeTop; + int N = root.children.size(); + for (int i = 0; i < N; i++) { + DrawableViewNode child = root.children.get(i); + child.place(currentTop, root.top - currentTop); + currentTop += child.treeHeight + child.bottomSpacing; + } + + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/PsdFile.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/PsdFile.java new file mode 100644 index 00000000..3d4ac04b --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/PsdFile.java @@ -0,0 +1,508 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.ui.util; + +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.image.BufferedImage; +import java.io.BufferedOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.List; + +/** + * Writes PSD file. Supports only 8 bits, RGB images with 4 channels. + */ +public class PsdFile { + private final Header mHeader; + + private final ColorMode mColorMode; + + private final ImageResources mImageResources; + + private final LayersMasksInfo mLayersMasksInfo; + + private final LayersInfo mLayersInfo; + + private final BufferedImage mMergedImage; + + private final Graphics2D mGraphics; + + public PsdFile(int width, int height) { + mHeader = new Header(width, height); + mColorMode = new ColorMode(); + mImageResources = new ImageResources(); + mLayersMasksInfo = new LayersMasksInfo(); + mLayersInfo = new LayersInfo(); + + mMergedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + mGraphics = mMergedImage.createGraphics(); + } + + public void addLayer(String name, BufferedImage image, Point offset) { + addLayer(name, image, offset, true); + } + + public void addLayer(String name, BufferedImage image, Point offset, boolean visible) { + mLayersInfo.addLayer(name, image, offset, visible); + if (visible) + mGraphics.drawImage(image, null, offset.x, offset.y); + } + + public void write(OutputStream stream) { + mLayersMasksInfo.setLayersInfo(mLayersInfo); + + DataOutputStream out = new DataOutputStream(new BufferedOutputStream(stream)); + try { + mHeader.write(out); + out.flush(); + + mColorMode.write(out); + mImageResources.write(out); + mLayersMasksInfo.write(out); + mLayersInfo.write(out); + out.flush(); + + mLayersInfo.writeImageData(out); + out.flush(); + + writeImage(mMergedImage, out, false); + out.flush(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + out.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + private static void writeImage(BufferedImage image, DataOutputStream out, boolean split) + throws IOException { + + if (!split) + out.writeShort(0); + + int width = image.getWidth(); + int height = image.getHeight(); + + final int length = width * height; + int[] pixels = new int[length]; + + image.getData().getDataElements(0, 0, width, height, pixels); + + byte[] a = new byte[length]; + byte[] r = new byte[length]; + byte[] g = new byte[length]; + byte[] b = new byte[length]; + + for (int i = 0; i < length; i++) { + final int pixel = pixels[i]; + a[i] = (byte) ((pixel >> 24) & 0xFF); + r[i] = (byte) ((pixel >> 16) & 0xFF); + g[i] = (byte) ((pixel >> 8) & 0xFF); + b[i] = (byte) (pixel & 0xFF); + } + + if (split) + out.writeShort(0); + if (split) + out.write(a); + if (split) + out.writeShort(0); + out.write(r); + if (split) + out.writeShort(0); + out.write(g); + if (split) + out.writeShort(0); + out.write(b); + if (!split) + out.write(a); + } + + @SuppressWarnings( { + "UnusedDeclaration" + }) + static class Header { + static final short MODE_BITMAP = 0; + + static final short MODE_GRAYSCALE = 1; + + static final short MODE_INDEXED = 2; + + static final short MODE_RGB = 3; + + static final short MODE_CMYK = 4; + + static final short MODE_MULTI_CHANNEL = 7; + + static final short MODE_DUOTONE = 8; + + static final short MODE_LAB = 9; + + final byte[] mSignature = "8BPS".getBytes(); //$NON-NLS-1$ + + final short mVersion = 1; + + final byte[] mReserved = new byte[6]; + + final short mChannelCount = 4; + + final int mHeight; + + final int mWidth; + + final short mDepth = 8; + + final short mMode = MODE_RGB; + + Header(int width, int height) { + mWidth = width; + mHeight = height; + } + + void write(DataOutputStream out) throws IOException { + out.write(mSignature); + out.writeShort(mVersion); + out.write(mReserved); + out.writeShort(mChannelCount); + out.writeInt(mHeight); + out.writeInt(mWidth); + out.writeShort(mDepth); + out.writeShort(mMode); + } + } + + // Unused at the moment + @SuppressWarnings( { + "UnusedDeclaration" + }) + static class ColorMode { + final int mLength = 0; + + void write(DataOutputStream out) throws IOException { + out.writeInt(mLength); + } + } + + // Unused at the moment + @SuppressWarnings( { + "UnusedDeclaration" + }) + static class ImageResources { + static final short RESOURCE_RESOLUTION_INFO = 0x03ED; + + int mLength = 0; + + final byte[] mSignature = "8BIM".getBytes(); //$NON-NLS-1$ + + final short mResourceId = RESOURCE_RESOLUTION_INFO; + + final short mPad = 0; + + final int mDataLength = 16; + + final short mHorizontalDisplayUnit = 0x48; // 72 dpi + + final int mHorizontalResolution = 1; + + final short mWidthDisplayUnit = 1; + + final short mVerticalDisplayUnit = 0x48; // 72 dpi + + final int mVerticalResolution = 1; + + final short mHeightDisplayUnit = 1; + + ImageResources() { + mLength = mSignature.length; + mLength += 2; + mLength += 2; + mLength += 4; + mLength += 8; + mLength += 8; + } + + void write(DataOutputStream out) throws IOException { + out.writeInt(mLength); + out.write(mSignature); + out.writeShort(mResourceId); + out.writeShort(mPad); + out.writeInt(mDataLength); + out.writeShort(mHorizontalDisplayUnit); + out.writeInt(mHorizontalResolution); + out.writeShort(mWidthDisplayUnit); + out.writeShort(mVerticalDisplayUnit); + out.writeInt(mVerticalResolution); + out.writeShort(mHeightDisplayUnit); + } + } + + @SuppressWarnings( { + "UnusedDeclaration" + }) + static class LayersMasksInfo { + int mMiscLength; + + int mLayerInfoLength; + + void setLayersInfo(LayersInfo layersInfo) { + mLayerInfoLength = layersInfo.getLength(); + // Round to the next multiple of 2 + if ((mLayerInfoLength & 0x1) == 0x1) + mLayerInfoLength++; + mMiscLength = mLayerInfoLength + 8; + } + + void write(DataOutputStream out) throws IOException { + out.writeInt(mMiscLength); + out.writeInt(mLayerInfoLength); + } + } + + @SuppressWarnings( { + "UnusedDeclaration" + }) + static class LayersInfo { + final List mLayers = new ArrayList(); + + void addLayer(String name, BufferedImage image, Point offset, boolean visible) { + mLayers.add(new Layer(name, image, offset, visible)); + } + + int getLength() { + int length = 2; + for (Layer layer : mLayers) { + length += layer.getLength(); + } + return length; + } + + void write(DataOutputStream out) throws IOException { + out.writeShort((short) -mLayers.size()); + for (Layer layer : mLayers) { + layer.write(out); + } + } + + void writeImageData(DataOutputStream out) throws IOException { + for (Layer layer : mLayers) { + layer.writeImageData(out); + } + // Global layer mask info length + out.writeInt(0); + } + } + + @SuppressWarnings( { + "UnusedDeclaration" + }) + static class Layer { + static final byte OPACITY_TRANSPARENT = 0x0; + + static final byte OPACITY_OPAQUE = (byte) 0xFF; + + static final byte CLIPPING_BASE = 0x0; + + static final byte CLIPPING_NON_BASE = 0x1; + + static final byte FLAG_TRANSPARENCY_PROTECTED = 0x1; + + static final byte FLAG_INVISIBLE = 0x2; + + final int mTop; + + final int mLeft; + + final int mBottom; + + final int mRight; + + final short mChannelCount = 4; + + final Channel[] mChannelInfo = new Channel[mChannelCount]; + + final byte[] mBlendSignature = "8BIM".getBytes(); //$NON-NLS-1$ + + final byte[] mBlendMode = "norm".getBytes(); //$NON-NLS-1$ + + final byte mOpacity = OPACITY_OPAQUE; + + final byte mClipping = CLIPPING_BASE; + + byte mFlags = 0x0; + + final byte mFiller = 0x0; + + int mExtraSize = 4 + 4; + + final int mMaskDataLength = 0; + + final int mBlendRangeDataLength = 0; + + final byte[] mName; + + final byte[] mLayerExtraSignature = "8BIM".getBytes(); //$NON-NLS-1$ + + final byte[] mLayerExtraKey = "luni".getBytes(); //$NON-NLS-1$ + + int mLayerExtraLength; + + final String mOriginalName; + + private BufferedImage mImage; + + Layer(String name, BufferedImage image, Point offset, boolean visible) { + final int height = image.getHeight(); + final int width = image.getWidth(); + final int length = width * height; + + mChannelInfo[0] = new Channel(Channel.ID_ALPHA, length); + mChannelInfo[1] = new Channel(Channel.ID_RED, length); + mChannelInfo[2] = new Channel(Channel.ID_GREEN, length); + mChannelInfo[3] = new Channel(Channel.ID_BLUE, length); + + mTop = offset.y; + mLeft = offset.x; + mBottom = offset.y + height; + mRight = offset.x + width; + + mOriginalName = name; + byte[] data = name.getBytes(); + + try { + mLayerExtraLength = 4 + mOriginalName.getBytes("UTF-16").length; //$NON-NLS-1$ + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + + final byte[] nameData = new byte[data.length + 1]; + nameData[0] = (byte) (data.length & 0xFF); + System.arraycopy(data, 0, nameData, 1, data.length); + + // This could be done in the same pass as above + if (nameData.length % 4 != 0) { + data = new byte[nameData.length + 4 - (nameData.length % 4)]; + System.arraycopy(nameData, 0, data, 0, nameData.length); + mName = data; + } else { + mName = nameData; + } + mExtraSize += mName.length; + mExtraSize += + mLayerExtraLength + 4 + mLayerExtraKey.length + mLayerExtraSignature.length; + + mImage = image; + + if (!visible) { + mFlags |= FLAG_INVISIBLE; + } + } + + int getLength() { + int length = 4 * 4 + 2; + + for (Channel channel : mChannelInfo) { + length += channel.getLength(); + } + + length += mBlendSignature.length; + length += mBlendMode.length; + length += 4; + length += 4; + length += mExtraSize; + + return length; + } + + void write(DataOutputStream out) throws IOException { + out.writeInt(mTop); + out.writeInt(mLeft); + out.writeInt(mBottom); + out.writeInt(mRight); + + out.writeShort(mChannelCount); + for (Channel channel : mChannelInfo) { + channel.write(out); + } + + out.write(mBlendSignature); + out.write(mBlendMode); + + out.write(mOpacity); + out.write(mClipping); + out.write(mFlags); + out.write(mFiller); + + out.writeInt(mExtraSize); + out.writeInt(mMaskDataLength); + + out.writeInt(mBlendRangeDataLength); + + out.write(mName); + + out.write(mLayerExtraSignature); + out.write(mLayerExtraKey); + out.writeInt(mLayerExtraLength); + out.writeInt(mOriginalName.length() + 1); + out.write(mOriginalName.getBytes("UTF-16")); //$NON-NLS-1$ + } + + void writeImageData(DataOutputStream out) throws IOException { + writeImage(mImage, out, true); + } + } + + @SuppressWarnings( { + "UnusedDeclaration" + }) + static class Channel { + static final short ID_RED = 0; + + static final short ID_GREEN = 1; + + static final short ID_BLUE = 2; + + static final short ID_ALPHA = -1; + + static final short ID_LAYER_MASK = -2; + + final short mId; + + final int mDataLength; + + Channel(short id, int dataLength) { + mId = id; + mDataLength = dataLength + 2; + } + + int getLength() { + return 2 + 4 + mDataLength; + } + + void write(DataOutputStream out) throws IOException { + out.writeShort(mId); + out.writeInt(mDataLength); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/TreeColumnResizer.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/TreeColumnResizer.java new file mode 100644 index 00000000..f1ea61a6 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/util/TreeColumnResizer.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2010 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.hierarchyviewerlib.ui.util; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.TreeColumn; + +public class TreeColumnResizer { + + private TreeColumn mColumn1; + + private TreeColumn mColumn2; + + private Composite mControl; + + private int mColumn1Width; + + private int mColumn2Width; + + private final static int MIN_COLUMN1_WIDTH = 18; + + private final static int MIN_COLUMN2_WIDTH = 3; + + public TreeColumnResizer(Composite control, TreeColumn column1, TreeColumn column2) { + this.mControl = control; + this.mColumn1 = column1; + this.mColumn2 = column2; + control.addListener(SWT.Resize, resizeListener); + column1.addListener(SWT.Resize, column1ResizeListener); + column2.setResizable(false); + } + + private Listener resizeListener = new Listener() { + @Override + public void handleEvent(Event e) { + if (mColumn1Width == 0 && mColumn2Width == 0) { + mColumn1Width = (mControl.getBounds().width - 18) / 2; + mColumn2Width = (mControl.getBounds().width - 18) / 2; + } else { + int dif = mControl.getBounds().width - 18 - (mColumn1Width + mColumn2Width); + int columnDif = Math.abs(mColumn1Width - mColumn2Width); + int mainColumnChange = Math.min(Math.abs(dif), columnDif); + int left = Math.max(0, Math.abs(dif) - columnDif); + if (dif < 0) { + if (mColumn1Width > mColumn2Width) { + mColumn1Width -= mainColumnChange; + } else { + mColumn2Width -= mainColumnChange; + } + mColumn1Width -= left / 2; + mColumn2Width -= left - left / 2; + } else { + if (mColumn1Width > mColumn2Width) { + mColumn2Width += mainColumnChange; + } else { + mColumn1Width += mainColumnChange; + } + mColumn1Width += left / 2; + mColumn2Width += left - left / 2; + } + } + mColumn1.removeListener(SWT.Resize, column1ResizeListener); + mColumn1.setWidth(mColumn1Width); + mColumn2.setWidth(mColumn2Width); + mColumn1.addListener(SWT.Resize, column1ResizeListener); + } + }; + + private Listener column1ResizeListener = new Listener() { + @Override + public void handleEvent(Event e) { + int widthDif = mColumn1Width - mColumn1.getWidth(); + mColumn1Width -= widthDif; + mColumn2Width += widthDif; + boolean column1Changed = false; + + // Strange, but these constants make the columns look the same. + + if (mColumn1Width < MIN_COLUMN1_WIDTH) { + mColumn2Width -= MIN_COLUMN1_WIDTH - mColumn1Width; + mColumn1Width += MIN_COLUMN1_WIDTH - mColumn1Width; + column1Changed = true; + } + if (mColumn2Width < MIN_COLUMN2_WIDTH) { + mColumn1Width += mColumn2Width - MIN_COLUMN2_WIDTH; + mColumn2Width = MIN_COLUMN2_WIDTH; + column1Changed = true; + } + if (column1Changed) { + mColumn1.removeListener(SWT.Resize, this); + mColumn1.setWidth(mColumn1Width); + mColumn1.addListener(SWT.Resize, this); + } + mColumn2.setWidth(mColumn2Width); + } + }; +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/auto-refresh.png b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/auto-refresh.png new file mode 100644 index 0000000000000000000000000000000000000000..240862f3a1fbaccbc809e0bac8e2574ce4d64ecc GIT binary patch literal 541 zcmV+&0^Px#32;bRa{vGh*8l(w*8xH(n|J^K00(qQO+^RV2pAV36(Wb_r2qf{uSrBfR5;76 zlfO$FVI0Ok@0ngr`^PL>U6I!6@AYtIK`oW ziY&wouUCtt_58%pE!XDatc;ZYFcT5h{0CrpZI{%diRXFD#bSV*DDvamuDU+e!iJGZ zW=ZRLq}QiXIpF8jFZ`Z_Kp+Ec+{K0lA$u&Es%vGnRCBKRwyxIiv4USl*GVK9&s zKYrld`z_w)ay*I0@nW<&dO6ol#xa(ee*b0 few1Z;=sMp5=}EeVxn5NO00000NkvXXu0mjf(<<(@ literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/capture-psd.png b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/capture-psd.png new file mode 100644 index 0000000000000000000000000000000000000000..0f25426b7fc1ab1243c706ce71dd0e8dbb071a43 GIT binary patch literal 339 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|I14-?i-C%- zgD|6$#_S59AbW|YuPgg44heBSPL)}W?LeWYo-U3d7N?U_W}MkDvr%Ni=?C3sQWFz^ z;6us`&neH|^1OWEq*HKhiWGkn+nuxt$q5;L%>@$a3#ASmIIuub=d0NKfBkFJ>tFpz zs@PU--Yl|=dx68xtHqizY=3_IXg~eysguu5)wTdL3$1-96I?=c=nHEw`28l zE;-%5zh~0lg9)NvW+vXXic@3bI%r_fz&g(%&-g@bitW$O$B)~`^h-+q7tCj{$uwL3 z5@=1@-=eu(%eP!8zZAp3y1U%izopr0AKKm4*&oF literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/device-view-selected.png b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/device-view-selected.png new file mode 100644 index 0000000000000000000000000000000000000000..fd107ed9b518a7afd04586b807a972ad25b9b3f5 GIT binary patch literal 254 zcmeAS@N?(olHy`uVBq!ia0vp^qChOf!3HF^2u;-kQjEnx?oJHr&dIz4a@dl*-CY>| zgW!U_%O?XxI14-?iy0WWg+Z8+Vb&Z8pdfpRr>`sfEe;`39d1VXNsT}u$r9Iy66gHf z+|;}h2Ir#G#FEq$h4Rdj3kEJrSJ^(FY@O1TaS?83{1OTVKP&WVo literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/device-view.png b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/device-view.png new file mode 100644 index 0000000000000000000000000000000000000000..9a7eed49380eca312a7539036e49dd3899c368de GIT binary patch literal 228 zcmeAS@N?(olHy`uVBq!ia0vp^qChOf!3HF^2u;-kQjEnx?oJHr&dIz4a@dl*-CY>| zgW!U_%O?XxI14-?iy0WWg+Z8+Vb&Z8pdfpRr>`sfEe;`39f{V3m*as#k|nMYCC>S| zxv6<249-QVi6yBi3gww484B*6z5(HleBwYw_MR?|Ar_~T6C|`61dr_~fB&zZ4G2_K zUEPX}H~)V!tN7CuO}1qVpV#IbID2*KlY|>vtG}=K?k@a4<4u16Q2!^Px#32;bRa{vGh*8l(w*8xH(n|J^K00(qQO+^RV2pAR|7&jcdkpKV#L`g(JR5;6R zlgn>ZbpVCGJ2SVBnYQW6q1Y$1Q$Z6$)ELBPL=!<&G-(2Af;){7OvLCvpe{ty7!qBu z&?Sk|1V}W-GJsftMJt8!khZp^(@xuY+_`h#_x|qvxiAuge7lpJoYi*}?!xd7Kke=B zAAGC1G1ch@fxo8fv**udzdZc$feUwbmH&p%PF&p8)4l$yuIALnzLq364A|5tiA2E2 zyd$pMn8_|qPQ05vbYS*>1H-@F{BT2W?W)NaVF)ShR9Gj}FLDNcTD@(c6 z@n2uRcKD-n0C9jXe*R;3X7IjmG%;Iy%Rh=2G-8JeW?+*q9%4D1z_e>{%{t2FQq!V8 z@@ndy!Q;;Lo5h%vQt9mL9A5E8YufeGB=t4s!mla$SMZf~Ea8)xKF;;Z`{?ExMzw?= z<(j>o{0Kno(_`5^iBwBpC7febTVOg_CGKrSRi45Xl0qWjceR9W=h5{%ikD)Ydl(z8QR#Nr*up)(`eU&BuB(`BvXTFB?8;D#R>4m6?mV}yV z`yeLcYDdYQVojcBSJR`U6KNa)PNWdYev*|vbQKhW;snAi5cv*)vx@27LdM1YF<(R? z?~anGOY>0MMr`pCmLK4tV%ZL<*gh^NU!iO90t=Oc1ojG5a}?o4M3GvPFkELoUb~f| ze~hAIU@F}>q2$`~+oJ0^8|Cxm`HlxdDOG z$+&lhiDXDoB=N#_94AM@Yy&4m7+NVbmUnykMeVlAFC00$Q}jIWtsRoKD-urCQhDS-RloNMRZ})lC25st^~%J` zOCqe6$16*Jy{wO)x_H+&{!3=((3Z!ae0G0BojTyzwk#A1vy<8E=X00G{=BpMHx=mF U8M{`f4FCWD07*qoM6N<$g1uD8KmY&$ literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/filtered.png b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/filtered.png new file mode 100644 index 0000000000000000000000000000000000000000..4fcab3f3abc6199764ffab0ad0c59ef636399c24 GIT binary patch literal 9242 zcmV+#B<0(QP)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2igc2 z0U#c<<_CX>@2HM@dakSAh-}001BWNklj_b(zMzW1GvzMH@N`Okj(+5%mL8SlM__{PKi=18q6c(4f) zWEAsTg!`sF)eI=%kLd0QYby*hUB?YF%XbS>KUc<@gXiLZI+OHL1wm!o1#n5jbe(*NV>Lf?9&v6mN3RE9(6s z41_m$?|1Su1OqOZ8^>gV)(`>dYnzr~Os^|H(|q8ok3#)hE!6MOSDu9Vz2Y-J`oBT{ z*4J;Zt@^r%hyd*qqWIG&{`URvT<)$%Xyj+xJ)Thf(7J^_(H*HudXn4HiGAkhSeMzx-YA zJ;ZwV#U`^&pdYn!L_~b?-ot%j7P=6w$Ismcw90FBBdvUt%0A{Ysp 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 literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/inspect-screenshot.png b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/inspect-screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..6e517012b273c0994c28447f3f76a11cf2005dd2 GIT binary patch literal 412 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|I14-?iy0WW zg+Z8+Vb&Z8pdfpRr>`sfEe;8B3HJNDB*TG1k|nMYCC>S|xv6<249-QVi6yBi3gww4 z84B*6z5(HleBwZZIXzt*Lo7}wCoB*!P%19d5jRUqNGM21NO;i5H8(XO!Qz47&u`q0 z%s`oMK&}T+roh=aL6VvIuv7nu?fHdL3|n`8XHMVnHE8J?u^A~Z9@p|V%}?8(mRb>d z>cD{wVza$xEGYTqevY9!F?_9*|U!2nwdycS$Id~kY&uKH5 sB=qP8Poj$HtqZ)0ma+-l9$gF!Q%b^IxC$4g0Yi?#)78&qol`;+0EEw&X#fBK literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/invalidate.png b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/invalidate.png new file mode 100644 index 0000000000000000000000000000000000000000..ee75f695cb9cc5645d56ffd7361c551dbc7df14e GIT binary patch literal 391 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|*pj^6T^Rm@ z;DWu&Cj&(|3p^r=85p>QL70(Y)*K0-AbW|YuPgg44q;YSL79aHtUw{j64!_l=ltB< z)VvY~=c3falGGH1^30M91$R&1fbd2>aiF3{o-U3d7N?g^-Y9s;fX8*dF@x+BORigM zCTM0zG}#(8lrP|rR8qbWd~xa0rw=Z7%rRgq`tZKQ?)0VYI<@RV9M=x8MKBr{&u>)T zz`a48-BGop$+19GW8><%u8vc^-?2{jNy-xMUTfp%Gb!3pH)7V+XP;FHYc@}vwwrge zLIj6Yxf#bb4y*6azA>4;j)4FG literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/load-all-views.png b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/load-all-views.png new file mode 100644 index 0000000000000000000000000000000000000000..3329ec9d94478202b534b64b30eea37ca2ac1f13 GIT binary patch literal 728 zcmV;}0w?{6P)Px#32;bRa{vGh*8l(w*8xH(n|J^K00(qQO+^RV2pAR`7KZ*Y5C8xIYDq*vR5;6( zlU+ztaTvyb=WL^8ZmBWKV(Nw3MUiAw1VuzelwBC1p^z7O<3$>XLeYgIx~X6kg-~o1 z1bqpHL=cgpQXrO=N@<|wSL$4IA7^J9?i^1Sa_pGwqUY}a^5@~l^FFVDLAPFXxU!~8 zn~byh>}Qo%Do#Bmg#-YX%Vl=j-AMpv*(@V56AsG~zN87;YX<<;^${VZf!A%iUQv0z?vr(MfDZl2|w6 zvFFHII)c>0+|)&8{2hct3jo;894!=kv)ihQyUwAL*xDPHNtUMYVI$34LogAbPaI}C z9zzOQ5EC|Eqh6-Cy2!3NHLZ5>NNt&M&^9ABxAEgrT)JL0GDd}1tZDJsUgB@24mqz z3`~?04h>?qUL{kjM~lNI_2l0s8g=Jxv6h}3rSrfPAyr{y_&5i@jM9(9m}xti$@3@+ z0j}@&(9pUsSNzvNCy_NP(YTpKv69xj-OP%65Ee6thrgmoK@A}I{@tXI>dN%mw%A`` zkps;9UO@KG(O7tM)p4aX0H|;^?>c6`JYbyN>pHvRbKApj`uh)?^E+LeARuA@0000< KMNUMnLSTYKAw>`X literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/load-overlay.png b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/load-overlay.png new file mode 100644 index 0000000000000000000000000000000000000000..48172525abf97a05bfaca20add0298f3555505e3 GIT binary patch literal 549 zcmV+=0^0qFP)Px#32;bRa{vGf6951U69E94oEQKA00(qQO+^RV2pAU?E-0q|b^rhYw@E}nR4C7l zkxOe6Q2<8I+?h<$X+m18F{N0ky0D;C!By6Z5=6*G7p}VTcL;)eQU8HK6hvJJE<{j@ zX$y)k8cgaVq>arqY11&x%p`N~bs@xE>sg=0$N7w6v8Y?k4QwG?8g<*(c82gL9@1wQeS2SwnvNjrxzoVr>#BH>cXr(ZK$oDamDXea_%Ew!` zu?yG8Ud&@h0gDeFbKuPjL^J@U7&~&;DOr!ri>e1VZfV)*qR*D=YbCC)Lr00000NkvXXu0mjfp%?Bd literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/load-view-hierarchy.png b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/load-view-hierarchy.png new file mode 100644 index 0000000000000000000000000000000000000000..8f01dda41cdb2a6000be0b37c9ae81fafd22e81a GIT binary patch literal 288 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|*pj^6T^Rm@ z;DWu&Cj&(|3p^r=85p>QL70(Y)*K0-AbW|YuPgg44k0leu97$zKcJ9giEBiObAE1a zYF-J0b5UwyNotBhd1gt5g1e`0KzJjcI8afwr;B5V#p$J!Vz~||@H9`pxcQdx&eOZv zb0fMpm5H%%{`->cv}Em6a45aff9C!wul1gCjXZV+o7R<7E!-@2xL&HK z=_&L3gR@Pq9aP@4+TS|rQTwF1jcaVS*WF%M!)GUC?kru}`RlJT`^K6ji)@NJcFQJg Z*JFxwD__R;;407=44$rjF6*2Ung9m>Wa|I` literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/not-selected.png b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/not-selected.png new file mode 100644 index 0000000000000000000000000000000000000000..db6f13b49d23a63b660d03316f4093100aa29c67 GIT binary patch literal 12468 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 literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/on-white.png b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/on-white.png new file mode 100644 index 0000000000000000000000000000000000000000..5f05662cf7d57e2d0baba0d343ff2036c6c2beb8 GIT binary patch literal 158 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|*pj^6T^Rm@ z;DWu&Cj&(|3p^r=85p>QL70(Y)*K0-AbW|YuPgg44q+BO^PO@^%YZ_1o-U3d7N?W{ u{Qv*Io{ft^n7K9KN<$#C8?S^8(6;PTEHb}u%sviO$>8bg=d#Wzp$PzEGbwxk literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/picker.png b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/picker.png new file mode 100644 index 0000000000000000000000000000000000000000..8ea2bed63da53d611b6aac434da7adda73f3f0b4 GIT binary patch literal 370 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|*pj^6T^Rm@ z;DWu&Cj&(|3p^r=f!eQwFr$;k>HXCpL|4;jNj&a#5hDm(pY7@3Zx9y0i+4A>S$x~mMBfk$`uWEUl z!{DCU@o$cNh1I)VT-(nbe7mY@o1Ew?_0_K&R`XnR_|be;@W~ox+XoNlN4Wh>vU(>~ z;nO9%z@zeF%LBd_%l`;fu(>(tSKpme`yi)s?zgC|>lHHXm*!96@>FiPnNpGSd$*_Z z++{zP@f7myIiq&i_V)W;Db5{nn_@yt{bL2s&zAYaoBMliJ%hGS)X`_FY)gT@XYh3O Kb6Mw<&;$U5HkUX6 literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/pixel-perfect-view-selected.png b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/pixel-perfect-view-selected.png new file mode 100644 index 0000000000000000000000000000000000000000..1e44000fd6ca122b6bd1d178ee87af4b39e3b794 GIT binary patch literal 734 zcmV<40wMj0P)4Tx0C=30lFMroQ51*2nNV6#gVa#ag$(#WG3wAL?Q+^Y+S;KGd6*`K{;WBNR$O%D5P30zxSsxbU6Bx#yhw zI~O8*729r#K+AHS?9`+*Un)z3TMTfF7?+SLddHs7G?fEw_bURxr^^-FZm!?j{I&n^ z`J;iiKcc4>mFr95=YiN=*LFdu12Mf`F+f-Z;!ZxB1Yre; z)_i^gM5{i30HQs;=7O*V$d=KtKpX9cVBoRm z?G$0~BJ6K^-tRTf+h2yrH+Z+Jcb(o}y#!$c4)gst`#yYh13HL(T>y|_WeIY?H3eQM zaQ+Dle+Ny0yn>iW1jpCV-(xDRstH*hD`vFmdCn<$H`;^zFoPY1#DV{Tv)%Usr_D)VZPZ(=lV7C#ekh| zwh-{0_H3AMn5l3!t2r0eY`8Pw{G>>uqLM&HW{e^kG^R;{|1SZKuYxi%HB~CFUp=$* zuU4*k&jm=f?fXunR(GWd+ise1X~xp8#HCSL9_J7HU9?+ZuY%G5000SaNLh0L01FcU z01FcV0GgZ_00010Nkl%%#yEZE%ztDFQxg+La>Q9+0)PMhW%&5R>cd5MVS?h!!7#kwSF<0E^0f$OCCA QYXATM07*qoM6N<$g3FCTng9R* literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/pixel-perfect-view.png b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/pixel-perfect-view.png new file mode 100644 index 0000000000000000000000000000000000000000..ec51cec03fce80c2a8f008de0b24a1cb5624f212 GIT binary patch literal 733 zcmV<30wVp1P)4Tx0C=30lFMroQ51*2nNV6#gVa#ag$(#WG3wAL?Q+^Y+S;KGd6*`K{;WBNR$O%D5P30zxSsxbU6Bx#yhw zI~O8*729r#K+AHS?9`+*Un)z3TMTfF7?+SLddHs7G?fEw_bURxr^^-FZm!?j{I&n^ z`J;iiKcc4>mFr95=YiN=*LFdu12Mf`F+f-Z;!ZxB1Yre; z)_i^gM5{i30HQs;=7O*V$d=KtKpX9cVBoRm z?G$0~BJ6K^-tRTf+h2yrH+Z+Jcb(o}y#!$c4)gst`#yYh13HL(T>y|_WeIY?H3eQM zaQ+Dle+Ny0yn>iW1jpCV-(xDRstH*hD`vFmdCn<$H`;^zFoPY1#DV{Tv)%Usr_D)VZPZ(=lV7C#ekh| zwh-{0_H3AMn5l3!t2r0eY`8Pw{G>>uqLM&HW{e^kG^R;{|1SZKuYxi%HB~CFUp=$* zuU4*k&jm=f?fXunR(GWd+ise1X~xp8#HCSL9_J7HU9?+ZuY%G5000SaNLh0L01FcU z01FcV0GgZ_0000~Nkl%%#xOB9{*Nqi=ImKUa>Q9+0?f?J4E+533=9km z3?Dy!WFS|Zi5zd?18^_jyLTVO3rb3gjO2(@9gIc_0*poq(c&X8Qm7698hT@Dl_V-* P00000NkvXXu0mjf)crx* literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/profile.png b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/profile.png new file mode 100644 index 0000000000000000000000000000000000000000..1e9fb5ab3837f2998ea00157e328fc28c6c2ed6a GIT binary patch literal 597 zcmV-b0;>IqP)gYjzPL2){0orYCqBjg2f@#r25{w$3cThOy1(1 z?!D*yf9HP=_lm0WKg+3Y;BW_BOMqB@I#?bfi~v###LHbATkR*m?eyNLFuS@csfsUWUr|pRHVf^CSuk z0PNg|ky<*8V?*_|C^?7t7V2iN`)4l7Tye!_3_xM{>Sa`hfl~i!qHWI~w+vEp4g$>j z#-2?*RZr?);%Mg49xE)jB>=nJxv3NG_@%y_x&WqeaJDl@46xSGTdaxy0hFupdEiG7 zKvZVy&kcZFuFqAt7NDPx#3B#d0sx|&)d_^V&G!lmM$EZvHnrL4sLgjC$vj9~W749; jfSE{z>1^&)r8(~x<#n#VMNPX?00000NkvXXu0mjf>s$>4 literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/red.png b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/red.png new file mode 100644 index 0000000000000000000000000000000000000000..a2ab85549405c348b0fc19aa4b6eeac65d046a95 GIT binary patch literal 383 zcmeAS@N?(olHy`uVBq!ia0vp^{2QL70(Y)*K0-AbW|YuPgg44iQmhw!5)w^?^c?C9V-A&iT2y zsd*&~&PAz-C8;S2<(VZJ3hti10pX2&;y^{WJY5_^G|tbxw9xNxg2>U2@!e}?C`&r} zKD?-W!;`1~Aol`~iYc7)5;8t0oaULgz%8iYNm3qnW<#m%Nz?dG_TR0|mtP1jXcLxM zeCoNW+y7&=>FhQTFx2QG&@1?{xaDL>71&7`dJSwE#C7KJ$@#?!hUta zw&{}k2Cj)S_&x+aD_o|*8{Qgx@b9!a_0RM#r%qpc_nTqBawF~!c1^Wfn=Q|B{0Q7I z|7>QNMVRcF+mm!QSvo&Fop9+;XjtCXr|hLo?H@M>WEya@CpknaEh literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/refresh-windows.png b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/refresh-windows.png new file mode 100644 index 0000000000000000000000000000000000000000..8fddcaed7615fa0abd8a271b37c765e503176b4a GIT binary patch literal 872 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 literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/selected-small.png b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/selected-small.png new file mode 100644 index 0000000000000000000000000000000000000000..538e3853b48966f22c7663365c855b37d3eee4aa GIT binary patch literal 12611 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

+ * There is no magic value reserved for "missing ping id or invalid store". + * The only proper way to know if the ping id is missing is to use {@link #hasPingId()}. + */ + public long getPingId() { + PreferenceStore prefs = getPreferenceStore(); + synchronized (DdmsPreferenceStore.class) { + // Note: getLong() returns 0L if the ID is missing so we do that too when + // there's no store. + return prefs == null ? 0L : prefs.getLong(PING_ID); + } + } + + /** + * Generates a new random ping ID and saves it in the preference store. + * + * @return The new ping ID. + */ + public long generateNewPingId() { + PreferenceStore prefs = getPreferenceStore(); + + Random rnd = new Random(); + long id = rnd.nextLong(); + + synchronized (DdmsPreferenceStore.class) { + prefs.setValue(PING_ID, id); + try { + prefs.save(); + } catch (IOException e) { + /* ignore exceptions while saving preferences */ + } + } + + return id; + } + + /** + * Returns the "ping opt in" value from the preference store. + * This would be true if there's a valid preference store and + * the user opted for sending ping statistics. + */ + public boolean isPingOptIn() { + PreferenceStore prefs = getPreferenceStore(); + synchronized (DdmsPreferenceStore.class) { + return prefs != null && prefs.contains(PING_OPT_IN); + } + } + + /** + * Saves the "ping opt in" value in the preference store. + * + * @param optIn The new user opt-in value. + */ + public void setPingOptIn(boolean optIn) { + PreferenceStore prefs = getPreferenceStore(); + + synchronized (DdmsPreferenceStore.class) { + prefs.setValue(PING_OPT_IN, optIn); + try { + prefs.save(); + } catch (IOException e) { + /* ignore exceptions while saving preferences */ + } + } + } + + /** + * Retrieves the ping time for the given app from the preference store. + * Callers should use {@link System#currentTimeMillis()} for time stamps. + * + * @param app The app name identifier. + * @return 0L if we don't have a preference store or there was no time + * recorded in the store for the requested app. Otherwise the time stamp + * from the store. + */ + public long getPingTime(String app) { + PreferenceStore prefs = getPreferenceStore(); + String timePref = PING_TIME + "." + app; //$NON-NLS-1$ + synchronized (DdmsPreferenceStore.class) { + return prefs == null ? 0 : prefs.getLong(timePref); + } + } + + /** + * Sets the ping time for the given app from the preference store. + * Callers should use {@link System#currentTimeMillis()} for time stamps. + * + * @param app The app name identifier. + * @param timeStamp The time stamp from the store. + * 0L is a special value that should not be used. + */ + public void setPingTime(String app, long timeStamp) { + PreferenceStore prefs = getPreferenceStore(); + String timePref = PING_TIME + "." + app; //$NON-NLS-1$ + synchronized (DdmsPreferenceStore.class) { + prefs.setValue(timePref, timeStamp); + try { + prefs.save(); + } catch (IOException ioe) { + /* ignore exceptions while saving preferences */ + } + } + } + + /** + * True if this is the first time the users runs ADT, which is detected by + * the lack of the setting set using {@link #setAdtUsed(boolean)} + * or this value being set to true. + * + * @return true if ADT has been used before + * + * @see #setAdtUsed(boolean) + */ + public boolean isAdtUsed() { + PreferenceStore prefs = getPreferenceStore(); + synchronized (DdmsPreferenceStore.class) { + if (prefs == null || !prefs.contains(ADT_USED)) { + return false; + } + return prefs.getBoolean(ADT_USED); + } + } + + /** + * Sets whether the ADT startup wizard has been shown. + * ADT sets first to false once the welcome wizard has been shown once. + * + * @param used true if ADT has been used + */ + public void setAdtUsed(boolean used) { + PreferenceStore prefs = getPreferenceStore(); + synchronized (DdmsPreferenceStore.class) { + prefs.setValue(ADT_USED, used); + try { + prefs.save(); + } catch (IOException ioe) { + /* ignore exceptions while saving preferences */ + } + } + } + + /** + * Retrieves the last SDK OS path. + *

+ * This is just an information value, the path may not exist, may not + * even be on an existing file system and/or may not point to an SDK + * anymore. + * + * @return The last SDK OS path from the preference store, or null if + * there is no store or an empty string if it is not defined. + */ + public String getLastSdkPath() { + PreferenceStore prefs = getPreferenceStore(); + synchronized (DdmsPreferenceStore.class) { + return prefs == null ? null : prefs.getString(LAST_SDK_PATH); + } + } + + /** + * Sets the last SDK OS path. + * + * @param osSdkPath The SDK OS Path. Can be null or empty. + */ + public void setLastSdkPath(String osSdkPath) { + PreferenceStore prefs = getPreferenceStore(); + synchronized (DdmsPreferenceStore.class) { + prefs.setValue(LAST_SDK_PATH, osSdkPath); + try { + prefs.save(); + } catch (IOException ioe) { + /* ignore exceptions while saving preferences */ + } + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkstats/src/main/java/com/android/sdkstats/SdkStatsPermissionDialog.java b/andmore-swt/org.eclipse.andmore.sdkstats/src/main/java/com/android/sdkstats/SdkStatsPermissionDialog.java new file mode 100644 index 00000000..3495122c --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/src/main/java/com/android/sdkstats/SdkStatsPermissionDialog.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2011 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.sdkstats; + +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.SWT; +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.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.program.Program; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Link; +import org.eclipse.swt.widgets.Shell; + +import java.io.IOException; + +/** + * Dialog to get user permission for ping service. + */ +public class SdkStatsPermissionDialog extends Dialog { + /* Text strings displayed in the opt-out dialog. */ + private static final String HEADER_TEXT = + "Thanks for using the Android SDK!"; + + /** Used in the ADT welcome wizard as well. */ + public static final String NOTICE_TEXT = + "We know you just want to get started but please read this first."; + + /** Used in the preference pane (PrefsDialog) as well. */ + public static final String BODY_TEXT = + "By choosing to send certain usage statistics to Google, you can " + + "help us improve the Android SDK. These usage statistics lets us " + + "measure things like active usage of the SDK, and let us know things " + + "like which versions of the SDK are in use and which tools are the " + + "most popular with developers. This limited data is not associated " + + "with personal information about you, and is examined on an aggregate " + + "basis, and is maintained in accordance with the Google Privacy Policy."; + + /** Used in the ADT welcome wizard as well. */ + public static final String PRIVACY_POLICY_LINK_TEXT = + "Google " + + "Privacy Policy"; + + /** Used in the preference pane (PrefsDialog) as well. */ + public static final String CHECKBOX_TEXT = + "Send usage statistics to Google."; + + /** Used in the ADT welcome wizard as well. */ + public static final String FOOTER_TEXT = + "If you later decide to change this setting, you can do so in the" + + "\"ddms\" tool under \"File\" > \"Preferences\" > \"Usage Stats\"."; + + private static final String BUTTON_TEXT = "Proceed"; + + /** List of Linux browser commands to try, in order (see openUrl). */ + private static final String[] LINUX_BROWSERS = new String[] { + "firefox -remote openurl(%URL%,new-window)", //$NON-NLS-1$ running FF + "mozilla -remote openurl(%URL%,new-window)", //$NON-NLS-1$ running Moz + "firefox %URL%", //$NON-NLS-1$ new FF + "mozilla %URL%", //$NON-NLS-1$ new Moz + "kfmclient openURL %URL%", //$NON-NLS-1$ Konqueror + "opera -newwindow %URL%", //$NON-NLS-1$ Opera + }; + + private static final boolean ALLOW_PING_DEFAULT = true; + private boolean mAllowPing = ALLOW_PING_DEFAULT; + + public SdkStatsPermissionDialog(Shell parentShell) { + super(parentShell); + setBlockOnOpen(true); + } + + @Override + protected void createButtonsForButtonBar(Composite parent) { + createButton(parent, Window.OK, BUTTON_TEXT, true); + } + + @Override + protected Control createDialogArea(Composite parent) { + Composite composite = (Composite) super.createDialogArea(parent); + composite.setLayout(new GridLayout(1, false)); + + final Label title = new Label(composite, SWT.CENTER | SWT.WRAP); + final FontData[] fontdata = title.getFont().getFontData(); + for (int i = 0; i < fontdata.length; i++) { + fontdata[i].setHeight(fontdata[i].getHeight() * 4 / 3); + } + title.setFont(new Font(getShell().getDisplay(), fontdata)); + title.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + title.setText(HEADER_TEXT); + + final Label notice = new Label(composite, SWT.WRAP); + notice.setFont(title.getFont()); + notice.setForeground(new Color(getShell().getDisplay(), 255, 0, 0)); + notice.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + notice.setText(NOTICE_TEXT); + notice.pack(); + + final Label bodyText = new Label(composite, SWT.WRAP); + GridData gd = new GridData(); + gd.widthHint = notice.getSize().x; // do not extend beyond the NOTICE text's width + gd.grabExcessHorizontalSpace = true; + bodyText.setLayoutData(gd); + bodyText.setText(BODY_TEXT); + + final Link privacyLink = new Link(composite, SWT.NO_FOCUS); + privacyLink.setText(PRIVACY_POLICY_LINK_TEXT); + privacyLink.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent event) { + openUrl(event.text); + } + }); + + final Button checkbox = new Button(composite, SWT.CHECK); + checkbox.setSelection(ALLOW_PING_DEFAULT); + checkbox.setText(CHECKBOX_TEXT); + checkbox.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent event) { + mAllowPing = checkbox.getSelection(); + } + }); + checkbox.setFocus(); + + final Label footer = new Label(composite, SWT.WRAP); + gd = new GridData(); + gd.widthHint = notice.getSize().x; + gd.grabExcessHorizontalSpace = true; + footer.setLayoutData(gd); + footer.setText(FOOTER_TEXT); + + return composite; + } + + /** + * Open a URL in an external browser. + * @param url to open - MUST be sanitized and properly formed! + */ + public static void openUrl(final String url) { + // TODO: consider using something like BrowserLauncher2 + // (http://browserlaunch2.sourceforge.net/) instead of these hacks. + + // SWT's Program.launch() should work on Mac, Windows, and GNOME + // (because the OS shell knows how to launch a default browser). + if (!Program.launch(url)) { + // Must be Linux non-GNOME (or something else broke). + // Try a few Linux browser commands in the background. + new Thread() { + @Override + public void run() { + for (String cmd : LINUX_BROWSERS) { + cmd = cmd.replaceAll("%URL%", url); //$NON-NLS-1$ + try { + Process proc = Runtime.getRuntime().exec(cmd); + if (proc.waitFor() == 0) break; // Success! + } catch (InterruptedException e) { + // Should never happen! + throw new RuntimeException(e); + } catch (IOException e) { + // Swallow the exception and try the next browser. + } + } + + // TODO: Pop up some sort of error here? + // (We're in a new thread; can't use the existing Display.) + } + }.start(); + } + } + + public boolean getPingUserPreference() { + return mAllowPing; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkstats/src/main/java/com/android/sdkstats/SdkStatsService.java b/andmore-swt/org.eclipse.andmore.sdkstats/src/main/java/com/android/sdkstats/SdkStatsService.java new file mode 100644 index 00000000..90ece909 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/src/main/java/com/android/sdkstats/SdkStatsService.java @@ -0,0 +1,558 @@ +/* + * 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.sdkstats; + +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLEncoder; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** Utility class to send "ping" usage reports to the server. */ +public class SdkStatsService { + + protected static final String SYS_PROP_OS_ARCH = "os.arch"; //$NON-NLS-1$ + protected static final String SYS_PROP_JAVA_VERSION = "java.version"; //$NON-NLS-1$ + protected static final String SYS_PROP_OS_VERSION = "os.version"; //$NON-NLS-1$ + protected static final String SYS_PROP_OS_NAME = "os.name"; //$NON-NLS-1$ + + /** Minimum interval between ping, in milliseconds. */ + private static final long PING_INTERVAL_MSEC = 86400 * 1000; // 1 day + + private static final boolean DEBUG = System.getenv("ANDROID_DEBUG_PING") != null; //$NON-NLS-1$ + + private DdmsPreferenceStore mStore = new DdmsPreferenceStore(); + + public SdkStatsService() { + } + + /** + * Send a "ping" to the Google toolbar server, if enough time has + * elapsed since the last ping, and if the user has not opted out. + *

+ * This is a simplified version of {@link #ping(String[])} that only + * sends an "application" name and a "version" string. See the explanation + * there for details. + * + * @param app The application name that reports the ping (e.g. "emulator" or "ddms".) + * Valid characters are a-zA-Z0-9 only. + * @param version The version string (e.g. "12" or "1.2.3.4", 4 groups max.) + * @see #ping(String[]) + */ + public void ping(String app, String version) { + doPing(app, version, null); + } + + /** + * Send a "ping" to the Google toolbar server, if enough time has + * elapsed since the last ping, and if the user has not opted out. + *

+ * The ping will not be sent if the user opt out dialog has not been shown yet. + * Use {@link #checkUserPermissionForPing(Shell)} to display the dialog requesting + * user permissions. + *

+ * Note: The actual ping (if any) is sent in a non-daemon background thread. + *

+ * The arguments are defined as follow: + *

    + *
  • Argument 0 is the "ping" command and is ignored.
  • + *
  • Argument 1 is the application name that reports the ping (e.g. "emulator" or "ddms".) + * Valid characters are a-zA-Z0-9 only.
  • + *
  • Argument 2 is the version string (e.g. "12" or "1.2.3.4", 4 groups max.)
  • + *
  • Arguments 3+ are optional and depend on the application name.
  • + *
  • "emulator" application currently has 3 optional arguments: + *
      + *
    • Arugment 3: android_gl_vendor
    • + *
    • Arugment 4: android_gl_renderer
    • + *
    • Arugment 5: android_gl_version
    • + *
    + *
  • + *
+ * + * @param arguments A non-empty non-null array of arguments to the ping as described above. + */ + public void ping(String[] arguments) { + if (arguments == null || arguments.length < 3) { + throw new IllegalArgumentException( + "Invalid ping arguments: expected ['ping', app, version] but got " + + (arguments == null ? "null" : Arrays.toString(arguments))); + } + int len = arguments.length; + String app = arguments[1]; + String version = arguments[2]; + + Map extras = new HashMap(); + + if ("emulator".equals(app)) { //$NON-NLS-1$ + if (len > 3) { + extras.put("glm", sanitizeGlArg(arguments[3])); //$NON-NLS-1$ vendor + } + if (len > 4) { + extras.put("glr", sanitizeGlArg(arguments[4])); //$NON-NLS-1$ renderer + } + if (len > 5) { + extras.put("glv", sanitizeGlArg(arguments[5])); //$NON-NLS-1$ version + } + } + + doPing(app, version, extras); + } + + private String sanitizeGlArg(String arg) { + if (arg == null) { + arg = ""; //$NON-NLS-1$ + } else { + try { + arg = arg.trim(); + arg = arg.replaceAll("[^A-Za-z0-9\\s_()./-]", " "); //$NON-NLS-1$ //$NON-NLS-2$ + arg = arg.replaceAll("\\s\\s+", " "); //$NON-NLS-1$ //$NON-NLS-2$ + + // Guard from arbitrarily long parameters + if (arg.length() > 128) { + arg = arg.substring(0, 128); + } + + arg = URLEncoder.encode(arg, "UTF-8"); //$NON-NLS-1$ + } catch (UnsupportedEncodingException e) { + arg = ""; //$NON-NLS-1$ + } + } + + return arg; + } + + /** + * Display a dialog to the user providing information about the ping service, + * and whether they'd like to opt-out of it. + * + * Once the dialog has been shown, it sets a preference internally indicating + * that the user has viewed this dialog. + */ + public void checkUserPermissionForPing(Shell parent) { + if (!mStore.hasPingId()) { + askUserPermissionForPing(parent); + mStore.generateNewPingId(); + } + } + + /** + * Prompt the user for whether they want to opt out of reporting, and save the user + * input in preferences. + */ + private void askUserPermissionForPing(final Shell parent) { + final Display display = parent.getDisplay(); + display.syncExec(new Runnable() { + @Override + public void run() { + SdkStatsPermissionDialog dialog = new SdkStatsPermissionDialog(parent); + dialog.open(); + mStore.setPingOptIn(dialog.getPingUserPreference()); + } + }); + } + + // ------- + + /** + * Pings the usage stats server, as long as the prefs contain the opt-in boolean + * + * @param app The application name that reports the ping (e.g. "emulator" or "ddms".) + * Will be normalized. Valid characters are a-zA-Z0-9 only. + * @param version The version string (e.g. "12" or "1.2.3.4", 4 groups max.) + * @param extras Extra key/value parameters to send. They are send as-is and must + * already be well suited and escaped using {@link URLEncoder#encode(String, String)}. + */ + protected void doPing(String app, String version, final Map extras) { + // Note: if you change the implementation here, you also need to change + // the overloaded SdkStatsServiceTest.doPing() used for testing. + + // Validate the application and version input. + final String nApp = normalizeAppName(app); + final String nVersion = normalizeVersion(version); + + // If the user has not opted in, do nothing and quietly return. + if (!mStore.isPingOptIn()) { + // user opted out. + return; + } + + // If the last ping *for this app* was too recent, do nothing. + long now = System.currentTimeMillis(); + long then = mStore.getPingTime(app); + if (now - then < PING_INTERVAL_MSEC) { + // too soon after a ping. + return; + } + + // Record the time of the attempt, whether or not it succeeds. + mStore.setPingTime(app, now); + + // Send the ping itself in the background (don't block if the + // network is down or slow or confused). + final long id = mStore.getPingId(); + new Thread() { + @Override + public void run() { + try { + URL url = createPingUrl(nApp, nVersion, id, extras); + actuallySendPing(url); + } catch (IOException e) { + e.printStackTrace(); + } + } + }.start(); + } + + + /** + * Unconditionally send a "ping" request to the server. + * + * @param url The URL to send to the server. + * * @throws IOException if the ping failed + */ + private void actuallySendPing(URL url) throws IOException { + assert url != null; + + if (DEBUG) { + System.err.println("Ping: " + url.toString()); //$NON-NLS-1$ + } + + // Discard the actual response, but make sure it reads OK + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + + // Believe it or not, a 404 response indicates success: + // the ping was logged, but no update is configured. + if (conn.getResponseCode() != HttpURLConnection.HTTP_OK && + conn.getResponseCode() != HttpURLConnection.HTTP_NOT_FOUND) { + throw new IOException( + conn.getResponseMessage() + ": " + url); //$NON-NLS-1$ + } + } + + /** + * Compute the ping URL to send the data to the server. + * + * @param app The application name that reports the ping (e.g. "emulator" or "ddms".) + * Valid characters are a-zA-Z0-9 only. + * @param version The version string already formatted as a 4 dotted group (e.g. "1.2.3.4".) + * @param id of the local installation + * @param extras Extra key/value parameters to send. They are send as-is and must + * already be well suited and escaped using {@link URLEncoder#encode(String, String)}. + */ + protected URL createPingUrl(String app, String version, long id, Map extras) + throws UnsupportedEncodingException, MalformedURLException { + + String osName = URLEncoder.encode(getOsName(), "UTF-8"); //$NON-NLS-1$ + String osArch = URLEncoder.encode(getOsArch(), "UTF-8"); //$NON-NLS-1$ + String jvmArch = URLEncoder.encode(getJvmInfo(), "UTF-8"); //$NON-NLS-1$ + + // Include the application's name as part of the as= value. + // Share the user ID for all apps, to allow unified activity reports. + + String extraStr = ""; //$NON-NLS-1$ + if (extras != null && !extras.isEmpty()) { + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : extras.entrySet()) { + sb.append('&').append(entry.getKey()).append('=').append(entry.getValue()); + } + extraStr = sb.toString(); + } + + URL url = new URL( + "http", //$NON-NLS-1$ + "tools.google.com", //$NON-NLS-1$ + "/service/update?as=androidsdk_" + app + //$NON-NLS-1$ + "&id=" + Long.toHexString(id) + //$NON-NLS-1$ + "&version=" + version + //$NON-NLS-1$ + "&os=" + osName + //$NON-NLS-1$ + "&osa=" + osArch + //$NON-NLS-1$ + "&vma=" + jvmArch + //$NON-NLS-1$ + extraStr); + return url; + } + + /** + * Detects and reports the host OS: "linux", "win" or "mac". + * For Windows and Mac also append the version, so for example + * Win XP will return win-5.1. + */ + protected String getOsName() { // made protected for testing + String os = getSystemProperty(SYS_PROP_OS_NAME); + + if (os == null || os.length() == 0) { + return "unknown"; //$NON-NLS-1$ + } + + String os2 = os.toLowerCase(Locale.US); + + if (os2.startsWith("mac")) { //$NON-NLS-1$ + os = "mac"; //$NON-NLS-1$ + String osVers = getOsVersion(); + if (osVers != null) { + os = os + '-' + osVers; + } + } else if (os2.startsWith("win")) { //$NON-NLS-1$ + os = "win"; //$NON-NLS-1$ + String osVers = getOsVersion(); + if (osVers != null) { + os = os + '-' + osVers; + } + } else if (os2.startsWith("linux")) { //$NON-NLS-1$ + os = "linux"; //$NON-NLS-1$ + + } else if (os.length() > 32) { + // Unknown -- send it verbatim so we can see it + // but protect against arbitrarily long values + os = os.substring(0, 32); + } + return os; + } + + /** + * Detects and returns the OS architecture: x86, x86_64, ppc. + * This may differ or be equal to the JVM architecture in the sense that + * a 64-bit OS can run a 32-bit JVM. + */ + protected String getOsArch() { // made protected for testing + String arch = getJvmArch(); + + if ("x86_64".equals(arch)) { //$NON-NLS-1$ + // This is a simple case: the JVM runs in 64-bit so the + // OS must be a 64-bit one. + return arch; + + } else if ("x86".equals(arch)) { //$NON-NLS-1$ + // This is the misleading case: the JVM is 32-bit but the OS + // might be either 32 or 64. We can't tell just from this + // property. + // Macs are always on 64-bit, so we just need to figure it + // out for Windows and Linux. + + String os = getOsName(); + if (os.startsWith("win")) { //$NON-NLS-1$ + // When WOW64 emulates a 32-bit environment under a 64-bit OS, + // it sets PROCESSOR_ARCHITEW6432 to AMD64 or IA64 accordingly. + // Ref: http://msdn.microsoft.com/en-us/library/aa384274(v=vs.85).aspx + + String w6432 = getSystemEnv("PROCESSOR_ARCHITEW6432"); //$NON-NLS-1$ + if (w6432 != null && w6432.indexOf("64") != -1) { //$NON-NLS-1$ + return "x86_64"; //$NON-NLS-1$ + } + } else if (os.startsWith("linux")) { //$NON-NLS-1$ + // Let's try the obvious. This works in Ubuntu and Debian + String s = getSystemEnv("HOSTTYPE"); //$NON-NLS-1$ + + s = sanitizeOsArch(s); + if (s.indexOf("86") != -1) { //$NON-NLS-1$ + arch = s; + } + } + } + + return arch; + } + + /** + * Returns the version of the OS version if it is defined as X.Y, or null otherwise. + *

+ * Example of returned versions can be found at http://lopica.sourceforge.net/os.html + *

+ * This method removes any exiting micro versions. + * Returns null if the version doesn't match X.Y.Z. + */ + protected String getOsVersion() { // made protected for testing + Pattern p = Pattern.compile("(\\d+)\\.(\\d+).*"); //$NON-NLS-1$ + String osVers = getSystemProperty(SYS_PROP_OS_VERSION); + if (osVers != null && osVers.length() > 0) { + Matcher m = p.matcher(osVers); + if (m.matches()) { + return m.group(1) + '.' + m.group(2); + } + } + return null; + } + + /** + * Detects and returns the JVM info: version + architecture. + * Examples: 1.4-ppc, 1.6-x86, 1.7-x86_64 + */ + protected String getJvmInfo() { // made protected for testing + return getJvmVersion() + '-' + getJvmArch(); + } + + /** + * Returns the major.minor Java version. + *

+ * The "java.version" property returns something like "1.6.0_20" + * of which we want to return "1.6". + */ + protected String getJvmVersion() { // made protected for testing + String version = getSystemProperty(SYS_PROP_JAVA_VERSION); + + if (version == null || version.length() == 0) { + return "unknown"; //$NON-NLS-1$ + } + + Pattern p = Pattern.compile("(\\d+)\\.(\\d+).*"); //$NON-NLS-1$ + Matcher m = p.matcher(version); + if (m.matches()) { + return m.group(1) + '.' + m.group(2); + } + + // Unknown version. Send it as-is within a reasonable size limit. + if (version.length() > 8) { + version = version.substring(0, 8); + } + return version; + } + + /** + * Detects and returns the JVM architecture. + *

+ * The HotSpot JVM has a private property for this, "sun.arch.data.model", + * which returns either "32" or "64". However it's not in any kind of spec. + *

+ * What we want is to know whether the JVM is running in 32-bit or 64-bit and + * the best indicator is to use the "os.arch" property. + * - On a 32-bit system, only a 32-bit JVM can run so it will be x86 or ppc.
+ * - On a 64-bit system, a 32-bit JVM will also return x86 since the OS needs + * to masquerade as a 32-bit OS for backward compatibility.
+ * - On a 64-bit system, a 64-bit JVM will properly return x86_64. + *

+     * JVM:       Java 32-bit   Java 64-bit
+     * Windows:   x86           x86_64
+     * Linux:     x86           x86_64
+     * Mac        untested      x86_64
+     * 
+ */ + protected String getJvmArch() { // made protected for testing + String arch = getSystemProperty(SYS_PROP_OS_ARCH); + return sanitizeOsArch(arch); + } + + private String sanitizeOsArch(String arch) { + if (arch == null || arch.length() == 0) { + return "unknown"; //$NON-NLS-1$ + } + + if (arch.equalsIgnoreCase("x86_64") || //$NON-NLS-1$ + arch.equalsIgnoreCase("ia64") || //$NON-NLS-1$ + arch.equalsIgnoreCase("amd64")) { //$NON-NLS-1$ + return "x86_64"; //$NON-NLS-1$ + } + + if (arch.length() >= 4 && arch.charAt(0) == 'i' && arch.indexOf("86") == 2) { //$NON-NLS-1$ + // Any variation of iX86 counts as x86 (i386, i486, i686). + return "x86"; //$NON-NLS-1$ + } + + if (arch.equalsIgnoreCase("PowerPC")) { //$NON-NLS-1$ + return "ppc"; //$NON-NLS-1$ + } + + // Unknown arch. Send it as-is but protect against arbitrarily long values. + if (arch.length() > 32) { + arch = arch.substring(0, 32); + } + return arch; + } + + /** + * Normalize the supplied application name. + * + * @param app to report + */ + protected String normalizeAppName(String app) { + // Filter out \W , non-word character: [^a-zA-Z_0-9] + String app2 = app.replaceAll("\\W", ""); //$NON-NLS-1$ //$NON-NLS-2$ + + if (app.length() == 0) { + throw new IllegalArgumentException("Bad app name: " + app); //$NON-NLS-1$ + } + + return app2; + } + + /** + * Validate the supplied application version, and normalize the version. + * + * @param version supplied by caller + * @return normalized dotted quad version + */ + protected String normalizeVersion(String version) { + + Pattern regex = Pattern.compile( + //1=major 2=minor 3=micro 4=build | 5=rc + "^(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?(?:\\.(\\d+)| +rc(\\d+))?"); //$NON-NLS-1$ + + Matcher m = regex.matcher(version); + if (m != null && m.lookingAt()) { + StringBuilder normal = new StringBuilder(); + for (int i = 1; i <= 4; i++) { + int v = 0; + // If build is null but we have an rc, take that number instead as the 4th part. + if (i == 4 && + i < m.groupCount() && + m.group(i) == null && + m.group(i+1) != null) { + i++; + } + if (m.group(i) != null) { + try { + v = Integer.parseInt(m.group(i)); + } catch (Exception ignore) { + } + } + if (i > 1) { + normal.append('.'); + } + normal.append(v); + } + return normal.toString(); + } + + throw new IllegalArgumentException("Bad version: " + version); //$NON-NLS-1$ + } + + /** + * Calls {@link System#getProperty(String)}. + * Allows unit-test to override the return value. + * @see System#getProperty(String) + */ + protected String getSystemProperty(String name) { + return System.getProperty(name); + } + + /** + * Calls {@link System#getenv(String)}. + * Allows unit-test to override the return value. + * @see System#getenv(String) + */ + protected String getSystemEnv(String name) { + return System.getenv(name); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkstats/src/test/java/com/android/sdkstats/SdkStatsServiceTest.java b/andmore-swt/org.eclipse.andmore.sdkstats/src/test/java/com/android/sdkstats/SdkStatsServiceTest.java new file mode 100644 index 00000000..69402b6e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/src/test/java/com/android/sdkstats/SdkStatsServiceTest.java @@ -0,0 +1,544 @@ +/* + * Copyright (C) 2012 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.sdkstats; + +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +import junit.framework.TestCase; + +public class SdkStatsServiceTest extends TestCase { + + private static class MockSdkStatsService extends SdkStatsService { + + private final String mOsName; + private final String mOsVersion; + private final String mOsArch; + private final String mJavaVersion; + private final Map mEnvVars = new HashMap(); + private URL mPingUrlResult; + + public MockSdkStatsService(String osName, + String osVersion, + String osArch, + String javaVersion) { + mOsName = osName; + mOsVersion = osVersion; + mOsArch = osArch; + mJavaVersion = javaVersion; + } + + public URL getPingUrlResult() { + return mPingUrlResult; + } + + public void setSystemEnv(String varName, String value) { + mEnvVars.put(varName, value); + } + + @Override + protected String getSystemProperty(String name) { + if (SdkStatsService.SYS_PROP_OS_NAME.equals(name)) { + return mOsName; + } else if (SdkStatsService.SYS_PROP_OS_VERSION.equals(name)) { + return mOsVersion; + } else if (SdkStatsService.SYS_PROP_OS_ARCH.equals(name)) { + return mOsArch; + } else if (SdkStatsService.SYS_PROP_JAVA_VERSION.equals(name)) { + return mJavaVersion; + } + // Don't use current properties values, we don't want the tests to be flaky + fail("SdkStatsServiceTest doesn't define a system.property for " + name); + return null; + } + + @Override + protected String getSystemEnv(String name) { + if (mEnvVars.containsKey(name)) { + return mEnvVars.get(name); + } + // Don't use current env vars, we don't want the tests to be flaky + fail("SdkStatsServiceTest doesn't define a system.getenv for " + name); + return null; + } + + @Override + protected void doPing(String app, String version, + Map extras) { + // The super.doPing() does: + // 1- normalize input, + // 2- check the ping time, + // 3- check/create the pind id, + // 4- create the ping URL + // 5- and send the network ping in a thread. + // In this mock version we just do steps 1 and 4 and record the URL; + // obvious we don't check the ping time in the prefs nor send the actual ping. + + // Validate the application and version input. + final String nApp = normalizeAppName(app); + final String nVersion = normalizeVersion(version); + + long id = 0x42; + try { + mPingUrlResult = createPingUrl(nApp, nVersion, id, extras); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + public void testSdkStatsService_getJvmArch() { + MockSdkStatsService m; + + m = new MockSdkStatsService("Windows", "4.0", "x86", "1.7"); + assertEquals("x86", m.getJvmArch()); + m = new MockSdkStatsService("Windows", "4.0", "i386", "1.7"); + assertEquals("x86", m.getJvmArch()); + m = new MockSdkStatsService("Windows", "4.0", "i486", "1.7"); + assertEquals("x86", m.getJvmArch()); + m = new MockSdkStatsService("Linux", "4.0", "i486-linux", "1.7"); + assertEquals("x86", m.getJvmArch()); + m = new MockSdkStatsService("Windows", "4.0", "i586", "1.7"); + assertEquals("x86", m.getJvmArch()); + m = new MockSdkStatsService("Windows", "4.0", "i686", "1.7"); + assertEquals("x86", m.getJvmArch()); + + m = new MockSdkStatsService("Mac OS", "10.0", "x86_64", "1.7"); + assertEquals("x86_64", m.getJvmArch()); + m = new MockSdkStatsService("Mac OS", "8.0", "PowerPC", "1.7"); + assertEquals("ppc", m.getJvmArch()); + + m = new MockSdkStatsService("Mac OS", "4.0", "x86_64", "1.7"); + assertEquals("x86_64", m.getJvmArch()); + m = new MockSdkStatsService("Windows", "4.0", "ia64", "1.7"); + assertEquals("x86_64", m.getJvmArch()); + m = new MockSdkStatsService("Windows", "4.0", "amd64", "1.7"); + assertEquals("x86_64", m.getJvmArch()); + + m = new MockSdkStatsService("Windows", "4.0", "atom", "1.7"); + assertEquals("atom", m.getJvmArch()); + + // 32 chars max + m = new MockSdkStatsService("Windows", "4.0", + "one3456789ten3456789twenty6789thirty6789", "1.7"); + assertEquals("one3456789ten3456789twenty6789th", m.getJvmArch()); + + m = new MockSdkStatsService("Windows", "4.0", "", "1.7"); + assertEquals("unknown", m.getJvmArch()); + + m = new MockSdkStatsService("Windows", "4.0", null, "1.7"); + assertEquals("unknown", m.getJvmArch()); + } + + public void testSdkStatsService_getJvmVersion() { + MockSdkStatsService m; + + m = new MockSdkStatsService("Windows", "4.0", "x86", "1.7.8_09"); + assertEquals("1.7", m.getJvmVersion()); + + m = new MockSdkStatsService("Windows", "4.0", "x86", ""); + assertEquals("unknown", m.getJvmVersion()); + + m = new MockSdkStatsService("Windows", "4.0", "x86", null); + assertEquals("unknown", m.getJvmVersion()); + + // 8 chars max + m = new MockSdkStatsService("Windows", "4.0", "x86", + "one3456789ten3456789twenty6789thirty6789"); + assertEquals("one34567", m.getJvmVersion()); + } + + public void testSdkStatsService_getJvmInfo() { + MockSdkStatsService m; + + m = new MockSdkStatsService("Windows", "4.0", "x86", "1.7.8_09"); + assertEquals("1.7-x86", m.getJvmInfo()); + + m = new MockSdkStatsService("Windows", "4.0", "amd64", "1.7.8_09"); + assertEquals("1.7-x86_64", m.getJvmInfo()); + + m = new MockSdkStatsService("Windows", "4.0", "", ""); + assertEquals("unknown-unknown", m.getJvmInfo()); + + m = new MockSdkStatsService("Windows", "4.0", null, null); + assertEquals("unknown-unknown", m.getJvmInfo()); + + // 8+32 chars max + m = new MockSdkStatsService("Windows", "4.0", + "one3456789ten3456789twenty6789thirty6789", + "one3456789ten3456789twenty6789thirty6789"); + assertEquals("one34567-one3456789ten3456789twenty6789th", m.getJvmInfo()); + } + + public void testSdkStatsService_getOsVersion() { + MockSdkStatsService m; + + m = new MockSdkStatsService("Windows", "4.0.32", "x86", "1.7.8_09"); + assertEquals("4.0", m.getOsVersion()); + + m = new MockSdkStatsService("Windows", "4.0", "x86", "1.7.8_09"); + assertEquals("4.0", m.getOsVersion()); + + m = new MockSdkStatsService("Windows", "4", "x86", "1.7.8_09"); + assertEquals(null, m.getOsVersion()); + + m = new MockSdkStatsService("Windows", "4.0;extrainfo", "x86", "1.7.8_09"); + assertEquals("4.0", m.getOsVersion()); + + m = new MockSdkStatsService("Mac OS", "10.8.32", "x86_64", "1.7.8_09"); + assertEquals("10.8", m.getOsVersion()); + + m = new MockSdkStatsService("Mac OS", "10.8", "x86_64", "1.7.8_09"); + assertEquals("10.8", m.getOsVersion()); + + m = new MockSdkStatsService("Other", "", "x86_64", "1.7.8_09"); + assertEquals(null, m.getOsVersion()); + + m = new MockSdkStatsService("Other", null, "x86_64", "1.7.8_09"); + assertEquals(null, m.getOsVersion()); + } + + public void testSdkStatsService_getOsArch() { + MockSdkStatsService m; + + // 64 bit jvm + m = new MockSdkStatsService("Mac OS", "10.8.32", "x86_64", "1.7.8_09"); + assertEquals("x86_64", m.getOsArch()); + + m = new MockSdkStatsService("Windows", "8.32", "x86_64", "1.7.8_09"); + assertEquals("x86_64", m.getOsArch()); + + m = new MockSdkStatsService("Linux", "8.32", "x86_64", "1.7.8_09"); + assertEquals("x86_64", m.getOsArch()); + + // 32 bit jvm with 32 vs 64 bit os + m = new MockSdkStatsService("Windows", "8.32", "x86", "1.7.8_09"); + m.setSystemEnv("PROCESSOR_ARCHITEW6432", null); + assertEquals("x86", m.getOsArch()); + + m = new MockSdkStatsService("Windows", "8.32", "x86", "1.7.8_09"); + m.setSystemEnv("PROCESSOR_ARCHITEW6432", "AMD64"); + assertEquals("x86_64", m.getOsArch()); + + m = new MockSdkStatsService("Windows", "8.32", "x86", "1.7.8_09"); + m.setSystemEnv("PROCESSOR_ARCHITEW6432", "IA64"); + assertEquals("x86_64", m.getOsArch()); + + // 32 bit jvm with 32 vs 64 bit os + m = new MockSdkStatsService("Linux", "8.32", "x86", "1.7.8_09"); + m.setSystemEnv("HOSTTYPE", null); + assertEquals("x86", m.getOsArch()); + + m = new MockSdkStatsService("Linux", "8.32", "x86", "1.7.8_09"); + m.setSystemEnv("HOSTTYPE", "i686-linux"); + assertEquals("x86", m.getOsArch()); + + m = new MockSdkStatsService("Linux", "8.32", "x86", "1.7.8_09"); + m.setSystemEnv("HOSTTYPE", "AMD64"); + assertEquals("x86_64", m.getOsArch()); + + m = new MockSdkStatsService("Linux", "8.32", "x86", "1.7.8_09"); + m.setSystemEnv("HOSTTYPE", "x86_64"); + assertEquals("x86_64", m.getOsArch()); + } + + public void testSdkStatsService_getOsName() { + MockSdkStatsService m; + + m = new MockSdkStatsService("Mac OS", "10.8.32", "x86_64", "1.7.8_09"); + assertEquals("mac-10.8", m.getOsName()); + + m = new MockSdkStatsService("mac", "10", "x86", "1.7.8_09"); + assertEquals("mac", m.getOsName()); + + m = new MockSdkStatsService("Windows", "6.2", "x86_64", "1.7.8_09"); + assertEquals("win-6.2", m.getOsName()); + + m = new MockSdkStatsService("win", "6.2", "x86", "1.7.8_09"); + assertEquals("win-6.2", m.getOsName()); + + m = new MockSdkStatsService("win", "6", "x86_64", "1.7.8_09"); + assertEquals("win", m.getOsName()); + + m = new MockSdkStatsService("Linux", "foobuntu-32", "x86", "1.7.8_09"); + assertEquals("linux", m.getOsName()); + + m = new MockSdkStatsService("linux", "1", "x86_64", "1.7.8_09"); + assertEquals("linux", m.getOsName()); + + m = new MockSdkStatsService("PowerPC", "32", "ppc", "1.7.8_09"); + assertEquals("PowerPC", m.getOsName()); + + m = new MockSdkStatsService("freebsd", "42", "x86_64", "1.7.8_09"); + assertEquals("freebsd", m.getOsName()); + + m = new MockSdkStatsService("openbsd", "43", "x86_64", "1.7.8_09"); + assertEquals("openbsd", m.getOsName()); + + // 32 chars max + m = new MockSdkStatsService("one3456789ten3456789twenty6789thirty6789", + "42", "x86_64", "1.7.8_09"); + assertEquals("one3456789ten3456789twenty6789th", m.getOsName()); + } + + public void testSdkStatsService_parseVersion() { + // Tests that the version parses supports the new "major.minor.micro rcPreview" format + // as well as "x.y.z.t" formats as well as Eclipse's "x.y.z.v2012somedate" formats. + + MockSdkStatsService m; + m = new MockSdkStatsService("Windows", "6.2", "x86_64", "1.7.8_09"); + + m.ping("monitor", "21"); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_monitor&" + + "id=42&" + + "version=21.0.0.0&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64", + m.getPingUrlResult().toString()); + + m.ping("monitor", "21.1"); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_monitor&" + + "id=42&" + + "version=21.1.0.0&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64", + m.getPingUrlResult().toString()); + + m.ping("monitor", "21.2.03"); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_monitor&" + + "id=42&" + + "version=21.2.3.0&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64", + m.getPingUrlResult().toString()); + + m.ping("monitor", "21.2.3.4"); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_monitor&" + + "id=42&" + + "version=21.2.3.4&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64", + m.getPingUrlResult().toString()); + + // More than 4 parts or extra stuff that is not an "rc" preview are ignored. + m.ping("monitor", "21.2.3.4.5.6.7.8"); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_monitor&" + + "id=42&" + + "version=21.2.3.4&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64", + m.getPingUrlResult().toString()); + + m.ping("monitor", "21.2.3.4.v20120101 the rest is ignored"); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_monitor&" + + "id=42&" + + "version=21.2.3.4&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64", + m.getPingUrlResult().toString()); + + // If the "rc" preview integer is present, it's equivalent to a 4th number. + m.ping("monitor", "21 rc4"); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_monitor&" + + "id=42&" + + "version=21.0.0.4&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64", + m.getPingUrlResult().toString()); + + m.ping("monitor", "21.01 rc5"); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_monitor&" + + "id=42&" + + "version=21.1.0.5&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64", + m.getPingUrlResult().toString()); + + m.ping("monitor", "21.02.03 rc6"); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_monitor&" + + "id=42&" + + "version=21.2.3.6&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64", + m.getPingUrlResult().toString()); + + // If there's a 4-part version number, the rc preview number isn't used. + m.ping("monitor", "21.2.3.4 rc7"); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_monitor&" + + "id=42&" + + "version=21.2.3.4&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64", + m.getPingUrlResult().toString()); + + // For Eclipse plugins, the 4th part might be a date. It is ignored. + m.ping("eclipse", "21.2.3.v20120102235958"); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_eclipse&" + + "id=42&" + + "version=21.2.3.0&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64", + m.getPingUrlResult().toString()); + } + + public void testSdkStatsService_glPing() { + MockSdkStatsService m; + m = new MockSdkStatsService("Windows", "6.2", "x86_64", "1.7.8_09"); + + // Send emulator ping with just emulator version, no GL stuff + m.ping("emulator", "12"); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_emulator&" + + "id=42&" + + "version=12.0.0.0&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64", + m.getPingUrlResult().toString()); + + // Send emulator ping with just emulator version, no GL stuff. + // This is the same request but using the variable string list API, arg 0 is the "ping" app. + m.ping(new String[] { "ping", "emulator", "12" }); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_emulator&" + + "id=42&" + + "version=12.0.0.0&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64", + m.getPingUrlResult().toString()); + + // Send a ping for a non-emulator app with extra parameters, no GL stuff + m.ping(new String[] { "ping", "not-emulator", "12", "arg1", "arg2", "arg3" }); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_notemulator&" + + "id=42&" + + "version=12.0.0.0&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64", + m.getPingUrlResult().toString()); + + // Send a ping for the emulator app with extra parameters, GL stuff is added, 3 parameters + m.ping(new String[] { "ping", "emulator", "12", "Vendor Inc.", "Some cool_GPU!!! (fast one!)", "1.2.3.4_preview" }); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_emulator&" + + "id=42&" + + "version=12.0.0.0&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64&" + + "glm=Vendor+Inc.&" + + "glr=Some+cool_GPU+%28fast+one+%29&" + + "glv=1.2.3.4_preview", + m.getPingUrlResult().toString()); + + // Send a ping for the emulator app with extra parameters, GL stuff is added, 2 parameters + m.ping(new String[] { "ping", "emulator", "12", "Vendor Inc.", "Some cool_GPU!!! (fast one!)" }); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_emulator&" + + "id=42&" + + "version=12.0.0.0&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64&" + + "glm=Vendor+Inc.&" + + "glr=Some+cool_GPU+%28fast+one+%29", + m.getPingUrlResult().toString()); + + // Send a ping for the emulator app with extra parameters, GL stuff is added, 1 parameter + m.ping(new String[] { "ping", "emulator", "12", "Vendor Inc." }); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_emulator&" + + "id=42&" + + "version=12.0.0.0&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64&" + + "glm=Vendor+Inc.", + m.getPingUrlResult().toString()); + + // Parameters that are more than 128 chars are cut short. + m.ping(new String[] { "ping", "emulator", "12", + // 130 chars each + "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789", + "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789", + "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789" }); + assertEquals( + "http://tools.google.com/service/update?" + + "as=androidsdk_emulator&" + + "id=42&" + + "version=12.0.0.0&" + + "os=win-6.2&" + + "osa=x86_64&" + + "vma=1.7-x86_64&" + + "glm=01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567&" + + "glr=01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567&" + + "glv=01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567", + m.getPingUrlResult().toString()); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/.classpath b/andmore-swt/org.eclipse.andmore.sdkuilib/.classpath new file mode 100644 index 00000000..1face890 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/.classpath @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/.gitignore b/andmore-swt/org.eclipse.andmore.sdkuilib/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/.project b/andmore-swt/org.eclipse.andmore.sdkuilib/.project new file mode 100644 index 00000000..c471b99d --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/.project @@ -0,0 +1,34 @@ + + + org.eclipse.andmore.sdkuilib + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.PluginNature + + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.core.resources.prefs b/andmore-swt/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..662c7dfc --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,3 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 +encoding/test-src=UTF-8 diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.jdt.core.prefs b/andmore-swt/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..529ef073 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,13 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.m2e.core.prefs b/andmore-swt/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/META-INF/MANIFEST.MF b/andmore-swt/org.eclipse.andmore.sdkuilib/META-INF/MANIFEST.MF new file mode 100644 index 00000000..fa75ea08 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/META-INF/MANIFEST.MF @@ -0,0 +1,28 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Localization: plugin +Bundle-Name: %Bundle-Name +Bundle-SymbolicName: org.eclipse.andmore.sdkuilib;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Vendor: %Bundle-Vendor +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Require-Bundle: org.eclipse.core.commands;bundle-version="3.8.1", + org.eclipse.equinox.common;bundle-version="3.8.0", + org.eclipse.jface;bundle-version="3.12.2", + org.apache.httpcomponents.httpclient;bundle-version="4.1.3", + org.apache.httpcomponents.httpcore;bundle-version="4.1.4", + org.eclipse.andmore.swt, + org.eclipse.andmore.ddmuilib, + org.eclipse.andmore.swtmenubar, + com.google.gson;bundle-version="2.2.4" +Bundle-ClassPath: ., + libs/commons-compress-1.8.1.jar +Export-Package: com.android.sdkuilib.internal.repository, + com.android.sdkuilib.internal.repository.core, + com.android.sdkuilib.internal.repository.icons, + com.android.sdkuilib.internal.repository.ui, + com.android.sdkuilib.internal.tasks, + com.android.sdkuilib.internal.widgets, + com.android.sdkuilib.repository, + com.android.sdkuilib.ui +Import-Package: com.android.sdklib.repository diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/MODULE_LICENSE_APACHE2 b/andmore-swt/org.eclipse.andmore.sdkuilib/MODULE_LICENSE_APACHE2 new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/NOTICE b/andmore-swt/org.eclipse.andmore.sdkuilib/NOTICE new file mode 100644 index 00000000..70c54220 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/NOTICE @@ -0,0 +1,190 @@ + + Copyright (c) 2005-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. + + 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. + + + 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 + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/build.properties b/andmore-swt/org.eclipse.andmore.sdkuilib/build.properties new file mode 100644 index 00000000..70d2e7a7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/build.properties @@ -0,0 +1,7 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + .,\ + libs/,\ + plugin.properties + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/libs/commons-compress-1.8.1.jar b/andmore-swt/org.eclipse.andmore.sdkuilib/libs/commons-compress-1.8.1.jar new file mode 100644 index 0000000000000000000000000000000000000000..66b0a56bd8b24478ddeb2d6eb2824280de1c1a9d GIT binary patch literal 365552 zcmb@tbChLUvM-#rZC2WLW~FW0wr$&XrES}`ZC2W-%>3%y+xPaleZP0Q`;E89SbOb1 zX2gn!9T6+$FJj6`0)s#S{PB4#DiivTAAkFT{C$@eR^q1-ml2_p``a=IfQR49T>UCs z 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%0AyznNceQhhsnRTxPd5v2(@3 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 + + 4.0.0 + + org.eclipse.andmore.sdkuilib + eclipse-plugin + sdkuilib + + ../pom.xml + org.eclipse.andmore + swt-droid-parent + 0.5.2-SNAPSHOT + + + + junit + junit + test + 4.12 + + + com.android.tools + dvlib + ${android.tools.version} + test + + + + + ${basedir}/test-src + + + org.eclipse.tycho + tycho-maven-plugin + true + + + org.apache.maven.plugins + maven-surefire-plugin + 2.12.4 + + + test + test + + + + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + compiletests + test-compile + + testCompile + + + + + + org.eclipse.tycho + tycho-source-plugin + ${tycho-version} + + + plugin-source + + plugin-source + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/AndroidTargetHash.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/AndroidTargetHash.java new file mode 100644 index 00000000..e4520beb --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/AndroidTargetHash.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2013 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.sdklib; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdklib.repository.descriptors.IdDisplay; +import com.google.common.base.Splitter; + +import java.util.List; + + +/** + * Helper methods to manipulate hash strings used by {@link IAndroidTarget#hashString()}. + */ +public abstract class AndroidTargetHash { + + /** + * Prefix used to build hash strings for platform targets + * @see SdkManager#getTargetFromHashString(String) + */ + public static final String PLATFORM_HASH_PREFIX = "android-"; + + /** + * String to compute hash for add-on targets.
+ * Format is {@code vendor:name:apiVersion}.
+ * + * Important: the vendor and name compontents are the display strings, not the + * newer id strings. + */ + public static final String ADD_ON_FORMAT = "%s:%s:%s"; //$NON-NLS-1$ + + /** + * String used to get a hash to the platform target. + * This format is compatible with the PlatformPackage.installId(). + */ + static final String PLATFORM_HASH = PLATFORM_HASH_PREFIX + "%s"; + + /** + * Returns the hash string for a given platform version. + * + * @param version A non-null platform version. + * @return A non-null hash string uniquely representing this platform target. + */ + @NonNull + public static String getPlatformHashString(@NonNull AndroidVersion version) { + return String.format(AndroidTargetHash.PLATFORM_HASH, version.getApiString()); + } + + /** + * Returns the {@link AndroidVersion} for the given hash string, + * if it represents a platform. If the hash string represents a preview platform, + * the returned {@link AndroidVersion} will have an unknown API level (set to 1 + * or a known matching API level.) + * + * @param hashString the hash string (e.g. "android-19" or "android-CUPCAKE") + * or a pure API level for convenience (e.g. "19" instead of the proper "android-19") + * @return a platform, or null + */ + @Nullable + public static AndroidVersion getPlatformVersion(@NonNull String hashString) { + if (hashString.startsWith(PLATFORM_HASH_PREFIX)) { + String suffix = hashString.substring(PLATFORM_HASH_PREFIX.length()); + if (!suffix.isEmpty()) { + if (Character.isDigit(suffix.charAt(0))) { + try { + int api = Integer.parseInt(suffix); + return new AndroidVersion(api, null); + } catch (NumberFormatException ignore) {} + } else { + int api = SdkVersionInfo.getApiByBuildCode(suffix, false); + if (api < 1) { + api = 1; + } + return new AndroidVersion(api, suffix); + } + } + } else if (!hashString.isEmpty() && Character.isDigit(hashString.charAt(0))) { + // For convenience, interpret a single integer as the proper "android-NN" form. + try { + int api = Integer.parseInt(hashString); + return new AndroidVersion(api, null); + } catch (NumberFormatException ignore) {} + } + + return null; + } + + @Nullable + public static AndroidVersion getAddOnVersion(@NonNull String hashString) { + List parts = Splitter.on(':').splitToList(hashString); + if (parts.size() != 3) { + return null; + } + + String apiLevelPart = parts.get(2); + try { + int apiLevel = Integer.parseInt(apiLevelPart); + return new AndroidVersion(apiLevel, null); + } catch (NumberFormatException e) { + return null; + } + } + + /** + * Gets the API level from a hash string, either a platform version or add-on version. + * + * @see #getAddOnVersion(String) + * @see #getPlatformVersion(String) + */ + @Nullable + public static AndroidVersion getVersionFromHash(@NonNull String hashString) { + if (isPlatform(hashString)) { + return getPlatformVersion(hashString); + } else { + return getAddOnVersion(hashString); + } + } + + /** + * Returns the hash string for a given add-on. + * + * @param addonVendorDisplay A non-null vendor. When using an {@link IdDisplay} source, + * this parameter should be the {@link IdDisplay#getDisplay()}. + * @param addonNameDisplay A non-null add-on name. When using an {@link IdDisplay} source, + * this parameter should be the {@link IdDisplay#getDisplay()}. + * @param version A non-null platform version (the addon's base platform version) + * @return A non-null hash string uniquely representing this add-on target. + */ + public static String getAddonHashString( + @NonNull String addonVendorDisplay, + @NonNull String addonNameDisplay, + @NonNull AndroidVersion version) { + return String.format(ADD_ON_FORMAT, + addonVendorDisplay, + addonNameDisplay, + version.getApiString()); + } + + /** + * Returns the hash string for a given target (add-on or platform.) + * + * @param target A non-null target. + * @return A non-null hash string uniquely representing this target. + */ + public static String getTargetHashString(@NonNull IAndroidTarget target) { + if (target.isPlatform()) { + return getPlatformHashString(target.getVersion()); + } else { + return getAddonHashString( + target.getVendor(), + target.getName(), + target.getVersion()); + } + } + + /** + * Given a hash string, indicates whether this is a platform hash string. + * If not, it's an addon hash string. + * + * @param hashString The hash string to test. + * @return True if this hash string starts by the platform prefix. + */ + public static boolean isPlatform(@NonNull String hashString) { + return hashString.startsWith(PLATFORM_HASH_PREFIX); + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/AndroidVersion.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/AndroidVersion.java new file mode 100644 index 00000000..49dffbc1 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/AndroidVersion.java @@ -0,0 +1,392 @@ +/* + * 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.sdklib; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdklib.repository.PkgProps; + +import java.util.Properties; +import java.util.regex.Pattern; + +/** + * Represents the version of a target or device. + *

+ * A version is defined by an API level and an optional code name. + *

  • Release versions of the Android platform are identified by their API level (integer), + * (technically the code name for release version is "REL" but this class will return + * null instead.)
  • + *
  • Preview versions of the platform are identified by a code name. Their API level + * is usually set to the value of the previous platform.
+ *

+ * While this class contains both values, its goal is to abstract them, so that code comparing 2+ + * versions doesn't have to deal with the logic of handle both values. + *

+ * There are some cases where ones may want to access the values directly. This can be done + * with {@link #getApiLevel()} and {@link #getCodename()}. + *

+ * For generic UI display of the API version, {@link #getApiString()} is to be used. + */ +public final class AndroidVersion implements Comparable { + + private final int mApiLevel; + private final String mCodename; + + /** The default AndroidVersion for minSdkVersion and targetSdkVersion if not specified */ + public static final AndroidVersion DEFAULT = new AndroidVersion(1, null); + + /** + * Thrown when an {@link AndroidVersion} object could not be created. + * @see AndroidVersion#AndroidVersion(Properties) + */ + public static final class AndroidVersionException extends Exception { + private static final long serialVersionUID = 1L; + + AndroidVersionException(String message, Throwable cause) { + super(message, cause); + } + } + + /** + * Creates an {@link AndroidVersion} with the given api level and codename. + * Codename should be null for a release version, otherwise it's a preview codename. + */ + public AndroidVersion(int apiLevel, @Nullable String codename) { + mApiLevel = apiLevel; + mCodename = sanitizeCodename(codename); + } + + /** + * Creates an {@link AndroidVersion} from {@link Properties}, with default values if the + * {@link Properties} object doesn't contain the expected values. + *

The {@link Properties} is expected to have been filled with + * {@link #saveProperties(Properties)}. + */ + public AndroidVersion(@Nullable Properties properties, + int defaultApiLevel, + @Nullable String defaultCodeName) { + if (properties == null) { + mApiLevel = defaultApiLevel; + mCodename = sanitizeCodename(defaultCodeName); + } else { + mApiLevel = Integer.parseInt(properties.getProperty(PkgProps.VERSION_API_LEVEL, + Integer.toString(defaultApiLevel))); + mCodename = sanitizeCodename( + properties.getProperty(PkgProps.VERSION_CODENAME, defaultCodeName)); + } + } + + /** + * Creates an {@link AndroidVersion} from {@link Properties}. The properties must contain + * android version information, or an exception will be thrown. + * @throws AndroidVersionException if no Android version information have been found + * + * @see #saveProperties(Properties) + */ + public AndroidVersion(@NonNull Properties properties) throws AndroidVersionException { + Exception error = null; + + String apiLevel = properties.getProperty(PkgProps.VERSION_API_LEVEL, null/*defaultValue*/); + if (apiLevel != null) { + try { + mApiLevel = Integer.parseInt(apiLevel); + mCodename = sanitizeCodename(properties.getProperty(PkgProps.VERSION_CODENAME, + null/*defaultValue*/)); + return; + } catch (NumberFormatException e) { + error = e; + } + } + + // reaching here means the Properties object did not contain the apiLevel which is required. + throw new AndroidVersionException(PkgProps.VERSION_API_LEVEL + " not found!", error); + } + + /** + * Creates an {@link AndroidVersion} from a string that may be an integer API + * level or a string codename. + *

+ * Important: An important limitation of this method is that cannot possible + * recreate the API level integer from a pure string codename. This is only OK to use + * if the caller can guarantee that only {@link #getApiString()} will be used later. + * Wrong things will happen if the caller then tries to resolve the numeric + * {@link #getApiLevel()}. + * + * @param apiOrCodename A non-null API integer or a codename in its "ALL_CAPS" format. + * "REL" is notable not a valid codename. + * @throws AndroidVersionException if the input isn't a pure integer or doesn't look like + * a valid string codename. + */ + public AndroidVersion(@NonNull String apiOrCodename) throws AndroidVersionException { + int apiLevel = 0; + String codename = null; + try { + apiLevel = Integer.parseInt(apiOrCodename); + } catch (NumberFormatException ignore) { + // We don't know the API level. Android platform codenames are all caps. + // REL is a release-reserved keyword which we can use here. + + if (!SdkConstants.CODENAME_RELEASE.equals(apiOrCodename)) { + if (Pattern.matches("[A-Z_]+", apiOrCodename)) { + codename = apiOrCodename; + } + } + } + + mApiLevel = apiLevel; + mCodename = sanitizeCodename(codename); + + if (mApiLevel <= 0 && codename == null) { + throw new AndroidVersionException( + "Invalid android API or codename " + apiOrCodename, //$NON-NLS-1$ + null); + } + } + + public void saveProperties(@NonNull Properties props) { + props.setProperty(PkgProps.VERSION_API_LEVEL, Integer.toString(mApiLevel)); + if (mCodename != null) { + props.setProperty(PkgProps.VERSION_CODENAME, mCodename); + } + } + + /** + * Returns the api level as an integer. + *

For target that are in preview mode, this can be superseded by + * {@link #getCodename()}. + *

To display the API level in the UI, use {@link #getApiString()}, which will use the + * codename if applicable. + * @see #getCodename() + * @see #getApiString() + */ + public int getApiLevel() { + return mApiLevel; + } + + /** + * Returns the API level as an integer. If this is a preview platform, it + * will return the expected final version of the API rather than the current API + * level. This is the "feature level" as opposed to the "release level" returned by + * {@link #getApiLevel()} in the sense that it is useful when you want + * to check the presence of a given feature from an API, and we consider the feature + * present in preview platforms as well. + * + * @return the API level of this version, +1 for preview platforms + */ + public int getFeatureLevel() { + //noinspection VariableNotUsedInsideIf + return mCodename != null ? mApiLevel + 1 : mApiLevel; + } + + /** + * Returns the version code name if applicable, null otherwise. + *

If the codename is non null, then the API level should be ignored, and this should be + * used as a unique identifier of the target instead. + */ + @Nullable + public String getCodename() { + return mCodename; + } + + /** + * Returns a string representing the API level and/or the code name. + */ + @NonNull + public String getApiString() { + if (mCodename != null) { + return mCodename; + } + + return Integer.toString(mApiLevel); + } + + /** + * Returns whether or not the version is a preview version. + */ + public boolean isPreview() { + return mCodename != null; + } + + /** + * Checks whether a device running a version similar to the receiver can run a project compiled + * for the given version. + *

+ * Be aware that this is not a perfect test, as other properties could break compatibility + * despite this method returning true. For a more comprehensive test, see + * {@link IAndroidTarget#canRunOn(IAndroidTarget)}. + *

+ * Nevertheless, when testing if an application can run on a device (where there is no + * access to the list of optional libraries), this method can give a good indication of whether + * there is a chance the application could run, or if there's a direct incompatibility. + */ + public boolean canRun(@NonNull AndroidVersion appVersion) { + // if the application is compiled for a preview version, the device must be running exactly + // the same. + if (appVersion.mCodename != null) { + return appVersion.mCodename.equals(mCodename); + } + + // otherwise, we check the api level (note that a device running a preview version + // will have the api level of the previous platform). + return mApiLevel >= appVersion.mApiLevel; + } + + /** + * Returns true if the AndroidVersion is an API level equals to + * apiLevel. + */ + public boolean equals(int apiLevel) { + return mCodename == null && apiLevel == mApiLevel; + } + + /** + * Compares the receiver with either an {@link AndroidVersion} object or a {@link String} + * object. + *

If obj is a {@link String}, then the method will first check if it's a string + * representation of a number, in which case it'll compare it to the api level. Otherwise, it'll + * compare it against the code name. + *

For all other type of object give as parameter, this method will return + * false. + */ + @Override + public boolean equals(Object obj) { + if (obj instanceof AndroidVersion) { + AndroidVersion version = (AndroidVersion)obj; + + if (mCodename == null) { + return version.mCodename == null && + mApiLevel == version.mApiLevel; + } else { + return mCodename.equals(version.mCodename) && + mApiLevel == version.mApiLevel; + } + + } else if (obj instanceof String) { + // if we have a code name, this must match. + if (mCodename != null) { + return mCodename.equals(obj); + } + + // else we try to convert to a int and compare to the api level + try { + int value = Integer.parseInt((String)obj); + return value == mApiLevel; + } catch (NumberFormatException e) { + // not a number? we'll return false below. + } + } + + return false; + } + + @Override + public int hashCode() { + if (mCodename != null) { + return mCodename.hashCode(); + } + + // there may be some collisions between the hashcode of the codename and the api level + // but it's acceptable. + return mApiLevel; + } + + /** + * Returns a string with the API Level and optional codename. + * Useful for debugging. + * For display purpose, please use {@link #getApiString()} instead. + */ + @Override + public String toString() { + String s = String.format("API %1$d", mApiLevel); //$NON-NLS-1$ + if (isPreview()) { + s += String.format(", %1$s preview", mCodename); //$NON-NLS-1$ + } + return s; + } + + /** + * Compares this object with the specified object for order. Returns a + * negative integer, zero, or a positive integer as this object is less + * than, equal to, or greater than the specified object. + * + * @param o the Object to be compared. + * @return a negative integer, zero, or a positive integer as this object is + * less than, equal to, or greater than the specified object. + */ + @Override + public int compareTo(@NonNull AndroidVersion o) { + return compareTo(o.mApiLevel, o.mCodename); + } + + public int compareTo(int apiLevel, @Nullable String codename) { + if (mCodename == null) { + if (codename == null) { + return mApiLevel - apiLevel; + } else { + if (mApiLevel == apiLevel) { + return -1; // same api level but argument is a preview for next version + } + + return mApiLevel - apiLevel; + } + } else { + // 'this' is a preview + if (mApiLevel == apiLevel) { + if (codename == null) { + return +1; + } else { + return mCodename.compareTo(codename); // strange case where the 2 previews + // have different codename? + } + } else { + return mApiLevel - apiLevel; + } + } + } + + /** + * Compares this version with the specified API and returns true if this version + * is greater or equal than the requested API -- that is the current version is a + * suitable min-api-level for the argument API. + */ + public boolean isGreaterOrEqualThan(int api) { + return compareTo(api, null /*codename*/) >= 0; + } + + /** + * Sanitizes the codename string according to the following rules: + * - A codename should be {@code null} for a release version or it should be a non-empty + * string for an actual preview. + * - In input, spacing is trimmed since it is irrelevant. + * - An empty string or the special codename "REL" means a release version + * and is converted to {@code null}. + * + * @param codename A possible-null codename. + * @return Null for a release version or a non-empty codename. + */ + @Nullable + private static String sanitizeCodename(@Nullable String codename) { + if (codename != null) { + codename = codename.trim(); + if (codename.isEmpty() || SdkConstants.CODENAME_RELEASE.equals(codename)) { + codename = null; + } + } + return codename; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/BuildToolInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/BuildToolInfo.java new file mode 100644 index 00000000..39fefd6d --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/BuildToolInfo.java @@ -0,0 +1,391 @@ +/* + * Copyright (C) 2013 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.sdklib; + +import static com.android.SdkConstants.FD_LIB; +import static com.android.SdkConstants.FN_AAPT; +import static com.android.SdkConstants.FN_AIDL; +import static com.android.SdkConstants.FN_BCC_COMPAT; +import static com.android.SdkConstants.FN_DEXDUMP; +import static com.android.SdkConstants.FN_DX; +import static com.android.SdkConstants.FN_DX_JAR; +import static com.android.SdkConstants.FN_JACK; +import static com.android.SdkConstants.FN_JILL; +import static com.android.SdkConstants.FN_LD_ARM; +import static com.android.SdkConstants.FN_LD_MIPS; +import static com.android.SdkConstants.FN_LD_X86; +import static com.android.SdkConstants.FN_RENDERSCRIPT; +import static com.android.SdkConstants.FN_SPLIT_SELECT; +import static com.android.SdkConstants.FN_ZIPALIGN; +import static com.android.SdkConstants.OS_FRAMEWORK_RS; +import static com.android.SdkConstants.OS_FRAMEWORK_RS_CLANG; +import static com.android.sdklib.BuildToolInfo.PathId.AAPT; +import static com.android.sdklib.BuildToolInfo.PathId.AIDL; +import static com.android.sdklib.BuildToolInfo.PathId.ANDROID_RS; +import static com.android.sdklib.BuildToolInfo.PathId.ANDROID_RS_CLANG; +import static com.android.sdklib.BuildToolInfo.PathId.BCC_COMPAT; +import static com.android.sdklib.BuildToolInfo.PathId.DEXDUMP; +import static com.android.sdklib.BuildToolInfo.PathId.DX; +import static com.android.sdklib.BuildToolInfo.PathId.DX_JAR; +import static com.android.sdklib.BuildToolInfo.PathId.JACK; +import static com.android.sdklib.BuildToolInfo.PathId.JILL; +import static com.android.sdklib.BuildToolInfo.PathId.LD_ARM; +import static com.android.sdklib.BuildToolInfo.PathId.LD_MIPS; +import static com.android.sdklib.BuildToolInfo.PathId.LD_X86; +import static com.android.sdklib.BuildToolInfo.PathId.LLVM_RS_CC; +import static com.android.sdklib.BuildToolInfo.PathId.SPLIT_SELECT; +import static com.android.sdklib.BuildToolInfo.PathId.ZIP_ALIGN; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.annotations.VisibleForTesting; +import com.android.annotations.VisibleForTesting.Visibility; +import com.android.sdklib.io.LegacyFileOp; +import com.android.sdklib.repository.FullRevision; +import com.android.sdklib.repository.NoPreviewRevision; +import com.android.utils.ILogger; +import com.google.common.collect.Maps; + +import java.io.File; +import java.util.Map; +import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Information on a specific build-tool folder. + *

+ * For unit tests, see: + * - sdklib/src/test/.../LocalSdkTest + * - sdklib/src/test/.../SdkManagerTest + * - sdklib/src/test/.../BuildToolInfoTest + */ +public class BuildToolInfo { + + /** Name of the file read by {@link #getRuntimeProps()} */ + private static final String FN_RUNTIME_PROPS = "runtime.properties"; + + /** + * Property in {@link #FN_RUNTIME_PROPS} indicating the desired runtime JVM. + * Type: {@link NoPreviewRevision#toShortString()}, e.g. "1.7.0" + */ + private static final String PROP_RUNTIME_JVM = "Runtime.Jvm"; + + /** + * First version with native multi-dex support. + */ + public static final int SDK_LEVEL_FOR_MULTIDEX_NATIVE_SUPPORT = 21; + + public enum PathId { + /** OS Path to the target's version of the aapt tool. */ + AAPT("1.0.0"), + /** OS Path to the target's version of the aidl tool. */ + AIDL("1.0.0"), + /** OS Path to the target's version of the dx tool. */ + DX("1.0.0"), + /** OS Path to the target's version of the dx.jar file. */ + DX_JAR("1.0.0"), + /** OS Path to the llvm-rs-cc binary for Renderscript. */ + LLVM_RS_CC("1.0.0"), + /** OS Path to the Renderscript include folder. */ + ANDROID_RS("1.0.0"), + /** OS Path to the Renderscript(clang) include folder. */ + ANDROID_RS_CLANG("1.0.0"), + + DEXDUMP("1.0.0"), + + // --- NEW IN 18.1.0 --- + + /** OS Path to the bcc_compat tool. */ + BCC_COMPAT("18.1.0"), + /** OS Path to the ARM linker. */ + LD_ARM("18.1.0"), + /** OS Path to the X86 linker. */ + LD_X86("18.1.0"), + /** OS Path to the MIPS linker. */ + LD_MIPS("18.1.0"), + + // --- NEW IN 19.1.0 --- + ZIP_ALIGN("19.1.0"), + + // --- NEW IN 21.x.y --- + JACK("21.1.0"), + JILL("21.1.0"), + + SPLIT_SELECT("22.0.0"); + + /** + * min revision this element was introduced. + * Controls {@link BuildToolInfo#isValid(ILogger)} + */ + private final FullRevision mMinRevision; + + /** + * Creates the enum with a min revision in which this + * tools appeared in the build tools. + * + * @param minRevision the min revision. + */ + PathId(@NonNull String minRevision) { + mMinRevision = FullRevision.parseRevision(minRevision); + } + + /** + * Returns whether the enum of present in a given rev of the build tools. + * + * @param fullRevision the build tools revision. + * @return true if the tool is present. + */ + boolean isPresentIn(@NonNull FullRevision fullRevision) { + return fullRevision.compareTo(mMinRevision) >= 0; + } + } + + /** The build-tool revision. */ + @NonNull + private final FullRevision mRevision; + + /** The path to the build-tool folder specific to this revision. */ + @NonNull + private final File mPath; + + private final Map mPaths = Maps.newEnumMap(PathId.class); + + public BuildToolInfo(@NonNull FullRevision revision, @NonNull File path) { + mRevision = revision; + mPath = path; + + add(AAPT, FN_AAPT); + add(AIDL, FN_AIDL); + add(DX, FN_DX); + add(DX_JAR, FD_LIB + File.separator + FN_DX_JAR); + add(LLVM_RS_CC, FN_RENDERSCRIPT); + add(ANDROID_RS, OS_FRAMEWORK_RS); + add(ANDROID_RS_CLANG, OS_FRAMEWORK_RS_CLANG); + add(DEXDUMP, FN_DEXDUMP); + add(BCC_COMPAT, FN_BCC_COMPAT); + add(LD_ARM, FN_LD_ARM); + add(LD_X86, FN_LD_X86); + add(LD_MIPS, FN_LD_MIPS); + add(ZIP_ALIGN, FN_ZIPALIGN); + add(JACK, FN_JACK); + add(JILL, FN_JILL); + add(SPLIT_SELECT, FN_SPLIT_SELECT); + } + + public BuildToolInfo( + @NonNull FullRevision revision, + @NonNull File mainPath, + @NonNull File aapt, + @NonNull File aidl, + @NonNull File dx, + @NonNull File dxJar, + @NonNull File llmvRsCc, + @NonNull File androidRs, + @NonNull File androidRsClang, + @Nullable File bccCompat, + @Nullable File ldArm, + @Nullable File ldX86, + @Nullable File ldMips, + @NonNull File zipAlign) { + mRevision = revision; + mPath = mainPath; + add(AAPT, aapt); + add(AIDL, aidl); + add(DX, dx); + add(DX_JAR, dxJar); + add(LLVM_RS_CC, llmvRsCc); + add(ANDROID_RS, androidRs); + add(ANDROID_RS_CLANG, androidRsClang); + add(ZIP_ALIGN, zipAlign); + + if (bccCompat != null) { + add(BCC_COMPAT, bccCompat); + } else if (BCC_COMPAT.isPresentIn(revision)) { + throw new IllegalArgumentException("BCC_COMPAT required in " + revision.toString()); + } + if (ldArm != null) { + add(LD_ARM, ldArm); + } else if (LD_ARM.isPresentIn(revision)) { + throw new IllegalArgumentException("LD_ARM required in " + revision.toString()); + } + + if (ldX86 != null) { + add(LD_X86, ldX86); + } else if (LD_X86.isPresentIn(revision)) { + throw new IllegalArgumentException("LD_X86 required in " + revision.toString()); + } + + if (ldMips != null) { + add(LD_MIPS, ldMips); + } else if (LD_MIPS.isPresentIn(revision)) { + throw new IllegalArgumentException("LD_MIPS required in " + revision.toString()); + } + } + + private void add(PathId id, String leaf) { + add(id, new File(mPath, leaf)); + } + + private void add(PathId id, File path) { + String str = path.getAbsolutePath(); + if (path.isDirectory() && str.charAt(str.length() - 1) != File.separatorChar) { + str += File.separatorChar; + } + mPaths.put(id, str); + } + + /** + * Returns the revision. + */ + @NonNull + public FullRevision getRevision() { + return mRevision; + } + + /** + * Returns the build-tool revision-specific folder. + *

+ * For compatibility reasons, use {@link #getPath(PathId)} if you need the path to a + * specific tool. + */ + @NonNull + public File getLocation() { + return mPath; + } + + /** + * Returns the path of a build-tool component. + * + * @param pathId the id representing the path to return. + * @return The absolute path for that tool, with a / separator if it's a folder. + * Null if the path-id is unknown. + */ + public String getPath(PathId pathId) { + assert pathId.isPresentIn(mRevision); + + return mPaths.get(pathId); + } + + /** + * Checks whether the build-tool is valid by verifying that the expected binaries + * are actually present. This checks that all known paths point to a valid file + * or directory. + * + * @param log An optional logger. If non-null, errors will be printed there. + * @return True if the build-tool folder contains all the expected tools. + */ + public boolean isValid(@Nullable ILogger log) { + for (Map.Entry entry : mPaths.entrySet()) { + File f = new File(entry.getValue()); + // check if file is missing. It's only ok if the revision of the build-tools + // is lower than the min rev of the element. + if (!f.exists() && entry.getKey().isPresentIn(mRevision)) { + if (log != null) { + log.warning("Build-tool %1$s is missing %2$s at %3$s", //$NON-NLS-1$ + mRevision.toString(), + entry.getKey(), f.getAbsolutePath()); + } + return false; + } + } + return true; + } + + /** + * Parses the build-tools runtime.props file, if present. + * + * @return The properties from runtime.props if present, otherwise an empty properties set. + */ + @NonNull + public Properties getRuntimeProps() { + LegacyFileOp fop = new LegacyFileOp(); + return fop.loadProperties(new File(mPath, FN_RUNTIME_PROPS)); + } + + /** + * Checks whether this build-tools package can run on the current JVM. + * + * @return True if the build-tools package has a Runtime.Jvm property and it is lesser or + * equal to the current JVM version. + * False if the property is present and the requirement is not met. + * True if there's an error parsing either versions and the comparison cannot be made. + */ + public boolean canRunOnJvm() { + Properties props = getRuntimeProps(); + String required = props.getProperty(PROP_RUNTIME_JVM); + if (required == null) { + // No requirement ==> accepts. + return true; + } + try { + NoPreviewRevision requiredVersion = NoPreviewRevision.parseRevision(required); + NoPreviewRevision currentVersion = getCurrentJvmVersion(); + return currentVersion.compareTo(requiredVersion) >= 0; + + } catch (NumberFormatException ignore) { + // Either we failed to parse the property version or the running JVM version. + // Right now take the relaxed policy of accepting it if we can't compare. + return true; + } + } + + @VisibleForTesting(visibility=Visibility.PRIVATE) + @Nullable + protected NoPreviewRevision getCurrentJvmVersion() throws NumberFormatException { + String javav = System.getProperty("java.version"); //$NON-NLS-1$ + // java Version is typically in the form "1.2.3_45" and we just need to keep up to "1.2.3" + // since our revision numbers are in 3-parts form (1.2.3). + Pattern p = Pattern.compile("((\\d+)(\\.\\d+)?(\\.\\d+)?).*"); //$NON-NLS-1$ + Matcher m = p.matcher(javav); + if (m.matches()) { + return NoPreviewRevision.parseRevision(m.group(1)); + } + return null; + } + + /** + * Returns a debug representation suitable for unit-tests. + * Note that unit-tests need to clean up the paths to avoid inconsistent results. + */ + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(""); //$NON-NLS-1$ + return builder.toString(); + } + + private String getPathString() { + StringBuilder sb = new StringBuilder("{"); + + for (Map.Entry entry : mPaths.entrySet()) { + if (entry.getKey().isPresentIn(mRevision)) { + if (sb.length() > 1) { + sb.append(", "); + } + sb.append(entry.getKey()).append('=').append(entry.getValue()); + } + } + + sb.append('}'); + + return sb.toString(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/IAndroidTarget.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/IAndroidTarget.java new file mode 100644 index 00000000..6606bc84 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/IAndroidTarget.java @@ -0,0 +1,334 @@ +/* + * 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.sdklib; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdklib.repository.descriptors.IdDisplay; + +import java.io.File; +import java.util.List; +import java.util.Map; + + + +/** + * A version of Android that applications can target when building. + */ +public interface IAndroidTarget extends Comparable { + + /** OS Path to the "android.jar" file. */ + int ANDROID_JAR = 1; + /** OS Path to the "framework.aidl" file. */ + int ANDROID_AIDL = 2; + /** OS Path to the "samples" folder which contains sample projects. */ + int SAMPLES = 4; + /** OS Path to the "skins" folder which contains the emulator skins. */ + int SKINS = 5; + /** OS Path to the "templates" folder which contains the templates for new projects. */ + int TEMPLATES = 6; + /** OS Path to the "data" folder which contains data & libraries for the SDK tools. */ + int DATA = 7; + /** OS Path to the "attrs.xml" file. */ + int ATTRIBUTES = 8; + /** OS Path to the "attrs_manifest.xml" file. */ + int MANIFEST_ATTRIBUTES = 9; + /** OS Path to the "data/layoutlib.jar" library. */ + int LAYOUT_LIB = 10; + /** OS Path to the "data/res" folder. */ + int RESOURCES = 11; + /** OS Path to the "data/fonts" folder. */ + int FONTS = 12; + /** OS Path to the "data/widgets.txt" file. */ + int WIDGETS = 13; + /** OS Path to the "data/activity_actions.txt" file. */ + int ACTIONS_ACTIVITY = 14; + /** OS Path to the "data/broadcast_actions.txt" file. */ + int ACTIONS_BROADCAST = 15; + /** OS Path to the "data/service_actions.txt" file. */ + int ACTIONS_SERVICE = 16; + /** OS Path to the "data/categories.txt" file. */ + int CATEGORIES = 17; + /** OS Path to the "sources" folder. */ + int SOURCES = 18; + /** OS Path to the target specific docs */ + int DOCS = 19; + /** OS Path to the "ant" folder which contains the ant build rules (ver 2 and above) */ + int ANT = 24; + /** OS Path to the "uiautomator.jar" file. */ + int UI_AUTOMATOR_JAR = 27; + + + /** + * Return value for {@link #getUsbVendorId()} meaning no USB vendor IDs are defined by the + * Android target. + */ + int NO_USB_ID = 0; + + /** An optional library provided by an Android Target */ + interface OptionalLibrary { + /** The name of the library, as used in the manifest (<uses-library>). */ + @NonNull + String getName(); + /** Location of the jar file. */ + @NonNull + File getJar(); + /** Description of the library. */ + @NonNull + String getDescription(); + /** Whether the library requires a manifest entry */ + boolean isManifestEntryRequired(); + } + + /** + * Returns the target location. + */ + String getLocation(); + + /** + * Returns the name of the vendor of the target. + */ + String getVendor(); + + /** + * Returns the name of the target. + */ + String getName(); + + /** + * Returns the full name of the target, possibly including vendor name. + */ + String getFullName(); + + /** + * Returns the name to be displayed when representing all the libraries this target contains. + */ + String getClasspathName(); + + /** + * Returns the name to be displayed when representing all the libraries this target contains. + */ + String getShortClasspathName(); + + /** + * Returns the description of the target. + */ + String getDescription(); + + /** + * Returns the version of the target. This is guaranteed to be non-null. + */ + @NonNull + AndroidVersion getVersion(); + + /** + * Returns the platform version as a readable string. + */ + String getVersionName(); + + /** Returns the revision number for the target. */ + int getRevision(); + + /** + * Returns true if the target is a standard Android platform. + */ + boolean isPlatform(); + + /** + * Returns the parent target. This is likely to only be non null if + * {@link #isPlatform()} returns false + */ + IAndroidTarget getParent(); + + /** + * Returns the path of a platform component. + * @param pathId the id representing the path to return. + * Any of the constants defined in the {@link IAndroidTarget} interface can be used. + */ + String getPath(int pathId); + + /** + * Returns the path of a platform component. + *

+ * This is like the legacy {@link #getPath(int)} method except it returns a {@link File}. + * + * @param pathId the id representing the path to return. + * Any of the constants defined in the {@link IAndroidTarget} interface can be used. + */ + File getFile(int pathId); + + /** + * Returns a BuildToolInfo for backward compatibility. If an older SDK is used this will return + * paths located in the platform-tools, otherwise it'll return paths located in the latest + * build-tools. + * @return a BuildToolInfo or null if none are available. + */ + BuildToolInfo getBuildToolInfo(); + + /** + * Returns the boot classpath for this target. + * In most case, this is similar to calling {@link #getPath(int)} with + * {@link IAndroidTarget#ANDROID_JAR}. + * + * @return a non null list of the boot classpath. + */ + @NonNull + List getBootClasspath(); + + /** + * Returns a list of optional libraries for this target. + * + * These libraries are not automatically added to the classpath. + * Using them requires adding a uses-library entry in the manifest. + * + * @return a list of libraries. + * + * @see OptionalLibrary#getName() + */ + @NonNull + List getOptionalLibraries(); + + /** + * Returns the additional libraries for this target. + * + * These libraries are automatically added to the classpath, but using them requires + * adding a uses-library entry in the manifest. + * + * @return a list of libraries. + * + * @see OptionalLibrary#getName() + */ + @NonNull + List getAdditionalLibraries(); + + /** + * Returns whether the target is able to render layouts. + */ + boolean hasRenderingLibrary(); + + /** + * Returns the available skin folders for this target. + *

+ * To get the skin names, use {@link File#getName()}.
+ * Skins come either from: + *

    + *
  • a platform ({@code sdk/platforms/N/skins/name})
  • + *
  • an add-on ({@code sdk/addons/name/skins/name})
  • + *
  • a tagged system-image ({@code sdk/system-images/platform-N/tag/abi/skins/name}.)
  • + *
+ * The array can be empty but not null. + */ + @NonNull + File[] getSkins(); + + /** + * Returns the default skin folder for this target. + *

+ * To get the skin name, use {@link File#getName()}. + */ + @Nullable + File getDefaultSkin(); + + /** + * Returns the list of libraries available for a given platform. + * + * @return an array of libraries provided by the platform or null if there is none. + */ + String[] getPlatformLibraries(); + + /** + * Return the value of a given property for this target. + * @return the property value or null if it was not found. + */ + String getProperty(String name); + + /** + * Returns the value of a given property for this target as an Integer value. + *

If the value is missing or is not an integer, the method will return the given default + * value. + * @param name the name of the property to return + * @param defaultValue the default value to return. + * + * @see Integer#decode(String) + */ + Integer getProperty(String name, Integer defaultValue); + + /** + * Returns the value of a given property for this target as a Boolean value. + *

If the value is missing or is not an boolean, the method will return the given default + * value. + * + * @param name the name of the property to return + * @param defaultValue the default value to return. + * + * @see Boolean#valueOf(String) + */ + + Boolean getProperty(String name, Boolean defaultValue); + + /** + * Returns all the properties associated with this target. This can be null if the target has + * no properties. + */ + Map getProperties(); + + /** + * Returns the USB Vendor ID for the vendor of this target. + *

If the target defines no USB Vendor ID, then the method return 0. + */ + int getUsbVendorId(); + + /** + * Returns an array of system images for this target. + * The array can be empty but not null. + */ + ISystemImage[] getSystemImages(); + + /** + * Returns the system image information for the given {@code tag} and {@code abiType}. + * + * @param tag A tag id-display. + * @param abiType An ABI type string. + * @return An existing {@link ISystemImage} for the requested {@code abiType} + * or null if none exists for this type. + */ + @Nullable + ISystemImage getSystemImage(@NonNull IdDisplay tag, @NonNull String abiType); + + /** + * Returns whether the given target is compatible with the receiver. + *

+ * This means that a project using the receiver's target can run on the given target. + *
+ * Example: + *

+     * CupcakeTarget.canRunOn(DonutTarget) == true
+     * 
. + * + * @param target the IAndroidTarget to test. + */ + boolean canRunOn(IAndroidTarget target); + + /** + * Returns a string able to uniquely identify a target. + * Typically the target will encode information such as api level, whether it's a platform + * or add-on, and if it's an add-on vendor and add-on name. + *

+ * See {@link AndroidTargetHash} for helper methods to manipulate hash strings. + */ + String hashString(); +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/ISystemImage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/ISystemImage.java new file mode 100644 index 00000000..b39a9081 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/ISystemImage.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2011 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.sdklib; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdklib.devices.Abi; +import com.android.sdklib.repository.descriptors.IdDisplay; + +import java.io.File; + + +/** + * Describes a system image as used by an {@link IAndroidTarget}. + * A system image has an installation path, a location type and an ABI type. + */ +public interface ISystemImage extends Comparable { + + /** Indicates the type of location for the system image folder in the SDK. */ + enum LocationType { + /** + * The system image is located in the legacy platform's {@link SdkConstants#FD_IMAGES} + * folder. + *

+ * Used by both platform and add-ons. + */ + IN_LEGACY_FOLDER, + + /** + * The system image is located in a sub-directory of the platform's + * {@link SdkConstants#FD_IMAGES} folder, allowing for multiple system + * images within the platform. + *

+ * Used by both platform and add-ons. + */ + IN_IMAGES_SUBFOLDER, + + /** + * The system image is located in the new SDK's {@link SdkConstants#FD_SYSTEM_IMAGES} + * folder. Supported as of Tools R14 and Repository XSD version 5. + *

+ * Used only by both platform up to Tools R22.6. + * Supported for add-ons as of Tools R22.8. + */ + IN_SYSTEM_IMAGE, + } + + /** Returns the actual location of an installed system image. */ + @NonNull + File getLocation(); + + /** Indicates the location strategy for this system image in the SDK. */ + @NonNull + LocationType getLocationType(); + + /** Returns the tag of the system image. */ + @NonNull + IdDisplay getTag(); + + /** Returns the vendor for an add-on's system image, or null for a platform system-image. */ + @Nullable + IdDisplay getAddonVendor(); + + /** + * Returns the ABI type. + * See {@link Abi} for a full list. + * Cannot be null nor empty. + */ + @NonNull + String getAbiType(); + + /** + * Returns the skins embedded in the system image.
+ * Only supported by system images using {@link LocationType#IN_SYSTEM_IMAGE}.
+ * The skins listed here are merged in the {@link IAndroidTarget#getSkins()} list. + * @return A non-null skin list, possibly empty. + */ + @NonNull + File[] getSkins(); +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/LegacyAndroidVersion.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/LegacyAndroidVersion.java new file mode 100644 index 00000000..e6d31e09 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/LegacyAndroidVersion.java @@ -0,0 +1,46 @@ +package com.android.sdklib; + +import java.util.Properties; + +public class LegacyAndroidVersion { + + private AndroidVersion androidVersion; + + public LegacyAndroidVersion(Properties properties) + throws AndroidVersion.AndroidVersionException + { + Exception error = null; + + String apiLevel = properties.getProperty("AndroidVersion.ApiLevel", null); + if (apiLevel != null) { + try + { + String codename = sanitizeCodename(properties.getProperty("AndroidVersion.CodeName", null)); + androidVersion = new AndroidVersion(Integer.parseInt(apiLevel), codename); + return; + } + catch (NumberFormatException e) + { + error = e; + } + } + throw new AndroidVersion.AndroidVersionException("AndroidVersion.ApiLevel not found!", error); + } + + public AndroidVersion getAndroidVersion() + { + return androidVersion; + } + + private static String sanitizeCodename(String codename) + { + if (codename != null) + { + codename = codename.trim(); + if ((codename.isEmpty()) || ("REL".equals(codename))) { + codename = null; + } + } + return codename; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/SdkManager.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/SdkManager.java new file mode 100644 index 00000000..91b57037 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/SdkManager.java @@ -0,0 +1,410 @@ +/* + * 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.sdklib; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.annotations.VisibleForTesting; +import com.android.annotations.VisibleForTesting.Visibility; +import com.android.prefs.AndroidLocation; +import com.android.prefs.AndroidLocation.AndroidLocationException; +import com.android.sdklib.internal.androidTarget.AddOnTarget; +import com.android.sdklib.internal.androidTarget.PlatformTarget; +import com.android.sdklib.repository.FullRevision; +import com.android.sdklib.repository.descriptors.IPkgDesc; +import com.android.sdklib.repository.descriptors.PkgType; +import com.android.sdklib.repository.local.LocalExtraPkgInfo; +import com.android.sdklib.repository.local.LocalPkgInfo; +import com.android.sdklib.repository.local.LocalSdk; +import com.android.utils.ILogger; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +/** + * The SDK manager parses the SDK folder and gives access to the content. + * @see PlatformTarget + * @see AddOnTarget + */ +public class SdkManager { + + @SuppressWarnings("unused") + private static final boolean DEBUG = System.getenv("SDKMAN_DEBUG") != null; //$NON-NLS-1$ + + /** Preference file containing the usb ids for adb */ + private static final String ADB_INI_FILE = "adb_usb.ini"; //$NON-NLS-1$ + //0--------90--------90--------90--------90--------90--------90--------90--------9 + private static final String ADB_INI_HEADER = + "# ANDROID 3RD PARTY USB VENDOR ID LIST -- DO NOT EDIT.\n" + //$NON-NLS-1$ + "# USE 'android update adb' TO GENERATE.\n" + //$NON-NLS-1$ + "# 1 USB VENDOR ID PER LINE.\n"; //$NON-NLS-1$ + + /** Embedded reference to the new local SDK object. */ + private final LocalSdk mLocalSdk; + + /** + * Create a new {@link SdkManager} instance. + * External users should use {@link #createManager(String, ILogger)}. + * + * @param osSdkPath the location of the SDK. + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected SdkManager(@NonNull String osSdkPath) { + mLocalSdk = new LocalSdk(new File(osSdkPath)); + } + + /** + * Creates an @{linkplain SdkManager} for an existing @{link LocalSdk}. + * + * @param localSdk the SDK to use with the SDK manager + */ + private SdkManager(@NonNull LocalSdk localSdk) { + mLocalSdk = localSdk; + } + + /** + * Creates an {@link SdkManager} for a given sdk location. + * @param osSdkPath the location of the SDK. + * @param log the ILogger object receiving warning/error from the parsing. + * @return the created {@link SdkManager} or null if the location is not valid. + */ + @Nullable + public static SdkManager createManager( + @NonNull String osSdkPath, + @NonNull ILogger log) { + try { + SdkManager manager = new SdkManager(osSdkPath); + manager.reloadSdk(log); + + return manager; + } catch (Throwable throwable) { + log.error(throwable, "Error parsing the sdk."); + } + + return null; + } + + /** + * Creates an @{linkplain SdkManager} for an existing @{link LocalSdk}. + * + * @param localSdk the SDK to use with the SDK manager + */ + @NonNull + public static SdkManager createManager(@NonNull LocalSdk localSdk) { + return new SdkManager(localSdk); + } + + @NonNull + public LocalSdk getLocalSdk() { + return mLocalSdk; + } + + /** + * Reloads the content of the SDK. + * + * @param log the ILogger object receiving warning/error from the parsing. + */ + public void reloadSdk(@NonNull ILogger log) { + mLocalSdk.clearLocalPkg(PkgType.PKG_ALL); + } + + /** + * Checks whether any of the SDK platforms/add-ons/build-tools have changed on-disk + * since we last loaded the SDK. This does not reload the SDK nor does it + * change the underlying targets. + * + * @return True if at least one directory or source.prop has changed. + */ + public boolean hasChanged() { + return hasChanged(null); + } + + /** + * Checks whether any of the SDK platforms/add-ons/build-tools have changed on-disk + * since we last loaded the SDK. This does not reload the SDK nor does it + * change the underlying targets. + * + * @param log An optional logger used to print verbose info on what changed. Can be null. + * @return True if at least one directory or source.prop has changed. + */ + public boolean hasChanged(@Nullable ILogger log) { + return mLocalSdk.hasChanged(EnumSet.of(PkgType.PKG_PLATFORM, + PkgType.PKG_ADDON, + PkgType.PKG_BUILD_TOOLS)); + } + + /** + * Returns the location of the SDK. + */ + @NonNull + public String getLocation() { + File f = mLocalSdk.getLocation(); + // Our LocalSdk is created with a file path, so we know the location won't be null. + assert f != null; + return f.getPath(); + } + + /** + * Returns the targets (platforms & addons) that are available in the SDK. + * The target list is created on demand the first time then cached. + * It will not refreshed unless {@link #reloadSdk(ILogger)} is called. + *

+ * The array can be empty but not null. + */ + @NonNull + public IAndroidTarget[] getTargets() { + return mLocalSdk.getTargets(); + } + + /** + * Returns an unmodifiable set of known build-tools revisions. Can be empty but not null. + * Deprecated. I don't think anything uses this. + */ + @Deprecated + @NonNull + public Set getBuildTools() { + LocalPkgInfo[] pkgs = mLocalSdk.getPkgsInfos(PkgType.PKG_BUILD_TOOLS); + TreeSet bt = new TreeSet(); + for (LocalPkgInfo pkg : pkgs) { + IPkgDesc d = pkg.getDesc(); + if (d.hasFullRevision()) { + bt.add(d.getFullRevision()); + } + } + return Collections.unmodifiableSet(bt); + } + + /** + * Returns the highest build-tool revision known. Can be null. + * + * @return The highest build-tool revision known, or null. + */ + @Nullable + public BuildToolInfo getLatestBuildTool() { + return mLocalSdk.getLatestBuildTool(); + } + + /** + * Returns the {@link BuildToolInfo} for the given revision. + * + * @param revision The requested revision. + * @return A {@link BuildToolInfo}. Can be null if {@code revision} is null or is + * not part of the known set returned by {@link #getBuildTools()}. + */ + @Nullable + public BuildToolInfo getBuildTool(@Nullable FullRevision revision) { + return mLocalSdk.getBuildTool(revision); + } + + /** + * Returns a target from a hash that was generated by {@link IAndroidTarget#hashString()}. + * + * @param hash the {@link IAndroidTarget} hash string. + * @return The matching {@link IAndroidTarget} or null. + */ + @Nullable + public IAndroidTarget getTargetFromHashString(@Nullable String hash) { + return mLocalSdk.getTargetFromHashString(hash); + } + + /** + * Updates adb with the USB devices declared in the SDK add-ons. + * @throws AndroidLocationException + * @throws IOException + */ + public void updateAdb() throws AndroidLocationException, IOException { + FileWriter writer = null; + try { + // get the android prefs location to know where to write the file. + File adbIni = new File(AndroidLocation.getFolder(), ADB_INI_FILE); + writer = new FileWriter(adbIni); + + // first, put all the vendor id in an HashSet to remove duplicate. + HashSet set = new HashSet(); + IAndroidTarget[] targets = getTargets(); + for (IAndroidTarget target : targets) { + if (target.getUsbVendorId() != IAndroidTarget.NO_USB_ID) { + set.add(target.getUsbVendorId()); + } + } + + // write file header. + writer.write(ADB_INI_HEADER); + + // now write the Id in a text file, one per line. + for (Integer i : set) { + writer.write(String.format("0x%04x\n", i)); //$NON-NLS-1$ + } + } finally { + if (writer != null) { + writer.close(); + } + } + } + + /** + * Returns the greatest {@link LayoutlibVersion} found amongst all platform + * targets currently loaded in the SDK. + *

+ * We only started recording Layoutlib Versions recently in the platform meta data + * so it's possible to have an SDK with many platforms loaded but no layoutlib + * version defined. + * + * @return The greatest {@link LayoutlibVersion} or null if none is found. + * @deprecated This does NOT solve the right problem and will be changed later. + */ + @Deprecated + @Nullable + public LayoutlibVersion getMaxLayoutlibVersion() { + LayoutlibVersion maxVersion = null; + + for (IAndroidTarget target : getTargets()) { + if (target instanceof PlatformTarget) { + LayoutlibVersion lv = ((PlatformTarget) target).getLayoutlibVersion(); + if (lv != null) { + if (maxVersion == null || lv.compareTo(maxVersion) > 0) { + maxVersion = lv; + } + } + } + } + + return maxVersion; + } + + /** + * Returns a map of the root samples directories located in the SDK/extras packages. + * No guarantee is made that the extras' samples directory actually contain any valid samples. + * The only guarantee is that the root samples directory actually exists. + * The map is { File: Samples root directory => String: Extra package display name. } + * + * @return A non-null possibly empty map of extra samples directories and their associated + * extra package display name. + */ + @NonNull + public Map getExtraSamples() { + + LocalPkgInfo[] pkgsInfos = mLocalSdk.getPkgsInfos(PkgType.PKG_EXTRA); + Map samples = new HashMap(); + + for (LocalPkgInfo info : pkgsInfos) { + assert info instanceof LocalExtraPkgInfo; + + File root = info.getLocalDir(); + File path = new File(root, SdkConstants.FD_SAMPLES); + if (path.isDirectory()) { + samples.put(path, info.getListDescription()); + continue; + } + // Some old-style extras simply have a single "sample" directory. + // Accept it if it contains an AndroidManifest.xml. + path = new File(root, SdkConstants.FD_SAMPLE); + if (path.isDirectory() && + new File(path, SdkConstants.FN_ANDROID_MANIFEST_XML).isFile()) { + samples.put(path, info.getListDescription()); + } + } + + return samples; + } + + /** + * Returns a map of all the extras found in the local SDK with their major revision. + *

+ * Map keys are in the form "vendor-id/path-id". These ids uniquely identify an extra package. + * The version is the incremental integer major revision of the package. + * + * @return A non-null possibly empty map of { string "vendor/path" => integer major revision } + * @deprecated Starting with add-on schema 6, extras can have full revisions instead of just + * major revisions. This API only returns the major revision. Callers should be modified + * to use the new {code LocalSdk.getPkgInfo(PkgType.PKG_EXTRAS)} API instead. + */ + @Deprecated + @NonNull + public Map getExtrasVersions() { + LocalPkgInfo[] pkgsInfos = mLocalSdk.getPkgsInfos(PkgType.PKG_EXTRA); + Map extraVersions = new TreeMap(); + + for (LocalPkgInfo info : pkgsInfos) { + assert info instanceof LocalExtraPkgInfo; + if (info instanceof LocalExtraPkgInfo) { + LocalExtraPkgInfo ei = (LocalExtraPkgInfo) info; + IPkgDesc d = ei.getDesc(); + String vendor = d.getVendor().getId(); + String path = d.getPath(); + int majorRev = d.getFullRevision().getMajor(); + + extraVersions.put(vendor + '/' + path, majorRev); + } + } + + return extraVersions; + } + + /** Returns the platform tools version if installed, null otherwise. */ + @Nullable + public String getPlatformToolsVersion() { + LocalPkgInfo info = mLocalSdk.getPkgInfo(PkgType.PKG_PLATFORM_TOOLS); + IPkgDesc d = info == null ? null : info.getDesc(); + if (d != null && d.hasFullRevision()) { + return d.getFullRevision().toShortString(); + } + + return null; + } + + + // ------------- + + public static class LayoutlibVersion implements Comparable { + private final int mApi; + private final int mRevision; + + public static final int NOT_SPECIFIED = 0; + + public LayoutlibVersion(int api, int revision) { + mApi = api; + mRevision = revision; + } + + public int getApi() { + return mApi; + } + + public int getRevision() { + return mRevision; + } + + @Override + public int compareTo(@NonNull LayoutlibVersion rhs) { + boolean useRev = this.mRevision > NOT_SPECIFIED && rhs.mRevision > NOT_SPECIFIED; + int lhsValue = (this.mApi << 16) + (useRev ? this.mRevision : 0); + int rhsValue = (rhs.mApi << 16) + (useRev ? rhs.mRevision : 0); + return lhsValue - rhsValue; + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/SdkVersionInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/SdkVersionInfo.java new file mode 100644 index 00000000..cfedbf1a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/SdkVersionInfo.java @@ -0,0 +1,351 @@ +/* + * Copyright (C) 2013 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.sdklib; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.google.common.base.Strings; + +import java.util.Locale; + +/** Information about available SDK Versions */ +public class SdkVersionInfo { + /** + * The highest known API level. Note that the tools may also look at the + * installed platforms to see if they can find more recently released + * platforms, e.g. when the tools have not yet been updated for a new + * release. This number is used as a baseline and any more recent platforms + * found can be used to increase the highest known number. + */ + public static final int HIGHEST_KNOWN_API = 23; + + /** + * Like {@link #HIGHEST_KNOWN_API} but does not include preview platforms + */ + public static final int HIGHEST_KNOWN_STABLE_API = 22; + + /** + * The lowest active API level in the ecosystem. This number will change over time + * as the distribution of older platforms decreases. + */ + public static final int LOWEST_ACTIVE_API = 8; + + /** + * Returns the Android version and code name of the given API level, or null + * if not known. The highest number (inclusive) that is supported + * is {@link SdkVersionInfo#HIGHEST_KNOWN_API}. + * + * @param api the api level + * @return a suitable version display name + */ + @Nullable + public static String getAndroidName(int api) { + // See http://source.android.com/source/build-numbers.html + String codeName = getCodeName(api); + String name = getVersionString(api); + if (name == null) { + return String.format("API %1$d", api); + } else if (codeName == null) { + return String.format("API %1$d: Android %2$s", api, name); + } else { + return String.format("API %1$d: Android %2$s (%3$s)", api, name, codeName); + } + } + + @Nullable + public static String getVersionString(int api) { + switch (api) { + case 1: return "1.0"; + case 2: return "1.1"; + case 3: return "1.5"; + case 4: return "1.6"; + case 5: return "2.0"; + case 6: return "2.0.1"; + case 7: return "2.1"; + case 8: return "2.2"; + case 9: return "2.3"; + case 10: return "2.3.3"; + case 11: return "3.0"; + case 12: return "3.1"; + case 13: return "3.2"; + case 14: return "4.0"; + case 15: return "4.0.3"; + case 16: return "4.1"; + case 17: return "4.2"; + case 18: return "4.3"; + case 19: return "4.4"; + case 20: return "4.4"; + case 21: return "5.0"; + case 22: return "5.1"; + case 23: return "5.X"; + // If you add more versions here, also update #getBuildCodes and + // #HIGHEST_KNOWN_API + + default: return null; + } + } + + @Nullable + public static String getCodeName(int api) { + switch (api) { + case 1: + case 2: + return null; + case 3: + return "Cupcake"; + case 4: + return "Donut"; + case 5: + case 6: + case 7: + return "Eclair"; + case 8: + return "Froyo"; + case 9: + case 10: + return "Gingerbread"; + case 11: + case 12: + case 13: + return "Honeycomb"; + case 14: + case 15: + return "IceCreamSandwich"; + case 16: + case 17: + case 18: + return "Jelly Bean"; + case 19: + return "KitKat"; + case 20: + return "KitKat Wear"; + case 21: + case 22: + return "Lollipop"; + case 23: + return "MNC"; + + // If you add more versions here, also update #getBuildCodes and + // #HIGHEST_KNOWN_API + + default: return null; + } + } + + /** + * Returns the applicable build code (for + * {@code android.os.Build.VERSION_CODES}) for the corresponding API level, + * or null if it's unknown. The highest number (inclusive) that is supported + * is {@link SdkVersionInfo#HIGHEST_KNOWN_API}. + * + * @param api the API level to look up a version code for + * @return the corresponding build code field name, or null + */ + @Nullable + public static String getBuildCode(int api) { + // See http://developer.android.com/reference/android/os/Build.VERSION_CODES.html + switch (api) { + case 1: return "BASE"; //$NON-NLS-1$ + case 2: return "BASE_1_1"; //$NON-NLS-1$ + case 3: return "CUPCAKE"; //$NON-NLS-1$ + case 4: return "DONUT"; //$NON-NLS-1$ + case 5: return "ECLAIR"; //$NON-NLS-1$ + case 6: return "ECLAIR_0_1"; //$NON-NLS-1$ + case 7: return "ECLAIR_MR1"; //$NON-NLS-1$ + case 8: return "FROYO"; //$NON-NLS-1$ + case 9: return "GINGERBREAD"; //$NON-NLS-1$ + case 10: return "GINGERBREAD_MR1"; //$NON-NLS-1$ + case 11: return "HONEYCOMB"; //$NON-NLS-1$ + case 12: return "HONEYCOMB_MR1"; //$NON-NLS-1$ + case 13: return "HONEYCOMB_MR2"; //$NON-NLS-1$ + case 14: return "ICE_CREAM_SANDWICH"; //$NON-NLS-1$ + case 15: return "ICE_CREAM_SANDWICH_MR1"; //$NON-NLS-1$ + case 16: return "JELLY_BEAN"; //$NON-NLS-1$ + case 17: return "JELLY_BEAN_MR1"; //$NON-NLS-1$ + case 18: return "JELLY_BEAN_MR2"; //$NON-NLS-1$ + case 19: return "KITKAT"; //$NON-NLS-1$ + case 20: return "KITKAT_WATCH"; //$NON-NLS-1$ + case 21: return "LOLLIPOP"; //$NON-NLS-1$ + case 22: return "LOLLIPOP_MR1"; //$NON-NLS-1$ + case 23: return "MNC"; //$NON-NLS-1$ + // If you add more versions here, also update #getAndroidName and + // #HIGHEST_KNOWN_API + } + + return null; + } + + /** + * Returns the API level of the given build code (e.g. JELLY_BEAN_MR1 => 17), or -1 if not + * recognized + * + * @param buildCode the build code name (not case sensitive) + * @param recognizeUnknowns if true, treat an unrecognized code name as a newly released + * platform the tools are not yet aware of, and set its API level to + * some higher number than all the currently known API versions + * @return the API level, or -1 if not recognized (unless recognizeUnknowns is true, in which + * {@link #HIGHEST_KNOWN_API} plus one is returned + */ + public static int getApiByBuildCode(@NonNull String buildCode, boolean recognizeUnknowns) { + for (int api = 1; api <= HIGHEST_KNOWN_API; api++) { + String code = getBuildCode(api); + if (code != null && code.equalsIgnoreCase(buildCode)) { + return api; + } + } + + if (buildCode.equalsIgnoreCase("L")) { + return 21; // For now the Build class also provides this as an alias to Lollipop + } + + return recognizeUnknowns ? HIGHEST_KNOWN_API + 1 : -1; + } + + /** + * Returns the API level of the given preview code name (e.g. JellyBeanMR2 => 17), or -1 if not + * recognized + * + * @param previewName the preview name (not case sensitive) + * @param recognizeUnknowns if true, treat an unrecognized code name as a newly released + * platform the tools are not yet aware of, and set its API level to + * some higher number than all the currently known API versions + * @return the API level, or -1 if not recognized (unless recognizeUnknowns is true, in which + * {@link #HIGHEST_KNOWN_API} plus one is returned + */ + public static int getApiByPreviewName(@NonNull String previewName, boolean recognizeUnknowns) { + // JellyBean => JELLY_BEAN + String codeName = camelCaseToUnderlines(previewName).toUpperCase(Locale.US); + return getApiByBuildCode(codeName, recognizeUnknowns); + } + + /** + * Converts a CamelCase word into an underlined_word + * + * @param string the CamelCase version of the word + * @return the underlined version of the word + */ + @NonNull + public static String camelCaseToUnderlines(@NonNull String string) { + if (string.isEmpty()) { + return string; + } + + StringBuilder sb = new StringBuilder(2 * string.length()); + int n = string.length(); + boolean lastWasUpperCase = Character.isUpperCase(string.charAt(0)); + for (int i = 0; i < n; i++) { + char c = string.charAt(i); + boolean isUpperCase = Character.isUpperCase(c); + if (isUpperCase && !lastWasUpperCase) { + sb.append('_'); + } + lastWasUpperCase = isUpperCase; + c = Character.toLowerCase(c); + sb.append(c); + } + + return sb.toString(); + } + + /** + * Converts an underlined_word into a CamelCase word + * + * @param string the underlined word to convert + * @return the CamelCase version of the word + */ + @NonNull + public static String underlinesToCamelCase(@NonNull String string) { + StringBuilder sb = new StringBuilder(string.length()); + int n = string.length(); + + int i = 0; + @SuppressWarnings("SpellCheckingInspection") + boolean upcaseNext = true; + for (; i < n; i++) { + char c = string.charAt(i); + if (c == '_') { + upcaseNext = true; + } else { + if (upcaseNext) { + c = Character.toUpperCase(c); + } + upcaseNext = false; + sb.append(c); + } + } + + return sb.toString(); + } + + /** + * Returns the {@link AndroidVersion} for a given version string, which is typically an API + * level number, but can also be a codename for a preview platform. Note: This should + * not be used to look up version names for build codes; for that, use {@link + * #getApiByBuildCode(String, boolean)}. The primary difference between this method is that + * {@link #getApiByBuildCode(String, boolean)} will return the final API number for a platform + * (e.g. for "KITKAT" it will return 19) whereas this method will return the API number for the + * codename as a preview platform (e.g. 18). + * + * @param apiOrPreviewName the version string + * @param targets an optional array of installed targets, if available. If the version + * string corresponds to a code name, this is used to search for a + * corresponding API level. + * @return an {@link com.android.sdklib.AndroidVersion}, or null if the version could not be + * determined (e.g. an empty or invalid API number or an unknown code name) + */ + @Nullable + public static AndroidVersion getVersion( + @Nullable String apiOrPreviewName, + @Nullable IAndroidTarget[] targets) { + if (Strings.isNullOrEmpty(apiOrPreviewName)) { + return null; + } + + if (Character.isDigit(apiOrPreviewName.charAt(0))) { + try { + int api = Integer.parseInt(apiOrPreviewName); + if (api >= 1) { + return new AndroidVersion(api, null); + } + return null; + } catch (NumberFormatException e) { + // Invalid version string + return null; + } + } + + // Codename + if (targets != null) { + for (int i = targets.length - 1; i >= 0; i--) { + IAndroidTarget target = targets[i]; + if (target.isPlatform()) { + AndroidVersion version = target.getVersion(); + if (version.isPreview() && apiOrPreviewName.equalsIgnoreCase(version.getCodename())) { + return new AndroidVersion(version.getApiLevel(), version.getCodename()); + } + } + } + } + + int api = getApiByPreviewName(apiOrPreviewName, false); + if (api != -1) { + return new AndroidVersion(api - 1, apiOrPreviewName); + } + + // Must be a future SDK platform + return new AndroidVersion(HIGHEST_KNOWN_API, apiOrPreviewName); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/SystemImage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/SystemImage.java new file mode 100644 index 00000000..79c9c41f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/SystemImage.java @@ -0,0 +1,325 @@ +/* + * Copyright (C) 2011 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.sdklib; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdklib.devices.Abi; +import com.android.sdklib.internal.androidTarget.PlatformTarget; +import com.android.sdklib.io.LegacyFileOp; +import com.android.sdklib.repository.descriptors.IdDisplay; +import com.google.common.base.Objects; + +import java.io.File; +import java.util.Arrays; +import java.util.Locale; + + +/** + * Describes a system image as used by an {@link IAndroidTarget}. + * A system image has an installation path, a location type, a tag and an ABI type. + */ +public class SystemImage implements ISystemImage { + + public static final IdDisplay DEFAULT_TAG = new IdDisplay("default", //$NON-NLS-1$ + "Default"); //$NON-NLS-1$ + + @Deprecated + private final LocationType mLocationtype; + private final IdDisplay mTag; + private final IdDisplay mAddonVendor; + private final String mAbiType; + private final File mLocation; + private final File[] mSkins; + + /** + * Creates a {@link SystemImage} description for an existing platform system image folder. + * + * @param location The location of an installed system image. + * @param locationType Where the system image folder is located for this ABI. + * @param tag The tag of the system-image. Use {@link #DEFAULT_TAG} for backward compatibility. + * @param abiType The ABI type. For example, one of {@link SdkConstants#ABI_ARMEABI}, + * {@link SdkConstants#ABI_ARMEABI_V7A}, {@link SdkConstants#ABI_INTEL_ATOM} or + * {@link SdkConstants#ABI_MIPS}. + * @param skins A non-null, possibly empty list of skins specific to this system image. + */ + public SystemImage( + @NonNull File location, + @NonNull LocationType locationType, + @NonNull IdDisplay tag, + @NonNull String abiType, + @NonNull File[] skins) { + this(location, locationType, tag, null /*addonVendor*/, abiType, skins); + } + + /** + * Creates a {@link SystemImage} description for an existing system image folder, + * for either platform or add-on. + * + * @param location The location of an installed system image. + * @param locationType Where the system image folder is located for this ABI. + * @param tagName The tag of the system-image. + * For an add-on, the tag-id must match the add-on's name-id. + * @param addonVendor Non-null add-on vendor name. Null for platforms. + * @param abiType The ABI type. For example, one of {@link SdkConstants#ABI_ARMEABI}, + * {@link SdkConstants#ABI_ARMEABI_V7A}, {@link SdkConstants#ABI_INTEL_ATOM} or + * {@link SdkConstants#ABI_MIPS}. + * @param skins A non-null, possibly empty list of skins specific to this system image. + */ + public SystemImage( + @NonNull File location, + @NonNull LocationType locationType, + @NonNull IdDisplay tagName, + @Nullable IdDisplay addonVendor, + @NonNull String abiType, + @NonNull File[] skins) { + mLocation = location; + mLocationtype = locationType; + mTag = tagName; + mAddonVendor = addonVendor; + mAbiType = abiType; + mSkins = skins; + } + + /** + * Creates a {@link SystemImage} description for a non-existing platform system image folder. + * The actual location is computed based on the {@code locationType}. + * + * @param sdkManager The current SDK manager. + * @param locationType Where the system image folder is located for this ABI. + * @param tag The tag of the system-image. Use {@link #DEFAULT_TAG} for backward compatibility. + * @param abiType The ABI type. For example, one of {@link SdkConstants#ABI_ARMEABI}, + * {@link SdkConstants#ABI_ARMEABI_V7A}, {@link SdkConstants#ABI_INTEL_ATOM} or + * {@link SdkConstants#ABI_MIPS}. + * @param skins A non-null, possibly empty list of skins specific to this system image. + * @throws IllegalArgumentException if the {@code target} used for + * {@link ISystemImage.LocationType#IN_SYSTEM_IMAGE} is not a {@link PlatformTarget}. + */ + public SystemImage( + @NonNull SdkManager sdkManager, + @NonNull IAndroidTarget target, + @NonNull LocationType locationType, + @NonNull IdDisplay tag, + @NonNull String abiType, + @NonNull File[] skins) { + this(sdkManager, target, locationType, tag, null /*addonVendor*/, abiType, skins); + } + + + /** + * Creates a {@link SystemImage} description for a non-existing system image folder, + * for either platform or add-on. + * The actual location is computed based on the {@code locationType}. + * + * @param sdkManager The current SDK manager. + * @param locationType Where the system image folder is located for this ABI. + * @param tag The tag of the system-image. Use {@link #DEFAULT_TAG} for backward compatibility. + * @param addonVendor Non-null add-on vendor name. Null for platforms. + * @param abiType The ABI type. For example, one of {@link SdkConstants#ABI_ARMEABI}, + * {@link SdkConstants#ABI_ARMEABI_V7A}, {@link SdkConstants#ABI_INTEL_ATOM} or + * {@link SdkConstants#ABI_MIPS}. + * @param skins A non-null, possibly empty list of skins specific to this system image. + * @throws IllegalArgumentException if the {@code target} used for + * {@link ISystemImage.LocationType#IN_SYSTEM_IMAGE} is not a {@link PlatformTarget}. + */ + public SystemImage( + @NonNull SdkManager sdkManager, + @NonNull IAndroidTarget target, + @NonNull LocationType locationType, + @NonNull IdDisplay tag, + @Nullable IdDisplay addonVendor, + @NonNull String abiType, + @NonNull File[] skins) { + mLocationtype = locationType; + mTag = tag; + mAddonVendor = addonVendor; + mAbiType = abiType; + mSkins = skins; + + File location = null; + switch(locationType) { + case IN_LEGACY_FOLDER: + location = new File(target.getLocation(), SdkConstants.OS_IMAGES_FOLDER); + break; + + case IN_IMAGES_SUBFOLDER: + location = LegacyFileOp.append(target.getLocation(), SdkConstants.OS_IMAGES_FOLDER, abiType); + break; + + case IN_SYSTEM_IMAGE: + location = getCanonicalFolder(sdkManager.getLocation(), + target.getVersion(), + tag.getId(), + addonVendor == null ? null : addonVendor.getId(), + abiType); + break; + default: + // This is not supposed to happen unless LocationType is + // extended without adjusting this code. + assert false : "SystemImage used with an incorrect locationType"; //$NON-NLS-1$ + } + mLocation = location; + } + + /** + * Static helper method that returns the canonical path for a system-image that uses + * the {@link ISystemImage.LocationType#IN_SYSTEM_IMAGE} location type. + *

+ * Such an image is located in {@code SDK/system-images/android-N/tag/abiType}. + * For this reason this method requires the root SDK as well as the platform, tag abd ABI type. + * + * @param sdkOsPath The OS path to the SDK. + * @param platformVersion The platform version. + * @param tagId An optional tag. If null, not tag folder is used. + * For legacy, use {@code SystemImage.DEFAULT_TAG.getId()}. + * @param addonVendorId An optional vendor-id for an add-on. If null, it's a platform sys-img. + * @param abiType An optional ABI type. If null, the parent directory is returned. + * @return A file that represents the location of the canonical system-image folder + * for this configuration. + */ + @NonNull + private static File getCanonicalFolder( + @NonNull String sdkOsPath, + @NonNull AndroidVersion platformVersion, + @Nullable String tagId, + @Nullable String addonVendorId, + @Nullable String abiType) { + File root; + if (addonVendorId == null) { + root = LegacyFileOp.append( + sdkOsPath, + SdkConstants.FD_SYSTEM_IMAGES, + AndroidTargetHash.getPlatformHashString(platformVersion)); + if (tagId != null) { + root = LegacyFileOp.append(root, tagId); + } + } else { + root = LegacyFileOp.append( + sdkOsPath, + SdkConstants.FD_SYSTEM_IMAGES, + AndroidTargetHash.getAddonHashString(addonVendorId, tagId, platformVersion)); + } + if (abiType == null) { + return root; + } else { + return LegacyFileOp.append(root, abiType); + } + } + + /** Returns the actual location of an installed system image. */ + @NonNull + @Override + public File getLocation() { + return mLocation; + } + + /** + * Indicates the location strategy for this system image in the SDK. + * @deprecated + */ + @NonNull + @Override + public LocationType getLocationType() { + return mLocationtype; + } + + /** Returns the tag of the system image. */ + @NonNull + @Override + public IdDisplay getTag() { + return mTag; + } + + /** Returns the vendor for an add-on's system image, or null for a platform system-image. */ + @Nullable + @Override + public IdDisplay getAddonVendor() { + return mAddonVendor; + } + + /** + * Returns the ABI type. + * See {@link Abi} for a full list. + * Cannot be null nor empty. + */ + @NonNull + @Override + public String getAbiType() { + return mAbiType; + } + + @NonNull + @Override + public File[] getSkins() { + return mSkins; + } + + /** + * Sort by tag & ABI name only. This is what matters from a user point of view. + */ + @Override + public int compareTo(ISystemImage other) { + int t = this.getTag().compareTo(other.getTag()); + if (t != 0) { + return t; + } + return this.getAbiType().compareToIgnoreCase(other.getAbiType()); + } + + /** + * Generates a string representation suitable for debug purposes. + * The string is not intended to be displayed to the user. + * + * {@inheritDoc} + */ + @NonNull + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("SystemImage"); + if (mAddonVendor != null) { + sb.append(" addon-vendor=").append(mAddonVendor.getId()).append(','); + } + sb.append(" tag=").append(mTag.getId()); + sb.append(", ABI=").append(mAbiType); + sb.append(", location ") + .append(mLocationtype.toString().replace('_', ' ').toLowerCase(Locale.US)) + .append("='") + .append(mLocation) + .append("'"); + return sb.toString(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof SystemImage)) { + return false; + } + SystemImage other = (SystemImage)o; + return mTag.equals(other.mTag) && mAbiType.equals(other.getAbiType()) && Objects.equal(mAddonVendor, other.mAddonVendor) + && mLocation.equals(other.mLocation) && Arrays.equals(mSkins, other.mSkins); + } + + @Override + public int hashCode() { + int hashCode = Objects.hashCode(mTag, mAbiType, mAddonVendor, mLocation); + hashCode *= 37; + hashCode += Arrays.hashCode(mSkins); + return hashCode; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/androidTarget/AddOnTarget.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/androidTarget/AddOnTarget.java new file mode 100644 index 00000000..83a3e554 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/androidTarget/AddOnTarget.java @@ -0,0 +1,463 @@ +/* + * 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.sdklib.internal.androidTarget; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdklib.AndroidTargetHash; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.BuildToolInfo; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.ISystemImage; +import com.android.sdklib.repository.descriptors.IdDisplay; +import com.google.common.collect.ImmutableList; + +import java.io.File; +import java.io.FileFilter; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +/** + * Represents an add-on target in the SDK. + * An add-on extends a standard {@link PlatformTarget}. + */ +public final class AddOnTarget implements IAndroidTarget { + + private final String mLocation; + private final PlatformTarget mBasePlatform; + private final String mName; + private final ISystemImage[] mSystemImages; + private final String mVendor; + private final int mRevision; + private final String mDescription; + private final boolean mHasRenderingLibrary; + private final boolean mHasRenderingResources; + + private File[] mSkins; + private File mDefaultSkin; + private ImmutableList mLibraries; + private int mVendorId = NO_USB_ID; + + /** + * Creates a new add-on + * @param location the OS path location of the add-on + * @param name the name of the add-on + * @param vendor the vendor name of the add-on + * @param revision the revision of the add-on + * @param description the add-on description + * @param systemImages list of supported system images. Can be null or empty. + * @param libMap A map containing the optional libraries. The map key is the fully-qualified + * library name. The value is a 2 string array with the .jar filename, and the description. + * @param hasRenderingLibrary whether the addon has a custom layoutlib.jar + * @param hasRenderingResources whether the add has custom framework resources. + * @param basePlatform the platform the add-on is extending. + */ + public AddOnTarget( + String location, + String name, + String vendor, + int revision, + String description, + ISystemImage[] systemImages, + Map libMap, + boolean hasRenderingLibrary, + boolean hasRenderingResources, + PlatformTarget basePlatform) { + if (!location.endsWith(File.separator)) { + location = location + File.separator; + } + + mLocation = location; + mName = name; + mVendor = vendor; + mRevision = revision; + mDescription = description; + mHasRenderingLibrary = hasRenderingLibrary; + mHasRenderingResources = hasRenderingResources; + mBasePlatform = basePlatform; + + // If the add-on does not have any system-image of its own, the list here + // is empty and it's up to the callers to query the parent platform. + mSystemImages = systemImages == null ? new ISystemImage[0] : systemImages; + Arrays.sort(mSystemImages); + + // handle the optional libraries. + if (libMap != null) { + ImmutableList.Builder builder = ImmutableList.builder(); + for (Entry entry : libMap.entrySet()) { + String jarFile = entry.getValue()[0]; + String desc = entry.getValue()[1]; + builder.add(new OptionalLibraryImpl( + entry.getKey(), + new File(mLocation, SdkConstants.OS_ADDON_LIBS_FOLDER + jarFile), + desc, + true /*requireManifestEntry*/)); + } + mLibraries = builder.build(); + } else { + mLibraries = ImmutableList.of(); + } + } + + @Override + public String getLocation() { + return mLocation; + } + + @Override + public String getName() { + return mName; + } + + @Override + @Nullable + public ISystemImage getSystemImage(@NonNull IdDisplay tag, @NonNull String abiType) { + for (ISystemImage sysImg : mSystemImages) { + if (sysImg.getTag().equals(tag) && sysImg.getAbiType().equals(abiType)) { + return sysImg; + } + } + return null; + } + + @Override + public ISystemImage[] getSystemImages() { + return mSystemImages; + } + + @Override + public String getVendor() { + return mVendor; + } + + @Override + public String getFullName() { + return String.format("%1$s (%2$s)", mName, mVendor); + } + + @Override + public String getClasspathName() { + return String.format("%1$s [%2$s]", mName, mBasePlatform.getClasspathName()); + } + + @Override + public String getShortClasspathName() { + return String.format("%1$s [%2$s]", mName, mBasePlatform.getVersionName()); + } + + @Override + public String getDescription() { + return mDescription; + } + + @NonNull + @Override + public AndroidVersion getVersion() { + // this is always defined by the base platform + return mBasePlatform.getVersion(); + } + + @Override + public String getVersionName() { + return mBasePlatform.getVersionName(); + } + + @Override + public int getRevision() { + return mRevision; + } + + @Override + public boolean isPlatform() { + return false; + } + + @Override + public IAndroidTarget getParent() { + return mBasePlatform; + } + + @Override + public String getPath(int pathId) { + switch (pathId) { + case SKINS: + return mLocation + SdkConstants.OS_SKINS_FOLDER; + case DOCS: + return mLocation + SdkConstants.FD_DOCS + File.separator + + SdkConstants.FD_DOCS_REFERENCE; + + case LAYOUT_LIB: + if (mHasRenderingLibrary) { + return mLocation + SdkConstants.FD_DATA + File.separator + + SdkConstants.FN_LAYOUTLIB_JAR; + } + return mBasePlatform.getPath(pathId); + + case RESOURCES: + if (mHasRenderingResources) { + return mLocation + SdkConstants.FD_DATA + File.separator + + SdkConstants.FD_RES; + } + return mBasePlatform.getPath(pathId); + + case FONTS: + if (mHasRenderingResources) { + return mLocation + SdkConstants.FD_DATA + File.separator + + SdkConstants.FD_FONTS; + } + return mBasePlatform.getPath(pathId); + + case SAMPLES: + // only return the add-on samples folder if there is actually a sample (or more) + File sampleLoc = new File(mLocation, SdkConstants.FD_SAMPLES); + if (sampleLoc.isDirectory()) { + File[] files = sampleLoc.listFiles(new FileFilter() { + @Override + public boolean accept(File pathname) { + return pathname.isDirectory(); + } + + }); + if (files != null && files.length > 0) { + return sampleLoc.getAbsolutePath(); + } + } + //$FALL-THROUGH$ + default : + return mBasePlatform.getPath(pathId); + } + } + + @Override + public File getFile(int pathId) { + return new File(getPath(pathId)); + } + + @Override + public BuildToolInfo getBuildToolInfo() { + return mBasePlatform.getBuildToolInfo(); + } + + @Override @NonNull + public List getBootClasspath() { + return mBasePlatform.getBootClasspath(); + } + + @NonNull + @Override + public List getOptionalLibraries() { + return mBasePlatform.getOptionalLibraries(); + } + + @NonNull + @Override + public List getAdditionalLibraries() { + return mLibraries; + } + + @Override + public boolean hasRenderingLibrary() { + return mHasRenderingLibrary || mHasRenderingResources; + } + + @NonNull + @Override + public File[] getSkins() { + return mSkins; + } + + @Nullable + @Override + public File getDefaultSkin() { + return mDefaultSkin; + } + + /** + * Returns the list of libraries of the underlying platform. + * + * {@inheritDoc} + */ + @Override + public String[] getPlatformLibraries() { + return mBasePlatform.getPlatformLibraries(); + } + + @Override + public String getProperty(String name) { + return mBasePlatform.getProperty(name); + } + + @Override + public Integer getProperty(String name, Integer defaultValue) { + return mBasePlatform.getProperty(name, defaultValue); + } + + @Override + public Boolean getProperty(String name, Boolean defaultValue) { + return mBasePlatform.getProperty(name, defaultValue); + } + + @Override + public Map getProperties() { + return mBasePlatform.getProperties(); + } + + @Override + public int getUsbVendorId() { + return mVendorId; + } + + @Override + public boolean canRunOn(IAndroidTarget target) { + // basic test + if (target == this) { + return true; + } + + /* + * The method javadoc indicates: + * Returns whether the given target is compatible with the receiver. + *

A target is considered compatible if applications developed for the receiver can + * run on the given target. + */ + + // The receiver is an add-on. There are 2 big use cases: The add-on has libraries + // or the add-on doesn't (in which case we consider it a platform). + if (mLibraries.isEmpty()) { + return mBasePlatform.canRunOn(target); + } else { + // the only targets that can run the receiver are the same add-on in the same or later + // versions. + // first check: vendor/name + if (!mVendor.equals(target.getVendor()) || !mName.equals(target.getName())) { + return false; + } + + // now check the version. At this point since we checked the add-on part, + // we can revert to the basic check on version/codename which are done by the + // base platform already. + return mBasePlatform.canRunOn(target); + } + + } + + @Override + public String hashString() { + return String.format(AndroidTargetHash.ADD_ON_FORMAT, mVendor, mName, + mBasePlatform.getVersion().getApiString()); + } + + @Override + public int hashCode() { + return hashString().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof AddOnTarget) { + AddOnTarget addon = (AddOnTarget)obj; + + return mVendor.equals(addon.mVendor) && mName.equals(addon.mName) && + mBasePlatform.getVersion().equals(addon.mBasePlatform.getVersion()); + } + + return false; + } + + /* + * Order by API level (preview/n count as between n and n+1). + * At the same API level, order as: Platform first, then add-on ordered by vendor and then name + * (non-Javadoc) + * @see java.lang.Comparable#compareTo(java.lang.Object) + */ + @Override + public int compareTo(@NonNull IAndroidTarget target) { + // quick check. + if (this == target) { + return 0; + } + + int versionDiff = getVersion().compareTo(target.getVersion()); + + // only if the version are the same do we care about platform/add-ons. + if (versionDiff == 0) { + // platforms go before add-ons. + if (target.isPlatform()) { + return +1; + } else { + AddOnTarget targetAddOn = (AddOnTarget)target; + + // both are add-ons of the same version. Compare per vendor then by name + int vendorDiff = mVendor.compareTo(targetAddOn.mVendor); + if (vendorDiff == 0) { + return mName.compareTo(targetAddOn.mName); + } else { + return vendorDiff; + } + } + + } + + return versionDiff; + } + + /** + * Returns a string representation suitable for debugging. + * The representation is not intended for display to the user. + * + * The representation is also purposely compact. It does not describe _all_ the properties + * of the target, only a few key ones. + * + * @see #getDescription() + */ + @Override + public String toString() { + return String.format("AddonTarget %1$s rev %2$d (based on %3$s)", //$NON-NLS-1$ + getVersion(), + getRevision(), + getParent().toString()); + } + + // ---- local methods. + + public void setSkins(@NonNull File[] skins, @NonNull File defaultSkin) { + mDefaultSkin = defaultSkin; + + // we mix the add-on and base platform skins + HashSet skinSet = new HashSet(); + skinSet.addAll(Arrays.asList(skins)); + skinSet.addAll(Arrays.asList(mBasePlatform.getSkins())); + + mSkins = skinSet.toArray(new File[skinSet.size()]); + Arrays.sort(mSkins); + } + + /** + * Sets the USB vendor id in the add-on. + */ + public void setUsbVendorId(int vendorId) { + if (vendorId == 0) { + throw new IllegalArgumentException( "VendorId must be > 0"); + } + + mVendorId = vendorId; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/androidTarget/MissingTarget.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/androidTarget/MissingTarget.java new file mode 100644 index 00000000..4454040d --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/androidTarget/MissingTarget.java @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2015 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.sdklib.internal.androidTarget; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdklib.AndroidTargetHash; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.BuildToolInfo; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.ISystemImage; +import com.android.sdklib.SdkVersionInfo; +import com.android.sdklib.repository.descriptors.IdDisplay; +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; + +import java.io.File; +import java.util.List; +import java.util.Map; + +/** + * A target that we don't have, but is referenced (e.g. by a system image). + */ +public class MissingTarget implements IAndroidTarget { + + private final String mVendor; + + private final AndroidVersion mVersion; + + private final List mSystemImages = Lists.newArrayList(); + + private final String mName; + + public MissingTarget(String vendor, String name, AndroidVersion version) { + mVendor = vendor; + mVersion = version; + mName = name; + } + + @Override + public String getLocation() { + return null; + } + + @Override + public String getVendor() { + return mVendor; + } + + @Override + public String getName() { + return mName; + } + + @Override + public String getFullName() { + return getName(); + } + + @Override + public String getClasspathName() { + return null; + } + + @Override + public String getShortClasspathName() { + return null; + } + + @Override + public String getDescription() { + return null; + } + + @NonNull + @Override + public AndroidVersion getVersion() { + return mVersion; + } + + @Override + public String getVersionName() { + return SdkVersionInfo.getAndroidName(getVersion().getApiLevel()); + } + + @Override + public int getRevision() { + return 0; + } + + @Override + public boolean isPlatform() { + return mVendor == null; + } + + @Override + public IAndroidTarget getParent() { + return null; + } + + @Override + public String getPath(int pathId) { + return null; + } + + @Override + public File getFile(int pathId) { + return null; + } + + @Override + public BuildToolInfo getBuildToolInfo() { + return null; + } + + @NonNull + @Override + public List getBootClasspath() { + return ImmutableList.of(); + } + + @NonNull + @Override + public List getOptionalLibraries() { + return ImmutableList.of(); + } + + @NonNull + @Override + public List getAdditionalLibraries() { + return ImmutableList.of(); + } + + @Override + public boolean hasRenderingLibrary() { + return false; + } + + @NonNull + @Override + public File[] getSkins() { + return new File[0]; + } + + @Nullable + @Override + public File getDefaultSkin() { + return null; + } + + @Override + public String[] getPlatformLibraries() { + return new String[0]; + } + + @Override + public String getProperty(String name) { + return null; + } + + @Override + public Integer getProperty(String name, Integer defaultValue) { + return null; + } + + @Override + public Boolean getProperty(String name, Boolean defaultValue) { + return null; + } + + @Override + public Map getProperties() { + return null; + } + + @Override + public int getUsbVendorId() { + return 0; + } + + @Override + public ISystemImage[] getSystemImages() { + return mSystemImages.toArray(new ISystemImage[mSystemImages.size()]); + } + + public void addSystemImage(ISystemImage image) { + mSystemImages.add(image); + } + + @Nullable + @Override + public ISystemImage getSystemImage(@NonNull IdDisplay tag, @NonNull String abiType) { + for (ISystemImage image : mSystemImages) { + if (tag.equals(image.getTag()) && abiType.equals(image.getAbiType())) { + return image; + } + } + return null; + } + + @Override + public boolean canRunOn(IAndroidTarget target) { + return false; + } + + @Override + public String hashString() { + return AndroidTargetHash.getTargetHashString(this); + } + + @Override + public int compareTo(IAndroidTarget o) { + return 0; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof MissingTarget)) { + return false; + } + MissingTarget other = (MissingTarget) obj; + return Objects.equal(mVendor, other.mVendor) && Objects.equal(mVersion, other.mVersion); + } + + @Override + public int hashCode() { + return Objects.hashCode(mVendor, mVersion); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/androidTarget/OptionalLibraryImpl.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/androidTarget/OptionalLibraryImpl.java new file mode 100644 index 00000000..97f7efff --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/androidTarget/OptionalLibraryImpl.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2015 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.sdklib.internal.androidTarget; + +import com.android.annotations.NonNull; +import com.android.sdklib.IAndroidTarget; + +import java.io.File; + +/** + * Internal implementation of OptionalLibrary + */ +public class OptionalLibraryImpl implements IAndroidTarget.OptionalLibrary { + + @NonNull + private final String mLibraryName; + @NonNull + private final File mJarFile; + @NonNull + private final String mDescription; + private final boolean mRequireManifestEntry; + + public OptionalLibraryImpl( + @NonNull String libraryName, + @NonNull File jarFile, + @NonNull String description, + boolean requireManifestEntry) { + mLibraryName = libraryName; + mJarFile = jarFile; + mDescription = description; + mRequireManifestEntry = requireManifestEntry; + } + + @Override + @NonNull + public String getName() { + return mLibraryName; + } + + @Override + @NonNull + public File getJar() { + return mJarFile; + } + + @Override + @NonNull + public String getDescription() { + return mDescription; + } + + @Override + public boolean isManifestEntryRequired() { + return mRequireManifestEntry; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/androidTarget/PlatformTarget.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/androidTarget/PlatformTarget.java new file mode 100644 index 00000000..cdd5a9ec --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/androidTarget/PlatformTarget.java @@ -0,0 +1,456 @@ +/* + * 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.sdklib.internal.androidTarget; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdklib.AndroidTargetHash; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.BuildToolInfo; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.ISystemImage; +import com.android.sdklib.SdkManager.LayoutlibVersion; +import com.android.sdklib.repository.descriptors.IdDisplay; +import com.android.utils.SparseArray; +import com.google.common.collect.ImmutableList; + +import java.io.File; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Represents a platform target in the SDK. + */ +public final class PlatformTarget implements IAndroidTarget { + + private static final String PLATFORM_VENDOR = "Android Open Source Project"; + + private static final String PLATFORM_NAME = "Android %s"; + private static final String PLATFORM_NAME_PREVIEW = "Android %s (Preview)"; + + /** the OS path to the root folder of the platform component. */ + private final String mRootFolderOsPath; + private final String mName; + private final AndroidVersion mVersion; + private final String mVersionName; + private final int mRevision; + private final Map mProperties; + private final SparseArray mPaths = new SparseArray(); + private File[] mSkins; + private final ISystemImage[] mSystemImages; + private final List mOptionalLibraries; + private final LayoutlibVersion mLayoutlibVersion; + private final BuildToolInfo mBuildToolInfo; + + /** + * Creates a Platform target. + * + * @param sdkOsPath the root folder of the SDK + * @param platformOSPath the root folder of the platform component + * @param apiVersion the API Level + codename. + * @param versionName the version name of the platform. + * @param revision the revision of the platform component. + * @param layoutlibVersion The {@link LayoutlibVersion}. May be null. + * @param systemImages list of supported system images + * @param properties the platform properties + */ + public PlatformTarget( + String sdkOsPath, + String platformOSPath, + AndroidVersion apiVersion, + String versionName, + int revision, + LayoutlibVersion layoutlibVersion, + ISystemImage[] systemImages, + Map properties, + List optionalLibraries, + @NonNull BuildToolInfo buildToolInfo) { + if (!platformOSPath.endsWith(File.separator)) { + platformOSPath = platformOSPath + File.separator; + } + mRootFolderOsPath = platformOSPath; + mProperties = Collections.unmodifiableMap(properties); + mVersion = apiVersion; + mVersionName = versionName; + mRevision = revision; + mLayoutlibVersion = layoutlibVersion; + mBuildToolInfo = buildToolInfo; + mSystemImages = systemImages == null ? new ISystemImage[0] : systemImages; + Arrays.sort(mSystemImages); + mOptionalLibraries = ImmutableList.copyOf(optionalLibraries); + + if (mVersion.isPreview()) { + mName = String.format(PLATFORM_NAME_PREVIEW, mVersionName); + } else { + mName = String.format(PLATFORM_NAME, mVersionName); + } + + // pre-build the path to the platform components + mPaths.put(ANDROID_JAR, mRootFolderOsPath + SdkConstants.FN_FRAMEWORK_LIBRARY); + mPaths.put(UI_AUTOMATOR_JAR, mRootFolderOsPath + SdkConstants.FN_UI_AUTOMATOR_LIBRARY); + mPaths.put(SOURCES, mRootFolderOsPath + SdkConstants.FD_ANDROID_SOURCES); + mPaths.put(ANDROID_AIDL, mRootFolderOsPath + SdkConstants.FN_FRAMEWORK_AIDL); + mPaths.put(SAMPLES, mRootFolderOsPath + SdkConstants.OS_PLATFORM_SAMPLES_FOLDER); + mPaths.put(SKINS, mRootFolderOsPath + SdkConstants.OS_SKINS_FOLDER); + mPaths.put(TEMPLATES, mRootFolderOsPath + SdkConstants.OS_PLATFORM_TEMPLATES_FOLDER); + mPaths.put(DATA, mRootFolderOsPath + SdkConstants.OS_PLATFORM_DATA_FOLDER); + mPaths.put(ATTRIBUTES, mRootFolderOsPath + SdkConstants.OS_PLATFORM_ATTRS_XML); + mPaths.put(MANIFEST_ATTRIBUTES, + mRootFolderOsPath + SdkConstants.OS_PLATFORM_ATTRS_MANIFEST_XML); + mPaths.put(RESOURCES, mRootFolderOsPath + SdkConstants.OS_PLATFORM_RESOURCES_FOLDER); + mPaths.put(FONTS, mRootFolderOsPath + SdkConstants.OS_PLATFORM_FONTS_FOLDER); + mPaths.put(LAYOUT_LIB, mRootFolderOsPath + SdkConstants.OS_PLATFORM_DATA_FOLDER + + SdkConstants.FN_LAYOUTLIB_JAR); + mPaths.put(WIDGETS, mRootFolderOsPath + SdkConstants.OS_PLATFORM_DATA_FOLDER + + SdkConstants.FN_WIDGETS); + mPaths.put(ACTIONS_ACTIVITY, mRootFolderOsPath + SdkConstants.OS_PLATFORM_DATA_FOLDER + + SdkConstants.FN_INTENT_ACTIONS_ACTIVITY); + mPaths.put(ACTIONS_BROADCAST, mRootFolderOsPath + SdkConstants.OS_PLATFORM_DATA_FOLDER + + SdkConstants.FN_INTENT_ACTIONS_BROADCAST); + mPaths.put(ACTIONS_SERVICE, mRootFolderOsPath + SdkConstants.OS_PLATFORM_DATA_FOLDER + + SdkConstants.FN_INTENT_ACTIONS_SERVICE); + mPaths.put(CATEGORIES, mRootFolderOsPath + SdkConstants.OS_PLATFORM_DATA_FOLDER + + SdkConstants.FN_INTENT_CATEGORIES); + mPaths.put(ANT, mRootFolderOsPath + SdkConstants.OS_PLATFORM_ANT_FOLDER); + } + + /** + * Returns the {@link LayoutlibVersion}. May be null. + */ + public LayoutlibVersion getLayoutlibVersion() { + return mLayoutlibVersion; + } + + @Override + @Nullable + public ISystemImage getSystemImage(@NonNull IdDisplay tag, @NonNull String abiType) { + for (ISystemImage sysImg : mSystemImages) { + if (sysImg.getTag().equals(tag) && sysImg.getAbiType().equals(abiType)) { + return sysImg; + } + } + return null; + } + + @Override + public ISystemImage[] getSystemImages() { + return mSystemImages; + } + + @Override + public String getLocation() { + return mRootFolderOsPath; + } + + /** + * {@inheritDoc} + *

+ * For Platform, the vendor name is always "Android". + * + * @see com.android.sdklib.IAndroidTarget#getVendor() + */ + @Override + public String getVendor() { + return PLATFORM_VENDOR; + } + + @Override + public String getName() { + return mName; + } + + @Override + public String getFullName() { + return mName; + } + + @Override + public String getClasspathName() { + return mName; + } + + @Override + public String getShortClasspathName() { + return mName; + } + + /* + * (non-Javadoc) + * + * Description for the Android platform is dynamically generated. + * + * @see com.android.sdklib.IAndroidTarget#getDescription() + */ + @Override + public String getDescription() { + return String.format("Standard Android platform %s", mVersionName); + } + + @NonNull + @Override + public AndroidVersion getVersion() { + return mVersion; + } + + @Override + public String getVersionName() { + return mVersionName; + } + + @Override + public int getRevision() { + return mRevision; + } + + @Override + public boolean isPlatform() { + return true; + } + + @Override + public IAndroidTarget getParent() { + return null; + } + + @Override + public String getPath(int pathId) { + return mPaths.get(pathId); + } + + @Override + public File getFile(int pathId) { + return new File(getPath(pathId)); + } + + @Override + public BuildToolInfo getBuildToolInfo() { + return mBuildToolInfo; + } + + @Override @NonNull + public List getBootClasspath() { + return ImmutableList.of(getPath(IAndroidTarget.ANDROID_JAR)); + } + + @NonNull + @Override + public List getOptionalLibraries() { + return mOptionalLibraries; + } + + /** + * Always returns null, as a standard platform has no additional libraries. + * + * {@inheritDoc} + * @see com.android.sdklib.IAndroidTarget#getAdditionalLibraries() + */ + @NonNull + @Override + public List getAdditionalLibraries() { + return ImmutableList.of(); + } + + /** + * Returns whether the target is able to render layouts. This is always true for platforms. + */ + @Override + public boolean hasRenderingLibrary() { + return true; + } + + @NonNull + @Override + public File[] getSkins() { + return mSkins; + } + + @Nullable + @Override + public File getDefaultSkin() { + // only one skin? easy. + if (mSkins.length == 1) { + return mSkins[0]; + } + + // look for the skin name in the platform props + String skinName = mProperties.get(SdkConstants.PROP_SDK_DEFAULT_SKIN); + if (skinName == null) { + // otherwise try to find a good default. + if (mVersion.getApiLevel() >= 4) { + // at this time, this is the default skin for all older platforms that had 2+ skins. + skinName = "WVGA800"; //$NON-NLS-1$ + } else { + skinName = "HVGA"; // this is for 1.5 and earlier. //$NON-NLS-1$ + } + } + + return new File(getFile(IAndroidTarget.SKINS), skinName); + } + + /** + * Currently always return a fixed list with "android.test.runner" in it. + *

+ * TODO change the fixed library list to be build-dependent later. + * {@inheritDoc} + */ + @Override + public String[] getPlatformLibraries() { + return new String[] { SdkConstants.ANDROID_TEST_RUNNER_LIB }; + } + + /** + * The platform has no USB Vendor Id: always return {@link IAndroidTarget#NO_USB_ID}. + * {@inheritDoc} + */ + @Override + public int getUsbVendorId() { + return NO_USB_ID; + } + + @Override + public boolean canRunOn(IAndroidTarget target) { + // basic test + if (target == this) { + return true; + } + + // if the platform has a codename (ie it's a preview of an upcoming platform), then + // both platforms must be exactly identical. + if (mVersion.getCodename() != null) { + return mVersion.equals(target.getVersion()); + } + + // target is compatible wit the receiver as long as its api version number is greater or + // equal. + return target.getVersion().getApiLevel() >= mVersion.getApiLevel(); + } + + @Override + public String hashString() { + return AndroidTargetHash.getPlatformHashString(mVersion); + } + + @Override + public int hashCode() { + return hashString().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof PlatformTarget) { + PlatformTarget platform = (PlatformTarget)obj; + + return mVersion.equals(platform.getVersion()); + } + + return false; + } + + /* + * Order by API level (preview/n count as between n and n+1). + * At the same API level, order as: Platform first, then add-on ordered by vendor and then name + * (non-Javadoc) + * @see java.lang.Comparable#compareTo(java.lang.Object) + */ + @Override + public int compareTo(IAndroidTarget target) { + // quick check. + if (this == target) { + return 0; + } + + int versionDiff = mVersion.compareTo(target.getVersion()); + + // only if the version are the same do we care about add-ons. + if (versionDiff == 0) { + // platforms go before add-ons. + if (target.isPlatform() == false) { + return -1; + } + } + + return versionDiff; + } + + /** + * Returns a string representation suitable for debugging. + * The representation is not intended for display to the user. + * + * The representation is also purposely compact. It does not describe _all_ the properties + * of the target, only a few key ones. + * + * @see #getDescription() + */ + @Override + public String toString() { + return String.format("PlatformTarget %1$s rev %2$d", //$NON-NLS-1$ + getVersion(), + getRevision()); + } + + @Override + public String getProperty(String name) { + return mProperties.get(name); + } + + @Override + public Integer getProperty(String name, Integer defaultValue) { + try { + String value = getProperty(name); + if (value != null) { + return Integer.decode(value); + } + } catch (NumberFormatException e) { + // ignore, return default value; + } + + return defaultValue; + } + + @Override + public Boolean getProperty(String name, Boolean defaultValue) { + String value = getProperty(name); + if (value != null) { + return Boolean.valueOf(value); + } + + return defaultValue; + } + + @Override + public Map getProperties() { + return mProperties; // mProperties is unmodifiable. + } + + // ---- platform only methods. + + public void setSkins(@NonNull File[] skins) { + mSkins = skins; + Arrays.sort(mSkins); + } + + public void setSamplesPath(String osLocation) { + mPaths.put(SAMPLES, osLocation); + } + + public void setSourcesPath(String osLocation) { + mPaths.put(SOURCES, osLocation); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/avd/AvdInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/avd/AvdInfo.java new file mode 100644 index 00000000..1c682b32 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/avd/AvdInfo.java @@ -0,0 +1,410 @@ +/* + * 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.sdklib.internal.avd; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.prefs.AndroidLocation.AndroidLocationException; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.ISystemImage; +import com.android.sdklib.SystemImage; +import com.android.sdklib.devices.Abi; +import com.android.sdklib.devices.Device; +import com.android.sdklib.repository.descriptors.IdDisplay; +import com.google.common.base.Charsets; +import com.google.common.io.Files; + +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.Map; + +/** + * An immutable structure describing an Android Virtual Device. + */ +public final class AvdInfo implements Comparable { + + /** + * Status for an {@link AvdInfo}. Indicates whether or not this AVD is valid. + */ + public enum AvdStatus { + /** No error */ + OK, + /** Missing 'path' property in the ini file */ + ERROR_PATH, + /** Missing config.ini file in the AVD data folder */ + ERROR_CONFIG, + /** Missing 'target' property in the ini file */ + ERROR_TARGET_HASH, + /** Target was not resolved from its hash */ + ERROR_TARGET, + /** Unable to parse config.ini */ + ERROR_PROPERTIES, + /** System Image folder in config.ini doesn't exist */ + ERROR_IMAGE_DIR, + /** The {@link Device} this AVD is based on has changed from its original configuration*/ + ERROR_DEVICE_CHANGED, + /** The {@link Device} this AVD is based on is no longer available */ + ERROR_DEVICE_MISSING, + /** the {@link SystemImage} this AVD is based on is no longer available */ + ERROR_IMAGE_MISSING + } + + private final String mName; + private final File mIniFile; + private final String mFolderPath; + private final String mTargetHash; + private final IAndroidTarget mTarget; + private final String mAbiType; + /** An immutable map of properties. This must not be modified. Map can be empty. Never null. */ + private final Map mProperties; + private final AvdStatus mStatus; + private final IdDisplay mTag; + + /** + * Creates a new valid AVD info. Values are immutable. + *

+ * Such an AVD is available and can be used. + * The error string is set to null. + * + * @param name The name of the AVD (for display or reference) + * @param iniFile The path to the config.ini file + * @param folderPath The path to the data directory + * @param targetHash the target hash + * @param target The target. Can be null, if the target was not resolved. + * @param tag The tag id/display. + * @param abiType Name of the abi. + * @param properties The property map. If null, an empty map will be created. + */ + public AvdInfo(@NonNull String name, + @NonNull File iniFile, + @NonNull String folderPath, + @NonNull String targetHash, + @Nullable IAndroidTarget target, + @NonNull IdDisplay tag, + @NonNull String abiType, + @Nullable Map properties) { + this(name, iniFile, folderPath, + targetHash, target, tag, abiType, + properties, AvdStatus.OK); + } + + /** + * Creates a new invalid AVD info. Values are immutable. + *

+ * Such an AVD is not complete and cannot be used. + * The error string must be non-null. + * + * @param name The name of the AVD (for display or reference) + * @param iniFile The path to the config.ini file + * @param folderPath The path to the data directory + * @param targetHash the target hash + * @param target The target. Can be null, if the target was not resolved. + * @param tag The tag id/display. + * @param abiType Name of the abi. + * @param properties The property map. If null, an empty map will be created. + * @param status The {@link AvdStatus} of this AVD. Cannot be null. + */ + public AvdInfo(@NonNull String name, + @NonNull File iniFile, + @NonNull String folderPath, + @NonNull String targetHash, + @Nullable IAndroidTarget target, + @NonNull IdDisplay tag, + @NonNull String abiType, + @Nullable Map properties, + @NonNull AvdStatus status) { + mName = name; + mIniFile = iniFile; + mFolderPath = folderPath; + mTargetHash = targetHash; + mTarget = target; + mTag = tag; + mAbiType = abiType; + mProperties = properties == null ? Collections.emptyMap() + : Collections.unmodifiableMap(properties); + mStatus = status; + } + + /** Returns the name of the AVD. */ + @NonNull + public String getName() { + return mName; + } + + /** Returns the path of the AVD data directory. */ + @NonNull + public String getDataFolderPath() { + return mFolderPath; + } + + /** Returns the tag id/display of the AVD. */ + @NonNull + public IdDisplay getTag() { + return mTag; + } + + /** Returns the processor type of the AVD. */ + @NonNull + public String getAbiType() { + return mAbiType; + } + + @NonNull + public String getCpuArch() { + String cpuArch = mProperties.get(AvdManager.AVD_INI_CPU_ARCH); + if (cpuArch != null) { + return cpuArch; + } + + // legacy + return SdkConstants.CPU_ARCH_ARM; + } + + @NonNull + public String getDeviceManufacturer() { + String deviceManufacturer = mProperties.get(AvdManager.AVD_INI_DEVICE_MANUFACTURER); + if (deviceManufacturer != null && !deviceManufacturer.isEmpty()) { + return deviceManufacturer; + } + + return ""; // $NON-NLS-1$ + } + + @NonNull + public String getDeviceName() { + String deviceName = mProperties.get(AvdManager.AVD_INI_DEVICE_NAME); + if (deviceName != null && !deviceName.isEmpty()) { + return deviceName; + } + + return ""; // $NON-NLS-1$ + } + + /** Convenience function to return a more user friendly name of the abi type. */ + @NonNull + public static String getPrettyAbiType(@NonNull AvdInfo avdInfo) { + return getPrettyAbiType(avdInfo.getTag(), avdInfo.getAbiType()); + } + + /** Convenience function to return a more user friendly name of the abi type. */ + @NonNull + public static String getPrettyAbiType(@NonNull ISystemImage sysImg) { + return getPrettyAbiType(sysImg.getTag(), sysImg.getAbiType()); + } + + /** Convenience function to return a more user friendly name of the abi type. */ + @NonNull + public static String getPrettyAbiType(@NonNull IdDisplay tag, @NonNull String rawAbi) { + String s = ""; // $NON-NLS-1$ + + if (!SystemImage.DEFAULT_TAG.equals(tag)) { + s = tag.getDisplay() + ' '; + } + + Abi abi = Abi.getEnum(rawAbi); + s += (abi == null ? rawAbi : abi.getDisplayName()) + " (" + rawAbi + ')'; + + return s; + } + + /** + * Returns the target hash string. + */ + @NonNull + public String getTargetHash() { + return mTargetHash; + } + + /** Returns the target of the AVD, or null if it has not been resolved. */ + @Nullable + public IAndroidTarget getTarget() { + return mTarget; + } + + /** Returns the {@link AvdStatus} of the receiver. */ + @NonNull + public AvdStatus getStatus() { + return mStatus; + } + + /** + * Helper method that returns the default AVD folder that would be used for a given + * AVD name if and only if the AVD was created with the default choice. + *

+ * Callers must NOT use this to "guess" the actual folder from an actual AVD since + * the purpose of the AVD .ini file is to be able to change this folder. Callers + * should however use this to create a new {@link AvdInfo} to setup its data folder + * to the default. + *

+ * The default is {@code getDefaultAvdFolder()/avdname.avd/}. + *

+ * For an actual existing AVD, callers must use {@link #getDataFolderPath()} instead. + * + * @param manager The AVD Manager, used to get the AVD storage path. + * @param avdName The name of the desired AVD. + * @param unique Whether to return the default or a unique variation of the default. + * @throws AndroidLocationException if there's a problem getting android root directory. + */ + @NonNull + public static File getDefaultAvdFolder(@NonNull AvdManager manager, @NonNull String avdName, + boolean unique) + throws AndroidLocationException { + String base = manager.getBaseAvdFolder(); + File result = new File(base, avdName + AvdManager.AVD_FOLDER_EXTENSION); + if (unique) { + int suffix = 0; + while (result.exists()) { + result = new File(base, String.format("%s_%d%s", avdName, (++suffix), + AvdManager.AVD_FOLDER_EXTENSION)); + } + } + return result; + } + + /** Compatibility forwarding until the usages in tools/swt are updated */ + @Deprecated + @NonNull + public static File getDefaultAvdFolder(@NonNull AvdManager manager, @NonNull String avdName) + throws AndroidLocationException{ + return getDefaultAvdFolder(manager, avdName, false); + } + + /** + * Helper method that returns the .ini {@link File} for a given AVD name. + *

+ * The default is {@code getDefaultAvdFolder()/avdname.ini}. + * + * @param manager The AVD Manager, used to get the AVD storage path. + * @param avdName The name of the desired AVD. + * @throws AndroidLocationException if there's a problem getting android root directory. + */ + @NonNull + public static File getDefaultIniFile(@NonNull AvdManager manager, @NonNull String avdName) + throws AndroidLocationException { + String avdRoot = manager.getBaseAvdFolder(); + return new File(avdRoot, avdName + AvdManager.INI_EXTENSION); + } + + /** + * Returns the .ini {@link File} for this AVD. + */ + @NonNull + public File getIniFile() { + return mIniFile; + } + + /** + * Helper method that returns the Config {@link File} for a given AVD name. + */ + @NonNull + public static File getConfigFile(@NonNull String path) { + return new File(path, AvdManager.CONFIG_INI); + } + + /** + * Returns the Config {@link File} for this AVD. + */ + @NonNull + public File getConfigFile() { + return getConfigFile(mFolderPath); + } + + /** + * Returns an unmodifiable map of properties for the AVD. + * This can be empty but not null. + * Callers must NOT try to modify this immutable map. + */ + @NonNull + public Map getProperties() { + return mProperties; + } + + /** + * Returns the error message for the AVD or null if {@link #getStatus()} + * returns {@link AvdStatus#OK} + */ + @Nullable + public String getErrorMessage() { + switch (mStatus) { + case ERROR_PATH: + return String.format("Missing AVD 'path' property in %1$s", getIniFile()); + case ERROR_CONFIG: + return String.format("Missing config.ini file in %1$s", mFolderPath); + case ERROR_TARGET_HASH: + return String.format("Missing 'target' property in %1$s", getIniFile()); + case ERROR_TARGET: + return String.format("Unknown target '%1$s' in %2$s", + mTargetHash, getIniFile()); + case ERROR_PROPERTIES: + return String.format("Failed to parse properties from %1$s", + getConfigFile()); + case ERROR_IMAGE_DIR: + return String.format( + "Missing system image for %1$s%2$s %3$s. Run 'android update avd -n %4$s'", + SystemImage.DEFAULT_TAG.equals(mTag) ? "" : (mTag.getDisplay() + " "), + mAbiType, + mTarget.getFullName(), + mName); + case ERROR_DEVICE_CHANGED: + return String.format("%1$s %2$s configuration has changed since AVD creation", + mProperties.get(AvdManager.AVD_INI_DEVICE_MANUFACTURER), + mProperties.get(AvdManager.AVD_INI_DEVICE_NAME)); + case ERROR_DEVICE_MISSING: + return String.format("%1$s %2$s no longer exists as a device", + mProperties.get(AvdManager.AVD_INI_DEVICE_MANUFACTURER), + mProperties.get(AvdManager.AVD_INI_DEVICE_NAME)); + case OK: + assert false; + return null; + } + + return null; + } + + /** + * Compares this object with the specified object for order. Returns a + * negative integer, zero, or a positive integer as this object is less + * than, equal to, or greater than the specified object. + * + * @param o the Object to be compared. + * @return a negative integer, zero, or a positive integer as this object is + * less than, equal to, or greater than the specified object. + */ + @Override + public int compareTo(AvdInfo o) { + // first handle possible missing targets (if the AVD failed to load for unresolved targets) + if (mTarget == null && o != null && o.mTarget == null) { + return 0; + } if (mTarget == null) { + return +1; + } else if (o == null || o.mTarget == null) { + return -1; + } + + // then compare the targets + int targetDiff = mTarget.compareTo(o.mTarget); + + if (targetDiff == 0) { + // same target? compare on the avd name + return mName.compareTo(o.mName); + } + + return targetDiff; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/avd/AvdManager.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/avd/AvdManager.java new file mode 100644 index 00000000..11e6bbb5 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/avd/AvdManager.java @@ -0,0 +1,2196 @@ +/* + * 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.sdklib.internal.avd; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.annotations.VisibleForTesting; +import com.android.annotations.VisibleForTesting.Visibility; +import com.android.io.FileWrapper; +import com.android.io.IAbstractFile; +import com.android.io.StreamException; +import com.android.prefs.AndroidLocation; +import com.android.prefs.AndroidLocation.AndroidLocationException; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.ISystemImage; +import com.android.sdklib.SdkManager; +import com.android.sdklib.SystemImage; +import com.android.sdklib.devices.Abi; +import com.android.sdklib.devices.Device; +import com.android.sdklib.devices.DeviceManager; +import com.android.sdklib.devices.DeviceManager.DeviceStatus; +import com.android.sdklib.internal.avd.AvdInfo.AvdStatus; +import com.android.sdklib.internal.project.ProjectProperties; +import com.android.sdklib.io.LegacyFileOp; +import com.android.sdklib.repository.descriptors.IdDisplay; +import com.android.sdklib.repository.local.LocalSdk; +import com.android.sdklib.repository.local.LocalSysImgPkgInfo; +import com.android.utils.GrabProcessOutput; +import com.android.utils.GrabProcessOutput.IProcessOutput; +import com.android.utils.GrabProcessOutput.Wait; +import com.android.utils.ILogger; +import com.android.utils.NullLogger; +import com.android.utils.Pair; +import com.google.common.base.Charsets; +import com.google.common.io.Closeables; +import com.google.common.io.Files; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Android Virtual Device Manager to manage AVDs. + */ +public class AvdManager { + + /** + * Exception thrown when something is wrong with a target path. + */ + private static final class InvalidTargetPathException extends Exception { + private static final long serialVersionUID = 1L; + + InvalidTargetPathException(String message) { + super(message); + } + } + + private static final Pattern INI_LINE_PATTERN = + Pattern.compile("^([a-zA-Z0-9._-]+)\\s*=\\s*(.*)\\s*$"); //$NON-NLS-1$ + + public static final String AVD_FOLDER_EXTENSION = ".avd"; //$NON-NLS-1$ + + /** Charset encoding used by the avd.ini/config.ini. */ + public static final String AVD_INI_ENCODING = "avd.ini.encoding"; //$NON-NLS-1$ + + /** + * The *absolute* path to the AVD folder (which contains the #CONFIG_INI file). + */ + public static final String AVD_INFO_ABS_PATH = "path"; //$NON-NLS-1$ + + /** + * The path to the AVD folder (which contains the #CONFIG_INI file) relative to + * the {@link AndroidLocation#FOLDER_DOT_ANDROID}. This information is written + * in the avd ini only if the AVD folder is located under the .android path + * (that is the relative that has no backward {@code ..} references). + */ + public static final String AVD_INFO_REL_PATH = "path.rel"; //$NON-NLS-1$ + + /** + * The {@link IAndroidTarget#hashString()} of the AVD. + */ + public static final String AVD_INFO_TARGET = "target"; //$NON-NLS-1$ + + /** + * AVD/config.ini key name representing the tag id of the specific avd + */ + public static final String AVD_INI_TAG_ID = "tag.id"; //$NON-NLS-1$ + + /** + * AVD/config.ini key name representing the tag display of the specific avd + */ + public static final String AVD_INI_TAG_DISPLAY = "tag.display"; //$NON-NLS-1$ + + /** + * AVD/config.ini key name representing the abi type of the specific avd + */ + public static final String AVD_INI_ABI_TYPE = "abi.type"; //$NON-NLS-1$ + + /** + * AVD/config.ini key name representing the CPU architecture of the specific avd + */ + public static final String AVD_INI_CPU_ARCH = "hw.cpu.arch"; //$NON-NLS-1$ + + /** + * AVD/config.ini key name representing the CPU architecture of the specific avd + */ + public static final String AVD_INI_CPU_MODEL = "hw.cpu.model"; //$NON-NLS-1$ + + /** + * AVD/config.ini key name representing the manufacturer of the device this avd was based on. + */ + public static final String AVD_INI_DEVICE_MANUFACTURER = "hw.device.manufacturer"; //$NON-NLS-1$ + + /** + * AVD/config.ini key name representing the name of the device this avd was based on. + */ + public static final String AVD_INI_DEVICE_NAME = "hw.device.name"; //$NON-NLS-1$ + + /** + * AVD/config.ini key name representing the SDK-relative path of the skin folder, if any, + * or a 320x480 like constant for a numeric skin size. + * + * @see #NUMERIC_SKIN_SIZE + */ + public static final String AVD_INI_SKIN_PATH = "skin.path"; //$NON-NLS-1$ + + /** + * AVD/config.ini key name representing the SDK-relative path of the skin folder to be selected if + * skins for this device become enabled. + */ + public static final String AVD_INI_BACKUP_SKIN_PATH = "skin.path.backup"; //$NON-NLS-1$ + + /** + * AVD/config.ini key name representing an UI name for the skin. + * This config key is ignored by the emulator. It is only used by the SDK manager or + * tools to give a friendlier name to the skin. + * If missing, use the {@link #AVD_INI_SKIN_PATH} key instead. + */ + public static final String AVD_INI_SKIN_NAME = "skin.name"; //$NON-NLS-1$ + + /** + * AVD/config.ini key name representing whether a dynamic skin should be displayed. + */ + public static final String AVD_INI_SKIN_DYNAMIC = "skin.dynamic"; //$NON-NLS-1$ + + /** + * AVD/config.ini key name representing the path to the sdcard file. + * If missing, the default name "sdcard.img" will be used for the sdcard, if there's such + * a file. + * + * @see #SDCARD_IMG + */ + public static final String AVD_INI_SDCARD_PATH = "sdcard.path"; //$NON-NLS-1$ + /** + * AVD/config.ini key name representing the size of the SD card. + * This property is for UI purposes only. It is not used by the emulator. + * + * @see #SDCARD_SIZE_PATTERN + * @see #parseSdcardSize(String, String[]) + */ + public static final String AVD_INI_SDCARD_SIZE = "sdcard.size"; //$NON-NLS-1$ + /** + * AVD/config.ini key name representing the first path where the emulator looks + * for system images. Typically this is the path to the add-on system image or + * the path to the platform system image if there's no add-on. + *

+ * The emulator looks at {@link #AVD_INI_IMAGES_1} before {@link #AVD_INI_IMAGES_2}. + */ + public static final String AVD_INI_IMAGES_1 = "image.sysdir.1"; //$NON-NLS-1$ + /** + * AVD/config.ini key name representing the second path where the emulator looks + * for system images. Typically this is the path to the platform system image. + * + * @see #AVD_INI_IMAGES_1 + */ + public static final String AVD_INI_IMAGES_2 = "image.sysdir.2"; //$NON-NLS-1$ + /** + * AVD/config.ini key name representing the presence of the snapshots file. + * This property is for UI purposes only. It is not used by the emulator. + * + * @see #SNAPSHOTS_IMG + */ + public static final String AVD_INI_SNAPSHOT_PRESENT = "snapshot.present"; //$NON-NLS-1$ + + /** + * AVD/config.ini key name representing whether hardware OpenGLES emulation is enabled + */ + public static final String AVD_INI_GPU_EMULATION = "hw.gpu.enabled"; //$NON-NLS-1$ + + /** + * AVD/config.ini key name representing how to emulate the front facing camera + */ + public static final String AVD_INI_CAMERA_FRONT = "hw.camera.front"; //$NON-NLS-1$ + + /** + * AVD/config.ini key name representing how to emulate the rear facing camera + */ + public static final String AVD_INI_CAMERA_BACK = "hw.camera.back"; //$NON-NLS-1$ + + /** + * AVD/config.ini key name representing the amount of RAM the emulated device should have + */ + public static final String AVD_INI_RAM_SIZE = "hw.ramSize"; + + /** + * AVD/config.ini key name representing the amount of memory available to applications by default + */ + public static final String AVD_INI_VM_HEAP_SIZE = "vm.heapSize"; + + /** + * AVD/config.ini key name representing the size of the data partition + */ + public static final String AVD_INI_DATA_PARTITION_SIZE = "disk.dataPartition.size"; + + /** + * AVD/config.ini key name representing the hash of the device this AVD is based on.
+ * This old hash is deprecated and shouldn't be used anymore. + * It represents the Device.hashCode() and is not stable accross implementations. + * @see #AVD_INI_DEVICE_HASH_V2 + */ + public static final String AVD_INI_DEVICE_HASH_V1 = "hw.device.hash"; + + /** + * AVD/config.ini key name representing the hash of the device hardware properties + * actually present in the config.ini. This replaces {@link #AVD_INI_DEVICE_HASH_V1}. + *

+ * To find this hash, use + * {@code DeviceManager.getHardwareProperties(device).get(AVD_INI_DEVICE_HASH_V2)}. + */ + public static final String AVD_INI_DEVICE_HASH_V2 = "hw.device.hash2"; + + /** + * Pattern to match pixel-sized skin "names", e.g. "320x480". + */ + public static final Pattern NUMERIC_SKIN_SIZE = Pattern.compile("([0-9]{2,})x([0-9]{2,})"); //$NON-NLS-1$ + + private static final String USERDATA_IMG = "userdata.img"; //$NON-NLS-1$ + private static final String BOOT_PROP = "boot.prop"; //$NON-NLS-1$ + static final String CONFIG_INI = "config.ini"; //$NON-NLS-1$ + private static final String SDCARD_IMG = "sdcard.img"; //$NON-NLS-1$ + private static final String SNAPSHOTS_IMG = "snapshots.img"; //$NON-NLS-1$ + + static final String INI_EXTENSION = ".ini"; //$NON-NLS-1$ + private static final Pattern INI_NAME_PATTERN = Pattern.compile("(.+)\\" + //$NON-NLS-1$ + INI_EXTENSION + "$", //$NON-NLS-1$ + Pattern.CASE_INSENSITIVE); + + private static final Pattern IMAGE_NAME_PATTERN = Pattern.compile("(.+)\\.img$", //$NON-NLS-1$ + Pattern.CASE_INSENSITIVE); + + /** + * Pattern for matching SD Card sizes, e.g. "4K" or "16M". + * Callers should use {@link #parseSdcardSize(String, String[])} instead of using this directly. + */ + private static final Pattern SDCARD_SIZE_PATTERN = Pattern.compile("(\\d+)([KMG])"); //$NON-NLS-1$ + + /** + * Minimal size of an SDCard image file in bytes. Currently 9 MiB. + */ + + public static final long SDCARD_MIN_BYTE_SIZE = 9<<20; + /** + * Maximal size of an SDCard image file in bytes. Currently 1023 GiB. + */ + public static final long SDCARD_MAX_BYTE_SIZE = 1023L<<30; + + /** The sdcard string represents a valid number but the size is outside of the allowed range. */ + public static final int SDCARD_SIZE_NOT_IN_RANGE = 0; + /** The sdcard string looks like a size number+suffix but the number failed to decode. */ + public static final int SDCARD_SIZE_INVALID = -1; + /** The sdcard string doesn't look like a size, it might be a path instead. */ + public static final int SDCARD_NOT_SIZE_PATTERN = -2; + + /** Regex used to validate characters that compose an AVD name. */ + public static final Pattern RE_AVD_NAME = Pattern.compile("[a-zA-Z0-9._-]+"); //$NON-NLS-1$ + + /** List of valid characters for an AVD name. Used for display purposes. */ + public static final String CHARS_AVD_NAME = "a-z A-Z 0-9 . _ -"; //$NON-NLS-1$ + + public static final String HARDWARE_INI = "hardware.ini"; //$NON-NLS-1$ + + /** + * Status returned by {@link AvdManager#isAvdNameConflicting(String)}. + */ + public enum AvdConflict { + /** There is no known conflict for the given AVD name. */ + NO_CONFLICT, + /** The AVD name conflicts with an existing valid AVD. */ + CONFLICT_EXISTING_AVD, + /** The AVD name conflicts with an existing invalid AVD. */ + CONFLICT_INVALID_AVD, + /** + * The AVD name does not conflict with any known AVD however there are + * files or directory that would cause a conflict if this were to be created. + */ + CONFLICT_EXISTING_PATH, + } + + // A map where the keys are the locations of the SDK and the values are the corresponding + // AvdManagers. This prevents us from creating multiple AvdManagers for the same SDK and having + // them get out of sync. + private static final Map mManagers = + Collections.synchronizedMap(new WeakHashMap()); + + private final ArrayList mAllAvdList = new ArrayList(); + private AvdInfo[] mValidAvdList; + private AvdInfo[] mBrokenAvdList; + private final LocalSdk myLocalSdk; + private final Map myDeviceManagers = + new HashMap(); + + /** + * Creates an AVD Manager for a given SDK represented by a {@link LocalSdk}. + * @param localSdk The SDK. + * @param log The log object to receive the log of the initial loading of the AVDs. + * This log object is not kept by this instance of AvdManager and each + * method takes its own logger. The rationale is that the AvdManager + * might be called from a variety of context, each with different + * logging needs. Cannot be null. + * @throws AndroidLocationException + */ + protected AvdManager(@NonNull LocalSdk localSdk, @NonNull ILogger log) + throws AndroidLocationException { + myLocalSdk = localSdk; + buildAvdList(mAllAvdList, log); + } + + /** + * Returns an AVD Manager for a given SDK represented by a {@link LocalSdk}. + * One AVD Manager instance is created by SDK location and then cached and reused. + * + * @param localSdk The SDK. + * @param log The log object to receive the log of the initial loading of the AVDs. + * This log object is not kept by this instance of AvdManager and each + * method takes its own logger. The rationale is that the AvdManager + * might be called from a variety of context, each with different + * logging needs. Cannot be null. + * @throws AndroidLocationException + */ + @NonNull + public static AvdManager getInstance(@NonNull LocalSdk localSdk, @NonNull ILogger log) + throws AndroidLocationException { + synchronized(mManagers) { + AvdManager manager; + if ((manager = mManagers.get(localSdk.getLocation().getPath())) != null) { + return manager; + } + manager = new AvdManager(localSdk, log); + + mManagers.put(localSdk.getLocation().getPath(), manager); + return manager; + } + } + + /** + * Returns the base folder where AVDs are created. + * + * @throws AndroidLocationException + */ + @NonNull + public String getBaseAvdFolder() throws AndroidLocationException { + assert AndroidLocation.getFolder().endsWith(File.separator); + return AndroidLocation.getFolder() + AndroidLocation.FOLDER_AVD; + } + + /** + * Returns the {@link LocalSdk} associated with the {@link AvdManager}. + */ + @NonNull + public LocalSdk getLocalSdk() { + return myLocalSdk; + } + + /** + * Returns the {@link SdkManager} associated with the {@link AvdManager}. + * Note: This is temporary and will be removed as SdkManager is phased out. + * TODO: Remove this when SdkManager is removed + */ + @NonNull + @Deprecated + public SdkManager getSdkManager() { + return SdkManager.createManager(myLocalSdk.getPath(), NullLogger.getLogger()); + } + + /** + * Parse the sdcard string to decode the size. + * Returns: + *

    + *
  • The size in bytes > 0 if the sdcard string is a valid size in the allowed range. + *
  • {@link #SDCARD_SIZE_NOT_IN_RANGE} (0) + * if the sdcard string is a valid size NOT in the allowed range. + *
  • {@link #SDCARD_SIZE_INVALID} (-1) + * if the sdcard string is number that fails to parse correctly. + *
  • {@link #SDCARD_NOT_SIZE_PATTERN} (-2) + * if the sdcard string is not a number, in which case it's probably a file path. + *
+ * + * @param sdcard The sdcard string, which can be a file path, a size string or something else. + * @param parsedStrings If non-null, an array of 2 strings. The first string will be + * filled with the parsed numeric size and the second one will be filled with the + * parsed suffix. This is filled even if the returned size is deemed out of range or + * failed to parse. The values are null if the sdcard is not a size pattern. + * @return A size in byte if > 0, or {@link #SDCARD_SIZE_NOT_IN_RANGE}, + * {@link #SDCARD_SIZE_INVALID} or {@link #SDCARD_NOT_SIZE_PATTERN} as error codes. + */ + public static long parseSdcardSize(@NonNull String sdcard, @Nullable String[] parsedStrings) { + + if (parsedStrings != null) { + assert parsedStrings.length == 2; + parsedStrings[0] = null; + parsedStrings[1] = null; + } + + Matcher m = SDCARD_SIZE_PATTERN.matcher(sdcard); + if (m.matches()) { + if (parsedStrings != null) { + assert parsedStrings.length == 2; + parsedStrings[0] = m.group(1); + parsedStrings[1] = m.group(2); + } + + // get the sdcard values for checks + try { + long sdcardSize = Long.parseLong(m.group(1)); + + String sdcardSizeModifier = m.group(2); + if ("K".equals(sdcardSizeModifier)) { //$NON-NLS-1$ + sdcardSize <<= 10; + } else if ("M".equals(sdcardSizeModifier)) { //$NON-NLS-1$ + sdcardSize <<= 20; + } else if ("G".equals(sdcardSizeModifier)) { //$NON-NLS-1$ + sdcardSize <<= 30; + } + + if (sdcardSize < SDCARD_MIN_BYTE_SIZE || + sdcardSize > SDCARD_MAX_BYTE_SIZE) { + return SDCARD_SIZE_NOT_IN_RANGE; + } + + return sdcardSize; + } catch (NumberFormatException e) { + // This could happen if the number is too large to fit in a long. + return SDCARD_SIZE_INVALID; + } + } + + return SDCARD_NOT_SIZE_PATTERN; + } + + /** + * Returns all the existing AVDs. + * @return a newly allocated array containing all the AVDs. + */ + @NonNull + public AvdInfo[] getAllAvds() { + synchronized (mAllAvdList) { + return mAllAvdList.toArray(new AvdInfo[mAllAvdList.size()]); + } + } + + /** + * Returns all the valid AVDs. + * @return a newly allocated array containing all valid the AVDs. + */ + @NonNull + public AvdInfo[] getValidAvds() { + synchronized (mAllAvdList) { + if (mValidAvdList == null) { + ArrayList list = new ArrayList(); + for (AvdInfo avd : mAllAvdList) { + if (avd.getStatus() == AvdStatus.OK) { + list.add(avd); + } + } + + mValidAvdList = list.toArray(new AvdInfo[list.size()]); + } + return mValidAvdList; + } + } + + /** + * Returns all the broken AVDs. + * @return a newly allocated array containing all the broken AVDs. + */ + @NonNull + public AvdInfo[] getBrokenAvds() { + synchronized (mAllAvdList) { + if (mBrokenAvdList == null) { + ArrayList list = new ArrayList(); + for (AvdInfo avd : mAllAvdList) { + if (avd.getStatus() != AvdStatus.OK) { + list.add(avd); + } + } + mBrokenAvdList = list.toArray(new AvdInfo[list.size()]); + } + return mBrokenAvdList; + } + } + + /** + * Returns the {@link AvdInfo} matching the given name. + *

+ * The search is case-insensitive. + * + * @param name the name of the AVD to return + * @param validAvdOnly if true, only look through the list of valid AVDs. + * @return the matching AvdInfo or null if none were found. + */ + @Nullable + public AvdInfo getAvd(@Nullable String name, boolean validAvdOnly) { + + boolean ignoreCase = SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS; + + if (validAvdOnly) { + for (AvdInfo info : getValidAvds()) { + String name2 = info.getName(); + if (name2.equals(name) || (ignoreCase && name2.equalsIgnoreCase(name))) { + return info; + } + } + } else { + synchronized (mAllAvdList) { + for (AvdInfo info : mAllAvdList) { + String name2 = info.getName(); + if (name2.equals(name) || (ignoreCase && name2.equalsIgnoreCase(name))) { + return info; + } + } + } + } + + return null; + } + + /** + * Returns whether an emulator is currently running the AVD. + */ + public boolean isAvdRunning(@NonNull AvdInfo info) { + try { + String pid = getAvdPid(info); + if (pid != null) { + String command; + if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS) { + command = "cmd /c \"tasklist /FI \"PID eq " + pid + "\" | findstr " + pid + + "\""; + } else { + command = "kill -0 " + pid; + } + try { + Process p = Runtime.getRuntime().exec(command); + // If the process ends with non-0 it means the process doesn't exist + return p.waitFor() == 0; + } catch (IOException e) { + // To be safe return true + return true; + } catch (InterruptedException e) { + // To be safe return true + return true; + } + } + } + catch (IOException e) { + // To be safe return true + return true; + } + return false; + } + + public void stopAvd(@NonNull AvdInfo info) { + try { + String pid = getAvdPid(info); + if (pid != null) { + String command; + if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS) { + command = "cmd /c \"taskkill /PID " + pid + "\""; + } else { + command = "kill " + pid; + } + try { + Process p = Runtime.getRuntime().exec(command); + // If the process ends with non-0 it means the process doesn't exist + p.waitFor(); + } catch (IOException e) { + } catch (InterruptedException e) { + } + } + } + catch (IOException e) { + } + } + + private String getAvdPid(@NonNull AvdInfo info) throws IOException { + // this is a file on Unix, and a directory on Windows. + File f = new File(info.getDataFolderPath(), "userdata-qemu.img.lock"); //$NON-NLS-1$ + if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS) { + f = new File(f, "pid"); + } + if (f.exists()) { + return Files.toString(f, Charsets.UTF_8); + } + return null; + } + + + + /** + * Returns whether this AVD name would generate a conflict. + * + * @param name the name of the AVD to return + * @return A pair of {@link AvdConflict} and the path or AVD name that conflicts. + */ + @NonNull + public Pair isAvdNameConflicting(@Nullable String name) { + + boolean ignoreCase = SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS; + + // Check whether we have a conflict with an existing or invalid AVD + // known to the manager. + synchronized (mAllAvdList) { + for (AvdInfo info : mAllAvdList) { + String name2 = info.getName(); + if (name2.equals(name) || (ignoreCase && name2.equalsIgnoreCase(name))) { + if (info.getStatus() == AvdStatus.OK) { + return Pair.of(AvdConflict.CONFLICT_EXISTING_AVD, name2); + } else { + return Pair.of(AvdConflict.CONFLICT_INVALID_AVD, name2); + } + } + } + } + + // No conflict with known AVDs. + // Are some existing files/folders in the way of creating this AVD? + + try { + File file = AvdInfo.getDefaultIniFile(this, name); + if (file.exists()) { + return Pair.of(AvdConflict.CONFLICT_EXISTING_PATH, file.getPath()); + } + + file = AvdInfo.getDefaultAvdFolder(this, name, false); + if (file.exists()) { + return Pair.of(AvdConflict.CONFLICT_EXISTING_PATH, file.getPath()); + } + + } catch (AndroidLocationException e) { + // ignore + } + + + return Pair.of(AvdConflict.NO_CONFLICT, null); + } + + /** + * Reloads the AVD list. + * @param log the log object to receive action logs. Cannot be null. + * @throws AndroidLocationException if there was an error finding the location of the + * AVD folder. + */ + public void reloadAvds(@NonNull ILogger log) throws AndroidLocationException { + // build the list in a temp list first, in case the method throws an exception. + // It's better than deleting the whole list before reading the new one. + ArrayList allList = new ArrayList(); + buildAvdList(allList, log); + + synchronized (mAllAvdList) { + mAllAvdList.clear(); + mAllAvdList.addAll(allList); + mValidAvdList = mBrokenAvdList = null; + } + } + + /** + * Creates a new AVD. It is expected that there is no existing AVD with this name already. + * + * @param avdFolder the data folder for the AVD. It will be created as needed. + * Unless you want to locate it in a specific directory, the ideal default is + * {@code AvdManager.AvdInfo.getAvdFolder}. + * @param avdName the name of the AVD + * @param target the target of the AVD + * @param tag the tag of the AVD + * @param abiType the abi type of the AVD + * @param skinFolder the skin folder path to use, if specified. Can be null. + * @param skinName the name of the skin. Can be null. Must have been verified by caller. + * Can be a size in the form "NNNxMMM" or a directory name matching skinFolder. + * @param sdcard the parameter value for the sdCard. Can be null. This is either a path to + * an existing sdcard image or a sdcard size (\d+, \d+K, \dM). + * @param hardwareConfig the hardware setup for the AVD. Can be null to use defaults. + * @param bootProps the optional boot properties for the AVD. Can be null. + * @param createSnapshot If true copy a blank snapshot image into the AVD. + * @param removePrevious If true remove any previous files. + * @param editExisting If true, edit an existing AVD, changing only the minimum required. + * This won't remove files unless required or unless {@code removePrevious} is set. + * @param log the log object to receive action logs. Cannot be null. + * @return The new {@link AvdInfo} in case of success (which has just been added to the + * internal list) or null in case of failure. + */ + @Nullable + public AvdInfo createAvd( + @NonNull File avdFolder, + @NonNull String avdName, + @NonNull IAndroidTarget target, + @NonNull IdDisplay tag, + @NonNull String abiType, + @Nullable File skinFolder, + @Nullable String skinName, + @Nullable String sdcard, + @Nullable Map hardwareConfig, + @Nullable Map bootProps, + boolean createSnapshot, + boolean removePrevious, + boolean editExisting, + @NonNull ILogger log) { + if (log == null) { + throw new IllegalArgumentException("log cannot be null"); + } + + File iniFile = null; + boolean needCleanup = false; + try { + if (avdFolder.exists()) { + if (removePrevious) { + // AVD already exists and removePrevious is set, try to remove the + // directory's content first (but not the directory itself). + try { + deleteContentOf(avdFolder); + } catch (SecurityException e) { + log.error(e, "Failed to delete %1$s", avdFolder.getAbsolutePath()); + } + } else if (!editExisting) { + // AVD shouldn't already exist if removePrevious is false and + // we're not editing an existing AVD. + log.error(null, + "Folder %1$s is in the way. Use --force if you want to overwrite.", + avdFolder.getAbsolutePath()); + return null; + } + } else { + // create the AVD folder. + avdFolder.mkdir(); + // We're not editing an existing AVD. + editExisting = false; + } + + // actually write the ini file + iniFile = createAvdIniFile(avdName, avdFolder, target, removePrevious); + + // writes the userdata.img in it. + + File userdataSrc = null; + + // Look for a system image in the add-on. + // If we don't find one there, look in the base platform. + ISystemImage systemImage = target.getSystemImage(tag, abiType); + + if (systemImage != null) { + File imageFolder = systemImage.getLocation(); + userdataSrc = new File(imageFolder, USERDATA_IMG); + } + + if ((userdataSrc == null || !userdataSrc.exists()) && !target.isPlatform()) { + // If we don't find a system-image in the add-on, look into the platform. + + systemImage = target.getParent().getSystemImage(tag, abiType); + if (systemImage != null) { + File imageFolder = systemImage.getLocation(); + userdataSrc = new File(imageFolder, USERDATA_IMG); + } + } + + if (userdataSrc == null || !userdataSrc.exists()) { + log.error(null, + "Unable to find a '%1$s' file for ABI %2$s to copy into the AVD folder.", + USERDATA_IMG, + abiType); + needCleanup = true; + return null; + } + + File userdataDest = new File(avdFolder, USERDATA_IMG); + + copyImageFile(userdataSrc, userdataDest); + + if (userdataDest.exists() == false) { + log.error(null, "Unable to create '%1$s' file in the AVD folder.", + userdataDest); + needCleanup = true; + return null; + } + + // Config file. + HashMap values = new HashMap(); + + if (setImagePathProperties(target, tag, abiType, values, log) == false) { + log.error(null, "Failed to set image path properties in the AVD folder."); + needCleanup = true; + return null; + } + + // Create the snapshot file + if (createSnapshot) { + File snapshotDest = new File(avdFolder, SNAPSHOTS_IMG); + if (snapshotDest.isFile() && editExisting) { + log.info("Snapshot image already present, was not changed.\n"); + + } else { + File toolsLib = new File(myLocalSdk.getLocation(), + SdkConstants.OS_SDK_TOOLS_LIB_EMULATOR_FOLDER); + File snapshotBlank = new File(toolsLib, SNAPSHOTS_IMG); + if (snapshotBlank.exists() == false) { + log.error(null, + "Unable to find a '%2$s%1$s' file to copy into the AVD folder.", + SNAPSHOTS_IMG, toolsLib); + needCleanup = true; + return null; + } + + copyImageFile(snapshotBlank, snapshotDest); + } + values.put(AVD_INI_SNAPSHOT_PRESENT, "true"); + } + + // Now the tag & abi type + values.put(AVD_INI_TAG_ID, tag.getId()); + values.put(AVD_INI_TAG_DISPLAY, tag.getDisplay()); + values.put(AVD_INI_ABI_TYPE, abiType); + + // and the cpu arch. + Abi abi = Abi.getEnum(abiType); + if (abi != null) { + values.put(AVD_INI_CPU_ARCH, abi.getCpuArch()); + + String model = abi.getCpuModel(); + if (model != null) { + values.put(AVD_INI_CPU_MODEL, model); + } + } else { + log.error(null, + "ABI %1$s is not supported by this version of the SDK Tools", abiType); + needCleanup = true; + return null; + } + + // Now the skin. + String skinPath = null; + + if (skinFolder == null && skinName == null) { + // Nothing specified. Use the default from the target. + skinFolder = target.getDefaultSkin(); + } + + if (skinFolder == null && skinName != null && + NUMERIC_SKIN_SIZE.matcher(skinName).matches()) { + // Numeric skin size. Set both skinPath and skinName to the same size. + skinPath = skinName; + + } else if (skinFolder != null && skinName == null) { + // Skin folder is specified, but not skin name. Adjust it. + skinName = skinFolder.getName(); + + } else if (skinFolder == null && skinName != null) { + // skin folder is not specified, but there's a non-numeric skin name. + // check whether the skin is in the target. + skinFolder = getSkinFolder(skinName, target); + } + + if (skinFolder != null) { + // skin does not exist! + if (!skinFolder.exists()) { + log.error(null, "Skin '%1$s' does not exist.", skinName); + return null; + } + + // if skinFolder is in the sdk, use the relative path + if (skinFolder.getPath().startsWith(myLocalSdk.getLocation().getPath())) { + try { + skinPath = LegacyFileOp.makeRelative(myLocalSdk.getLocation(), skinFolder); + } catch (IOException e) { + // In case it fails, just use the absolute path + skinPath = skinFolder.getAbsolutePath(); + } + } else { + // Skin isn't in the sdk. Just use the absolute path. + skinPath = skinFolder.getAbsolutePath(); + } + } + + // Set skin.name for display purposes in the AVD manager and + // set skin.path for use by the emulator. + if (skinName != null) { + values.put(AVD_INI_SKIN_NAME, skinName); + } + if (skinPath != null) { + values.put(AVD_INI_SKIN_PATH, skinPath); + } + + if (sdcard != null && !sdcard.isEmpty()) { + // Sdcard is possibly a size. In that case we create a file called 'sdcard.img' + // in the AVD folder, and do not put any value in config.ini. + + long sdcardSize = parseSdcardSize(sdcard, null/*parsedStrings*/); + + if (sdcardSize == SDCARD_SIZE_NOT_IN_RANGE) { + log.error(null, "SD Card size must be in the range 9 MiB..1023 GiB."); + needCleanup = true; + return null; + + } else if (sdcardSize == SDCARD_SIZE_INVALID) { + log.error(null, "Unable to parse SD Card size"); + needCleanup = true; + return null; + + } else if (sdcardSize == SDCARD_NOT_SIZE_PATTERN) { + File sdcardFile = new File(sdcard); + if (sdcardFile.isFile()) { + // sdcard value is an external sdcard, so we put its path into the config.ini + values.put(AVD_INI_SDCARD_PATH, sdcard); + } else { + log.error(null, "'%1$s' is not recognized as a valid sdcard value.\n" + + "Value should be:\n" + "1. path to an sdcard.\n" + + "2. size of the sdcard to create: [K|M]", sdcard); + needCleanup = true; + return null; + } + } else { + // create the sdcard. + File sdcardFile = new File(avdFolder, SDCARD_IMG); + + boolean runMkSdcard = true; + if (sdcardFile.exists()) { + if (sdcardFile.length() == sdcardSize && editExisting) { + // There's already an sdcard file with the right size and we're + // not overriding it... so don't remove it. + runMkSdcard = false; + log.info("SD Card already present with same size, was not changed.\n"); + } + } + + if (runMkSdcard) { + String path = sdcardFile.getAbsolutePath(); + + // execute mksdcard with the proper parameters. + File toolsFolder = new File(myLocalSdk.getLocation(), + SdkConstants.FD_TOOLS); + File mkSdCard = new File(toolsFolder, SdkConstants.mkSdCardCmdName()); + + if (mkSdCard.isFile() == false) { + log.error(null, "'%1$s' is missing from the SDK tools folder.", + mkSdCard.getName()); + needCleanup = true; + return null; + } + + if (createSdCard(mkSdCard.getAbsolutePath(), sdcard, path, log) == false) { + log.error(null, "Failed to create sdcard in the AVD folder."); + needCleanup = true; + return null; // mksdcard output has already been displayed, no need to + // output anything else. + } + } + + // add a property containing the size of the sdcard for display purpose + // only when the dev does 'android list avd' + values.put(AVD_INI_SDCARD_SIZE, sdcard); + } + } + + // add the hardware config to the config file. + // priority order is: + // - values provided by the user + // - values provided by the skin + // - values provided by the target (add-on only). + // - values provided by the sys img + // In order to follow this priority, we'll add the lowest priority values first and then + // override by higher priority values. + // In the case of a platform with override values from the user, the skin value might + // already be there, but it's ok. + + HashMap finalHardwareValues = new HashMap(); + + FileWrapper sysImgHardwareFile = new FileWrapper(systemImage.getLocation(), + AvdManager.HARDWARE_INI); + if (sysImgHardwareFile.isFile()) { + Map targetHardwareConfig = ProjectProperties.parsePropertyFile( + sysImgHardwareFile, log); + + if (targetHardwareConfig != null) { + finalHardwareValues.putAll(targetHardwareConfig); + values.putAll(targetHardwareConfig); + } + } + + FileWrapper targetHardwareFile = new FileWrapper(target.getLocation(), + AvdManager.HARDWARE_INI); + if (targetHardwareFile.isFile()) { + Map targetHardwareConfig = ProjectProperties.parsePropertyFile( + targetHardwareFile, log); + + if (targetHardwareConfig != null) { + finalHardwareValues.putAll(targetHardwareConfig); + values.putAll(targetHardwareConfig); + } + } + + // get the hardware properties for this skin + if (skinFolder != null) { + FileWrapper skinHardwareFile = new FileWrapper(skinFolder, AvdManager.HARDWARE_INI); + if (skinHardwareFile.isFile()) { + Map skinHardwareConfig = + ProjectProperties.parsePropertyFile(skinHardwareFile, log); + + if (skinHardwareConfig != null) { + finalHardwareValues.putAll(skinHardwareConfig); + values.putAll(skinHardwareConfig); + } + } + } + + // finally put the hardware provided by the user. + if (hardwareConfig != null) { + finalHardwareValues.putAll(hardwareConfig); + values.putAll(hardwareConfig); + } + + File configIniFile = new File(avdFolder, CONFIG_INI); + writeIniFile(configIniFile, values, true); + + if (bootProps != null && !bootProps.isEmpty()) { + File bootPropsFile = new File(avdFolder, BOOT_PROP); + writeIniFile(bootPropsFile, bootProps, false); + } + + // Generate the log report first because we want to control where line breaks + // are located when generating the hardware config list. + StringBuilder report = new StringBuilder(); + + if (target.isPlatform()) { + if (editExisting) { + report.append(String.format("Updated AVD '%1$s' based on %2$s", + avdName, target.getName())); + } else { + report.append(String.format("Created AVD '%1$s' based on %2$s", + avdName, target.getName())); + } + } else { + if (editExisting) { + report.append(String.format("Updated AVD '%1$s' based on %2$s (%3$s)", avdName, + target.getName(), target.getVendor())); + } else { + report.append(String.format("Created AVD '%1$s' based on %2$s (%3$s)", avdName, + target.getName(), target.getVendor())); + } + } + report.append(String.format(", %s processor", AvdInfo.getPrettyAbiType(tag, abiType))); + + // display the chosen hardware config + if (!finalHardwareValues.isEmpty()) { + report.append(",\nwith the following hardware config:\n"); + List keys = new ArrayList(finalHardwareValues.keySet()); + Collections.sort(keys); + for (String key : keys) { + String value = finalHardwareValues.get(key); + report.append(String.format("%s=%s\n", key, value)); + } + } else { + report.append("\n"); + } + + log.info(report.toString()); + + // create the AvdInfo object, and add it to the list + AvdInfo newAvdInfo = new AvdInfo( + avdName, + iniFile, + avdFolder.getAbsolutePath(), + target.hashString(), + target, + tag, abiType, + values); + + AvdInfo oldAvdInfo = getAvd(avdName, false /*validAvdOnly*/); + + synchronized (mAllAvdList) { + if (oldAvdInfo != null && (removePrevious || editExisting)) { + mAllAvdList.remove(oldAvdInfo); + } + mAllAvdList.add(newAvdInfo); + mValidAvdList = mBrokenAvdList = null; + } + + if ((removePrevious || editExisting) && + newAvdInfo != null && + oldAvdInfo != null && + !oldAvdInfo.getDataFolderPath().equals(newAvdInfo.getDataFolderPath())) { + log.warning("Removing previous AVD directory at %s", + oldAvdInfo.getDataFolderPath()); + // Remove the old data directory + File dir = new File(oldAvdInfo.getDataFolderPath()); + try { + deleteContentOf(dir); + dir.delete(); + } catch (SecurityException e) { + log.error(e, "Failed to delete %1$s", dir.getAbsolutePath()); + } + } + + return newAvdInfo; + } catch (AndroidLocationException e) { + log.error(e, null); + } catch (IOException e) { + log.error(e, null); + } catch (SecurityException e) { + log.error(e, null); + } finally { + if (needCleanup) { + if (iniFile != null && iniFile.exists()) { + iniFile.delete(); + } + + try { + deleteContentOf(avdFolder); + avdFolder.delete(); + } catch (SecurityException e) { + log.error(e, "Failed to delete %1$s", avdFolder.getAbsolutePath()); + } + } + } + + return null; + } + + /** + * Copy the nominated file to the given destination. + * + * @throws FileNotFoundException + * @throws IOException + */ + private void copyImageFile(@NonNull File source, @NonNull File destination) + throws FileNotFoundException, IOException { + FileInputStream fis = new FileInputStream(source); + FileOutputStream fos = new FileOutputStream(destination); + + byte[] buffer = new byte[4096]; + int count; + while ((count = fis.read(buffer)) != -1) { + fos.write(buffer, 0, count); + } + + fos.close(); + fis.close(); + } + + /** + * Returns the path to the target images folder as a relative path to the SDK, if the folder + * is not empty. If the image folder is empty or does not exist, null is returned. + * @throws InvalidTargetPathException if the target image folder is not in the current SDK. + */ + private String getImageRelativePath(@NonNull IAndroidTarget target, + @NonNull IdDisplay tag, + @NonNull String abiType) + throws InvalidTargetPathException { + + ISystemImage systemImage = target.getSystemImage(tag, abiType); + if (systemImage == null) { + // ABI Type is unknown for target + return null; + } + + File folder = systemImage.getLocation(); + String imageFullPath = folder.getAbsolutePath(); + + // make this path relative to the SDK location + String sdkLocation = myLocalSdk.getPath(); + if (!imageFullPath.startsWith(sdkLocation)) { + // this really really should not happen. + assert false; + throw new InvalidTargetPathException("Target location is not inside the SDK."); + } + + if (folder.isDirectory()) { + String[] list = folder.list(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return IMAGE_NAME_PATTERN.matcher(name).matches(); + } + }); + + if (list.length > 0) { + // Remove the SDK root path, e.g. /sdk/dir1/dir2 => /dir1/dir2 + imageFullPath = imageFullPath.substring(sdkLocation.length()); + // The path is relative, so it must not start with a file separator + if (imageFullPath.charAt(0) == File.separatorChar) { + imageFullPath = imageFullPath.substring(1); + } + // For compatibility with previous versions, we denote folders + // by ending the path with file separator + if (!imageFullPath.endsWith(File.separator)) { + imageFullPath += File.separator; + } + + return imageFullPath; + } + } + + return null; + } + + /** + * Returns the path to the skin, as a relative path to the SDK. + * @param skinName The name of the skin to find. Case-sensitive. + * @param target The target where to find the skin. + * @param log the log object to receive action logs. Cannot be null. + */ + @Deprecated + private String getSkinRelativePath(@NonNull String skinName, + @NonNull IAndroidTarget target, + @NonNull ILogger log) { + if (log == null) { + throw new IllegalArgumentException("log cannot be null"); + } + + // first look to see if the skin is in the target + File skin = getSkinFolder(skinName, target); + + // skin really does not exist! + if (skin.exists() == false) { + log.error(null, "Skin '%1$s' does not exist.", skinName); + return null; + } + + // get the skin path + String path = skin.getAbsolutePath(); + + // make this path relative to the SDK location + + String sdkLocation = myLocalSdk.getPath(); + if (path.startsWith(sdkLocation) == false) { + // this really really should not happen. + log.error(null, "Target location is not inside the SDK."); + assert false; + return null; + } + + path = path.substring(sdkLocation.length()); + if (path.charAt(0) == File.separatorChar) { + path = path.substring(1); + } + return path; + } + + /** + * Returns the full absolute OS path to a skin specified by name for a given target. + * @param skinName The name of the skin to find. Case-sensitive. + * @param target The target where to find the skin. + * @return a {@link File} that may or may not actually exist. + */ + private File getSkinFolder(@NonNull String skinName, @NonNull IAndroidTarget target) { + String path = target.getPath(IAndroidTarget.SKINS); + File skin = new File(path, skinName); + + if (skin.exists() == false && target.isPlatform() == false) { + target = target.getParent(); + + path = target.getPath(IAndroidTarget.SKINS); + skin = new File(path, skinName); + } + + return skin; + } + + /** + * Creates the ini file for an AVD. + * + * @param name of the AVD. + * @param avdFolder path for the data folder of the AVD. + * @param target of the AVD. + * @param removePrevious True if an existing ini file should be removed. + * @throws AndroidLocationException if there's a problem getting android root directory. + * @throws IOException if {@link File#getAbsolutePath()} fails. + */ + private File createAvdIniFile(@NonNull String name, + @NonNull File avdFolder, + @NonNull IAndroidTarget target, + boolean removePrevious) + throws AndroidLocationException, IOException { + File iniFile = AvdInfo.getDefaultIniFile(this, name); + + if (removePrevious) { + if (iniFile.isFile()) { + iniFile.delete(); + } else if (iniFile.isDirectory()) { + deleteContentOf(iniFile); + iniFile.delete(); + } + } + + String absPath = avdFolder.getAbsolutePath(); + String relPath = null; + String androidPath = AndroidLocation.getFolder(); + if (absPath.startsWith(androidPath)) { + // Compute the AVD path relative to the android path. + assert androidPath.endsWith(File.separator); + relPath = absPath.substring(androidPath.length()); + } + + HashMap values = new HashMap(); + if (relPath != null) { + values.put(AVD_INFO_REL_PATH, relPath); + } + values.put(AVD_INFO_ABS_PATH, absPath); + values.put(AVD_INFO_TARGET, target.hashString()); + writeIniFile(iniFile, values, true); + + return iniFile; + } + + /** + * Creates the ini file for an AVD. + * + * @param info of the AVD. + * @throws AndroidLocationException if there's a problem getting android root directory. + * @throws IOException if {@link File#getAbsolutePath()} fails. + */ + private File createAvdIniFile(@NonNull AvdInfo info) + throws AndroidLocationException, IOException { + return createAvdIniFile(info.getName(), + new File(info.getDataFolderPath()), + info.getTarget(), + false /*removePrevious*/); + } + + /** + * Actually deletes the files of an existing AVD. + *

+ * This also remove it from the manager's list, The caller does not need to + * call {@link #removeAvd(AvdInfo)} afterwards. + *

+ * This method is designed to somehow work with an unavailable AVD, that is an AVD that + * could not be loaded due to some error. That means this method still tries to remove + * the AVD ini file or its folder if it can be found. An error will be output if any of + * these operations fail. + * + * @param avdInfo the information on the AVD to delete + * @param log the log object to receive action logs. Cannot be null. + * @return True if the AVD was deleted with no error. + */ + public boolean deleteAvd(@NonNull AvdInfo avdInfo, @NonNull ILogger log) { + try { + boolean error = false; + + File f = avdInfo.getIniFile(); + if (f != null && f.exists()) { + log.info("Deleting file %1$s\n", f.getCanonicalPath()); + if (!f.delete()) { + log.error(null, "Failed to delete %1$s\n", f.getCanonicalPath()); + error = true; + } + } + + String path = avdInfo.getDataFolderPath(); + if (path != null) { + f = new File(path); + if (f.exists()) { + log.info("Deleting folder %1$s\n", f.getCanonicalPath()); + if (deleteContentOf(f) == false || f.delete() == false) { + log.error(null, "Failed to delete %1$s\n", f.getCanonicalPath()); + error = true; + } + } + } + + removeAvd(avdInfo); + + if (error) { + log.info("\nAVD '%1$s' deleted with errors. See errors above.\n", + avdInfo.getName()); + } else { + log.info("\nAVD '%1$s' deleted.\n", avdInfo.getName()); + return true; + } + + } catch (IOException e) { + log.error(e, null); + } catch (SecurityException e) { + log.error(e, null); + } + return false; + } + + /** + * Moves and/or rename an existing AVD and its files. + * This also change it in the manager's list. + *

+ * The caller should make sure the name or path given are valid, do not exist and are + * actually different than current values. + * + * @param avdInfo the information on the AVD to move. + * @param newName the new name of the AVD if non null. + * @param paramFolderPath the new data folder if non null. + * @param log the log object to receive action logs. Cannot be null. + * @return True if the move succeeded or there was nothing to do. + * If false, this method will have had already output error in the log. + */ + public boolean moveAvd(@NonNull AvdInfo avdInfo, + @Nullable String newName, + @Nullable String paramFolderPath, + @NonNull ILogger log) { + + try { + if (paramFolderPath != null) { + File f = new File(avdInfo.getDataFolderPath()); + log.warning("Moving '%1$s' to '%2$s'.", + avdInfo.getDataFolderPath(), + paramFolderPath); + if (!f.renameTo(new File(paramFolderPath))) { + log.error(null, "Failed to move '%1$s' to '%2$s'.", + avdInfo.getDataFolderPath(), paramFolderPath); + return false; + } + + // update AVD info + AvdInfo info = new AvdInfo( + avdInfo.getName(), + avdInfo.getIniFile(), + paramFolderPath, + avdInfo.getTargetHash(), + avdInfo.getTarget(), + avdInfo.getTag(), + avdInfo.getAbiType(), + avdInfo.getProperties()); + replaceAvd(avdInfo, info); + + // update the ini file + createAvdIniFile(info); + } + + if (newName != null) { + File oldIniFile = avdInfo.getIniFile(); + File newIniFile = AvdInfo.getDefaultIniFile(this, newName); + + log.warning("Moving '%1$s' to '%2$s'.", oldIniFile.getPath(), newIniFile.getPath()); + if (!oldIniFile.renameTo(newIniFile)) { + log.error(null, "Failed to move '%1$s' to '%2$s'.", + oldIniFile.getPath(), newIniFile.getPath()); + return false; + } + + // update AVD info + AvdInfo info = new AvdInfo( + newName, + avdInfo.getIniFile(), + avdInfo.getDataFolderPath(), + avdInfo.getTargetHash(), + avdInfo.getTarget(), + avdInfo.getTag(), + avdInfo.getAbiType(), + avdInfo.getProperties()); + replaceAvd(avdInfo, info); + } + + log.info("AVD '%1$s' moved.\n", avdInfo.getName()); + + } catch (AndroidLocationException e) { + log.error(e, null); + } catch (IOException e) { + log.error(e, null); + } + + // nothing to do or succeeded + return true; + } + + /** + * Helper method to recursively delete a folder's content (but not the folder itself). + * + * @throws SecurityException like {@link File#delete()} does if file/folder is not writable. + */ + private boolean deleteContentOf(File folder) throws SecurityException { + File[] files = folder.listFiles(); + if (files != null) { + for (File f : files) { + if (f.isDirectory()) { + if (deleteContentOf(f) == false) { + return false; + } + } + if (f.delete() == false) { + return false; + } + + } + } + + return true; + } + + /** + * Returns a list of files that are potential AVD ini files. + *

+ * This lists the $HOME/.android/avd/.ini files. + * Such files are properties file than then indicate where the AVD folder is located. + *

+ * Note: the method is to be considered private. It is made protected so that + * unit tests can easily override the AVD root. + * + * @return A new {@link File} array or null. The array might be empty. + * @throws AndroidLocationException if there's a problem getting android root directory. + */ + private File[] buildAvdFilesList() throws AndroidLocationException { + File folder = new File(getBaseAvdFolder()); + + // ensure folder validity. + if (folder.isFile()) { + throw new AndroidLocationException( + String.format("%1$s is not a valid folder.", folder.getAbsolutePath())); + } else if (folder.exists() == false) { + // folder is not there, we create it and return + folder.mkdirs(); + return null; + } + + File[] avds = folder.listFiles(new FilenameFilter() { + @Override + public boolean accept(File parent, String name) { + if (INI_NAME_PATTERN.matcher(name).matches()) { + // check it's a file and not a folder + boolean isFile = new File(parent, name).isFile(); + return isFile; + } + + return false; + } + }); + + return avds; + } + + /** + * Computes the internal list of available AVDs + * @param allList the list to contain all the AVDs + * @param log the log object to receive action logs. Cannot be null. + * + * @throws AndroidLocationException if there's a problem getting android root directory. + */ + private void buildAvdList(ArrayList allList, ILogger log) + throws AndroidLocationException { + File[] avds = buildAvdFilesList(); + if (avds != null) { + for (File avd : avds) { + AvdInfo info = parseAvdInfo(avd, log); + if (info != null && !allList.contains(info)) { + allList.add(info); + } + } + } + } + + private DeviceManager getDeviceManager(ILogger logger) { + DeviceManager manager = myDeviceManagers.get(logger); + if (manager == null) { + manager = DeviceManager.createInstance(myLocalSdk.getLocation(), logger); + manager.registerListener(new DeviceManager.DevicesChangedListener() { + @Override + public void onDevicesChanged() { + myDeviceManagers.clear(); + } + }); + myDeviceManagers.put(logger, manager); + } + return manager; + } + + /** + * Parses an AVD .ini file to create an {@link AvdInfo}. + * + * @param iniPath The path to the AVD .ini file + * @param log the log object to receive action logs. Cannot be null. + * @return A new {@link AvdInfo} with an {@link AvdStatus} indicating whether this AVD is + * valid or not. + */ + private AvdInfo parseAvdInfo(File iniPath, ILogger log) { + Map map = parseIniFile( + new FileWrapper(iniPath), + log); + + String avdPath = map.get(AVD_INFO_ABS_PATH); + String targetHash = map.get(AVD_INFO_TARGET); + + if (!(new File(avdPath).isDirectory())) { + // Try to fallback on the relative path, if present. + String relPath = map.get(AVD_INFO_REL_PATH); + if (relPath != null) { + try { + String androidPath = AndroidLocation.getFolder(); + File f = new File(androidPath, relPath); + if (f.isDirectory()) { + avdPath = f.getAbsolutePath(); + } + } catch (AndroidLocationException ignore) {} + } + } + + IAndroidTarget target = null; + FileWrapper configIniFile = null; + Map properties = null; + + if (targetHash != null) { + target = myLocalSdk.getTargetFromHashString(targetHash); + } + + // load the AVD properties. + if (avdPath != null) { + configIniFile = new FileWrapper(avdPath, CONFIG_INI); + } + + if (configIniFile != null) { + if (!configIniFile.isFile()) { + log.warning("Missing file '%1$s'.", configIniFile.getPath()); + } else { + properties = parseIniFile(configIniFile, log); + } + } + + // get name + String name = iniPath.getName(); + Matcher matcher = INI_NAME_PATTERN.matcher(iniPath.getName()); + if (matcher.matches()) { + name = matcher.group(1); + } + + // get tag + IdDisplay tag = SystemImage.DEFAULT_TAG; + String tagId = properties == null ? null : properties.get(AVD_INI_TAG_ID); + if (tagId != null) { + String tagDisp = properties == null ? null : properties.get(AVD_INI_TAG_DISPLAY); + if (tagDisp == null || tagDisp.isEmpty()) { + tagDisp = LocalSysImgPkgInfo.tagIdToDisplay(tagId); + } + tag = new IdDisplay(tagId, tagDisp); + } + + // get abi type + String abiType = properties == null ? null : properties.get(AVD_INI_ABI_TYPE); + // for the avds created previously without enhancement, i.e. They are created based + // on previous API Levels. They are supposed to have ARM processor type + if (abiType == null) { + abiType = SdkConstants.ABI_ARMEABI; + } + + // check the image.sysdir are valid + boolean validImageSysdir = true; + if (properties != null) { + String imageSysDir = properties.get(AVD_INI_IMAGES_1); + if (imageSysDir != null) { + File f = new File(myLocalSdk.getLocation(), imageSysDir); + if (f.isDirectory() == false) { + validImageSysdir = false; + } else { + imageSysDir = properties.get(AVD_INI_IMAGES_2); + if (imageSysDir != null) { + f = new File(myLocalSdk.getLocation(), imageSysDir); + if (f.isDirectory() == false) { + validImageSysdir = false; + } + } + } + } + } + + // Check the system image from the target + ISystemImage sysImage = target != null ? target.getSystemImage(tag, abiType) : null; + + // Get the device status if this AVD is associated with a device + DeviceStatus deviceStatus = null; + boolean updateHashV2 = false; + if (properties != null) { + String deviceName = properties.get(AVD_INI_DEVICE_NAME); + String deviceMfctr = properties.get(AVD_INI_DEVICE_MANUFACTURER); + + Device d = null; + + if (deviceName != null && deviceMfctr != null) { + DeviceManager devMan = getDeviceManager(log); + d = devMan.getDevice(deviceName, deviceMfctr); + deviceStatus = d == null ? DeviceStatus.MISSING : DeviceStatus.EXISTS; + + if (d != null) { + updateHashV2 = true; + String hashV2 = properties.get(AVD_INI_DEVICE_HASH_V2); + if (hashV2 != null) { + String newHashV2 = DeviceManager.hasHardwarePropHashChanged(d, hashV2); + if (newHashV2 == null) { + updateHashV2 = false; + } else { + properties.put(AVD_INI_DEVICE_HASH_V2, newHashV2); + } + } + + String hashV1 = properties.get(AVD_INI_DEVICE_HASH_V1); + if (hashV1 != null) { + // will recompute a hash v2 and save it below + properties.remove(AVD_INI_DEVICE_HASH_V1); + } + } + } + } + + + // TODO: What about missing sdcard, skins, etc? + + AvdStatus status; + + if (avdPath == null) { + status = AvdStatus.ERROR_PATH; + } else if (configIniFile == null) { + status = AvdStatus.ERROR_CONFIG; + } else if (targetHash == null) { + status = AvdStatus.ERROR_TARGET_HASH; + } else if (target == null) { + status = AvdStatus.ERROR_TARGET; + } else if (properties == null) { + status = AvdStatus.ERROR_PROPERTIES; + } else if (validImageSysdir == false) { + status = AvdStatus.ERROR_IMAGE_DIR; + } else if (deviceStatus == DeviceStatus.CHANGED) { + status = AvdStatus.ERROR_DEVICE_CHANGED; + } else if (deviceStatus == DeviceStatus.MISSING) { + status = AvdStatus.ERROR_DEVICE_MISSING; + } else if (sysImage == null) { + status = AvdStatus.ERROR_IMAGE_MISSING; + } else { + status = AvdStatus.OK; + } + + AvdInfo info = new AvdInfo( + name, + iniPath, + avdPath, + targetHash, + target, + tag, + abiType, + properties, + status); + + if (updateHashV2) { + try { + return updateDeviceChanged(info, log); + } catch (IOException ignore) {} + } + + return info; + } + + /** + * Writes a .ini file from a set of properties, using UTF-8 encoding. + * The keys are sorted. + * The file should be read back later by {@link #parseIniFile(IAbstractFile, ILogger)}. + * + * @param iniFile The file to generate. + * @param values The properties to place in the ini file. + * @param addEncoding When true, add a property {@link #AVD_INI_ENCODING} indicating the + * encoding used to write the file. + * @throws IOException if {@link FileWriter} fails to open, write or close the file. + */ + private static void writeIniFile(File iniFile, Map values, boolean addEncoding) + throws IOException { + + Charset charset = Charsets.UTF_8; + OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(iniFile), charset); + try { + if (addEncoding) { + // Write down the charset used in case we want to use it later. + writer.write(String.format("%1$s=%2$s\n", AVD_INI_ENCODING, charset.name())); + } + + ArrayList keys = new ArrayList(values.keySet()); + Collections.sort(keys); + + for (String key : keys) { + String value = values.get(key); + writer.write(String.format("%1$s=%2$s\n", key, value)); + } + } finally { + writer.close(); + } + } + + /** + * Parses a property file and returns a map of the content. + *

+ * If the file is not present, null is returned with no error messages sent to the log. + *

+ * Charset encoding will be either the system's default or the one specified by the + * {@link #AVD_INI_ENCODING} key if present. + * + * @param propFile the property file to parse + * @param log the ILogger object receiving warning/error from the parsing. + * @return the map of (key,value) pairs, or null if the parsing failed. + */ + private static Map parseIniFile( + @NonNull IAbstractFile propFile, + @Nullable ILogger log) { + return parseIniFileImpl(propFile, log, null /*charset*/); + } + + /** + * Implementation helper for the {@link #parseIniFile(IAbstractFile, ILogger)} method. + * Don't call this one directly. + * + * @param propFile the property file to parse + * @param log the ILogger object receiving warning/error from the parsing. + * @param charset When a specific charset is specified, this will be used as-is. + * When null, the default charset will first be used and if the key + * {@link #AVD_INI_ENCODING} is found the parsing will restart using that specific + * charset. + * @return the map of (key,value) pairs, or null if the parsing failed. + */ + private static Map parseIniFileImpl( + @NonNull IAbstractFile propFile, + @Nullable ILogger log, + @Nullable Charset charset) { + + BufferedReader reader = null; + try { + boolean canChangeCharset = false; + if (charset == null) { + canChangeCharset = true; + charset = Charsets.ISO_8859_1; + } + reader = new BufferedReader(new InputStreamReader(propFile.getContents(), charset)); + + String line = null; + Map map = new HashMap(); + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (!line.isEmpty() && line.charAt(0) != '#') { + + Matcher m = INI_LINE_PATTERN.matcher(line); + if (m.matches()) { + // Note: we do NOT escape values. + String key = m.group(1); + String value = m.group(2); + + // If we find the charset encoding and it's not the same one and + // it's a valid one, re-read the file using that charset. + if (canChangeCharset && + AVD_INI_ENCODING.equals(key) && + !charset.name().equals(value) && + Charset.isSupported(value)) { + charset = Charset.forName(value); + return parseIniFileImpl(propFile, log, charset); + } + + map.put(key, value); + } else { + if (log != null) { + log.warning("Error parsing '%1$s': \"%2$s\" is not a valid syntax", + propFile.getOsLocation(), + line); + } + return null; + } + } + } + + return map; + } catch (FileNotFoundException e) { + // this should not happen since we usually test the file existence before + // calling the method. + // Return null below. + } catch (IOException e) { + if (log != null) { + log.warning("Error parsing '%1$s': %2$s.", + propFile.getOsLocation(), + e.getMessage()); + } + } catch (StreamException e) { + if (log != null) { + log.warning("Error parsing '%1$s': %2$s.", + propFile.getOsLocation(), + e.getMessage()); + } + } finally { + try { + Closeables.close(reader, true /* swallowIOException */); + } catch (IOException e) { + // cannot happen. + } + } + + return null; + } + + /** + * Invokes the tool to create a new SD card image file. + * + * @param toolLocation The path to the mksdcard tool. + * @param size The size of the new SD Card, compatible with {@link #SDCARD_SIZE_PATTERN}. + * @param location The path of the new sdcard image file to generate. + * @param log the log object to receive action logs. Cannot be null. + * @return True if the sdcard could be created. + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected boolean createSdCard(String toolLocation, String size, String location, ILogger log) { + try { + String[] command = new String[3]; + command[0] = toolLocation; + command[1] = size; + command[2] = location; + Process process = Runtime.getRuntime().exec(command); + + final ArrayList errorOutput = new ArrayList(); + final ArrayList stdOutput = new ArrayList(); + + int status = GrabProcessOutput.grabProcessOutput( + process, + Wait.WAIT_FOR_READERS, + new IProcessOutput() { + @Override + public void out(@Nullable String line) { + if (line != null) { + stdOutput.add(line); + } + } + + @Override + public void err(@Nullable String line) { + if (line != null) { + errorOutput.add(line); + } + } + }); + + if (status == 0) { + return true; + } else { + for (String error : errorOutput) { + log.error(null, error); + } + } + + } catch (InterruptedException e) { + // pass, print error below + } catch (IOException e) { + // pass, print error below + } + + log.error(null, "Failed to create the SD card."); + return false; + } + + /** + * Removes an {@link AvdInfo} from the internal list. + * + * @param avdInfo The {@link AvdInfo} to remove. + * @return true if this {@link AvdInfo} was present and has been removed. + */ + public boolean removeAvd(AvdInfo avdInfo) { + synchronized (mAllAvdList) { + if (mAllAvdList.remove(avdInfo)) { + mValidAvdList = mBrokenAvdList = null; + return true; + } + } + + return false; + } + + /** + * Updates an AVD with new path to the system image folders. + * @param name the name of the AVD to update. + * @param log the log object to receive action logs. Cannot be null. + * @throws IOException + */ + public void updateAvd(String name, ILogger log) throws IOException { + // find the AVD to update. It should be be in the broken list. + AvdInfo avd = null; + synchronized (mAllAvdList) { + for (AvdInfo info : mAllAvdList) { + if (info.getName().equals(name)) { + avd = info; + break; + } + } + } + + if (avd == null) { + // not in the broken list, just return. + log.error(null, "There is no Android Virtual Device named '%s'.", name); + return; + } + + updateAvd(avd, log); + } + + + /** + * Updates an AVD with new path to the system image folders. + * @param avd the AVD to update. + * @param log the log object to receive action logs. Cannot be null. + * @throws IOException + */ + public AvdInfo updateAvd(AvdInfo avd, ILogger log) throws IOException { + // get the properties. This is a unmodifiable Map. + Map oldProperties = avd.getProperties(); + + // create a new map + Map properties = new HashMap(); + if (oldProperties != null) { + properties.putAll(oldProperties); + } + + AvdStatus status; + + // create the path to the new system images. + if (setImagePathProperties(avd.getTarget(), + avd.getTag(), + avd.getAbiType(), + properties, + log)) { + if (properties.containsKey(AVD_INI_IMAGES_1)) { + log.info("Updated '%1$s' with value '%2$s'\n", AVD_INI_IMAGES_1, + properties.get(AVD_INI_IMAGES_1)); + } + + if (properties.containsKey(AVD_INI_IMAGES_2)) { + log.info("Updated '%1$s' with value '%2$s'\n", AVD_INI_IMAGES_2, + properties.get(AVD_INI_IMAGES_2)); + } + + status = AvdStatus.OK; + } else { + log.error(null, "Unable to find non empty system images folders for %1$s", + avd.getName()); + //FIXME: display paths to empty image folders? + status = AvdStatus.ERROR_IMAGE_DIR; + } + + return updateAvd(avd, properties, status, log); + } + + public AvdInfo updateAvd(AvdInfo avd, + Map newProperties, + AvdStatus status, + ILogger log) throws IOException { + // now write the config file + File configIniFile = new File(avd.getDataFolderPath(), CONFIG_INI); + writeIniFile(configIniFile, newProperties, true); + + // finally create a new AvdInfo for this unbroken avd and add it to the list. + // instead of creating the AvdInfo object directly we reparse it, to detect other possible + // errors + // FIXME: We may want to create this AvdInfo by reparsing the AVD instead. This could detect other errors. + AvdInfo newAvd = new AvdInfo( + avd.getName(), + avd.getIniFile(), + avd.getDataFolderPath(), + avd.getTargetHash(), + avd.getTarget(), + avd.getTag(), + avd.getAbiType(), + newProperties); + + replaceAvd(avd, newAvd); + + return newAvd; + } + + /** + * Updates the device-specific part of an AVD ini. + * @param avd the AVD to update. + * @param log the log object to receive action logs. Cannot be null. + * @return The new AVD on success. + * @throws IOException + */ + public AvdInfo updateDeviceChanged(AvdInfo avd, ILogger log) throws IOException { + + // Overwrite the properties derived from the device and nothing else + Map properties = new HashMap(avd.getProperties()); + + DeviceManager devMan = getDeviceManager(log); + Collection devices = devMan.getDevices(DeviceManager.ALL_DEVICES); + String name = properties.get(AvdManager.AVD_INI_DEVICE_NAME); + String manufacturer = properties.get(AvdManager.AVD_INI_DEVICE_MANUFACTURER); + + if (properties != null && devices != null && name != null && manufacturer != null) { + for (Device d : devices) { + if (d.getId().equals(name) && d.getManufacturer().equals(manufacturer)) { + properties.putAll(DeviceManager.getHardwareProperties(d)); + try { + return updateAvd(avd, properties, AvdStatus.OK, log); + } catch (IOException e) { + log.error(e, null); + } + } + } + } else { + log.error(null, "Base device information incomplete or missing."); + } + return null; + } + + /** + * Sets the paths to the system images in a properties map. + * + * @param target the target in which to find the system images. + * @param abiType the abi type of the avd to find + * the architecture-dependent system images. + * @param properties the properties in which to set the paths. + * @param log the log object to receive action logs. Cannot be null. + * @return true if success, false if some path are missing. + */ + private boolean setImagePathProperties(IAndroidTarget target, + IdDisplay tag, + String abiType, + Map properties, + ILogger log) { + properties.remove(AVD_INI_IMAGES_1); + properties.remove(AVD_INI_IMAGES_2); + + try { + String property = AVD_INI_IMAGES_1; + + // First the image folders of the target itself + String imagePath = getImageRelativePath(target, tag, abiType); + if (imagePath != null) { + properties.put(property, imagePath); + property = AVD_INI_IMAGES_2; + } + + // If the target is an add-on we need to add the Platform image as a backup. + IAndroidTarget parent = target.getParent(); + if (parent != null) { + imagePath = getImageRelativePath(parent, tag, abiType); + if (imagePath != null) { + properties.put(property, imagePath); + } + } + + // we need at least one path! + return properties.containsKey(AVD_INI_IMAGES_1); + } catch (InvalidTargetPathException e) { + log.error(e, e.getMessage()); + } + + return false; + } + + /** + * Replaces an old {@link AvdInfo} with a new one in the lists storing them. + * @param oldAvd the {@link AvdInfo} to remove. + * @param newAvd the {@link AvdInfo} to add. + */ + private void replaceAvd(AvdInfo oldAvd, AvdInfo newAvd) { + synchronized (mAllAvdList) { + mAllAvdList.remove(oldAvd); + mAllAvdList.add(newAvd); + mValidAvdList = mBrokenAvdList = null; + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/avd/HardwareProperties.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/avd/HardwareProperties.java new file mode 100644 index 00000000..6b91b459 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/avd/HardwareProperties.java @@ -0,0 +1,327 @@ +/* + * 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.sdklib.internal.avd; + +import com.android.utils.ILogger; +import com.google.common.base.Charsets; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Map; +import java.util.TreeMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class HardwareProperties { + /** AVD/config.ini key for whether hardware buttons are present. */ + public static final String HW_MAINKEYS = "hw.mainKeys"; + + /** AVD/config.ini key indicating whether trackball is present. */ + public static final String HW_TRACKBALL = "hw.trackBall"; + + /** AVD/config.ini key indicating whether qwerty keyboard is present. */ + public static final String HW_KEYBOARD = "hw.keyboard"; + + /** AVD/config.ini key indicating whether dpad is present. */ + public static final String HW_DPAD = "hw.dPad"; + + /** AVD/config.ini key indicating whether gps is present. */ + public static final String HW_GPS = "hw.gps"; + + /** AVD/config.ini key indicating whether the device is running on battery. */ + public static final String HW_BATTERY = "hw.battery"; + + /** AVD/config.ini key indicating whether accelerometer is present. */ + public static final String HW_ACCELEROMETER = "hw.accelerometer"; + + /** AVD/config.ini key indicating whether gyroscope is present. */ + public static final String HW_ORIENTATION_SENSOR = "hw.sensors.orientation"; + + /** AVD/config.ini key indicating whether h/w mic is present. */ + public static final String HW_AUDIO_INPUT = "hw.audioInput"; + + /** AVD/config.ini key indicating whether sdcard is present. */ + public static final String HW_SDCARD = "hw.sdCard"; + + /** AVD/config.ini key for LCD density. */ + public static final String HW_LCD_DENSITY = "hw.lcd.density"; + + /** AVD/config.ini key indicating whether proximity sensor present. */ + public static final String HW_PROXIMITY_SENSOR = "hw.sensors.proximity"; + + /** AVD/config.ini key for initial device orientation. */ + public static final String HW_INITIAL_ORIENTATION = "hw.initialOrientation"; + + + private static final Pattern PATTERN_PROP = Pattern.compile( + "^([a-zA-Z0-9._-]+)\\s*=\\s*(.*)\\s*$"); + + /** Property name in the generated avd config file; String; e.g. "hw.screen" */ + private static final String HW_PROP_NAME = "name"; //$NON-NLS-1$ + /** Property type, one of {@link HardwarePropertyType} */ + private static final String HW_PROP_TYPE = "type"; //$NON-NLS-1$ + /** Default value of the property. String matching the property type. */ + private static final String HW_PROP_DEFAULT = "default"; //$NON-NLS-1$ + /** User-visible name of the property. String. */ + private static final String HW_PROP_ABSTRACT = "abstract"; //$NON-NLS-1$ + /** User-visible description of the property. String. */ + private static final String HW_PROP_DESC = "description"; //$NON-NLS-1$ + /** Comma-separate values for a property of type "enum" */ + private static final String HW_PROP_ENUM = "enum"; //$NON-NLS-1$ + + public static final String BOOLEAN_YES = "yes"; + public static final String BOOLEAN_NO = "no"; + public static final Pattern DISKSIZE_PATTERN = Pattern.compile("\\d+[MK]B"); //$NON-NLS-1$ + + /** Represents the type of a hardware property value. */ + public enum HardwarePropertyType { + INTEGER ("integer", false /*isEnum*/), //$NON-NLS-1$ + BOOLEAN ("boolean", false /*isEnum*/), //$NON-NLS-1$ + DISKSIZE ("diskSize", false /*isEnum*/), //$NON-NLS-1$ + STRING ("string", false /*isEnum*/), //$NON-NLS-1$ + INTEGER_ENUM("integer", true /*isEnum*/), //$NON-NLS-1$ + STRING_ENUM ("string", true /*isEnum*/); //$NON-NLS-1$ + + + private String mName; + private boolean mIsEnum; + + HardwarePropertyType(String name, boolean isEnum) { + mName = name; + mIsEnum = isEnum; + } + + /** Returns the name of the type (e.g. "string", "boolean", etc.) */ + public String getName() { + return mName; + } + + /** Indicates whether this type is an enum (e.g. "enum of strings"). */ + public boolean isEnum() { + return mIsEnum; + } + + /** Returns the internal HardwarePropertyType object matching the given type name. */ + public static HardwarePropertyType getEnum(String name, boolean isEnum) { + for (HardwarePropertyType type : values()) { + if (type.mName.equals(name) && type.mIsEnum == isEnum) { + return type; + } + } + + return null; + } + } + + public static final class HardwareProperty { + private String mName; + private HardwarePropertyType mType; + /** the string representation of the default value. can be null. */ + private String mDefault; + /** the choices for an enum. Null if not an enum. */ + private String[] mEnum; + private String mAbstract; + private String mDescription; + + public HardwareProperty() { + // initialize strings to sane defaults, as not all properties will be set from + // the ini file + mName = ""; + mDefault = ""; + mAbstract = ""; + mDescription = ""; + } + + /** Returns the hardware config name of the property, e.g. "hw.screen" */ + public String getName() { + return mName; + } + + /** Returns the property type, one of {@link HardwarePropertyType} */ + public HardwarePropertyType getType() { + return mType; + } + + /** + * Returns the default value of the property. + * String matching the property type. + * Can be null. + */ + public String getDefault() { + return mDefault; + } + + /** Returns the user-visible name of the property. */ + public String getAbstract() { + return mAbstract; + } + + /** Returns the user-visible description of the property. */ + public String getDescription() { + return mDescription; + } + + /** Returns the possible values for an enum property. Can be null. */ + public String[] getEnum() { + return mEnum; + } + + public boolean isValidForUi() { + // don't display single string type for now. + return mType != HardwarePropertyType.STRING || mType.isEnum(); + } + } + + /** + * Parses the hardware definition file. + * @param file the property file to parse + * @param log the ILogger object receiving warning/error from the parsing. Cannot be null. + * @return the map of (key,value) pairs, or null if the parsing failed. + */ + public static Map parseHardwareDefinitions(File file, ILogger log) { + BufferedReader reader = null; + try { + FileInputStream fis = new FileInputStream(file); + reader = new BufferedReader(new InputStreamReader(fis, Charsets.UTF_8)); + + Map map = new TreeMap(); + + String line = null; + HardwareProperty prop = null; + while ((line = reader.readLine()) != null) { + if (!line.isEmpty() && line.charAt(0) != '#') { + Matcher m = PATTERN_PROP.matcher(line); + if (m.matches()) { + String key = m.group(1); + String value = m.group(2); + + if (HW_PROP_NAME.equals(key)) { + prop = new HardwareProperty(); + prop.mName = value; + map.put(prop.mName, prop); + } + + if (prop == null) { + log.warning("Error parsing '%1$s': missing '%2$s'", + file.getAbsolutePath(), HW_PROP_NAME); + return null; + } + + if (HW_PROP_TYPE.equals(key)) { + // Note: we don't know yet whether this type is an enum. + // This is indicated by the "enum = value" line that is parsed later. + prop.mType = HardwarePropertyType.getEnum(value, false); + assert (prop.mType != null); + } else if (HW_PROP_DEFAULT.equals(key)) { + prop.mDefault = value; + } else if (HW_PROP_ABSTRACT.equals(key)) { + prop.mAbstract = value; + } else if (HW_PROP_DESC.equals(key)) { + prop.mDescription = value; + } else if (HW_PROP_ENUM.equals(key)) { + if (!prop.mType.isEnum()) { + // Change the type to an enum, if valid. + prop.mType = HardwarePropertyType.getEnum(prop.mType.getName(), + true); + assert (prop.mType != null); + } + + // Sanitize input: trim spaces, ignore empty entries. + String[] v = value.split(","); + int n = 0; + for (int i = 0; i < v.length; i++) { + String s = v[i] = v[i].trim(); + if (!s.isEmpty()) { + n++; + } + } + prop.mEnum = new String[n]; + n = 0; + for (int i = 0; i < v.length; i++) { + String s = v[i]; + if (!s.isEmpty()) { + prop.mEnum[n++] = s; + } + } + } + } else { + log.warning("Error parsing '%1$s': \"%2$s\" is not a valid syntax", + file.getAbsolutePath(), line); + return null; + } + } + } + + return map; + } catch (FileNotFoundException e) { + // this should not happen since we usually test the file existence before + // calling the method. + // Return null below. + } catch (IOException e) { + log.warning("Error parsing '%1$s': %2$s.", file.getAbsolutePath(), + e.getMessage()); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + // ignore + } + } + } + + return null; + } + + /** + * Returns the boolean value matching the given index. + * This is the reverse of {@link #getBooleanValueIndex(String)}. + * + * @param index 0 or 1. + * @return {@link #BOOLEAN_YES} for 0 or {@link #BOOLEAN_NO} for 1. + * @throws IndexOutOfBoundsException if index is neither 0 nor 1. + */ + public static String getBooleanValue(int index) { + if (index == 0) { + return BOOLEAN_YES; + } else if (index == 1) { + return BOOLEAN_NO; + } + throw new IndexOutOfBoundsException("HardwareProperty boolean index must 0 (true) or 1 (false) but was " + index); + } + + /** + * Returns the index of a boolean value. + * This if the reverse of {@link #getBooleanValue(int)}. + * + * @param value Either {@link #BOOLEAN_YES} or {@link #BOOLEAN_NO}. + * @return 0 for {@link #BOOLEAN_YES}, 1 for {@link #BOOLEAN_NO} or -1 for anything else. + */ + public static int getBooleanValueIndex(String value) { + if (BOOLEAN_YES.equals(value)) { + return 0; + } else if (BOOLEAN_NO.equals(value)) { + return 1; + } + + return -1; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/BuildConfig.template b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/BuildConfig.template new file mode 100644 index 00000000..0344b55c --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/BuildConfig.template @@ -0,0 +1,6 @@ +/** Automatically generated file. DO NOT MODIFY */ +package #PACKAGE#; + +public final class BuildConfig { + public final static boolean DEBUG = #DEBUG#; +} \ No newline at end of file diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/BuildConfigGenerator.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/BuildConfigGenerator.java new file mode 100644 index 00000000..96fa976d --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/BuildConfigGenerator.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2011 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.sdklib.internal.build; + +import com.android.SdkConstants; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +/** + * Class able to generate a BuildConfig class in Android project. + * The BuildConfig class contains constants related to the build target. + * + * @deprecated Use Android-Builder instead + */ +@Deprecated +public class BuildConfigGenerator { + + public static final String BUILD_CONFIG_NAME = "BuildConfig.java"; + + private static final String PH_PACKAGE = "#PACKAGE#"; + private static final String PH_DEBUG = "#DEBUG#"; + + private final String mGenFolder; + private final String mAppPackage; + private final boolean mDebug; + + /** + * Creates a generator + * @param genFolder the gen folder of the project + * @param appPackage the application package + * @param debug whether it's a debug build + */ + public BuildConfigGenerator(String genFolder, String appPackage, boolean debug) { + mGenFolder = genFolder; + mAppPackage = appPackage; + mDebug = debug; + } + + /** + * Returns a File representing where the BuildConfig class will be. + */ + public File getFolderPath() { + File genFolder = new File(mGenFolder); + return new File(genFolder, mAppPackage.replace('.', File.separatorChar)); + } + + public File getBuildConfigFile() { + File folder = getFolderPath(); + return new File(folder, BUILD_CONFIG_NAME); + } + + /** + * Generates the BuildConfig class. + */ + public void generate() throws IOException { + String template = readEmbeddedTextFile("BuildConfig.template"); + + Map map = new HashMap(); + map.put(PH_PACKAGE, mAppPackage); + map.put(PH_DEBUG, Boolean.toString(mDebug)); + + String content = replaceParameters(template, map); + + File pkgFolder = getFolderPath(); + if (pkgFolder.isDirectory() == false) { + pkgFolder.mkdirs(); + } + + File buildConfigJava = new File(pkgFolder, BUILD_CONFIG_NAME); + writeFile(buildConfigJava, content); + } + + /** + * Reads and returns the content of a text file embedded in the jar file. + * @param filepath the file path to the text file + * @return null if the file could not be read + * @throws IOException + */ + private String readEmbeddedTextFile(String filepath) throws IOException { + InputStream is = BuildConfigGenerator.class.getResourceAsStream(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(); + } + + // this really shouldn't happen unless the sdklib packaging is broken. + throw new IOException("BuildConfig template is missing!"); + } + + private void writeFile(File file, String content) throws IOException { + FileOutputStream fos = null; + try { + fos = new FileOutputStream(file); + InputStream source = new ByteArrayInputStream(content.getBytes(SdkConstants.UTF_8)); + + byte[] buffer = new byte[1024]; + int count = 0; + while ((count = source.read(buffer)) != -1) { + fos.write(buffer, 0, count); + } + } finally { + if (fos != null) { + fos.close(); + } + } + } + + /** + * Replaces placeholders found in a string with values. + * + * @param str the string to search for placeholders. + * @param parameters a map of to search for in the string + * @return A new String object with the placeholder replaced by the values. + */ + private String replaceParameters(String str, Map parameters) { + + for (Entry entry : parameters.entrySet()) { + String value = entry.getValue(); + if (value != null) { + str = str.replaceAll(entry.getKey(), value); + } + } + + return str; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/DebugKeyProvider.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/DebugKeyProvider.java new file mode 100644 index 00000000..0f492eab --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/DebugKeyProvider.java @@ -0,0 +1,215 @@ +/* + * 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.sdklib.internal.build; + +import com.android.prefs.AndroidLocation; +import com.android.prefs.AndroidLocation.AndroidLocationException; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.UnrecoverableEntryException; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; + +/** + * A provider of a dummy key to sign Android application for debugging purpose. + *

This provider uses a custom keystore to create and store a key with a known password. + * + * @deprecated Use Android-Builder instead + */ +@Deprecated +public class DebugKeyProvider { + + public interface IKeyGenOutput { + void out(String message); + void err(String message); + } + + private static final String PASSWORD_STRING = "android"; + private static final char[] PASSWORD_CHAR = PASSWORD_STRING.toCharArray(); + private static final String DEBUG_ALIAS = "AndroidDebugKey"; + + // Certificate CN value. This is a hard-coded value for the debug key. + // Android Market checks against this value in order to refuse applications signed with + // debug keys. + private static final String CERTIFICATE_DESC = "CN=Android Debug,O=Android,C=US"; + + private KeyStore.PrivateKeyEntry mEntry; + + public static class KeytoolException extends Exception { + /** default serial uid */ + private static final long serialVersionUID = 1L; + private String mJavaHome = null; + private String mCommandLine = null; + + KeytoolException(String message) { + super(message); + } + + KeytoolException(String message, String javaHome, String commandLine) { + super(message); + + mJavaHome = javaHome; + mCommandLine = commandLine; + } + + public String getJavaHome() { + return mJavaHome; + } + + public String getCommandLine() { + return mCommandLine; + } + } + + /** + * Creates a provider using a keystore at the given location. + *

The keystore, and a new random android debug key are created if they do not yet exist. + *

Password for the store/key is android, and the key alias is + * AndroidDebugKey. + * @param osKeyStorePath the OS path to the keystore, or null if the default one + * is to be used. + * @param storeType an optional keystore type, or null if the default is to + * be used. + * @param output an optional {@link IKeyGenOutput} object to get the stdout and stderr + * of the keytool process call. + * @throws KeytoolException If the creation of the debug key failed. + * @throws AndroidLocationException + */ + public DebugKeyProvider(String osKeyStorePath, String storeType, IKeyGenOutput output) + throws KeyStoreException, NoSuchAlgorithmException, CertificateException, + UnrecoverableEntryException, IOException, KeytoolException, AndroidLocationException { + + if (osKeyStorePath == null) { + osKeyStorePath = getDefaultKeyStoreOsPath(); + } + + if (loadKeyEntry(osKeyStorePath, storeType) == false) { + // create the store with the key + createNewStore(osKeyStorePath, storeType, output); + } + } + + /** + * Returns the OS path to the default debug keystore. + * + * @return The OS path to the default debug keystore. + * @throws KeytoolException + * @throws AndroidLocationException + */ + public static String getDefaultKeyStoreOsPath() + throws KeytoolException, AndroidLocationException { + String folder = AndroidLocation.getFolder(); + if (folder == null) { + throw new KeytoolException("Failed to get HOME directory!\n"); + } + String osKeyStorePath = folder + "debug.keystore"; + + return osKeyStorePath; + } + + /** + * Returns the debug {@link PrivateKey} to use to sign applications for debug purpose. + * @return the private key or null if its creation failed. + */ + @SuppressWarnings("unused") // the thrown Exceptions are not actually thrown + public PrivateKey getDebugKey() throws KeyStoreException, NoSuchAlgorithmException, + UnrecoverableKeyException, UnrecoverableEntryException { + if (mEntry != null) { + return mEntry.getPrivateKey(); + } + + return null; + } + + /** + * Returns the debug {@link Certificate} to use to sign applications for debug purpose. + * @return the certificate or null if its creation failed. + */ + @SuppressWarnings("unused") // the thrown Exceptions are not actually thrown + public Certificate getCertificate() throws KeyStoreException, NoSuchAlgorithmException, + UnrecoverableKeyException, UnrecoverableEntryException { + if (mEntry != null) { + return mEntry.getCertificate(); + } + + return null; + } + + /** + * Loads the debug key from the keystore. + * @param osKeyStorePath the OS path to the keystore. + * @param storeType an optional keystore type, or null if the default is to + * be used. + * @return true if success, false if the keystore does not exist. + */ + private boolean loadKeyEntry(String osKeyStorePath, String storeType) throws KeyStoreException, + NoSuchAlgorithmException, CertificateException, IOException, + UnrecoverableEntryException { + FileInputStream fis = null; + try { + KeyStore keyStore = KeyStore.getInstance( + storeType != null ? storeType : KeyStore.getDefaultType()); + fis = new FileInputStream(osKeyStorePath); + keyStore.load(fis, PASSWORD_CHAR); + mEntry = (KeyStore.PrivateKeyEntry)keyStore.getEntry( + DEBUG_ALIAS, new KeyStore.PasswordProtection(PASSWORD_CHAR)); + } catch (FileNotFoundException e) { + return false; + } finally { + if (fis != null) { + try { + fis.close(); + } catch (IOException e) { + // pass + } + } + } + + return true; + } + + /** + * Creates a new store + * @param osKeyStorePath the location of the store + * @param storeType an optional keystore type, or null if the default is to + * be used. + * @param output an optional {@link IKeyGenOutput} object to get the stdout and stderr + * of the keytool process call. + * @throws KeyStoreException + * @throws NoSuchAlgorithmException + * @throws CertificateException + * @throws UnrecoverableEntryException + * @throws IOException + * @throws KeytoolException + */ + private void createNewStore(String osKeyStorePath, String storeType, IKeyGenOutput output) + throws KeyStoreException, NoSuchAlgorithmException, CertificateException, + UnrecoverableEntryException, IOException, KeytoolException { + + if (KeystoreHelper.createNewStore(osKeyStorePath, storeType, PASSWORD_STRING, DEBUG_ALIAS, + PASSWORD_STRING, CERTIFICATE_DESC, 30 /* validity*/, output)) { + loadKeyEntry(osKeyStorePath, storeType); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/KeystoreHelper.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/KeystoreHelper.java new file mode 100644 index 00000000..6ba4e723 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/KeystoreHelper.java @@ -0,0 +1,173 @@ +/* + * 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.sdklib.internal.build; + +import com.android.annotations.Nullable; +import com.android.sdklib.internal.build.DebugKeyProvider.IKeyGenOutput; +import com.android.sdklib.internal.build.DebugKeyProvider.KeytoolException; +import com.android.utils.GrabProcessOutput; +import com.android.utils.GrabProcessOutput.IProcessOutput; +import com.android.utils.GrabProcessOutput.Wait; + +import java.io.File; +import java.io.IOException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableEntryException; +import java.security.cert.CertificateException; +import java.util.ArrayList; + +/** + * A Helper to create new keystore/key. + * + * @deprecated Use Android-Builder instead + */ +@Deprecated +public final class KeystoreHelper { + + /** + * Creates a new store + * @param osKeyStorePath the location of the store + * @param storeType an optional keystore type, or null if the default is to + * be used. + * @param output an optional {@link IKeyGenOutput} object to get the stdout and stderr + * of the keytool process call. + * @throws KeyStoreException + * @throws NoSuchAlgorithmException + * @throws CertificateException + * @throws UnrecoverableEntryException + * @throws IOException + * @throws KeytoolException + */ + public static boolean createNewStore( + String osKeyStorePath, + String storeType, + String storePassword, + String alias, + String keyPassword, + String description, + int validityYears, + final IKeyGenOutput output) + throws KeyStoreException, NoSuchAlgorithmException, CertificateException, + UnrecoverableEntryException, IOException, KeytoolException { + + // get the executable name of keytool depending on the platform. + String os = System.getProperty("os.name"); + + String keytoolCommand; + if (os.startsWith("Windows")) { + keytoolCommand = "keytool.exe"; + } else { + keytoolCommand = "keytool"; + } + + String javaHome = System.getProperty("java.home"); + + if (javaHome != null && !javaHome.isEmpty()) { + keytoolCommand = javaHome + File.separator + "bin" + File.separator + keytoolCommand; + } + + // create the command line to call key tool to build the key with no user input. + ArrayList commandList = new ArrayList(); + commandList.add(keytoolCommand); + commandList.add("-genkey"); + commandList.add("-alias"); + commandList.add(alias); + commandList.add("-keyalg"); + commandList.add("RSA"); + commandList.add("-dname"); + commandList.add(description); + commandList.add("-validity"); + commandList.add(Integer.toString(validityYears * 365)); + commandList.add("-keypass"); + commandList.add(keyPassword); + commandList.add("-keystore"); + commandList.add(osKeyStorePath); + commandList.add("-storepass"); + commandList.add(storePassword); + if (storeType != null) { + commandList.add("-storetype"); + commandList.add(storeType); + } + + String[] commandArray = commandList.toArray(new String[commandList.size()]); + + // launch the command line process + int result = 0; + try { + Process process = Runtime.getRuntime().exec(commandArray); + result = GrabProcessOutput.grabProcessOutput( + process, + Wait.WAIT_FOR_READERS, + new IProcessOutput() { + @Override + public void out(@Nullable String line) { + if (line != null) { + if (output != null) { + output.out(line); + } else { + System.out.println(line); + } + } + } + + @Override + public void err(@Nullable String line) { + if (line != null) { + if (output != null) { + output.err(line); + } else { + System.err.println(line); + } + } + } + }); + } catch (Exception e) { + // create the command line as one string for debugging purposes + StringBuilder builder = new StringBuilder(); + boolean firstArg = true; + for (String arg : commandArray) { + boolean hasSpace = arg.indexOf(' ') != -1; + + if (firstArg == true) { + firstArg = false; + } else { + builder.append(' '); + } + + if (hasSpace) { + builder.append('"'); + } + + builder.append(arg); + + if (hasSpace) { + builder.append('"'); + } + } + + throw new KeytoolException("Failed to create key: " + e.getMessage(), + javaHome, builder.toString()); + } + + if (result != 0) { + return false; + } + + return true; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/SignedJarBuilder.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/SignedJarBuilder.java new file mode 100644 index 00000000..afb7e658 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/SignedJarBuilder.java @@ -0,0 +1,399 @@ +/* + * 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.sdklib.internal.build; + +import com.android.SdkConstants; +import com.android.sdklib.internal.build.SignedJarBuilder.IZipEntryFilter.ZipAbortException; + +import sun.misc.BASE64Encoder; +import sun.security.pkcs.ContentInfo; +import sun.security.pkcs.PKCS7; +import sun.security.pkcs.SignerInfo; +import sun.security.x509.AlgorithmId; +import sun.security.x509.X500Name; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.security.DigestOutputStream; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.X509Certificate; +import java.util.Map; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +/** + * A Jar file builder with signature support. + * + * @deprecated Use Android-Builder instead + */ +@Deprecated +public class SignedJarBuilder { + private static final String DIGEST_ALGORITHM = "SHA1"; + private static final String DIGEST_ATTR = "SHA1-Digest"; + private static final String DIGEST_MANIFEST_ATTR = "SHA1-Digest-Manifest"; + + /** Write to another stream and also feed it to the Signature object. */ + private static class SignatureOutputStream extends FilterOutputStream { + private Signature mSignature; + private int mCount = 0; + + public SignatureOutputStream(OutputStream out, Signature sig) { + super(out); + mSignature = sig; + } + + @Override + public void write(int b) throws IOException { + try { + mSignature.update((byte) b); + } catch (SignatureException e) { + throw new IOException("SignatureException: " + e); + } + super.write(b); + mCount++; + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + try { + mSignature.update(b, off, len); + } catch (SignatureException e) { + throw new IOException("SignatureException: " + e); + } + super.write(b, off, len); + mCount += len; + } + + public int size() { + return mCount; + } + } + + private JarOutputStream mOutputJar; + private PrivateKey mKey; + private X509Certificate mCertificate; + private Manifest mManifest; + private BASE64Encoder mBase64Encoder; + private MessageDigest mMessageDigest; + + private byte[] mBuffer = new byte[4096]; + + /** + * Classes which implement this interface provides a method to check whether a file should + * be added to a Jar file. + */ + public interface IZipEntryFilter { + + /** + * An exception thrown during packaging of a zip file into APK file. + * This is typically thrown by implementations of + * {@link IZipEntryFilter#checkEntry(String)}. + */ + class ZipAbortException extends Exception { + private static final long serialVersionUID = 1L; + + public ZipAbortException() { + super(); + } + + public ZipAbortException(String format, Object... args) { + super(String.format(format, args)); + } + + public ZipAbortException(Throwable cause, String format, Object... args) { + super(String.format(format, args), cause); + } + + public ZipAbortException(Throwable cause) { + super(cause); + } + } + + + /** + * Checks a file for inclusion in a Jar archive. + * @param archivePath the archive file path of the entry + * @return true if the file should be included. + * @throws ZipAbortException if writing the file should be aborted. + */ + boolean checkEntry(String archivePath) throws ZipAbortException; + } + + /** + * Creates a {@link SignedJarBuilder} with a given output stream, and signing information. + *

If either key or certificate is null then + * the archive will not be signed. + * @param out the {@link OutputStream} where to write the Jar archive. + * @param key the {@link PrivateKey} used to sign the archive, or null. + * @param certificate the {@link X509Certificate} used to sign the archive, or + * null. + * @throws IOException + * @throws NoSuchAlgorithmException + */ + public SignedJarBuilder(OutputStream out, PrivateKey key, X509Certificate certificate) + throws IOException, NoSuchAlgorithmException { + mOutputJar = new JarOutputStream(new BufferedOutputStream(out)); + mOutputJar.setLevel(9); + mKey = key; + mCertificate = certificate; + + if (mKey != null && mCertificate != null) { + mManifest = new Manifest(); + Attributes main = mManifest.getMainAttributes(); + main.putValue("Manifest-Version", "1.0"); + main.putValue("Created-By", "1.0 (Android)"); + + mBase64Encoder = new BASE64Encoder(); + mMessageDigest = MessageDigest.getInstance(DIGEST_ALGORITHM); + } + } + + /** + * Writes a new {@link File} into the archive. + * @param inputFile the {@link File} to write. + * @param jarPath the filepath inside the archive. + * @throws IOException + */ + public void writeFile(File inputFile, String jarPath) throws IOException { + // Get an input stream on the file. + FileInputStream fis = new FileInputStream(inputFile); + try { + + // create the zip entry + JarEntry entry = new JarEntry(jarPath); + entry.setTime(inputFile.lastModified()); + + writeEntry(fis, entry); + } finally { + // close the file stream used to read the file + fis.close(); + } + } + + /** + * Copies the content of a Jar/Zip archive into the receiver archive. + *

An optional {@link IZipEntryFilter} allows to selectively choose which files + * to copy over. + * @param input the {@link InputStream} for the Jar/Zip to copy. + * @param filter the filter or null + * @throws IOException + * @throws ZipAbortException if the {@link IZipEntryFilter} filter indicated that the write + * must be aborted. + */ + public void writeZip(InputStream input, IZipEntryFilter filter) + throws IOException, ZipAbortException { + ZipInputStream zis = new ZipInputStream(input); + + try { + // loop on the entries of the intermediary package and put them in the final package. + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + String name = entry.getName(); + + // do not take directories or anything inside a potential META-INF folder. + if (entry.isDirectory() || name.startsWith("META-INF/")) { + continue; + } + + // if we have a filter, we check the entry against it + if (filter != null && filter.checkEntry(name) == false) { + continue; + } + + JarEntry newEntry; + + // Preserve the STORED method of the input entry. + if (entry.getMethod() == JarEntry.STORED) { + newEntry = new JarEntry(entry); + } else { + // Create a new entry so that the compressed len is recomputed. + newEntry = new JarEntry(name); + } + + writeEntry(zis, newEntry); + + zis.closeEntry(); + } + } finally { + zis.close(); + } + } + + /** + * Closes the Jar archive by creating the manifest, and signing the archive. + * @throws IOException + * @throws GeneralSecurityException + */ + public void close() throws IOException, GeneralSecurityException { + if (mManifest != null) { + // write the manifest to the jar file + mOutputJar.putNextEntry(new JarEntry(JarFile.MANIFEST_NAME)); + mManifest.write(mOutputJar); + + // CERT.SF + Signature signature = Signature.getInstance("SHA1with" + mKey.getAlgorithm()); + signature.initSign(mKey); + mOutputJar.putNextEntry(new JarEntry("META-INF/CERT.SF")); + SignatureOutputStream out = new SignatureOutputStream(mOutputJar, signature); + writeSignatureFile(out); + + // CERT.* + mOutputJar.putNextEntry(new JarEntry("META-INF/CERT." + mKey.getAlgorithm())); + writeSignatureBlock(signature, mCertificate, mKey); + + // close out at the end because it can also close mOutputJar. + // (there's some timing issue here I think, because it's worked before with out + // being closed after writing CERT.SF). + out.close(); + } + + mOutputJar.close(); + mOutputJar = null; + } + + /** + * Clean up of the builder for interrupted workflow. + * This does nothing if {@link #close()} was called successfully. + */ + public void cleanUp() { + if (mOutputJar != null) { + try { + mOutputJar.close(); + } catch (IOException e) { + // pass + } + } + } + + /** + * Adds an entry to the output jar, and write its content from the {@link InputStream} + * @param input The input stream from where to write the entry content. + * @param entry the entry to write in the jar. + * @throws IOException + */ + private void writeEntry(InputStream input, JarEntry entry) throws IOException { + // add the entry to the jar archive + mOutputJar.putNextEntry(entry); + + // read the content of the entry from the input stream, and write it into the archive. + int count; + while ((count = input.read(mBuffer)) != -1) { + mOutputJar.write(mBuffer, 0, count); + + // update the digest + if (mMessageDigest != null) { + mMessageDigest.update(mBuffer, 0, count); + } + } + + // close the entry for this file + mOutputJar.closeEntry(); + + if (mManifest != null) { + // update the manifest for this entry. + Attributes attr = mManifest.getAttributes(entry.getName()); + if (attr == null) { + attr = new Attributes(); + mManifest.getEntries().put(entry.getName(), attr); + } + attr.putValue(DIGEST_ATTR, mBase64Encoder.encode(mMessageDigest.digest())); + } + } + + /** Writes a .SF file with a digest to the manifest. */ + private void writeSignatureFile(SignatureOutputStream out) + throws IOException, GeneralSecurityException { + Manifest sf = new Manifest(); + Attributes main = sf.getMainAttributes(); + main.putValue("Signature-Version", "1.0"); + main.putValue("Created-By", "1.0 (Android)"); + + BASE64Encoder base64 = new BASE64Encoder(); + MessageDigest md = MessageDigest.getInstance(DIGEST_ALGORITHM); + PrintStream print = new PrintStream( + new DigestOutputStream(new ByteArrayOutputStream(), md), + true, SdkConstants.UTF_8); + + // Digest of the entire manifest + mManifest.write(print); + print.flush(); + main.putValue(DIGEST_MANIFEST_ATTR, base64.encode(md.digest())); + + Map entries = mManifest.getEntries(); + for (Map.Entry entry : entries.entrySet()) { + // Digest of the manifest stanza for this entry. + print.print("Name: " + entry.getKey() + "\r\n"); + for (Map.Entry att : entry.getValue().entrySet()) { + print.print(att.getKey() + ": " + att.getValue() + "\r\n"); + } + print.print("\r\n"); + print.flush(); + + Attributes sfAttr = new Attributes(); + sfAttr.putValue(DIGEST_ATTR, base64.encode(md.digest())); + sf.getEntries().put(entry.getKey(), sfAttr); + } + + sf.write(out); + + // A bug in the java.util.jar implementation of Android platforms + // up to version 1.6 will cause a spurious IOException to be thrown + // if the length of the signature file is a multiple of 1024 bytes. + // As a workaround, add an extra CRLF in this case. + if ((out.size() % 1024) == 0) { + out.write('\r'); + out.write('\n'); + } + } + + /** Write the certificate file with a digital signature. */ + private void writeSignatureBlock(Signature signature, X509Certificate publicKey, + PrivateKey privateKey) + throws IOException, GeneralSecurityException { + SignerInfo signerInfo = new SignerInfo( + new X500Name(publicKey.getIssuerX500Principal().getName()), + publicKey.getSerialNumber(), + AlgorithmId.get(DIGEST_ALGORITHM), + AlgorithmId.get(privateKey.getAlgorithm()), + signature.sign()); + + PKCS7 pkcs7 = new PKCS7( + new AlgorithmId[] { AlgorithmId.get(DIGEST_ALGORITHM) }, + new ContentInfo(ContentInfo.DATA_OID, null), + new X509Certificate[] { publicKey }, + new SignerInfo[] { signerInfo }); + + pkcs7.encodeSignedData(mOutputJar); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/SymbolLoader.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/SymbolLoader.java new file mode 100644 index 00000000..cbb41920 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/SymbolLoader.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2012 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.sdklib.internal.build; + +import com.google.common.base.Charsets; +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.Table; +import com.google.common.io.Files; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +/** + * A class to load the text symbol file generated by aapt with the + * --output-text-symbols option. + * + * @deprecated Use Android-Builder instead + */ +@Deprecated +public class SymbolLoader { + + private final File mSymbolFile; + private Table mSymbols; + + public static class SymbolEntry { + private final String mName; + private final String mType; + private final String mValue; + + public SymbolEntry(String name, String type, String value) { + mName = name; + mType = type; + mValue = value; + } + + public String getValue() { + return mValue; + } + + public String getName() { + return mName; + } + + public String getType() { + return mType; + } + } + + public SymbolLoader(File symbolFile) { + mSymbolFile = symbolFile; + } + + public void load() throws IOException { + List lines = Files.readLines(mSymbolFile, Charsets.UTF_8); + + mSymbols = HashBasedTable.create(); + + int lineIndex = 1; + String line = null; + try { + final int count = lines.size(); + for (; lineIndex <= count ; lineIndex++) { + line = lines.get(lineIndex-1); + + // format is " " + // don't want to split on space as value could contain spaces. + int pos = line.indexOf(' '); + String type = line.substring(0, pos); + int pos2 = line.indexOf(' ', pos + 1); + String className = line.substring(pos + 1, pos2); + int pos3 = line.indexOf(' ', pos2 + 1); + String name = line.substring(pos2 + 1, pos3); + String value = line.substring(pos3 + 1); + + mSymbols.put(className, name, new SymbolEntry(name, type, value)); + } + } catch (Exception e) { + String s = String.format("File format error reading %s\tline %d: '%s'", + mSymbolFile.getAbsolutePath(), lineIndex, line); + throw new IOException(s, e); + } + } + + Table getSymbols() { + return mSymbols; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/SymbolWriter.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/SymbolWriter.java new file mode 100644 index 00000000..3dfaa6fe --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/SymbolWriter.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2012 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.sdklib.internal.build; + +import com.android.SdkConstants; +import com.android.sdklib.internal.build.SymbolLoader.SymbolEntry; +import com.google.common.base.Charsets; +import com.google.common.base.Splitter; +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.Lists; +import com.google.common.collect.Table; +import com.google.common.io.Closer; +import com.google.common.io.Files; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * A class to write R.java classes based on data read from text symbol files generated by + * aapt with the --output-text-symbols option. + * + * @deprecated Use Android-Builder instead + */ +@Deprecated +public class SymbolWriter { + + private final String mOutFolder; + private final String mPackageName; + private final List mSymbols = Lists.newArrayList(); + private final SymbolLoader mValues; + + public SymbolWriter(String outFolder, String packageName, SymbolLoader values) { + mOutFolder = outFolder; + mPackageName = packageName; + mValues = values; + } + + public void addSymbolsToWrite(SymbolLoader symbols) { + mSymbols.add(symbols); + } + + private Table getAllSymbols() { + Table symbols = HashBasedTable.create(); + + for (SymbolLoader symbolLoader : mSymbols) { + symbols.putAll(symbolLoader.getSymbols()); + } + + return symbols; + } + + public void write() throws IOException { + Splitter splitter = Splitter.on('.'); + Iterable folders = splitter.split(mPackageName); + File file = new File(mOutFolder); + for (String folder : folders) { + file = new File(file, folder); + } + file.mkdirs(); + file = new File(file, SdkConstants.FN_RESOURCE_CLASS); + + Closer closer = Closer.create(); + try { + BufferedWriter writer = closer.register(Files.newWriter(file, Charsets.UTF_8)); + + writer.write("/* AUTO-GENERATED FILE. DO NOT MODIFY.\n"); + writer.write(" *\n"); + writer.write(" * This class was automatically generated by the\n"); + writer.write(" * aapt tool from the resource data it found. It\n"); + writer.write(" * should not be modified by hand.\n"); + writer.write(" */\n"); + + writer.write("package "); + writer.write(mPackageName); + writer.write(";\n\npublic final class R {\n"); + + Table symbols = getAllSymbols(); + Table values = mValues.getSymbols(); + + Set rowSet = symbols.rowKeySet(); + List rowList = Lists.newArrayList(rowSet); + Collections.sort(rowList); + + for (String row : rowList) { + writer.write("\tpublic static final class "); + writer.write(row); + writer.write(" {\n"); + + Map rowMap = symbols.row(row); + Set symbolSet = rowMap.keySet(); + ArrayList symbolList = Lists.newArrayList(symbolSet); + Collections.sort(symbolList); + + for (String symbolName : symbolList) { + // get the matching SymbolEntry from the values Table. + SymbolEntry value = values.get(row, symbolName); + if (value != null) { + writer.write("\t\tpublic static final "); + writer.write(value.getType()); + writer.write(" "); + writer.write(value.getName()); + writer.write(" = "); + writer.write(value.getValue()); + writer.write(";\n"); + } + } + + writer.write("\t}\n"); + } + + writer.write("}\n"); + } catch (Throwable e) { + throw closer.rethrow(e); + } finally { + closer.close(); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/project/IPropertySource.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/project/IPropertySource.java new file mode 100644 index 00000000..92bd6b93 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/project/IPropertySource.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2012 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.sdklib.internal.project; + +/** + * A source able to return properties by name. + * + */ +public interface IPropertySource { + String getProperty(String name); + void debugPrint(); +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/project/ProjectCreator.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/project/ProjectCreator.java new file mode 100644 index 00000000..c5712d75 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/project/ProjectCreator.java @@ -0,0 +1,1571 @@ +/* + * 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.sdklib.internal.project; + +import com.android.SdkConstants; +import com.android.io.FileWrapper; +import com.android.io.FolderWrapper; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.SdkManager; +import com.android.sdklib.internal.project.ProjectProperties.PropertyType; +import com.android.utils.ILogger; +import com.android.xml.AndroidManifest; +import com.android.xml.AndroidXPathFactory; + +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +/** + * Creates the basic files needed to get an Android project up and running. + */ +public class ProjectCreator { + + /** Version of the build.xml. Stored in version-tag */ + private static final int MIN_BUILD_VERSION_TAG = 1; + + /** Package path substitution string used in template files, i.e. "PACKAGE_PATH" */ + private static final String PH_PACKAGE_PATH = "PACKAGE_PATH"; + /** Package name substitution string used in template files, i.e. "PACKAGE" */ + private static final String PH_PACKAGE = "PACKAGE"; + /** Activity name substitution string used in template files, i.e. "ACTIVITY_NAME". + * @deprecated This is only used for older templates. For new ones see + * {@link #PH_ACTIVITY_ENTRY_NAME}, and {@link #PH_ACTIVITY_CLASS_NAME}. */ + @Deprecated + private static final String PH_ACTIVITY_NAME = "ACTIVITY_NAME"; + /** Activity name substitution string used in manifest templates, i.e. "ACTIVITY_ENTRY_NAME".*/ + private static final String PH_ACTIVITY_ENTRY_NAME = "ACTIVITY_ENTRY_NAME"; + /** Activity name substitution string used in class templates, i.e. "ACTIVITY_CLASS_NAME".*/ + private static final String PH_ACTIVITY_CLASS_NAME = "ACTIVITY_CLASS_NAME"; + /** Activity FQ-name substitution string used in class templates, i.e. "ACTIVITY_FQ_NAME".*/ + private static final String PH_ACTIVITY_FQ_NAME = "ACTIVITY_FQ_NAME"; + /** Original Activity class name substitution string used in class templates, i.e. + * "ACTIVITY_TESTED_CLASS_NAME".*/ + private static final String PH_ACTIVITY_TESTED_CLASS_NAME = "ACTIVITY_TESTED_CLASS_NAME"; + /** Project name substitution string used in template files, i.e. "PROJECT_NAME". */ + public static final String PH_PROJECT_NAME = "PROJECT_NAME"; + /** Application icon substitution string used in the manifest template */ + private static final String PH_ICON = "ICON"; + /** Version tag name substitution string used in template files, i.e. "VERSION_TAG". */ + private static final String PH_VERSION_TAG = "VERSION_TAG"; + /** Target name substitution string used in template files, i.e. "TARGET". */ + private static final String PH_TARGET = "TARGET"; + /** Gradle plugin substitution string used in the build.gradle template */ + private static final String PH_PLUGIN = "PLUGIN"; + /** Gradle artifact version substitution string used in the build.gradle template */ + private static final String PH_ARTIFACT_VERSION = "ARTIFACT_VERSION"; + /** Build tool revision substitution string used in the build.gradle template */ + private static final String PH_BUILD_TOOL_REV = "BUILD_TOOL_REV"; + + /** The xpath to find a project name in an Ant build file. */ + private static final String XPATH_PROJECT_NAME = "/project/@name"; + + /** Pattern for characters accepted in a project name. Since this will be used as a + * directory name, we're being a bit conservative on purpose: dot and space cannot be used. */ + public static final Pattern RE_PROJECT_NAME = Pattern.compile("[a-zA-Z0-9_]+"); + /** List of valid characters for a project name. Used for display purposes. */ + public static final String CHARS_PROJECT_NAME = "a-z A-Z 0-9 _"; + + /** Pattern for characters accepted in a package name. A package is list of Java identifier + * separated by a dot. We need to have at least one dot (e.g. a two-level package name). + * A Java identifier cannot start by a digit. */ + public static final Pattern RE_PACKAGE_NAME = + Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)+"); + /** List of valid characters for a project name. Used for display purposes. */ + public static final String CHARS_PACKAGE_NAME = "a-z A-Z 0-9 _"; + + /** Pattern for characters accepted in an activity name, which is a Java identifier. */ + public static final Pattern RE_ACTIVITY_NAME = + Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*"); + /** List of valid characters for a project name. Used for display purposes. */ + public static final String CHARS_ACTIVITY_NAME = "a-z A-Z 0-9 _"; + + /** Gradle plugin to use with standard projects */ + private static final String PLUGIN_PROJECT = "android"; + /** Gradle plugin to use with library projects */ + private static final String PLUGIN_LIB_PROJECT = "android-library"; + + + public enum OutputLevel { + /** Silent mode. Project creation will only display errors. */ + SILENT, + /** Normal mode. Project creation will display what's being done, display + * error but not warnings. */ + NORMAL, + /** Verbose mode. Project creation will display what's being done, errors and warnings. */ + VERBOSE + } + + /** + * Exception thrown when a project creation fails, typically because a template + * file cannot be written. + */ + private static class ProjectCreateException extends Exception { + /** default UID. This will not be serialized anyway. */ + private static final long serialVersionUID = 1L; + + @SuppressWarnings("unused") + ProjectCreateException(String message) { + super(message); + } + + ProjectCreateException(Throwable t, String format, Object... args) { + super(format != null ? String.format(format, args) : format, t); + } + + ProjectCreateException(String format, Object... args) { + super(String.format(format, args)); + } + } + + /** The {@link OutputLevel} verbosity. */ + private final OutputLevel mLevel; + /** Logger for errors and output. Cannot be null. */ + private final ILogger mLog; + /** The OS path of the SDK folder. */ + private final String mSdkFolder; + /** The {@link SdkManager} instance. */ + private final SdkManager mSdkManager; + + /** + * Helper class to create android projects. + * + * @param sdkManager The {@link SdkManager} instance. + * @param sdkFolder The OS path of the SDK folder. + * @param level The {@link OutputLevel} verbosity. + * @param log Logger for errors and output. Cannot be null. + */ + public ProjectCreator(SdkManager sdkManager, String sdkFolder, OutputLevel level, ILogger log) { + mSdkManager = sdkManager; + mSdkFolder = sdkFolder; + mLevel = level; + mLog = log; + } + + /** + * Creates a new (ant) project. + *

+ * The caller should have already checked and sanitized the parameters. + * + * @param folderPath the folder of the project to create. + * @param projectName the name of the project. The name must match the + * {@link #RE_PROJECT_NAME} regex. + * @param packageName the package of the project. The name must match the + * {@link #RE_PACKAGE_NAME} regex. + * @param activityEntry the activity of the project as it will appear in the manifest. Can be + * null if no activity should be created. The name must match the + * {@link #RE_ACTIVITY_NAME} regex. + * @param target the project target. + * @param library whether the project is a library. + * @param pathToMainProject if non-null the project will be setup to test a main project + * located at the given path. + */ + public void createProject(String folderPath, String projectName, + String packageName, String activityEntry, IAndroidTarget target, boolean library, + String pathToMainProject) { + + // create project folder if it does not exist + File projectFolder = checkNewProjectLocation(folderPath); + if (projectFolder == null) { + return; + } + + try { + boolean isTestProject = pathToMainProject != null; + + // first create the project properties. + + // location of the SDK goes in localProperty + ProjectPropertiesWorkingCopy localProperties = ProjectProperties.create(folderPath, + PropertyType.LOCAL); + localProperties.setProperty(ProjectProperties.PROPERTY_SDK, mSdkFolder); + localProperties.save(); + + // target goes in project properties + ProjectPropertiesWorkingCopy projectProperties = ProjectProperties.create(folderPath, + PropertyType.PROJECT); + projectProperties.setProperty(ProjectProperties.PROPERTY_TARGET, target.hashString()); + if (library) { + projectProperties.setProperty(ProjectProperties.PROPERTY_LIBRARY, "true"); + } + projectProperties.save(); + + // create a ant.properties file with just the application package + ProjectPropertiesWorkingCopy antProperties = ProjectProperties.create(folderPath, + PropertyType.ANT); + + if (isTestProject) { + antProperties.setProperty(ProjectProperties.PROPERTY_TESTED_PROJECT, + pathToMainProject); + } + + antProperties.save(); + + // create the map for place-holders of values to replace in the templates + final HashMap keywords = new HashMap(); + + // create the required folders. + // compute src folder path + final String packagePath = + stripString(packageName.replace(".", File.separator), + File.separatorChar); + + // put this path in the place-holder map for project files that needs to list + // files manually. + keywords.put(PH_PACKAGE_PATH, packagePath); + keywords.put(PH_PACKAGE, packageName); + keywords.put(PH_VERSION_TAG, Integer.toString(MIN_BUILD_VERSION_TAG)); + + + // compute some activity related information + String fqActivityName = null, activityPath = null, activityClassName = null; + String originalActivityEntry = activityEntry; + String originalActivityClassName = null; + if (activityEntry != null) { + if (isTestProject) { + // append Test so that it doesn't collide with the main project activity. + activityEntry += "Test"; + + // get the classname from the original activity entry. + int pos = originalActivityEntry.lastIndexOf('.'); + if (pos != -1) { + originalActivityClassName = originalActivityEntry.substring(pos + 1); + } else { + originalActivityClassName = originalActivityEntry; + } + } + + // get the fully qualified name of the activity + fqActivityName = AndroidManifest.combinePackageAndClassName(packageName, + activityEntry); + + // get the activity path (replace the . to /) + activityPath = stripString(fqActivityName.replace(".", File.separator), + File.separatorChar); + + // remove the last segment, so that we only have the path to the activity, but + // not the activity filename itself. + activityPath = activityPath.substring(0, + activityPath.lastIndexOf(File.separatorChar)); + + // finally, get the class name for the activity + activityClassName = fqActivityName.substring(fqActivityName.lastIndexOf('.') + 1); + } + + // at this point we have the following for the activity: + // activityEntry: this is the manifest entry. For instance .MyActivity + // fqActivityName: full-qualified class name: com.foo.MyActivity + // activityClassName: only the classname: MyActivity + // originalActivityClassName: the classname of the activity being tested (if applicable) + + // Add whatever activity info is needed in the place-holder map. + // Older templates only expect ACTIVITY_NAME to be the same (and unmodified for tests). + if (target.getVersion().getApiLevel() < 4) { // legacy + if (originalActivityEntry != null) { + keywords.put(PH_ACTIVITY_NAME, originalActivityEntry); + } + } else { + // newer templates make a difference between the manifest entries, classnames, + // as well as the main and test classes. + if (activityEntry != null) { + keywords.put(PH_ACTIVITY_ENTRY_NAME, activityEntry); + keywords.put(PH_ACTIVITY_CLASS_NAME, activityClassName); + keywords.put(PH_ACTIVITY_FQ_NAME, fqActivityName); + if (originalActivityClassName != null) { + keywords.put(PH_ACTIVITY_TESTED_CLASS_NAME, originalActivityClassName); + } + } + } + + // Take the project name from the command line if there's one + if (projectName != null) { + keywords.put(PH_PROJECT_NAME, projectName); + } else { + if (activityClassName != null) { + // Use the activity class name as project name + keywords.put(PH_PROJECT_NAME, activityClassName); + } else { + // We need a project name. Just pick up the basename of the project + // directory. + projectName = projectFolder.getName(); + keywords.put(PH_PROJECT_NAME, projectName); + } + } + + // create the source folder for the activity + if (activityClassName != null) { + String srcActivityFolderPath = + SdkConstants.FD_SOURCES + File.separator + activityPath; + File sourceFolder = createDirs(projectFolder, srcActivityFolderPath); + + String javaTemplate = isTestProject ? "java_tests_file.template" + : "java_file.template"; + String activityFileName = activityClassName + ".java"; + + installTargetTemplate(javaTemplate, new File(sourceFolder, activityFileName), + keywords, target); + } else { + // we should at least create 'src' + createDirs(projectFolder, SdkConstants.FD_SOURCES); + } + + // create other useful folders + File resourceFolder = createDirs(projectFolder, SdkConstants.FD_RESOURCES); + createDirs(projectFolder, SdkConstants.FD_OUTPUT); + createDirs(projectFolder, SdkConstants.FD_NATIVE_LIBS); + + if (isTestProject == false) { + /* Make res files only for non test projects */ + File valueFolder = createDirs(resourceFolder, SdkConstants.FD_RES_VALUES); + installTargetTemplate("strings.template", new File(valueFolder, "strings.xml"), + keywords, target); + + File layoutFolder = createDirs(resourceFolder, SdkConstants.FD_RES_LAYOUT); + installTargetTemplate("layout.template", new File(layoutFolder, "main.xml"), + keywords, target); + + // create the icons + if (installIcons(resourceFolder, target)) { + keywords.put(PH_ICON, "android:icon=\"@drawable/ic_launcher\""); + } else { + keywords.put(PH_ICON, ""); + } + } + + /* Make AndroidManifest.xml and build.xml files */ + String manifestTemplate = "AndroidManifest.template"; + if (isTestProject) { + manifestTemplate = "AndroidManifest.tests.template"; + } + + installTargetTemplate(manifestTemplate, + new File(projectFolder, SdkConstants.FN_ANDROID_MANIFEST_XML), + keywords, target); + + installTemplate("build.template", + new File(projectFolder, SdkConstants.FN_BUILD_XML), + keywords); + + // install the proguard config file. + installTemplate(SdkConstants.FN_PROJECT_PROGUARD_FILE, + new File(projectFolder, SdkConstants.FN_PROJECT_PROGUARD_FILE), + null /*keywords*/); + } catch (Exception e) { + mLog.error(e, null); + } + } + + /** + * Creates a new (gradle) project. + *

+ * The caller should have already checked and sanitized the parameters. + * + * @param folderPath the folder of the project to create. + * @param projectName the name of the project. The name must match the + * {@link #RE_PROJECT_NAME} regex. + * @param packageName the package of the project. The name must match the + * {@link #RE_PACKAGE_NAME} regex. + * @param activityEntry the activity of the project as it will appear in the manifest. Can be + * null if no activity should be created. The name must match the + * {@link #RE_ACTIVITY_NAME} regex. + * @param target the project target. + * @param library whether the project is a library. + * @param artifactVersion the version of the gradle artifact in maven. + */ + public void createGradleProject(String folderPath, String projectName, + String packageName, String activityEntry, IAndroidTarget target, boolean library, + String artifactVersion) { + + // create project folder if it does not exist + File projectFolder = checkNewProjectLocation(folderPath); + if (projectFolder == null) { + return; + } + + try { + // first create the project properties. + + // location of the SDK goes in localProperty + ProjectPropertiesWorkingCopy localProperties = ProjectProperties.create(folderPath, + PropertyType.LOCAL); + localProperties.setProperty(ProjectProperties.PROPERTY_SDK, mSdkFolder); + localProperties.save(); + + // create the map for place-holders of values to replace in the templates + final HashMap keywords = new HashMap(); + final HashMap testKeywords = new HashMap(); + + // create the required folders. + // compute src folder path + final String packagePath = + stripString(packageName.replace(".", File.separator), + File.separatorChar); + + // put this path in the place-holder map for project files that needs to list + // files manually. + keywords.put(PH_PACKAGE_PATH, packagePath); + keywords.put(PH_PACKAGE, packageName); + + testKeywords.put(PH_PACKAGE_PATH, packagePath); + testKeywords.put(PH_PACKAGE, packageName); + + // compute some activity related information + String activityPath = null, activityClassName = null; + String testActivityPath = null, testActivityClassName = null; + if (activityEntry != null) { + // get the fully qualified name of the activity + String fqActivityName = AndroidManifest.combinePackageAndClassName(packageName, + activityEntry); + + // get the activity path (replace the . to /) + activityPath = stripString(fqActivityName.replace(".", File.separator), + File.separatorChar); + + // remove the last segment, so that we only have the path to the activity, but + // not the activity filename itself. + activityPath = activityPath.substring(0, + activityPath.lastIndexOf(File.separatorChar)); + + // finally, get the class name for the activity + activityClassName = fqActivityName.substring(fqActivityName.lastIndexOf('.') + 1); + + // at this point we have the following for the activity: + // activityEntry: this is the manifest entry. For instance .MyActivity + // fqActivityName: full-qualified class name: com.foo.MyActivity + // activityClassName: only the classname: MyActivity + + // append Test so that it doesn't collide with the main project activity. + String testActivityEntry = activityEntry + "Test"; + + // get the fully qualified name of the test + String testFqActivityName = AndroidManifest.combinePackageAndClassName(packageName, + testActivityEntry); + + // get the test path (replace the . to /) + testActivityPath = stripString(testFqActivityName.replace(".", File.separator), + File.separatorChar); + + // remove the last segment, so that we only have the path to the test, but + // not the test filename itself. + testActivityPath = testActivityPath.substring(0, + testActivityPath.lastIndexOf(File.separatorChar)); + + // finally, get the class name for the test + testActivityClassName = testFqActivityName.substring(testFqActivityName.lastIndexOf('.') + 1); + + // at this point we have the following for the test: + // testActivityEntry: this is the manifest entry. For instance .MyActivityTest + // testFqActivityName: full-qualified class name: com.foo.MyActivityTest + // testActivityClassName: only the classname: MyActivityTest + + // Add whatever activity info is needed in the place-holder map. + // Older templates only expect ACTIVITY_NAME to be the same (and unmodified for tests). + if (target.getVersion().getApiLevel() < 4) { // legacy + keywords.put(PH_ACTIVITY_NAME, activityEntry); + testKeywords.put(PH_ACTIVITY_NAME, activityEntry); + } else { + // newer templates make a difference between the manifest entries, classnames, + // as well as the main and test classes. + keywords.put(PH_ACTIVITY_ENTRY_NAME, activityEntry); + keywords.put(PH_ACTIVITY_CLASS_NAME, activityClassName); + keywords.put(PH_ACTIVITY_FQ_NAME, fqActivityName); + + testKeywords.put(PH_ACTIVITY_ENTRY_NAME, testActivityEntry); + testKeywords.put(PH_ACTIVITY_CLASS_NAME, testActivityClassName); + testKeywords.put(PH_ACTIVITY_FQ_NAME, testFqActivityName); + testKeywords.put(PH_ACTIVITY_TESTED_CLASS_NAME, activityClassName); + } + } + + // Take the project name from the command line if there's one + if (projectName != null) { + keywords.put(PH_PROJECT_NAME, projectName); + testKeywords.put(PH_PROJECT_NAME, projectName); + } else { + // Use the activity class name as project name, else just + // pick up the basename of the project directory. + keywords.put(PH_PROJECT_NAME, (activityClassName != null) ? + activityClassName : projectFolder.getName()); + testKeywords.put(PH_PROJECT_NAME, (testActivityClassName != null) ? + testActivityClassName : projectFolder.getName()); + } + + String srcMainPath = SdkConstants.FD_SOURCES + File.separator + + SdkConstants.FD_MAIN; + String srcTestPath = SdkConstants.FD_SOURCES + File.separator + + SdkConstants.FD_TEST; + + // create the source folders for the activity + String srcMainCodePath = srcMainPath + File.separator + SdkConstants.FD_JAVA; + createDirs(projectFolder, srcMainCodePath); + if (activityClassName != null) { + String srcActivityFolderPath = + srcMainCodePath + File.separator + activityPath; + File sourceFolder = createDirs(projectFolder, srcActivityFolderPath); + + String activityFileName = activityClassName + ".java"; + + installTargetTemplate("java_file.template", + new File(sourceFolder, activityFileName), keywords, target); + } + + // create the source folders for the test + String srcTestCodePath = srcTestPath + File.separator + SdkConstants.FD_JAVA; + createDirs(projectFolder, srcTestCodePath); + if (testActivityClassName != null) { + String srcActivityFolderPath = + srcTestCodePath + File.separator + testActivityPath; + File sourceFolder = createDirs(projectFolder, srcActivityFolderPath); + + String activityFileName = testActivityClassName + ".java"; + + installTargetTemplate("java_tests_file.template", + new File(sourceFolder, activityFileName), testKeywords, target); + } + + // create the res xml files + String srcMainResPath = srcMainPath + File.separator + SdkConstants.FD_RES; + File resourceFolder = createDirs(projectFolder, srcMainResPath); + + File valueFolder = createDirs(resourceFolder, SdkConstants.FD_RES_VALUES); + installTargetTemplate("strings.template", new File(valueFolder, "strings.xml"), + keywords, target); + + File layoutFolder = createDirs(resourceFolder, SdkConstants.FD_RES_LAYOUT); + installTargetTemplate("layout.template", new File(layoutFolder, "main.xml"), + keywords, target); + + // create the icons + if (installIcons(resourceFolder, target)) { + keywords.put(PH_ICON, "android:icon=\"@drawable/ic_launcher\""); + } else { + keywords.put(PH_ICON, ""); + } + + // Create the AndroidManifest.xml and build.gradle files + installTargetTemplate("AndroidManifest.template", + new File(projectFolder, srcMainPath + File.separator + + SdkConstants.FN_ANDROID_MANIFEST_XML), + keywords, target); + + String buildToolRev = mSdkManager.getLatestBuildTool().getRevision().toString(); + + keywords.put(PH_BUILD_TOOL_REV, buildToolRev); + keywords.put(PH_ARTIFACT_VERSION, artifactVersion); + keywords.put(PH_TARGET, target.hashString()); + keywords.put(PH_PLUGIN, (library) ? PLUGIN_LIB_PROJECT : PLUGIN_PROJECT); + + installTemplate("build_gradle.template", + new File(projectFolder, SdkConstants.FN_BUILD_GRADLE), + keywords); + + // Create the gradle wrapper files + createDirs(projectFolder, SdkConstants.FD_GRADLE_WRAPPER); + installGradleWrapperFile(SdkConstants.FD_GRADLE_WRAPPER + File.separator + + SdkConstants.FN_GRADLE_WRAPPER_JAR, + projectFolder); + installGradleWrapperFile(SdkConstants.FD_GRADLE_WRAPPER + File.separator + + SdkConstants.FN_GRADLE_WRAPPER_PROPERTIES, + projectFolder); + installGradleWrapperFile(SdkConstants.FN_GRADLE_WRAPPER_WIN, projectFolder); + installGradleWrapperFile(SdkConstants.FN_GRADLE_WRAPPER_UNIX, projectFolder); + new File(projectFolder, SdkConstants.FN_GRADLE_WRAPPER_UNIX).setExecutable(true, false); + } catch (Exception e) { + mLog.error(e, null); + } + } + + private File checkNewProjectLocation(String folderPath) { + File projectFolder = new File(folderPath); + if (!projectFolder.exists()) { + + boolean created = false; + Throwable t = null; + try { + created = projectFolder.mkdirs(); + } catch (Exception e) { + t = e; + } + + if (created) { + println("Created project directory: %1$s", projectFolder); + } else { + mLog.error(t, "Could not create directory: %1$s", projectFolder); + return null; + } + } else { + Exception e = null; + String error = null; + try { + String[] content = projectFolder.list(); + if (content == null) { + error = "Project folder '%1$s' is not a directory."; + } else if (content.length != 0) { + error = "Project folder '%1$s' is not empty. Please consider using '%2$s update' instead."; + } + } catch (Exception e1) { + e = e1; + } + + if (e != null || error != null) { + mLog.error(e, error, projectFolder, SdkConstants.androidCmdName()); + } + } + return projectFolder; + } + + /** + * Updates an existing project. + *

+ * Workflow: + *

    + *
  • Check AndroidManifest.xml is present (required) + *
  • Check if there's a legacy properties file and convert it + *
  • Check there's a project.properties with a target *or* --target was specified + *
  • Update default.prop if --target was specified + *
  • Refresh/create "sdk" in local.properties + *
  • Build.xml: create if not present or if version-tag is found or not. version-tag:custom + * prevent any overwrite. version-tag:[integer] will override. missing version-tag will query + * the dev. + *
+ * + * @param folderPath the folder of the project to update. This folder must exist. + * @param target the project target. Can be null. + * @param projectName The project name from --name. Can be null. + * @param libraryPath the path to a library to add to the references. Can be null. + * @return true if the project was successfully updated. + */ + @SuppressWarnings("deprecation") + public boolean updateProject(String folderPath, IAndroidTarget target, String projectName, + String libraryPath) { + // since this is an update, check the folder does point to a project + FileWrapper androidManifest = checkProjectFolder(folderPath, + SdkConstants.FN_ANDROID_MANIFEST_XML); + if (androidManifest == null) { + return false; + } + + // get the parent folder. + FolderWrapper projectFolder = (FolderWrapper) androidManifest.getParentFolder(); + + boolean hasProguard = false; + + // Check there's a project.properties with a target *or* --target was specified + IAndroidTarget originalTarget = null; + boolean writeProjectProp = false; + ProjectProperties props = ProjectProperties.load(projectFolder, PropertyType.PROJECT); + + if (props == null) { + // no project.properties, try to load default.properties + props = ProjectProperties.load(projectFolder, PropertyType.LEGACY_DEFAULT); + writeProjectProp = true; + } + + if (props != null) { + String targetHash = props.getProperty(ProjectProperties.PROPERTY_TARGET); + originalTarget = mSdkManager.getTargetFromHashString(targetHash); + + // if the project is already setup with proguard, we won't copy the proguard config. + hasProguard = props.getProperty(ProjectProperties.PROPERTY_PROGUARD_CONFIG) != null; + } + + if (originalTarget == null && target == null) { + mLog.error(null, + "The project either has no target set or the target is invalid.\n" + + "Please provide a --target to the '%1$s update' command.", + SdkConstants.androidCmdName()); + return false; + } + + boolean saveProjectProps = false; + + ProjectPropertiesWorkingCopy propsWC = null; + + // Update default.prop if --target was specified + if (target != null || writeProjectProp) { + // we already attempted to load the file earlier, if that failed, create it. + if (props == null) { + propsWC = ProjectProperties.create(projectFolder, PropertyType.PROJECT); + } else { + propsWC = props.makeWorkingCopy(PropertyType.PROJECT); + } + + // set or replace the target + if (target != null) { + propsWC.setProperty(ProjectProperties.PROPERTY_TARGET, target.hashString()); + } + saveProjectProps = true; + } + + if (libraryPath != null) { + // At this point, the default properties already exists, either because they were + // already there or because they were created with a new target + if (propsWC == null) { + assert props != null; + propsWC = props.makeWorkingCopy(); + } + + // check the reference is valid + File libProject = new File(libraryPath); + String resolvedPath; + if (libProject.isAbsolute() == false) { + libProject = new File(projectFolder, libraryPath); + try { + resolvedPath = libProject.getCanonicalPath(); + } catch (IOException e) { + mLog.error(e, "Unable to resolve path to library project: %1$s", libraryPath); + return false; + } + } else { + resolvedPath = libProject.getAbsolutePath(); + } + + println("Resolved location of library project to: %1$s", resolvedPath); + + // check the lib project exists + if (checkProjectFolder(resolvedPath, SdkConstants.FN_ANDROID_MANIFEST_XML) == null) { + mLog.error(null, "No Android Manifest at: %1$s", resolvedPath); + return false; + } + + // look for other references to figure out the index + int index = 1; + while (true) { + String propName = ProjectProperties.PROPERTY_LIB_REF + Integer.toString(index); + assert props != null; + if (props == null) { + // This should not happen yet SDK bug 20535 says it can, not sure how. + break; + } + String ref = props.getProperty(propName); + if (ref == null) { + break; + } else { + index++; + } + } + + String propName = ProjectProperties.PROPERTY_LIB_REF + Integer.toString(index); + propsWC.setProperty(propName, libraryPath); + saveProjectProps = true; + } + + // save the default props if needed. + if (saveProjectProps) { + try { + assert propsWC != null; + propsWC.save(); + if (writeProjectProp) { + println("Updated and renamed %1$s to %2$s", + PropertyType.LEGACY_DEFAULT.getFilename(), + PropertyType.PROJECT.getFilename()); + } else { + println("Updated %1$s", PropertyType.PROJECT.getFilename()); + } + } catch (Exception e) { + mLog.error(e, "Failed to write %1$s file in '%2$s'", + PropertyType.PROJECT.getFilename(), + folderPath); + return false; + } + + if (writeProjectProp) { + // need to delete the default prop file. + ProjectProperties.delete(projectFolder, PropertyType.LEGACY_DEFAULT); + } + } + + // Refresh/create "sdk" in local.properties + // because the file may already exists and contain other values (like apk config), + // we first try to load it. + props = ProjectProperties.load(projectFolder, PropertyType.LOCAL); + if (props == null) { + propsWC = ProjectProperties.create(projectFolder, PropertyType.LOCAL); + } else { + propsWC = props.makeWorkingCopy(); + } + + // set or replace the sdk location. + propsWC.setProperty(ProjectProperties.PROPERTY_SDK, mSdkFolder); + try { + propsWC.save(); + println("Updated %1$s", PropertyType.LOCAL.getFilename()); + } catch (Exception e) { + mLog.error(e, "Failed to write %1$s file in '%2$s'", + PropertyType.LOCAL.getFilename(), + folderPath); + return false; + } + + // legacy: check if build.properties must be renamed to ant.properties. + props = ProjectProperties.load(projectFolder, PropertyType.ANT); + if (props == null) { + props = ProjectProperties.load(projectFolder, PropertyType.LEGACY_BUILD); + if (props != null) { + try { + // get a working copy with the new property type + propsWC = props.makeWorkingCopy(PropertyType.ANT); + propsWC.save(); + + // delete the old file + ProjectProperties.delete(projectFolder, PropertyType.LEGACY_BUILD); + + println("Renamed %1$s to %2$s", + PropertyType.LEGACY_BUILD.getFilename(), + PropertyType.ANT.getFilename()); + } catch (Exception e) { + mLog.error(e, "Failed to write %1$s file in '%2$s'", + PropertyType.ANT.getFilename(), + folderPath); + return false; + } + } + } + + // Build.xml: create if not present or no in it + File buildXml = new File(projectFolder, SdkConstants.FN_BUILD_XML); + boolean needsBuildXml = projectName != null || !buildXml.exists(); + + // if it seems there's no need for a new build.xml, look for inside the file + // to try to detect old ones that may need updating. + if (!needsBuildXml) { + // we are looking for version-tag: followed by either an integer or "custom". + if (checkFileContainsRegexp(buildXml, "version-tag:\\s*custom") != null) { //$NON-NLS-1$ + println("%1$s: Found version-tag: custom. File will not be updated.", + SdkConstants.FN_BUILD_XML); + } else { + Matcher m = checkFileContainsRegexp(buildXml, "version-tag:\\s*(\\d+)"); //$NON-NLS-1$ + if (m == null) { + println("----------\n" + + "%1$s: Failed to find version-tag string. File must be updated.\n" + + "In order to not erase potential customizations, the file will not be automatically regenerated.\n" + + "If no changes have been made to the file, delete it manually and run the command again.\n" + + "If you have made customizations to the build process, the file must be manually updated.\n" + + "It is recommended to:\n" + + "\t* Copy current file to a safe location.\n" + + "\t* Delete original file.\n" + + "\t* Run command again to generate a new file.\n" + + "\t* Port customizations to the new file, by looking at the new rules file\n" + + "\t located at /tools/ant/build.xml\n" + + "\t* Update file to contain\n" + + "\t version-tag: custom\n" + + "\t to prevent file from being rewritten automatically by the SDK tools.\n" + + "----------\n", + SdkConstants.FN_BUILD_XML); + } else { + String versionStr = m.group(1); + if (versionStr != null) { + // can't fail due to regexp above. + int version = Integer.parseInt(versionStr); + if (version < MIN_BUILD_VERSION_TAG) { + println("%1$s: Found version-tag: %2$d. Expected version-tag: %3$d: file must be updated.", + SdkConstants.FN_BUILD_XML, version, MIN_BUILD_VERSION_TAG); + needsBuildXml = true; + } + } + } + } + } + + if (needsBuildXml) { + // create the map for place-holders of values to replace in the templates + final HashMap keywords = new HashMap(); + + // put the current version-tag value + keywords.put(PH_VERSION_TAG, Integer.toString(MIN_BUILD_VERSION_TAG)); + + // if there was no project name on the command line, figure one out. + if (projectName == null) { + // otherwise, take it from the existing build.xml if it exists already. + if (buildXml.exists()) { + try { + XPathFactory factory = XPathFactory.newInstance(); + XPath xpath = factory.newXPath(); + + projectName = xpath.evaluate(XPATH_PROJECT_NAME, + new InputSource(new FileInputStream(buildXml))); + } catch (XPathExpressionException e) { + // this is ok since we're going to recreate the file. + mLog.error(e, "Unable to find existing project name from %1$s", + SdkConstants.FN_BUILD_XML); + } catch (FileNotFoundException e) { + // can't happen since we check above. + } + } + + // if the project is still null, then we find another way. + if (projectName == null) { + extractPackageFromManifest(androidManifest, keywords); + if (keywords.containsKey(PH_ACTIVITY_ENTRY_NAME)) { + String activity = keywords.get(PH_ACTIVITY_ENTRY_NAME); + // keep only the last segment if applicable + int pos = activity.lastIndexOf('.'); + if (pos != -1) { + activity = activity.substring(pos + 1); + } + + // Use the activity as project name + projectName = activity; + + println("No project name specified, using Activity name '%1$s'.\n" + + "If you wish to change it, edit the first line of %2$s.", + activity, SdkConstants.FN_BUILD_XML); + } else { + // We need a project name. Just pick up the basename of the project + // directory. + File projectCanonicalFolder = projectFolder; + try { + projectCanonicalFolder = projectCanonicalFolder.getCanonicalFile(); + } catch (IOException e) { + // ignore, keep going + } + + // Use the folder name as project name + projectName = projectCanonicalFolder.getName(); + + println("No project name specified, using project folder name '%1$s'.\n" + + "If you wish to change it, edit the first line of %2$s.", + projectName, SdkConstants.FN_BUILD_XML); + } + } + } + + // put the project name in the map for replacement during the template installation. + keywords.put(PH_PROJECT_NAME, projectName); + + if (mLevel == OutputLevel.VERBOSE) { + println("Regenerating %1$s with project name %2$s", + SdkConstants.FN_BUILD_XML, + keywords.get(PH_PROJECT_NAME)); + } + + try { + installTemplate("build.template", buildXml, keywords); + } catch (ProjectCreateException e) { + mLog.error(e, null); + return false; + } + } + + if (hasProguard == false) { + try { + installTemplate(SdkConstants.FN_PROJECT_PROGUARD_FILE, + // Write ProGuard config files with the extension .pro which + // is what is used in the ProGuard documentation and samples + new File(projectFolder, SdkConstants.FN_PROJECT_PROGUARD_FILE), + null /*placeholderMap*/); + } catch (ProjectCreateException e) { + mLog.error(e, null); + return false; + } + } + + return true; + } + + /** + * Updates a test project with a new path to the main (tested) project. + * @param folderPath the path of the test project. + * @param pathToMainProject the path to the main project, relative to the test project. + */ + @SuppressWarnings("deprecation") + public void updateTestProject(final String folderPath, final String pathToMainProject, + final SdkManager sdkManager) { + // since this is an update, check the folder does point to a project + if (checkProjectFolder(folderPath, SdkConstants.FN_ANDROID_MANIFEST_XML) == null) { + return; + } + + // check the path to the main project is valid. + File mainProject = new File(pathToMainProject); + String resolvedPath; + if (mainProject.isAbsolute() == false) { + mainProject = new File(folderPath, pathToMainProject); + try { + resolvedPath = mainProject.getCanonicalPath(); + } catch (IOException e) { + mLog.error(e, "Unable to resolve path to main project: %1$s", pathToMainProject); + return; + } + } else { + resolvedPath = mainProject.getAbsolutePath(); + } + + println("Resolved location of main project to: %1$s", resolvedPath); + + // check the main project exists + if (checkProjectFolder(resolvedPath, SdkConstants.FN_ANDROID_MANIFEST_XML) == null) { + mLog.error(null, "No Android Manifest at: %1$s", resolvedPath); + return; + } + + // now get the target from the main project + ProjectProperties projectProp = ProjectProperties.load(resolvedPath, PropertyType.PROJECT); + if (projectProp == null) { + // legacy support for older file name. + projectProp = ProjectProperties.load(resolvedPath, PropertyType.LEGACY_DEFAULT); + if (projectProp == null) { + mLog.error(null, "No %1$s at: %2$s", PropertyType.PROJECT.getFilename(), + resolvedPath); + return; + } + } + + String targetHash = projectProp.getProperty(ProjectProperties.PROPERTY_TARGET); + if (targetHash == null) { + mLog.error(null, "%1$s in the main project has no target property.", + PropertyType.PROJECT.getFilename()); + return; + } + + IAndroidTarget target = sdkManager.getTargetFromHashString(targetHash); + if (target == null) { + mLog.error(null, "Main project target %1$s is not a valid target.", targetHash); + return; + } + + // update test-project does not support the --name parameter, therefore the project + // name should generally not be passed to updateProject(). + // However if build.xml does not exist then updateProject() will recreate it. In this + // case we will need the project name. + // To do this, we look for the parent project name and add "test" to it. + // If the main project does not have a project name (yet), then the default behavior + // will be used (look for activity and then folder name) + String projectName = null; + XPathFactory factory = XPathFactory.newInstance(); + XPath xpath = factory.newXPath(); + + File testBuildXml = new File(folderPath, SdkConstants.FN_BUILD_XML); + if (testBuildXml.isFile() == false) { + File mainBuildXml = new File(resolvedPath, SdkConstants.FN_BUILD_XML); + if (mainBuildXml.isFile()) { + try { + // get the name of the main project and add Test to it. + String mainProjectName = xpath.evaluate(XPATH_PROJECT_NAME, + new InputSource(new FileInputStream(mainBuildXml))); + projectName = mainProjectName + "Test"; + } catch (XPathExpressionException e) { + // it's ok, updateProject() will figure out a name automatically. + // We do log the error though as the build.xml file may be broken. + mLog.warning("Failed to parse %1$s.\n" + + "File may not be valid. Consider running 'android update project' on the main project.", + mainBuildXml.getPath()); + } catch (FileNotFoundException e) { + // should not happen since we check first. + } + } + } + + // now update the project as if it's a normal project + if (updateProject(folderPath, target, projectName, null /*libraryPath*/) == false) { + // error message has already been displayed. + return; + } + + // add the test project specific properties. + // At this point, we know build.prop has been renamed ant.prop + ProjectProperties antProps = ProjectProperties.load(folderPath, PropertyType.ANT); + ProjectPropertiesWorkingCopy antWorkingCopy; + if (antProps == null) { + antWorkingCopy = ProjectProperties.create(folderPath, PropertyType.ANT); + } else { + antWorkingCopy = antProps.makeWorkingCopy(); + } + + // set or replace the path to the main project + antWorkingCopy.setProperty(ProjectProperties.PROPERTY_TESTED_PROJECT, pathToMainProject); + try { + antWorkingCopy.save(); + println("Updated %1$s", PropertyType.ANT.getFilename()); + } catch (Exception e) { + mLog.error(e, "Failed to write %1$s file in '%2$s'", + PropertyType.ANT.getFilename(), + folderPath); + return; + } + } + + /** + * Checks whether the give folderPath is a valid project folder, and returns + * a {@link FileWrapper} to the required file. + *

This checks that the folder exists and contains an AndroidManifest.xml file in it. + *

Any error are output using {@link #mLog}. + * @param folderPath the folder to check + * @param requiredFilename the file name of the file that's required. + * @return a {@link FileWrapper} to the AndroidManifest.xml file, or null otherwise. + */ + private FileWrapper checkProjectFolder(String folderPath, String requiredFilename) { + // project folder must exist and be a directory, since this is an update + FolderWrapper projectFolder = new FolderWrapper(folderPath); + if (!projectFolder.isDirectory()) { + mLog.error(null, "Project folder '%1$s' is not a valid directory.", + projectFolder); + return null; + } + + // Check AndroidManifest.xml is present + FileWrapper requireFile = new FileWrapper(projectFolder, requiredFilename); + if (!requireFile.isFile()) { + mLog.error(null, + "%1$s is not a valid project (%2$s not found).", + folderPath, requiredFilename); + return null; + } + + return requireFile; + } + + /** + * Looks for a given regex in a file and returns the matcher if any line of the input file + * contains the requested regexp. + * + * @param file the file to search. + * @param regexp the regexp to search for. + * + * @return a Matcher or null if the regexp is not found. + */ + private Matcher checkFileContainsRegexp(File file, String regexp) { + Pattern p = Pattern.compile(regexp); + + BufferedReader in = null; + try { + in = new BufferedReader(new FileReader(file)); + String line; + + while ((line = in.readLine()) != null) { + Matcher m = p.matcher(line); + if (m.find()) { + return m; + } + } + + in.close(); + } catch (Exception e) { + // ignore + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException e) { + // ignore + } + } + } + + return null; + } + + /** + * Extracts a "full" package & activity name from an AndroidManifest.xml. + *

+ * The keywords dictionary is always filed the package name under the key {@link #PH_PACKAGE}. + * If an activity name can be found, it is filed under the key {@link #PH_ACTIVITY_ENTRY_NAME}. + * When no activity is found, this key is not created. + * + * @param manifestFile The AndroidManifest.xml file + * @param outKeywords Place where to put the out parameters: package and activity names. + * @return True if the package/activity was parsed and updated in the keyword dictionary. + */ + private boolean extractPackageFromManifest(File manifestFile, + Map outKeywords) { + try { + XPath xpath = AndroidXPathFactory.newXPath(); + + InputSource source = new InputSource(new FileReader(manifestFile)); + String packageName = xpath.evaluate("/manifest/@package", source); + + source = new InputSource(new FileReader(manifestFile)); + + // Select the "android:name" attribute of all nodes but only if they + // contain a sub-node with an "android:name" attribute which + // is 'android.intent.action.MAIN' and an with an + // "android:name" attribute which is 'android.intent.category.LAUNCHER' + String expression = String.format("/manifest/application/activity" + + "[intent-filter/action/@%1$s:name='android.intent.action.MAIN' and " + + "intent-filter/category/@%1$s:name='android.intent.category.LAUNCHER']" + + "/@%1$s:name", AndroidXPathFactory.DEFAULT_NS_PREFIX); + + NodeList activityNames = (NodeList) xpath.evaluate(expression, source, + XPathConstants.NODESET); + + // If we get here, both XPath expressions were valid so we're most likely dealing + // with an actual AndroidManifest.xml file. The nodes may not have the requested + // attributes though, if which case we should warn. + + if (packageName == null || packageName.isEmpty()) { + mLog.error(null, + "Missing in '%1$s'", + manifestFile.getName()); + return false; + } + + // Get the first activity that matched earlier. If there is no activity, + // activityName is set to an empty string and the generated "combined" name + // will be in the form "package." (with a dot at the end). + String activityName = ""; + if (activityNames.getLength() > 0) { + activityName = activityNames.item(0).getNodeValue(); + } + + if (mLevel == OutputLevel.VERBOSE && activityNames.getLength() > 1) { + println("WARNING: There is more than one activity defined in '%1$s'.\n" + + "Only the first one will be used. If this is not appropriate, you need\n" + + "to specify one of these values manually instead:", + manifestFile.getName()); + + for (int i = 0; i < activityNames.getLength(); i++) { + String name = activityNames.item(i).getNodeValue(); + name = combinePackageActivityNames(packageName, name); + println("- %1$s", name); + } + } + + if (activityName.isEmpty()) { + mLog.warning("Missing in '%2$s'.\n" + + "No activity will be generated.", + AndroidXPathFactory.DEFAULT_NS_PREFIX, manifestFile.getName()); + } else { + outKeywords.put(PH_ACTIVITY_ENTRY_NAME, activityName); + } + + outKeywords.put(PH_PACKAGE, packageName); + return true; + + } catch (IOException e) { + mLog.error(e, "Failed to read %1$s", manifestFile.getName()); + } catch (XPathExpressionException e) { + Throwable t = e.getCause(); + mLog.error(t == null ? e : t, + "Failed to parse %1$s", + manifestFile.getName()); + } + + return false; + } + + private String combinePackageActivityNames(String packageName, String activityName) { + // Activity Name can have 3 forms: + // - ".Name" means this is a class name in the given package name. + // The full FQCN is thus packageName + ".Name" + // - "Name" is an older variant of the former. Full FQCN is packageName + "." + "Name" + // - "com.blah.Name" is a full FQCN. Ignore packageName and use activityName as-is. + // To be valid, the package name should have at least two components. This is checked + // later during the creation of the build.xml file, so we just need to detect there's + // a dot but not at pos==0. + + int pos = activityName.indexOf('.'); + if (pos == 0) { + return packageName + activityName; + } else if (pos > 0) { + return activityName; + } else { + return packageName + "." + activityName; + } + } + + /** + * Installs a new file that is based on a template file provided by a given target. + * Each match of each key from the place-holder map in the template will be replaced with its + * corresponding value in the created file. + * + * @param templateName the name of to the template file + * @param destFile the path to the destination file, relative to the project + * @param placeholderMap a map of (place-holder, value) to create the file from the template. + * @param target the Target of the project that will be providing the template. + * @throws ProjectCreateException + */ + private void installTargetTemplate(String templateName, File destFile, + Map placeholderMap, IAndroidTarget target) + throws ProjectCreateException { + // query the target for its template directory + String templateFolder = target.getPath(IAndroidTarget.TEMPLATES); + final String sourcePath = templateFolder + File.separator + templateName; + + installFullPathTemplate(sourcePath, destFile, placeholderMap); + } + + /** + * Installs a new file from the gradle wrapper template. + * + * @param templateName the name of the template file + * @param projectFolder the path to the project folder + * @throws ProjectCreateException + */ + public void installGradleWrapperFile(String templateName, File projectFolder) + throws ProjectCreateException { + String templateFolder = mSdkFolder + File.separator + + SdkConstants.OS_SDK_TOOLS_TEMPLATES_GRADLE_WRAPPER_FOLDER; + + installBinaryFile(new File(templateFolder, templateName), + new File(projectFolder, templateName)); + } + + /** + * Installs a new file that is based on a template file provided by the tools folder. + * Each match of each key from the place-holder map in the template will be replaced with its + * corresponding value in the created file. + * + * @param templateName the name of to the template file + * @param destFile the path to the destination file, relative to the project + * @param placeholderMap a map of (place-holder, value) to create the file from the template. + * @throws ProjectCreateException + */ + public void installTemplate(String templateName, File destFile, + Map placeholderMap) + throws ProjectCreateException { + // query the target for its template directory + String templateFolder = mSdkFolder + File.separator + SdkConstants.OS_SDK_TOOLS_LIB_FOLDER; + final String sourcePath = templateFolder + File.separator + templateName; + + installFullPathTemplate(sourcePath, destFile, placeholderMap); + } + + /** + * Installs a new file that is based on a template. + * Each match of each key from the place-holder map in the template will be replaced with its + * corresponding value in the created file. + * + * @param sourcePath the full path to the source template file + * @param destFile the destination file + * @param placeholderMap a map of (place-holder, value) to create the file from the template. + * @throws ProjectCreateException + */ + private void installFullPathTemplate(String sourcePath, File destFile, + Map placeholderMap) throws ProjectCreateException { + + boolean existed = destFile.exists(); + + try { + BufferedWriter out = new BufferedWriter(new FileWriter(destFile)); + BufferedReader in = new BufferedReader(new FileReader(sourcePath)); + String line; + + while ((line = in.readLine()) != null) { + if (placeholderMap != null) { + for (Map.Entry entry : placeholderMap.entrySet()) { + line = line.replace(entry.getKey(), entry.getValue()); + } + } + + out.write(line); + out.newLine(); + } + + out.close(); + in.close(); + } catch (Exception e) { + throw new ProjectCreateException(e, "Could not access %1$s: %2$s", + destFile, e.getMessage()); + } + + println("%1$s file %2$s", + existed ? "Updated" : "Added", + destFile); + } + + /** + * Installs the project icons. + * @param resourceFolder the resource folder + * @param target the target of the project. + * @return true if any icon was installed. + */ + private boolean installIcons(File resourceFolder, IAndroidTarget target) + throws ProjectCreateException { + // query the target for its template directory + String templateFolder = target.getPath(IAndroidTarget.TEMPLATES); + + boolean installedIcon = false; + + installedIcon |= installIcon(templateFolder, "ic_launcher_xhdpi.png", resourceFolder, + "drawable-xhdpi"); + installedIcon |= installIcon(templateFolder, "ic_launcher_hdpi.png", resourceFolder, + "drawable-hdpi"); + installedIcon |= installIcon(templateFolder, "ic_launcher_mdpi.png", resourceFolder, + "drawable-mdpi"); + installedIcon |= installIcon(templateFolder, "ic_launcher_ldpi.png", resourceFolder, + "drawable-ldpi"); + + return installedIcon; + } + + /** + * Installs an Icon in the project. + * @return true if the icon was installed. + */ + private boolean installIcon(String templateFolder, String iconName, File resourceFolder, + String folderName) throws ProjectCreateException { + File icon = new File(templateFolder, iconName); + if (icon.exists()) { + File drawable = createDirs(resourceFolder, folderName); + installBinaryFile(icon, new File(drawable, "ic_launcher.png")); + return true; + } + + return false; + } + + /** + * Installs a binary file + * @param source the source file to copy + * @param destination the destination file to write + * @throws ProjectCreateException + */ + private void installBinaryFile(File source, File destination) throws ProjectCreateException { + byte[] buffer = new byte[8192]; + + FileInputStream fis = null; + FileOutputStream fos = null; + try { + fis = new FileInputStream(source); + fos = new FileOutputStream(destination); + + int read; + while ((read = fis.read(buffer)) != -1) { + fos.write(buffer, 0, read); + } + + } catch (FileNotFoundException e) { + // shouldn't happen since we check before. + } catch (IOException e) { + throw new ProjectCreateException(e, "Failed to read binary file: %1$s", + source.getAbsolutePath()); + } finally { + if (fis != null) { + try { + fis.close(); + } catch (IOException e) { + // ignore + } + } + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + // ignore + } + } + } + + } + + /** + * Prints a message unless silence is enabled. + *

+ * This is just a convenience wrapper around {@link ILogger#info(String, Object...)} from + * {@link #mLog} after testing if output level is {@link OutputLevel#VERBOSE}. + * + * @param format Format for String.format + * @param args Arguments for String.format + */ + private void println(String format, Object... args) { + if (mLevel != OutputLevel.SILENT) { + if (!format.endsWith("\n")) { + format += "\n"; + } + mLog.info(format, args); + } + } + + /** + * Creates a new folder, along with any parent folders that do not exists. + * + * @param parent the parent folder + * @param name the name of the directory to create. + * @throws ProjectCreateException + */ + private File createDirs(File parent, String name) throws ProjectCreateException { + final File newFolder = new File(parent, name); + boolean existedBefore = true; + + if (!newFolder.exists()) { + if (!newFolder.mkdirs()) { + throw new ProjectCreateException("Could not create directory: %1$s", newFolder); + } + existedBefore = false; + } + + if (newFolder.isDirectory()) { + if (!newFolder.canWrite()) { + throw new ProjectCreateException("Path is not writable: %1$s", newFolder); + } + } else { + throw new ProjectCreateException("Path is not a directory: %1$s", newFolder); + } + + if (!existedBefore) { + try { + println("Created directory %1$s", newFolder.getCanonicalPath()); + } catch (IOException e) { + throw new ProjectCreateException( + "Could not determine canonical path of created directory", e); + } + } + + return newFolder; + } + + /** + * Strips the string of beginning and trailing characters (multiple + * characters will be stripped, example stripString("..test...", '.') + * results in "test"; + * + * @param s the string to strip + * @param strip the character to strip from beginning and end + * @return the stripped string or the empty string if everything is stripped. + */ + private static String stripString(String s, char strip) { + final int sLen = s.length(); + int newStart = 0, newEnd = sLen - 1; + + while (newStart < sLen && s.charAt(newStart) == strip) { + newStart++; + } + while (newEnd >= 0 && s.charAt(newEnd) == strip) { + newEnd--; + } + + /* + * newEnd contains a char we want, and substring takes end as being + * exclusive + */ + newEnd++; + + if (newStart >= sLen || newEnd < 0) { + return ""; + } + + return s.substring(newStart, newEnd); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/project/ProjectProperties.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/project/ProjectProperties.java new file mode 100644 index 00000000..7c574694 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/project/ProjectProperties.java @@ -0,0 +1,594 @@ +/* + * 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.sdklib.internal.project; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.io.FolderWrapper; +import com.android.io.IAbstractFile; +import com.android.io.IAbstractFolder; +import com.android.io.StreamException; +import com.android.utils.ILogger; +import com.google.common.io.Closeables; + +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Class representing project properties for both ADT and Ant-based build. + *

The class is associated to a {@link PropertyType} that indicate which of the project + * property file is represented. + *

To load an existing file, use {@link #load(IAbstractFolder, PropertyType)}. + *

The class is meant to be always in sync (or at least not newer) than the file it represents. + * Once created, it can only be updated through {@link #reload()} + * + *

The make modification or make new file, use a {@link ProjectPropertiesWorkingCopy} instance, + * either through {@link #create(IAbstractFolder, PropertyType)} or through + * {@link #makeWorkingCopy()}. + * + */ +public class ProjectProperties implements IPropertySource { + protected static final Pattern PATTERN_PROP = Pattern.compile( + "^([a-zA-Z0-9._-]+)\\s*=\\s*(.*)\\s*$"); + + /** The property name for the project target */ + public static final String PROPERTY_TARGET = "target"; + /** The property name for the renderscript build target */ + public static final String PROPERTY_RS_TARGET = "renderscript.target"; + /** The property name for the renderscript support mode */ + public static final String PROPERTY_RS_SUPPORT = "renderscript.support.mode"; + /** The version of the build tools to use to compile */ + public static final String PROPERTY_BUILD_TOOLS = "sdk.buildtools"; + + public static final String PROPERTY_LIBRARY = "android.library"; + public static final String PROPERTY_LIB_REF = "android.library.reference."; + private static final String PROPERTY_LIB_REF_REGEX = "android.library.reference.\\d+"; + + public static final String PROPERTY_PROGUARD_CONFIG = "proguard.config"; + public static final String PROPERTY_RULES_PATH = "layoutrules.jars"; + + public static final String PROPERTY_SDK = "sdk.dir"; + public static final String PROPERTY_NDK = "ndk.dir"; + // LEGACY - Kept so that we can actually remove it from local.properties. + private static final String PROPERTY_SDK_LEGACY = "sdk-location"; + + public static final String PROPERTY_SPLIT_BY_DENSITY = "split.density"; + public static final String PROPERTY_SPLIT_BY_ABI = "split.abi"; + public static final String PROPERTY_SPLIT_BY_LOCALE = "split.locale"; + + public static final String PROPERTY_TESTED_PROJECT = "tested.project.dir"; + + public static final String PROPERTY_BUILD_SOURCE_DIR = "source.dir"; + public static final String PROPERTY_BUILD_OUT_DIR = "out.dir"; + + public static final String PROPERTY_PACKAGE = "package"; + public static final String PROPERTY_VERSIONCODE = "versionCode"; + public static final String PROPERTY_PROJECTS = "projects"; + public static final String PROPERTY_KEY_STORE = "key.store"; + public static final String PROPERTY_KEY_ALIAS = "key.alias"; + + public enum PropertyType { + ANT(SdkConstants.FN_ANT_PROPERTIES, BUILD_HEADER, new String[] { + PROPERTY_BUILD_SOURCE_DIR, PROPERTY_BUILD_OUT_DIR + }, null), + PROJECT(SdkConstants.FN_PROJECT_PROPERTIES, DEFAULT_HEADER, new String[] { + PROPERTY_TARGET, PROPERTY_LIBRARY, PROPERTY_LIB_REF_REGEX, + PROPERTY_KEY_STORE, PROPERTY_KEY_ALIAS, PROPERTY_PROGUARD_CONFIG, + PROPERTY_RULES_PATH + }, null), + LOCAL(SdkConstants.FN_LOCAL_PROPERTIES, LOCAL_HEADER, new String[] { + PROPERTY_SDK + }, + new String[] { PROPERTY_SDK_LEGACY }), + @Deprecated + LEGACY_DEFAULT("default.properties", null, null, null), + @Deprecated + LEGACY_BUILD("build.properties", null, null, null); + + + private final String mFilename; + private final String mHeader; + private final Set mKnownProps; + private final Set mRemovedProps; + + /** + * Returns the PropertyTypes ordered the same way Ant order them. + */ + public static PropertyType[] getOrderedTypes() { + return new PropertyType[] { + PropertyType.LOCAL, PropertyType.ANT, PropertyType.PROJECT + }; + } + + PropertyType(String filename, String header, String[] validProps, String[] removedProps) { + mFilename = filename; + mHeader = header; + HashSet s = new HashSet(); + if (validProps != null) { + s.addAll(Arrays.asList(validProps)); + } + mKnownProps = Collections.unmodifiableSet(s); + + s = new HashSet(); + if (removedProps != null) { + s.addAll(Arrays.asList(removedProps)); + } + mRemovedProps = Collections.unmodifiableSet(s); + + } + + public String getFilename() { + return mFilename; + } + + public String getHeader() { + return mHeader; + } + + /** + * Returns whether a given property is known for the property type. + */ + public boolean isKnownProperty(String name) { + for (String propRegex : mKnownProps) { + if (propRegex.equals(name) || Pattern.matches(propRegex, name)) { + return true; + } + } + + return false; + } + + /** + * Returns whether a given property should be removed for the property type. + */ + public boolean isRemovedProperty(String name) { + for (String propRegex : mRemovedProps) { + if (propRegex.equals(name) || Pattern.matches(propRegex, name)) { + return true; + } + } + + return false; + } + } + + private static final String LOCAL_HEADER = +// 1-------10--------20--------30--------40--------50--------60--------70--------80 + "# This file is automatically generated by Android Tools.\n" + + "# Do not modify this file -- YOUR CHANGES WILL BE ERASED!\n" + + "#\n" + + "# This file must *NOT* be checked into Version Control Systems,\n" + + "# as it contains information specific to your local configuration.\n" + + "\n"; + + private static final String DEFAULT_HEADER = +// 1-------10--------20--------30--------40--------50--------60--------70--------80 + "# This file is automatically generated by Android Tools.\n" + + "# Do not modify this file -- YOUR CHANGES WILL BE ERASED!\n" + + "#\n" + + "# This file must be checked in Version Control Systems.\n" + + "#\n" + + "# To customize properties used by the Ant build system edit\n" + + "# \"ant.properties\", and override values to adapt the script to your\n" + + "# project structure.\n" + + "#\n" + + "# To enable ProGuard to shrink and obfuscate your code, uncomment this " + + "(available properties: sdk.dir, user.home):\n" + + // Note: always use / separators in the properties paths. Both Ant and + // our ExportHelper will convert them properly according to the platform. + "#" + PROPERTY_PROGUARD_CONFIG + "=${" + PROPERTY_SDK +"}/" + + SdkConstants.FD_TOOLS + '/' + SdkConstants.FD_PROGUARD + '/' + + SdkConstants.FN_ANDROID_PROGUARD_FILE + ':' + + SdkConstants.FN_PROJECT_PROGUARD_FILE +'\n' + + "\n"; + + private static final String BUILD_HEADER = +// 1-------10--------20--------30--------40--------50--------60--------70--------80 + "# This file is used to override default values used by the Ant build system.\n" + + "#\n" + + "# This file must be checked into Version Control Systems, as it is\n" + + "# integral to the build system of your project.\n" + + "\n" + + "# This file is only used by the Ant script.\n" + + "\n" + + "# You can use this to override default values such as\n" + + "# 'source.dir' for the location of your java source folder and\n" + + "# 'out.dir' for the location of your output folder.\n" + + "\n" + + "# You can also use it define how the release builds are signed by declaring\n" + + "# the following properties:\n" + + "# 'key.store' for the location of your keystore and\n" + + "# 'key.alias' for the name of the key to use.\n" + + "# The password will be asked during the build when you use the 'release' target.\n" + + "\n"; + + protected final IAbstractFolder mProjectFolder; + protected final Map mProperties; + protected final PropertyType mType; + + /** + * Loads a project properties file and return a {@link ProjectProperties} object + * containing the properties. + * + * @param projectFolderOsPath the project folder. + * @param type One the possible {@link PropertyType}s. + */ + public static ProjectProperties load(String projectFolderOsPath, PropertyType type) { + IAbstractFolder wrapper = new FolderWrapper(projectFolderOsPath); + return load(wrapper, type); + } + + /** + * Loads a project properties file and return a {@link ProjectProperties} object + * containing the properties. + * + * @param projectFolder the project folder. + * @param type One the possible {@link PropertyType}s. + */ + public static ProjectProperties load(IAbstractFolder projectFolder, PropertyType type) { + if (projectFolder.exists()) { + IAbstractFile propFile = projectFolder.getFile(type.mFilename); + if (propFile.exists()) { + Map map = parsePropertyFile(propFile, null /* log */); + if (map != null) { + return new ProjectProperties(projectFolder, map, type); + } + } + } + return null; + } + + /** + * Deletes a project properties file. + * + * @param projectFolder the project folder. + * @param type One the possible {@link PropertyType}s. + * @return true if success. + */ + public static boolean delete(IAbstractFolder projectFolder, PropertyType type) { + if (projectFolder.exists()) { + IAbstractFile propFile = projectFolder.getFile(type.mFilename); + if (propFile.exists()) { + return propFile.delete(); + } + } + + return false; + } + + /** + * Deletes a project properties file. + * + * @param projectFolderOsPath the project folder. + * @param type One the possible {@link PropertyType}s. + * @return true if success. + */ + public static boolean delete(String projectFolderOsPath, PropertyType type) { + IAbstractFolder wrapper = new FolderWrapper(projectFolderOsPath); + return delete(wrapper, type); + } + + + /** + * Creates a new project properties object, with no properties. + *

The file is not created until {@link ProjectPropertiesWorkingCopy#save()} is called. + * @param projectFolderOsPath the project folder. + * @param type the type of property file to create + * + * @see #createEmpty(String, PropertyType) + */ + public static ProjectPropertiesWorkingCopy create(@NonNull String projectFolderOsPath, + @NonNull PropertyType type) { + // create and return a ProjectProperties with an empty map. + IAbstractFolder folder = new FolderWrapper(projectFolderOsPath); + return create(folder, type); + } + + /** + * Creates a new project properties object, with no properties. + *

The file is not created until {@link ProjectPropertiesWorkingCopy#save()} is called. + * @param projectFolder the project folder. + * @param type the type of property file to create + * + * @see #createEmpty(IAbstractFolder, PropertyType) + */ + public static ProjectPropertiesWorkingCopy create(@NonNull IAbstractFolder projectFolder, + @NonNull PropertyType type) { + // create and return a ProjectProperties with an empty map. + return new ProjectPropertiesWorkingCopy(projectFolder, new HashMap(), type); + } + + /** + * Creates a new project properties object, with no properties. + *

Nothing can be added to it, unless a {@link ProjectPropertiesWorkingCopy} is created + * first with {@link #makeWorkingCopy()}. + * @param projectFolderOsPath the project folder. + * @param type the type of property file to create + * + * @see #create(String, PropertyType) + */ + public static ProjectProperties createEmpty(@NonNull String projectFolderOsPath, + @NonNull PropertyType type) { + // create and return a ProjectProperties with an empty map. + IAbstractFolder folder = new FolderWrapper(projectFolderOsPath); + return createEmpty(folder, type); + } + + /** + * Creates a new project properties object, with no properties. + *

Nothing can be added to it, unless a {@link ProjectPropertiesWorkingCopy} is created + * first with {@link #makeWorkingCopy()}. + * @param projectFolder the project folder. + * @param type the type of property file to create + * + * @see #create(IAbstractFolder, PropertyType) + */ + public static ProjectProperties createEmpty(@NonNull IAbstractFolder projectFolder, + @NonNull PropertyType type) { + // create and return a ProjectProperties with an empty map. + return new ProjectProperties(projectFolder, new HashMap(), type); + } + + /** + * Returns the location of this property file. + */ + public IAbstractFile getFile() { + return mProjectFolder.getFile(mType.mFilename); + } + + /** + * Creates and returns a copy of the current properties as a + * {@link ProjectPropertiesWorkingCopy} that can be modified and saved. + * @return a new instance of {@link ProjectPropertiesWorkingCopy} + */ + public ProjectPropertiesWorkingCopy makeWorkingCopy() { + return makeWorkingCopy(mType); + } + + /** + * Creates and returns a copy of the current properties as a + * {@link ProjectPropertiesWorkingCopy} that can be modified and saved. This also allows + * converting to a new type, by specifying a different {@link PropertyType}. + * + * @param type the {@link PropertyType} of the prop file to save. + * + * @return a new instance of {@link ProjectPropertiesWorkingCopy} + */ + public ProjectPropertiesWorkingCopy makeWorkingCopy(PropertyType type) { + // copy the current properties in a new map + Map propList = new HashMap(mProperties); + + return new ProjectPropertiesWorkingCopy(mProjectFolder, propList, type); + } + + /** + * Returns the type of the property file. + * + * @see PropertyType + */ + public PropertyType getType() { + return mType; + } + + /** + * Returns the value of a property. + * @param name the name of the property. + * @return the property value or null if the property is not set. + */ + @Override + public synchronized String getProperty(String name) { + return mProperties.get(name); + } + + /** + * Returns a set of the property keys. Unlike {@link Map#keySet()} this is not a view of the + * map keys. Modifying the returned {@link Set} will not impact the underlying {@link Map}. + */ + public synchronized Set keySet() { + return new HashSet(mProperties.keySet()); + } + + /** + * Reloads the properties from the underlying file. + */ + public synchronized void reload() { + if (mProjectFolder.exists()) { + IAbstractFile propFile = mProjectFolder.getFile(mType.mFilename); + if (propFile.exists()) { + Map map = parsePropertyFile(propFile, null /* log */); + if (map != null) { + mProperties.clear(); + mProperties.putAll(map); + } + } + } + } + + /** + * Parses a property file (using UTF-8 encoding) and returns a map of the content. + *

+ * If the file is not present, null is returned with no error messages sent to the log. + *

+ * IMPORTANT: This method is now unfortunately used in multiple places to parse random + * property files. This is NOT a safe practice since there is no corresponding method + * to write property files unless you use {@link ProjectPropertiesWorkingCopy#save()}. + * Code that writes INI or properties without at least using {@link #escape(String)} will + * certainly not load back correct data.
+ * Unless there's a strong legacy need to support existing files, new callers should + * probably just use Java's {@link Properties} which has well defined semantics. + * It's also a mistake to write/read property files using this code and expect it to + * work with Java's {@link Properties} or external tools (e.g. ant) since there can be + * differences in escaping and in character encoding. + * + * @param propFile the property file to parse + * @param log the ILogger object receiving warning/error from the parsing. + * @return the map of (key,value) pairs, or null if the parsing failed. + */ + public static Map parsePropertyFile( + @NonNull IAbstractFile propFile, + @Nullable ILogger log) { + InputStream is = null; + try { + is = propFile.getContents(); + return parsePropertyStream(is, + propFile.getOsLocation(), + log); + } catch (StreamException e) { + if (log != null) { + log.warning("Error parsing '%1$s': %2$s.", + propFile.getOsLocation(), + e.getMessage()); + } + } finally { + try { + Closeables.close(is, true /* swallowIOException */); + } catch (IOException e) { + // cannot happen + } + } + + + return null; + } + + /** + * Parses a property file (using UTF-8 encoding) and returns a map of the content. + *

+ * Always closes the given input stream on exit. + *

+ * IMPORTANT: This method is now unfortunately used in multiple places to parse random + * property files. This is NOT a safe practice since there is no corresponding method + * to write property files unless you use {@link ProjectPropertiesWorkingCopy#save()}. + * Code that writes INI or properties without at least using {@link #escape(String)} will + * certainly not load back correct data.
+ * Unless there's a strong legacy need to support existing files, new callers should + * probably just use Java's {@link Properties} which has well defined semantics. + * It's also a mistake to write/read property files using this code and expect it to + * work with Java's {@link Properties} or external tools (e.g. ant) since there can be + * differences in escaping and in character encoding. + * + * @param propStream the input stream of the property file to parse. + * @param propPath the file path, for display purposed in case of error. + * @param log the ILogger object receiving warning/error from the parsing. + * @return the map of (key,value) pairs, or null if the parsing failed. + */ + public static Map parsePropertyStream( + @NonNull InputStream propStream, + @NonNull String propPath, + @Nullable ILogger log) { + BufferedReader reader = null; + try { + //noinspection IOResourceOpenedButNotSafelyClosed + reader = new BufferedReader( + new InputStreamReader(propStream, SdkConstants.INI_CHARSET)); + + String line = null; + Map map = new HashMap(); + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (!line.isEmpty() && line.charAt(0) != '#') { + + Matcher m = PATTERN_PROP.matcher(line); + if (m.matches()) { + map.put(m.group(1), unescape(m.group(2))); + } else { + if (log != null) { + log.warning("Error parsing '%1$s': \"%2$s\" is not a valid syntax", + propPath, + line); + } + return null; + } + } + } + + return map; + } catch (FileNotFoundException e) { + // this should not happen since we usually test the file existence before + // calling the method. + // Return null below. + } catch (IOException e) { + if (log != null) { + log.warning("Error parsing '%1$s': %2$s.", + propPath, + e.getMessage()); + } + } finally { + try { + Closeables.close(reader, true /* swallowIOException */); + } catch (IOException e) { + // cannot happen + } + try { + Closeables.close(propStream, true /* swallowIOException */); + } catch (IOException e) { + // cannot happen + } + } + + return null; + } + + /** + * Private constructor. + *

+ * Use {@link #load(String, PropertyType)} or {@link #create(String, PropertyType)} + * to instantiate. + */ + protected ProjectProperties( + @NonNull IAbstractFolder projectFolder, + @NonNull Map map, + @NonNull PropertyType type) { + mProjectFolder = projectFolder; + mProperties = map; + mType = type; + } + + private static String unescape(String value) { + return value.replaceAll("\\\\\\\\", "\\\\"); + } + + protected static String escape(String value) { + return value.replaceAll("\\\\", "\\\\\\\\"); + } + + @Override + public void debugPrint() { + System.out.println("DEBUG PROJECTPROPERTIES: " + mProjectFolder); + System.out.println("type: " + mType); + for (Entry entry : mProperties.entrySet()) { + System.out.println(entry.getKey() + " -> " + entry.getValue()); + } + System.out.println("<<< DEBUG PROJECTPROPERTIES"); + + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/project/ProjectPropertiesWorkingCopy.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/project/ProjectPropertiesWorkingCopy.java new file mode 100644 index 00000000..4fac8f9d --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/project/ProjectPropertiesWorkingCopy.java @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2010 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.sdklib.internal.project; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.io.IAbstractFile; +import com.android.io.IAbstractFolder; +import com.android.io.StreamException; +import com.google.common.io.Closeables; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.regex.Matcher; + +/** + * A modifiable and savable copy of a {@link ProjectProperties}. + *

This copy gives access to modification method such as {@link #setProperty(String, String)} + * and {@link #removeProperty(String)}. + * + * To get access to an instance, use {@link ProjectProperties#makeWorkingCopy()} or + * {@link ProjectProperties#create(IAbstractFolder, PropertyType)}. + */ +public class ProjectPropertiesWorkingCopy extends ProjectProperties { + + private static final Map COMMENT_MAP = new HashMap(); + static { +// 1-------10--------20--------30--------40--------50--------60--------70--------80 + COMMENT_MAP.put(PROPERTY_TARGET, + "# Project target.\n"); + COMMENT_MAP.put(PROPERTY_SPLIT_BY_DENSITY, + "# Indicates whether an apk should be generated for each density.\n"); + COMMENT_MAP.put(PROPERTY_SDK, + "# location of the SDK. This is only used by Ant\n" + + "# For customization when using a Version Control System, please read the\n" + + "# header note.\n"); + COMMENT_MAP.put(PROPERTY_PACKAGE, + "# Package of the application being exported\n"); + COMMENT_MAP.put(PROPERTY_VERSIONCODE, + "# Major version code\n"); + COMMENT_MAP.put(PROPERTY_PROJECTS, + "# List of the Android projects being used for the export.\n" + + "# The list is made of paths that are relative to this project,\n" + + "# using forward-slash (/) as separator, and are separated by colons (:).\n"); + } + + + /** + * Sets a new properties. If a property with the same name already exists, it is replaced. + * @param name the name of the property. + * @param value the value of the property. + */ + public synchronized void setProperty(String name, String value) { + mProperties.put(name, value); + } + + /** + * Removes a property and returns its previous value (or null if the property did not exist). + * @param name the name of the property to remove. + */ + public synchronized String removeProperty(String name) { + return mProperties.remove(name); + } + + /** + * Merges all properties from the given file into the current properties. + *

+ * This emulates the Ant behavior: existing properties are not overridden. + * Only new undefined properties become defined. + *

+ * Typical usage: + *

    + *
  • Create a ProjectProperties with {@code PropertyType#ANT} + *
  • Merge in values using {@code PropertyType#PROJECT} + *
  • The result is that this contains all the properties from default plus those + * overridden by the build.properties file. + *
+ * + * @param type One the possible {@link ProjectProperties.PropertyType}s. + * @return this object, for chaining. + */ + public synchronized ProjectPropertiesWorkingCopy merge(PropertyType type) { + if (mProjectFolder.exists() && mType != type) { + IAbstractFile propFile = mProjectFolder.getFile(type.getFilename()); + if (propFile.exists()) { + Map map = parsePropertyFile(propFile, null /* log */); + if (map != null) { + for (Entry entry : map.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + if (!mProperties.containsKey(key) && value != null) { + mProperties.put(key, value); + } + } + } + } + } + return this; + } + + + /** + * Saves the property file, using UTF-8 encoding. + * @throws IOException + * @throws StreamException + */ + public synchronized void save() throws IOException, StreamException { + IAbstractFile toSave = mProjectFolder.getFile(mType.getFilename()); + + // write the whole file in a byte array before dumping it in the file. This + // This is so that if the file already existing + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + OutputStreamWriter writer = new OutputStreamWriter(baos, SdkConstants.INI_CHARSET); + + if (toSave.exists()) { + InputStream contentStream = toSave.getContents(); + InputStreamReader isr = null; + BufferedReader reader = null; + + try { + contentStream = toSave.getContents(); + //noinspection IOResourceOpenedButNotSafelyClosed + isr = new InputStreamReader(contentStream, SdkConstants.INI_CHARSET); + //noinspection IOResourceOpenedButNotSafelyClosed + reader = new BufferedReader(isr); + + // since we're reading the existing file and replacing values with new ones, or skipping + // removed values, we need to record what properties have been visited, so that + // we can figure later what new properties need to be added at the end of the file. + Set visitedProps = new HashSet(); + + String line = null; + while ((line = reader.readLine()) != null) { + // check if this is a line containing a property. + if (!line.isEmpty() && line.charAt(0) != '#') { + + Matcher m = PATTERN_PROP.matcher(line); + if (m.matches()) { + String key = m.group(1); + String value = m.group(2); + + // record the prop + visitedProps.add(key); + + // check if this property must be removed. + if (mType.isRemovedProperty(key)) { + value = null; + } else if (mProperties.containsKey(key)) { // if the property still exists. + // put the new value. + value = mProperties.get(key); + } else { + // property doesn't exist. Check if it's a known property. + // if it's a known one, we'll remove it, otherwise, leave it untouched. + if (mType.isKnownProperty(key)) { + value = null; + } + } + + // if the value is still valid, write it down. + if (value != null) { + writeValue(writer, key, value, false /*addComment*/); + } + } else { + // the line was wrong, let's just ignore it so that it's removed from the + // file. + } + } else { + // non-property line: just write the line in the output as-is. + writer.append(line).append('\n'); + } + } + + // now add the new properties. + for (Entry entry : mProperties.entrySet()) { + if (!visitedProps.contains(entry.getKey())) { + String value = entry.getValue(); + if (value != null) { + writeValue(writer, entry.getKey(), value, true /*addComment*/); + } + } + } + } finally { + try { + Closeables.close(reader, true /* swallowIOException */); + } catch (IOException e) { + // cannot happen + } + try { + Closeables.close(isr, true /* swallowIOException */); + } catch (IOException e) { + // cannot happen + } + try { + Closeables.close(contentStream, true /* swallowIOException */); + } catch (IOException e) { + // cannot happen + } + } + + } else { + // new file, just write it all + + // write the header (can be null, for example for PropertyType.LEGACY_BUILD) + if (mType.getHeader() != null) { + writer.write(mType.getHeader()); + } + + // write the properties. + for (Entry entry : mProperties.entrySet()) { + String value = entry.getValue(); + if (value != null) { + writeValue(writer, entry.getKey(), value, true /*addComment*/); + } + } + } + + writer.flush(); + + // now put the content in the file. + OutputStream filestream = toSave.getOutputStream(); + filestream.write(baos.toByteArray()); + filestream.flush(); + filestream.close(); + } + + private void writeValue(OutputStreamWriter writer, String key, String value, + boolean addComment) throws IOException { + if (addComment) { + String comment = COMMENT_MAP.get(key); + if (comment != null) { + writer.write(comment); + } + } + + writer.write(String.format("%s=%s\n", key, escape(value))); + } + + /** + * Private constructor. + *

+ * Use {@link #load(String, PropertyType)} or {@link #create(String, PropertyType)} + * to instantiate. + */ + ProjectPropertiesWorkingCopy(IAbstractFolder projectFolder, Map map, + PropertyType type) { + super(projectFolder, map, type); + } + + @NonNull + public ProjectProperties makeReadOnlyCopy() { + // copy the current properties in a new map + Map propList = new HashMap(mProperties); + + return new ProjectProperties(mProjectFolder, propList, mType); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/AdbWrapper.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/AdbWrapper.java new file mode 100644 index 00000000..ae256eb3 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/AdbWrapper.java @@ -0,0 +1,157 @@ +/* + * 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.sdklib.internal.repository; + +import com.android.SdkConstants; + +import java.io.File; +import java.io.IOException; + +/** + * A lightweight wrapper to start & stop ADB. + * This is specific to the SDK Manager install process. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class AdbWrapper { + + /* + * Note: we could bring ddmlib in SdkManager for that purpose, however this allows us to + * specialize the start/stop methods to our needs (e.g. a task monitor, etc.) + */ + + private final String mAdbOsLocation; + private final ITaskMonitor mMonitor; + + /** + * Creates a new lightweight ADB wrapper. + * + * @param osSdkPath The root OS path of the SDK. Cannot be null. + * @param monitor A logger object. Cannot be null. + */ + public AdbWrapper(String osSdkPath, ITaskMonitor monitor) { + mMonitor = monitor; + + if (!osSdkPath.endsWith(File.separator)) { + osSdkPath += File.separator; + } + mAdbOsLocation = osSdkPath + SdkConstants.OS_SDK_PLATFORM_TOOLS_FOLDER + + SdkConstants.FN_ADB; + } + + private void display(String format, Object...args) { + mMonitor.log(format, args); + } + + private void displayError(String format, Object...args) { + mMonitor.logError(format, args); + } + + /** + * Starts the adb host side server. + * @return true if success + */ + public synchronized boolean startAdb() { + if (mAdbOsLocation == null) { + displayError("Error: missing path to ADB."); //$NON-NLS-1$ + return false; + } + + Process proc; + int status = -1; + + try { + ProcessBuilder processBuilder = new ProcessBuilder( + mAdbOsLocation, + "start-server"); //$NON-NLS-1$ + proc = processBuilder.start(); + status = proc.waitFor(); + + // Implementation note: normally on Windows we need to capture stderr/stdout + // to make sure the process isn't blocked if it's output isn't read. However + // in this case this happens to hang when reading stdout with no proper way + // to properly close the streams. On the other hand the output from start + // server is rather short and not very interesting so we just drop it. + + } catch (IOException ioe) { + displayError("Unable to run 'adb': %1$s.", ioe.getMessage()); //$NON-NLS-1$ + // we'll return false; + } catch (InterruptedException ie) { + displayError("Unable to run 'adb': %1$s.", ie.getMessage()); //$NON-NLS-1$ + // we'll return false; + } + + if (status != 0) { + displayError(String.format( + "Starting ADB server failed (code %d).", //$NON-NLS-1$ + status)); + return false; + } + + display("Starting ADB server succeeded."); //$NON-NLS-1$ + + return true; + } + + /** + * Stops the adb host side server. + * @return true if success + */ + public synchronized boolean stopAdb() { + if (mAdbOsLocation == null) { + displayError("Error: missing path to 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(); + + // See comment in startAdb about not needing/wanting to capture stderr/stdout. + } + catch (IOException ioe) { + // we'll return false; + } + catch (InterruptedException ie) { + // we'll return false; + } + + // adb kill-server returns: + // 0 if adb was running and was correctly killed. + // 1 if adb wasn't running and thus wasn't killed. + // This error case is not worth reporting. + + if (status != 0 && status != 1) { + displayError(String.format( + "Stopping ADB server failed (code %d).", //$NON-NLS-1$ + status)); + return false; + } + + display("Stopping ADB server succeeded."); //$NON-NLS-1$ + return true; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/AddonsListFetcher.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/AddonsListFetcher.java new file mode 100644 index 00000000..816c7f0e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/AddonsListFetcher.java @@ -0,0 +1,614 @@ +/* + * Copyright (C) 2010 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.sdklib.internal.repository; + +import com.android.annotations.VisibleForTesting; +import com.android.annotations.VisibleForTesting.Visibility; +import com.android.io.NonClosingInputStream; +import com.android.io.NonClosingInputStream.CloseBehavior; +import com.android.sdklib.repository.SdkAddonsListConstants; +import com.android.sdklib.repository.SdkRepoConstants; + +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.xml.sax.ErrorHandler; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.net.ssl.SSLKeyException; +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import javax.xml.validation.Validator; + +/** + * Fetches and loads an sdk-addons-list XML. + *

+ * Such an XML contains a simple list of add-ons site that are to be loaded by default by the + * SDK Manager.
+ * The XML must conform to the sdk-addons-list-N.xsd.
+ * Constants used in the XML are defined in {@link SdkAddonsListConstants}. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class AddonsListFetcher { + + public enum SiteType { + ADDON_SITE, + SYS_IMG_SITE + } + + /** + * An immutable structure representing an add-on site. + */ + public static class Site { + private final String mUrl; + private final String mUiName; + private final SiteType mType; + + private Site(String url, String uiName, SiteType type) { + mType = type; + mUrl = url.trim(); + mUiName = uiName; + } + + public String getUrl() { + return mUrl; + } + + public String getUiName() { + return mUiName; + } + + public SiteType getType() { + return mType; + } + + /** Returns a debug string representation of this object. Not for user display. */ + @Override + public String toString() { + return String.format("<%1$s URL='%2$s' Name='%3$s'>", //$NON-NLS-1$ + mType, mUrl, mUiName); + } + } + + /** + * Fetches the addons list from the given URL. + * + * @param url The URL of an XML file resource that conforms to the latest sdk-addons-list-N.xsd. + * For the default operation, use {@link SdkAddonsListConstants#URL_ADDON_LIST}. + * Cannot be null. + * @param cache The {@link DownloadCache} instance to use. Cannot be null. + * @param monitor A monitor to report errors. Cannot be null. + * @return An array of {@link Site} on success (possibly empty), or null on error. + */ + public Site[] fetch(String url, DownloadCache cache, ITaskMonitor monitor) { + + url = url == null ? "" : url.trim(); + + monitor.setProgressMax(6); + monitor.setDescription("Fetching %1$s", url); + monitor.incProgress(1); + + Exception[] exception = new Exception[] { null }; + Boolean[] validatorFound = new Boolean[] { Boolean.FALSE }; + String[] validationError = new String[] { null }; + Document validatedDoc = null; + String validatedUri = null; + + String[] defaultNames = new String[SdkAddonsListConstants.NS_LATEST_VERSION]; + for (int version = SdkAddonsListConstants.NS_LATEST_VERSION, i = 0; + version >= 1; + version--, i++) { + defaultNames[i] = SdkAddonsListConstants.getDefaultName(version); + } + + InputStream xml = fetchXmlUrl(url, cache, monitor.createSubMonitor(1), exception); + if (xml != null) { + int version = getXmlSchemaVersion(xml); + if (version == 0) { + closeStream(xml); + xml = null; + } + } + + String baseUrl = url; + if (!baseUrl.endsWith("/")) { //$NON-NLS-1$ + int pos = baseUrl.lastIndexOf('/'); + if (pos > 0) { + baseUrl = baseUrl.substring(0, pos + 1); + } + } + + // If we can't find the latest version, try earlier schema versions. + if (xml == null && defaultNames.length > 0) { + ITaskMonitor subMonitor = monitor.createSubMonitor(1); + subMonitor.setProgressMax(defaultNames.length); + + for (String name : defaultNames) { + String newUrl = baseUrl + name; + if (newUrl.equals(url)) { + continue; + } + xml = fetchXmlUrl(newUrl, cache, subMonitor.createSubMonitor(1), exception); + if (xml != null) { + int version = getXmlSchemaVersion(xml); + if (version == 0) { + closeStream(xml); + xml = null; + } else { + url = newUrl; + subMonitor.incProgress( + subMonitor.getProgressMax() - subMonitor.getProgress()); + break; + } + } + } + } else { + monitor.incProgress(1); + } + + if (xml != null) { + monitor.setDescription("Validate XML"); + + // Explore the XML to find the potential XML schema version + int version = getXmlSchemaVersion(xml); + + if (version >= 1 && version <= SdkAddonsListConstants.NS_LATEST_VERSION) { + // This should be a version we can handle. Try to validate it + // and report any error as invalid XML syntax, + + String uri = validateXml(xml, url, version, validationError, validatorFound); + if (uri != null) { + // Validation was successful + validatedDoc = getDocument(xml, monitor); + validatedUri = uri; + + } + } else if (version > SdkAddonsListConstants.NS_LATEST_VERSION) { + // The schema used is more recent than what is supported by this tool. + // We don't have an upgrade-path support yet, so simply ignore the document. + closeStream(xml); + return null; + } + } + + // If any exception was handled during the URL fetch, display it now. + if (exception[0] != null) { + String reason = null; + if (exception[0] instanceof FileNotFoundException) { + // FNF has no useful getMessage, so we need to special handle it. + reason = "File not found"; + } else if (exception[0] instanceof UnknownHostException && + exception[0].getMessage() != null) { + // This has no useful getMessage yet could really use one + reason = String.format("Unknown Host %1$s", exception[0].getMessage()); + } else if (exception[0] instanceof SSLKeyException) { + // That's a common error and we have a pref for it. + reason = "HTTPS SSL error. You might want to force download through HTTP in the settings."; + } else if (exception[0].getMessage() != null) { + reason = exception[0].getMessage(); + } else { + // We don't know what's wrong. Let's give the exception class at least. + reason = String.format("Unknown (%1$s)", exception[0].getClass().getName()); + } + + monitor.logError("Failed to fetch URL %1$s, reason: %2$s", url, reason); + } + + if (validationError[0] != null) { + monitor.logError("%s", validationError[0]); //$NON-NLS-1$ + } + + // Stop here if we failed to validate the XML. We don't want to load it. + if (validatedDoc == null) { + closeStream(xml); + return null; + } + + monitor.incProgress(1); + + Site[] result = null; + + if (xml != null) { + monitor.setDescription("Parse XML"); + monitor.incProgress(1); + result = parseAddonsList(validatedDoc, validatedUri, baseUrl, monitor); + } + + // done + monitor.incProgress(1); + + closeStream(xml); + return result; + } + + /** + * Fetches the document at the given URL and returns it as a stream. Returns + * null if anything wrong happens. + * + * @param urlString The URL to load, as a string. + * @param monitor {@link ITaskMonitor} related to this URL. + * @param outException If non null, where to store any exception that + * happens during the fetch. + * @see UrlOpener UrlOpener, which handles all URL logic. + */ + private InputStream fetchXmlUrl(String urlString, + DownloadCache cache, + ITaskMonitor monitor, + Exception[] outException) { + try { + InputStream xml = cache.openCachedUrl(urlString, monitor); + if (xml != null) { + xml.mark(500000); + xml = new NonClosingInputStream(xml); + ((NonClosingInputStream) xml).setCloseBehavior(CloseBehavior.RESET); + } + return xml; + } catch (Exception e) { + if (outException != null) { + outException[0] = e; + } + } + + return null; + } + + /** + * Closes the stream, ignore any exception from InputStream.close(). + * If the stream is a NonClosingInputStream, sets it to CloseBehavior.CLOSE first. + */ + private void closeStream(InputStream is) { + if (is != null) { + if (is instanceof NonClosingInputStream) { + ((NonClosingInputStream) is).setCloseBehavior(CloseBehavior.CLOSE); + } + try { + is.close(); + } catch (IOException ignore) {} + } + } + + /** + * Manually parses the root element of the XML to extract the schema version + * at the end of the xmlns:sdk="http://schemas.android.com/sdk/android/addons-list/$N" + * declaration. + * + * @return 1..{@link SdkAddonsListConstants#NS_LATEST_VERSION} for a valid schema version + * or 0 if no schema could be found. + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected int getXmlSchemaVersion(InputStream xml) { + if (xml == null) { + return 0; + } + + // Get an XML document + Document doc = null; + try { + assert xml.markSupported(); + xml.reset(); + + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setIgnoringComments(false); + factory.setValidating(false); + + // Parse the old document using a non namespace aware builder + factory.setNamespaceAware(false); + DocumentBuilder builder = factory.newDocumentBuilder(); + + // We don't want the default handler which prints errors to stderr. + builder.setErrorHandler(new ErrorHandler() { + @Override + public void warning(SAXParseException e) throws SAXException { + // pass + } + @Override + public void fatalError(SAXParseException e) throws SAXException { + throw e; + } + @Override + public void error(SAXParseException e) throws SAXException { + throw e; + } + }); + + doc = builder.parse(xml); + + // Prepare a new document using a namespace aware builder + factory.setNamespaceAware(true); + builder = factory.newDocumentBuilder(); + + } catch (Exception e) { + // Failed to reset XML stream + // Failed to get builder factor + // Failed to create XML document builder + // Failed to parse XML document + // Failed to read XML document + //--For debug--System.err.println("getXmlSchemaVersion exception: " + e.toString()); + } + + if (doc == null) { + return 0; + } + + // Check the root element is an XML with at least the following properties: + // + // + // Note that we don't have namespace support enabled, we just do it manually. + + Pattern nsPattern = Pattern.compile(SdkAddonsListConstants.NS_PATTERN); + + String prefix = null; + for (Node child = doc.getFirstChild(); child != null; child = child.getNextSibling()) { + if (child.getNodeType() == Node.ELEMENT_NODE) { + prefix = null; + String name = child.getNodeName(); + int pos = name.indexOf(':'); + if (pos > 0 && pos < name.length() - 1) { + prefix = name.substring(0, pos); + name = name.substring(pos + 1); + } + if (SdkAddonsListConstants.NODE_SDK_ADDONS_LIST.equals(name)) { + NamedNodeMap attrs = child.getAttributes(); + String xmlns = "xmlns"; //$NON-NLS-1$ + if (prefix != null) { + xmlns += ":" + prefix; //$NON-NLS-1$ + } + Node attr = attrs.getNamedItem(xmlns); + if (attr != null) { + String uri = attr.getNodeValue(); + if (uri != null) { + Matcher m = nsPattern.matcher(uri); + if (m.matches()) { + String version = m.group(1); + try { + return Integer.parseInt(version); + } catch (NumberFormatException e) { + return 0; + } + } + } + } + } + } + } + + return 0; + } + + /** + * Validates this XML against one of the requested SDK Repository schemas. + * If the XML was correctly validated, returns the schema that worked. + * If it doesn't validate, returns null and stores the error in outError[0]. + * If we can't find a validator, returns null and set validatorFound[0] to false. + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected String validateXml(InputStream xml, String url, int version, + String[] outError, Boolean[] validatorFound) { + + if (xml == null) { + return null; + } + + try { + Validator validator = getValidator(version); + + if (validator == null) { + validatorFound[0] = Boolean.FALSE; + outError[0] = String.format( + "XML verification failed for %1$s.\nNo suitable XML Schema Validator could be found in your Java environment. Please consider updating your version of Java.", + url); + return null; + } + + validatorFound[0] = Boolean.TRUE; + + // Reset the stream if it supports that operation. + assert xml.markSupported(); + xml.reset(); + + // Validation throws a bunch of possible Exceptions on failure. + validator.validate(new StreamSource(xml)); + return SdkAddonsListConstants.getSchemaUri(version); + + } catch (SAXParseException e) { + outError[0] = String.format( + "XML verification failed for %1$s.\nLine %2$d:%3$d, Error: %4$s", + url, + e.getLineNumber(), + e.getColumnNumber(), + e.toString()); + + } catch (Exception e) { + outError[0] = String.format( + "XML verification failed for %1$s.\nError: %2$s", + url, + e.toString()); + } + return null; + } + + /** + * Helper method that returns a validator for our XSD, or null if the current Java + * implementation can't process XSD schemas. + * + * @param version The version of the XML Schema. + * See {@link SdkAddonsListConstants#getXsdStream(int)} + */ + private Validator getValidator(int version) throws SAXException { + InputStream xsdStream = SdkAddonsListConstants.getXsdStream(version); + SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + + if (factory == null) { + return null; + } + + // This may throw a SAX Exception if the schema itself is not a valid XSD + Schema schema = factory.newSchema(new StreamSource(xsdStream)); + + Validator validator = schema == null ? null : schema.newValidator(); + + return validator; + } + + /** + * Takes an XML document as a string as parameter and returns a DOM for it. + * + * On error, returns null and prints a (hopefully) useful message on the monitor. + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected Document getDocument(InputStream xml, ITaskMonitor monitor) { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setIgnoringComments(true); + factory.setNamespaceAware(true); + + DocumentBuilder builder = factory.newDocumentBuilder(); + assert xml.markSupported(); + xml.reset(); + Document doc = builder.parse(new InputSource(xml)); + + return doc; + } catch (ParserConfigurationException e) { + monitor.logError("Failed to create XML document builder"); + + } catch (SAXException e) { + monitor.logError("Failed to parse XML document"); + + } catch (IOException e) { + monitor.logError("Failed to read XML document"); + } + + return null; + } + + /** + * Parse all sites defined in the Addons list XML and returns an array of sites. + * + * @param doc The XML DOM to parse. + * @param nsUri The addons-list schema URI of the document. + * @param baseUrl The base URL of the caller (e.g. where addons-list-N.xml was fetched from.) + * @param monitor A non-null monitor to print to. + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected Site[] parseAddonsList( + Document doc, + String nsUri, + String baseUrl, + ITaskMonitor monitor) { + + String testBaseUrl = System.getenv("SDK_TEST_BASE_URL"); //$NON-NLS-1$ + if (testBaseUrl != null) { + if (testBaseUrl.length() <= 0 || !testBaseUrl.endsWith("/")) { //$NON-NLS-1$ + testBaseUrl = null; + } + } + + Node root = getFirstChild(doc, nsUri, SdkAddonsListConstants.NODE_SDK_ADDONS_LIST); + if (root != null) { + ArrayList sites = new ArrayList(); + + for (Node child = root.getFirstChild(); + child != null; + child = child.getNextSibling()) { + if (child.getNodeType() == Node.ELEMENT_NODE && + nsUri.equals(child.getNamespaceURI())) { + + String elementName = child.getLocalName(); + SiteType type = null; + + if (SdkAddonsListConstants.NODE_SYS_IMG_SITE.equals(elementName)) { + type = SiteType.SYS_IMG_SITE; + + } else if (SdkAddonsListConstants.NODE_ADDON_SITE.equals(elementName)) { + type = SiteType.ADDON_SITE; + } + + // Not an addon-site nor a sys-img-site, don't process this. + if (type == null) { + continue; + } + + Node url = getFirstChild(child, nsUri, SdkAddonsListConstants.NODE_URL); + Node name = getFirstChild(child, nsUri, SdkAddonsListConstants.NODE_NAME); + + if (name != null && url != null) { + String strUrl = url.getTextContent().trim(); + String strName = name.getTextContent().trim(); + + if (testBaseUrl != null && + strUrl.startsWith(SdkRepoConstants.URL_GOOGLE_SDK_SITE)) { + strUrl = testBaseUrl + + strUrl.substring(SdkRepoConstants.URL_GOOGLE_SDK_SITE.length()); + } else if (!strUrl.startsWith("http://") && //$NON-NLS-1$ + !strUrl.startsWith("https://")) { //$NON-NLS-1$ + // This looks like a relative URL, add the fetcher's base URL to it. + strUrl = baseUrl + strUrl; + } + + if (!strUrl.isEmpty() && !strName.isEmpty()) { + sites.add(new Site(strUrl, strName, type)); + } + } + } + } + + return sites.toArray(new Site[sites.size()]); + } + + return null; + } + + /** + * Returns the first child element with the given XML local name. + * If xmlLocalName is null, returns the very first child element. + */ + private Node getFirstChild(Node node, String nsUri, String xmlLocalName) { + + for(Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) { + if (child.getNodeType() == Node.ELEMENT_NODE && + nsUri.equals(child.getNamespaceURI())) { + if (xmlLocalName == null || child.getLocalName().equals(xmlLocalName)) { + return child; + } + } + } + + return null; + } + + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/CanceledByUserException.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/CanceledByUserException.java new file mode 100644 index 00000000..64ed8b12 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/CanceledByUserException.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2011 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.sdklib.internal.repository; + +/** + * Exception thrown by {@link DownloadCache} and {@link UrlOpener} when a user + * cancels an HTTP Basic authentication or NTML authentication dialog. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class CanceledByUserException extends Exception { + private static final long serialVersionUID = -7669346110926032403L; + + public CanceledByUserException(String message) { + super(message); + } +} + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/DownloadCache.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/DownloadCache.java new file mode 100644 index 00000000..274c64ce --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/DownloadCache.java @@ -0,0 +1,848 @@ +/* + * Copyright (C) 2012 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.sdklib.internal.repository; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.annotations.VisibleForTesting; +import com.android.annotations.VisibleForTesting.Visibility; +import com.android.prefs.AndroidLocation; +import com.android.prefs.AndroidLocation.AndroidLocationException; +import com.android.sdklib.io.LegacyFileOp; +import com.android.sdklib.io.IFileOp; +import com.android.utils.Pair; + +import org.apache.http.Header; +import org.apache.http.HttpHeaders; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.message.BasicHeader; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; + + +/** + * A simple cache for the XML resources handled by the SDK Manager. + *

+ * Callers should use {@link #openDirectUrl} to download "large files" + * that should not be cached (like actual installation packages which are several MBs big) + * and call {@link #openCachedUrl(String, ITaskMonitor)} to download small XML files. + *

+ * The cache can work in 3 different strategies (direct is a pass-through, fresh-cache is the + * default and tries to update resources if they are older than 10 minutes by respecting + * either ETag or Last-Modified, and finally server-cache is a strategy to always serve + * cached entries if present.) + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class DownloadCache { + + /* + * HTTP/1.1 references: + * - Possible headers: + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html + * - Rules about conditional requests: + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4 + * - Error codes: + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1.1 + */ + + private static final boolean DEBUG = System.getenv("SDKMAN_DEBUG_CACHE") != null; //$NON-NLS-1$ + + /** Key for the Status-Code in the info properties. */ + private static final String KEY_STATUS_CODE = "Status-Code"; //$NON-NLS-1$ + /** Key for the URL in the info properties. */ + private static final String KEY_URL = "URL"; //$NON-NLS-1$ + + /** Prefix of binary files stored in the {@link SdkConstants#FD_CACHE} directory. */ + private static final String BIN_FILE_PREFIX = "sdkbin"; //$NON-NLS-1$ + /** Prefix of meta info files stored in the {@link SdkConstants#FD_CACHE} directory. */ + private static final String INFO_FILE_PREFIX = "sdkinf"; //$NON-NLS-1$ + /* Revision suffixed to the prefix. */ + private static final String REV_FILE_PREFIX = "-1_"; //$NON-NLS-1$ + + /** + * Minimum time before we consider a cached entry is potentially stale. + * Expressed in milliseconds. + *

+ * When using the {@link Strategy#FRESH_CACHE}, the cache will not try to refresh + * a cached file if it's has been saved more recently than this time. + * When using the direct mode or the serve mode, the cache either doesn't serve + * cached files or always serves caches files so this expiration delay is not used. + *

+ * Default is 10 minutes. + *

+ * TODO: change for a dynamic preference later. + */ + private static final long MIN_TIME_EXPIRED_MS = 10*60*1000; + /** + * Maximum time before we consider a cache entry to be stale. + * Expressed in milliseconds. + *

+ * When using the {@link Strategy#FRESH_CACHE}, entries that have no ETag + * or Last-Modified will be refreshed if their file timestamp is older than + * this value. + *

+ * Default is 4 hours. + *

+ * TODO: change for a dynamic preference later. + */ + private static final long MAX_TIME_EXPIRED_MS = 4*60*60*1000; + + /** + * The maximum file size we'll cache for "small" files. + * 640KB is more than enough and is already a stretch since these are read in memory. + * (The actual typical size of the files handled here is in the 4-64KB range.) + */ + private static final int MAX_SMALL_FILE_SIZE = 640 * 1024; + + /** + * HTTP Headers that are saved in an info file. + * For HTTP/1.1 header names, see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html + */ + private static final String[] INFO_HTTP_HEADERS = { + HttpHeaders.LAST_MODIFIED, + HttpHeaders.ETAG, + HttpHeaders.CONTENT_LENGTH, + HttpHeaders.DATE + }; + + private final IFileOp mFileOp; + private final File mCacheRoot; + private final Strategy mStrategy; + + public enum Strategy { + /** + * Exclusively serves data from the cache. If files are available in the + * cache, serve them as is (without trying to refresh them). If files are + * not available, they are not fetched at all. + */ + ONLY_CACHE, + /** + * If the files are available in the cache, serve them as-is, otherwise + * download them and return the cached version. No expiration or refresh + * is attempted if a file is in the cache. + */ + SERVE_CACHE, + /** + * If the files are available in the cache, check if there's an update + * (either using an e-tag check or comparing to the default time expiration). + * If files have expired or are not in the cache then download them and return + * the cached version. + */ + FRESH_CACHE, + /** + * Disables caching. URLs are always downloaded and returned directly. + * Downloaded streams aren't cached locally. + */ + DIRECT + } + + /** Creates a default instance of the URL cache */ + public DownloadCache(@NonNull Strategy strategy) { + this(new LegacyFileOp(), strategy); + } + + /** Creates a default instance of the URL cache */ + public DownloadCache(@NonNull IFileOp LegacyFileOp, @NonNull Strategy strategy) { + mFileOp = LegacyFileOp; + mCacheRoot = initCacheRoot(); + + // If this is defined in the environment, never use the cache. Useful for testing. + if (System.getenv("SDKMAN_DISABLE_CACHE") != null) { //$NON-NLS-1$ + strategy = Strategy.DIRECT; + } + + mStrategy = mCacheRoot == null ? Strategy.DIRECT : strategy; + } + + @NonNull + public Strategy getStrategy() { + return mStrategy; + } + + @Nullable + public File getCacheRoot() { + return mCacheRoot; + } + + /** + * Computes the size of the cached files. + * + * @return The sum of the byte size of the cached files. + */ + public long getCurrentSize() { + long size = 0; + + if (mCacheRoot != null) { + File[] files = mFileOp.listFiles(mCacheRoot); + for (File f : files) { + if (mFileOp.isFile(f)) { + String name = f.getName(); + if (name.startsWith(BIN_FILE_PREFIX) || name.startsWith(INFO_FILE_PREFIX)) { + size += f.length(); + } + } + } + } + + return size; + } + + /** + * Removes all cached files from the cache directory. + */ + public void clearCache() { + if (mCacheRoot != null) { + File[] files = mFileOp.listFiles(mCacheRoot); + for (File f : files) { + if (mFileOp.isFile(f)) { + String name = f.getName(); + if (name.startsWith(BIN_FILE_PREFIX) || name.startsWith(INFO_FILE_PREFIX)) { + mFileOp.delete(f); + } + } + } + } + } + + /** + * Removes all obsolete cached files from the cache directory + * that do not match the latest revision. + */ + public void clearOldCache() { + String prefix1 = BIN_FILE_PREFIX + REV_FILE_PREFIX; + String prefix2 = INFO_FILE_PREFIX + REV_FILE_PREFIX; + if (mCacheRoot != null) { + File[] files = mFileOp.listFiles(mCacheRoot); + for (File f : files) { + if (mFileOp.isFile(f)) { + String name = f.getName(); + if (name.startsWith(BIN_FILE_PREFIX) || + name.startsWith(INFO_FILE_PREFIX)) { + if (!name.startsWith(prefix1) && !name.startsWith(prefix2)) { + mFileOp.delete(f); + } + } + } + } + } + } + + /** + * Returns the directory to be used as a cache. + * Creates it if necessary. + * Makes it possible to disable or override the cache location in unit tests. + * + * @return An existing directory to use as a cache root dir, + * or null in case of error in which case the cache will be disabled. + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + @Nullable + protected File initCacheRoot() { + try { + File root = new File(AndroidLocation.getFolder()); + root = new File(root, SdkConstants.FD_CACHE); + if (!mFileOp.exists(root)) { + mFileOp.mkdirs(root); + } + return root; + } catch (AndroidLocationException e) { + // No root? Disable the cache. + return null; + } + } + + /** + * Calls {@link UrlOpener#openUrl(String, boolean, ITaskMonitor, Header[])} + * to actually perform a download. + *

+ * Isolated so that it can be overridden by unit tests. + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + @NonNull + protected Pair openUrl( + @NonNull String url, + boolean needsMarkResetSupport, + @NonNull ITaskMonitor monitor, + @Nullable Header[] headers) throws IOException, CanceledByUserException { + return UrlOpener.openUrl(url, needsMarkResetSupport, monitor, headers); + } + + + /** + * Does a direct download of the given URL using {@link UrlOpener}. + * This does not check the download cache and does not attempt to cache the file. + * Instead the HttpClient library returns a progressive download stream. + *

+ * For details on realm authentication and user/password handling, + * check the underlying {@link UrlOpener#openUrl(String, boolean, ITaskMonitor, Header[])} + * documentation. + *

+ * The resulting input stream may not support mark/reset. + * + * @param urlString the URL string to be opened. + * @param headers An optional set of headers to pass when requesting the resource. Can be null. + * @param monitor {@link ITaskMonitor} which is related to this URL + * fetching. + * @return Returns a pair with a {@link InputStream} and an {@link HttpResponse}. + * The pair is never null. + * The input stream can be null in case of error, although in general the + * method will probably throw an exception instead. + * The caller should look at the response code's status and only accept the + * input stream if it's the desired code (e.g. 200 or 206). + * @throws IOException Exception thrown when there are problems retrieving + * the URL or its content. + * @throws CanceledByUserException Exception thrown if the user cancels the + * authentication dialog. + */ + @NonNull + public Pair openDirectUrl( + @NonNull String urlString, + @Nullable Header[] headers, + @NonNull ITaskMonitor monitor) + throws IOException, CanceledByUserException { + if (DEBUG) { + System.out.println(String.format("%s : Direct download", urlString)); //$NON-NLS-1$ + } + return openUrl( + urlString, + false /*needsMarkResetSupport*/, + monitor, + headers); + } + + /** + * This is a simplified convenience method that calls + * {@link #openDirectUrl(String, Header[], ITaskMonitor)} + * without passing any specific HTTP headers and returns the resulting input stream + * and the HTTP status code. + * See the original method's description for details on its behavior. + *

+ * {@link #openDirectUrl(String, Header[], ITaskMonitor)} can accept customized + * HTTP headers to send with the requests and also returns the full HTTP + * response -- status line with code and protocol and all headers. + *

+ * The resulting input stream may not support mark/reset. + * + * @param urlString the URL string to be opened. + * @param monitor {@link ITaskMonitor} which is related to this URL + * fetching. + * @return Returns a pair with a {@link InputStream} and an HTTP status code. + * The pair is never null. + * The input stream can be null in case of error, although in general the + * method will probably throw an exception instead. + * The caller should look at the response code's status and only accept the + * input stream if it's the desired code (e.g. 200 or 206). + * @throws IOException Exception thrown when there are problems retrieving + * the URL or its content. + * @throws CanceledByUserException Exception thrown if the user cancels the + * authentication dialog. + * @see #openDirectUrl(String, Header[], ITaskMonitor) + */ + @NonNull + public Pair openDirectUrl( + @NonNull String urlString, + @NonNull ITaskMonitor monitor) + throws IOException, CanceledByUserException { + if (DEBUG) { + System.out.println(String.format("%s : Direct download", urlString)); //$NON-NLS-1$ + } + Pair result = openUrl( + urlString, + false /*needsMarkResetSupport*/, + monitor, + null /*headers*/); + return Pair.of(result.getFirst(), result.getSecond().getStatusLine().getStatusCode()); + } + + /** + * Downloads a small file, typically XML manifests. + * The current {@link Strategy} governs whether the file is served as-is + * from the cache, potentially updated first or directly downloaded. + *

+ * For large downloads (e.g. installable archives) please do not invoke the + * cache and instead use the {@link #openDirectUrl} method. + *

+ * For details on realm authentication and user/password handling, + * check the underlying {@link UrlOpener#openUrl(String, boolean, ITaskMonitor, Header[])} + * documentation. + * + * @param urlString the URL string to be opened. + * @param monitor {@link ITaskMonitor} which is related to this URL + * fetching. + * @return Returns an {@link InputStream} holding the URL content. + * Returns null if there's no content (e.g. resource not found.) + * Returns null if the document is not cached and strategy is {@link Strategy#ONLY_CACHE}. + * @throws IOException Exception thrown when there are problems retrieving + * the URL or its content. + * @throws CanceledByUserException Exception thrown if the user cancels the + * authentication dialog. + */ + @NonNull + public InputStream openCachedUrl(@NonNull String urlString, @NonNull ITaskMonitor monitor) + throws IOException, CanceledByUserException { + // Don't cache in direct mode. + if (mStrategy == Strategy.DIRECT) { + Pair result = openUrl( + urlString, + true /*needsMarkResetSupport*/, + monitor, + null /*headers*/); + return result.getFirst(); + } + + File cached = new File(mCacheRoot, getCacheFilename(urlString)); + File info = new File(mCacheRoot, getInfoFilename(cached.getName())); + + boolean useCached = mFileOp.exists(cached); + + if (useCached && mStrategy == Strategy.FRESH_CACHE) { + // Check whether the file should be served from the cache or + // refreshed first. + + long cacheModifiedMs = mFileOp.lastModified(cached); /* last mod time in epoch/millis */ + boolean checkCache = true; + + Properties props = readInfo(info); + if (props == null) { + // No properties, no chocolate for you. + useCached = false; + } else { + long minExpiration = System.currentTimeMillis() - MIN_TIME_EXPIRED_MS; + checkCache = cacheModifiedMs < minExpiration; + + if (!checkCache && DEBUG) { + System.out.println(String.format( + "%s : Too fresh [%,d ms], not checking yet.", //$NON-NLS-1$ + urlString, cacheModifiedMs - minExpiration)); + } + } + + if (useCached && checkCache) { + assert props != null; + + // Right now we only support 200 codes and will requery all 404s. + String code = props.getProperty(KEY_STATUS_CODE, ""); //$NON-NLS-1$ + useCached = Integer.toString(HttpStatus.SC_OK).equals(code); + + if (!useCached && DEBUG) { + System.out.println(String.format( + "%s : cache disabled by code %s", //$NON-NLS-1$ + urlString, code)); + } + + if (useCached) { + // Do we have a valid Content-Length? If so, it should match the file size. + try { + long length = Long.parseLong(props.getProperty(HttpHeaders.CONTENT_LENGTH, + "-1")); //$NON-NLS-1$ + if (length >= 0) { + useCached = length == mFileOp.length(cached); + + if (!useCached && DEBUG) { + System.out.println(String.format( + "%s : cache disabled by length mismatch %d, expected %d", //$NON-NLS-1$ + urlString, length, cached.length())); + } + } + } catch (NumberFormatException ignore) {} + } + + if (useCached) { + // Do we have an ETag and/or a Last-Modified? + String etag = props.getProperty(HttpHeaders.ETAG); + String lastMod = props.getProperty(HttpHeaders.LAST_MODIFIED); + + if (etag != null || lastMod != null) { + // Details on how to use them is defined at + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4 + // Bottom line: + // - if there's an ETag, it should be used first with an + // If-None-Match header. That's a strong comparison for HTTP/1.1 servers. + // - otherwise use a Last-Modified if an If-Modified-Since header exists. + // In this case, we place both and the rules indicates a spec-abiding + // server should strongly match ETag and weakly the Modified-Since. + + // TODO there are some servers out there which report ETag/Last-Mod + // yet don't honor them when presented with a precondition. In this + // case we should identify it in the reply and invalidate ETag support + // for these servers and instead fallback on the pure-timeout case below. + + AtomicInteger statusCode = new AtomicInteger(0); + InputStream is = null; + List

headers = new ArrayList
(2); + + if (etag != null) { + headers.add(new BasicHeader(HttpHeaders.IF_NONE_MATCH, etag)); + } + + if (lastMod != null) { + headers.add(new BasicHeader(HttpHeaders.IF_MODIFIED_SINCE, lastMod)); + } + + if (!headers.isEmpty()) { + is = downloadAndCache(urlString, monitor, cached, info, + headers.toArray(new Header[headers.size()]), + statusCode); + } + + if (is != null && statusCode.get() == HttpStatus.SC_OK) { + // The resource was modified, the server said there was something + // new, which has been cached. We can return that to the caller. + return is; + } + + // If we get here, we should have is == null and code + // could be: + // - 304 for not-modified -- same resource, still available, in + // which case we'll use the cached one. + // - 404 -- resource doesn't exist anymore in which case there's + // no point in retrying. + // - For any other code, just retry a download. + + if (is != null) { + try { + is.close(); + } catch (Exception ignore) {} + is = null; + } + + if (statusCode.get() == HttpStatus.SC_NOT_MODIFIED) { + // Cached file was not modified. + // Change its timestamp for the next MIN_TIME_EXPIRED_MS check. + cached.setLastModified(System.currentTimeMillis()); + + // At this point useCached==true so we'll return + // the cached file below. + } else { + // URL fetch returned something other than 200 or 304. + // For 404, we're done, no need to check the server again. + // For all other codes, we'll retry a download below. + useCached = false; + if (statusCode.get() == HttpStatus.SC_NOT_FOUND) { + return null; + } + } + } else { + // If we don't have an Etag nor Last-Modified, let's use a + // basic file timestamp and compare to a 1 hour threshold. + + long maxExpiration = System.currentTimeMillis() - MAX_TIME_EXPIRED_MS; + useCached = cacheModifiedMs >= maxExpiration; + + if (!useCached && DEBUG) { + System.out.println(String.format( + "[%1$s] cache disabled by timestamp %2$tD %2$tT < %3$tD %3$tT", //$NON-NLS-1$ + urlString, cacheModifiedMs, maxExpiration)); + } + } + } + } + } + + if (useCached) { + // The caller needs an InputStream that supports the reset() operation. + // The default FileInputStream does not, so load the file into a byte + // array and return that. + try { + InputStream is = readCachedFile(cached); + if (is != null) { + if (DEBUG) { + System.out.println(String.format("%s : Use cached file", urlString)); //$NON-NLS-1$ + } + + return is; + } + } catch (IOException ignore) {} + } + + if (!useCached && mStrategy == Strategy.ONLY_CACHE) { + // We don't have a document to serve from the cache. + if (DEBUG) { + System.out.println(String.format("%s : file not in cache", urlString)); //$NON-NLS-1$ + } + return null; + } + + // If we're not using the cache, try to remove the cache and download again. + try { + mFileOp.delete(cached); + mFileOp.delete(info); + } catch (SecurityException ignore) {} + + return downloadAndCache(urlString, monitor, cached, info, + null /*headers*/, null /*statusCode*/); + } + + + + // -------------- + + @Nullable + private InputStream readCachedFile(@NonNull File cached) throws IOException { + InputStream is = null; + + int inc = 65536; + int curr = 0; + long len = cached.length(); + assert len < Integer.MAX_VALUE; + if (len >= MAX_SMALL_FILE_SIZE) { + // This is supposed to cache small files, not 2+ GB files. + return null; + } + byte[] result = new byte[(int) (len > 0 ? len : inc)]; + + try { + is = mFileOp.newFileInputStream(cached); + + int n; + while ((n = is.read(result, curr, result.length - curr)) != -1) { + curr += n; + if (curr == result.length) { + byte[] temp = new byte[curr + inc]; + System.arraycopy(result, 0, temp, 0, curr); + result = temp; + } + } + + return new ByteArrayInputStream(result, 0, curr); + + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException ignore) {} + } + } + } + + /** + * Download, cache and return as an in-memory byte stream. + * The download is only done if the server returns 200/OK. + * On success, store an info file next to the download with + * a few headers. + *

+ * This method deletes the cached file and the info file ONLY if it + * attempted a download and it failed to complete. It doesn't erase + * anything if there's no download because the server returned a 404 + * or 304 or similar. + * + * @return An in-memory byte buffer input stream for the downloaded + * and locally cached file, or null if nothing was downloaded + * (including if it was a 304 Not-Modified status code.) + */ + @Nullable + private InputStream downloadAndCache( + @NonNull String urlString, + @NonNull ITaskMonitor monitor, + @NonNull File cached, + @NonNull File info, + @Nullable Header[] headers, + @Nullable AtomicInteger outStatusCode) + throws FileNotFoundException, IOException, CanceledByUserException { + InputStream is = null; + OutputStream os = null; + + int inc = 65536; + int curr = 0; + byte[] result = new byte[inc]; + + try { + Pair r = + openUrl(urlString, true /*needsMarkResetSupport*/, monitor, headers); + + is = r.getFirst(); + HttpResponse response = r.getSecond(); + + if (DEBUG) { + System.out.println(String.format("%s : fetch: %s => %s", //$NON-NLS-1$ + urlString, + headers == null ? "" : Arrays.toString(headers), //$NON-NLS-1$ + response.getStatusLine())); + } + + int code = response.getStatusLine().getStatusCode(); + + if (outStatusCode != null) { + outStatusCode.set(code); + } + + if (code != HttpStatus.SC_OK) { + // Only a 200 response code makes sense here. + // Even the other 20x codes should not apply, e.g. no content or partial + // content are not statuses we want to handle and should never happen. + // (see http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1.1 for list) + return null; + } + + os = mFileOp.newFileOutputStream(cached); + + int n; + while ((n = is.read(result, curr, result.length - curr)) != -1) { + if (os != null && n > 0) { + os.write(result, curr, n); + } + + curr += n; + + if (os != null && curr > MAX_SMALL_FILE_SIZE) { + // If the file size exceeds our "small file size" threshold, + // stop caching. We don't want to fill the disk. + try { + os.close(); + } catch (IOException ignore) {} + try { + cached.delete(); + info.delete(); + } catch (SecurityException ignore) {} + os = null; + } + if (curr == result.length) { + byte[] temp = new byte[curr + inc]; + System.arraycopy(result, 0, temp, 0, curr); + result = temp; + } + } + + // Close the output stream, signaling it was stored properly. + if (os != null) { + try { + os.close(); + os = null; + + saveInfo(urlString, response, info); + } catch (IOException ignore) {} + } + + return new ByteArrayInputStream(result, 0, curr); + + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException ignore) {} + } + if (os != null) { + try { + os.close(); + } catch (IOException ignore) {} + // If we get here with the output stream not null, it means there + // was an issue and we don't want to keep that file. We'll try to + // delete it. + try { + mFileOp.delete(cached); + mFileOp.delete(info); + } catch (SecurityException ignore) {} + } + } + } + + /** + * Saves part of the HTTP Response to the info file. + */ + private void saveInfo( + @NonNull String urlString, + @NonNull HttpResponse response, + @NonNull File info) throws IOException { + Properties props = new Properties(); + + // we don't need the status code & URL right now. + // Save it in case we want to have it later (e.g. to differentiate 200 and 404.) + props.setProperty(KEY_URL, urlString); + props.setProperty(KEY_STATUS_CODE, + Integer.toString(response.getStatusLine().getStatusCode())); + + for (String name : INFO_HTTP_HEADERS) { + Header h = response.getFirstHeader(name); + if (h != null) { + props.setProperty(name, h.getValue()); + } + } + + mFileOp.saveProperties(info, props, "## Meta data for SDK Manager cache. Do not modify."); //$NON-NLS-1$ + } + + /** + * Reads the info properties file. + * @return The properties found or null if there's no file or it can't be read. + */ + @Nullable + private Properties readInfo(@NonNull File info) { + if (mFileOp.exists(info)) { + return mFileOp.loadProperties(info); + } + return null; + } + + /** + * Computes the cache filename for the given URL. + * The filename uses the {@link #BIN_FILE_PREFIX}, the full URL string's hashcode and + * a sanitized portion of the URL filename. The returned filename is never + * more than 64 characters to ensure maximum file system compatibility. + * + * @param urlString The download URL. + * @return A leaf filename for the cached download file. + */ + @NonNull + private String getCacheFilename(@NonNull String urlString) { + + int code = 0; + for (int i = 0, j = urlString.length(); i < j; i++) { + code = code * 31 + urlString.charAt(i); + } + String hash = String.format("%08x", code); + + String leaf = urlString.toLowerCase(Locale.US); + if (leaf.length() >= 2) { + int index = urlString.lastIndexOf('/', leaf.length() - 2); + leaf = urlString.substring(index + 1); + } + + leaf = leaf.replaceAll("[^a-z0-9_-]+", "_"); + leaf = leaf.replaceAll("__+", "_"); + + leaf = hash + '-' + leaf; + String prefix = BIN_FILE_PREFIX + REV_FILE_PREFIX; + int n = 64 - prefix.length(); + if (leaf.length() > n) { + leaf = leaf.substring(0, n); + } + + return prefix + leaf; + } + + @NonNull + private String getInfoFilename(@NonNull String cacheFilename) { + return cacheFilename.replaceFirst(BIN_FILE_PREFIX, INFO_FILE_PREFIX); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/ITask.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/ITask.java new file mode 100644 index 00000000..58e76f9c --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/ITask.java @@ -0,0 +1,31 @@ +/* + * 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.sdklib.internal.repository; + + +/** + * A task that executes and can update a monitor to display its status. + * The task will generally be run in a separate thread. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public interface ITask { + void run(ITaskMonitor monitor); +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/ITaskFactory.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/ITaskFactory.java new file mode 100644 index 00000000..ce7133aa --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/ITaskFactory.java @@ -0,0 +1,64 @@ +/* + * 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.sdklib.internal.repository; + + +/** + * A factory that can start and run new {@link ITask}s. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public interface ITaskFactory { + + /** + * Starts a new task with a new {@link ITaskMonitor}. + *

+ * The task will execute in a thread and runs it own UI loop. + * This means the task can perform UI operations using + * {@code Display#asyncExec(Runnable)}. + *

+ * In either case, the method only returns when the task has finished. + * + * @param title The title of the task, displayed in the monitor if any. + * @param task The task to run. + */ + void start(String title, ITask task); + + /** + * Starts a new task contributing to an already existing {@link ITaskMonitor}. + *

+ * To use this properly, you should use {@link ITaskMonitor#createSubMonitor(int)} + * and give the sub-monitor to the new task with the number of work units you want + * it to fill. The {@link #start} method will make sure to fill the progress + * when the task is completed, in case the actual task did not. + *

+ * When a task is started from within a monitor, it reuses the thread + * from the parent. Otherwise it starts a new thread and runs it own + * UI loop. This means the task can perform UI operations using + * {@code Display#asyncExec(Runnable)}. + *

+ * In either case, the method only returns when the task has finished. + * + * @param title The title of the task, displayed in the monitor if any. + * @param parentMonitor The parent monitor. Can be null. + * @param task The task to run and have it display on the monitor. + */ + void start(String title, ITaskMonitor parentMonitor, ITask task); +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/ITaskMonitor.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/ITaskMonitor.java new file mode 100644 index 00000000..89aac97f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/ITaskMonitor.java @@ -0,0 +1,152 @@ +/* + * 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.sdklib.internal.repository; + +import com.android.utils.ILogger; + + +/** + * A monitor interface for a {@link ITask}. + *

+ * Depending on the task factory that created the task, there might not be any UI + * or it might not implement all the methods, in which case calling them would be + * a no-op but is guaranteed not to crash. + *

+ * If the task runs in a non-UI worker thread, the task factory implementation + * will take care of the update the UI in the correct thread. The task itself + * must not have to deal with it. + *

+ * A monitor typically has 3 levels of text displayed:
+ * - A title may be present on a task dialog, typically when a task + * dialog is created. This is not covered by this monitor interface.
+ * - A description displays prominent information on what the task + * is currently doing. This is expected to vary over time, typically changing + * with each sub-monitor, and typically only the last description is visible. + * For example an updater would typically have descriptions such as "downloading", + * "installing" and finally "done". This is set using {@link #setDescription}.
+ * - A verbose optional log that can provide more information than the summary + * description and is typically displayed in some kind of scrollable multi-line + * text field so that the user can keep track of what happened. 3 levels are + * provided: error, normal and verbose. An UI may hide the log till an error is + * logged and/or might hide the verbose text unless a flag is checked by the user. + * This is set using {@link #log}, {@link #logError} and {@link #logVerbose}. + *

+ * A monitor is also an {@link ILogger} implementation. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public interface ITaskMonitor extends ILogger { + + /** + * Sets the description in the current task dialog. + * This method can be invoked from a non-UI thread. + */ + void setDescription(String format, Object...args); + + /** + * Logs a "normal" information line. + * This method can be invoked from a non-UI thread. + */ + void log(String format, Object...args); + + /** + * Logs an "error" information line. + * This method can be invoked from a non-UI thread. + */ + void logError(String format, Object...args); + + /** + * Logs a "verbose" information line, that is extra details which are typically + * not that useful for the end-user and might be hidden until explicitly shown. + * This method can be invoked from a non-UI thread. + */ + void logVerbose(String format, Object...args); + + /** + * Sets the max value of the progress bar. + * This method can be invoked from a non-UI thread. + * + * This method MUST be invoked once before using {@link #incProgress(int)} or + * {@link #getProgress()} or {@link #createSubMonitor(int)}. Callers are + * discouraged from using more than once -- implementations can either discard + * the next calls or behave incoherently. + */ + void setProgressMax(int max); + + /** + * Returns the max value of the progress bar, as last set by {@link #setProgressMax(int)}. + * Returns 0 if the max has never been set yet. + */ + int getProgressMax(); + + /** + * Increments the current value of the progress bar. + * This method can be invoked from a non-UI thread. + * + * Callers MUST use setProgressMax before using this method. + */ + void incProgress(int delta); + + /** + * Returns the current value of the progress bar, + * between 0 and up to {@link #setProgressMax(int)} - 1. + * + * Callers MUST use setProgressMax before using this method. + */ + int getProgress(); + + /** + * Returns true if the user requested to cancel the operation. + * It is up to the task thread to pool this and exit as soon + * as possible. + */ + boolean isCancelRequested(); + + /** + * Creates a sub-monitor that will use up to tickCount on the progress bar. + * tickCount must be 1 or more. + */ + ITaskMonitor createSubMonitor(int tickCount); + + /** + * Display a yes/no question dialog box. + * + * Implementations MUST allow this to be called from any thread, e.g. by + * making sure the dialog is opened synchronously in the ui thread. + * + * @param title The title of the dialog box + * @param message The error message + * @return true if YES was clicked. + */ + boolean displayPrompt(final String title, final String message); + + /** + * Launch an interface which asks for user credentials. Implementations + * MUST allow this to be called from any thread, e.g. by making sure the + * dialog is opened synchronously in the UI thread. + * + * @param title The title of the dialog box. + * @param message The message to be displayed as an instruction. + * @return Returns the user provided credentials. Some fields may be blank if the user + * did not provide any input. + If operation is canceled by user the return value must be null. + */ + UserCredentials displayLoginCredentialsPrompt(String title, String message); +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/LocalSdkParser.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/LocalSdkParser.java new file mode 100644 index 00000000..69064eff --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/LocalSdkParser.java @@ -0,0 +1,804 @@ +/* + * 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.sdklib.internal.repository; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.io.FileWrapper; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.ISystemImage; +import com.android.sdklib.ISystemImage.LocationType; +import com.android.sdklib.SdkManager; +import com.android.sdklib.internal.androidTarget.PlatformTarget; +import com.android.sdklib.internal.project.ProjectProperties; +import com.android.sdklib.internal.repository.packages.AddonPackage; +import com.android.sdklib.internal.repository.packages.BuildToolPackage; +import com.android.sdklib.internal.repository.packages.DocPackage; +import com.android.sdklib.internal.repository.packages.ExtraPackage; +import com.android.sdklib.internal.repository.packages.Package; +import com.android.sdklib.internal.repository.packages.PlatformPackage; +import com.android.sdklib.internal.repository.packages.PlatformToolPackage; +import com.android.sdklib.internal.repository.packages.SamplePackage; +import com.android.sdklib.internal.repository.packages.SourcePackage; +import com.android.sdklib.internal.repository.packages.SystemImagePackage; +import com.android.sdklib.internal.repository.packages.ToolPackage; +import com.android.sdklib.io.LegacyFileOp; +import com.android.sdklib.repository.AddonManifestIniProps; +import com.android.sdklib.repository.descriptors.PkgType; +import com.android.utils.ILogger; +import com.android.utils.Pair; +import com.google.common.collect.Lists; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +/** + * Scans a local SDK to find which packages are currently installed. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class LocalSdkParser { + + private Package[] mPackages; + + /** Parse all SDK folders. */ + public static final int PARSE_ALL = PkgType.PKG_ALL_INT; + /** Parse the SDK/tools folder. */ + public static final int PARSE_TOOLS = PkgType.PKG_TOOLS.getIntValue(); + /** Parse the SDK/platform-tools folder */ + public static final int PARSE_PLATFORM_TOOLS = PkgType.PKG_PLATFORM_TOOLS.getIntValue(); + /** Parse the SDK/docs folder. */ + public static final int PARSE_DOCS = PkgType.PKG_DOC.getIntValue(); + /** + * Equivalent to parsing the SDK/platforms folder but does so + * by using the valid targets loaded by the {@link SdkManager}. + * Parsing the platforms also parses the SDK/system-images folder. + */ + public static final int PARSE_PLATFORMS = PkgType.PKG_PLATFORM.getIntValue(); + /** + * Equivalent to parsing the SDK/addons folder but does so + * by using the valid targets loaded by the {@link SdkManager}. + */ + public static final int PARSE_ADDONS = PkgType.PKG_ADDON.getIntValue(); + /** Parse the SDK/samples folder. + * Note: this will not detect samples located in the SDK/extras packages. */ + public static final int PARSE_SAMPLES = PkgType.PKG_SAMPLE.getIntValue(); + /** Parse the SDK/sources folder. */ + public static final int PARSE_SOURCES = PkgType.PKG_SOURCE.getIntValue(); + /** Parse the SDK/extras folder. */ + public static final int PARSE_EXTRAS = PkgType.PKG_EXTRA.getIntValue(); + /** Parse the SDK/build-tools folder. */ + public static final int PARSE_BUILD_TOOLS = PkgType.PKG_BUILD_TOOLS.getIntValue(); + + public LocalSdkParser() { + // pass + } + + /** + * Returns the packages found by the last call to {@link #parseSdk}. + *

+ * This returns initially returns null. + * Once the parseSdk() method has been called, this returns a possibly empty but non-null array. + */ + public Package[] getPackages() { + return mPackages; + } + + /** + * Clear the internal packages list. After this call, {@link #getPackages()} will return + * null till {@link #parseSdk} is called. + */ + public void clearPackages() { + mPackages = null; + } + + /** + * Scan the give SDK to find all the packages already installed at this location. + *

+ * Store the packages internally. You can use {@link #getPackages()} to retrieve them + * at any time later. + *

+ * Equivalent to calling {@code parseSdk(..., PARSE_ALL, ...); } + * + * @param osSdkRoot The path to the SDK folder, typically {@code sdkManager.getLocation()}. + * @param sdkManager An existing SDK manager to list current platforms and addons. + * @param monitor A monitor to track progress. Cannot be null. + * @return The packages found. Can be retrieved later using {@link #getPackages()}. + */ + @NonNull + public Package[] parseSdk( + @NonNull String osSdkRoot, + @NonNull SdkManager sdkManager, + @NonNull ITaskMonitor monitor) { + return parseSdk(osSdkRoot, sdkManager, PARSE_ALL, monitor); + } + + /** + * Scan the give SDK to find all the packages already installed at this location. + *

+ * Store the packages internally. You can use {@link #getPackages()} to retrieve them + * at any time later. + * + * @param osSdkRoot The path to the SDK folder, typically {@code sdkManager.getLocation()}. + * @param sdkManager An existing SDK manager to list current platforms and addons. + * @param parseFilter Either {@link #PARSE_ALL} or an ORed combination of the other + * {@code PARSE_} constants to indicate what should be parsed. + * @param monitor A monitor to track progress. Cannot be null. + * @return The packages found. Can be retrieved later using {@link #getPackages()}. + */ + @NonNull + public Package[] parseSdk( + @NonNull String osSdkRoot, + @NonNull SdkManager sdkManager, + int parseFilter, + @NonNull ITaskMonitor monitor) { + ArrayList packages = new ArrayList(); + HashSet visited = new HashSet(); + + monitor.setProgressMax(11); + + File dir = null; + Package pkg = null; + + if ((parseFilter & PARSE_DOCS) != 0) { + dir = new File(osSdkRoot, SdkConstants.FD_DOCS); + pkg = scanDoc(dir, monitor); + if (pkg != null) { + packages.add(pkg); + visited.add(dir); + } + } + monitor.incProgress(1); + + if ((parseFilter & PARSE_TOOLS) != 0) { + dir = new File(osSdkRoot, SdkConstants.FD_TOOLS); + pkg = scanTools(dir, monitor); + if (pkg != null) { + packages.add(pkg); + visited.add(dir); + } + } + monitor.incProgress(1); + + if ((parseFilter & PARSE_PLATFORM_TOOLS) != 0) { + dir = new File(osSdkRoot, SdkConstants.FD_PLATFORM_TOOLS); + pkg = scanPlatformTools(dir, monitor); + if (pkg != null) { + packages.add(pkg); + visited.add(dir); + } + } + monitor.incProgress(1); + + if ((parseFilter & PARSE_BUILD_TOOLS) != 0) { + scanBuildTools(sdkManager, visited, packages, monitor); + } + monitor.incProgress(1); + + // for platforms, add-ons and samples, rely on the SdkManager parser + if ((parseFilter & (PARSE_ADDONS | PARSE_PLATFORMS)) != 0) { + File samplesRoot = new File(osSdkRoot, SdkConstants.FD_SAMPLES); + + for(IAndroidTarget target : sdkManager.getTargets()) { + Properties props = parseProperties(new File(target.getLocation(), + SdkConstants.FN_SOURCE_PROP)); + + try { + pkg = null; + if (target.isPlatform() && (parseFilter & PARSE_PLATFORMS) != 0) { + pkg = PlatformPackage.create(target, props); + + if (samplesRoot.isDirectory()) { + // Get the samples dir for a platform if it is located in the new + // root /samples dir. We purposely ignore "old" samples that are + // located under the platform dir. + File samplesDir = new File(target.getPath(IAndroidTarget.SAMPLES)); + if (samplesDir.exists() && + samplesDir.getParentFile().equals(samplesRoot)) { + Properties samplesProps = parseProperties( + new File(samplesDir, SdkConstants.FN_SOURCE_PROP)); + if (samplesProps != null) { + Package pkg2 = SamplePackage.create(target, samplesProps); + packages.add(pkg2); + } + visited.add(samplesDir); + } + } + } else if ((parseFilter & PARSE_ADDONS) != 0) { + pkg = AddonPackage.create(target, props); + } + + if (pkg != null) { + for (ISystemImage systemImage : target.getSystemImages()) { + if (systemImage.getLocationType() == LocationType.IN_SYSTEM_IMAGE) { + File siDir = systemImage.getLocation(); + if (siDir.isDirectory()) { + Properties siProps = parseProperties( + new File(siDir, SdkConstants.FN_SOURCE_PROP)); + Package pkg2 = new SystemImagePackage( + target.getVersion(), + 0 /*rev*/, // use the one from siProps + systemImage.getAbiType(), + siProps, + siDir.getAbsolutePath()); + packages.add(pkg2); + visited.add(siDir); + } + } + } + } + + } catch (Exception e) { + monitor.error(e, null); + } + + if (pkg != null) { + packages.add(pkg); + visited.add(new File(target.getLocation())); + } + } + } + monitor.incProgress(1); + + if ((parseFilter & PARSE_PLATFORMS) != 0) { + scanMissingSystemImages(sdkManager, visited, packages, monitor); + } + monitor.incProgress(1); + if ((parseFilter & PARSE_ADDONS) != 0) { + scanMissingAddons(sdkManager, visited, packages, monitor); + } + monitor.incProgress(1); + if ((parseFilter & PARSE_SAMPLES) != 0) { + scanMissingSamples(sdkManager, visited, packages, monitor); + } + monitor.incProgress(1); + if ((parseFilter & PARSE_EXTRAS) != 0) { + scanExtras(sdkManager, visited, packages, monitor); + } + monitor.incProgress(1); + if ((parseFilter & PARSE_EXTRAS) != 0) { + scanExtrasDirectory(osSdkRoot, visited, packages, monitor); + } + monitor.incProgress(1); + if ((parseFilter & PARSE_SOURCES) != 0) { + scanSources(sdkManager, visited, packages, monitor); + } + monitor.incProgress(1); + + Collections.sort(packages); + + mPackages = packages.toArray(new Package[packages.size()]); + return mPackages; + } + + /** + * Find any directory in the /extras/vendors/path folders for extra packages. + * This isn't a recursive search. + */ + private void scanExtras(SdkManager sdkManager, + HashSet visited, + ArrayList packages, + ILogger log) { + File root = new File(sdkManager.getLocation(), SdkConstants.FD_EXTRAS); + + for (File vendor : listFilesNonNull(root)) { + if (vendor.isDirectory()) { + scanExtrasDirectory(vendor.getAbsolutePath(), visited, packages, log); + } + } + } + + /** + * Find any other directory in the given "root" directory that hasn't been visited yet + * and assume they contain extra packages. This is not a recursive search. + */ + private void scanExtrasDirectory(String extrasRoot, + HashSet visited, + ArrayList packages, + ILogger log) { + File root = new File(extrasRoot); + + for (File dir : listFilesNonNull(root)) { + if (dir.isDirectory() && !visited.contains(dir)) { + Properties props = parseProperties(new File(dir, SdkConstants.FN_SOURCE_PROP)); + if (props != null) { + try { + Package pkg = ExtraPackage.create( + null, //source + props, //properties + null, //vendor + dir.getName(), //path + 0, //revision + null, //license + null, //description + null, //descUrl + dir.getPath() //archiveOsPath + ); + + packages.add(pkg); + visited.add(dir); + } catch (Exception e) { + log.error(e, null); + } + } + } + } + } + + /** + * Find any other sub-directories under the /samples root that hasn't been visited yet + * and assume they contain sample packages. This is not a recursive search. + *

+ * The use case is to find samples dirs under /samples when their target isn't loaded. + */ + private void scanMissingSamples(SdkManager sdkManager, + HashSet visited, + ArrayList packages, + ILogger log) { + File root = new File(sdkManager.getLocation()); + root = new File(root, SdkConstants.FD_SAMPLES); + + for (File dir : listFilesNonNull(root)) { + if (dir.isDirectory() && !visited.contains(dir)) { + Properties props = parseProperties(new File(dir, SdkConstants.FN_SOURCE_PROP)); + if (props != null) { + try { + Package pkg = SamplePackage.create(dir.getAbsolutePath(), props); + packages.add(pkg); + visited.add(dir); + } catch (Exception e) { + log.error(e, null); + } + } + } + } + } + + /** + * The sdk manager only lists valid addons. However here we also want to find "broken" + * addons, i.e. addons that failed to load for some reason. + *

+ * Find any other sub-directories under the /add-ons root that hasn't been visited yet + * and assume they contain broken addons. + */ + private void scanMissingAddons(SdkManager sdkManager, + HashSet visited, + ArrayList packages, + ILogger log) { + File addons = new File(new File(sdkManager.getLocation()), SdkConstants.FD_ADDONS); + + for (File dir : listFilesNonNull(addons)) { + if (dir.isDirectory() && !visited.contains(dir)) { + Pair, String> infos = + parseAddonProperties(dir, sdkManager.getTargets(), log); + Properties sourceProps = + parseProperties(new File(dir, SdkConstants.FN_SOURCE_PROP)); + + Map addonProps = infos.getFirst(); + String error = infos.getSecond(); + try { + Package pkg = AddonPackage.createBroken(dir.getAbsolutePath(), + sourceProps, + addonProps, + error); + packages.add(pkg); + visited.add(dir); + } catch (Exception e) { + log.error(e, null); + } + } + } + } + + /** + * Parses the add-on properties and decodes any error that occurs when + * loading an addon. + * + * @param addonDir the location of the addon directory. + * @param targetList The list of Android target that were already loaded + * from the SDK. + * @param log the ILogger object receiving warning/error from the parsing. + * @return A pair with the property map and an error string. Both can be + * null but not at the same time. If a non-null error is present + * then the property map must be ignored. The error should be + * translatable as it might show up in the SdkManager UI. + */ + @Deprecated // Copied from SdkManager.java, dup of LocalAddonPkgInfo.parseAddonProperties. + @NonNull + public static Pair, String> parseAddonProperties( + @NonNull File addonDir, @NonNull IAndroidTarget[] targetList, + @NonNull ILogger log) { + Map propertyMap = null; + String error = null; + + FileWrapper addOnManifest = new FileWrapper(addonDir, + SdkConstants.FN_MANIFEST_INI); + + do { + if (!addOnManifest.isFile()) { + error = String.format("File not found: %1$s", + SdkConstants.FN_MANIFEST_INI); + break; + } + + propertyMap = ProjectProperties.parsePropertyFile(addOnManifest, + log); + if (propertyMap == null) { + error = String.format("Failed to parse properties from %1$s", + SdkConstants.FN_MANIFEST_INI); + break; + } + + // look for some specific values in the map. + // we require name, vendor, and api + String name = propertyMap.get(AddonManifestIniProps.ADDON_NAME); + if (name == null) { + error = String.format("'%1$s' is missing from %2$s.", + AddonManifestIniProps.ADDON_NAME, + SdkConstants.FN_MANIFEST_INI); + break; + } + + String vendor = propertyMap.get(AddonManifestIniProps.ADDON_VENDOR); + if (vendor == null) { + error = String.format("'%1$s' is missing from %2$s.", + AddonManifestIniProps.ADDON_VENDOR, + SdkConstants.FN_MANIFEST_INI); + break; + } + + String api = propertyMap.get(AddonManifestIniProps.ADDON_API); + if (api == null) { + error = String.format("'%1$s' is missing from %2$s.", + AddonManifestIniProps.ADDON_API, + SdkConstants.FN_MANIFEST_INI); + break; + } + + // Look for a platform that has a matching api level or codename. + PlatformTarget baseTarget = null; + for (IAndroidTarget target : targetList) { + if (target.isPlatform() && target.getVersion().equals(api)) { + baseTarget = (PlatformTarget) target; + break; + } + } + + if (baseTarget == null) { + error = String.format( + "Unable to find base platform with API level '%1$s'", + api); + break; + } + + // get the add-on revision + String revision = propertyMap.get(AddonManifestIniProps.ADDON_REVISION); + if (revision == null) { + revision = propertyMap.get(AddonManifestIniProps.ADDON_REVISION_OLD); + } + if (revision != null) { + try { + Integer.parseInt(revision); + } catch (NumberFormatException e) { + // looks like revision does not parse to a number. + error = String.format( + "%1$s is not a valid number in %2$s.", + AddonManifestIniProps.ADDON_REVISION, + SdkConstants.FN_BUILD_PROP); + break; + } + } + + } while (false); + + return Pair.of(propertyMap, error); + } + + + /** + * The sdk manager only lists valid system image via its addons or platform targets. + * However here we also want to find "broken" system images, that is system images + * that are located in the sdk/system-images folder but somehow not loaded properly. + */ + private void scanMissingSystemImages(SdkManager sdkManager, + HashSet visited, + ArrayList packages, + ILogger log) { + File siRoot = new File(sdkManager.getLocation(), SdkConstants.FD_SYSTEM_IMAGES); + + // The system-images folder contains a list of platform folders. + for (File platformDir : listFilesNonNull(siRoot)) { + if (platformDir.isDirectory() && !visited.contains(platformDir)) { + visited.add(platformDir); + + // In the platform directory, we expect a list of abi folders + // or a list of tag/abi folders. Basically parse any folder that has + // a source.prop file within 2 levels. + List propFiles = Lists.newArrayList(); + + for (File dir1 : listFilesNonNull(platformDir)) { + if (dir1.isDirectory() && !visited.contains(dir1)) { + visited.add(dir1); + File prop1 = new File(dir1, SdkConstants.FN_SOURCE_PROP); + if (prop1.isFile()) { + propFiles.add(prop1); + } else { + for (File dir2 : listFilesNonNull(dir1)) { + if (dir2.isDirectory() && !visited.contains(dir2)) { + visited.add(dir2); + File prop2 = new File(dir2, SdkConstants.FN_SOURCE_PROP); + if (prop2.isFile()) { + propFiles.add(prop2); + } + } + } + } + } + } + + for (File propFile : propFiles) { + Properties props = parseProperties(propFile); + try { + Package pkg = SystemImagePackage.createBroken(propFile.getParentFile(), + props); + packages.add(pkg); + } catch (Exception e) { + log.error(e, null); + } + } + } + } + } + + /** + * Scan the sources/folders and register valid as well as broken source packages. + */ + private void scanSources(SdkManager sdkManager, + HashSet visited, + ArrayList packages, + ILogger log) { + File srcRoot = new File(sdkManager.getLocation(), SdkConstants.FD_PKG_SOURCES); + + // The sources folder contains a list of platform folders. + for (File platformDir : listFilesNonNull(srcRoot)) { + if (platformDir.isDirectory() && !visited.contains(platformDir)) { + visited.add(platformDir); + + // Ignore empty directories + File[] srcFiles = platformDir.listFiles(); + if (srcFiles != null && srcFiles.length > 0) { + Properties props = + parseProperties(new File(platformDir, SdkConstants.FN_SOURCE_PROP)); + + try { + Package pkg = SourcePackage.create(platformDir, props); + packages.add(pkg); + } catch (Exception e) { + log.error(e, null); + } + } + } + } + } + + /** + * Try to find a tools package at the given location. + * Returns null if not found. + */ + private Package scanTools(File toolFolder, ILogger log) { + // Can we find some properties? + Properties props = parseProperties(new File(toolFolder, SdkConstants.FN_SOURCE_PROP)); + + // We're not going to check that all tools are present. At the very least + // we should expect to find android and an emulator adapted to the current OS. + boolean hasEmulator = false; + boolean hasAndroid = false; + String android1 = SdkConstants.androidCmdName().replace(".bat", ".exe"); + String android2 = android1.indexOf('.') == -1 ? null : android1.replace(".exe", ".bat"); + for (File file : listFilesNonNull(toolFolder)) { + String name = file.getName(); + if (SdkConstants.FN_EMULATOR.equals(name)) { + hasEmulator = true; + } + if (android1.equals(name) || (android2 != null && android2.equals(name))) { + hasAndroid = true; + } + } + + if (!hasAndroid || !hasEmulator) { + return null; + } + + // Create our package. use the properties if we found any. + try { + Package pkg = ToolPackage.create( + null, //source + props, //properties + 0, //revision + null, //license + "Tools", //description + null, //descUrl + toolFolder.getPath() //archiveOsPath + ); + + return pkg; + } catch (Exception e) { + log.error(e, null); + } + return null; + } + + /** + * Try to find a platform-tools package at the given location. + * Returns null if not found. + */ + private Package scanPlatformTools(File platformToolsFolder, ILogger log) { + // Can we find some properties? + Properties props = parseProperties(new File(platformToolsFolder, + SdkConstants.FN_SOURCE_PROP)); + + // We're not going to check that all tools are present. At the very least + // we should expect to find adb, aidl, aapt and dx (adapted to the current OS). + + if (platformToolsFolder.listFiles() == null) { + // ListFiles is null if the directory doesn't even exist. + // Not going to find anything in there... + return null; + } + + // Create our package. use the properties if we found any. + try { + Package pkg = PlatformToolPackage.create( + null, //source + props, //properties + 0, //revision + null, //license + "Platform Tools", //description + null, //descUrl + platformToolsFolder.getPath() //archiveOsPath + ); + + return pkg; + } catch (Exception e) { + log.error(e, null); + } + return null; + } + + /** + * Scan the build-tool/folders and register valid as well as broken build tool packages. + */ + private void scanBuildTools( + SdkManager sdkManager, + HashSet visited, + ArrayList packages, + ILogger log) { + File buildToolRoot = new File(sdkManager.getLocation(), SdkConstants.FD_BUILD_TOOLS); + + // The build-tool root folder contains a list of revisioned folders. + for (File buildToolDir : listFilesNonNull(buildToolRoot)) { + if (buildToolDir.isDirectory() && !visited.contains(buildToolDir)) { + visited.add(buildToolDir); + + // Ignore empty directories + File[] srcFiles = buildToolDir.listFiles(); + if (srcFiles != null && srcFiles.length > 0) { + Properties props = + parseProperties(new File(buildToolDir, SdkConstants.FN_SOURCE_PROP)); + + try { + Package pkg = BuildToolPackage.create(buildToolDir, props); + packages.add(pkg); + } catch (Exception e) { + log.error(e, null); + } + } + } + } + } + + /** + * Try to find a docs package at the given location. + * Returns null if not found. + */ + private Package scanDoc(File docFolder, ILogger log) { + // Can we find some properties? + Properties props = parseProperties(new File(docFolder, SdkConstants.FN_SOURCE_PROP)); + + // To start with, a doc folder should have an "index.html" to be acceptable. + // We don't actually check the content of the file. + if (new File(docFolder, "index.html").isFile()) { + try { + Package pkg = DocPackage.create( + null, //source + props, //properties + 0, //apiLevel + null, //codename + 0, //revision + null, //license + null, //description + null, //descUrl + docFolder.getPath() //archiveOsPath + ); + + return pkg; + } catch (Exception e) { + log.error(e, null); + } + } + + return null; + } + + /** + * Parses the given file as properties file if it exists. + * Returns null if the file does not exist, cannot be parsed or has no properties. + */ + private Properties parseProperties(File propsFile) { + FileInputStream fis = null; + try { + if (propsFile.exists()) { + fis = new FileInputStream(propsFile); + + Properties props = new Properties(); + props.load(fis); + + // To be valid, there must be at least one property in it. + if (!props.isEmpty()) { + return props; + } + } + + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (fis != null) { + try { + fis.close(); + } catch (IOException e) { + } + } + } + return null; + } + + /** + * Helper method that calls {@link File#listFiles()} and returns + * a non-null empty list if the input is not a directory or has + * no files. + */ + @NonNull + private static File[] listFilesNonNull(@NonNull File dir) { + if (dir.isDirectory()) { + File[] files = dir.listFiles(); + if (files != null) { + return files; + } + } + return LegacyFileOp.EMPTY_FILE_ARRAY; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/NullTaskMonitor.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/NullTaskMonitor.java new file mode 100644 index 00000000..27269541 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/NullTaskMonitor.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2011 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.sdklib.internal.repository; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.utils.ILogger; +import com.android.utils.NullLogger; + + +/** + * A no-op implementation of the {@link ITaskMonitor} interface. + *

+ * This can be passed to methods that require a monitor when the caller doesn't + * have any UI to update or means to report tracked progress. + * A custom {@link ILogger} is used. Clients could use {@link NullLogger} if + * they really don't care about the logging either. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class NullTaskMonitor implements ITaskMonitor { + + private final ILogger mLog; + + /** + * Creates a no-op {@link ITaskMonitor} that defers logging to the specified + * logger. + *

+ * This can be passed to methods that require a monitor when the caller doesn't + * have any UI to update or means to report tracked progress. + * + * @param log An {@link ILogger}. Must not be null. Consider using {@link NullLogger}. + */ + public NullTaskMonitor(ILogger log) { + mLog = log; + } + + @Override + public void setDescription(String format, Object...args) { + // pass + } + + @Override + public void log(String format, Object...args) { + mLog.info(format, args); + } + + @Override + public void logError(String format, Object...args) { + mLog.error(null /*throwable*/, format, args); + } + + @Override + public void logVerbose(String format, Object...args) { + mLog.verbose(format, args); + } + + @Override + public void setProgressMax(int max) { + // pass + } + + @Override + public int getProgressMax() { + return 0; + } + + @Override + public void incProgress(int delta) { + // pass + } + + /** Always return 1. */ + @Override + public int getProgress() { + return 1; + } + + /** Always return false. */ + @Override + public boolean isCancelRequested() { + return false; + } + + @Override + public ITaskMonitor createSubMonitor(int tickCount) { + return this; + } + + /** Always return false. */ + @Override + public boolean displayPrompt(final String title, final String message) { + return false; + } + + /** Always return null. */ + @Override + public UserCredentials displayLoginCredentialsPrompt(String title, String message) { + return null; + } + + // --- ILogger --- + + @Override + public void error(@Nullable Throwable t, @Nullable String errorFormat, Object... args) { + mLog.error(t, errorFormat, args); + } + + @Override + public void warning(@NonNull String warningFormat, Object... args) { + mLog.warning(warningFormat, args); + } + + @Override + public void info(@NonNull String msgFormat, Object... args) { + mLog.info(msgFormat, args); + } + + @Override + public void verbose(@NonNull String msgFormat, Object... args) { + mLog.verbose(msgFormat, args); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/SdkStats.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/SdkStats.java new file mode 100644 index 00000000..cb1af741 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/SdkStats.java @@ -0,0 +1,627 @@ +/* + * Copyright (C) 2012 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.sdklib.internal.repository; + +import com.android.annotations.VisibleForTesting; +import com.android.annotations.VisibleForTesting.Visibility; +import com.android.io.NonClosingInputStream; +import com.android.io.NonClosingInputStream.CloseBehavior; +import com.android.sdklib.repository.SdkStatsConstants; +import com.android.utils.SparseArray; + +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.xml.sax.ErrorHandler; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.UnknownHostException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.net.ssl.SSLKeyException; +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import javax.xml.validation.Validator; + + +/** + * Retrieves stats on platforms. + *

+ * This returns information stored on the repository in a different XML file + * and isn't directly tied to the existence of the listed platforms. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class SdkStats { + + public static class PlatformStatBase { + private final int mApiLevel; + private final String mVersionName; + private final String mCodeName; + private final float mShare; + + public PlatformStatBase(int apiLevel, + String versionName, + String codeName, + float share) { + mApiLevel = apiLevel; + mVersionName = versionName; + mCodeName = codeName; + mShare = share; + } + + /** The Android API Level for the platform. An int > 0. */ + public int getApiLevel() { + return mApiLevel; + } + + /** The official codename for this platform, for example "Cupcake". */ + public String getCodeName() { + return mCodeName; + } + + /** The official version name of this platform, for example "Android 1.5". */ + public String getVersionName() { + return mVersionName; + } + + /** An approximate share percentage of this platform and all the + * platforms of lower API level. */ + public float getShare() { + return mShare; + } + + /** Returns a string representation of this object, for debugging purposes. */ + @Override + public String toString() { + return String.format("api=%d, code=%s, vers=%s, share=%.1f%%", //$NON-NLS-1$ + mApiLevel, mCodeName, mVersionName, mShare); + } + } + + public static class PlatformStat extends PlatformStatBase { + private final float mAccumShare; + + public PlatformStat(int apiLevel, + String versionName, + String codeName, + float share, + float accumShare) { + super(apiLevel, versionName, codeName, share); + mAccumShare = accumShare; + } + + public PlatformStat(PlatformStatBase base, float accumShare) { + super(base.getApiLevel(), + base.getVersionName(), + base.getCodeName(), + base.getShare()); + mAccumShare = accumShare; + } + + /** The accumulated approximate share percentage of that platform. */ + public float getAccumShare() { + return mAccumShare; + } + + /** Returns a string representation of this object, for debugging purposes. */ + @Override + public String toString() { + return String.format("", super.toString(), mAccumShare); + } + } + + private final SparseArray mStats = new SparseArray(); + + public SdkStats() { + } + + public SparseArray getStats() { + return mStats; + } + + public void load(DownloadCache cache, boolean forceHttp, ITaskMonitor monitor) { + + String url = SdkStatsConstants.URL_STATS; + + if (forceHttp) { + url = url.replaceAll("https://", "http://"); //$NON-NLS-1$ //$NON-NLS-2$ + } + + monitor.setProgressMax(5); + monitor.setDescription("Fetching %1$s", url); + monitor.incProgress(1); + + Exception[] exception = new Exception[] { null }; + Boolean[] validatorFound = new Boolean[] { Boolean.FALSE }; + String[] validationError = new String[] { null }; + Document validatedDoc = null; + String validatedUri = null; + + InputStream xml = fetchXmlUrl(url, cache, monitor.createSubMonitor(1), exception); + + if (xml != null) { + monitor.setDescription("Validate XML"); + + // Explore the XML to find the potential XML schema version + int version = getXmlSchemaVersion(xml); + + if (version >= 1 && version <= SdkStatsConstants.NS_LATEST_VERSION) { + // This should be a version we can handle. Try to validate it + // and report any error as invalid XML syntax, + + String uri = validateXml(xml, url, version, validationError, validatorFound); + if (uri != null) { + // Validation was successful + validatedDoc = getDocument(xml, monitor); + validatedUri = uri; + + } + } else if (version > SdkStatsConstants.NS_LATEST_VERSION) { + // The schema used is more recent than what is supported by this tool. + // We don't have an upgrade-path support yet, so simply ignore the document. + closeStream(xml); + return; + } + } + + // If any exception was handled during the URL fetch, display it now. + if (exception[0] != null) { + String reason = null; + if (exception[0] instanceof FileNotFoundException) { + // FNF has no useful getMessage, so we need to special handle it. + reason = "File not found"; + } else if (exception[0] instanceof UnknownHostException && + exception[0].getMessage() != null) { + // This has no useful getMessage yet could really use one + reason = String.format("Unknown Host %1$s", exception[0].getMessage()); + } else if (exception[0] instanceof SSLKeyException) { + // That's a common error and we have a pref for it. + reason = "HTTPS SSL error. You might want to force download through HTTP in the settings."; + } else if (exception[0].getMessage() != null) { + reason = exception[0].getMessage(); + } else { + // We don't know what's wrong. Let's give the exception class at least. + reason = String.format("Unknown (%1$s)", exception[0].getClass().getName()); + } + + monitor.logError("Failed to fetch URL %1$s, reason: %2$s", url, reason); + } + + if (validationError[0] != null) { + monitor.logError("%s", validationError[0]); //$NON-NLS-1$ + } + + // Stop here if we failed to validate the XML. We don't want to load it. + if (validatedDoc == null) { + closeStream(xml); + return; + } + + monitor.incProgress(1); + + if (xml != null) { + monitor.setDescription("Parse XML"); + monitor.incProgress(1); + parseStatsDocument(validatedDoc, validatedUri, monitor); + } + + // done + monitor.incProgress(1); + closeStream(xml); + } + + /** + * Fetches the document at the given URL and returns it as a stream. Returns + * null if anything wrong happens. + * + * @param urlString The URL to load, as a string. + * @param monitor {@link ITaskMonitor} related to this URL. + * @param outException If non null, where to store any exception that + * happens during the fetch. + * @see UrlOpener UrlOpener, which handles all URL logic. + */ + private InputStream fetchXmlUrl(String urlString, + DownloadCache cache, + ITaskMonitor monitor, + Exception[] outException) { + try { + InputStream xml = cache.openCachedUrl(urlString, monitor); + if (xml != null) { + xml.mark(500000); + xml = new NonClosingInputStream(xml); + ((NonClosingInputStream) xml).setCloseBehavior(CloseBehavior.RESET); + } + return xml; + } catch (Exception e) { + if (outException != null) { + outException[0] = e; + } + } + + return null; + } + + /** + * Closes the stream, ignore any exception from InputStream.close(). + * If the stream is a NonClosingInputStream, sets it to CloseBehavior.CLOSE first. + */ + private void closeStream(InputStream is) { + if (is != null) { + if (is instanceof NonClosingInputStream) { + ((NonClosingInputStream) is).setCloseBehavior(CloseBehavior.CLOSE); + } + try { + is.close(); + } catch (IOException ignore) {} + } + } + + /** + * Manually parses the root element of the XML to extract the schema version + * at the end of the xmlns:sdk="http://schemas.android.com/sdk/android/addons-list/$N" + * declaration. + * + * @return 1..{@link SdkStatsConstants#NS_LATEST_VERSION} for a valid schema version + * or 0 if no schema could be found. + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected int getXmlSchemaVersion(InputStream xml) { + if (xml == null) { + return 0; + } + + // Get an XML document + Document doc = null; + try { + xml.reset(); + + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setIgnoringComments(false); + factory.setValidating(false); + + // Parse the old document using a non namespace aware builder + factory.setNamespaceAware(false); + DocumentBuilder builder = factory.newDocumentBuilder(); + + // We don't want the default handler which prints errors to stderr. + builder.setErrorHandler(new ErrorHandler() { + @Override + public void warning(SAXParseException e) throws SAXException { + // pass + } + @Override + public void fatalError(SAXParseException e) throws SAXException { + throw e; + } + @Override + public void error(SAXParseException e) throws SAXException { + throw e; + } + }); + + doc = builder.parse(xml); + + // Prepare a new document using a namespace aware builder + factory.setNamespaceAware(true); + builder = factory.newDocumentBuilder(); + + } catch (Exception e) { + // Failed to reset XML stream + // Failed to get builder factor + // Failed to create XML document builder + // Failed to parse XML document + // Failed to read XML document + } + + if (doc == null) { + return 0; + } + + // Check the root element is an XML with at least the following properties: + // + // + // Note that we don't have namespace support enabled, we just do it manually. + + Pattern nsPattern = Pattern.compile(SdkStatsConstants.NS_PATTERN); + + String prefix = null; + for (Node child = doc.getFirstChild(); child != null; child = child.getNextSibling()) { + if (child.getNodeType() == Node.ELEMENT_NODE) { + prefix = null; + String name = child.getNodeName(); + int pos = name.indexOf(':'); + if (pos > 0 && pos < name.length() - 1) { + prefix = name.substring(0, pos); + name = name.substring(pos + 1); + } + if (SdkStatsConstants.NODE_SDK_STATS.equals(name)) { + NamedNodeMap attrs = child.getAttributes(); + String xmlns = "xmlns"; //$NON-NLS-1$ + if (prefix != null) { + xmlns += ":" + prefix; //$NON-NLS-1$ + } + Node attr = attrs.getNamedItem(xmlns); + if (attr != null) { + String uri = attr.getNodeValue(); + if (uri != null) { + Matcher m = nsPattern.matcher(uri); + if (m.matches()) { + String version = m.group(1); + try { + return Integer.parseInt(version); + } catch (NumberFormatException e) { + return 0; + } + } + } + } + } + } + } + + return 0; + } + + /** + * Validates this XML against one of the requested SDK Repository schemas. + * If the XML was correctly validated, returns the schema that worked. + * If it doesn't validate, returns null and stores the error in outError[0]. + * If we can't find a validator, returns null and set validatorFound[0] to false. + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected String validateXml(InputStream xml, String url, int version, + String[] outError, Boolean[] validatorFound) { + + if (xml == null) { + return null; + } + + try { + Validator validator = getValidator(version); + + if (validator == null) { + validatorFound[0] = Boolean.FALSE; + outError[0] = String.format( + "XML verification failed for %1$s.\nNo suitable XML Schema Validator could be found in your Java environment. Please consider updating your version of Java.", + url); + return null; + } + + validatorFound[0] = Boolean.TRUE; + + // Reset the stream if it supports that operation. + xml.reset(); + + // Validation throws a bunch of possible Exceptions on failure. + validator.validate(new StreamSource(xml)); + return SdkStatsConstants.getSchemaUri(version); + + } catch (SAXParseException e) { + outError[0] = String.format( + "XML verification failed for %1$s.\nLine %2$d:%3$d, Error: %4$s", + url, + e.getLineNumber(), + e.getColumnNumber(), + e.toString()); + + } catch (Exception e) { + outError[0] = String.format( + "XML verification failed for %1$s.\nError: %2$s", + url, + e.toString()); + } + return null; + } + + /** + * Helper method that returns a validator for our XSD, or null if the current Java + * implementation can't process XSD schemas. + * + * @param version The version of the XML Schema. + * See {@link SdkStatsConstants#getXsdStream(int)} + */ + private Validator getValidator(int version) throws SAXException { + InputStream xsdStream = SdkStatsConstants.getXsdStream(version); + try { + SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + + if (factory == null) { + return null; + } + + // This may throw a SAX Exception if the schema itself is not a valid XSD + Schema schema = factory.newSchema(new StreamSource(xsdStream)); + + Validator validator = schema == null ? null : schema.newValidator(); + + return validator; + } finally { + if (xsdStream != null) { + try { + xsdStream.close(); + } catch (IOException ignore) {} + } + } + } + + /** + * Takes an XML document as a string as parameter and returns a DOM for it. + * + * On error, returns null and prints a (hopefully) useful message on the monitor. + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected Document getDocument(InputStream xml, ITaskMonitor monitor) { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setIgnoringComments(true); + factory.setNamespaceAware(true); + + DocumentBuilder builder = factory.newDocumentBuilder(); + xml.reset(); + Document doc = builder.parse(new InputSource(xml)); + + return doc; + } catch (ParserConfigurationException e) { + monitor.logError("Failed to create XML document builder"); + + } catch (SAXException e) { + monitor.logError("Failed to parse XML document"); + + } catch (IOException e) { + monitor.logError("Failed to read XML document"); + } + + return null; + } + + /** + * Parses all valid platforms found in the XML. + * Changes the stats array returned by {@link #getStats()} + * (also returns the value directly, useful for unit tests.) + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected SparseArray parseStatsDocument( + Document doc, + String nsUri, + ITaskMonitor monitor) { + + String baseUrl = System.getenv("SDK_TEST_BASE_URL"); //$NON-NLS-1$ + if (baseUrl != null) { + if (baseUrl.length() <= 0 || !baseUrl.endsWith("/")) { //$NON-NLS-1$ + baseUrl = null; + } + } + + SparseArray platforms = new SparseArray(); + int maxApi = 0; + + Node root = getFirstChild(doc, nsUri, SdkStatsConstants.NODE_SDK_STATS); + if (root != null) { + for (Node child = root.getFirstChild(); + child != null; + child = child.getNextSibling()) { + if (child.getNodeType() == Node.ELEMENT_NODE && + nsUri.equals(child.getNamespaceURI()) && + child.getLocalName().equals(SdkStatsConstants.NODE_PLATFORM)) { + + try { + Node node = getFirstChild(child, nsUri, SdkStatsConstants.NODE_API_LEVEL); + int apiLevel = Integer.parseInt(node.getTextContent().trim()); + + if (apiLevel < 1) { + // bad API level, ignore it. + continue; + } + + if (platforms.indexOfKey(apiLevel) >= 0) { + // if we already loaded that API, ignore duplicates + continue; + } + + String codeName = + getFirstChild(child, nsUri, SdkStatsConstants.NODE_CODENAME). + getTextContent().trim(); + String versName = + getFirstChild(child, nsUri, SdkStatsConstants.NODE_VERSION). + getTextContent().trim(); + + if (codeName == null || versName == null || + codeName.isEmpty() || versName.isEmpty()) { + // bad names. ignore. + continue; + } + + node = getFirstChild(child, nsUri, SdkStatsConstants.NODE_SHARE); + float percent = Float.parseFloat(node.getTextContent().trim()); + + if (percent < 0 || percent > 100) { + // invalid percentage. ignore. + continue; + } + + PlatformStatBase p = new PlatformStatBase( + apiLevel, versName, codeName, percent); + platforms.put(apiLevel, p); + + maxApi = apiLevel > maxApi ? apiLevel : maxApi; + + } catch (Exception ignore) { + // Error parsing this platform. Ignore it. + continue; + } + } + } + } + + mStats.clear(); + + // Compute cumulative share percents & fill in final map + for (int api = 1; api <= maxApi; api++) { + PlatformStatBase p = platforms.get(api); + if (p == null) { + continue; + } + + float sum = p.getShare(); + for (int j = api + 1; j <= maxApi; j++) { + PlatformStatBase pj = platforms.get(j); + if (pj != null) { + sum += pj.getShare(); + } + } + + mStats.put(api, new PlatformStat(p, sum)); + } + + return mStats; + } + + /** + * Returns the first child element with the given XML local name. + * If xmlLocalName is null, returns the very first child element. + */ + private Node getFirstChild(Node node, String nsUri, String xmlLocalName) { + + for(Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) { + if (child.getNodeType() == Node.ELEMENT_NODE && + nsUri.equals(child.getNamespaceURI())) { + if (xmlLocalName == null || child.getLocalName().equals(xmlLocalName)) { + return child; + } + } + } + + return null; + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/UrlOpener.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/UrlOpener.java new file mode 100644 index 00000000..338463cb --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/UrlOpener.java @@ -0,0 +1,530 @@ +/* + * Copyright (C) 2011 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.sdklib.internal.repository; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.utils.Pair; + +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.ProtocolVersion; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.AuthState; +import org.apache.http.auth.Credentials; +import org.apache.http.auth.NTCredentials; +import org.apache.http.auth.params.AuthPNames; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.params.AuthPolicy; +import org.apache.http.client.protocol.ClientContext; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.conn.ProxySelectorRoutePlanner; +import org.apache.http.message.BasicHttpResponse; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; +import org.apache.http.protocol.BasicHttpContext; +import org.apache.http.protocol.HttpContext; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.FileNotFoundException; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.Proxy; +import java.net.ProxySelector; +import java.net.URI; +import java.net.URL; +import java.net.URLConnection; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; + +/** + * This class holds static methods for downloading URL resources. + * @see #openUrl(String, boolean, ITaskMonitor, Header[]) + *

+ * Implementation detail: callers should use {@link DownloadCache} instead of this class. + * {@link DownloadCache#openDirectUrl} is a direct pass-through to {@link UrlOpener} since + * there's no caching. However from an implementation perspective it's still recommended + * to pass down a {@link DownloadCache} instance, which will let us override the implementation + * later on (for testing, for example.) + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +class UrlOpener { + + private static final boolean DEBUG = + System.getenv("ANDROID_DEBUG_URL_OPENER") != null; //$NON-NLS-1$ + + private static Map sRealmCache = + new HashMap(); + + /** Timeout to establish a connection, in milliseconds. */ + private static int sConnectionTimeoutMs; + /** Timeout waiting for data on a socket, in milliseconds. */ + private static int sSocketTimeoutMs; + + static { + if (DEBUG) { + Properties props = System.getProperties(); + for (String key : new String[] { + "http.proxyHost", //$NON-NLS-1$ + "http.proxyPort", //$NON-NLS-1$ + "https.proxyHost", //$NON-NLS-1$ + "https.proxyPort" }) { //$NON-NLS-1$ + String prop = props.getProperty(key); + if (prop != null) { + System.out.printf( + "SdkLib.UrlOpener Java.Prop %s='%s'\n", //$NON-NLS-1$ + key, prop); + } + } + } + + try { + sConnectionTimeoutMs = Integer.parseInt(System.getenv("ANDROID_SDKMAN_CONN_TIMEOUT")); + } catch (Exception ignore) { + sConnectionTimeoutMs = 2 * 60 * 1000; + } + + try { + sSocketTimeoutMs = Integer.parseInt(System.getenv("ANDROID_SDKMAN_READ_TIMEOUT")); + } catch (Exception ignore) { + sSocketTimeoutMs = 1 * 60 * 1000; + } + } + + /** + * This class cannot be instantiated. + * @see #openUrl(String, boolean, ITaskMonitor, Header[]) + */ + private UrlOpener() { + } + + /** + * Opens a URL. It can be a simple URL or one which requires basic + * authentication. + *

+ * Tries to access the given URL. If http response is either + * {@code HttpStatus.SC_UNAUTHORIZED} or + * {@code HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED}, asks for + * login/password and tries to authenticate into proxy server and/or URL. + *

+ * This implementation relies on the Apache Http Client due to its + * capabilities of proxy/http authentication.
+ * Proxy configuration is determined by {@link ProxySelectorRoutePlanner} using the JVM proxy + * settings by default. + *

+ * For more information see:
+ * - {@code http://hc.apache.org/httpcomponents-client-ga/}
+ * - {@code http://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/impl/conn/ProxySelectorRoutePlanner.html} + *

+ * There's a very simple realm cache implementation. + * Login/Password for each realm are stored in a static {@link Map}. + * Before asking the user the method verifies if the information is already + * available in the memory cache. + * + * @param url the URL string to be opened. + * @param needsMarkResetSupport Indicates the caller must have an input stream that + * supports the mark/reset operations (as indicated by {@link InputStream#markSupported()}. + * Implementation detail: If the original stream does not, it will be fetched and wrapped + * into a {@link ByteArrayInputStream}. This can only work sanely if the resource is a + * small file that can fit in memory. It also means the caller has no chance of showing + * a meaningful download progress. If unsure, callers should set this to false. + * @param monitor {@link ITaskMonitor} to output status. + * @param headers An optional array of HTTP headers to use in the GET request. + * @return Returns a {@link Pair} with {@code first} holding an {@link InputStream} + * and {@code second} holding an {@link HttpResponse}. + * The returned pair is never null and contains + * at least a code; for http requests that provide them the response + * also contains locale, headers and an status line. + * The input stream can be null, especially in case of error. + * The caller must only accept the stream if the response code is 200 or similar. + * @throws IOException Exception thrown when there are problems retrieving + * the URL or its content. + * @throws CanceledByUserException Exception thrown if the user cancels the + * authentication dialog. + */ + @NonNull + static Pair openUrl( + @NonNull String url, + boolean needsMarkResetSupport, + @NonNull ITaskMonitor monitor, + @Nullable Header[] headers) + throws IOException, CanceledByUserException { + + Exception fallbackOnJavaUrlConnect = null; + Pair result = null; + + try { + result = openWithHttpClient(url, monitor, headers); + + } catch (UnknownHostException e) { + // Host in unknown. No need to even retry with the Url object, + // if it's broken, it's broken. It's already an IOException but + // it could use a better message. + throw new IOException("Unknown Host " + e.getMessage(), e); + + } catch (ClientProtocolException e) { + // We get this when HttpClient fails to accept the current protocol, + // e.g. when processing file:// URLs. + fallbackOnJavaUrlConnect = e; + + } catch (IOException e) { + throw e; + + } catch (CanceledByUserException e) { + // HTTP Basic Auth or NTLM login was canceled by user. + throw e; + + } catch (Exception e) { + if (DEBUG) { + System.out.printf("[HttpClient Error] %s : %s\n", url, e.toString()); + } + + fallbackOnJavaUrlConnect = e; + } + + if (fallbackOnJavaUrlConnect != null) { + // If the protocol is not supported by HttpClient (e.g. file:///), + // revert to the standard java.net.Url.open. + + try { + result = openWithUrl(url, headers); + } catch (IOException e) { + throw e; + } catch (Exception e) { + if (DEBUG && !fallbackOnJavaUrlConnect.equals(e)) { + System.out.printf("[Url Error] %s : %s\n", url, e.toString()); + } + } + } + + // If the caller requires an InputStream that supports mark/reset, let's + // make sure we have such a stream. + if (result != null && needsMarkResetSupport) { + InputStream is = result.getFirst(); + if (is != null) { + if (!is.markSupported()) { + try { + // Consume the whole input stream and offer a byte array stream instead. + // This can only work sanely if the resource is a small file that can + // fit in memory. It also means the caller has no chance of showing + // a meaningful download progress. + InputStream is2 = toByteArrayInputStream(is); + if (is2 != null) { + result = Pair.of(is2, result.getSecond()); + try { + is.close(); + } catch (Exception ignore) {} + } + } catch (Exception e3) { + // Ignore. If this can't work, caller will fail later. + } + } + } + } + + if (result == null) { + // Make up an error code if we don't have one already. + HttpResponse outResponse = new BasicHttpResponse( + new ProtocolVersion("HTTP", 1, 0), //$NON-NLS-1$ + HttpStatus.SC_METHOD_FAILURE, ""); //$NON-NLS-1$; // 420=Method Failure + result = Pair.of(null, outResponse); + } + + return result; + } + + // ByteArrayInputStream is the duct tape of input streams. + private static InputStream toByteArrayInputStream(InputStream is) throws IOException { + int inc = 4096; + int curr = 0; + byte[] result = new byte[inc]; + + int n; + while ((n = is.read(result, curr, result.length - curr)) != -1) { + curr += n; + if (curr == result.length) { + byte[] temp = new byte[curr + inc]; + System.arraycopy(result, 0, temp, 0, curr); + result = temp; + } + } + + return new ByteArrayInputStream(result, 0, curr); + } + + private static Pair openWithUrl( + String url, + Header[] inHeaders) throws IOException { + URL u = new URL(url); + + URLConnection c = u.openConnection(); + + c.setConnectTimeout(sConnectionTimeoutMs); + c.setReadTimeout(sSocketTimeoutMs); + + if (inHeaders != null) { + for (Header header : inHeaders) { + c.setRequestProperty(header.getName(), header.getValue()); + } + } + + // Trigger the access to the resource + // (at which point setRequestProperty can't be used anymore.) + int code = 200; + + if (c instanceof HttpURLConnection) { + code = ((HttpURLConnection) c).getResponseCode(); + } + + // Get the input stream. That can fail for a file:// that doesn't exist + // in which case we set the response code to 404. + // Also we need a buffered input stream since the caller need to use is.reset(). + InputStream is = null; + try { + is = new BufferedInputStream(c.getInputStream()); + } catch (Exception ignore) { + if (is == null && code == 200) { + code = 404; + } + } + + HttpResponse outResponse = new BasicHttpResponse( + new ProtocolVersion(u.getProtocol(), 1, 0), // make up the protocol version + code, ""); //$NON-NLS-1$; + + Map> outHeaderMap = c.getHeaderFields(); + + for (Entry> entry : outHeaderMap.entrySet()) { + String name = entry.getKey(); + if (name != null) { + List values = entry.getValue(); + if (!values.isEmpty()) { + outResponse.setHeader(name, values.get(0)); + } + } + } + + return Pair.of(is, outResponse); + } + + @NonNull + private static Pair openWithHttpClient( + @NonNull String url, + @NonNull ITaskMonitor monitor, + Header[] inHeaders) + throws IOException, ClientProtocolException, CanceledByUserException { + UserCredentials result = null; + String realm = null; + + HttpParams params = new BasicHttpParams(); + HttpConnectionParams.setConnectionTimeout(params, sConnectionTimeoutMs); + HttpConnectionParams.setSoTimeout(params, sSocketTimeoutMs); + + // use the simple one + final DefaultHttpClient httpClient = new DefaultHttpClient(params); + + + // create local execution context + HttpContext localContext = new BasicHttpContext(); + final HttpGet httpGet = new HttpGet(url); + if (inHeaders != null) { + for (Header header : inHeaders) { + httpGet.addHeader(header); + } + } + + // retrieve local java configured network in case there is the need to + // authenticate a proxy + ProxySelectorRoutePlanner routePlanner = new ProxySelectorRoutePlanner( + httpClient.getConnectionManager().getSchemeRegistry(), + ProxySelector.getDefault()); + httpClient.setRoutePlanner(routePlanner); + + // Set preference order for authentication options. + // In particular, we don't add AuthPolicy.SPNEGO, which is given preference over NTLM in + // servers that support both, as it is more secure. However, we don't seem to handle it + // very well, so we leave it off the list. + // See http://hc.apache.org/httpcomponents-client-ga/tutorial/html/authentication.html for + // more info. + List authpref = new ArrayList(); + authpref.add(AuthPolicy.BASIC); + authpref.add(AuthPolicy.DIGEST); + authpref.add(AuthPolicy.NTLM); + httpClient.getParams().setParameter(AuthPNames.PROXY_AUTH_PREF, authpref); + httpClient.getParams().setParameter(AuthPNames.TARGET_AUTH_PREF, authpref); + + if (DEBUG) { + try { + URI uri = new URI(url); + ProxySelector sel = routePlanner.getProxySelector(); + if (sel != null && uri.getScheme().startsWith("httP")) { //$NON-NLS-1$ + List list = sel.select(uri); + System.out.printf( + "SdkLib.UrlOpener:\n Connect to: %s\n Proxy List: %s\n", //$NON-NLS-1$ + url, + list == null ? "(null)" : Arrays.toString(list.toArray()));//$NON-NLS-1$ + } + } catch (Exception e) { + System.out.printf( + "SdkLib.UrlOpener: Failed to get proxy info for %s: %s\n", //$NON-NLS-1$ + url, e.toString()); + } + } + + boolean trying = true; + // loop while the response is being fetched + while (trying) { + // connect and get status code + HttpResponse response = httpClient.execute(httpGet, localContext); + int statusCode = response.getStatusLine().getStatusCode(); + + if (DEBUG) { + System.out.printf(" Status: %d\n", statusCode); //$NON-NLS-1$ + } + + // check whether any authentication is required + AuthState authenticationState = null; + if (statusCode == HttpStatus.SC_UNAUTHORIZED) { + // Target host authentication required + authenticationState = (AuthState) localContext + .getAttribute(ClientContext.TARGET_AUTH_STATE); + } + if (statusCode == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED) { + // Proxy authentication required + authenticationState = (AuthState) localContext + .getAttribute(ClientContext.PROXY_AUTH_STATE); + } + if (statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_NOT_MODIFIED) { + // in case the status is OK and there is a realm and result, + // cache it + if (realm != null && result != null) { + sRealmCache.put(realm, result); + } + } + + // there is the need for authentication + if (authenticationState != null) { + + // get scope and realm + AuthScope authScope = authenticationState.getAuthScope(); + + // If the current realm is different from the last one it means + // a pass was performed successfully to the last URL, therefore + // cache the last realm + if (realm != null && !realm.equals(authScope.getRealm())) { + sRealmCache.put(realm, result); + } + + realm = authScope.getRealm(); + + // in case there is cache for this Realm, use it to authenticate + if (sRealmCache.containsKey(realm)) { + result = sRealmCache.get(realm); + } else { + // since there is no cache, request for login and password + result = monitor.displayLoginCredentialsPrompt("Site Authentication", + "Please login to the following domain: " + realm + + "\n\nServer requiring authentication:\n" + authScope.getHost()); + if (result == null) { + throw new CanceledByUserException("User canceled login dialog."); + } + } + + // retrieve authentication data + String user = result.getUserName(); + String password = result.getPassword(); + String workstation = result.getWorkstation(); + String domain = result.getDomain(); + + // proceed in case there is indeed a user + if (user != null && !user.isEmpty()) { + Credentials credentials = new NTCredentials(user, password, + workstation, domain); + httpClient.getCredentialsProvider().setCredentials(authScope, credentials); + trying = true; + } else { + trying = false; + } + } else { + trying = false; + } + + HttpEntity entity = response.getEntity(); + + if (entity != null) { + if (trying) { + // in case another pass to the Http Client will be performed, close the entity. + entity.getContent().close(); + } else { + // since no pass to the Http Client is needed, retrieve the + // entity's content. + + // Note: don't use something like a BufferedHttpEntity since it would consume + // all content and store it in memory, resulting in an OutOfMemory exception + // on a large download. + InputStream is = new FilterInputStream(entity.getContent()) { + @Override + public void close() throws IOException { + // Since Http Client is no longer needed, close it. + + // Bug #21167: we need to tell http client to shutdown + // first, otherwise the super.close() would continue + // downloading and not return till complete. + + httpClient.getConnectionManager().shutdown(); + super.close(); + } + }; + + HttpResponse outResponse = new BasicHttpResponse(response.getStatusLine()); + outResponse.setHeaders(response.getAllHeaders()); + outResponse.setLocale(response.getLocale()); + + return Pair.of(is, outResponse); + } + } else if (statusCode == HttpStatus.SC_NOT_MODIFIED) { + // It's ok to not have an entity (e.g. nothing to download) for a 304 + HttpResponse outResponse = new BasicHttpResponse(response.getStatusLine()); + outResponse.setHeaders(response.getAllHeaders()); + outResponse.setLocale(response.getLocale()); + + return Pair.of(null, outResponse); + } + } + + // We get here if we did not succeed. Callers do not expect a null result. + httpClient.getConnectionManager().shutdown(); + throw new FileNotFoundException(url); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/UserCredentials.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/UserCredentials.java new file mode 100644 index 00000000..62949564 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/UserCredentials.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2011 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.sdklib.internal.repository; + +/** + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class UserCredentials { + private final String mUserName; + private final String mPassword; + private final String mWorkstation; + private final String mDomain; + + public UserCredentials(String userName, String password, String workstation, String domain) { + mUserName = userName; + mPassword = password; + mWorkstation = workstation; + mDomain = domain; + } + + public String getUserName() { + return mUserName; + } + + public String getPassword() { + return mPassword; + } + + public String getWorkstation() { + return mWorkstation; + } + + public String getDomain() { + return mDomain; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/ArchFilter.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/ArchFilter.java new file mode 100644 index 00000000..e4401de0 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/ArchFilter.java @@ -0,0 +1,324 @@ +/* + * Copyright (C) 2014 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.sdklib.internal.repository.archives; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdklib.repository.NoPreviewRevision; + +import java.util.Properties; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class ArchFilter { + + private static final String PROP_HOST_OS = "Archive.HostOs"; //$NON-NLS-1$ + private static final String PROP_HOST_BITS = "Archive.HostBits"; //$NON-NLS-1$ + private static final String PROP_JVM_BITS = "Archive.JvmBits"; //$NON-NLS-1$ + private static final String PROP_MIN_JVM_VERSION = "Archive.MinJvmVers"; //$NON-NLS-1$ + + /** + * The legacy property used to serialize {@link LegacyOs} in source.properties files. + *

+ * Replaced by {@code ArchFilter.PROP_HOST_OS}. + */ + public static final String LEGACY_PROP_OS = "Archive.Os"; //$NON-NLS-1$ + + /** + * The legacy property used to serialize {@link LegacyArch} in source.properties files. + *

+ * Replaced by {@code ArchFilter.PROP_HOST_BITS} and {@code ArchFilter.PROP_JVM_BITS}. + */ + public static final String LEGACY_PROP_ARCH = "Archive.Arch"; //$NON-NLS-1$ + + private final HostOs mHostOs; + private final BitSize mHostBits; + private final BitSize mJvmBits; + private final NoPreviewRevision mMinJvmVersion; + + /** + * Creates a new {@link ArchFilter} with the specified filter attributes. + *

+ * This filters represents the attributes requires for a package's {@link Archive} to + * be installable on the current architecture. Not all fields are required -- those that + * are not specified imply there is no limitation on that particular attribute. + * + * + * @param hostOs The host OS or null if there's no limitation for this package. + * @param hostBits The host bit size or null if there's no limitation for this package. + * @param jvmBits The JVM bit size or null if there's no limitation for this package. + * @param minJvmVersion The minimal JVM version required by this package + * or null if there's no limitation for this package. + */ + public ArchFilter(@Nullable HostOs hostOs, + @Nullable BitSize hostBits, + @Nullable BitSize jvmBits, + @Nullable NoPreviewRevision minJvmVersion) { + mHostOs = hostOs; + mHostBits = hostBits; + mJvmBits = jvmBits; + mMinJvmVersion = minJvmVersion; + } + + /** + * Creates an {@link ArchFilter} using properties previously saved in a {@link Properties} + * object, typically by the {@link ArchFilter#saveProperties(Properties)} method. + *

+ * Missing properties are set to null and will not filter. + * + * @param props A properties object previously filled by {@link #saveProperties(Properties)}. + * If null, a default empty {@link ArchFilter} is created. + */ + public ArchFilter(@Nullable Properties props) { + HostOs hostOs = null; + BitSize hostBits = null; + BitSize jvmBits = null; + NoPreviewRevision minJvmVers = null; + + if (props != null) { + hostOs = HostOs .fromXmlName(props.getProperty(PROP_HOST_OS)); + hostBits = BitSize.fromXmlName(props.getProperty(PROP_HOST_BITS)); + jvmBits = BitSize.fromXmlName(props.getProperty(PROP_JVM_BITS)); + + try { + minJvmVers = NoPreviewRevision.parseRevision(props.getProperty(PROP_MIN_JVM_VERSION)); + } catch (NumberFormatException ignore) {} + + // Backward compatibility with older PROP_OS and PROP_ARCH values + if (!props.containsKey(PROP_HOST_OS) && props.containsKey(LEGACY_PROP_OS)) { + hostOs = HostOs.fromXmlName(props.getProperty(LEGACY_PROP_OS)); + } + if (!props.containsKey(PROP_HOST_BITS) && + !props.containsKey(PROP_HOST_BITS) && + props.containsKey(LEGACY_PROP_ARCH)) { + // We'll only handle the typical x86 and x86_64 values of the old PROP_ARCH + // value and ignore the PPC value. "Any" is equivalent to keeping the new + // attributes to null. + String v = props.getProperty(LEGACY_PROP_ARCH).toLowerCase(); + + if (v.indexOf("x86_64") > 0) { + // JVM in 64-bit x86_64 mode so host-bits should be 64 too. + hostBits = jvmBits = BitSize._64; + } else if (v.indexOf("x86") > 0) { + // JVM in 32-bit x86 mode, but host-bits could be either 32 or 64 + // so we don't set this one. + jvmBits = BitSize._32; + } + } + } + + mHostOs = hostOs; + mHostBits = hostBits; + mJvmBits = jvmBits; + mMinJvmVersion = minJvmVers; + } + + /** @return the host OS or null if there's no limitation for this package. */ + @Nullable + public HostOs getHostOS() { + return mHostOs; + } + + /** @return the host bit size or null if there's no limitation for this package. */ + @Nullable + public BitSize getHostBits() { + return mHostBits; + } + + /** @return the JVM bit size or null if there's no limitation for this package. */ + @Nullable + public BitSize getJvmBits() { + return mJvmBits; + } + + /** @return the minimal JVM version required by this package + * or null if there's no limitation for this package. */ + @Nullable + public NoPreviewRevision getMinJvmVersion() { + return mMinJvmVersion; + } + + /** + * Checks whether {@code this} {@link ArchFilter} is compatible with the right-hand side one. + *

+ * Typically this is used to check whether "this downloaded package is compatible with the + * current architecture", which would be expressed as: + *

+     * DownloadedArchive.filter.isCompatibleWith(ArhFilter.getCurrent())
+     * 
+ * For the host OS & bit size attribute, if the attributes are non-null they must be equal. + * For the min-jvm-version, "this" version (the package we want to install) needs to be lower + * or equal to the "required" (current host) version. + * + * @param required The requirements to meet. + * @return True if this filter meets or exceeds the given requirements. + */ + public boolean isCompatibleWith(@NonNull ArchFilter required) { + if (mHostOs != null + && required.mHostOs != null + && !mHostOs.equals(required.mHostOs)) { + return false; + } + + if (mHostBits != null + && required.mHostBits != null + && !mHostBits.equals(required.mHostBits)) { + return false; + } + + if (mJvmBits != null + && required.mJvmBits != null + && !mJvmBits.equals(required.mJvmBits)) { + return false; + } + + if (mMinJvmVersion != null + && required.mMinJvmVersion != null + && mMinJvmVersion.compareTo(required.mMinJvmVersion) > 0) { + return false; + } + + return true; + } + + /** + * Returns an {@link ArchFilter} that represents the current host platform. + * @return an {@link ArchFilter} that represents the current host platform. + */ + @NonNull + public static ArchFilter getCurrent() { + String os = System.getProperty("os.name"); //$NON-NLS-1$ + HostOs hostOS = null; + if (os.startsWith("Mac")) { //$NON-NLS-1$ + hostOS = HostOs.MACOSX; + } else if (os.startsWith("Windows")) { //$NON-NLS-1$ + hostOS = HostOs.WINDOWS; + } else if (os.startsWith("Linux")) { //$NON-NLS-1$ + hostOS = HostOs.LINUX; + } + + BitSize jvmBits; + String arch = System.getProperty("os.arch"); //$NON-NLS-1$ + + if (arch.equalsIgnoreCase("x86_64") || //$NON-NLS-1$ + arch.equalsIgnoreCase("ia64") || //$NON-NLS-1$ + arch.equalsIgnoreCase("amd64")) { //$NON-NLS-1$ + jvmBits = BitSize._64; + } else { + jvmBits = BitSize._32; + } + + // TODO figure out the host bit size. + // When jvmBits is 64 we know it's surely 64 + // but that's not necessarily obvious when jvmBits is 32. + BitSize hostBits = jvmBits; + + NoPreviewRevision minJvmVersion = null; + String javav = System.getProperty("java.version"); //$NON-NLS-1$ + // java Version is typically in the form "1.2.3_45" and we just need to keep up to "1.2.3" + // since our revision numbers are in 3-parts form (1.2.3). + Pattern p = Pattern.compile("((\\d+)(\\.\\d+)?(\\.\\d+)?).*"); //$NON-NLS-1$ + Matcher m = p.matcher(javav); + if (m.matches()) { + minJvmVersion = NoPreviewRevision.parseRevision(m.group(1)); + } + + return new ArchFilter(hostOS, hostBits, jvmBits, minJvmVersion); + } + + /** + * Save this {@link ArchFilter} attributes into the the given {@link Properties} object. + * These properties can later be given to the constructor that takes a {@link Properties} object. + *

+ * Null attributes are not saved in the properties. + * + * @param props A non-null properties object to fill with non-null attributes. + */ + void saveProperties(@NonNull Properties props) { + if (mHostOs != null) { + props.setProperty(PROP_HOST_OS, mHostOs.getXmlName()); + } + if (mHostBits != null) { + props.setProperty(PROP_HOST_BITS, mHostBits.getXmlName()); + } + if (mJvmBits != null) { + props.setProperty(PROP_JVM_BITS, mJvmBits.getXmlName()); + } + if (mMinJvmVersion != null) { + props.setProperty(PROP_MIN_JVM_VERSION, mMinJvmVersion.toShortString()); + } + } + + /** String for debug purposes. */ + @Override + public String toString() { + return ""; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((mHostOs == null) ? 0 : mHostOs.hashCode()); + result = prime * result + ((mHostBits == null) ? 0 : mHostBits.hashCode()); + result = prime * result + ((mJvmBits == null) ? 0 : mJvmBits.hashCode()); + result = prime * result + ((mMinJvmVersion == null) ? 0 : mMinJvmVersion.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + ArchFilter other = (ArchFilter) obj; + if (mHostBits != other.mHostBits) { + return false; + } + if (mHostOs != other.mHostOs) { + return false; + } + if (mJvmBits != other.mJvmBits) { + return false; + } + if (mMinJvmVersion == null) { + if (other.mMinJvmVersion != null) { + return false; + } + } else if (!mMinJvmVersion.equals(other.mMinJvmVersion)) { + return false; + } + return true; + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/Archive.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/Archive.java new file mode 100644 index 00000000..69ed686c --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/Archive.java @@ -0,0 +1,375 @@ +/* + * 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.sdklib.internal.repository.archives; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.annotations.VisibleForTesting; +import com.android.annotations.VisibleForTesting.Visibility; +import com.android.sdklib.repository.IDescription; +import com.android.sdklib.internal.repository.packages.Package; +import com.android.sdklib.internal.repository.sources.SdkSource; +import com.android.sdklib.io.LegacyFileOp; + +import java.io.File; +import java.util.Properties; + + +/** + * A {@link Archive} is the base class for "something" that can be downloaded from + * the SDK repository. + *

+ * A package has some attributes (revision, description) and a list of archives + * which represent the downloadable bits. + *

+ * Packages are offered by a {@link SdkSource} (a download site). + * The {@link ArchiveInstaller} takes care of downloading, unpacking and installing an archive. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. +*/ +@Deprecated +public class Archive implements IDescription, Comparable { + + private final String mUrl; + private final long mSize; + private final String mChecksum; + private final ChecksumType mChecksumType = ChecksumType.SHA1; + private final Package mPackage; + private final String mLocalOsPath; + private final boolean mIsLocal; + private final ArchFilter mArchFilter; + + /** + * Creates a new remote archive. + * This is typically called when inflating a remote-package info from XML meta-data. + * + * @param pkg The package that contains this archive. Typically not null. + * @param archFilter The {@link ArchFilter} for the archive. Typically not null. + * @param url The URL where the archive is available. + * Typically not null but code should be able to handles both. + * @param size The expected size in bytes of the archive to download. + * @param checksum The expected checksum string of the archive. Currently only the + * {@link ChecksumType#SHA1} format is supported. + */ + public Archive(@Nullable Package pkg, + @Nullable ArchFilter archFilter, + @Nullable String url, + long size, + @NonNull String checksum) { + mPackage = pkg; + mArchFilter = archFilter != null ? archFilter : new ArchFilter(null); + mUrl = url == null ? null : url.trim(); + mLocalOsPath = null; + mSize = size; + mChecksum = checksum; + mIsLocal = false; + } + + /** + * Creates a new local archive. + * This is typically called when inflating a local-package info by reading a local + * source.properties file. In this case a few properties like the URL, checksum and + * size are not defined. + * + * @param pkg The package that contains this archive. Cannot be null. + * @param props A set of properties. Can be null. + * @param localOsPath The OS path where the archive is installed if this represents a + * local package. Null for a remote package. + */ + @VisibleForTesting(visibility=Visibility.PACKAGE) + public Archive(@NonNull Package pkg, + @Nullable Properties props, + @Nullable String localOsPath) { + mPackage = pkg; + mArchFilter = new ArchFilter(props); + mUrl = null; + mLocalOsPath = localOsPath; + mSize = 0; + mChecksum = ""; + mIsLocal = localOsPath != null; + } + + /** + * Save the properties of the current archive in the give {@link Properties} object. + * These properties will later be give the constructor that takes a {@link Properties} object. + */ + void saveProperties(@NonNull Properties props) { + mArchFilter.saveProperties(props); + } + + /** + * Returns true if this is a locally installed archive. + * Returns false if this is a remote archive that needs to be downloaded. + */ + public boolean isLocal() { + return mIsLocal; + } + + /** + * Returns the package that created and owns this archive. + * It should generally not be null. + */ + @Nullable + public Package getParentPackage() { + return mPackage; + } + + /** + * Returns the archive size, an int > 0. + * Size will be 0 if this a local installed folder of unknown size. + */ + public long getSize() { + return mSize; + } + + /** + * Returns the SHA1 archive checksum, as a 40-char hex. + * Can be empty but not null for local installed folders. + */ + @NonNull + public String getChecksum() { + return mChecksum; + } + + /** + * Returns the checksum type, always {@link ChecksumType#SHA1} right now. + */ + @NonNull + public ChecksumType getChecksumType() { + return mChecksumType; + } + + /** + * Returns the download archive URL, either absolute or relative to the repository xml. + * Always return null for a local installed folder. + * @see #getLocalOsPath() + */ + @Nullable + public String getUrl() { + return mUrl; + } + + /** + * Returns the local OS folder where a local archive is installed. + * Always return null for remote archives. + * @see #getUrl() + */ + @Nullable + public String getLocalOsPath() { + return mLocalOsPath; + } + + /** + * Returns the architecture filter. + * This non-null filter indicates which host/jvm this archive is compatible with. + */ + @NonNull + public ArchFilter getArchFilter() { + return mArchFilter; + } + + /** + * Generates a description of the {@link ArchFilter} supported by this archive. + */ + public String getOsDescription() { + StringBuilder sb = new StringBuilder(); + + HostOs hos = mArchFilter.getHostOS(); + sb.append(hos == null ? "any OS" : hos.getUiName()); + + BitSize jvmBits = mArchFilter.getJvmBits(); + if (jvmBits != null) { + sb.append(", JVM ").append(jvmBits.getSize()).append("-bits"); + } + + BitSize hostBits = mArchFilter.getJvmBits(); + if (hostBits != null) { + sb.append(", Host ").append(hostBits.getSize()).append("-bits"); + } + + return sb.toString(); + } + + /** + * Returns the short description of the source, if not null. + * Otherwise returns the default Object toString result. + *

+ * This is mostly helpful for debugging. + * For UI display, use the {@link IDescription} interface. + */ + @Override + public String toString() { + String s = getShortDescription(); + if (s != null) { + return s; + } + return super.toString(); + } + + /** + * Generates a short description for this archive. + */ + @Override + public String getShortDescription() { + return String.format("Archive for %1$s", getOsDescription()); + } + + /** + * Generates a longer description for this archive. + */ + @Override + public String getLongDescription() { + return String.format("%1$s\n%2$s\n%3$s", + getShortDescription(), + getSizeDescription(), + getSha1Description()); + } + + public String getSizeDescription() { + long size = getSize(); + String sizeStr; + if (size < 1024) { + sizeStr = String.format("%d Bytes", size); + } else if (size < 1024 * 1024) { + sizeStr = String.format("%d KiB", Math.round(size / 1024.0)); + } else if (size < 1024 * 1024 * 1024) { + sizeStr = String.format("%.1f MiB", + Math.round(10.0 * size / (1024 * 1024.0))/ 10.0); + } else { + sizeStr = String.format("%.1f GiB", + Math.round(10.0 * size / (1024 * 1024 * 1024.0))/ 10.0); + } + + return String.format("Size: %1$s", sizeStr); + } + + public String getSha1Description() { + return String.format("SHA1: %1$s", getChecksum()); + } + + /** + * Returns true if this archive can be installed on the current platform. + */ + public boolean isCompatible() { + ArchFilter current = ArchFilter.getCurrent(); + return mArchFilter.isCompatibleWith(current); + } + + /** + * Delete the archive folder if this is a local archive. + */ + public void deleteLocal() { + if (isLocal()) { + new LegacyFileOp().deleteFileOrFolder(new File(getLocalOsPath())); + } + } + + /** + * Archives are compared using their {@link Package} ordering. + * + * @see Package#compareTo(Package) + */ + @Override + public int compareTo(Archive rhs) { + if (mPackage != null && rhs != null) { + return mPackage.compareTo(rhs.getParentPackage()); + } + return 0; + } + + /** + * Note: An {@link Archive}'s hash code does NOT depend on the parent {@link Package} hash code. + *

+ * {@inheritDoc} + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((mArchFilter == null) ? 0 : mArchFilter.hashCode()); + result = prime * result + ((mChecksum == null) ? 0 : mChecksum.hashCode()); + result = prime * result + ((mChecksumType == null) ? 0 : mChecksumType.hashCode()); + result = prime * result + (mIsLocal ? 1231 : 1237); + result = prime * result + ((mLocalOsPath == null) ? 0 : mLocalOsPath.hashCode()); + result = prime * result + (int) (mSize ^ (mSize >>> 32)); + result = prime * result + ((mUrl == null) ? 0 : mUrl.hashCode()); + return result; + } + + /** + * Note: An {@link Archive}'s equality does NOT depend on the parent {@link Package} equality. + *

+ * {@inheritDoc} + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof Archive)) { + return false; + } + Archive other = (Archive) obj; + if (mArchFilter == null) { + if (other.mArchFilter != null) { + return false; + } + } else if (!mArchFilter.equals(other.mArchFilter)) { + return false; + } + if (mChecksum == null) { + if (other.mChecksum != null) { + return false; + } + } else if (!mChecksum.equals(other.mChecksum)) { + return false; + } + if (mChecksumType == null) { + if (other.mChecksumType != null) { + return false; + } + } else if (!mChecksumType.equals(other.mChecksumType)) { + return false; + } + if (mIsLocal != other.mIsLocal) { + return false; + } + if (mLocalOsPath == null) { + if (other.mLocalOsPath != null) { + return false; + } + } else if (!mLocalOsPath.equals(other.mLocalOsPath)) { + return false; + } + if (mSize != other.mSize) { + return false; + } + if (mUrl == null) { + if (other.mUrl != null) { + return false; + } + } else if (!mUrl.equals(other.mUrl)) { + return false; + } + return true; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/ArchiveInstaller.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/ArchiveInstaller.java new file mode 100644 index 00000000..7a34385e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/ArchiveInstaller.java @@ -0,0 +1,1220 @@ +/* + * Copyright (C) 2010 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.sdklib.internal.repository.archives; + +import com.android.SdkConstants; +import com.android.annotations.Nullable; +import com.android.annotations.VisibleForTesting; +import com.android.annotations.VisibleForTesting.Visibility; +import com.android.sdklib.SdkManager; +import com.android.sdklib.internal.repository.CanceledByUserException; +import com.android.sdklib.internal.repository.DownloadCache; +import com.android.sdklib.internal.repository.ITaskMonitor; +import com.android.sdklib.internal.repository.packages.Package; +import com.android.sdklib.internal.repository.sources.SdkSource; +import com.android.sdklib.io.LegacyFileOp; +import com.android.sdklib.io.IFileOp; +import com.android.sdklib.repository.RepoConstants; +import com.android.utils.GrabProcessOutput; +import com.android.utils.GrabProcessOutput.IProcessOutput; +import com.android.utils.GrabProcessOutput.Wait; +import com.android.utils.Pair; + +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipFile; +import org.apache.http.Header; +import org.apache.http.HttpHeaders; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.message.BasicHeader; + +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Properties; +import java.util.Set; +import java.util.TreeSet; +import java.util.regex.Pattern; + +/** + * Performs the work of installing a given {@link Archive}. + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class ArchiveInstaller { + + private static final String PROP_STATUS_CODE = "StatusCode"; //$NON-NLS-1$ + public static final String ENV_VAR_IGNORE_COMPAT = "ANDROID_SDK_IGNORE_COMPAT"; //$NON-NLS-1$ + + public static final int NUM_MONITOR_INC = 100; + + /** The current {@link LegacyFileOp} to use. Never null. */ + private final IFileOp mFileOp; + + /** + * Generates an {@link ArchiveInstaller} that relies on the default {@link LegacyFileOp}. + */ + public ArchiveInstaller() { + mFileOp = new LegacyFileOp(); + } + + /** + * Generates an {@link ArchiveInstaller} that relies on the given {@link LegacyFileOp}. + * + * @param fileUtils An alternate version of {@link LegacyFileOp} to use for file operations. + */ + protected ArchiveInstaller(IFileOp fileUtils) { + mFileOp = fileUtils; + } + + /** Returns current {@link LegacyFileOp} to use. Never null. */ + protected IFileOp getFileOp() { + return mFileOp; + } + + /** + * Install this {@link ArchiveReplacement}s. + * A "replacement" is composed of the actual new archive to install + * (c.f. {@link ArchiveReplacement#getNewArchive()} and an optional + * archive being replaced (c.f. {@link ArchiveReplacement#getReplaced()}. + * In the case of a new install, the later should be null. + *

+ * The new archive to install will be skipped if it is incompatible. + * + * @return True if the archive was installed, false otherwise. + */ + public boolean install(ArchiveReplacement archiveInfo, + String osSdkRoot, + boolean forceHttp, + SdkManager sdkManager, + DownloadCache cache, + ITaskMonitor monitor) { + + Archive newArchive = archiveInfo.getNewArchive(); + Package pkg = newArchive.getParentPackage(); + + String name = pkg.getShortDescription(); + + if (newArchive.isLocal()) { + // This should never happen. + monitor.log("Skipping already installed archive: %1$s for %2$s", + name, + newArchive.getOsDescription()); + return false; + } + + // In detail mode, give us a way to force install of incompatible archives. + boolean checkIsCompatible = System.getenv(ENV_VAR_IGNORE_COMPAT) == null; + + if (checkIsCompatible && !newArchive.isCompatible()) { + monitor.log("Skipping incompatible archive: %1$s for %2$s", + name, + newArchive.getOsDescription()); + return false; + } + + Pair files = downloadFile(newArchive, osSdkRoot, cache, monitor, forceHttp); + File tmpFile = files == null ? null : files.getFirst(); + File propsFile = files == null ? null : files.getSecond(); + if (tmpFile != null) { + // Unarchive calls the pre/postInstallHook methods. + if (unarchive(archiveInfo, osSdkRoot, tmpFile, sdkManager, monitor)) { + monitor.log("Installed %1$s", name); + // Delete the temp archive if it exists, only on success + mFileOp.deleteFileOrFolder(tmpFile); + mFileOp.deleteFileOrFolder(propsFile); + return true; + } + } + + return false; + } + + /** + * Downloads an archive and returns the temp file with it. + * Caller is responsible with deleting the temp file when done. + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected Pair downloadFile(Archive archive, + String osSdkRoot, + DownloadCache cache, + ITaskMonitor monitor, + boolean forceHttp) { + + String pkgName = archive.getParentPackage().getShortDescription(); + monitor.setDescription("Downloading %1$s", pkgName); + monitor.log("Downloading %1$s", pkgName); + + String link = archive.getUrl(); + if (!link.startsWith("http://") //$NON-NLS-1$ + && !link.startsWith("https://") //$NON-NLS-1$ + && !link.startsWith("ftp://")) { //$NON-NLS-1$ + // Make the URL absolute by prepending the source + Package pkg = archive.getParentPackage(); + SdkSource src = pkg.getParentSource(); + if (src == null) { + monitor.logError("Internal error: no source for archive %1$s", pkgName); + return null; + } + + // take the URL to the repository.xml and remove the last component + // to get the base + String repoXml = src.getUrl(); + int pos = repoXml.lastIndexOf('/'); + String base = repoXml.substring(0, pos + 1); + + link = base + link; + } + + if (forceHttp) { + link = link.replaceAll("https://", "http://"); //$NON-NLS-1$ //$NON-NLS-2$ + } + + // Get the basename of the file we're downloading, i.e. the last component + // of the URL + int pos = link.lastIndexOf('/'); + String base = link.substring(pos + 1); + + // Rather than create a real temp file in the system, we simply use our + // temp folder (in the SDK base folder) and use the archive name for the + // download. This allows us to reuse or continue downloads. + + File tmpFolder = getTempFolder(osSdkRoot); + if (!mFileOp.isDirectory(tmpFolder)) { + if (mFileOp.isFile(tmpFolder)) { + mFileOp.deleteFileOrFolder(tmpFolder); + } + if (!mFileOp.mkdirs(tmpFolder)) { + monitor.logError("Failed to create directory %1$s", tmpFolder.getPath()); + return null; + } + } + File tmpFile = new File(tmpFolder, base); + + // property file were we'll keep partial/resume information for reuse. + File propsFile = new File(tmpFolder, base + ".inf"); //$NON-NLS-1$ + + // if the file exists, check its checksum & size. Use it if complete + if (mFileOp.exists(tmpFile)) { + if (mFileOp.length(tmpFile) == archive.getSize()) { + String chksum = ""; //$NON-NLS-1$ + try { + chksum = fileChecksum(archive.getChecksumType().getMessageDigest(), + tmpFile, + monitor); + } catch (NoSuchAlgorithmException e) { + // Ignore. + } + if (chksum.equalsIgnoreCase(archive.getChecksum())) { + // File is good, let's use it. + return Pair.of(tmpFile, propsFile); + } else { + // The file has the right size but the wrong content. + // Just remove it and this will trigger a full download below. + mFileOp.deleteFileOrFolder(tmpFile); + } + } + } + + Header[] resumeHeaders = preparePartialDownload(archive, tmpFile, propsFile); + + if (fetchUrl(archive, resumeHeaders, tmpFile, propsFile, link, pkgName, cache, monitor)) { + // Fetching was successful, let's use this file. + return Pair.of(tmpFile, propsFile); + } + return null; + } + + /** + * Prepares to do a partial/resume download. + * + * @param archive The archive we're trying to download. + * @param tmpFile The destination file to download (e.g. something.zip) + * @param propsFile A properties file generated by the last partial download (e.g. .zip.inf) + * @return Null in case we should perform a full download, or a set of headers + * to resume a partial download. + */ + private Header[] preparePartialDownload(Archive archive, File tmpFile, File propsFile) { + // We need both the destination file and its properties to do a resume. + if (mFileOp.isFile(tmpFile) && mFileOp.isFile(propsFile)) { + // The caller already checked the case were the destination file has the + // right size _and_ checksum, so we know at this point one of them is wrong + // here. + // We can obviously only resume a file if its size is smaller than expected. + if (mFileOp.length(tmpFile) < archive.getSize()) { + Properties props = mFileOp.loadProperties(propsFile); + + List

headers = new ArrayList
(2); + headers.add(new BasicHeader(HttpHeaders.RANGE, + String.format("bytes=%d-", mFileOp.length(tmpFile)))); + + // Don't use the properties if there's not at least a 200 or 206 code from + // the last download. + int status = 0; + try { + status = Integer.parseInt(props.getProperty(PROP_STATUS_CODE)); + } catch (Exception ignore) {} + + if (status == HttpStatus.SC_OK || status == HttpStatus.SC_PARTIAL_CONTENT) { + // Do we have an ETag and/or a Last-Modified? + String etag = props.getProperty(HttpHeaders.ETAG); + String lastMod = props.getProperty(HttpHeaders.LAST_MODIFIED); + + if (etag != null && !etag.isEmpty()) { + headers.add(new BasicHeader(HttpHeaders.IF_MATCH, etag)); + } else if (lastMod != null && !lastMod.isEmpty()) { + headers.add(new BasicHeader(HttpHeaders.IF_MATCH, lastMod)); + } + + return headers.toArray(new Header[headers.size()]); + } + } + } + + // Existing file is either of different size or content. + // Remove the existing file and request a full download. + mFileOp.deleteFileOrFolder(tmpFile); + mFileOp.deleteFileOrFolder(propsFile); + + return null; + } + + /** + * Computes the SHA-1 checksum of the content of the given file. + * Returns an empty string on error (rather than null). + */ + private String fileChecksum(MessageDigest digester, File tmpFile, ITaskMonitor monitor) { + InputStream is = null; + try { + is = new FileInputStream(tmpFile); + + byte[] buf = new byte[65536]; + int n; + + while ((n = is.read(buf)) >= 0) { + if (n > 0) { + digester.update(buf, 0, n); + } + } + + return getDigestChecksum(digester); + + } catch (FileNotFoundException e) { + // The FNF message is just the URL. Make it a bit more useful. + monitor.logError("File not found: %1$s", e.getMessage()); + + } catch (Exception e) { + monitor.logError("%1$s", e.getMessage()); //$NON-NLS-1$ + + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + // pass + } + } + } + + return ""; //$NON-NLS-1$ + } + + /** + * Returns the SHA-1 from a {@link MessageDigest} as an hex string + * that can be compared with {@link Archive#getChecksum()}. + */ + private String getDigestChecksum(MessageDigest digester) { + int n; + // Create an hex string from the digest + byte[] digest = digester.digest(); + n = digest.length; + String hex = "0123456789abcdef"; //$NON-NLS-1$ + char[] hexDigest = new char[n * 2]; + for (int i = 0; i < n; i++) { + int b = digest[i] & 0x0FF; + hexDigest[i*2 + 0] = hex.charAt(b >>> 4); + hexDigest[i*2 + 1] = hex.charAt(b & 0x0f); + } + + return new String(hexDigest); + } + + /** + * Actually performs the download. + * Also computes the SHA1 of the file on the fly. + *

+ * Success is defined as downloading as many bytes as was expected and having the same + * SHA1 as expected. Returns true on success or false if any of those checks fail. + *

+ * Increments the monitor by {@link #NUM_MONITOR_INC}. + * + * @param archive The archive we're trying to download. + * @param resumeHeaders The headers to use for a partial resume, or null when fetching + * a whole new file. + * @param tmpFile The destination file to download (e.g. something.zip) + * @param propsFile A properties file generated by the last partial download (e.g. .zip.inf) + * @param urlString The URL as a string + * @param pkgName The archive's package name, used for progress output. + * @param cache The {@link DownloadCache} instance to use. + * @param monitor The monitor to output the progress and errors. + * @return True if we fetched the file successfully. + * False if the download failed or was aborted. + */ + private boolean fetchUrl(Archive archive, + Header[] resumeHeaders, + File tmpFile, + File propsFile, + String urlString, + String pkgName, + DownloadCache cache, + ITaskMonitor monitor) { + + FileOutputStream os = null; + InputStream is = null; + int inc_remain = NUM_MONITOR_INC; + try { + Pair result = + cache.openDirectUrl(urlString, resumeHeaders, monitor); + + is = result.getFirst(); + HttpResponse resp = result.getSecond(); + int status = resp.getStatusLine().getStatusCode(); + if (status == HttpStatus.SC_NOT_FOUND) { + throw new Exception("URL not found."); + } + if (is == null) { + throw new Exception("No content."); + } + + + Properties props = new Properties(); + props.setProperty(PROP_STATUS_CODE, Integer.toString(status)); + if (resp.containsHeader(HttpHeaders.ETAG)) { + props.setProperty(HttpHeaders.ETAG, + resp.getFirstHeader(HttpHeaders.ETAG).getValue()); + } + if (resp.containsHeader(HttpHeaders.LAST_MODIFIED)) { + props.setProperty(HttpHeaders.LAST_MODIFIED, + resp.getFirstHeader(HttpHeaders.LAST_MODIFIED).getValue()); + } + + try { + mFileOp.saveProperties(propsFile, props, "## Android SDK Download."); //$NON-NLS-1$ + } catch (IOException ignore) {} + + // On success, status can be: + // - 206 (Partial content), if resumeHeaders is not null (we asked for a partial + // download, and we get partial content for that download => we'll need to append + // to the existing file.) + // - 200 (OK) meaning we're getting whole new content from scratch. This can happen + // even if resumeHeaders is not null (typically means the server has a new version + // of the file to serve.) In this case we reset the file and write from scratch. + + boolean append = status == HttpStatus.SC_PARTIAL_CONTENT; + if (status != HttpStatus.SC_OK && !(append && resumeHeaders != null)) { + throw new Exception(String.format("Unexpected HTTP Status %1$d", status)); + } + MessageDigest digester = archive.getChecksumType().getMessageDigest(); + + if (append) { + // Seed the digest with the existing content. + InputStream temp = null; + try { + temp = new FileInputStream(tmpFile); + + byte[] buf = new byte[65536]; + int n; + + while ((n = temp.read(buf)) >= 0) { + if (n > 0) { + digester.update(buf, 0, n); + } + } + } catch (Exception ignore) { + } finally { + if (temp != null) { + try { + temp.close(); + } catch (IOException ignore) {} + } + } + } + + // Open the output stream in append for a resume, or reset for a full download. + os = new FileOutputStream(tmpFile, append); + + byte[] buf = new byte[65536]; + int n; + + long total = 0; + long size = archive.getSize(); + if (append) { + long len = mFileOp.length(tmpFile); + int percent = (int) (len * 100 / size); + size -= len; + monitor.logVerbose( + "Resuming %1$s download at %2$d (%3$d%%)", pkgName, len, percent); + } + long inc = size / NUM_MONITOR_INC; + long next_inc = inc; + + long startMs = System.currentTimeMillis(); + long nextMs = startMs + 2000; // start update after 2 seconds + + while ((n = is.read(buf)) >= 0) { + if (n > 0) { + os.write(buf, 0, n); + digester.update(buf, 0, n); + } + + long timeMs = System.currentTimeMillis(); + + total += n; + if (total >= next_inc) { + monitor.incProgress(1); + inc_remain--; + next_inc += inc; + } + + if (timeMs > nextMs) { + long delta = timeMs - startMs; + if (total > 0 && delta > 0) { + // percent left to download + int percent = (int) (100 * total / size); + // speed in KiB/s + float speed = (float)total / (float)delta * (1000.f / 1024.f); + // time left to download the rest at the current KiB/s rate + int timeLeft = (speed > 1e-3) ? + (int)(((size - total) / 1024.0f) / speed) : + 0; + String timeUnit = "seconds"; + if (timeLeft > 120) { + timeUnit = "minutes"; + timeLeft /= 60; + } + + monitor.setDescription( + "Downloading %1$s (%2$d%%, %3$.0f KiB/s, %4$d %5$s left)", + pkgName, + percent, + speed, + timeLeft, + timeUnit); + } + nextMs = timeMs + 1000; // update every second + } + + if (monitor.isCancelRequested()) { + monitor.log("Download aborted by user at %1$d bytes.", total); + return false; + } + + } + + if (total != size) { + monitor.logError( + "Download finished with wrong size. Expected %1$d bytes, got %2$d bytes.", + size, total); + return false; + } + + // Create an hex string from the digest + String actual = getDigestChecksum(digester); + String expected = archive.getChecksum(); + if (!actual.equalsIgnoreCase(expected)) { + monitor.logError("Download finished with wrong checksum. Expected %1$s, got %2$s.", + expected, actual); + return false; + } + + return true; + + } catch (CanceledByUserException e) { + // HTTP Basic Auth or NTLM login was canceled by user. + // Don't output an error in the log. + + } catch (FileNotFoundException e) { + // The FNF message is just the URL. Make it a bit more useful. + monitor.logError("URL not found: %1$s", e.getMessage()); + + } catch (Exception e) { + monitor.logError("Download interrupted: %1$s", e.getMessage()); //$NON-NLS-1$ + + } finally { + if (os != null) { + try { + os.close(); + } catch (IOException e) { + // pass + } + } + + if (is != null) { + try { + is.close(); + } catch (IOException e) { + // pass + } + } + if (inc_remain > 0) { + monitor.incProgress(inc_remain); + } + } + + return false; + } + + /** + * Install the given archive in the given folder. + */ + private boolean unarchive(ArchiveReplacement archiveInfo, + String osSdkRoot, + File archiveFile, + SdkManager sdkManager, + ITaskMonitor monitor) { + boolean success = false; + Archive newArchive = archiveInfo.getNewArchive(); + Package pkg = newArchive.getParentPackage(); + String pkgName = pkg.getShortDescription(); + monitor.setDescription("Installing %1$s", pkgName); + monitor.log("Installing %1$s", pkgName); + + // Ideally we want to always unzip in a temp folder which name depends on the package + // type (e.g. addon, tools, etc.) and then move the folder to the destination folder. + // If the destination folder exists, it will be renamed and deleted at the very + // end if everything succeeded. This provides a nice atomic swap and should leave the + // original folder untouched in case something wrong (e.g. program crash) in the + // middle of the unzip operation. + // + // However that doesn't work on Windows, we always end up not being able to move the + // new folder. There are actually 2 cases: + // A- A process such as a the explorer is locking the *old* folder or a file inside + // (e.g. adb.exe) + // In this case we really shouldn't be tried to work around it and we need to let + // the user know and let it close apps that access that folder. + // B- A process is locking the *new* folder. Very often this turns to be a file indexer + // or an anti-virus that is busy scanning the new folder that we just unzipped. + // + // So we're going to change the strategy: + // 1- Try to move the old folder to a temp/old folder. This might fail in case of issue A. + // Note: for platform-tools, we can try killing adb first. + // If it still fails, we do nothing and ask the user to terminate apps that can be + // locking that folder. + // 2- Once the old folder is out of the way, we unzip the archive directly into the + // optimal new location. We no longer unzip it in a temp folder and move it since we + // know that's what fails in most of the cases. + // 3- If the unzip fails, remove everything and try to restore the old folder by doing + // a *copy* in place and not a folder move (which will likely fail too). + + String pkgKind = pkg.getClass().getSimpleName(); + + File destFolder = null; + File oldDestFolder = null; + + try { + // -0- Compute destination directory and check install pre-conditions + + destFolder = pkg.getInstallFolder(osSdkRoot, sdkManager); + + if (destFolder == null) { + // this should not seriously happen. + monitor.log("Failed to compute installation directory for %1$s.", pkgName); + return false; + } + + if (!pkg.preInstallHook(newArchive, monitor, osSdkRoot, destFolder)) { + monitor.log("Skipping archive: %1$s", pkgName); + return false; + } + + // -1- move old folder. + + if (mFileOp.exists(destFolder)) { + // Create a new temp/old dir + if (oldDestFolder == null) { + oldDestFolder = getNewTempFolder(osSdkRoot, pkgKind, "old"); //$NON-NLS-1$ + } + if (oldDestFolder == null) { + // this should not seriously happen. + monitor.logError("Failed to find a temp directory in %1$s.", osSdkRoot); + return false; + } + + // Try to move the current dest dir to the temp/old one. Tell the user if it failed. + while(true) { + if (!moveFolder(destFolder, oldDestFolder)) { + monitor.logError("Failed to rename directory %1$s to %2$s.", + destFolder.getPath(), oldDestFolder.getPath()); + + if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) { + boolean tryAgain = true; + + tryAgain = windowsDestDirLocked(osSdkRoot, destFolder, monitor); + + if (tryAgain) { + // loop, trying to rename the temp dir into the destination + continue; + } else { + return false; + } + } + } + break; + } + } + + assert !mFileOp.exists(destFolder); + + // -2- Unzip new content directly in place. + + if (!mFileOp.mkdirs(destFolder)) { + monitor.logError("Failed to create directory %1$s", destFolder.getPath()); + return false; + } + + if (!unzipFolder(archiveInfo, + archiveFile, + destFolder, + monitor)) { + return false; + } + + if (!generateSourceProperties(newArchive, destFolder)) { + monitor.logError("Failed to generate source.properties in directory %1$s", + destFolder.getPath()); + return false; + } + + // In case of success, if we were replacing an archive + // and the older one had a different path, remove it now. + Archive oldArchive = archiveInfo.getReplaced(); + if (oldArchive != null && oldArchive.isLocal()) { + String oldPath = oldArchive.getLocalOsPath(); + File oldFolder = oldPath == null ? null : new File(oldPath); + if (oldFolder == null && oldArchive.getParentPackage() != null) { + oldFolder = oldArchive.getParentPackage().getInstallFolder( + osSdkRoot, sdkManager); + } + if (oldFolder != null && mFileOp.exists(oldFolder) && + !oldFolder.equals(destFolder)) { + monitor.logVerbose("Removing old archive at %1$s", oldFolder.getAbsolutePath()); + mFileOp.deleteFileOrFolder(oldFolder); + } + } + + success = true; + pkg.postInstallHook(newArchive, monitor, destFolder); + return true; + + } finally { + if (!success) { + // In case of failure, we try to restore the old folder content. + if (oldDestFolder != null) { + restoreFolder(oldDestFolder, destFolder); + } + + // We also call the postInstallHool with a null directory to give a chance + // to the archive to cleanup after preInstallHook. + pkg.postInstallHook(newArchive, monitor, null /*installDir*/); + } + + // Cleanup if the unzip folder is still set. + mFileOp.deleteFileOrFolder(oldDestFolder); + } + } + + private boolean windowsDestDirLocked( + String osSdkRoot, + File destFolder, + final ITaskMonitor monitor) { + String msg = null; + + assert SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS; + + File findLockExe = LegacyFileOp.append( + osSdkRoot, SdkConstants.FD_TOOLS, SdkConstants.FD_LIB, SdkConstants.FN_FIND_LOCK); + + if (mFileOp.exists(findLockExe)) { + try { + final StringBuilder result = new StringBuilder(); + String command[] = new String[] { + findLockExe.getAbsolutePath(), + destFolder.getAbsolutePath() + }; + Process process = Runtime.getRuntime().exec(command); + int retCode = GrabProcessOutput.grabProcessOutput( + process, + Wait.WAIT_FOR_READERS, + new IProcessOutput() { + @Override + public void out(@Nullable String line) { + if (line != null) { + result.append(line).append("\n"); + } + } + + @Override + public void err(@Nullable String line) { + if (line != null) { + monitor.logError("[find_lock] Error: %1$s", line); + } + } + }); + + if (retCode == 0 && result.length() > 0) { + // TODO create a better dialog + + String found = result.toString().trim(); + monitor.logError("[find_lock] Directory locked by %1$s", found); + + TreeSet apps = new TreeSet(Arrays.asList( + found.split(Pattern.quote(";")))); //$NON-NLS-1$ + StringBuilder appStr = new StringBuilder(); + for (String app : apps) { + appStr.append("\n - ").append(app.trim()); //$NON-NLS-1$ + } + + msg = String.format( + "-= Warning ! =-\n" + + "The following processes: %1$s\n" + + "are locking the following directory: \n" + + " %2$s\n" + + "Please close these applications so that the installation can continue.\n" + + "When ready, press YES to try again.", + appStr.toString(), + destFolder.getPath()); + } + + } catch (Exception e) { + monitor.error(e, "[find_lock failed]"); + } + + + } + + if (msg == null) { + // Old way: simply display a generic text and let user figure it out. + msg = String.format( + "-= Warning ! =-\n" + + "A folder failed to be moved. On Windows this " + + "typically means that a program is using that folder (for " + + "example Windows Explorer or your anti-virus software.)\n" + + "Please momentarily deactivate your anti-virus software or " + + "close any running programs that may be accessing the " + + "directory '%1$s'.\n" + + "When ready, press YES to try again.", + destFolder.getPath()); + } + + boolean tryAgain = monitor.displayPrompt("SDK Manager: failed to install", msg); + return tryAgain; + } + + /** + * Tries to rename/move a folder. + *

+ * Contract: + *

    + *
  • When we start, oldDir must exist and be a directory. newDir must not exist.
  • + *
  • On successful completion, oldDir must not exists. + * newDir must exist and have the same content.
  • + *
  • On failure completion, oldDir must have the same content as before. + * newDir must not exist.
  • + *
+ *

+ * The simple "rename" operation on a folder can typically fail on Windows for a variety + * of reason, in fact as soon as a single process holds a reference on a directory. The + * most common case are the Explorer, the system's file indexer, Tortoise SVN cache or + * an anti-virus that are busy indexing a new directory having been created. + * + * @param oldDir The old location to move. It must exist and be a directory. + * @param newDir The new location where to move. It must not exist. + * @return True if the move succeeded. On failure, we try hard to not have touched the old + * directory in order not to loose its content. + */ + private boolean moveFolder(File oldDir, File newDir) { + // This is a simple folder rename that works on Linux/Mac all the time. + // + // On Windows this might fail if an indexer is busy looking at a new directory + // (e.g. right after we unzip our archive), so it fails let's be nice and give + // it a bit of time to succeed. + for (int i = 0; i < 5; i++) { + if (mFileOp.renameTo(oldDir, newDir)) { + return true; + } + try { + Thread.sleep(500 /*ms*/); + } catch (InterruptedException e) { + // ignore + } + } + + return false; + } + + /** + * Unzips a zip file into the given destination directory. + * + * The archive file MUST have a unique "root" folder. + * This root folder is skipped when unarchiving. + */ + @SuppressWarnings("unchecked") + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected boolean unzipFolder( + ArchiveReplacement archiveInfo, + File archiveFile, + File unzipDestFolder, + ITaskMonitor monitor) { + + Archive newArchive = archiveInfo.getNewArchive(); + Package pkg = newArchive.getParentPackage(); + String pkgName = pkg.getShortDescription(); + long compressedSize = newArchive.getSize(); + + ZipFile zipFile = null; + try { + zipFile = new ZipFile(archiveFile); + + // To advance the percent and the progress bar, we don't know the number of + // items left to unzip. However we know the size of the archive and the size of + // each uncompressed item. The zip file format overhead is negligible so that's + // a good approximation. + long incStep = compressedSize / NUM_MONITOR_INC; + long incTotal = 0; + long incCurr = 0; + int lastPercent = 0; + + byte[] buf = new byte[65536]; + + Enumeration entries = zipFile.getEntries(); + while (entries.hasMoreElements()) { + ZipArchiveEntry entry = entries.nextElement(); + + String name = entry.getName(); + + // ZipFile entries should have forward slashes, but not all Zip + // implementations can be expected to do that. + name = name.replace('\\', '/'); + + // Zip entries are always packages in a top-level directory (e.g. docs/index.html). + int pos = name.indexOf('/'); + if (pos == -1) { + // All zip entries should have a root folder. + // This zip entry seems located at the root of the zip. + // Rather than ignore the file, just place it at the root. + } else if (pos == name.length() - 1) { + // This is a zip *directory* entry in the form dir/, so essentially + // it's the root directory of the SDK. It's safe to ignore that one + // since we want to use our own root directory and we'll recreate + // root directories as needed. + // A direct consequence is that if a malformed archive has multiple + // root directories, their content will all be merged together. + continue; + } else { + // This is the expected behavior: the zip entry is in the form root/file + // or root/dir/. We want to use our top-level directory so we drop the + // first segment of the path name. + name = name.substring(pos + 1); + } + + File destFile = new File(unzipDestFolder, name); + + if (name.endsWith("/")) { //$NON-NLS-1$ + // Create directory if it doesn't exist yet. This allows us to create + // empty directories. + if (!mFileOp.isDirectory(destFile) && !mFileOp.mkdirs(destFile)) { + monitor.logError("Failed to create directory %1$s", + destFile.getPath()); + return false; + } + continue; + } else if (name.indexOf('/') != -1) { + // Otherwise it's a file in a sub-directory. + + // Sanity check: since we're always unzipping in a fresh temp folder + // the destination file shouldn't already exist. + if (mFileOp.exists(destFile)) { + monitor.logVerbose("Duplicate file found: %1$s", name); + } + + // Make sure the parent directory has been created. + File parentDir = destFile.getParentFile(); + if (!mFileOp.isDirectory(parentDir)) { + if (!mFileOp.mkdirs(parentDir)) { + monitor.logError("Failed to create directory %1$s", + parentDir.getPath()); + return false; + } + } + } + + FileOutputStream fos = null; + long remains = entry.getSize(); + try { + fos = new FileOutputStream(destFile); + + // Java bug 4040920: do not rely on the input stream EOF and don't + // try to read more than the entry's size. + InputStream entryContent = zipFile.getInputStream(entry); + int n; + while (remains > 0 && + (n = entryContent.read( + buf, 0, (int) Math.min(remains, buf.length))) != -1) { + remains -= n; + if (n > 0) { + fos.write(buf, 0, n); + } + } + } catch (EOFException e) { + monitor.logError("Error uncompressing file %s. Size: %d bytes, Unwritten: %d bytes.", + entry.getName(), entry.getSize(), remains); + throw e; + } finally { + if (fos != null) { + fos.close(); + } + } + + pkg.postUnzipFileHook(newArchive, monitor, mFileOp, destFile, entry); + + // Increment progress bar to match. We update only between files. + for(incTotal += entry.getCompressedSize(); incCurr < incTotal; incCurr += incStep) { + monitor.incProgress(1); + } + + int percent = (int) (100 * incTotal / compressedSize); + if (percent != lastPercent) { + monitor.setDescription("Unzipping %1$s (%2$d%%)", pkgName, percent); + lastPercent = percent; + } + + if (monitor.isCancelRequested()) { + return false; + } + } + + return true; + + } catch (IOException e) { + monitor.logError("Unzip failed: %1$s", e.getMessage()); + + } finally { + if (zipFile != null) { + try { + zipFile.close(); + } catch (IOException e) { + // pass + } + } + } + + return false; + } + + /** + * Returns an unused temp folder path in the form of osBasePath/temp/prefix.suffixNNN. + *

+ * This does not actually create the folder. It just scan the base path for + * a free folder name to use and returns the file to use to reference it. + *

+ * This operation is not atomic so there's no guarantee the folder can't get + * created in between. This is however unlikely and the caller can assume the + * returned folder does not exist yet. + *

+ * Returns null if no such folder can be found (e.g. if all candidates exist, + * which is rather unlikely) or if the base temp folder cannot be created. + */ + private File getNewTempFolder(String osBasePath, String prefix, String suffix) { + File baseTempFolder = getTempFolder(osBasePath); + + if (!mFileOp.isDirectory(baseTempFolder)) { + if (mFileOp.isFile(baseTempFolder)) { + mFileOp.deleteFileOrFolder(baseTempFolder); + } + if (!mFileOp.mkdirs(baseTempFolder)) { + return null; + } + } + + for (int i = 1; i < 100; i++) { + File folder = new File(baseTempFolder, + String.format("%1$s.%2$s%3$02d", prefix, suffix, i)); //$NON-NLS-1$ + if (!mFileOp.exists(folder)) { + return folder; + } + } + return null; + } + + /** + * Returns the single fixed "temp" folder used by the SDK Manager. + * This folder is always at osBasePath/temp. + *

+ * This does not actually create the folder. + */ + private File getTempFolder(String osBasePath) { + File baseTempFolder = new File(osBasePath, RepoConstants.FD_TEMP); + return baseTempFolder; + } + + /** + * Generates a source.properties in the destination folder that contains all the infos + * relevant to this archive, this package and the source so that we can reload them + * locally later. + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected boolean generateSourceProperties(Archive archive, File unzipDestFolder) { + + // Create a version of Properties that returns a sorted key set. + // This is used by Properties#saveProperties and should ensure the + // properties are in a stable order. Unit tests rely on this fact. + @SuppressWarnings("serial") + Properties props = new Properties() { + @Override + public synchronized Enumeration keys() { + Set sortedSet = new TreeSet(keySet()); + final Iterator it = sortedSet.iterator(); + return new Enumeration() { + @Override + public boolean hasMoreElements() { + return it.hasNext(); + } + + @Override + public Object nextElement() { + return it.next(); + } + + }; + } + }; + + archive.saveProperties(props); + + Package pkg = archive.getParentPackage(); + if (pkg != null) { + pkg.saveProperties(props); + } + + try { + mFileOp.saveProperties( + new File(unzipDestFolder, SdkConstants.FN_SOURCE_PROP), + props, + "## Android Tool: Source of this archive."); //$NON-NLS-1$ + return true; + } catch (IOException ignore) { + return false; + } + } + + /** + * Recursively restore srcFolder into destFolder by performing a copy of the file + * content rather than rename/moves. + * + * @param srcFolder The source folder to restore. + * @param destFolder The destination folder where to restore. + * @return True if the folder was successfully restored, false if it was not at all or + * only partially restored. + */ + private boolean restoreFolder(File srcFolder, File destFolder) { + boolean result = true; + + // Process sub-folders first + File[] srcFiles = mFileOp.listFiles(srcFolder); + if (srcFiles == null) { + // Source does not exist. That is quite odd. + return false; + } + + if (mFileOp.isFile(destFolder)) { + if (!mFileOp.delete(destFolder)) { + // There's already a file in there where we want a directory and + // we can't delete it. This is rather unexpected. Just give up on + // that folder. + return false; + } + } else if (!mFileOp.isDirectory(destFolder)) { + mFileOp.mkdirs(destFolder); + } + + // Get all the files and dirs of the current destination. + // We are not going to clean up the destination first. + // Instead we'll copy over and just remove any remaining files or directories. + Set destDirs = new HashSet(); + Set destFiles = new HashSet(); + File[] files = mFileOp.listFiles(destFolder); + if (files != null) { + for (File f : files) { + if (mFileOp.isDirectory(f)) { + destDirs.add(f); + } else { + destFiles.add(f); + } + } + } + + // First restore all source directories. + for (File dir : srcFiles) { + if (mFileOp.isDirectory(dir)) { + File d = new File(destFolder, dir.getName()); + destDirs.remove(d); + if (!restoreFolder(dir, d)) { + result = false; + } + } + } + + // Remove any remaining directories not processed above. + for (File dir : destDirs) { + mFileOp.deleteFileOrFolder(dir); + } + + // Copy any source files over to the destination. + for (File file : srcFiles) { + if (mFileOp.isFile(file)) { + File f = new File(destFolder, file.getName()); + destFiles.remove(f); + try { + mFileOp.copyFile(file, f); + } catch (IOException e) { + result = false; + } + } + } + + // Remove any remaining files not processed above. + for (File file : destFiles) { + mFileOp.deleteFileOrFolder(file); + } + + return result; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/ArchiveReplacement.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/ArchiveReplacement.java new file mode 100644 index 00000000..4cf85f2c --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/ArchiveReplacement.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2011 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.sdklib.internal.repository.archives; + +import com.android.sdklib.repository.IDescription; +import com.android.sdklib.internal.repository.packages.Package; + + +/** + * Represents an archive that we want to install and the archive that it is + * going to replace, if any. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class ArchiveReplacement implements IDescription { + + private final Archive mNewArchive; + private final Archive mReplaced; + + /** + * Creates a new replacement where the {@code newArchive} will replace the + * currently installed {@code replaced} archive. + * When {@code newArchive} is not intended to replace anything (e.g. because + * the user is installing a new package not present on her system yet), then + * {@code replace} shall be null. + * + * @param newArchive A "new archive" to be installed. This is always an archive + * that comes from a remote site. This may be null. + * @param replaced An optional local archive that the new one will replace. + * Can be null if this archive does not replace anything. + */ + public ArchiveReplacement(Archive newArchive, Archive replaced) { + mNewArchive = newArchive; + mReplaced = replaced; + } + + /** + * Returns the "new archive" to be installed. + * This may be null for missing archives. + */ + public Archive getNewArchive() { + return mNewArchive; + } + + /** + * Returns an optional local archive that the new one will replace. + * Can be null if this archive does not replace anything. + */ + public Archive getReplaced() { + return mReplaced; + } + + /** + * Returns the long description of the parent package of the new archive, if not null. + * Otherwise returns an empty string. + */ + @Override + public String getLongDescription() { + if (mNewArchive != null) { + Package p = mNewArchive.getParentPackage(); + if (p != null) { + return p.getLongDescription(); + } + } + return ""; + } + + /** + * Returns the short description of the parent package of the new archive, if not null. + * Otherwise returns an empty string. + */ + @Override + public String getShortDescription() { + if (mNewArchive != null) { + Package p = mNewArchive.getParentPackage(); + if (p != null) { + return p.getShortDescription(); + } + } + return ""; + } + + /** + * Returns the short description of the parent package of the new archive, if not null. + * Otherwise returns the default Object toString result. + *

+ * This is mostly helpful for debugging. For UI display, use the {@link IDescription} + * interface. + */ + @Override + public String toString() { + if (mNewArchive != null) { + Package p = mNewArchive.getParentPackage(); + if (p != null) { + return p.getShortDescription(); + } + } + return super.toString(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/BitSize.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/BitSize.java new file mode 100644 index 00000000..5f72c76f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/BitSize.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2014 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.sdklib.internal.repository.archives; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; + +/** + * The Architecture that this archive can be downloaded on. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public enum BitSize { + _32(32), + _64(64); + + private final int mSize; + + BitSize(int size) { + mSize = size; + } + + /** Returns the size of the architecture. */ + public int getSize() { + return mSize; + } + + /** Returns the XML name of the bit size. */ + @NonNull + public String getXmlName() { + return Integer.toString(mSize); + } + /** + * Returns the enum value matching the given XML name. + * @return A valid {@link HostOs} constant or null if not a valid XML name. + */ + @Nullable + public static BitSize fromXmlName(@Nullable String xmlName) { + if (xmlName != null) { + for (BitSize v : values()) { + if (v.getXmlName().equalsIgnoreCase(xmlName)) { + return v; + } + } + } + return null; + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/ChecksumType.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/ChecksumType.java new file mode 100644 index 00000000..93049345 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/ChecksumType.java @@ -0,0 +1,54 @@ +/* + * 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.sdklib.internal.repository.archives; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * The checksum type. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public enum ChecksumType { + /** A SHA1 checksum, represented as a 40-hex string. */ + SHA1("SHA-1"); //$NON-NLS-1$ + + private final String mAlgorithmName; + + /** + * Constructs a {@link ChecksumType} with the algorithm name + * suitable for {@link MessageDigest#getInstance(String)}. + *

+ * These names are officially documented at + * http://java.sun.com/javase/6/docs/technotes/guides/security/StandardNames.html#MessageDigest + */ + ChecksumType(String algorithmName) { + mAlgorithmName = algorithmName; + } + + /** + * Returns a new {@link MessageDigest} instance for this checksum type. + * @throws NoSuchAlgorithmException if this algorithm is not available. + */ + public MessageDigest getMessageDigest() throws NoSuchAlgorithmException { + return MessageDigest.getInstance(mAlgorithmName); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/HostOs.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/HostOs.java new file mode 100644 index 00000000..6c7d4431 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/HostOs.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2014 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.sdklib.internal.repository.archives; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; + +import java.util.Locale; + +/** + * The OS that this archive can be downloaded on.
+ * The represents a "host" where the SDK tools and the SDK Manager can run, + * not the Android device targets. + *

+ * The actual OS requirements for the SDK are listed at + * http://d.android.com/sdk + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public enum HostOs { + /** Any of the Unix-like host OSes. */ + LINUX("Linux"), + /** Any variation of MacOS X. */ + MACOSX("MacOS X"), + /** Any variation of Windows. */ + WINDOWS("Windows"); + + private final String mUiName; + + HostOs(@NonNull String uiName) { + mUiName = uiName; + } + + /** + * Returns the UI name of the OS. + */ + @NonNull + public String getUiName() { + return mUiName; + } + + /** + * Returns the XML name of the OS. + * @returns Null, windows, macosx or linux. + */ + @NonNull + public String getXmlName() { + return toString().toLowerCase(Locale.US); + } + + /** + * Returns the enum value matching the given XML name. + * @return A valid {@link HostOs} constnat or null if not a valid XML name. + */ + @Nullable + public static HostOs fromXmlName(@Nullable String xmlName) { + if (xmlName != null) { + for (HostOs v : values()) { + if (v.getXmlName().equalsIgnoreCase(xmlName)) { + return v; + } + } + } + return null; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/LegacyArch.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/LegacyArch.java new file mode 100644 index 00000000..51d78dc7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/LegacyArch.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2014 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.sdklib.internal.repository.archives; + + + +/** + * The legacy Architecture that this archive can be downloaded on. + *

+ * This attribute was used for the <archive> element in repo schema 1-9. + * add-on schema 1-6 and sys-img schema 1-2. + * Starting with repo schema 10, add-on schema 7 and sys-img schema 3, this is replaced + * by the <host-bit> and <jvm-bit> elements and {@link ArchFilter}. + * + * @see HostOs + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public enum LegacyArch { + ANY("Any"), + PPC("PowerPC"), + X86("x86"), + X86_64("x86_64"); + + private final String mUiName; + + LegacyArch(String uiName) { + mUiName = uiName; + } + + /** Returns the UI name of the architecture. */ + public String getUiName() { + return mUiName; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/LegacyOs.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/LegacyOs.java new file mode 100644 index 00000000..2f096b47 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/LegacyOs.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2014 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.sdklib.internal.repository.archives; + + + +/** + * The legacy OS that this archive can be downloaded on. + *

+ * This attribute was used for the <archive> element in repo schema 1-9. + * add-on schema 1-6 and sys-img schema 1-2. + * Starting with repo schema 10, add-on schema 7 and sys-img schema 3, this is replaced + * by the <host-os> element and {@link ArchFilter}. + * + * @see HostOs + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public enum LegacyOs { + ANY("Any"), + LINUX("Linux"), + MACOSX("MacOS X"), + WINDOWS("Windows"); + + private final String mUiName; + + LegacyOs(String uiName) { + mUiName = uiName; + } + + /** Returns the UI name of the OS. */ + public String getUiName() { + return mUiName; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/AddonPackage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/AddonPackage.java new file mode 100644 index 00000000..f5c404db --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/AddonPackage.java @@ -0,0 +1,765 @@ +/* + * 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.sdklib.internal.repository.packages; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.VisibleForTesting; +import com.android.annotations.VisibleForTesting.Visibility; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.IAndroidTarget.OptionalLibrary; +import com.android.sdklib.SdkManager; +import com.android.sdklib.internal.repository.sources.SdkSource; +import com.android.sdklib.repository.AddonManifestIniProps; +import com.android.sdklib.repository.IDescription; +import com.android.sdklib.repository.MajorRevision; +import com.android.sdklib.repository.PkgProps; +import com.android.sdklib.repository.SdkAddonConstants; +import com.android.sdklib.repository.SdkRepoConstants; +import com.android.sdklib.repository.descriptors.IPkgDesc; +import com.android.sdklib.repository.descriptors.IdDisplay; +import com.android.sdklib.repository.descriptors.PkgDesc; +import com.android.sdklib.repository.local.LocalAddonPkgInfo; +import com.android.utils.Pair; +import com.google.common.collect.ImmutableList; + +import org.w3c.dom.Node; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; + +/** + * Represents an add-on XML node in an SDK repository. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class AddonPackage extends MajorRevisionPackage + implements IAndroidVersionProvider, IPlatformDependency, + IExactApiLevelDependency, ILayoutlibVersion { + + private final String mVendorId; + private final String mVendorDisplay; + private final String mNameId; + private final String mDisplayName; + private final AndroidVersion mVersion; + private final IPkgDesc mPkgDesc; + + /** + * The helper handling the layoutlib version. + */ + private final LayoutlibVersionMixin mLayoutlibVersion; + + /** An add-on library. */ + public static class Lib { + private final String mName; + private final String mDescription; + + public Lib(String name, String description) { + mName = name; + mDescription = description; + } + + public String getName() { + return mName; + } + + public String getDescription() { + return mDescription; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((mDescription == null) ? 0 : mDescription.hashCode()); + result = prime * result + ((mName == null) ? 0 : mName.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof Lib)) { + return false; + } + Lib other = (Lib) obj; + if (mDescription == null) { + if (other.mDescription != null) { + return false; + } + } else if (!mDescription.equals(other.mDescription)) { + return false; + } + if (mName == null) { + if (other.mName != null) { + return false; + } + } else if (!mName.equals(other.mName)) { + return false; + } + return true; + } + } + + private final Lib[] mLibs; + + /** + * Creates a new add-on package from the attributes and elements of the given XML node. + * This constructor should throw an exception if the package cannot be created. + * + * @param source The {@link SdkSource} where this is loaded from. + * @param packageNode The XML element being parsed. + * @param nsUri The namespace URI of the originating XML document, to be able to deal with + * parameters that vary according to the originating XML schema. + * @param licenses The licenses loaded from the XML originating document. + */ + public AddonPackage( + SdkSource source, + Node packageNode, + String nsUri, + Map licenses) { + super(source, packageNode, nsUri, licenses); + + // --- name id/display --- + // addon-4.xsd introduces the name-id, name-display, vendor-id and vendor-display. + // These are not optional but we still need to support a fallback for older addons + // that only provide name and vendor. If the addon provides neither set of fields, + // it will simply not work as expected. + + String nameId = PackageParserUtils.getXmlString(packageNode, + SdkRepoConstants.NODE_NAME_ID); + String nameDisp = PackageParserUtils.getXmlString(packageNode, + SdkRepoConstants.NODE_NAME_DISPLAY); + String name = PackageParserUtils.getXmlString(packageNode, + SdkRepoConstants.NODE_NAME); + + // The old is equivalent to the new + if (nameDisp.isEmpty()) { + nameDisp = name; + } + + // For a missing id, we simply use a sanitized version of the display name + if (nameId.isEmpty()) { + nameId = LocalAddonPkgInfo.sanitizeDisplayToNameId(!name.isEmpty() ? name : nameDisp); + } + + assert !nameId.isEmpty(); + assert !nameDisp.isEmpty(); + + mNameId = nameId.trim(); + mDisplayName = nameDisp.trim(); + + // --- vendor id/display --- + // Same processing for vendor id vs display + + String vendorId = PackageParserUtils.getXmlString(packageNode, + SdkAddonConstants.NODE_VENDOR_ID); + String vendorDisp = PackageParserUtils.getXmlString(packageNode, + SdkAddonConstants.NODE_VENDOR_DISPLAY); + String vendor = PackageParserUtils.getXmlString(packageNode, + SdkAddonConstants.NODE_VENDOR); + + // The old is equivalent to the new + if (vendorDisp.isEmpty()) { + vendorDisp = vendor; + } + + // For a missing id, we simply use a sanitized version of the display vendor + if (vendorId.isEmpty()) { + boolean hasVendor = !vendor.isEmpty(); + vendorId = LocalAddonPkgInfo.sanitizeDisplayToNameId(hasVendor ? vendor : vendorDisp); + } + + assert !vendorId.isEmpty(); + assert !vendorDisp.isEmpty(); + + mVendorId = vendorId.trim(); + mVendorDisplay = vendorDisp.trim(); + + // --- other attributes + + int apiLevel = + PackageParserUtils.getXmlInt(packageNode, SdkAddonConstants.NODE_API_LEVEL, 0); + mVersion = new AndroidVersion(apiLevel, null /*codeName*/); + + mLibs = parseLibs( + PackageParserUtils.findChildElement(packageNode, SdkAddonConstants.NODE_LIBS)); + + mLayoutlibVersion = new LayoutlibVersionMixin(packageNode); + + mPkgDesc = setDescriptions( + PkgDesc.Builder.newAddon(mVersion, (MajorRevision) getRevision(), + new IdDisplay(mVendorId, mVendorDisplay), + new IdDisplay(mNameId, mDisplayName))) + .create(); + } + + /** + * Creates a new platform package based on an actual {@link IAndroidTarget} (which + * {@link IAndroidTarget#isPlatform()} false) from the {@link SdkManager}. + * This is used to list local SDK folders in which case there is one archive which + * URL is the actual target location. + *

+ * By design, this creates a package with one and only one archive. + */ + public static Package create(IAndroidTarget target, Properties props) { + return new AddonPackage(target, props); + } + + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected AddonPackage(IAndroidTarget target, Properties props) { + this(null /*source*/, target, props); + } + + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected AddonPackage(SdkSource source, IAndroidTarget target, Properties props) { + super( source, //source + props, //properties + target.getRevision(), //revision + null, //license + target.getDescription(), //description + null, //descUrl + target.getLocation() //archiveOsPath + ); + + // --- name id/display --- + // addon-4.xsd introduces the name-id, name-display, vendor-id and vendor-display. + // These are not optional but we still need to support a fallback for older addons + // that only provide name and vendor. If the addon provides neither set of fields, + // it will simply not work as expected. + + String nameId = getProperty(props, PkgProps.ADDON_NAME_ID, ""); //$NON-NLS-1$ + String nameDisp = getProperty(props, PkgProps.ADDON_NAME_DISPLAY, ""); //$NON-NLS-1$ + String name = getProperty(props, PkgProps.ADDON_NAME, target.getName()); + + // The old is equivalent to the new + //noinspection ConstantConditions + if (nameDisp.isEmpty()) { + nameDisp = name; + } + + // For a missing id, we simply use a sanitized version of the display name + //noinspection ConstantConditions + if (nameId.isEmpty()) { + nameId = LocalAddonPkgInfo.sanitizeDisplayToNameId(!name.isEmpty() ? name : nameDisp); + } + + assert !nameId.isEmpty(); + assert !nameDisp.isEmpty(); + + mNameId = nameId.trim(); + mDisplayName = nameDisp.trim(); + + // --- vendor id/display --- + // Same processing for vendor id vs display + + String vendorId = getProperty(props, PkgProps.ADDON_VENDOR_ID, ""); //$NON-NLS-1$ + String vendorDisp = getProperty(props, PkgProps.ADDON_VENDOR_DISPLAY, ""); //$NON-NLS-1$ + String vendor = getProperty(props, PkgProps.ADDON_VENDOR, target.getVendor()); + + // The old is equivalent to the new + //noinspection ConstantConditions + if (vendorDisp.isEmpty()) { + vendorDisp = vendor; + } + + // For a missing id, we simply use a sanitized version of the display vendor + //noinspection ConstantConditions + if (vendorId.isEmpty()) { + //noinspection ConstantConditions + boolean hasVendor = !vendor.isEmpty(); + vendorId = LocalAddonPkgInfo.sanitizeDisplayToNameId(hasVendor ? vendor : vendorDisp); + } + + assert !vendorId.isEmpty(); + assert !vendorDisp.isEmpty(); + + mVendorId = vendorId.trim(); + mVendorDisplay = vendorDisp.trim(); + + // --- other attributes + + mVersion = target.getVersion(); + mLayoutlibVersion = new LayoutlibVersionMixin(props); + + List optLibs = target.getAdditionalLibraries(); + if (optLibs.isEmpty()) { + mLibs = new Lib[0]; + } else { + mLibs = new Lib[optLibs.size()]; + for (int i = 0; i < optLibs.size(); i++) { + OptionalLibrary optionalLibrary = optLibs.get(i); + mLibs[i] = new Lib(optionalLibrary.getName(), optionalLibrary.getDescription()); + } + } + + mPkgDesc = setDescriptions( + PkgDesc.Builder.newAddon(mVersion, (MajorRevision) getRevision(), + new IdDisplay(mVendorId, mVendorDisplay), + new IdDisplay(mNameId, mDisplayName))) + .create(); + } + + @Override + @NonNull + public IPkgDesc getPkgDesc() { + return mPkgDesc; + } + + /** + * Creates a broken addon which we know failed to load properly. + * + * @param archiveOsPath The absolute OS path of the addon folder. + * @param sourceProps The properties parsed from the addon's source.properties. Can be null. + * @param addonProps The properties parsed from the addon manifest (NOT the source.properties). + * @param error The error indicating why this addon failed to be loaded. + */ + public static Package createBroken( + String archiveOsPath, + Properties sourceProps, + Map addonProps, + String error) { + + + String nameId = getProperty(sourceProps, PkgProps.ADDON_NAME_ID, null); + String nameDisp = getProperty(sourceProps, PkgProps.ADDON_NAME_DISPLAY, null); + if (nameDisp == null) { + nameDisp = getProperty(sourceProps, PkgProps.ADDON_NAME_DISPLAY, null); + } + if (nameDisp == null) { + nameDisp = addonProps.get(AddonManifestIniProps.ADDON_NAME); + } + if (nameDisp == null) { + nameDisp = "Unknown"; + } + if (nameId == null) { + nameId = LocalAddonPkgInfo.sanitizeDisplayToNameId(nameDisp); + } + + String vendorId = getProperty(sourceProps, PkgProps.ADDON_VENDOR_ID, null); + String vendorDisp = getProperty(sourceProps, PkgProps.ADDON_VENDOR_DISPLAY, null); + if (vendorDisp == null) { + vendorDisp = getProperty(sourceProps, PkgProps.ADDON_VENDOR_DISPLAY, null); + } + if (vendorDisp == null) { + vendorDisp = addonProps.get(AddonManifestIniProps.ADDON_VENDOR); + } + if (vendorDisp == null) { + vendorDisp = "Unknown"; + } + if (vendorId == null) { + vendorId = LocalAddonPkgInfo.sanitizeDisplayToNameId(vendorDisp); + } + + String api = addonProps.get(AddonManifestIniProps.ADDON_API); + String revision = addonProps.get(AddonManifestIniProps.ADDON_REVISION); + + String shortDesc = String.format("%1$s by %2$s, Android API %3$s, revision %4$s [*]", + nameDisp, + vendorDisp, + api, + revision); + + String longDesc = String.format( + "%1$s\n" + + "[*] Addon failed to load: %2$s", + shortDesc, + error); + + int apiLevel = IExactApiLevelDependency.API_LEVEL_INVALID; + + try { + apiLevel = Integer.parseInt(api); + } catch(NumberFormatException ignore) {} + + int intRevision = MajorRevision.MISSING_MAJOR_REV; + try { + intRevision = Integer.parseInt(revision); + } catch (NumberFormatException ignore) {} + + IPkgDesc desc = PkgDesc.Builder + .newAddon(new AndroidVersion(apiLevel, null), + new MajorRevision(intRevision), + new IdDisplay(vendorId, vendorDisp), + new IdDisplay(nameId, nameDisp)) + .setDescriptionShort(shortDesc) + .create(); + + return new BrokenPackage(null/*props*/, shortDesc, longDesc, + IMinApiLevelDependency.MIN_API_LEVEL_NOT_SPECIFIED, + apiLevel, + archiveOsPath, + desc); + } + + @Override + public int getExactApiLevel() { + return mVersion.getApiLevel(); + } + + /** + * Save the properties of the current packages in the given {@link Properties} object. + * These properties will later be given to a constructor that takes a {@link Properties} object. + */ + @Override + public void saveProperties(Properties props) { + super.saveProperties(props); + + mVersion.saveProperties(props); + mLayoutlibVersion.saveProperties(props); + + props.setProperty(PkgProps.ADDON_NAME_ID, mNameId); + props.setProperty(PkgProps.ADDON_NAME_DISPLAY, mDisplayName); + props.setProperty(PkgProps.ADDON_VENDOR_ID, mVendorId); + props.setProperty(PkgProps.ADDON_VENDOR_DISPLAY, mVendorDisplay); + } + + /** + * Parses a element. + */ + private Lib[] parseLibs(Node libsNode) { + ArrayList libs = new ArrayList(); + + if (libsNode != null) { + String nsUri = libsNode.getNamespaceURI(); + for(Node child = libsNode.getFirstChild(); + child != null; + child = child.getNextSibling()) { + + if (child.getNodeType() == Node.ELEMENT_NODE && + nsUri.equals(child.getNamespaceURI()) && + SdkRepoConstants.NODE_LIB.equals(child.getLocalName())) { + libs.add(parseLib(child)); + } + } + } + + return libs.toArray(new Lib[libs.size()]); + } + + /** + * Parses a element from a container. + */ + private Lib parseLib(Node libNode) { + return new Lib(PackageParserUtils.getXmlString(libNode, SdkRepoConstants.NODE_NAME), + PackageParserUtils.getXmlString(libNode, SdkRepoConstants.NODE_DESCRIPTION)); + } + + /** Returns the vendor id, a string, for add-on packages. */ + @NonNull + public String getVendorId() { + return mVendorId; + } + + /** Returns the vendor, a string for display purposes. */ + @NonNull + public String getDisplayVendor() { + return mVendorDisplay; + } + + /** Returns the name id, a string, for add-on packages or for libraries. */ + @NonNull + public String getNameId() { + return mNameId; + } + + /** Returns the name, a string for display purposes. */ + @NonNull + public String getDisplayName() { + return mDisplayName; + } + + /** + * Returns the version of the platform dependency of this package. + *

+ * An add-on has the same {@link AndroidVersion} as the platform it depends on. + */ + @Override @NonNull + public AndroidVersion getAndroidVersion() { + return mVersion; + } + + /** Returns the libs defined in this add-on. Can be an empty array but not null. */ + @NonNull + public Lib[] getLibs() { + return mLibs; + } + + /** + * Returns the layoutlib version. + *

+ * The first integer is the API of layoublib, which should be > 0. + * It will be equal to {@link ILayoutlibVersion#LAYOUTLIB_API_NOT_SPECIFIED} (0) + * if the layoutlib version isn't specified. + *

+ * The second integer is the revision for that given API. It is >= 0 + * and works as a minor revision number, incremented for the same API level. + * + * @since sdk-addon-2.xsd + */ + @NonNull + @Override + public Pair getLayoutlibVersion() { + return mLayoutlibVersion.getLayoutlibVersion(); + } + + /** + * Returns a string identifier to install this package from the command line. + * For add-ons, we use "addon-vendor-name-N" where N is the base platform API. + *

+ * {@inheritDoc} + */ + @NonNull + @Override + public String installId() { + return encodeAddonName(); + } + + /** + * Returns a description of this package that is suitable for a list display. + *

+ * {@inheritDoc} + */ + @Override + public String getListDescription() { + String ld = getListDisplay(); + if (!ld.isEmpty()) { + return String.format("%1$s%2$s", ld, isObsolete() ? " (Obsolete)" : ""); + } + + return String.format("%1$s%2$s", + getDisplayName(), + isObsolete() ? " (Obsolete)" : ""); + } + + /** + * Returns a short description for an {@link IDescription}. + */ + @Override + public String getShortDescription() { + String ld = getListDisplay(); + if (!ld.isEmpty()) { + return String.format("%1$s, revision %2$s%3$s", + ld, + getRevision().toShortString(), + isObsolete() ? " (Obsolete)" : ""); + } + + return String.format("%1$s, Android API %2$s, revision %3$s%4$s", + getDisplayName(), + mVersion.getApiString(), + getRevision().toShortString(), + isObsolete() ? " (Obsolete)" : ""); + } + + /** + * Returns a long description for an {@link IDescription}. + * + * The long description is whatever the XML contains for the <description> field, + * or the short description if the former is empty. + */ + @Override + public String getLongDescription() { + String s = String.format("%1$s, Android API %2$s, revision %3$s%4$s\nBy %5$s", + getDisplayName(), + mVersion.getApiString(), + getRevision().toShortString(), + isObsolete() ? " (Obsolete)" : "", //$NON-NLS-2$ + getDisplayVendor()); + + String d = getDescription(); + if (d != null && !d.isEmpty()) { + s += '\n' + d; + } + + s += String.format("\nRequires SDK Platform Android API %1$s", + mVersion.getApiString()); + return s; + } + + /** + * Computes a potential installation folder if an archive of this package were + * to be installed right away in the given SDK root. + *

+ * An add-on package is typically installed in SDK/add-ons/"addon-name"-"api-level". + * The name needs to be sanitized to be acceptable as a directory name. + * However if we can find a different directory under SDK/add-ons that already + * has this add-ons installed, we'll use that one. + * + * @param osSdkRoot The OS path of the SDK root folder. + * @param sdkManager An existing SDK manager to list current platforms and addons. + * @return A new {@link File} corresponding to the directory to use to install this package. + */ + @Override + public File getInstallFolder(String osSdkRoot, SdkManager sdkManager) { + File addons = new File(osSdkRoot, SdkConstants.FD_ADDONS); + + // First find if this add-on is already installed. If so, reuse the same directory. + for (IAndroidTarget target : sdkManager.getTargets()) { + if (!target.isPlatform() && target.getVersion().equals(mVersion)) { + // Starting with addon-4.xsd, the addon source.properties differentiate + // between ids and display strings. However the addon target which relies + // on the manifest.ini does not so we need to cover both cases. + // TODO fix when we get rid of manifest.ini for addons + if ((target.getName().equals(getNameId()) && + target.getVendor().equals(getVendorId())) || + (target.getName().equals(getDisplayName()) && + target.getVendor().equals(getDisplayVendor()))) { + return new File(target.getLocation()); + } + } + } + + // Compute a folder directory using the addon declared name and vendor strings. + String name = encodeAddonName(); + + for (int i = 0; i < 100; i++) { + String name2 = i == 0 ? name : String.format("%s-%d", name, i); //$NON-NLS-1$ + File folder = new File(addons, name2); + if (!folder.exists()) { + return folder; + } + } + + // We shouldn't really get here. I mean, seriously, we tried hard enough. + return null; + } + + private String encodeAddonName() { + String name = String.format("addon-%s-%s-%s", //$NON-NLS-1$ + getNameId(), getVendorId(), mVersion.getApiString()); + name = name.toLowerCase(Locale.US); + name = name.replaceAll("[^a-z0-9_-]+", "_"); //$NON-NLS-1$ //$NON-NLS-2$ + name = name.replaceAll("_+", "_"); //$NON-NLS-1$ //$NON-NLS-2$ + return name; + } + + @Override + public boolean sameItemAs(Package pkg) { + if (pkg instanceof AddonPackage) { + AddonPackage newPkg = (AddonPackage)pkg; + + // check they are the same add-on. + if (getNameId().equals(newPkg.getNameId()) && + getAndroidVersion().equals(newPkg.getAndroidVersion())) { + // Check the vendor-id field. + if (getVendorId().equals(newPkg.getVendorId())) { + return true; + } + + // When loading addons from the v3 schema that only had a + // field, the vendor field has been converted to vendor-display so + // as a transition mechanism we should test this also. + // TODO: in a couple iterations of the SDK Manager, remove this check + // and only compare using the vendor-id field. + return getDisplayVendor().equals(newPkg.getDisplayVendor()); + } + } + + return false; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((mLayoutlibVersion == null) ? 0 : mLayoutlibVersion.hashCode()); + result = prime * result + Arrays.hashCode(mLibs); + result = prime * result + ((mDisplayName == null) ? 0 : mDisplayName.hashCode()); + result = prime * result + ((mVendorDisplay == null) ? 0 : mVendorDisplay.hashCode()); + result = prime * result + ((mVersion == null) ? 0 : mVersion.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (!(obj instanceof AddonPackage)) { + return false; + } + AddonPackage other = (AddonPackage) obj; + if (mLayoutlibVersion == null) { + if (other.mLayoutlibVersion != null) { + return false; + } + } else if (!mLayoutlibVersion.equals(other.mLayoutlibVersion)) { + return false; + } + if (!Arrays.equals(mLibs, other.mLibs)) { + return false; + } + if (mNameId == null) { + if (other.mNameId != null) { + return false; + } + } else if (!mNameId.equals(other.mNameId)) { + return false; + } + if (mVendorId == null) { + if (other.mVendorId != null) { + return false; + } + } else if (!mVendorId.equals(other.mVendorId)) { + return false; + } + if (mVersion == null) { + if (other.mVersion != null) { + return false; + } + } else if (!mVersion.equals(other.mVersion)) { + return false; + } + return true; + } + + /** + * For addon packages, we want to add vendor|name to the sorting key + * before the revision number. + *

+ * {@inheritDoc} + */ + @Override + protected String comparisonKey() { + String s = super.comparisonKey(); + int pos = s.indexOf("|r:"); //$NON-NLS-1$ + assert pos > 0; + s = s.substring(0, pos) + + "|vid:" + getVendorId() + //$NON-NLS-1$ + "|nid:" + getNameId() + //$NON-NLS-1$ + s.substring(pos); + return s; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/BrokenPackage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/BrokenPackage.java new file mode 100644 index 00000000..3ee5ed28 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/BrokenPackage.java @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2010 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.sdklib.internal.repository.packages; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdklib.SdkManager; +import com.android.sdklib.repository.IDescription; +import com.android.sdklib.internal.repository.ITaskMonitor; +import com.android.sdklib.internal.repository.archives.Archive; +import com.android.sdklib.repository.FullRevision; +import com.android.sdklib.repository.descriptors.IPkgDesc; + +import java.io.File; +import java.util.Properties; + +/** + * Represents an SDK repository package that is incomplete. + * It has a distinct icon and a specific error that is supposed to help the user on how to fix it. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class BrokenPackage extends MajorRevisionPackage + implements IExactApiLevelDependency, IMinApiLevelDependency { + + /** + * The minimal API level required by this package, if > 0, + * or {@link #MIN_API_LEVEL_NOT_SPECIFIED} if there is no such requirement. + */ + private final int mMinApiLevel; + + /** + * The exact API level required by this package, if > 0, + * or {@link #API_LEVEL_INVALID} if there is no such requirement. + */ + private final int mExactApiLevel; + + private final String mShortDescription; + private final String mLongDescription; + private final IPkgDesc mPkgDesc; + + /** + * Creates a new "broken" package that represents a package that we failed to load, + * for whatever error indicated in {@code longDescription}. + * There is also an optional API level dependency that can be specified. + *

+ * By design, this creates a package with one and only one archive. + */ + BrokenPackage(@Nullable Properties props, + @NonNull String shortDescription, + @NonNull String longDescription, + int minApiLevel, + int exactApiLevel, + @Nullable String archiveOsPath, + @NonNull IPkgDesc pkgDesc) { + super( null, //source + props, //properties + 0, //revision will be taken from props + null, //license + longDescription, //description + null, //descUrl + archiveOsPath //archiveOsPath + ); + mShortDescription = shortDescription; + mLongDescription = longDescription; + mMinApiLevel = minApiLevel; + mExactApiLevel = exactApiLevel; + mPkgDesc = pkgDesc; + } + + @Override + @NonNull + public IPkgDesc getPkgDesc() { + return mPkgDesc; + } + + /** + * Save the properties of the current packages in the given {@link Properties} object. + * These properties will later be given to a constructor that takes a {@link Properties} object. + *

+ * Base implementation override: We don't actually save properties for a broken package. + */ + @Override + public void saveProperties(Properties props) { + // Nop. We don't actually save properties for a broken package. + } + + /** + * Returns the minimal API level required by this package, if > 0, + * or {@link #MIN_API_LEVEL_NOT_SPECIFIED} if there is no such requirement. + */ + @Override + public int getMinApiLevel() { + return mMinApiLevel; + } + + /** + * Returns the exact API level required by this package, if > 0, + * or {@link #API_LEVEL_INVALID} if the value was missing. + */ + @Override + public int getExactApiLevel() { + return mExactApiLevel; + } + + /** + * Returns a string identifier to install this package from the command line. + * For broken packages, we return an empty string. These are not installable. + *

+ * {@inheritDoc} + */ + @Override + public String installId() { + return ""; //$NON-NLS-1$ + } + + /** + * Returns a description of this package that is suitable for a list display. + *

+ * {@inheritDoc} + */ + @Override + public String getListDescription() { + String ld = getListDisplay(); + if (!ld.isEmpty()) { + return String.format("%1$s%2$s", ld, isObsolete() ? " (Obsolete)" : ""); + } + + return mShortDescription; + } + + /** + * Returns a short description for an {@link IDescription}. + */ + @Override + public String getShortDescription() { + return mShortDescription; + } + + /** + * Returns a long description for an {@link IDescription}. + * + * The long description uses what was given to the constructor. + * If it's missing, it will use whatever the XML contains for the <description> field, + * or the short description if the former is empty. + */ + @Override + public String getLongDescription() { + + String s = mLongDescription; + if (s != null && !s.isEmpty()) { + return s; + } + + s = getDescription(); + if (s != null && !s.isEmpty()) { + return s; + } + return getShortDescription(); + } + + /** + * We should not be attempting to install a broken package. + * + * {@inheritDoc} + */ + @Override + public File getInstallFolder(String osSdkRoot, SdkManager sdkManager) { + // We should not be attempting to install a broken package. + return null; + } + + @Override + public boolean sameItemAs(Package pkg) { + if (pkg instanceof BrokenPackage) { + return mShortDescription.equals(((BrokenPackage) pkg).mShortDescription) && + getDescription().equals(pkg.getDescription()) && + getMinApiLevel() == ((BrokenPackage) pkg).getMinApiLevel(); + } + + return false; + } + + @Override + public boolean preInstallHook(Archive archive, + ITaskMonitor monitor, + String osSdkRoot, + File installFolder) { + // Nothing specific to do. + return super.preInstallHook(archive, monitor, osSdkRoot, installFolder); + } + + /** + * Computes a hash of the installed content (in case of successful install.) + * + * {@inheritDoc} + */ + @Override + public void postInstallHook(Archive archive, ITaskMonitor monitor, File installFolder) { + // Nothing specific to do. + super.postInstallHook(archive, monitor, installFolder); + } + + /** + * Similar to {@link BuildToolPackage#comparisonKey()}, but we need to use + * {@link #getPkgDesc} instead of {@link #getRevision()} + */ + @Override + protected String comparisonKey() { + String s = super.comparisonKey(); + FullRevision rev = getPkgDesc().getFullRevision(); + if (rev != null) { + int pos = s.indexOf("|r:"); //$NON-NLS-1$ + assert pos > 0; + String reverseSort = String.format("|rr:%1$04d.%2$04d.%3$04d.", //$NON-NLS-1$ + 9999 - rev.getMajor(), + 9999 - rev.getMinor(), + 9999 - rev.getMicro()); + + s = s.substring(0, pos) + reverseSort + s.substring(pos); + } + return s; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/BuildToolPackage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/BuildToolPackage.java new file mode 100644 index 00000000..2aa30aeb --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/BuildToolPackage.java @@ -0,0 +1,367 @@ +/* + * Copyright (C) 2013 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.sdklib.internal.repository.packages; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.VisibleForTesting; +import com.android.annotations.VisibleForTesting.Visibility; +import com.android.sdklib.SdkManager; +import com.android.sdklib.internal.repository.sources.SdkSource; +import com.android.sdklib.repository.FullRevision; +import com.android.sdklib.repository.FullRevision.PreviewComparison; +import com.android.sdklib.repository.IDescription; +import com.android.sdklib.repository.PkgProps; +import com.android.sdklib.repository.PreciseRevision; +import com.android.sdklib.repository.descriptors.IPkgDesc; +import com.android.sdklib.repository.descriptors.PkgDesc; + +import org.w3c.dom.Node; + +import java.io.File; +import java.util.HashSet; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +/** + * Represents a build-tool XML node in an SDK repository. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class BuildToolPackage extends FullRevisionPackage { + + /** The base value returned by {@link BuildToolPackage#installId()}. */ + private static final String INSTALL_ID_BASE = SdkConstants.FD_BUILD_TOOLS + '-'; + + private final IPkgDesc mPkgDesc; + + /** + * Creates a new build-tool package from the attributes and elements of the given XML node. + * This constructor should throw an exception if the package cannot be created. + * + * @param source The {@link SdkSource} where this is loaded from. + * @param packageNode The XML element being parsed. + * @param nsUri The namespace URI of the originating XML document, to be able to deal with + * parameters that vary according to the originating XML schema. + * @param licenses The licenses loaded from the XML originating document. + */ + public BuildToolPackage( + SdkSource source, + Node packageNode, + String nsUri, + Map licenses) { + super(source, packageNode, nsUri, licenses); + + mPkgDesc = setDescriptions(PkgDesc.Builder + .newBuildTool(getRevision()) + ) + .create(); + } + + /** + * Creates either a valid {@link BuildToolPackage} or a {@link BrokenPackage}. + *

+ * If the build-tool directory contains valid properties, + * this creates a new {@link BuildToolPackage} with the reversion listed in the properties. + * Otherwise returns a new {@link BrokenPackage} with some explanation on what failed. + *

+ * Note that the folder name is not enforced. A build-tool directory must have a + * a source.props with a revision property and a few expected binaries inside to be + * valid. + * + * @param buildToolDir The SDK/build-tool/revision folder + * @param props The properties located in {@code buildToolDir} or null if not found. + * @return A new {@link BuildToolPackage} or a new {@link BrokenPackage}. + */ + public static Package create(File buildToolDir, Properties props) { + String error = null; + + // Try to load the reversion from the sources.props. + // If we don't find them, the package is broken. + if (props == null) { + error = String.format("Missing file %1$s in build-tool/%2$s", + SdkConstants.FN_SOURCE_PROP, + buildToolDir.getName()); + } + + // Check we can find the revision in the source properties + FullRevision rev = null; + if (error == null) { + String revStr = getProperty(props, PkgProps.PKG_REVISION, null); + + if (revStr != null) { + try { + rev = FullRevision.parseRevision(revStr); + } catch (NumberFormatException ignore) {} + } + + if (rev == null) { + error = String.format("Missing revision property in %1$s", + SdkConstants.FN_SOURCE_PROP); + } + } + + if (error == null) { + // Check the directory contains the expected binaries. + + if (!buildToolDir.isDirectory()) { + error = String.format("build-tool/%1$s folder is missing", + buildToolDir.getName()); + } else { + File[] files = buildToolDir.listFiles(); + if (files == null || files.length == 0) { + error = String.format("build-tool/%1$s folder is empty", + buildToolDir.getName()); + } else { + Set names = new HashSet(); + for (File file : files) { + names.add(file.getName()); + } + for (String name : new String[] { SdkConstants.FN_AAPT, + SdkConstants.FN_AIDL, + SdkConstants.FN_DX } ) { + if (!names.contains(name)) { + if (error == null) { + error = String.format("build-tool/%1$s folder is missing ", + buildToolDir.getName()); + } else { + error += ", "; + } + error += name; + } + } + } + } + } + + if (error == null && rev != null) { + BuildToolPackage pkg = new BuildToolPackage( + null, //source + props, + 0, //revision (extracted from props) + null, //license + null, //description + null, //descUrl + buildToolDir.getAbsolutePath()); + + if (pkg.hasCompatibleArchive()) { + return pkg; + } else { + error = "Package is not compatible with current OS"; + } + } + + + StringBuilder sb = new StringBuilder("Broken Build-Tools Package"); + if (rev != null) { + sb.append(String.format(", revision %1$s", rev.toShortString())); + } + + String shortDesc = sb.toString(); + + if (error != null) { + sb.append('\n').append(error); + } + + String longDesc = sb.toString(); + + IPkgDesc desc = PkgDesc.Builder + .newBuildTool(rev != null ? rev : new FullRevision(FullRevision.MISSING_MAJOR_REV)) + .setDescriptionShort(shortDesc) + .create(); + + return new BrokenPackage(props, shortDesc, longDesc, + IMinApiLevelDependency.MIN_API_LEVEL_NOT_SPECIFIED, + IExactApiLevelDependency.API_LEVEL_INVALID, + buildToolDir.getAbsolutePath(), + desc); + } + + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected BuildToolPackage( + SdkSource source, + Properties props, + int revision, + String license, + String description, + String descUrl, + String archiveOsPath) { + super(source, + props, + revision, + license, + description, + descUrl, + archiveOsPath); + + mPkgDesc = setDescriptions(PkgDesc.Builder.newBuildTool(getRevision())).create(); + } + + @Override + @NonNull + public IPkgDesc getPkgDesc() { + return mPkgDesc; + } + + /** + * Returns a string identifier to install this package from the command line. + * For build-tools, we use "build-tools-" followed by the full revision string + * where spaces are changed to underscore to be more script-friendly. + *

+ * {@inheritDoc} + */ + @Override + public String installId() { + return getPkgDesc().getInstallId(); + } + + /** + * Returns a description of this package that is suitable for a list display. + *

+ * {@inheritDoc} + */ + @Override + public String getListDescription() { + String ld = getListDisplay(); + if (!ld.isEmpty()) { + return String.format("%1$s%2$s", ld, isObsolete() ? " (Obsolete)" : ""); + } + + return String.format("Android SDK Build-tools%1$s", + isObsolete() ? " (Obsolete)" : ""); + } + + /** + * Returns a short description for an {@link IDescription}. + */ + @Override + public String getShortDescription() { + String ld = getListDisplay(); + if (!ld.isEmpty()) { + return String.format("%1$s, revision %2$s%3$s", + ld, + getRevision().toShortString(), + isObsolete() ? " (Obsolete)" : ""); + } + + return String.format("Android SDK Build-tools, revision %1$s%2$s", + getRevision().toShortString(), + isObsolete() ? " (Obsolete)" : ""); + } + + /** Returns a long description for an {@link IDescription}. */ + @Override + public String getLongDescription() { + String s = getDescription(); + if (s == null || s.isEmpty()) { + s = getShortDescription(); + } + + if (s.indexOf("revision") == -1) { + s += String.format("\nRevision %1$s%2$s", + getRevision().toShortString(), + isObsolete() ? " (Obsolete)" : ""); + } + + return s; + } + + /** + * Computes a potential installation folder if an archive of this package were + * to be installed right away in the given SDK root. + *

+ * A build-tool package is typically installed in SDK/build-tools/revision. + * Revision spaces are replaced by underscores for ease of use in command-line. + * + * @param osSdkRoot The OS path of the SDK root folder. + * @param sdkManager An existing SDK manager to list current platforms and addons. + * @return A new {@link File} corresponding to the directory to use to install this package. + */ + @Override + public File getInstallFolder(String osSdkRoot, SdkManager sdkManager) { + File folder = new File(osSdkRoot, SdkConstants.FD_BUILD_TOOLS); + StringBuilder sb = new StringBuilder(); + + PreciseRevision revision = getPkgDesc().getPreciseRevision(); + int[] version = revision.toIntArray(false); + for (int i = 0; i < version.length; i++) { + sb.append(version[i]); + if (i != version.length - 1) { + sb.append('.'); + } + } + if (getPkgDesc().getPreciseRevision().isPreview()) { + sb.append(PkgDesc.PREVIEW_SUFFIX); + } + + folder = new File(folder, sb.toString()); + return folder; + } + + /** + * Check whether 2 platform-tool packages are the same and have the + * same preview bit. + */ + @Override + public boolean sameItemAs(Package pkg) { + // Implementation note: here we don't want to care about the preview number + // so we ignore the preview when calling sameItemAs(); however we do care + // about both packages being either previews or not previews (i.e. the type + // must match but the preview number doesn't need to.) + // The end result is that a package such as "1.2 rc 4" will be an update for "1.2 rc 3". + return sameItemAs(pkg, PreviewComparison.COMPARE_TYPE); + } + + @Override + public boolean sameItemAs(Package pkg, PreviewComparison comparePreview) { + // Contrary to other package types, build-tools do not "update themselves" + // so 2 build tools with 2 different revisions are not the same item. + if (pkg instanceof BuildToolPackage) { + BuildToolPackage rhs = (BuildToolPackage) pkg; + return rhs.getRevision().compareTo(getRevision(), comparePreview) == 0; + } + return false; + } + + /** + * For build-tool package use their revision number like version numbers and + * we want them sorted from higher to lower. To do that, insert a fake revision + * number using 9999-value into the sorting key. + *

+ * {@inheritDoc} + */ + @Override + protected String comparisonKey() { + String s = super.comparisonKey(); + int pos = s.indexOf("|r:"); //$NON-NLS-1$ + assert pos > 0; + + FullRevision rev = getRevision(); + String reverseSort = String.format("|rr:%1$04d.%2$04d.%3$04d.", //$NON-NLS-1$ + 9999 - rev.getMajor(), + 9999 - rev.getMinor(), + 9999 - rev.getMicro()); + + s = s.substring(0, pos) + reverseSort + s.substring(pos); + return s; + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/DocPackage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/DocPackage.java new file mode 100644 index 00000000..3582f5b1 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/DocPackage.java @@ -0,0 +1,333 @@ +/* + * 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.sdklib.internal.repository.packages; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.SdkManager; +import com.android.sdklib.internal.repository.sources.SdkSource; +import com.android.sdklib.repository.IDescription; +import com.android.sdklib.repository.MajorRevision; +import com.android.sdklib.repository.SdkRepoConstants; +import com.android.sdklib.repository.descriptors.IPkgDesc; +import com.android.sdklib.repository.descriptors.PkgDesc; + +import org.w3c.dom.Node; + +import java.io.File; +import java.util.Map; +import java.util.Properties; + +/** + * Represents a doc XML node in an SDK repository. + *

+ * Note that a doc package has a version and thus implements {@link IAndroidVersionProvider}. + * However there is no mandatory dependency that limits installation so this does not + * implement {@link IPlatformDependency}. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class DocPackage extends MajorRevisionPackage implements IAndroidVersionProvider { + + private final AndroidVersion mVersion; + private final IPkgDesc mPkgDesc; + + /** + * Creates a new doc package from the attributes and elements of the given XML node. + * This constructor should throw an exception if the package cannot be created. + * + * @param source The {@link SdkSource} where this is loaded from. + * @param packageNode The XML element being parsed. + * @param nsUri The namespace URI of the originating XML document, to be able to deal with + * parameters that vary according to the originating XML schema. + * @param licenses The licenses loaded from the XML originating document. + */ + public DocPackage(SdkSource source, + Node packageNode, + String nsUri, + Map licenses) { + super(source, packageNode, nsUri, licenses); + + int apiLevel = + PackageParserUtils.getXmlInt (packageNode, SdkRepoConstants.NODE_API_LEVEL, 0); + String codeName = + PackageParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_CODENAME); + if (codeName.isEmpty()) { + codeName = null; + } + mVersion = new AndroidVersion(apiLevel, codeName); + + mPkgDesc = setDescriptions( + PkgDesc.Builder.newDoc(mVersion, (MajorRevision) getRevision())) + .create(); + } + + /** + * Manually create a new package with one archive and the given attributes. + * This is used to create packages from local directories in which case there must be + * one archive which URL is the actual target location. + *

+ * By design, this creates a package with one and only one archive. + */ + public static Package create(SdkSource source, + Properties props, + int apiLevel, + String codename, + int revision, + String license, + String description, + String descUrl, + String archiveOsPath) { + return new DocPackage(source, props, apiLevel, codename, revision, license, description, + descUrl, archiveOsPath); + } + + private DocPackage(SdkSource source, + Properties props, + int apiLevel, + String codename, + int revision, + String license, + String description, + String descUrl, + String archiveOsPath) { + super(source, + props, + revision, + license, + description, + descUrl, + archiveOsPath); + mVersion = new AndroidVersion(props, apiLevel, codename); + + mPkgDesc = setDescriptions(PkgDesc.Builder.newDoc(mVersion, (MajorRevision) getRevision())) + .create(); + } + + @Override + @NonNull + public IPkgDesc getPkgDesc() { + return mPkgDesc; + } + + /** + * Save the properties of the current packages in the given {@link Properties} object. + * These properties will later be give the constructor that takes a {@link Properties} object. + */ + @Override + public void saveProperties(Properties props) { + super.saveProperties(props); + + mVersion.saveProperties(props); + } + + /** + * Returns the version, for platform, add-on and doc packages. + * Can be 0 if this is a local package of unknown api-level. + */ + @Override @NonNull + public AndroidVersion getAndroidVersion() { + return mVersion; + } + + /** + * Returns a string identifier to install this package from the command line. + * For docs, we use "doc-N" where N is the API or the preview codename. + *

+ * {@inheritDoc} + */ + @Override + public String installId() { + return "doc-" + mVersion.getApiString(); //$NON-NLS-1$ + } + + /** + * Returns a description of this package that is suitable for a list display. + *

+ * {@inheritDoc} + */ + @Override + public String getListDescription() { + String ld = getListDisplay(); + if (!ld.isEmpty()) { + return String.format("%1$s%2$s", ld, isObsolete() ? " (Obsolete)" : ""); + } + if (mVersion.isPreview()) { + return String.format("Documentation for Android '%1$s' Preview SDK%2$s", + mVersion.getCodename(), + isObsolete() ? " (Obsolete)" : ""); + } else { + return String.format("Documentation for Android SDK%2$s", + mVersion.getApiLevel(), + isObsolete() ? " (Obsolete)" : ""); + } + } + + /** + * Returns a short description for an {@link IDescription}. + */ + @Override + public String getShortDescription() { + String ld = getListDisplay(); + if (!ld.isEmpty()) { + return String.format("%1$s, revision %2$s%3$s", + ld, + getRevision().toShortString(), + isObsolete() ? " (Obsolete)" : ""); + } + + if (mVersion.isPreview()) { + return String.format("Documentation for Android '%1$s' Preview SDK, revision %2$s%3$s", + mVersion.getCodename(), + getRevision().toShortString(), + isObsolete() ? " (Obsolete)" : ""); + } else { + return String.format("Documentation for Android SDK, API %1$d, revision %2$s%3$s", + mVersion.getApiLevel(), + getRevision().toShortString(), + isObsolete() ? " (Obsolete)" : ""); + } + } + + /** + * Returns a long description for an {@link IDescription}. + * + * The long description is whatever the XML contains for the <description> field, + * or the short description if the former is empty. + */ + @Override + public String getLongDescription() { + String s = getDescription(); + if (s == null || s.isEmpty()) { + s = getShortDescription(); + } + + if (s.indexOf("revision") == -1) { + s += String.format("\nRevision %1$s%2$s", + getRevision().toShortString(), + isObsolete() ? " (Obsolete)" : ""); + } + + return s; + } + + /** + * Computes a potential installation folder if an archive of this package were + * to be installed right away in the given SDK root. + *

+ * A "doc" package should always be located in SDK/docs. + * + * @param osSdkRoot The OS path of the SDK root folder. + * @param sdkManager An existing SDK manager to list current platforms and addons. + * @return A new {@link File} corresponding to the directory to use to install this package. + */ + @Override + public File getInstallFolder(String osSdkRoot, SdkManager sdkManager) { + return new File(osSdkRoot, SdkConstants.FD_DOCS); + } + + /** + * Consider doc packages to be the same if they cover the same API level, + * regardless of their revision number. + */ + @Override + public boolean sameItemAs(Package pkg) { + if (pkg instanceof DocPackage) { + AndroidVersion rev2 = ((DocPackage) pkg).getAndroidVersion(); + return this.getAndroidVersion().equals(rev2); + } + + return false; + } + + /** + * {@inheritDoc} + *


+ * Doc packages are a bit different since there can only be one doc installed at + * the same time. + *

+ * We now consider that docs for different APIs are NOT updates, e.g. doc for API N+1 + * is no longer considered an update for doc API N. + * However docs that have the same API version (API level + codename) are considered + * updates if they have a higher revision number (so 15 rev 2 is an update for 15 rev 1, + * but is not an update for 14 rev 1.) + */ + @Override + public UpdateInfo canBeUpdatedBy(Package replacementPackage) { + // check they are the same kind of object + if (!(replacementPackage instanceof DocPackage)) { + return UpdateInfo.INCOMPATIBLE; + } + + DocPackage replacementDoc = (DocPackage)replacementPackage; + + AndroidVersion replacementVersion = replacementDoc.getAndroidVersion(); + + // Check if they're the same exact (api and codename) + if (replacementVersion.equals(mVersion)) { + // exact same version, so check the revision level + if (replacementPackage.getRevision().compareTo(this.getRevision()) > 0) { + return UpdateInfo.UPDATE; + } + } else { + // not the same version? we check if they have the same api level and the new one + // is a preview, in which case it's also an update (since preview have the api level + // of the _previous_ version.) + if (replacementVersion.getApiLevel() == mVersion.getApiLevel() && + replacementVersion.isPreview()) { + return UpdateInfo.UPDATE; + } + } + + // not an upgrade but not incompatible either. + return UpdateInfo.NOT_UPDATE; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((mVersion == null) ? 0 : mVersion.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (!(obj instanceof DocPackage)) { + return false; + } + DocPackage other = (DocPackage) obj; + if (mVersion == null) { + if (other.mVersion != null) { + return false; + } + } else if (!mVersion.equals(other.mVersion)) { + return false; + } + return true; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/ExtraPackage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/ExtraPackage.java new file mode 100644 index 00000000..fccbd211 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/ExtraPackage.java @@ -0,0 +1,692 @@ +/* + * 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.sdklib.internal.repository.packages; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.annotations.VisibleForTesting; +import com.android.annotations.VisibleForTesting.Visibility; +import com.android.sdklib.SdkManager; +import com.android.sdklib.internal.repository.LocalSdkParser; +import com.android.sdklib.internal.repository.NullTaskMonitor; +import com.android.sdklib.internal.repository.archives.Archive; +import com.android.sdklib.internal.repository.sources.SdkSource; +import com.android.sdklib.repository.FullRevision; +import com.android.sdklib.repository.IDescription; +import com.android.sdklib.repository.PkgProps; +import com.android.sdklib.repository.RepoConstants; +import com.android.sdklib.repository.descriptors.IPkgDescExtra; +import com.android.sdklib.repository.descriptors.IdDisplay; +import com.android.sdklib.repository.descriptors.PkgDesc; +import com.android.sdklib.repository.descriptors.PkgDescExtra; +import com.android.sdklib.repository.local.LocalExtraPkgInfo; +import com.android.utils.NullLogger; +import org.w3c.dom.Node; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Map; +import java.util.Properties; +import java.util.regex.Pattern; + +/** + * Represents a extra XML node in an SDK repository. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class ExtraPackage extends NoPreviewRevisionPackage + implements IMinApiLevelDependency, IMinToolsDependency { + + /** Mixin handling the min-tools dependency. */ + private final MinToolsMixin mMinToolsMixin; + + /** + * The extra display name. Used in the UI to represent the package. It can be anything. + */ + private final String mDisplayName; + + /** + * The vendor id + name. + * The id is a simple alphanumeric string [a-zA-Z0-9_-]. + * The display name is used in the UI to represent the vendor. It can be anything. + */ + private final IdDisplay mVendor; + + /** + * The sub-folder name. It must be a non-empty single-segment path. + */ + private final String mPath; + + /** + * The optional old_paths, if any. If present, this is a list of old "path" values that + * we'd like to migrate to the current "path" name for this extra. + */ + private final String mOldPaths; + + /** + * The minimal API level required by this extra package, if > 0, + * or {@link #MIN_API_LEVEL_NOT_SPECIFIED} if there is no such requirement. + */ + private final int mMinApiLevel; + + /** + * The project-files listed by this extra package. + * The array can be empty but not null. + */ + private final String[] mProjectFiles; + + private final IPkgDescExtra mPkgDesc; + + /** + * Creates a new tool package from the attributes and elements of the given XML node. + * This constructor should throw an exception if the package cannot be created. + * + * @param source The {@link SdkSource} where this is loaded from. + * @param packageNode The XML element being parsed. + * @param nsUri The namespace URI of the originating XML document, to be able to deal with + * parameters that vary according to the originating XML schema. + * @param licenses The licenses loaded from the XML originating document. + */ + public ExtraPackage( + SdkSource source, + Node packageNode, + String nsUri, + Map licenses) { + super(source, packageNode, nsUri, licenses); + + mMinToolsMixin = new MinToolsMixin(packageNode); + + mPath = PackageParserUtils.getXmlString(packageNode, RepoConstants.NODE_PATH); + + // Read name-display, vendor-display and vendor-id, introduced in addon-4.xsd. + // These are not optional, they are mandatory in addon-4 but we still treat them + // as optional so that we can fallback on using which was the only one + // defined in addon-3.xsd. + String name = + PackageParserUtils.getXmlString(packageNode, RepoConstants.NODE_NAME_DISPLAY); + String vname = + PackageParserUtils.getXmlString(packageNode, RepoConstants.NODE_VENDOR_DISPLAY); + String vid = + PackageParserUtils.getXmlString(packageNode, RepoConstants.NODE_VENDOR_ID); + + if (vid.isEmpty()) { + // If vid is missing, use the old attribute. + // Note that in a valid XML, vendor-id cannot be an empty string. + // The only reason vid can be empty is when is missing, which + // happens in an addon-3 schema, in which case the old needs to be used. + String vendor = PackageParserUtils.getXmlString(packageNode, RepoConstants.NODE_VENDOR); + vid = sanitizeLegacyVendor(vendor); + if (vname.isEmpty()) { + vname = vendor; + } + } + if (vname.isEmpty()) { + // The vendor-display name can be empty, in which case we use the vendor-id. + vname = vid; + } + mVendor = new IdDisplay(vid.trim(), vname.trim()); + + if (name.isEmpty()) { + // If name is missing, use the attribute as done in an addon-3 schema. + name = LocalExtraPkgInfo.getPrettyName(mVendor, mPath); + } + mDisplayName = name.trim(); + + mMinApiLevel = PackageParserUtils.getXmlInt( + packageNode, RepoConstants.NODE_MIN_API_LEVEL, MIN_API_LEVEL_NOT_SPECIFIED); + + mProjectFiles = parseProjectFiles( + PackageParserUtils.findChildElement(packageNode, RepoConstants.NODE_PROJECT_FILES)); + + mOldPaths = PackageParserUtils.getXmlString(packageNode, RepoConstants.NODE_OLD_PATHS); + + mPkgDesc = + (IPkgDescExtra)setDescriptions(PkgDesc.Builder.newExtra(mVendor, mPath, mDisplayName, + getOldPaths(), getRevision())).create(); + } + + private String[] parseProjectFiles(Node projectFilesNode) { + ArrayList paths = new ArrayList(); + + if (projectFilesNode != null) { + String nsUri = projectFilesNode.getNamespaceURI(); + for(Node child = projectFilesNode.getFirstChild(); + child != null; + child = child.getNextSibling()) { + + if (child.getNodeType() == Node.ELEMENT_NODE && + nsUri.equals(child.getNamespaceURI()) && + RepoConstants.NODE_PATH.equals(child.getLocalName())) { + String path = child.getTextContent(); + if (path != null) { + path = path.trim(); + if (!path.isEmpty()) { + paths.add(path); + } + } + } + } + } + + return paths.toArray(new String[paths.size()]); + } + + /** + * Manually create a new package with one archive and the given attributes or properties. + * This is used to create packages from local directories in which case there must be + * one archive which URL is the actual target location. + *

+ * By design, this creates a package with one and only one archive. + */ + public static Package create(SdkSource source, + Properties props, + String vendor, + String path, + int revision, + String license, + String description, + String descUrl, + String archiveOsPath) { + ExtraPackage ep = new ExtraPackage(source, props, vendor, path, revision, license, + description, descUrl, archiveOsPath); + return ep; + } + + /** + * Constructor used to create a mock {@link ExtraPackage}. + * Most of the attributes here are optional. + * When not defined, they will be extracted from the {@code props} properties. + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected ExtraPackage(SdkSource source, + Properties props, + String vendorId, + String path, + int revision, + String license, + String description, + String descUrl, + String archiveOsPath) { + super(source, + props, + revision, + license, + description, + descUrl, + archiveOsPath); + + mMinToolsMixin = new MinToolsMixin( + source, + props, + revision, + license, + description, + descUrl, + archiveOsPath); + + // The path argument comes before whatever could be in the properties + mPath = path != null ? path : getProperty(props, PkgProps.EXTRA_PATH, path); + + String name = getProperty(props, PkgProps.EXTRA_NAME_DISPLAY, ""); //$NON-NLS-1$ + String vname = getProperty(props, PkgProps.EXTRA_VENDOR_DISPLAY, ""); //$NON-NLS-1$ + String vid = vendorId != null ? vendorId : + getProperty(props, PkgProps.EXTRA_VENDOR_ID, ""); //$NON-NLS-1$ + + if (vid == null || vid.isEmpty()) { + // If vid is missing, use the old attribute. + // did not exist prior to schema repo-v3 and tools r8. + String vendor = getProperty(props, PkgProps.EXTRA_VENDOR, ""); //$NON-NLS-1$ + vid = sanitizeLegacyVendor(vendor); + if (vname == null || vname.isEmpty()) { + vname = vendor; + } + } + if (vname == null || vname.isEmpty()) { + // The vendor-display name can be empty, in which case we use the vendor-id. + vname = vid; + } + mVendor = new IdDisplay(vid.trim(), vname.trim()); + + if (name == null || name.isEmpty()) { + // If name is missing, use the attribute as done in an addon-3 schema. + name = LocalExtraPkgInfo.getPrettyName(mVendor, mPath); + } + mDisplayName = name.trim(); + + mOldPaths = getProperty(props, PkgProps.EXTRA_OLD_PATHS, null); + + mMinApiLevel = getPropertyInt(props, PkgProps.EXTRA_MIN_API_LEVEL, + MIN_API_LEVEL_NOT_SPECIFIED); + + String projectFiles = getProperty(props, PkgProps.EXTRA_PROJECT_FILES, null); + ArrayList filePaths = new ArrayList(); + if (projectFiles != null && !projectFiles.isEmpty()) { + for (String filePath : projectFiles.split(Pattern.quote(File.pathSeparator))) { + filePath = filePath.trim(); + if (!filePath.isEmpty()) { + filePaths.add(filePath); + } + } + } + + mProjectFiles = filePaths.toArray(new String[filePaths.size()]); + + mPkgDesc = (IPkgDescExtra) setDescriptions(PkgDesc.Builder + .newExtra(mVendor, mPath, mDisplayName, getOldPaths(), getRevision())) + .create(); + } + + @Override + @NonNull + public IPkgDescExtra getPkgDesc() { + return mPkgDesc; + } + + /** + * Save the properties of the current packages in the given {@link Properties} object. + * These properties will later be give the constructor that takes a {@link Properties} object. + */ + @Override + public void saveProperties(Properties props) { + super.saveProperties(props); + mMinToolsMixin.saveProperties(props); + + props.setProperty(PkgProps.EXTRA_PATH, mPath); + props.setProperty(PkgProps.EXTRA_NAME_DISPLAY, mDisplayName); + props.setProperty(PkgProps.EXTRA_VENDOR_DISPLAY, mVendor.getDisplay()); + props.setProperty(PkgProps.EXTRA_VENDOR_ID, mVendor.getId()); + + if (getMinApiLevel() != MIN_API_LEVEL_NOT_SPECIFIED) { + props.setProperty(PkgProps.EXTRA_MIN_API_LEVEL, Integer.toString(getMinApiLevel())); + } + + if (mProjectFiles.length > 0) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < mProjectFiles.length; i++) { + if (i > 0) { + sb.append(File.pathSeparatorChar); + } + sb.append(mProjectFiles[i]); + } + props.setProperty(PkgProps.EXTRA_PROJECT_FILES, sb.toString()); + } + + if (mOldPaths != null && !mOldPaths.isEmpty()) { + props.setProperty(PkgProps.EXTRA_OLD_PATHS, mOldPaths); + } + } + + /** + * The minimal revision of the tools package required by this extra package, if > 0, + * or {@link #MIN_TOOLS_REV_NOT_SPECIFIED} if there is no such requirement. + */ + @Override + public FullRevision getMinToolsRevision() { + return mMinToolsMixin.getMinToolsRevision(); + } + + /** + * Returns the minimal API level required by this extra package, if > 0, + * or {@link #MIN_API_LEVEL_NOT_SPECIFIED} if there is no such requirement. + */ + @Override + public int getMinApiLevel() { + return mMinApiLevel; + } + + /** + * The project-files listed by this extra package. + * The array can be empty but not null. + *

+ * IMPORTANT: directory separators are NOT translated and may not match + * the {@link File#separatorChar} of the current platform. It's up to the + * user to adequately interpret the paths. + * Similarly, no guarantee is made on the validity of the paths. + * Users are expected to apply all usual sanity checks such as removing + * "./" and "../" and making sure these paths don't reference files outside + * of the installed archive. + * + * @since sdk-repository-4.xsd or sdk-addon-2.xsd + */ + public String[] getProjectFiles() { + return mProjectFiles; + } + + /** + * Returns the old_paths, a list of obsolete path names for the extra package. + *

+ * These can be used by the installer to migrate an extra package using one of the + * old paths into the new path. + *

+ * These can also be used to recognize "old" renamed packages as the same as + * the current one. + * + * @return A list of old paths. Can be empty but not null. + */ + public String[] getOldPaths() { + return PkgDescExtra.convertOldPaths(mOldPaths); + } + + /** + * Returns the sanitized path folder name. It is a single-segment path. + *

+ * The package is installed in SDK/extras/vendor_name/path_name. + */ + public String getPath() { + // The XSD specifies the XML vendor and path should only contain [a-zA-Z0-9]+ + // and cannot be empty. Let's be defensive and enforce that anyway since things + // like "____" are still valid values that we don't want to allow. + + // Sanitize the path + String path = mPath.replaceAll("[^a-zA-Z0-9-]+", "_"); //$NON-NLS-1$ + if (path.isEmpty() || path.equals("_")) { //$NON-NLS-1$ + int h = path.hashCode(); + path = String.format("extra%08x", h); //$NON-NLS-1$ + } + + return path; + } + + /** + * Returns the vendor id. + */ + public String getVendorId() { + return mVendor.getId(); + } + + public String getVendorDisplay() { + return mVendor.getDisplay(); + } + + public String getDisplayName() { + return mDisplayName; + } + + /** Transforms the legacy vendor name into a usable vendor id. */ + private String sanitizeLegacyVendor(String vendorDisplay) { + // The XSD specifies the XML vendor and path should only contain [a-zA-Z0-9]+ + // and cannot be empty. Let's be defensive and enforce that anyway since things + // like "____" are still valid values that we don't want to allow. + + if (vendorDisplay != null && !vendorDisplay.isEmpty()) { + String vendor = vendorDisplay.trim(); + // Sanitize the vendor + vendor = vendor.replaceAll("[^a-zA-Z0-9-]+", "_"); //$NON-NLS-1$ + if (vendor.equals("_")) { //$NON-NLS-1$ + int h = vendor.hashCode(); + vendor = String.format("vendor%08x", h); //$NON-NLS-1$ + } + + return vendor; + } + + return ""; //$NON-NLS-1$ + + } + + /** + * Returns a string identifier to install this package from the command line. + * For extras, we use "extra-vendor-path". + *

+ * {@inheritDoc} + */ + @Override + public String installId() { + return String.format("extra-%1$s-%2$s", //$NON-NLS-1$ + getVendorId(), + getPath()); + } + + /** + * Returns a description of this package that is suitable for a list display. + *

+ * {@inheritDoc} + */ + @Override + public String getListDescription() { + String ld = getListDisplay(); + if (!ld.isEmpty()) { + return String.format("%1$s%2$s", ld, isObsolete() ? " (Obsolete)" : ""); + } + + String s = String.format("%1$s%2$s", + getDisplayName(), + isObsolete() ? " (Obsolete)" : ""); //$NON-NLS-2$ + + return s; + } + + /** + * Returns a short description for an {@link IDescription}. + */ + @Override + public String getShortDescription() { + String ld = getListDisplay(); + if (!ld.isEmpty()) { + return String.format("%1$s, revision %2$s%3$s", + ld, + getRevision().toShortString(), + isObsolete() ? " (Obsolete)" : ""); + } + + String s = String.format("%1$s, revision %2$s%3$s", + getDisplayName(), + getRevision().toShortString(), + isObsolete() ? " (Obsolete)" : ""); //$NON-NLS-2$ + + return s; + } + + /** + * Returns a long description for an {@link IDescription}. + * + * The long description is whatever the XML contains for the <description> field, + * or the short description if the former is empty. + */ + @Override + public String getLongDescription() { + String s = String.format("%1$s, revision %2$s%3$s\nBy %4$s", + getDisplayName(), + getRevision().toShortString(), + isObsolete() ? " (Obsolete)" : "", //$NON-NLS-2$ + getVendorDisplay()); + + String d = getDescription(); + if (d != null && !d.isEmpty()) { + s += '\n' + d; + } + + if (!getMinToolsRevision().equals(MIN_TOOLS_REV_NOT_SPECIFIED)) { + s += String.format("\nRequires tools revision %1$s", + getMinToolsRevision().toShortString()); + } + + if (getMinApiLevel() != MIN_API_LEVEL_NOT_SPECIFIED) { + s += String.format("\nRequires SDK Platform Android API %1$s", getMinApiLevel()); + } + + File localPath = getLocalArchivePath(); + if (localPath != null) { + // For a local archive, also put the install path in the long description. + // This should help users locate the extra on their drive. + s += String.format("\nLocation: %1$s", localPath.getAbsolutePath()); + } else { + // For a non-installed archive, indicate where it would be installed. + s += String.format("\nInstall path: %1$s", + getInstallSubFolder(null/*sdk root*/).getPath()); + } + + return s; + } + + /** + * Computes a potential installation folder if an archive of this package were + * to be installed right away in the given SDK root. + *

+ * A "tool" package should always be located in SDK/tools. + * + * @param osSdkRoot The OS path of the SDK root folder. Must NOT be null. + * @param sdkManager An existing SDK manager to list current platforms and addons. + * Not used in this implementation. + * @return A new {@link File} corresponding to the directory to use to install this package. + */ + @Override + public File getInstallFolder(String osSdkRoot, SdkManager sdkManager) { + + // First find if this extra is already installed. If so, reuse the same directory. + LocalSdkParser localParser = new LocalSdkParser(); + Package[] pkgs = localParser.parseSdk( + osSdkRoot, + sdkManager, + LocalSdkParser.PARSE_EXTRAS, + new NullTaskMonitor(NullLogger.getLogger())); + + for (Package pkg : pkgs) { + if (sameItemAs(pkg) && pkg instanceof ExtraPackage) { + File localPath = ((ExtraPackage) pkg).getLocalArchivePath(); + if (localPath != null) { + return localPath; + } + } + } + + return getInstallSubFolder(osSdkRoot); + } + + /** + * Computes the "sub-folder" install path, relative to the given SDK root. + * For an extra package, this is generally ".../extra/vendor-id/path". + * + * @param osSdkRoot The OS path of the SDK root folder if known. + * This CAN be null, in which case the path will start at /extra. + * @return Either /extra/vendor/path or sdk-root/extra/vendor-id/path. + */ + private File getInstallSubFolder(@Nullable String osSdkRoot) { + // The /extras dir at the root of the SDK + File path = new File(osSdkRoot, SdkConstants.FD_EXTRAS); + + String vendor = getVendorId(); + if (vendor != null && !vendor.isEmpty()) { + path = new File(path, vendor); + } + + String name = getPath(); + if (name != null && !name.isEmpty()) { + path = new File(path, name); + } + + return path; + } + + @Override + public boolean sameItemAs(Package pkg) { + // Extra packages are similar if they have the same path and vendor + if (pkg instanceof ExtraPackage) { + ExtraPackage ep = (ExtraPackage) pkg; + return PkgDescExtra.compatibleVendorAndPath(mPkgDesc, ep.mPkgDesc); + } + + return false; + } + + /** + * For extra packages, we want to add vendor|path to the sorting key + * before the revision number. + *

+ * {@inheritDoc} + */ + @Override + protected String comparisonKey() { + String s = super.comparisonKey(); + int pos = s.indexOf("|r:"); //$NON-NLS-1$ + assert pos > 0; + s = s.substring(0, pos) + + "|ve:" + getVendorId() + //$NON-NLS-1$ + "|pa:" + getPath() + //$NON-NLS-1$ + s.substring(pos); + return s; + } + + // --- + + /** + * If this package is installed, returns the install path of the archive if valid. + * Returns null if not installed or if the path does not exist. + */ + private File getLocalArchivePath() { + Archive[] archives = getArchives(); + if (archives.length == 1 && archives[0].isLocal()) { + File path = new File(archives[0].getLocalOsPath()); + if (path.isDirectory()) { + return path; + } + } + + return null; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = mMinToolsMixin.hashCode(super.hashCode()); + result = prime * result + mMinApiLevel; + result = prime * result + ((mPath == null) ? 0 : mPath.hashCode()); + result = prime * result + Arrays.hashCode(mProjectFiles); + result = prime * result + ((mVendor == null) ? 0 : mVendor.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (!(obj instanceof ExtraPackage)) { + return false; + } + ExtraPackage other = (ExtraPackage) obj; + if (mMinApiLevel != other.mMinApiLevel) { + return false; + } + if (mPath == null) { + if (other.mPath != null) { + return false; + } + } else if (!mPath.equals(other.mPath)) { + return false; + } + if (!Arrays.equals(mProjectFiles, other.mProjectFiles)) { + return false; + } + if (mVendor == null) { + if (other.mVendor != null) { + return false; + } + } else if (!mVendor.equals(other.mVendor)) { + return false; + } + return mMinToolsMixin.equals(obj); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/FullRevisionPackage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/FullRevisionPackage.java new file mode 100644 index 00000000..5e9b6316 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/FullRevisionPackage.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2012 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.sdklib.internal.repository.packages; + +import com.android.sdklib.internal.repository.sources.SdkSource; +import com.android.sdklib.repository.FullRevision; +import com.android.sdklib.repository.FullRevision.PreviewComparison; +import com.android.sdklib.repository.PkgProps; +import com.android.sdklib.repository.SdkRepoConstants; + +import org.w3c.dom.Node; + +import java.util.Map; +import java.util.Properties; + +/** + * Represents a package in an SDK repository that has a {@link FullRevision}, + * which is a multi-part revision number (major.minor.micro) and an optional preview revision. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public abstract class FullRevisionPackage extends Package + implements IFullRevisionProvider { + + private final FullRevision mPreviewVersion; + + /** + * Creates a new package from the attributes and elements of the given XML node. + * This constructor should throw an exception if the package cannot be created. + * + * @param source The {@link SdkSource} where this is loaded from. + * @param packageNode The XML element being parsed. + * @param nsUri The namespace URI of the originating XML document, to be able to deal with + * parameters that vary according to the originating XML schema. + * @param licenses The licenses loaded from the XML originating document. + */ + FullRevisionPackage(SdkSource source, + Node packageNode, + String nsUri, + Map licenses) { + super(source, packageNode, nsUri, licenses); + + mPreviewVersion = PackageParserUtils.parseFullRevisionElement( + PackageParserUtils.findChildElement(packageNode, SdkRepoConstants.NODE_REVISION)); + } + + /** + * Manually create a new package with one archive and the given attributes. + * This is used to create packages from local directories in which case there must be + * one archive which URL is the actual target location. + *

+ * Properties from props are used first when possible, e.g. if props is non null. + *

+ * By design, this creates a package with one and only one archive. + */ + public FullRevisionPackage( + SdkSource source, + Properties props, + int revision, + String license, + String description, + String descUrl, + String archiveOsPath) { + super(source, props, revision, license, description, descUrl, archiveOsPath); + + FullRevision rev = PackageParserUtils.getPropertyFull(props, PkgProps.PKG_REVISION); + if (rev == null) { + rev = new FullRevision(revision); + } + mPreviewVersion = rev; + } + + @Override + public FullRevision getRevision() { + return mPreviewVersion; + } + + @Override + public void saveProperties(Properties props) { + super.saveProperties(props); + props.setProperty(PkgProps.PKG_REVISION, mPreviewVersion.toShortString()); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((mPreviewVersion == null) ? 0 : mPreviewVersion.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (!(obj instanceof FullRevisionPackage)) { + return false; + } + FullRevisionPackage other = (FullRevisionPackage) obj; + if (mPreviewVersion == null) { + if (other.mPreviewVersion != null) { + return false; + } + } else if (!mPreviewVersion.equals(other.mPreviewVersion)) { + return false; + } + return true; + } + + /** + * Computes whether the given package is a suitable update for the current package. + *

+ * A specific case here is that a release package can update a preview, whereas + * a preview can only update another preview. + *

+ * {@inheritDoc} + */ + @Override + public UpdateInfo canBeUpdatedBy(Package replacementPackage) { + if (replacementPackage == null) { + return UpdateInfo.INCOMPATIBLE; + } + + // check they are the same item, ignoring the preview bit. + if (!sameItemAs(replacementPackage, PreviewComparison.IGNORE)) { + return UpdateInfo.INCOMPATIBLE; + } + + // a preview cannot update a non-preview + if (!getRevision().isPreview() && replacementPackage.getRevision().isPreview()) { + return UpdateInfo.INCOMPATIBLE; + } + + // check revision number + if (replacementPackage.getRevision().compareTo(this.getRevision()) > 0) { + return UpdateInfo.UPDATE; + } + + // not an upgrade but not incompatible either. + return UpdateInfo.NOT_UPDATE; + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IAndroidVersionProvider.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IAndroidVersionProvider.java new file mode 100644 index 00000000..0f1a53f0 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IAndroidVersionProvider.java @@ -0,0 +1,43 @@ +/* + * 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.sdklib.internal.repository.packages; + +import com.android.annotations.NonNull; +import com.android.sdklib.AndroidVersion; + +/** + * Interface for packages that provide an {@link AndroidVersion}. + *

+ * Note that {@link IPlatformDependency} is a similar interface, but with a different semantic. + * The {@link IPlatformDependency} denotes that a given package can only be installed if the + * requested platform is present, whereas this interface denotes that the given package simply + * has a version, which is not necessarily a dependency. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public interface IAndroidVersionProvider { + + /** + * Returns the android version, for platform, add-on and doc packages. + * Can be 0 if this is a local package of unknown api-level. + */ + @NonNull + AndroidVersion getAndroidVersion(); +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IExactApiLevelDependency.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IExactApiLevelDependency.java new file mode 100644 index 00000000..7ed20659 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IExactApiLevelDependency.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2011 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.sdklib.internal.repository.packages; + +import com.android.sdklib.repository.RepoConstants; + +/** + * Interface used to decorate a {@link Package} that has a dependency + * on a specific API level, e.g. which XML has a {@code } element. + *

+ * For example an add-on package requires a platform with an exact API level to be installed + * at the same time. + * This is not the same as {@link IMinApiLevelDependency} which requests that a platform with at + * least the requested API level be present or installed at the same time. + *

+ * Such package requires the {@code } element. It is not an optional + * property, however it can be invalid. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public interface IExactApiLevelDependency { + + /** + * The value of {@link #getExactApiLevel()} when the {@link RepoConstants#NODE_API_LEVEL} + * was not specified in the XML source. + */ + int API_LEVEL_INVALID = 0; + + /** + * Returns the exact API level required by this package, if > 0, + * or {@link #API_LEVEL_INVALID} if the value was missing. + *

+ * This attribute is mandatory and should not be normally missing. + * It can only happen when dealing with an invalid repository XML. + */ + int getExactApiLevel(); +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IFullRevisionProvider.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IFullRevisionProvider.java new file mode 100644 index 00000000..6da200cb --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IFullRevisionProvider.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2012 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.sdklib.internal.repository.packages; + +import com.android.sdklib.repository.FullRevision; +import com.android.sdklib.repository.MajorRevision; +import com.android.sdklib.repository.FullRevision.PreviewComparison; + + + +/** + * Interface for packages that provide a {@link FullRevision}, + * which is a multi-part revision number (major.minor.micro) and an optional preview revision. + *

+ * This interface is a tag. It indicates that {@link Package#getRevision()} returns a + * {@link FullRevision} instead of a limited {@link MajorRevision}.
+ * The preview version number is available via {@link Package#getRevision()}. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public interface IFullRevisionProvider { + + /** + * Returns whether the given package represents the same item as the current package. + *

+ * Two packages are considered the same if they represent the same thing, except for the + * revision number. + * @param pkg The package to compare + * @param comparePreview How to compare previews. + * @return true if the items are the same. + */ + boolean sameItemAs(Package pkg, PreviewComparison comparePreview); + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/ILayoutlibVersion.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/ILayoutlibVersion.java new file mode 100644 index 00000000..30b2112d --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/ILayoutlibVersion.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2011 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.sdklib.internal.repository.packages; + +import com.android.utils.Pair; + +/** + * Interface used to decorate a {@link Package} that provides a version for layout lib. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public interface ILayoutlibVersion { + + int LAYOUTLIB_API_NOT_SPECIFIED = 0; + int LAYOUTLIB_REV_NOT_SPECIFIED = 0; + + /** + * Returns the layoutlib version. Mandatory starting with repository XSD rev 4. + *

+ * The first integer is the API of layoublib, which should be > 0. + * It will be equal to {@link #LAYOUTLIB_API_NOT_SPECIFIED} (0) if the layoutlib + * version isn't specified. + *

+ * The second integer is the revision for that given API. It is >= 0 + * and works as a minor revision number, incremented for the same API level. + * + * @since sdk-repository-4.xsd + */ + Pair getLayoutlibVersion(); +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IMinApiLevelDependency.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IMinApiLevelDependency.java new file mode 100644 index 00000000..60e6d344 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IMinApiLevelDependency.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2010 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.sdklib.internal.repository.packages; + +import com.android.sdklib.repository.SdkRepoConstants; + +/** + * Interface used to decorate a {@link Package} that has a dependency + * on a minimal API level, e.g. which XML has a <min-api-level> element. + *

+ * A package that has this dependency can only be installed if a platform with at least the + * requested API level is present or installed at the same time. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public interface IMinApiLevelDependency { + + /** + * The value of {@link #getMinApiLevel()} when the {@link SdkRepoConstants#NODE_MIN_API_LEVEL} + * was not specified in the XML source. + */ + int MIN_API_LEVEL_NOT_SPECIFIED = 0; + + /** + * Returns the minimal API level required by this package, if > 0, + * or {@link #MIN_API_LEVEL_NOT_SPECIFIED} if there is no such requirement. + */ + int getMinApiLevel(); +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IMinPlatformToolsDependency.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IMinPlatformToolsDependency.java new file mode 100644 index 00000000..49c84f81 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IMinPlatformToolsDependency.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2010 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.sdklib.internal.repository.packages; + +import com.android.sdklib.repository.FullRevision; +import com.android.sdklib.repository.SdkRepoConstants; + +/** + * Interface used to decorate a {@link Package} that has a dependency + * on a minimal platform-tools revision, e.g. which XML has a + * <min-platform-tools-rev> element. + *

+ * A package that has this dependency can only be installed if the requested platform-tools + * revision is present or installed at the same time. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public interface IMinPlatformToolsDependency { + + /** + * The value of {@link #getMinPlatformToolsRevision()} when the + * {@link SdkRepoConstants#NODE_MIN_PLATFORM_TOOLS_REV} was not specified in the XML source. + * Since this is a required attribute in the XML schema, it can only happen when dealing + * with an invalid repository XML. + */ + FullRevision MIN_PLATFORM_TOOLS_REV_INVALID = + new FullRevision(FullRevision.MISSING_MAJOR_REV); + + /** + * The minimal revision of the tools package required by this package if > 0, + * or {@link #MIN_PLATFORM_TOOLS_REV_INVALID} if the value was missing. + *

+ * This attribute is mandatory and should not be normally missing. + * It can only happen when dealing with an invalid repository XML. + */ + FullRevision getMinPlatformToolsRevision(); + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IMinToolsDependency.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IMinToolsDependency.java new file mode 100644 index 00000000..ebdbf591 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IMinToolsDependency.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2010 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.sdklib.internal.repository.packages; + +import com.android.sdklib.repository.FullRevision; +import com.android.sdklib.repository.SdkRepoConstants; + +/** + * Interface used to decorate a {@link Package} that has a dependency + * on a minimal tools revision, e.g. which XML has a <min-tools-rev> element. + *

+ * A package that has this dependency can only be installed if the requested tools revision + * is present or installed at the same time. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public interface IMinToolsDependency { + + /** + * The value of {@link #getMinToolsRevision()} when the + * {@link SdkRepoConstants#NODE_MIN_TOOLS_REV} was not specified in the XML source. + */ + FullRevision MIN_TOOLS_REV_NOT_SPECIFIED = + new FullRevision(FullRevision.MISSING_MAJOR_REV); + + /** + * The minimal revision of the tools package required by this extra package if > 0, + * or {@link #MIN_TOOLS_REV_NOT_SPECIFIED} if there is no such requirement. + */ + FullRevision getMinToolsRevision(); +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IPlatformDependency.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IPlatformDependency.java new file mode 100644 index 00000000..ccbb267c --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IPlatformDependency.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2010 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.sdklib.internal.repository.packages; + +import com.android.sdklib.AndroidVersion; + +/** + * Interface used to decorate a {@link Package} that has a dependency + * on a specific platform (API level and/or code name). + *

+ * A package that has this dependency can only be installed if a platform with at least the + * requested API level is present or installed at the same time. + *

+ * Note that although this interface looks like {@link IAndroidVersionProvider}, it does + * not convey the same semantic since {@link IAndroidVersionProvider} does not + * imply any dependency being a limiting factor as far as installation is concerned. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public interface IPlatformDependency { + + /** Returns the version of the platform dependency of this package. */ + AndroidVersion getAndroidVersion(); + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/LayoutlibVersionMixin.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/LayoutlibVersionMixin.java new file mode 100644 index 00000000..7d18edaa --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/LayoutlibVersionMixin.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2011 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.sdklib.internal.repository.packages; + +import com.android.sdklib.repository.PkgProps; +import com.android.sdklib.repository.RepoConstants; +import com.android.utils.Pair; + +import org.w3c.dom.Node; + +import java.util.Properties; + +/** + * Helper class to handle the layoutlib version provided by a package. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class LayoutlibVersionMixin implements ILayoutlibVersion { + + /** + * The layoutlib version. + * The first integer is the API of layoublib, which should be > 0. + * It will be equal to {@link #LAYOUTLIB_API_NOT_SPECIFIED} (0) if the layoutlib + * version isn't specified. + * The second integer is the revision for that given API. It is >= 0 + * and works as a minor revision number, incremented for the same API level. + */ + private final Pair mLayoutlibVersion; + + /** + * Parses an XML node to process the {@code } element. + * + * The layoutlib element is new in the XSD rev 4, so we need to cope with it missing + * in earlier XMLs. + */ + public LayoutlibVersionMixin(Node pkgNode) { + + int api = LAYOUTLIB_API_NOT_SPECIFIED; + int rev = LAYOUTLIB_REV_NOT_SPECIFIED; + + Node layoutlibNode = + PackageParserUtils.findChildElement(pkgNode, RepoConstants.NODE_LAYOUT_LIB); + + if (layoutlibNode != null) { + api = PackageParserUtils.getXmlInt(layoutlibNode, RepoConstants.NODE_API, 0); + rev = PackageParserUtils.getXmlInt(layoutlibNode, RepoConstants.NODE_REVISION, 0); + } + + mLayoutlibVersion = Pair.of(api, rev); + } + + /** + * Parses the layoutlib version optionally available in the given {@link Properties}. + */ + public LayoutlibVersionMixin(Properties props) { + int layoutlibApi = Package.getPropertyInt(props, PkgProps.LAYOUTLIB_API, + LAYOUTLIB_API_NOT_SPECIFIED); + int layoutlibRev = Package.getPropertyInt(props, PkgProps.LAYOUTLIB_REV, + LAYOUTLIB_REV_NOT_SPECIFIED); + mLayoutlibVersion = Pair.of(layoutlibApi, layoutlibRev); + } + + /** + * Stores the layoutlib version in the given {@link Properties}. + */ + void saveProperties(Properties props) { + if (mLayoutlibVersion.getFirst().intValue() != LAYOUTLIB_API_NOT_SPECIFIED) { + props.setProperty(PkgProps.LAYOUTLIB_API, mLayoutlibVersion.getFirst().toString()); + props.setProperty(PkgProps.LAYOUTLIB_REV, mLayoutlibVersion.getSecond().toString()); + } + } + + /** + * Returns the layoutlib version. + *

+ * The first integer is the API of layoublib, which should be > 0. + * It will be equal to {@link #LAYOUTLIB_API_NOT_SPECIFIED} (0) if the layoutlib + * version isn't specified. + *

+ * The second integer is the revision for that given API. It is >= 0 + * and works as a minor revision number, incremented for the same API level. + * + * @since sdk-repository-4.xsd and sdk-addon-2.xsd + */ + @Override + public Pair getLayoutlibVersion() { + return mLayoutlibVersion; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((mLayoutlibVersion == null) ? 0 : mLayoutlibVersion.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof LayoutlibVersionMixin)) { + return false; + } + LayoutlibVersionMixin other = (LayoutlibVersionMixin) obj; + if (mLayoutlibVersion == null) { + if (other.mLayoutlibVersion != null) { + return false; + } + } else if (!mLayoutlibVersion.equals(other.mLayoutlibVersion)) { + return false; + } + return true; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/MajorRevisionPackage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/MajorRevisionPackage.java new file mode 100644 index 00000000..38bc3a0e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/MajorRevisionPackage.java @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2012 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.sdklib.internal.repository.packages; + +import com.android.sdklib.internal.repository.sources.SdkSource; +import com.android.sdklib.repository.FullRevision; +import com.android.sdklib.repository.MajorRevision; +import com.android.sdklib.repository.PkgProps; +import com.android.sdklib.repository.SdkRepoConstants; + +import org.w3c.dom.Node; + +import java.util.Map; +import java.util.Properties; + +/** + * Represents a package in an SDK repository that has a {@link MajorRevision}, + * which is a single major revision number (not minor, micro or previews). + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public abstract class MajorRevisionPackage extends Package { + + private final MajorRevision mRevision; + + /** + * Creates a new package from the attributes and elements of the given XML node. + * This constructor should throw an exception if the package cannot be created. + * + * @param source The {@link SdkSource} where this is loaded from. + * @param packageNode The XML element being parsed. + * @param nsUri The namespace URI of the originating XML document, to be able to deal with + * parameters that vary according to the originating XML schema. + * @param licenses The licenses loaded from the XML originating document. + */ + MajorRevisionPackage(SdkSource source, + Node packageNode, + String nsUri, + Map licenses) { + super(source, packageNode, nsUri, licenses); + + mRevision = new MajorRevision( + PackageParserUtils.getXmlInt(packageNode, SdkRepoConstants.NODE_REVISION, 0)); + } + + /** + * Manually create a new package with one archive and the given attributes. + * This is used to create packages from local directories in which case there must be + * one archive which URL is the actual target location. + *

+ * Properties from props are used first when possible, e.g. if props is non null. + *

+ * By design, this creates a package with one and only one archive. + */ + public MajorRevisionPackage( + SdkSource source, + Properties props, + int revision, + String license, + String description, + String descUrl, + String archiveOsPath) { + super(source, props, revision, license, description, descUrl, archiveOsPath); + + String revStr = getProperty(props, PkgProps.PKG_REVISION, null); + + MajorRevision rev = null; + if (revStr != null) { + try { + rev = MajorRevision.parseRevision(revStr); + } catch (NumberFormatException ignore) {} + } + if (rev == null) { + rev = new MajorRevision(revision); + } + + mRevision = rev; + } + + /** + * Returns the revision, an int > 0, for all packages (platform, add-on, tool, doc). + * Can be 0 if this is a local package of unknown revision. + */ + @Override + public FullRevision getRevision() { + return mRevision; + } + + + @Override + public void saveProperties(Properties props) { + super.saveProperties(props); + props.setProperty(PkgProps.PKG_REVISION, mRevision.toString()); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((mRevision == null) ? 0 : mRevision.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (!(obj instanceof MajorRevisionPackage)) { + return false; + } + MajorRevisionPackage other = (MajorRevisionPackage) obj; + if (mRevision == null) { + if (other.mRevision != null) { + return false; + } + } else if (!mRevision.equals(other.mRevision)) { + return false; + } + return true; + } + + @Override + public UpdateInfo canBeUpdatedBy(Package replacementPackage) { + if (replacementPackage == null) { + return UpdateInfo.INCOMPATIBLE; + } + + // check they are the same item. + if (!sameItemAs(replacementPackage)) { + return UpdateInfo.INCOMPATIBLE; + } + + // check revision number + if (replacementPackage.getRevision().compareTo(this.getRevision()) > 0) { + return UpdateInfo.UPDATE; + } + + // not an upgrade but not incompatible either. + return UpdateInfo.NOT_UPDATE; + } + + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/MinToolsMixin.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/MinToolsMixin.java new file mode 100644 index 00000000..b85ba8a8 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/MinToolsMixin.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2013 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.sdklib.internal.repository.packages; + +import com.android.sdklib.internal.repository.sources.SdkSource; +import com.android.sdklib.repository.FullRevision; +import com.android.sdklib.repository.PkgProps; +import com.android.sdklib.repository.SdkRepoConstants; + +import org.w3c.dom.Node; + +import java.util.Properties; + +/** + * Represents an XML node in an SDK repository that has a min-tools-rev requirement. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +class MinToolsMixin implements IMinToolsDependency { + + /** + * The minimal revision of the tools package required by this extra package, if > 0, + * or {@link #MIN_TOOLS_REV_NOT_SPECIFIED} if there is no such requirement. + */ + private final FullRevision mMinToolsRevision; + + /** + * Creates a new mixin from the attributes and elements of the given XML node. + * This constructor should throw an exception if the package cannot be created. + * + * @param packageNode The XML element being parsed. + */ + MinToolsMixin(Node packageNode) { + + mMinToolsRevision = PackageParserUtils.parseFullRevisionElement( + PackageParserUtils.findChildElement(packageNode, SdkRepoConstants.NODE_MIN_TOOLS_REV)); + } + + /** + * Manually create a new mixin with one archive and the given attributes. + * This is used to create packages from local directories in which case there must be + * one archive which URL is the actual target location. + *

+ * Properties from props are used first when possible, e.g. if props is non null. + *

+ * By design, this creates a package with one and only one archive. + */ + public MinToolsMixin( + SdkSource source, + Properties props, + int revision, + String license, + String description, + String descUrl, + String archiveOsPath) { + + String revStr = Package.getProperty(props, PkgProps.MIN_TOOLS_REV, null); + + FullRevision rev = MIN_TOOLS_REV_NOT_SPECIFIED; + if (revStr != null) { + try { + rev = FullRevision.parseRevision(revStr); + } catch (NumberFormatException ignore) {} + } + + mMinToolsRevision = rev; + } + + /** + * The minimal revision of the tools package required by this extra package, if > 0, + * or {@link #MIN_TOOLS_REV_NOT_SPECIFIED} if there is no such requirement. + */ + @Override + public FullRevision getMinToolsRevision() { + return mMinToolsRevision; + } + + public void saveProperties(Properties props) { + if (!getMinToolsRevision().equals(MIN_TOOLS_REV_NOT_SPECIFIED)) { + props.setProperty(PkgProps.MIN_TOOLS_REV, getMinToolsRevision().toShortString()); + } + } + + @Override + public int hashCode() { + return hashCode(super.hashCode()); + } + + int hashCode(int superHashCode) { + final int prime = 31; + int result = superHashCode; + result = prime * result + ((mMinToolsRevision == null) ? 0 : mMinToolsRevision.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (!(obj instanceof IMinToolsDependency)) { + return false; + } + IMinToolsDependency other = (IMinToolsDependency) obj; + if (mMinToolsRevision == null) { + if (other.getMinToolsRevision() != null) { + return false; + } + } else if (!mMinToolsRevision.equals(other.getMinToolsRevision())) { + return false; + } + return true; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/MinToolsPackage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/MinToolsPackage.java new file mode 100644 index 00000000..8ea682af --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/MinToolsPackage.java @@ -0,0 +1,117 @@ +/* + * 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.sdklib.internal.repository.packages; + +import com.android.sdklib.internal.repository.sources.SdkSource; +import com.android.sdklib.repository.FullRevision; + +import org.w3c.dom.Node; + +import java.util.Map; +import java.util.Properties; + +/** + * Represents an XML node in an SDK repository that has a min-tools-rev requirement. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public abstract class MinToolsPackage extends MajorRevisionPackage implements IMinToolsDependency { + + private final MinToolsMixin mMinToolsMixin; + + /** + * Creates a new package from the attributes and elements of the given XML node. + * This constructor should throw an exception if the package cannot be created. + * + * @param source The {@link SdkSource} where this is loaded from. + * @param packageNode The XML element being parsed. + * @param nsUri The namespace URI of the originating XML document, to be able to deal with + * parameters that vary according to the originating XML schema. + * @param licenses The licenses loaded from the XML originating document. + */ + MinToolsPackage(SdkSource source, Node packageNode, String nsUri, Map licenses) { + super(source, packageNode, nsUri, licenses); + + mMinToolsMixin = new MinToolsMixin(packageNode); + } + + /** + * Manually create a new package with one archive and the given attributes. + * This is used to create packages from local directories in which case there must be + * one archive which URL is the actual target location. + *

+ * Properties from props are used first when possible, e.g. if props is non null. + *

+ * By design, this creates a package with one and only one archive. + */ + public MinToolsPackage( + SdkSource source, + Properties props, + int revision, + String license, + String description, + String descUrl, + String archiveOsPath) { + super(source, props, revision, license, description, descUrl, archiveOsPath); + + mMinToolsMixin = new MinToolsMixin( + source, + props, + revision, + license, + description, + descUrl, + archiveOsPath); + } + + /** + * The minimal revision of the tools package required by this extra package, if > 0, + * or {@link #MIN_TOOLS_REV_NOT_SPECIFIED} if there is no such requirement. + */ + @Override + public FullRevision getMinToolsRevision() { + return mMinToolsMixin.getMinToolsRevision(); + } + + @Override + public void saveProperties(Properties props) { + super.saveProperties(props); + mMinToolsMixin.saveProperties(props); + } + + @Override + public int hashCode() { + return mMinToolsMixin.hashCode(super.hashCode()); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (!(obj instanceof MinToolsPackage)) { + return false; + } + return mMinToolsMixin.equals(obj); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/NoPreviewRevisionPackage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/NoPreviewRevisionPackage.java new file mode 100644 index 00000000..fc5a5dd1 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/NoPreviewRevisionPackage.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2013 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.sdklib.internal.repository.packages; + +import com.android.sdklib.internal.repository.sources.SdkSource; +import com.android.sdklib.repository.NoPreviewRevision; +import com.android.sdklib.repository.PkgProps; +import com.android.sdklib.repository.SdkRepoConstants; + +import org.w3c.dom.Node; + +import java.util.Map; +import java.util.Properties; + +/** + * Represents a package in an SDK repository that has a {@link NoPreviewRevision}, + * which is a single major.minor.micro revision number and no preview. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public abstract class NoPreviewRevisionPackage extends Package { + + private final NoPreviewRevision mRevision; + + /** + * Creates a new package from the attributes and elements of the given XML node. + * This constructor should throw an exception if the package cannot be created. + * + * @param source The {@link SdkSource} where this is loaded from. + * @param packageNode The XML element being parsed. + * @param nsUri The namespace URI of the originating XML document, to be able to deal with + * parameters that vary according to the originating XML schema. + * @param licenses The licenses loaded from the XML originating document. + */ + NoPreviewRevisionPackage(SdkSource source, + Node packageNode, + String nsUri, + Map licenses) { + super(source, packageNode, nsUri, licenses); + + mRevision = PackageParserUtils.parseNoPreviewRevisionElement( + PackageParserUtils.findChildElement(packageNode, SdkRepoConstants.NODE_REVISION)); + } + + /** + * Manually create a new package with one archive and the given attributes. + * This is used to create packages from local directories in which case there must be + * one archive which URL is the actual target location. + *

+ * Properties from props are used first when possible, e.g. if props is non null. + *

+ * By design, this creates a package with one and only one archive. + */ + public NoPreviewRevisionPackage( + SdkSource source, + Properties props, + int revision, + String license, + String description, + String descUrl, + String archiveOsPath) { + super(source, props, revision, license, description, descUrl, archiveOsPath); + + String revStr = getProperty(props, PkgProps.PKG_REVISION, null); + + NoPreviewRevision rev = null; + if (revStr != null) { + try { + rev = NoPreviewRevision.parseRevision(revStr); + } catch (NumberFormatException ignore) {} + } + if (rev == null) { + rev = new NoPreviewRevision(revision); + } + + mRevision = rev; + } + + /** + * Returns the revision, an int > 0, for all packages (platform, add-on, tool, doc). + * Can be 0 if this is a local package of unknown revision. + */ + @Override + public NoPreviewRevision getRevision() { + return mRevision; + } + + + @Override + public void saveProperties(Properties props) { + super.saveProperties(props); + props.setProperty(PkgProps.PKG_REVISION, mRevision.toString()); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((mRevision == null) ? 0 : mRevision.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (!(obj instanceof NoPreviewRevisionPackage)) { + return false; + } + NoPreviewRevisionPackage other = (NoPreviewRevisionPackage) obj; + if (mRevision == null) { + if (other.mRevision != null) { + return false; + } + } else if (!mRevision.equals(other.mRevision)) { + return false; + } + return true; + } + + @Override + public UpdateInfo canBeUpdatedBy(Package replacementPackage) { + if (replacementPackage == null) { + return UpdateInfo.INCOMPATIBLE; + } + + // check they are the same item. + if (!sameItemAs(replacementPackage)) { + return UpdateInfo.INCOMPATIBLE; + } + + // check revision number + if (replacementPackage.getRevision().compareTo(this.getRevision()) > 0) { + return UpdateInfo.UPDATE; + } + + // not an upgrade but not incompatible either. + return UpdateInfo.NOT_UPDATE; + } + + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/Package.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/Package.java new file mode 100644 index 00000000..4cf6f9b6 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/Package.java @@ -0,0 +1,902 @@ +/* + * 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.sdklib.internal.repository.packages; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.annotations.VisibleForTesting; +import com.android.annotations.VisibleForTesting.Visibility; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.SdkManager; +import com.android.sdklib.repository.IDescription; +import com.android.sdklib.repository.IListDescription; +import com.android.sdklib.internal.repository.ITaskMonitor; +import com.android.sdklib.internal.repository.archives.Archive; +import com.android.sdklib.internal.repository.sources.SdkAddonSource; +import com.android.sdklib.internal.repository.sources.SdkRepoSource; +import com.android.sdklib.internal.repository.sources.SdkSource; +import com.android.sdklib.io.IFileOp; +import com.android.sdklib.repository.*; +import com.android.sdklib.repository.descriptors.IPkgDesc; + +import com.android.sdklib.repository.descriptors.PkgDesc; +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.w3c.dom.Node; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Map; +import java.util.Properties; + +/** + * A {@link Package} is the base class for "something" that can be downloaded from + * the SDK repository. + *

+ * A package has some attributes (revision, description) and a list of archives + * which represent the downloadable bits. + *

+ * Packages are contained by a {@link SdkSource} (a download site). + *

+ * Derived classes must implement the {@link IDescription} methods. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public abstract class Package implements IDescription, IListDescription, Comparable { + + private final String mObsolete; + private final License mLicense; + private final String mListDisplay; + private final String mDescription; + private final String mDescUrl; + @Deprecated + private final String mReleaseNote; + @Deprecated + private final String mReleaseUrl; + private final Archive[] mArchives; + private final SdkSource mSource; + + + // figure if we'll need to set the unix permissions + private static final boolean sUsingUnixPerm = + SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN || + SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_LINUX; + + /** + * Enum for the result of {@link Package#canBeUpdatedBy(Package)}. This used so that we can + * differentiate between a package that is totally incompatible, and one that is the same item + * but just not an update. + * @see #canBeUpdatedBy(Package) + */ + public enum UpdateInfo { + /** Means that the 2 packages are not the same thing */ + INCOMPATIBLE, + /** Means that the 2 packages are the same thing but one does not upgrade the other. + *

+ * TODO: this name is confusing. We need to dig deeper. */ + NOT_UPDATE, + /** Means that the 2 packages are the same thing, and one is the upgrade of the other */ + UPDATE + } + + /** + * Creates a new package from the attributes and elements of the given XML node. + * This constructor should throw an exception if the package cannot be created. + * + * @param source The {@link SdkSource} where this is loaded from. + * @param packageNode The XML element being parsed. + * @param nsUri The namespace URI of the originating XML document, to be able to deal with + * parameters that vary according to the originating XML schema. + * @param licenses The licenses loaded from the XML originating document. + */ + Package(SdkSource source, Node packageNode, String nsUri, Map licenses) { + mSource = source; + mListDisplay = + PackageParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_LIST_DISPLAY); + mDescription = + PackageParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_DESCRIPTION); + mDescUrl = + PackageParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_DESC_URL); + mReleaseNote = + PackageParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_RELEASE_NOTE); + mReleaseUrl = + PackageParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_RELEASE_URL); + mObsolete = + PackageParserUtils.getOptionalXmlString(packageNode, SdkRepoConstants.NODE_OBSOLETE); + + mLicense = parseLicense(packageNode, licenses); + mArchives = parseArchives( + PackageParserUtils.findChildElement(packageNode, SdkRepoConstants.NODE_ARCHIVES)); + } + + /** + * Manually create a new package with one archive and the given attributes. + * This is used to create packages from local directories in which case there must be + * one archive which URL is the actual target location. + *

+ * Properties from props are used first when possible, e.g. if props is non null. + *

+ * By design, this creates a package with one and only one archive. + */ + public Package( + SdkSource source, + Properties props, + int revision, + String license, + String description, + String descUrl, + String archiveOsPath) { + + if (description == null) { + description = ""; + } + if (descUrl == null) { + descUrl = ""; + } + + license = getProperty(props, PkgProps.PKG_LICENSE, license); + if (license != null) { + mLicense = new License(license, getProperty(props, PkgProps.PKG_LICENSE_REF, null)); + } + else { + mLicense = null; + } + mListDisplay = getProperty(props, PkgProps.PKG_LIST_DISPLAY, ""); //$NON-NLS-1$ + mDescription = getProperty(props, PkgProps.PKG_DESC, description); + mDescUrl = getProperty(props, PkgProps.PKG_DESC_URL, descUrl); + mReleaseNote = getProperty(props, PkgProps.PKG_RELEASE_NOTE, ""); //$NON-NLS-1$ + mReleaseUrl = getProperty(props, PkgProps.PKG_RELEASE_URL, ""); //$NON-NLS-1$ + mObsolete = getProperty(props, PkgProps.PKG_OBSOLETE, null); + + // If source is null and we can find a source URL in the properties, generate + // a dummy source just to store the URL. This allows us to easily remember where + // a package comes from. + String srcUrl = getProperty(props, PkgProps.PKG_SOURCE_URL, null); + if (props != null && source == null && srcUrl != null) { + // Both Addon and Extra packages can come from an addon source. + // For Extras, we can tell by looking at the source URL. + if (this instanceof AddonPackage || + ((this instanceof ExtraPackage) && + srcUrl.endsWith(SdkAddonConstants.URL_DEFAULT_FILENAME))) { + source = new SdkAddonSource(srcUrl, null /*uiName*/); + } else { + source = new SdkRepoSource(srcUrl, null /*uiName*/); + } + } + mSource = source; + + // Note: if archiveOsPath is non-null, this makes a local archive (e.g. a locally + // installed package.) If it's null, this makes a remote archive. + mArchives = initializeArchives(props, archiveOsPath); + } + + /** + * Returns the {@link IPkgDesc} describing this package's meta data. + * + * @return A non-null {@link IPkgDesc}. + */ + @NonNull + public abstract IPkgDesc getPkgDesc(); + + /** + * Called by the constructor to get the initial {@link #mArchives} array. + *

+ * This is invoked by the local-package constructor and allows mock testing + * classes to override the archives created. + * This is an implementation details and clients must not + * rely on this. + * + * @return Always return a non-null array. The array may be empty. + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected Archive[] initializeArchives( + Properties props, + String archiveOsPath) { + return new Archive[] { + new Archive(this, + props, + archiveOsPath) }; + } + + /** + * Utility method that returns a property from a {@link Properties} object. + * Returns the default value if props is null or if the property is not defined. + * + * @param props The {@link Properties} to search into. + * If null, the default value is returned. + * @param propKey The name of the property. Must not be null. + * @param defaultValue The default value to return if {@code props} is null or if the + * key is not found. Can be null. + * @return The string value of the given key in the properties, or null if the key + * isn't found or if {@code props} is null. + */ + @Nullable + static String getProperty( + @Nullable Properties props, + @NonNull String propKey, + @Nullable String defaultValue) { + return PackageParserUtils.getProperty(props, propKey, defaultValue); + } + + /** + * Utility method that returns an integer property from a {@link Properties} object. + * Returns the default value if props is null or if the property is not defined or + * cannot be parsed to an integer. + * + * @param props The {@link Properties} to search into. + * If null, the default value is returned. + * @param propKey The name of the property. Must not be null. + * @param defaultValue The default value to return if {@code props} is null or if the + * key is not found. Can be null. + * @return The integer value of the given key in the properties, or the {@code defaultValue}. + */ + static int getPropertyInt( + @Nullable Properties props, + @NonNull String propKey, + int defaultValue) { + return PackageParserUtils.getPropertyInt(props, propKey, defaultValue); + } + + /** + * Save the properties of the current packages in the given {@link Properties} object. + * These properties will later be give the constructor that takes a {@link Properties} object. + */ + public void saveProperties(@NonNull Properties props) { + if (mLicense != null) { + String license = mLicense.getLicense(); + if (license != null && !license.isEmpty()) { + props.setProperty(PkgProps.PKG_LICENSE, license); + } + String licenseRef = mLicense.getLicenseRef(); + if (licenseRef != null && !licenseRef.isEmpty()) { + props.setProperty(PkgProps.PKG_LICENSE_REF, licenseRef); + } + } + if (mListDisplay != null && !mListDisplay.isEmpty()) { + props.setProperty(PkgProps.PKG_LIST_DISPLAY, mListDisplay); + } + if (mDescription != null && !mDescription.isEmpty()) { + props.setProperty(PkgProps.PKG_DESC, mDescription); + } + if (mDescUrl != null && !mDescUrl.isEmpty()) { + props.setProperty(PkgProps.PKG_DESC_URL, mDescUrl); + } + + if (mReleaseNote != null && !mReleaseNote.isEmpty()) { + props.setProperty(PkgProps.PKG_RELEASE_NOTE, mReleaseNote); + } + if (mReleaseUrl != null && !mReleaseUrl.isEmpty()) { + props.setProperty(PkgProps.PKG_RELEASE_URL, mReleaseUrl); + } + if (mObsolete != null) { + props.setProperty(PkgProps.PKG_OBSOLETE, mObsolete); + } + if (mSource != null) { + props.setProperty(PkgProps.PKG_SOURCE_URL, mSource.getUrl()); + } + } + + /** + * Parses the uses-licence node of this package, if any, and returns the license + * definition if there's one. Returns null if there's no uses-license element or no + * license of this name defined. + */ + @Nullable + private License parseLicense(@NonNull Node packageNode, @NonNull Map licenses) { + Node usesLicense = + PackageParserUtils.findChildElement(packageNode, SdkRepoConstants.NODE_USES_LICENSE); + if (usesLicense != null) { + Node ref = usesLicense.getAttributes().getNamedItem(SdkRepoConstants.ATTR_REF); + if (ref != null) { + String licenseRef = ref.getNodeValue(); + return new License(licenses.get(licenseRef), licenseRef); + } + } + return null; + } + + /** + * Parses an XML node to process the element. + * Always return a non-null array. The array may be empty. + */ + @NonNull + private Archive[] parseArchives(@NonNull Node archivesNode) { + ArrayList archives = new ArrayList(); + + if (archivesNode != null) { + String nsUri = archivesNode.getNamespaceURI(); + for(Node child = archivesNode.getFirstChild(); + child != null; + child = child.getNextSibling()) { + + if (child.getNodeType() == Node.ELEMENT_NODE && + nsUri.equals(child.getNamespaceURI()) && + SdkRepoConstants.NODE_ARCHIVE.equals(child.getLocalName())) { + archives.add(parseArchive(child)); + } + } + } + + return archives.toArray(new Archive[archives.size()]); + } + + /** + * Parses one element from an container. + */ + @NonNull + private Archive parseArchive(@NonNull Node archiveNode) { + Archive a = new Archive( + this, + PackageParserUtils.parseArchFilter(archiveNode), + PackageParserUtils.getXmlString(archiveNode, SdkRepoConstants.NODE_URL), + PackageParserUtils.getXmlLong (archiveNode, SdkRepoConstants.NODE_SIZE, 0), + PackageParserUtils.getXmlString(archiveNode, SdkRepoConstants.NODE_CHECKSUM) + ); + + return a; + } + + /** + * Returns the source that created (and owns) this package. Can be null. + */ + @Nullable + public SdkSource getParentSource() { + return mSource; + } + + /** + * Returns true if the package is deemed obsolete, that is it contains an + * actual <obsolete> element. + */ + public boolean isObsolete() { + return mObsolete != null; + } + + /** + * Returns the revision, an int > 0, for all packages (platform, add-on, tool, doc). + * Can be 0 if this is a local package of unknown revision. + */ + @NonNull + public abstract FullRevision getRevision(); + + /** + * Returns the optional description for all packages (platform, add-on, tool, doc) or + * for a lib. It is null if the element has not been specified in the repository XML. + */ + @Nullable + public License getLicense() { + return mLicense; + } + + /** + * Returns the optional description for all packages (platform, add-on, tool, doc) or + * for a lib. This is the raw description available from the XML meta data and is typically + * only used internally. + *

+ * For actual display in the UI, use the methods from {@link IDescription} instead. + *

+ * Can be empty but not null. + */ + @NonNull + public String getDescription() { + return mDescription; + } + + /** + * Returns the optional list-display for all packages as defined in the XML meta data + * and is typically only used internally. + *

+ * For actual display in the UI, use {@link IListDescription} instead. + *

+ * Can be empty but not null. + */ + @NonNull + public String getListDisplay() { + return mListDisplay; + } + + /** + * Returns the optional description URL for all packages (platform, add-on, tool, doc). + * Can be empty but not null. + */ + @NonNull + public String getDescUrl() { + return mDescUrl; + } + + /** + * Returns the optional release note for all packages (platform, add-on, tool, doc) or + * for a lib. Can be empty but not null. + */ + @NonNull + public String getReleaseNote() { + return mReleaseNote; + } + + /** + * Returns the optional release note URL for all packages (platform, add-on, tool, doc). + * Can be empty but not null. + */ + @NonNull + public String getReleaseNoteUrl() { + return mReleaseUrl; + } + + /** + * Returns the archives defined in this package. + * Can be an empty array but not null. + */ + @NonNull + public Archive[] getArchives() { + return mArchives; + } + + /** + * Returns true if this package contains the exact given archive. + * Important: This compares object references, not object equality. + */ + public boolean hasArchive(Archive archive) { + for (Archive a : mArchives) { + if (a == archive) { + return true; + } + } + return false; + } + + /** + * Returns whether the {@link Package} has at least one {@link Archive} compatible with + * the host platform. + */ + public boolean hasCompatibleArchive() { + for (Archive archive : mArchives) { + if (archive.isCompatible()) { + return true; + } + } + + return false; + } + + /** + * Returns a short, reasonably unique string identifier that can be used + * to identify this package when installing from the command-line interface. + * {@code 'android list sdk'} will show these IDs and then in turn they can + * be provided to {@code 'android update sdk --no-ui --filter'} to select + * some specific packages. + *

+ * The identifiers must have the following properties:
+ * - They must contain only simple alphanumeric characters.
+ * - Commas, whitespace and any special character that could be obviously problematic + * to a shell interface should be avoided (so dash/underscore are OK, but things + * like colon, pipe or dollar should be avoided.)
+ * - The name must be consistent across calls and reasonably unique for the package + * type. Collisions can occur but should be rare.
+ * - Different package types should have a clearly different name pattern.
+ * - The revision number should not be included, as this would prevent updates + * from being automated (which is the whole point.)
+ * - It must remain reasonably human readable.
+ * - If no such id can exist (for example for a local package that cannot be installed) + * then an empty string should be returned. Don't return null. + *

+ * Important: This is not a strong unique identifier for the package. + * If you need a strong unique identifier, you should use {@link #comparisonKey()} + * and the {@link Comparable} interface. + */ + @NonNull + public abstract String installId(); + + /** + * Returns the short description of the source, if not null. + * Otherwise returns the default Object toString result. + *

+ * This is mostly helpful for debugging. + * For UI display, use the {@link IDescription} interface. + */ + @NonNull + @Override + public String toString() { + String s = getShortDescription(); + if (s != null) { + return s; + } + return super.toString(); + } + + /** + * Returns a description of this package that is suitable for a list display. + * Should not be empty. Can never be null. + *

+ * Derived classes should use {@link #getListDisplay()} if it's not empty. + *

+ * When it is empty, the default behavior is to recompute a string that depends + * on the package type. + *

+ * In both cases, the string should indicate whether the package is marked as obsolete. + *

+ * Note that this is the "base" name for the package with no specific revision nor API + * mentioned as this is likely used in a table that will already provide these details. + * In contrast, {@link #getShortDescription()} should be used if you want more details + * such as the package revision number or the API, if applicable, all in the same string. + */ + @NonNull + @Override + public abstract String getListDescription(); + + /** + * Returns a short description for an {@link IDescription}. + * Can be empty but not null. + */ + @NonNull + @Override + public abstract String getShortDescription(); + + /** + * Returns a long description for an {@link IDescription}. + * Can be empty but not null. + */ + @NonNull + @Override + public String getLongDescription() { + StringBuilder sb = new StringBuilder(); + + String s = getDescription(); + if (s != null) { + sb.append(s); + } + if (sb.length() > 0) { + sb.append("\n"); + } + + sb.append(String.format("Revision %1$s%2$s", + getRevision().toShortString(), + isObsolete() ? " (Obsolete)" : "")); + + s = getDescUrl(); + if (s != null && !s.isEmpty()) { + sb.append(String.format("\n\nMore information at %1$s", s)); + } + + s = getReleaseNote(); + if (s != null && !s.isEmpty()) { + sb.append("\n\nRelease note:\n").append(s); + } + + s = getReleaseNoteUrl(); + if (s != null && !s.isEmpty()) { + sb.append("\nRelease note URL: ").append(s); + } + + return sb.toString(); + } + + /** + * A package is local (that is 'installed locally') if it contains a single + * archive that is local. If not local, it's a remote package, only available + * on a remote source for download and installation. + */ + public boolean isLocal() { + return mArchives.length == 1 && mArchives[0].isLocal(); + } + + /** + * Computes a potential installation folder if an archive of this package were + * to be installed right away in the given SDK root. + *

+ * Some types of packages install in a fix location, for example docs and tools. + * In this case the returned folder may already exist with a different archive installed + * at the desired location.
+ * For other packages types, such as add-on or platform, the folder name is only partially + * relevant to determine the content and thus a real check will be done to provide an + * existing or new folder depending on the current content of the SDK. + *

+ * Note that the installer *will* create all directories returned here just before + * installation so this method must not attempt to create them. + * + * @param osSdkRoot The OS path of the SDK root folder. + * @param sdkManager An existing SDK manager to list current platforms and addons. + * @return A new {@link File} corresponding to the directory to use to install this package. + */ + @NonNull + public abstract File getInstallFolder(String osSdkRoot, SdkManager sdkManager); + + /** + * Hook called right before an archive is installed. The archive has already + * been downloaded successfully and will be installed in the directory specified by + * installFolder when this call returns. + *

+ * The hook lets the package decide if installation of this specific archive should + * be continue. The installer will still install the remaining packages if possible. + *

+ * The base implementation always return true. + *

+ * Note that the installer *will* create all directories specified by + * {@link #getInstallFolder} just before installation, so they must not be + * created here. This is also called before the previous install dir is removed + * so the previous content is still there during upgrade. + * + * @param archive The archive that will be installed + * @param monitor The {@link ITaskMonitor} to display errors. + * @param osSdkRoot The OS path of the SDK root folder. + * @param installFolder The folder where the archive will be installed. Note that this + * is not the folder where the archive was temporary + * unzipped. The installFolder, if it exists, contains the old + * archive that will soon be replaced by the new one. + * @return True if installing this archive shall continue, false if it should be skipped. + */ + public boolean preInstallHook(Archive archive, ITaskMonitor monitor, + String osSdkRoot, File installFolder) { + // Nothing to do in base class. + return true; + } + + /** + * Hook called right after a file has been unzipped (during an install). + *

+ * The base class implementation makes sure to properly adjust set executable + * permission on Linux and MacOS system if the zip entry was marked as +x. + * + * @param archive The archive that is being installed. + * @param monitor The {@link ITaskMonitor} to display errors. + * @param fileOp The {@link IFileOp} used by the archive installer. + * @param unzippedFile The file that has just been unzipped in the install temp directory. + * @param zipEntry The {@link ZipArchiveEntry} that has just been unzipped. + */ + public void postUnzipFileHook( + Archive archive, + ITaskMonitor monitor, + IFileOp fileOp, + File unzippedFile, + ZipArchiveEntry zipEntry) { + + // if needed set the permissions. + if (sUsingUnixPerm && fileOp.isFile(unzippedFile)) { + // get the mode and test if it contains the executable bit + int mode = zipEntry.getUnixMode(); + if ((mode & 0111) != 0) { + try { + fileOp.setExecutablePermission(unzippedFile); + } catch (IOException ignore) {} + } + } + + } + + /** + * Hook called right after an archive has been installed. + * + * @param archive The archive that has been installed. + * @param monitor The {@link ITaskMonitor} to display errors. + * @param installFolder The folder where the archive was successfully installed. + * Null if the installation failed, in case the archive needs to + * do some cleanup after preInstallHook. + */ + public void postInstallHook(Archive archive, ITaskMonitor monitor, File installFolder) { + // Nothing to do in base class. + } + + /** + * Returns whether the give package represents the same item as the current package. + *

+ * Two packages are considered the same if they represent the same thing, except for the + * revision number. + * @param pkg the package to compare. + * @return true if the item as equivalent. + */ + public abstract boolean sameItemAs(Package pkg); + + /** + * Computes whether the given package is a suitable update for the current package. + *

+ * An update is just that: a new package that supersedes the current one. If the new + * package does not represent the same item or if it has the same or lower revision as the + * current one, it's not an update. + * + * @param replacementPackage The potential replacement package. + * @return One of the {@link UpdateInfo} values. + * + * @see #sameItemAs(Package) + */ + @NonNull + public abstract UpdateInfo canBeUpdatedBy(Package replacementPackage); + + /** + * Returns an ordering suitable for display like this:
+ * - Tools
+ * - Platform-Tools
+ * - Built-Tools
+ * - Docs.
+ * - Platform n preview
+ * - Platform n
+ * - Platform n-1
+ * - Samples packages
+ * - Add-on based on n preview
+ * - Add-on based on n
+ * - Add-on based on n-1
+ * - Extra packages
+ *

+ * Important: this must NOT be used to compare if two packages are the same thing. + * This is achieved by {@link #sameItemAs(Package)} or {@link #canBeUpdatedBy(Package)}. + *

+ * The order done here is suitable for display, and this may not be the appropriate + * order when comparing whether packages are equal or of greater revision -- if you need + * to compare revisions, then use {@link #getRevision()}{@code .compareTo(rev)} directly. + *

+ * This {@link #compareTo(Package)} method is purely an implementation detail to + * perform the right ordering of the packages in the list of available or installed packages. + *

+ * Important: Derived classes should consider overriding {@link #comparisonKey()} + * instead of this method. + */ + @Override + public int compareTo(Package other) { + String s1 = this.comparisonKey(); + String s2 = other.comparisonKey(); + + int r = s1.compareTo(s2); + return r; + } + + /** + * Computes a comparison key for each package used by {@link #compareTo(Package)}. + * The key is a string. + * The base package class return a string that encodes the package type, + * the revision number and the platform version, if applicable, in the form: + *

+     *      t:N|v:NNNN.P|r:NNNN|
+     * 
+ * All fields must start by a "letter colon" prefix and end with a vertical pipe (|, ASCII 124). + *

+ * The string format may change between releases and clients should not + * store them outside of the session or expect them to be consistent between + * different releases. They are purely an internal implementation details of the + * {@link #compareTo(Package)} method. + *

+ * Derived classes should get the string from the super class and then append + * or insert their own |-separated content. + * For example an extra vendor name & path can be inserted before the revision + * number, since it has more sorting weight. + */ + @NonNull + protected String comparisonKey() { + + StringBuilder sb = new StringBuilder(); + + sb.append("t:"); //$NON-NLS-1$ + if (this instanceof ToolPackage) { + sb.append(0); + } else if (this instanceof PlatformToolPackage) { + sb.append(1); + } else if (this instanceof BuildToolPackage) { + sb.append(2); + } else if (this instanceof DocPackage) { + sb.append(3); + } else if (this instanceof PlatformPackage) { + sb.append(4); + } else if (this instanceof SamplePackage) { + sb.append(5); + } else if ((this instanceof SystemImagePackage) && ((SystemImagePackage) this).isPlatform()) { + sb.append(6); + } else if (this instanceof AddonPackage) { + sb.append(7); + } else if (this instanceof SystemImagePackage) { + sb.append(8); + } else { + // extras and everything else + sb.append(9); + } + + + // We insert the package version here because it is more important + // than the revision number. + // In the list display, we want package version to be sorted + // top-down, so we'll use 10k-api as the sorting key. The day we + // reach 10k APIs, we'll need to revisit this. + sb.append("|v:"); //$NON-NLS-1$ + if (this instanceof IAndroidVersionProvider) { + AndroidVersion v = ((IAndroidVersionProvider) this).getAndroidVersion(); + + sb.append(String.format("%1$04d.%2$d", //$NON-NLS-1$ + 10000 - v.getApiLevel(), + v.isPreview() ? 1 : 0 + )); + } + + // Append revision number + // Note: pad revision numbers to 4 digits (e.g. make sure 12>3 by comparing 0012 and 0003) + sb.append("|r:"); //$NON-NLS-1$ + FullRevision rev = getRevision(); + sb.append(String.format("%1$04d.%2$04d.%3$04d.", //$NON-NLS-1$ + rev.getMajor(), + rev.getMinor(), + rev.getMicro())); + // Hack: When comparing packages for installation purposes, we want to treat + // "final releases" packages as more important than rc/preview packages. + // However like for the API level above, when sorting for list display purposes + // we want the final release package listed before its rc/preview packages. + if (rev.isPreview()) { + sb.append(rev.getPreview()); + } else { + sb.append('0'); // 0=Final (!preview), to make "18.0" < "18.1" (18 Final < 18 RC1) + } + + sb.append('|'); + return sb.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(mArchives); + result = prime * result + ((mObsolete == null) ? 0 : mObsolete.hashCode()); + result = prime * result + getRevision().hashCode(); + result = prime * result + ((mSource == null) ? 0 : mSource.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof Package)) { + return false; + } + Package other = (Package) obj; + if (!Arrays.equals(mArchives, other.mArchives)) { + return false; + } + if (mObsolete == null) { + if (other.mObsolete != null) { + return false; + } + } else if (!mObsolete.equals(other.mObsolete)) { + return false; + } + if (!getRevision().equals(other.getRevision())) { + return false; + } + if (mSource == null) { + if (other.mSource != null) { + return false; + } + } else if (!mSource.equals(other.mSource)) { + return false; + } + return true; + } + + // TODO(jbakermalone): This is moved here from the more logical location in PkgDesc.Builder since Package will soon be forked into + // studio and this version deprecated, whereas PkgDesc will not. + protected PkgDesc.Builder setDescriptions(PkgDesc.Builder builder) { + builder.setDescriptionShort(getShortDescription()); + builder.setDescriptionUrl(getDescUrl()); + builder.setListDisplay(getListDisplay()); + builder.setIsObsolete(isObsolete()); + builder.setLicense(getLicense()); + return builder; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/PackageParserUtils.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/PackageParserUtils.java new file mode 100644 index 00000000..eeee4a9e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/PackageParserUtils.java @@ -0,0 +1,427 @@ +/* + * 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.sdklib.internal.repository.packages; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdklib.internal.repository.archives.ArchFilter; +import com.android.sdklib.internal.repository.archives.BitSize; +import com.android.sdklib.internal.repository.archives.HostOs; +import com.android.sdklib.internal.repository.archives.LegacyArch; +import com.android.sdklib.internal.repository.archives.LegacyOs; +import com.android.sdklib.repository.FullRevision; +import com.android.sdklib.repository.NoPreviewRevision; +import com.android.sdklib.repository.MajorRevision; +import com.android.sdklib.repository.PkgProps; +import com.android.sdklib.repository.SdkRepoConstants; + +import org.w3c.dom.Node; + +import java.util.Properties; + +/** + * Misc utilities to help extracting elements and attributes out of a repository XML document. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class PackageParserUtils { + + /** + * Parse the {@link ArchFilter} of an <archive> element.. + *

+ * Starting with repo schema 10, add-on schema 7 and sys-img schema 3, this is done using + * specific optional elements contained within the <archive> element. + *

+ * If none of the new element are defined, for backward compatibility we try to find + * the previous style XML attributes "os" and "arch" in the <archive> element. + * + * @param archiveNode + * @return A new {@link ArchFilter} + */ + @NonNull + public static ArchFilter parseArchFilter(@NonNull Node archiveNode) { + String hos = PackageParserUtils.getOptionalXmlString(archiveNode, SdkRepoConstants.NODE_HOST_OS); + String hb = PackageParserUtils.getOptionalXmlString(archiveNode, SdkRepoConstants.NODE_HOST_BITS); + String jb = PackageParserUtils.getOptionalXmlString(archiveNode, SdkRepoConstants.NODE_JVM_BITS); + String mjv = PackageParserUtils.getOptionalXmlString(archiveNode, SdkRepoConstants.NODE_MIN_JVM_VERSION); + + if (hos != null || hb != null || jb != null || mjv != null) { + NoPreviewRevision rev = null; + try { + rev = NoPreviewRevision.parseRevision(mjv); + } catch (NumberFormatException ignore) {} + + return new ArchFilter( + HostOs.fromXmlName(hos), + BitSize.fromXmlName(hb), + BitSize.fromXmlName(jb), + rev); + } + + Properties props = new Properties(); + + LegacyOs o = (LegacyOs) PackageParserUtils.getEnumAttribute( + archiveNode, SdkRepoConstants.LEGACY_ATTR_OS, LegacyOs.values(), null); + if (o != null) { + props.setProperty(ArchFilter.LEGACY_PROP_OS, o.toString()); + } + + LegacyArch a = (LegacyArch) PackageParserUtils.getEnumAttribute( + archiveNode, SdkRepoConstants.LEGACY_ATTR_ARCH, LegacyArch.values(), null); + if (a != null) { + props.setProperty(ArchFilter.LEGACY_PROP_ARCH, a.toString()); + } + + return new ArchFilter(props); + } + + /** + * Parses a full revision element such as or . + * This supports both the single-integer format as well as the full revision + * format with major/minor/micro/preview sub-elements. + * + * @param revisionNode The node to parse. + * @return A new {@link FullRevision}. If parsing failed, major is set to + * {@link FullRevision#MISSING_MAJOR_REV}. + */ + public static FullRevision parseFullRevisionElement(Node revisionNode) { + // This needs to support two modes: + // - For repository XSD >= 7, contains sub-elements such as or . + // - Otherwise for repository XSD < 7, contains an integer. + // The element is mandatory, so it's easy to distinguish between both cases. + int major = FullRevision.MISSING_MAJOR_REV, + minor = FullRevision.IMPLICIT_MINOR_REV, + micro = FullRevision.IMPLICIT_MICRO_REV, + preview = FullRevision.NOT_A_PREVIEW; + + if (revisionNode != null) { + if (PackageParserUtils.findChildElement(revisionNode, + SdkRepoConstants.NODE_MAJOR_REV) != null) { + // has a sub-element, so it's a repository XSD >= 7. + major = PackageParserUtils.getXmlInt(revisionNode, + SdkRepoConstants.NODE_MAJOR_REV, FullRevision.MISSING_MAJOR_REV); + minor = PackageParserUtils.getXmlInt(revisionNode, + SdkRepoConstants.NODE_MINOR_REV, FullRevision.IMPLICIT_MINOR_REV); + micro = PackageParserUtils.getXmlInt(revisionNode, + SdkRepoConstants.NODE_MICRO_REV, FullRevision.IMPLICIT_MICRO_REV); + preview = PackageParserUtils.getXmlInt(revisionNode, + SdkRepoConstants.NODE_PREVIEW, FullRevision.NOT_A_PREVIEW); + } else { + try { + String majorStr = revisionNode.getTextContent().trim(); + major = Integer.parseInt(majorStr); + } catch (Exception e) { + } + } + } + + return new FullRevision(major, minor, micro, preview); + } + + /** + * Parses a no-preview revision element such as >. + * This supports both the single-integer format as well as the full revision + * format with major/minor/micro sub-elements. + * + * @param revisionNode The node to parse. + * @return A new {@link NoPreviewRevision}. If parsing failed, major is set to + * {@link FullRevision#MISSING_MAJOR_REV}. + */ + public static NoPreviewRevision parseNoPreviewRevisionElement(Node revisionNode) { + // This needs to support two modes: + // - For addon XSD >= 6, contains sub-elements such as or . + // - Otherwise for addon XSD < 6, contains an integer. + // The element is mandatory, so it's easy to distinguish between both cases. + int major = FullRevision.MISSING_MAJOR_REV, + minor = FullRevision.IMPLICIT_MINOR_REV, + micro = FullRevision.IMPLICIT_MICRO_REV; + + if (revisionNode != null) { + if (PackageParserUtils.findChildElement(revisionNode, + SdkRepoConstants.NODE_MAJOR_REV) != null) { + // has a sub-element, so it's a repository XSD >= 7. + major = PackageParserUtils.getXmlInt(revisionNode, + SdkRepoConstants.NODE_MAJOR_REV, FullRevision.MISSING_MAJOR_REV); + minor = PackageParserUtils.getXmlInt(revisionNode, + SdkRepoConstants.NODE_MINOR_REV, FullRevision.IMPLICIT_MINOR_REV); + micro = PackageParserUtils.getXmlInt(revisionNode, + SdkRepoConstants.NODE_MICRO_REV, FullRevision.IMPLICIT_MICRO_REV); + } else { + try { + String majorStr = revisionNode.getTextContent().trim(); + major = Integer.parseInt(majorStr); + } catch (Exception e) { + } + } + } + + return new NoPreviewRevision(major, minor, micro); + } + + /** + * Returns the first child element with the given XML local name and the same NS URI. + * If xmlLocalName is null, returns the very first child element. + */ + public static Node findChildElement(Node node, String xmlLocalName) { + if (node != null) { + String nsUri = node.getNamespaceURI(); + for(Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) { + if (child.getNodeType() == Node.ELEMENT_NODE) { + String nsUriChild = child.getNamespaceURI(); + if ((nsUri == null && nsUriChild == null) || + (nsUri != null && nsUri.equals(nsUriChild))) { + if (xmlLocalName == null || xmlLocalName.equals(child.getLocalName())) { + return child; + } + } + } + } + } + return null; + } + + /** + * Retrieves the value of that XML element as a string. + * Returns an empty string whether the element is missing or empty, + * so you can't tell the difference. + *

+ * Note: use {@link #getOptionalXmlString(Node, String)} if you need to know when the + * element is missing versus empty. + * + * @param node The XML parent node to parse. + * @param xmlLocalName The XML local name to find in the parent node. + * @return The text content of the element. Returns an empty string whether the element + * is missing or empty, so you can't tell the difference. + */ + public static String getXmlString(Node node, String xmlLocalName) { + return getXmlString(node, xmlLocalName, ""); //$NON-NLS-1$ + } + + /** + * Retrieves the value of that XML element as a string. + * Returns the defaultValue if the element is missing or empty. + *

+ * Note: use {@link #getOptionalXmlString(Node, String)} if you need to know when the + * element is missing versus empty. + * + * @param node The XML parent node to parse. + * @param xmlLocalName The XML local name to find in the parent node. + * @param defaultValue A default value to return if the element is missing. + * @return The text content of the element + * or the defaultValue if the element is missing or empty. + */ + public static String getXmlString(Node node, String xmlLocalName, String defaultValue) { + Node child = findChildElement(node, xmlLocalName); + String content = child == null ? null : child.getTextContent(); + return content == null || content.isEmpty() ? defaultValue : content; + } + + /** + * Retrieves the value of that XML element as a string. + * Returns null when the element is missing, so you can tell between a missing element + * and an empty one. + *

+ * Note: use {@link #getXmlString(Node, String)} if you don't need to know when the + * element is missing versus empty. + * + * @param node The XML parent node to parse. + * @param xmlLocalName The XML local name to find in the parent node. + * @return The text content of the element. Returns null when the element is missing. + * Returns an empty string whether the element is present but empty. + */ + public static String getOptionalXmlString(Node node, String xmlLocalName) { + Node child = findChildElement(node, xmlLocalName); + return child == null ? null : child.getTextContent(); //$NON-NLS-1$ + } + + /** + * Retrieves the value of that XML element as an integer. + * Returns the default value when the element is missing or is not an integer. + */ + public static int getXmlInt(Node node, String xmlLocalName, int defaultValue) { + String s = getXmlString(node, xmlLocalName); + try { + return Integer.parseInt(s); + } catch (NumberFormatException e) { + return defaultValue; + } + } + + /** + * Retrieves the value of that XML element as a long. + * Returns the default value when the element is missing or is not an integer. + */ + public static long getXmlLong(Node node, String xmlLocalName, long defaultValue) { + String s = getXmlString(node, xmlLocalName); + try { + return Long.parseLong(s); + } catch (NumberFormatException e) { + return defaultValue; + } + } + + /** + * Retrieve an attribute which value must match one of the given enums using a + * case-insensitive name match. + * + * Returns defaultValue if the attribute does not exist or its value does not match + * the given enum values. + */ + public static Object getEnumAttribute( + Node archiveNode, + String attrName, + Object[] values, + Object defaultValue) { + + Node attr = archiveNode.getAttributes().getNamedItem(attrName); + if (attr != null) { + String found = attr.getNodeValue(); + for (Object value : values) { + if (value.toString().equalsIgnoreCase(found)) { + return value; + } + } + } + + return defaultValue; + } + + /** + * Utility method that returns a property from a {@link Properties} object. + * Returns the default value if props is null or if the property is not defined. + * + * @param props The {@link Properties} to search into. + * If null, the default value is returned. + * @param propKey The name of the property. Must not be null. + * @param defaultValue The default value to return if {@code props} is null or if the + * key is not found. Can be null. + * @return The string value of the given key in the properties, or null if the key + * isn't found or if {@code props} is null. + */ + @Nullable + public static String getProperty( + @Nullable Properties props, + @NonNull String propKey, + @Nullable String defaultValue) { + if (props == null) { + return defaultValue; + } + return props.getProperty(propKey, defaultValue); + } + + /** + * Utility method that returns an integer property from a {@link Properties} object. + * Returns the default value if props is null or if the property is not defined or + * cannot be parsed to an integer. + * + * @param props The {@link Properties} to search into. + * If null, the default value is returned. + * @param propKey The name of the property. Must not be null. + * @param defaultValue The default value to return if {@code props} is null or if the + * key is not found. Can be null. + * @return The integer value of the given key in the properties, or the {@code defaultValue}. + */ + public static int getPropertyInt( + @Nullable Properties props, + @NonNull String propKey, + int defaultValue) { + String s = props != null ? props.getProperty(propKey, null) : null; + if (s != null) { + try { + return Integer.parseInt(s); + } catch (Exception ignore) {} + } + return defaultValue; + } + + /** + * Utility method to parse the {@link PkgProps#PKG_REVISION} property as a full + * revision (major.minor.micro.preview). + * + * @param props The properties to parse. + * @return A {@link FullRevision} or null if there is no such property or it couldn't be parsed. + * @param propKey The name of the property. Must not be null. + */ + @Nullable + public static FullRevision getPropertyFull( + @Nullable Properties props, + @NonNull String propKey) { + String revStr = getProperty(props, propKey, null); + + FullRevision rev = null; + if (revStr != null) { + try { + rev = FullRevision.parseRevision(revStr); + } catch (NumberFormatException ignore) {} + } + + return rev; + } + + /** + * Utility method to parse the {@link PkgProps#PKG_REVISION} property as a major + * revision (major integer, no minor/micro/preview parts.) + * + * @param props The properties to parse. + * @return A {@link MajorRevision} or null if there is no such property or it couldn't be parsed. + * @param propKey The name of the property. Must not be null. + */ + @Nullable + public static MajorRevision getPropertyMajor( + @Nullable Properties props, + @NonNull String propKey) { + String revStr = getProperty(props, propKey, null); + + MajorRevision rev = null; + if (revStr != null) { + try { + rev = MajorRevision.parseRevision(revStr); + } catch (NumberFormatException ignore) {} + } + + return rev; + } + + /** + * Utility method to parse the {@link PkgProps#PKG_REVISION} property as a no-preview + * revision (major.minor.micro integers but no preview part.) + * + * @param props The properties to parse. + * @return A {@link NoPreviewRevision} or + * null if there is no such property or it couldn't be parsed. + * @param propKey The name of the property. Must not be null. + */ + @Nullable + public static NoPreviewRevision getPropertyNoPreview( + @Nullable Properties props, + @NonNull String propKey) { + String revStr = getProperty(props, propKey, null); + + NoPreviewRevision rev = null; + if (revStr != null) { + try { + rev = NoPreviewRevision.parseRevision(revStr); + } catch (NumberFormatException ignore) {} + } + + return rev; + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/PlatformPackage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/PlatformPackage.java new file mode 100644 index 00000000..ff4e4bb7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/PlatformPackage.java @@ -0,0 +1,388 @@ +/* + * 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.sdklib.internal.repository.packages; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.annotations.VisibleForTesting; +import com.android.annotations.VisibleForTesting.Visibility; +import com.android.sdklib.AndroidTargetHash; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.SdkManager; +import com.android.sdklib.repository.IDescription; +import com.android.sdklib.internal.repository.sources.SdkSource; +import com.android.sdklib.repository.MajorRevision; +import com.android.sdklib.repository.PkgProps; +import com.android.sdklib.repository.SdkRepoConstants; +import com.android.sdklib.repository.descriptors.IPkgDesc; +import com.android.sdklib.repository.descriptors.PkgDesc; +import com.android.utils.Pair; + +import org.w3c.dom.Node; + +import java.io.File; +import java.util.Map; +import java.util.Properties; + +/** + * Represents a platform XML node in an SDK repository. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class PlatformPackage extends MinToolsPackage + implements IAndroidVersionProvider, ILayoutlibVersion { + + /** The package version, for platform, add-on and doc packages. */ + private final AndroidVersion mVersion; + + /** The version, a string, for platform packages. */ + private final String mVersionName; + + /** The ABI of the system-image included in this platform. Can be null but not empty. */ + private final String mIncludedAbi; + + /** The helper handling the layoutlib version. */ + private final LayoutlibVersionMixin mLayoutlibVersion; + + private final IPkgDesc mPkgDesc; + + /** + * Creates a new platform package from the attributes and elements of the given XML node. + * This constructor should throw an exception if the package cannot be created. + * + * @param source The {@link SdkSource} where this is loaded from. + * @param packageNode The XML element being parsed. + * @param nsUri The namespace URI of the originating XML document, to be able to deal with + * parameters that vary according to the originating XML schema. + * @param licenses The licenses loaded from the XML originating document. + */ + public PlatformPackage( + SdkSource source, + Node packageNode, + String nsUri, + Map licenses) { + super(source, packageNode, nsUri, licenses); + + mVersionName = + PackageParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_VERSION); + + int apiLevel = + PackageParserUtils.getXmlInt (packageNode, SdkRepoConstants.NODE_API_LEVEL, 0); + String codeName = + PackageParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_CODENAME); + if (codeName.isEmpty()) { + codeName = null; + } + mVersion = new AndroidVersion(apiLevel, codeName); + + mIncludedAbi = PackageParserUtils.getOptionalXmlString(packageNode, + SdkRepoConstants.NODE_ABI_INCLUDED); + + mLayoutlibVersion = new LayoutlibVersionMixin(packageNode); + + mPkgDesc = setDescriptions(PkgDesc.Builder + .newPlatform(mVersion, (MajorRevision) getRevision(), getMinToolsRevision())) + .create(); + } + + /** + * Creates a new platform package based on an actual {@link IAndroidTarget} (which + * must have {@link IAndroidTarget#isPlatform()} true) from the {@link SdkManager}. + * This is used to list local SDK folders in which case there is one archive which + * URL is the actual target location. + *

+ * By design, this creates a package with one and only one archive. + */ + public static Package create(@NonNull IAndroidTarget target, @Nullable Properties props) { + return new PlatformPackage(target, props); + } + + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected PlatformPackage(@NonNull IAndroidTarget target, @Nullable Properties props) { + this(null /*source*/, target, props); + } + + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected PlatformPackage(@Nullable SdkSource source, + @NonNull IAndroidTarget target, + @Nullable Properties props) { + super( source, //source + props, //properties + target.getRevision(), //revision + null, //license + target.getDescription(), //description + null, //descUrl + target.getLocation() //archiveOsPath + ); + + mVersion = target.getVersion(); + mVersionName = target.getVersionName(); + mLayoutlibVersion = new LayoutlibVersionMixin(props); + mIncludedAbi = props == null ? null : props.getProperty(PkgProps.PLATFORM_INCLUDED_ABI); + + mPkgDesc = setDescriptions(PkgDesc.Builder + .newPlatform(mVersion, (MajorRevision) getRevision(), getMinToolsRevision())) + .create(); + } + + @Override + @NonNull + public IPkgDesc getPkgDesc() { + return mPkgDesc; + } + + /** + * Save the properties of the current packages in the given {@link Properties} object. + * These properties will later be given to a constructor that takes a {@link Properties} object. + */ + @Override + public void saveProperties(Properties props) { + super.saveProperties(props); + + mVersion.saveProperties(props); + mLayoutlibVersion.saveProperties(props); + + if (mVersionName != null) { + props.setProperty(PkgProps.PLATFORM_VERSION, mVersionName); + } + + if (mIncludedAbi != null) { + props.setProperty(PkgProps.PLATFORM_INCLUDED_ABI, mIncludedAbi); + } + + } + + /** Returns the version, a string, for platform packages. */ + public String getVersionName() { + return mVersionName; + } + + /** Returns the package version, for platform, add-on and doc packages. */ + @Override @NonNull + public AndroidVersion getAndroidVersion() { + return mVersion; + } + + /** + * Returns the ABI of the system-image included in this platform. + * + * @return Null if the platform does not include any system-image. + * Otherwise should be a valid non-empty ABI string (e.g. "x86" or "armeabi-v7a"). + */ + public String getIncludedAbi() { + return mIncludedAbi; + } + + /** + * Returns the layoutlib version. Mandatory starting with repository XSD rev 4. + *

+ * The first integer is the API of layoublib, which should be > 0. + * It will be equal to {@link ILayoutlibVersion#LAYOUTLIB_API_NOT_SPECIFIED} (0) + * if the layoutlib version isn't specified. + *

+ * The second integer is the revision for that given API. It is >= 0 + * and works as a minor revision number, incremented for the same API level. + * + * @since sdk-repository-4.xsd + */ + @Override + public Pair getLayoutlibVersion() { + return mLayoutlibVersion.getLayoutlibVersion(); + } + + /** + * Returns a string identifier to install this package from the command line. + * For platforms, we use "android-N" where N is the API or the preview codename. + *

+ * {@inheritDoc} + */ + @Override + public String installId() { + return AndroidTargetHash.getPlatformHashString(mVersion); + } + + /** + * Returns a description of this package that is suitable for a list display. + *

+ * {@inheritDoc} + */ + @Override + public String getListDescription() { + String ld = getListDisplay(); + if (!ld.isEmpty()) { + return String.format("%1$s%2$s", ld, isObsolete() ? " (Obsolete)" : ""); + } + + String s; + if (mVersion.isPreview()) { + s = String.format("SDK Platform Android %1$s Preview%2$s", + getVersionName(), + isObsolete() ? " (Obsolete)" : ""); //$NON-NLS-2$ + } else { + s = String.format("SDK Platform Android %1$s%2$s", + getVersionName(), + isObsolete() ? " (Obsolete)" : ""); //$NON-NLS-2$ + } + + return s; + } + + /** + * Returns a short description for an {@link IDescription}. + */ + @Override + public String getShortDescription() { + String ld = getListDisplay(); + if (!ld.isEmpty()) { + return String.format("%1$s, revision %2$s%3$s", + ld, + getRevision().toShortString(), + isObsolete() ? " (Obsolete)" : ""); + } + + String s; + if (mVersion.isPreview()) { + s = String.format("SDK Platform Android %1$s Preview, revision %2$s%3$s", + getVersionName(), + getRevision().toShortString(), + isObsolete() ? " (Obsolete)" : ""); //$NON-NLS-2$ + } else { + s = String.format("SDK Platform Android %1$s, API %2$d, revision %3$s%4$s", + getVersionName(), + mVersion.getApiLevel(), + getRevision().toShortString(), + isObsolete() ? " (Obsolete)" : ""); //$NON-NLS-2$ + } + + return s; + } + + /** + * Returns a long description for an {@link IDescription}. + * + * The long description is whatever the XML contains for the <description> field, + * or the short description if the former is empty. + */ + @Override + public String getLongDescription() { + String s = getDescription(); + if (s == null || s.isEmpty()) { + s = getShortDescription(); + } + + if (s.indexOf("revision") == -1) { + s += String.format("\nRevision %1$s%2$s", + getRevision().toShortString(), + isObsolete() ? " (Obsolete)" : ""); + } + + return s; + } + + /** + * Computes a potential installation folder if an archive of this package were + * to be installed right away in the given SDK root. + *

+ * A platform package is typically installed in SDK/platforms/android-"version". + * However if we can find a different directory under SDK/platform that already + * has this platform version installed, we'll use that one. + * + * @param osSdkRoot The OS path of the SDK root folder. + * @param sdkManager An existing SDK manager to list current platforms and addons. + * @return A new {@link File} corresponding to the directory to use to install this package. + */ + @Override + public File getInstallFolder(String osSdkRoot, SdkManager sdkManager) { + + // First find if this platform is already installed. If so, reuse the same directory. + for (IAndroidTarget target : sdkManager.getTargets()) { + if (target.isPlatform() && target.getVersion().equals(mVersion)) { + return new File(target.getLocation()); + } + } + + File platforms = new File(osSdkRoot, SdkConstants.FD_PLATFORMS); + File folder = new File(platforms, + String.format("android-%s", getAndroidVersion().getApiString())); //$NON-NLS-1$ + + return folder; + } + + @Override + public boolean sameItemAs(Package pkg) { + if (pkg instanceof PlatformPackage) { + PlatformPackage newPkg = (PlatformPackage)pkg; + + // check they are the same version. + return newPkg.getAndroidVersion().equals(this.getAndroidVersion()); + } + + return false; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + + ((mLayoutlibVersion == null) ? 0 : mLayoutlibVersion.hashCode()); + result = prime * result + ((mVersion == null) ? 0 : mVersion.hashCode()); + result = prime * result + ((mVersionName == null) ? 0 : mVersionName.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (!(obj instanceof PlatformPackage)) { + return false; + } + PlatformPackage other = (PlatformPackage) obj; + if (mLayoutlibVersion == null) { + if (other.mLayoutlibVersion != null) { + return false; + } + } else if (!mLayoutlibVersion.equals(other.mLayoutlibVersion)) { + return false; + } + if (mVersion == null) { + if (other.mVersion != null) { + return false; + } + } else if (!mVersion.equals(other.mVersion)) { + return false; + } + if (mVersionName == null) { + if (other.mVersionName != null) { + return false; + } + } else if (!mVersionName.equals(other.mVersionName)) { + return false; + } + return true; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/PlatformToolPackage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/PlatformToolPackage.java new file mode 100644 index 00000000..85c69532 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/PlatformToolPackage.java @@ -0,0 +1,313 @@ +/* + * Copyright (C) 2010 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.sdklib.internal.repository.packages; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.VisibleForTesting; +import com.android.annotations.VisibleForTesting.Visibility; +import com.android.sdklib.SdkManager; +import com.android.sdklib.internal.repository.AdbWrapper; +import com.android.sdklib.internal.repository.ITaskMonitor; +import com.android.sdklib.internal.repository.archives.Archive; +import com.android.sdklib.internal.repository.sources.SdkSource; +import com.android.sdklib.repository.FullRevision.PreviewComparison; +import com.android.sdklib.repository.IDescription; +import com.android.sdklib.repository.descriptors.IPkgDesc; +import com.android.sdklib.repository.descriptors.PkgDesc; +import org.w3c.dom.Node; + +import java.io.File; +import java.util.HashSet; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +/** + * Represents a platform-tool XML node in an SDK repository. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class PlatformToolPackage extends FullRevisionPackage { + + /** The value returned by {@link PlatformToolPackage#installId()}. */ + public static final String INSTALL_ID = "platform-tools"; //$NON-NLS-1$ + /** The value returned by {@link PlatformToolPackage#installId()}. */ + public static final String INSTALL_ID_PREVIEW = "platform-tools-preview"; //$NON-NLS-1$ + + private final IPkgDesc mPkgDesc; + + /** + * Creates a new platform-tool package from the attributes and elements of the given XML node. + * This constructor should throw an exception if the package cannot be created. + * + * @param source The {@link SdkSource} where this is loaded from. + * @param packageNode The XML element being parsed. + * @param nsUri The namespace URI of the originating XML document, to be able to deal with + * parameters that vary according to the originating XML schema. + * @param licenses The licenses loaded from the XML originating document. + */ + public PlatformToolPackage(SdkSource source, Node packageNode, + String nsUri, Map licenses) { + super(source, packageNode, nsUri, licenses); + + mPkgDesc = setDescriptions(PkgDesc.Builder.newPlatformTool(getRevision())).create(); + } + + /** + * Manually create a new package with one archive and the given attributes or properties. + * This is used to create packages from local directories in which case there must be + * one archive which URL is the actual target location. + *

+ * By design, this creates a package with one and only one archive. + */ + public static Package create( + SdkSource source, + Properties props, + int revision, + String license, + String description, + String descUrl, + String archiveOsPath) { + + PlatformToolPackage ptp = new PlatformToolPackage(source, props, revision, license, + description, descUrl, archiveOsPath); + + File platformToolsFolder = new File(archiveOsPath); + String error = null; + if (!platformToolsFolder.isDirectory()) { + error = "platform-tools folder is missing"; + } else { + File[] files = platformToolsFolder.listFiles(); + if (files == null || files.length == 0) { + error = "platform-tools folder is empty"; + } else { + Set names = new HashSet(); + for (File file : files) { + names.add(file.getName()); + } + + // Package-tools revision 17+ matches sdk-repository-8 and above + // and only requires adb (other tools moved to the build-tool packages.) + String[] expected = new String[] { SdkConstants.FN_ADB }; + if (ptp.getRevision().getMajor() < 17) { + // Platform-tools before revision 17 should have adb, aapt, aidl and dx. + expected = new String[] { SdkConstants.FN_ADB, + SdkConstants.FN_AAPT, + SdkConstants.FN_AIDL, + SdkConstants.FN_DX }; + } + + for (String name : expected) { + if (!names.contains(name)) { + if (error == null) { + error = "platform-tools folder is missing "; + } else { + error += ", "; + } + error += name; + } + } + } + } + + if (error != null) { + String shortDesc = ptp.getShortDescription() + " [*]"; //$NON-NLS-1$ + + String longDesc = String.format( + "Broken Platform-Tools Package: %1$s\n" + + "[*] Package cannot be used due to error: %2$s", + description, + error); + + BrokenPackage ba = new BrokenPackage(props, shortDesc, longDesc, + IMinApiLevelDependency.MIN_API_LEVEL_NOT_SPECIFIED, + IExactApiLevelDependency.API_LEVEL_INVALID, + archiveOsPath, + PkgDesc.Builder.newPlatformTool(ptp.getRevision()) + .setDescriptionShort(shortDesc) + .create()); + return ba; + } + + + return ptp; + } + + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected PlatformToolPackage( + SdkSource source, + Properties props, + int revision, + String license, + String description, + String descUrl, + String archiveOsPath) { + super(source, + props, + revision, + license, + description, + descUrl, + archiveOsPath); + + mPkgDesc = setDescriptions(PkgDesc.Builder.newPlatformTool(getRevision())).create(); + } + + @Override + @NonNull + public IPkgDesc getPkgDesc() { + return mPkgDesc; + } + + /** + * Returns a string identifier to install this package from the command line. + * For platform-tools, we use "platform-tools" or "platform-tools-preview" since + * this package type is unique. + *

+ * {@inheritDoc} + */ + @Override + public String installId() { + if (getRevision().isPreview()) { + return INSTALL_ID_PREVIEW; + } else { + return INSTALL_ID; + } + } + + /** + * Returns a description of this package that is suitable for a list display. + *

+ * {@inheritDoc} + */ + @Override + public String getListDescription() { + String ld = getListDisplay(); + if (!ld.isEmpty()) { + return String.format("%1$s%2$s", ld, isObsolete() ? " (Obsolete)" : ""); + } + + return String.format("Android SDK Platform-tools%1$s", + isObsolete() ? " (Obsolete)" : ""); + } + + /** + * Returns a short description for an {@link IDescription}. + */ + @Override + public String getShortDescription() { + String ld = getListDisplay(); + if (!ld.isEmpty()) { + return String.format("%1$s, revision %2$s%3$s", + ld, + getRevision().toShortString(), + isObsolete() ? " (Obsolete)" : ""); + } + + return String.format("Android SDK Platform-tools, revision %1$s%2$s", + getRevision().toShortString(), + isObsolete() ? " (Obsolete)" : ""); + } + + /** Returns a long description for an {@link IDescription}. */ + @Override + public String getLongDescription() { + String s = getDescription(); + if (s == null || s.isEmpty()) { + s = getShortDescription(); + } + + if (s.indexOf("revision") == -1) { + s += String.format("\nRevision %1$s%2$s", + getRevision().toShortString(), + isObsolete() ? " (Obsolete)" : ""); + } + + return s; + } + + /** + * Computes a potential installation folder if an archive of this package were + * to be installed right away in the given SDK root. + *

+ * A "platform-tool" package should always be located in SDK/platform-tools. + * There can be only one installed at once. + * + * @param osSdkRoot The OS path of the SDK root folder. + * @param sdkManager An existing SDK manager to list current platforms and addons. + * @return A new {@link File} corresponding to the directory to use to install this package. + */ + @Override + public File getInstallFolder(String osSdkRoot, SdkManager sdkManager) { + return new File(osSdkRoot, SdkConstants.FD_PLATFORM_TOOLS); + } + + /** + * Check whether 2 platform-tool packages are the same and have the + * same preview bit. + */ + @Override + public boolean sameItemAs(Package pkg) { + return sameItemAs(pkg, PreviewComparison.COMPARE_TYPE); + } + + @Override + public boolean sameItemAs(Package pkg, PreviewComparison comparePreview) { + // only one platform-tool package so any platform-tool package is the same item. + if (pkg instanceof PlatformToolPackage) { + switch (comparePreview) { + case IGNORE: + return true; + + case COMPARE_NUMBER: + // Voluntary break-through. + case COMPARE_TYPE: + // There's only one platform-tools so the preview number doesn't matter; + // however previews can only match previews by default so both cases + // are treated the same. + return pkg.getRevision().isPreview() == getRevision().isPreview(); + } + } + return false; + } + + /** + * Hook called right before an archive is installed. + * This is used here to stop ADB before trying to replace the platform-tool package. + * + * @param archive The archive that will be installed + * @param monitor The {@link ITaskMonitor} to display errors. + * @param osSdkRoot The OS path of the SDK root folder. + * @param installFolder The folder where the archive will be installed. Note that this + * is not the folder where the archive was temporary + * unzipped. The installFolder, if it exists, contains the old + * archive that will soon be replaced by the new one. + * @return True if installing this archive shall continue, false if it should be skipped. + */ + @Override + public boolean preInstallHook(Archive archive, ITaskMonitor monitor, + String osSdkRoot, File installFolder) { + AdbWrapper aw = new AdbWrapper(osSdkRoot, monitor); + aw.stopAdb(); + return super.preInstallHook(archive, monitor, osSdkRoot, installFolder); + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/SamplePackage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/SamplePackage.java new file mode 100644 index 00000000..d31805ed --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/SamplePackage.java @@ -0,0 +1,571 @@ +/* + * 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.sdklib.internal.repository.packages; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.AndroidVersion.AndroidVersionException; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.SdkManager; +import com.android.sdklib.repository.IDescription; +import com.android.sdklib.internal.repository.ITaskMonitor; +import com.android.sdklib.internal.repository.archives.Archive; +import com.android.sdklib.internal.repository.sources.SdkSource; +import com.android.sdklib.io.IFileOp; +import com.android.sdklib.repository.MajorRevision; +import com.android.sdklib.repository.PkgProps; +import com.android.sdklib.repository.SdkRepoConstants; +import com.android.sdklib.repository.descriptors.IPkgDesc; +import com.android.sdklib.repository.descriptors.PkgDesc; + +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.w3c.dom.Node; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Map; +import java.util.Properties; + +/** + * Represents a sample XML node in an SDK repository. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class SamplePackage extends MinToolsPackage + implements IAndroidVersionProvider, IMinApiLevelDependency { + + /** The matching platform version. */ + private final AndroidVersion mVersion; + + /** + * The minimal API level required by this extra package, if > 0, + * or {@link #MIN_API_LEVEL_NOT_SPECIFIED} if there is no such requirement. + */ + private final int mMinApiLevel; + + private final IPkgDesc mPkgDesc; + + /** + * Creates a new sample package from the attributes and elements of the given XML node. + * This constructor should throw an exception if the package cannot be created. + * + * @param source The {@link SdkSource} where this is loaded from. + * @param packageNode The XML element being parsed. + * @param nsUri The namespace URI of the originating XML document, to be able to deal with + * parameters that vary according to the originating XML schema. + * @param licenses The licenses loaded from the XML originating document. + */ + public SamplePackage(SdkSource source, + Node packageNode, + String nsUri, + Map licenses) { + super(source, packageNode, nsUri, licenses); + + int apiLevel = + PackageParserUtils.getXmlInt (packageNode, SdkRepoConstants.NODE_API_LEVEL, 0); + String codeName = + PackageParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_CODENAME); + if (codeName.isEmpty()) { + codeName = null; + } + mVersion = new AndroidVersion(apiLevel, codeName); + + mMinApiLevel = PackageParserUtils.getXmlInt(packageNode, + SdkRepoConstants.NODE_MIN_API_LEVEL, + MIN_API_LEVEL_NOT_SPECIFIED); + + mPkgDesc = setDescriptions(PkgDesc.Builder + .newSample(mVersion, (MajorRevision) getRevision(), getMinToolsRevision())) + .create(); + } + + /** + * Creates a new sample package based on an actual {@link IAndroidTarget} (which + * must have {@link IAndroidTarget#isPlatform()} true) from the {@link SdkManager}. + *

+ * The target must have an existing sample directory that uses the /samples + * root form rather than the old form where the samples dir was located under the + * platform dir. + *

+ * This is used to list local SDK folders in which case there is one archive which + * URL is the actual samples path location. + *

+ * By design, this creates a package with one and only one archive. + */ + public static Package create(IAndroidTarget target, Properties props) { + return new SamplePackage(target, props); + } + + private SamplePackage(IAndroidTarget target, Properties props) { + super( null, //source + props, //properties + 0, //revision will be taken from props + null, //license + null, //description + null, //descUrl + target.getPath(IAndroidTarget.SAMPLES) //archiveOsPath + ); + + mVersion = target.getVersion(); + + mMinApiLevel = getPropertyInt(props, PkgProps.SAMPLE_MIN_API_LEVEL, + MIN_API_LEVEL_NOT_SPECIFIED); + + mPkgDesc = setDescriptions(PkgDesc.Builder + .newSample(mVersion, (MajorRevision) getRevision(), getMinToolsRevision())) + .create(); + } + + /** + * Creates a new sample package from an actual directory path and previously + * saved properties. + *

+ * This is used to list local SDK folders in which case there is one archive which + * URL is the actual samples path location. + *

+ * By design, this creates a package with one and only one archive. + * + * @throws AndroidVersionException if the {@link AndroidVersion} can't be restored + * from properties. + */ + public static Package create(String archiveOsPath, Properties props) + throws AndroidVersionException { + return new SamplePackage(archiveOsPath, props); + } + + private SamplePackage(String archiveOsPath, Properties props) throws AndroidVersionException { + super(null, //source + props, //properties + 0, //revision will be taken from props + null, //license + null, //description + null, //descUrl + archiveOsPath //archiveOsPath + ); + + mVersion = new AndroidVersion(props); + + mMinApiLevel = getPropertyInt(props, PkgProps.SAMPLE_MIN_API_LEVEL, + MIN_API_LEVEL_NOT_SPECIFIED); + + mPkgDesc = setDescriptions(PkgDesc.Builder + .newSample(mVersion, (MajorRevision) getRevision(), getMinToolsRevision())) + .create(); + } + + @Override + @NonNull + public IPkgDesc getPkgDesc() { + return mPkgDesc; + } + + /** + * Save the properties of the current packages in the given {@link Properties} object. + * These properties will later be given to a constructor that takes a {@link Properties} object. + */ + @Override + public void saveProperties(Properties props) { + super.saveProperties(props); + + mVersion.saveProperties(props); + + if (getMinApiLevel() != MIN_API_LEVEL_NOT_SPECIFIED) { + props.setProperty(PkgProps.SAMPLE_MIN_API_LEVEL, Integer.toString(getMinApiLevel())); + } + } + + /** + * Returns the minimal API level required by this extra package, if > 0, + * or {@link #MIN_API_LEVEL_NOT_SPECIFIED} if there is no such requirement. + */ + @Override + public int getMinApiLevel() { + return mMinApiLevel; + } + + /** Returns the matching platform version. */ + @Override @NonNull + public AndroidVersion getAndroidVersion() { + return mVersion; + } + + /** + * Returns a string identifier to install this package from the command line. + * For samples, we use "sample-N" where N is the API or the preview codename. + *

+ * {@inheritDoc} + */ + @Override + public String installId() { + return "sample-" + mVersion.getApiString(); //$NON-NLS-1$ + } + + /** + * Returns a description of this package that is suitable for a list display. + *

+ * {@inheritDoc} + */ + @Override + public String getListDescription() { + String ld = getListDisplay(); + if (!ld.isEmpty()) { + return String.format("%1$s%2$s", ld, isObsolete() ? " (Obsolete)" : ""); + } + + String s = String.format("Samples for SDK API %1$s%2$s%3$s", + mVersion.getApiString(), + mVersion.isPreview() ? " Preview" : "", + isObsolete() ? " (Obsolete)" : ""); + return s; + } + + /** + * Returns a short description for an {@link IDescription}. + */ + @Override + public String getShortDescription() { + String ld = getListDisplay(); + if (!ld.isEmpty()) { + return String.format("%1$s, revision %2$s%3$s", + ld, + getRevision().toShortString(), + isObsolete() ? " (Obsolete)" : ""); + } + + String s = String.format("Samples for SDK API %1$s%2$s, revision %3$s%4$s", + mVersion.getApiString(), + mVersion.isPreview() ? " Preview" : "", + getRevision().toShortString(), + isObsolete() ? " (Obsolete)" : ""); + return s; + } + + /** + * Returns a long description for an {@link IDescription}. + * + * The long description is whatever the XML contains for the <description> field, + * or the short description if the former is empty. + */ + @Override + public String getLongDescription() { + String s = getDescription(); + if (s == null || s.isEmpty()) { + s = getShortDescription(); + } + + if (s.indexOf("revision") == -1) { + s += String.format("\nRevision %1$s%2$s", + getRevision().toShortString(), + isObsolete() ? " (Obsolete)" : ""); + } + + return s; + } + + /** + * Computes a potential installation folder if an archive of this package were + * to be installed right away in the given SDK root. + *

+ * A sample package is typically installed in SDK/samples/android-"version". + * However if we can find a different directory that already has this sample + * version installed, we'll use that one. + * + * @param osSdkRoot The OS path of the SDK root folder. + * @param sdkManager An existing SDK manager to list current platforms and addons. + * @return A new {@link File} corresponding to the directory to use to install this package. + */ + @Override + public File getInstallFolder(String osSdkRoot, SdkManager sdkManager) { + + // The /samples dir at the root of the SDK + File samplesRoot = new File(osSdkRoot, SdkConstants.FD_SAMPLES); + + // First find if this sample is already installed. If so, reuse the same directory. + for (IAndroidTarget target : sdkManager.getTargets()) { + if (target.isPlatform() && + target.getVersion().equals(mVersion)) { + String p = target.getPath(IAndroidTarget.SAMPLES); + File f = new File(p); + if (f.isDirectory()) { + // We *only* use this directory if it's using the "new" location + // under SDK/samples. We explicitly do not reuse the "old" location + // under SDK/platform/android-N/samples. + if (f.getParentFile().equals(samplesRoot)) { + return f; + } + } + } + } + + // Otherwise, get a suitable default + File folder = new File(samplesRoot, + String.format("android-%s", getAndroidVersion().getApiString())); //$NON-NLS-1$ + + for (int n = 1; folder.exists(); n++) { + // Keep trying till we find an unused directory. + folder = new File(samplesRoot, + String.format("android-%s_%d", getAndroidVersion().getApiString(), n)); //$NON-NLS-1$ + } + + return folder; + } + + @Override + public boolean sameItemAs(Package pkg) { + if (pkg instanceof SamplePackage) { + SamplePackage newPkg = (SamplePackage)pkg; + + // check they are the same version. + return newPkg.getAndroidVersion().equals(this.getAndroidVersion()); + } + + return false; + } + + /** + * Makes sure the base /samples folder exists before installing. + * + * {@inheritDoc} + */ + @Override + public boolean preInstallHook(Archive archive, + ITaskMonitor monitor, + String osSdkRoot, + File installFolder) { + + if (installFolder != null && installFolder.isDirectory()) { + // Get the hash computed during the last installation + String storedHash = readContentHash(installFolder); + if (storedHash != null && !storedHash.isEmpty()) { + + // Get the hash of the folder now + String currentHash = computeContentHash(installFolder); + + if (!storedHash.equals(currentHash)) { + // The hashes differ. The content was modified. + // Ask the user if we should still wipe the old samples. + + String pkgName = archive.getParentPackage().getShortDescription(); + + String msg = String.format( + "-= Warning ! =-\n" + + "You are about to replace the content of the folder:\n " + + " %1$s\n" + + "by the new package:\n" + + " %2$s.\n" + + "\n" + + "However it seems that the content of the existing samples " + + "has been modified since it was last installed. Are you sure " + + "you want to DELETE the existing samples? This cannot be undone.\n" + + "Please select YES to delete the existing sample and replace them " + + "by the new ones.\n" + + "Please select NO to skip this package. You can always install it later.", + installFolder.getAbsolutePath(), + pkgName); + + // Returns true if we can wipe & replace. + return monitor.displayPrompt("SDK Manager: overwrite samples?", msg); + } + } + } + + // The default is to allow installation + return super.preInstallHook(archive, monitor, osSdkRoot, installFolder); + } + + /** + * Computes a hash of the installed content (in case of successful install.) + * + * {@inheritDoc} + */ + @Override + public void postInstallHook(Archive archive, ITaskMonitor monitor, File installFolder) { + super.postInstallHook(archive, monitor, installFolder); + + if (installFolder != null) { + String h = computeContentHash(installFolder); + saveContentHash(installFolder, h); + } + } + + /** + * Set all the files from a sample package as read-only so that + * users don't end up modifying sources by mistake in Eclipse + * (samples are copied if using the NPW > Create from sample.) + */ + @Override + public void postUnzipFileHook( + Archive archive, + ITaskMonitor monitor, + IFileOp fileOp, + File unzippedFile, + ZipArchiveEntry zipEntry) { + super.postUnzipFileHook(archive, monitor, fileOp, unzippedFile, zipEntry); + + if (fileOp.isFile(unzippedFile) && + !SdkConstants.FN_SOURCE_PROP.equals(unzippedFile.getName())) { + fileOp.setReadOnly(unzippedFile); + } + } + + /** + * Reads the hash from the properties file, if it exists. + * Returns null if something goes wrong, e.g. there's no property file or + * it doesn't contain our hash. Returns an empty string if the hash wasn't + * correctly computed last time by {@link #saveContentHash(File, String)}. + */ + private String readContentHash(File folder) { + Properties props = new Properties(); + + FileInputStream fis = null; + try { + File f = new File(folder, SdkConstants.FN_CONTENT_HASH_PROP); + if (f.isFile()) { + fis = new FileInputStream(f); + props.load(fis); + return props.getProperty("content-hash", null); //$NON-NLS-1$ + } + } catch (Exception e) { + // ignore + } finally { + if (fis != null) { + try { + fis.close(); + } catch (IOException e) { + } + } + } + + return null; + } + + /** + * Saves the hash using a properties file + */ + private void saveContentHash(File folder, String hash) { + Properties props = new Properties(); + + props.setProperty("content-hash", hash == null ? "" : hash); //$NON-NLS-1$ //$NON-NLS-2$ + + FileOutputStream fos = null; + try { + File f = new File(folder, SdkConstants.FN_CONTENT_HASH_PROP); + fos = new FileOutputStream(f); + props.store( fos, "## Android - hash of this archive."); //$NON-NLS-1$ + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + } + } + } + } + + /** + * Computes a hash of the files names and sizes installed in the folder + * using the SHA-1 digest. + * Returns null if the digest algorithm is not available. + */ + private String computeContentHash(File installFolder) { + MessageDigest md = null; + try { + // SHA-1 is a standard algorithm. + // http://java.sun.com/j2se/1.4.2/docs/guide/security/CryptoSpec.html#AppB + md = MessageDigest.getInstance("SHA-1"); //$NON-NLS-1$ + } catch (NoSuchAlgorithmException e) { + // We're unlikely to get there unless this JVM is not spec conforming + // in which case there won't be any hash available. + } + + if (md != null) { + hashDirectoryContent(installFolder, md); + return getDigestHexString(md); + } + + return null; + } + + /** + * Computes a hash of the *content* of this directory. The hash only uses + * the files names and the file sizes. + */ + private void hashDirectoryContent(File folder, MessageDigest md) { + if (folder == null || md == null || !folder.isDirectory()) { + return; + } + + for (File f : folder.listFiles()) { + if (f.isDirectory()) { + hashDirectoryContent(f, md); + + } else { + String name = f.getName(); + + // Skip the file we use to store the content hash + if (name == null || SdkConstants.FN_CONTENT_HASH_PROP.equals(name)) { + continue; + } + + try { + md.update(name.getBytes(SdkConstants.UTF_8)); + } catch (UnsupportedEncodingException e) { + // There is no valid reason for UTF-8 to be unsupported. Ignore. + } + try { + long len = f.length(); + md.update((byte) (len & 0x0FF)); + md.update((byte) ((len >> 8) & 0x0FF)); + md.update((byte) ((len >> 16) & 0x0FF)); + md.update((byte) ((len >> 24) & 0x0FF)); + + } catch (SecurityException e) { + // Might happen if file is not readable. Ignore. + } + } + } + } + + /** + * Returns a digest as an hex string. + */ + private String getDigestHexString(MessageDigest digester) { + // Create an hex string from the digest + byte[] digest = digester.digest(); + int n = digest.length; + String hex = "0123456789abcdef"; //$NON-NLS-1$ + char[] hexDigest = new char[n * 2]; + for (int i = 0; i < n; i++) { + int b = digest[i] & 0x0FF; + hexDigest[i*2 + 0] = hex.charAt(b >>> 4); + hexDigest[i*2 + 1] = hex.charAt(b & 0x0f); + } + + return new String(hexDigest); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/SourcePackage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/SourcePackage.java new file mode 100644 index 00000000..6942431e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/SourcePackage.java @@ -0,0 +1,377 @@ +/* + * Copyright (C) 2011 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.sdklib.internal.repository.packages; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.VisibleForTesting; +import com.android.annotations.VisibleForTesting.Visibility; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.AndroidVersion.AndroidVersionException; +import com.android.sdklib.SdkManager; +import com.android.sdklib.repository.IDescription; +import com.android.sdklib.internal.repository.ITaskMonitor; +import com.android.sdklib.internal.repository.archives.Archive; +import com.android.sdklib.internal.repository.sources.SdkSource; +import com.android.sdklib.io.IFileOp; +import com.android.sdklib.repository.MajorRevision; +import com.android.sdklib.repository.SdkRepoConstants; +import com.android.sdklib.repository.descriptors.IPkgDesc; +import com.android.sdklib.repository.descriptors.PkgDesc; + +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.w3c.dom.Node; + +import java.io.File; +import java.util.Map; +import java.util.Properties; + +/** + * Represents a source XML node in an SDK repository. + *

+ * Note that a source package has a version and thus implements {@link IAndroidVersionProvider}. + * However there is no mandatory dependency that limits installation so this does not + * implement {@link IPlatformDependency}. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class SourcePackage extends MajorRevisionPackage implements IAndroidVersionProvider { + + /** The package version, for platform, add-on and doc packages. */ + private final AndroidVersion mVersion; + + private final IPkgDesc mPkgDesc; + + /** + * Creates a new source package from the attributes and elements of the given XML node. + * This constructor should throw an exception if the package cannot be created. + * + * @param source The {@link SdkSource} where this is loaded from. + * @param packageNode The XML element being parsed. + * @param nsUri The namespace URI of the originating XML document, to be able to deal with + * parameters that vary according to the originating XML schema. + * @param licenses The licenses loaded from the XML originating document. + */ + public SourcePackage( + SdkSource source, + Node packageNode, + String nsUri, + Map licenses) { + super(source, packageNode, nsUri, licenses); + + int apiLevel = + PackageParserUtils.getXmlInt(packageNode, SdkRepoConstants.NODE_API_LEVEL, 0); + String codeName = + PackageParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_CODENAME); + if (codeName.isEmpty()) { + codeName = null; + } + mVersion = new AndroidVersion(apiLevel, codeName); + + mPkgDesc = setDescriptions(PkgDesc.Builder.newSource(mVersion, (MajorRevision)getRevision())).create(); + } + + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected SourcePackage( + AndroidVersion platformVersion, + int revision, + Properties props, + String localOsPath) { + this(null /*source*/, platformVersion, revision, props, localOsPath); + } + + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected SourcePackage( + SdkSource source, + AndroidVersion platformVersion, + int revision, + Properties props, + String localOsPath) { + super( source, //source + props, //properties + revision, //revision + null, //license + null, //description + null, //descUrl + localOsPath //archiveOsPath + ); + mVersion = platformVersion; + + mPkgDesc = setDescriptions(PkgDesc.Builder.newSource(mVersion, (MajorRevision) getRevision())).create(); + } + + /** + * Creates either a valid {@link SourcePackage} or a {@link BrokenPackage}. + *

+ * If the source directory contains valid properties, this creates a new {@link SourcePackage} + * with the android version listed in the properties. + * Otherwise returns a new {@link BrokenPackage} with some explanation on what failed. + * + * @param srcDir The SDK/sources/android-N folder + * @param props The properties located in {@code srcDir} or null if not found. + * @return A new {@link SourcePackage} or a new {@link BrokenPackage}. + */ + public static Package create(File srcDir, Properties props) { + AndroidVersion version = null; + String error = null; + + // Try to load the android version from the sources.props. + // If we don't find them, it would explain why this package is broken. + if (props == null) { + error = String.format("Missing file %1$s", SdkConstants.FN_SOURCE_PROP); + } else { + try { + version = new AndroidVersion(props); + // The constructor will extract the revision from the properties + // and it will not consider a missing revision as being fatal. + return new SourcePackage(version, 0 /*revision*/, props, srcDir.getAbsolutePath()); + } catch (AndroidVersionException e) { + error = String.format("Invalid file %1$s: %2$s", + SdkConstants.FN_SOURCE_PROP, + e.getMessage()); + } + } + + if (version == null) { + try { + // Try to parse the first number out of the platform folder name. + // This is just a wild guess in case we can create a broken package using that info. + String platform = srcDir.getParentFile().getName(); + platform = platform.replaceAll("[^0-9]+", " ").trim(); //$NON-NLS-1$ //$NON-NLS-2$ + int pos = platform.indexOf(' '); + if (pos >= 0) { + platform = platform.substring(0, pos); + } + int apiLevel = Integer.parseInt(platform); + version = new AndroidVersion(apiLevel, null /*codename*/); + } catch (Exception ignore) { + } + } + + StringBuilder sb = new StringBuilder("Broken Source Package"); + if (version != null) { + sb.append(String.format(", API %1$s", version.getApiString())); + } + + String shortDesc = sb.toString(); + + if (error != null) { + sb.append('\n').append(error); + } + + String longDesc = sb.toString(); + + IPkgDesc desc = PkgDesc.Builder + .newSource(version != null ? version : new AndroidVersion(0, null), + new MajorRevision(MajorRevision.MISSING_MAJOR_REV)) + .setDescriptionShort(shortDesc) + .create(); + + return new BrokenPackage(props, shortDesc, longDesc, + IMinApiLevelDependency.MIN_API_LEVEL_NOT_SPECIFIED, + version==null ? IExactApiLevelDependency.API_LEVEL_INVALID : version.getApiLevel(), + srcDir.getAbsolutePath(), + desc); + } + + @Override + @NonNull + public IPkgDesc getPkgDesc() { + return mPkgDesc; + } + + /** + * Save the properties of the current packages in the given {@link Properties} object. + * These properties will later be given to a constructor that takes a {@link Properties} object. + */ + @Override + public void saveProperties(Properties props) { + super.saveProperties(props); + mVersion.saveProperties(props); + } + + /** + * Returns the android version of this package. + */ + @Override @NonNull + public AndroidVersion getAndroidVersion() { + return mVersion; + } + + /** + * Returns a string identifier to install this package from the command line. + * For sources, we use "source-N" where N is the API or the preview codename. + *

+ * {@inheritDoc} + */ + @Override + public String installId() { + return "source-" + mVersion.getApiString(); //$NON-NLS-1$ + } + + /** + * Returns a description of this package that is suitable for a list display. + *

+ * {@inheritDoc} + */ + @Override + public String getListDescription() { + String ld = getListDisplay(); + if (!ld.isEmpty()) { + return String.format("%1$s%2$s", ld, isObsolete() ? " (Obsolete)" : ""); + } + + if (mVersion.isPreview()) { + return String.format("Sources for Android '%1$s' Preview SDK%2$s", + mVersion.getCodename(), + isObsolete() ? " (Obsolete)" : ""); + } else { + return String.format("Sources for Android SDK%2$s", + mVersion.getApiLevel(), + isObsolete() ? " (Obsolete)" : ""); + } + } + + /** + * Returns a short description for an {@link IDescription}. + */ + @Override + public String getShortDescription() { + String ld = getListDisplay(); + if (!ld.isEmpty()) { + return String.format("%1$s, revision %2$s%3$s", + ld, + getRevision().toShortString(), + isObsolete() ? " (Obsolete)" : ""); + } + + if (mVersion.isPreview()) { + return String.format("Sources for Android '%1$s' Preview SDK, revision %2$s%3$s", + mVersion.getCodename(), + getRevision().toShortString(), + isObsolete() ? " (Obsolete)" : ""); + } else { + return String.format("Sources for Android SDK, API %1$d, revision %2$s%3$s", + mVersion.getApiLevel(), + getRevision().toShortString(), + isObsolete() ? " (Obsolete)" : ""); + } + } + + /** + * Returns a long description for an {@link IDescription}. + * + * The long description is whatever the XML contains for the {@code description} field, + * or the short description if the former is empty. + */ + @Override + public String getLongDescription() { + String s = getDescription(); + if (s == null || s.isEmpty()) { + s = getShortDescription(); + } + + if (s.indexOf("revision") == -1) { + s += String.format("\nRevision %1$s%2$s", + getRevision().toShortString(), + isObsolete() ? " (Obsolete)" : ""); + } + + return s; + } + + /** + * Computes a potential installation folder if an archive of this package were + * to be installed right away in the given SDK root. + *

+ * A sources package is typically installed in SDK/sources/platform. + * + * @param osSdkRoot The OS path of the SDK root folder. + * @param sdkManager An existing SDK manager to list current platforms and addons. + * @return A new {@link File} corresponding to the directory to use to install this package. + */ + @Override + public File getInstallFolder(String osSdkRoot, SdkManager sdkManager) { + File folder = new File(osSdkRoot, SdkConstants.FD_PKG_SOURCES); + folder = new File(folder, "android-" + mVersion.getApiString()); //$NON-NLS-1$ + return folder; + } + + /** + * Set all the files from a source package as read-only + * so that users don't end up modifying sources by mistake in Eclipse. + */ + @Override + public void postUnzipFileHook( + Archive archive, + ITaskMonitor monitor, + IFileOp fileOp, + File unzippedFile, + ZipArchiveEntry zipEntry) { + super.postUnzipFileHook(archive, monitor, fileOp, unzippedFile, zipEntry); + + if (fileOp.isFile(unzippedFile) && + !SdkConstants.FN_SOURCE_PROP.equals(unzippedFile.getName())) { + fileOp.setReadOnly(unzippedFile); + } + } + + @Override + public boolean sameItemAs(Package pkg) { + if (pkg instanceof SourcePackage) { + SourcePackage newPkg = (SourcePackage)pkg; + + // check they are the same version. + return getAndroidVersion().equals(newPkg.getAndroidVersion()); + } + + return false; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((mVersion == null) ? 0 : mVersion.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (!(obj instanceof SourcePackage)) { + return false; + } + SourcePackage other = (SourcePackage) obj; + if (mVersion == null) { + if (other.mVersion != null) { + return false; + } + } else if (!mVersion.equals(other.mVersion)) { + return false; + } + return true; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/SystemImagePackage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/SystemImagePackage.java new file mode 100644 index 00000000..c5ce04a7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/SystemImagePackage.java @@ -0,0 +1,621 @@ +/* + * Copyright (C) 2011 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.sdklib.internal.repository.packages; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.annotations.VisibleForTesting; +import com.android.annotations.VisibleForTesting.Visibility; +import com.android.sdklib.AndroidTargetHash; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.AndroidVersion.AndroidVersionException; +import com.android.sdklib.SdkManager; +import com.android.sdklib.SystemImage; +import com.android.sdklib.devices.Abi; +import com.android.sdklib.internal.repository.sources.SdkSource; +import com.android.sdklib.repository.*; +import com.android.sdklib.repository.descriptors.IPkgDesc; +import com.android.sdklib.repository.descriptors.IdDisplay; +import com.android.sdklib.repository.descriptors.PkgDesc; +import com.android.sdklib.repository.local.LocalSysImgPkgInfo; +import org.w3c.dom.Node; + +import java.io.File; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.regex.Pattern; + +/** + * Represents a system-image XML node in an SDK repository. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class SystemImagePackage extends MajorRevisionPackage + implements IAndroidVersionProvider, IPlatformDependency { + + /** The package version, for platform, add-on and doc packages. */ + private final AndroidVersion mVersion; + + /** The ABI of the system-image. Must not be null nor empty. */ + private final String mAbi; + + private final IPkgDesc mPkgDesc; + + private final IdDisplay mTag; + private final IdDisplay mAddonVendor; + + /** + * Creates a new system-image package from the attributes and elements of the given XML node. + * This constructor should throw an exception if the package cannot be created. + * + * @param source The {@link SdkSource} where this is loaded from. + * @param packageNode The XML element being parsed. + * @param nsUri The namespace URI of the originating XML document, to be able to deal with + * parameters that vary according to the originating XML schema. + * @param licenses The licenses loaded from the XML originating document. + */ + public SystemImagePackage(SdkSource source, + Node packageNode, + String nsUri, + Map licenses) { + super(source, packageNode, nsUri, licenses); + + int apiLevel = + PackageParserUtils.getXmlInt(packageNode, SdkRepoConstants.NODE_API_LEVEL, 0); + String codeName = + PackageParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_CODENAME); + if (codeName.isEmpty()) { + codeName = null; + } + mVersion = new AndroidVersion(apiLevel, codeName); + + mAbi = PackageParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_ABI); + + // tag id + String tagId = PackageParserUtils.getXmlString(packageNode, + SdkSysImgConstants.ATTR_TAG_ID, + SystemImage.DEFAULT_TAG.getId()); + String tagDisp = PackageParserUtils.getOptionalXmlString(packageNode, + SdkSysImgConstants.ATTR_TAG_DISPLAY); + if (tagDisp == null || tagDisp.isEmpty()) { + tagDisp = LocalSysImgPkgInfo.tagIdToDisplay(tagId); + } + assert tagId != null; + assert tagDisp != null; + mTag = new IdDisplay(tagId, tagDisp); + + + Node addonNode = + PackageParserUtils.findChildElement(packageNode, SdkSysImgConstants.NODE_ADD_ON); + + IPkgDesc desc = null; + IdDisplay vendor = null; + + if (addonNode == null) { + // A platform system-image + desc = setDescriptions(PkgDesc.Builder + .newSysImg(mVersion, mTag, mAbi, (MajorRevision) getRevision())) + .create(); + } else { + // An add-on system-image + String vendorId = PackageParserUtils.getXmlString( + addonNode, + SdkAddonConstants.NODE_VENDOR_ID); + String vendorDisp = PackageParserUtils.getXmlString( + addonNode, + SdkAddonConstants.NODE_VENDOR_DISPLAY, + vendorId); + + assert !vendorId.isEmpty(); + assert !vendorDisp.isEmpty(); + + vendor = new IdDisplay(vendorId, vendorDisp); + + desc = setDescriptions(PkgDesc.Builder + .newAddonSysImg(mVersion, vendor, mTag, mAbi, (MajorRevision) getRevision())) + .create(); + } + + mPkgDesc = desc; + mAddonVendor = vendor; + } + + @VisibleForTesting(visibility=Visibility.PRIVATE) + public SystemImagePackage( + AndroidVersion platformVersion, + int revision, + String abi, + Properties props, + String localOsPath) { + this(null /*source*/, platformVersion, revision, abi, props, localOsPath); + } + + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected SystemImagePackage( + SdkSource source, + AndroidVersion platformVersion, + int revision, + String abi, + Properties props, + String localOsPath) { + super( source, //source + props, //properties + revision, //revision + null, //license + null, //description + null, //descUrl + localOsPath //archiveOsPath + ); + mVersion = platformVersion; + if (abi == null && props != null) { + abi = props.getProperty(PkgProps.SYS_IMG_ABI); + } + assert abi != null : "To use this SystemImagePackage constructor you must pass an ABI as a parameter or as a PROP_ABI property"; + mAbi = abi; + + mTag = LocalSysImgPkgInfo.extractTagFromProps(props); + + String vendorId = getProperty(props, PkgProps.ADDON_VENDOR_ID, null); + String vendorDisp = getProperty(props, PkgProps.ADDON_VENDOR_DISPLAY, vendorId); + + IPkgDesc desc = null; + IdDisplay vendor = null; + + if (vendorId == null) { + // A platform system-image + desc = setDescriptions(PkgDesc.Builder.newSysImg(mVersion, mTag, mAbi, (MajorRevision)getRevision())).create(); + } + else { + // An add-on system-image + assert !vendorId.isEmpty(); + assert !vendorDisp.isEmpty(); + + vendor = new IdDisplay(vendorId, vendorDisp); + + desc = setDescriptions(PkgDesc.Builder.newAddonSysImg(mVersion, vendor, mTag, mAbi, (MajorRevision)getRevision())).create(); + } + + mPkgDesc = desc; + mAddonVendor = vendor; + } + + /** + * Creates a {@link BrokenPackage} representing a system image that failed to load + * with the regular {@link SdkManager} workflow. + * + * @param abiDir The SDK/system-images/android-N/tag/abi folder + * @param props The properties located in {@code abiDir} or null if not found. + * @return A new {@link BrokenPackage} that represents this installed package. + */ + public static Package createBroken(File abiDir, Properties props) { + AndroidVersion version = null; + String abiType = abiDir.getName(); + String error = null; + IdDisplay tag = null; + + // Try to load the android version, tag & ABI from the sources.props. + // If we don't find them, it would explain why this package is broken. + if (props == null) { + error = String.format("Missing file %1$s", SdkConstants.FN_SOURCE_PROP); + } else { + try { + version = new AndroidVersion(props); + + tag = LocalSysImgPkgInfo.extractTagFromProps(props); + String abi = props.getProperty(PkgProps.SYS_IMG_ABI); + if (abi != null) { + abiType = abi; + } else { + error = String.format("Invalid file %1$s: Missing property %2$s", + SdkConstants.FN_SOURCE_PROP, + PkgProps.SYS_IMG_ABI); + } + } catch (AndroidVersionException e) { + error = String.format("Invalid file %1$s: %2$s", + SdkConstants.FN_SOURCE_PROP, + e.getMessage()); + } + } + + try { + // Try to parse the first number out of the platform folder name. + // Also try to parse the tag if not known yet. + // Folder structure should be: + // Tools < 22.6 / API < 20: sdk/system-images/android-N/abi/ + // Tools >=22.6 / API >=20: sdk/system-images/android-N/tag/abi/ + String[] segments = abiDir.getAbsolutePath().split(Pattern.quote(File.separator)); + int len = segments.length; + for (int i = len - 2; version == null && i >= 0; i--) { + if (SdkConstants.FD_SYSTEM_IMAGES.equals(segments[i])) { + if (version == null) { + String platform = segments[i+1]; + try { + platform = platform.replaceAll("[^0-9]+", " ").trim(); //$NON-NLS-1$ //$NON-NLS-2$ + int pos = platform.indexOf(' '); + if (pos >= 0) { + platform = platform.substring(0, pos); + } + int apiLevel = Integer.parseInt(platform); + version = new AndroidVersion(apiLevel, null /*codename*/); + } catch (Exception ignore) {} + } + if (tag == null && i+2 < len) { + // If we failed to find a tag id in the properties, check whether + // we can guess one from the system image folder path. It should + // match the limited tag id character set and not be one of the + // known ABIs. + String abiOrTag = segments[i+2].trim(); + if (abiOrTag.matches("[A-Za-z0-9_-]+")) { + if (Abi.getEnum(abiOrTag) == null) { + tag = new IdDisplay(abiOrTag, + LocalSysImgPkgInfo.tagIdToDisplay(abiOrTag)); + } + } + } + } + } + } catch (Exception ignore) {} + + String vendorId = getProperty(props, PkgProps.ADDON_VENDOR_ID, null); + String vendorDisp = getProperty(props, PkgProps.ADDON_VENDOR_DISPLAY, vendorId); + + StringBuilder sb = new StringBuilder("Broken "); + sb.append(getAbiDisplayNameInternal(abiType)).append(' '); + if (tag != null && !tag.getId().equals(SystemImage.DEFAULT_TAG.getId())) { + sb.append(tag).append(' '); + } + sb.append("System Image"); + if (vendorDisp != null) { + sb.append(", by ").append(vendorDisp); + } + if (version != null) { + sb.append(String.format(", API %1$s", version.getApiString())); + } + + String shortDesc = sb.toString(); + + if (error != null) { + sb.append('\n').append(error); + } + + String longDesc = sb.toString(); + + if (tag == null) { + // No tag? Use the default. + tag = SystemImage.DEFAULT_TAG; + } + assert tag != null; + + IPkgDesc desc = PkgDesc.Builder + .newSysImg(version != null ? version : new AndroidVersion(0, null), + tag, + abiType, + new MajorRevision(MajorRevision.MISSING_MAJOR_REV)) + .setDescriptionShort(shortDesc) + .create(); + + return new BrokenPackage(props, shortDesc, longDesc, + IMinApiLevelDependency.MIN_API_LEVEL_NOT_SPECIFIED, + version==null ? IExactApiLevelDependency.API_LEVEL_INVALID : version.getApiLevel(), + abiDir.getAbsolutePath(), + desc); + } + + @Override + @NonNull + public IPkgDesc getPkgDesc() { + return mPkgDesc; + } + + /** + * Save the properties of the current packages in the given {@link Properties} object. + * These properties will later be given to a constructor that takes a {@link Properties} object. + */ + @Override + public void saveProperties(Properties props) { + super.saveProperties(props); + + mVersion.saveProperties(props); + props.setProperty(PkgProps.SYS_IMG_ABI, mAbi); + props.setProperty(PkgProps.SYS_IMG_TAG_ID, mTag.getId()); + props.setProperty(PkgProps.SYS_IMG_TAG_DISPLAY, mTag.getDisplay()); + + if (mAddonVendor != null) { + props.setProperty(PkgProps.ADDON_VENDOR_ID, mAddonVendor.getId()); + props.setProperty(PkgProps.ADDON_VENDOR_DISPLAY, mAddonVendor.getDisplay()); + } + } + + /** Returns the tag of the system-image. */ + @NonNull + public IdDisplay getTag() { + return mTag; + } + + /** Returns the ABI of the system-image. Cannot be null nor empty. */ + public String getAbi() { + return mAbi; + } + + /** Returns a display-friendly name for the ABI of the system-image. */ + public String getAbiDisplayName() { + return getAbiDisplayNameInternal(mAbi); + } + + private static String getAbiDisplayNameInternal(String abi) { + return abi.replace("armeabi", "ARM EABI") //$NON-NLS-1$ //$NON-NLS-2$ + .replace("arm64", "ARM 64") //$NON-NLS-1$ //$NON-NLS-2$ + .replace("x86", "Intel x86 Atom") //$NON-NLS-1$ //$NON-NLS-2$ + .replace("x86_64", "Intel x86_64 Atom") //$NON-NLS-1$ //$NON-NLS-2$ + .replace("mips", "MIPS") //$NON-NLS-1$ //$NON-NLS-2$ + .replace("-", " "); //$NON-NLS-1$ //$NON-NLS-2$ + } + + /** + * Returns the version of the platform dependency of this package. + *

+ * A system-image has the same {@link AndroidVersion} as the platform it depends on. + */ + @NonNull + @Override + public AndroidVersion getAndroidVersion() { + return mVersion; + } + + /** + * Returns true if the system-image belongs to a standard Android platform. + * In this case {@link #getAddonVendor()} returns null. + *

+ * {@inheritDoc} + */ + @Override + public String installId() { + StringBuilder sb = new StringBuilder("sys-img-"); //$NON-NLS-1$ + sb.append(getAbi()).append('-'); + if (!isPlatform()) { + sb.append("addon-"); + } + sb.append(SystemImage.DEFAULT_TAG.equals(getTag()) ? "android" : getTag().getId()); + sb.append('-'); + if (!isPlatform()) { + sb.append(getAddonVendor().getId()).append('-'); + } + sb.append(getAndroidVersion().getApiString()); + + String s = sb.toString(); + s = s.toLowerCase(Locale.US).replaceAll("[^a-z0-9_.-]+", "_").replaceAll("_+", "_"); + return s; + + } + + /** + * Returns a description of this package that is suitable for a list display. + *

+ * {@inheritDoc} + */ + @Override + public String getListDescription() { + String ld = getListDisplay(); + if (!ld.isEmpty()) { + return String.format("%1$s%2$s", ld, isObsolete() ? " (Obsolete)" : ""); + } + + boolean isDefaultTag = SystemImage.DEFAULT_TAG.equals(mTag); + return String.format("%1$s%2$s System Image%3$s", + isDefaultTag ? "" : (mTag.getDisplay() + " "), + getAbiDisplayName(), + isObsolete() ? " (Obsolete)" : ""); + } + + /** + * Returns a short description for an {@link IDescription}. + */ + @Override + public String getShortDescription() { + String ld = getListDisplay(); + if (!ld.isEmpty()) { + return String.format("%1$s, %2$s API %3$s, revision %4$s%5$s", + ld, + mAddonVendor == null ? "Android" : mAddonVendor.getDisplay(), + mVersion.getApiString(), + getRevision().toShortString(), + isObsolete() ? " (Obsolete)" : ""); + } + + boolean isDefaultTag = SystemImage.DEFAULT_TAG.equals(mTag); + return String.format("%1$s%2$s System Image, %3$s API %4$s, revision %5$s%6$s", + isDefaultTag ? "" : (mTag.getDisplay() + " "), + getAbiDisplayName(), + mAddonVendor == null ? "Android" : mAddonVendor.getDisplay(), + mVersion.getApiString(), + getRevision().toShortString(), + isObsolete() ? " (Obsolete)" : ""); + } + + /** + * Returns a long description for an {@link IDescription}. + * + * The long description is whatever the XML contains for the {@code description} field, + * or the short description if the former is empty. + */ + @Override + public String getLongDescription() { + String s = getDescription(); + if (s == null || s.isEmpty()) { + s = getShortDescription(); + } + + if (s.indexOf("revision") == -1) { + s += String.format("\nRevision %1$s%2$s", + getRevision().toShortString(), + isObsolete() ? " (Obsolete)" : ""); + } + + s += String.format("\nRequires SDK Platform Android API %1$s", + mVersion.getApiString()); + return s; + } + + /** + * Computes a potential installation folder if an archive of this package were + * to be installed right away in the given SDK root. + *

+ * A system-image package is typically installed in SDK/systems/platform/tag/abi. + * The name needs to be sanitized to be acceptable as a directory name. + * + * @param osSdkRoot The OS path of the SDK root folder. + * @param sdkManager An existing SDK manager to list current platforms and addons. + * @return A new {@link File} corresponding to the directory to use to install this package. + */ + @Override + public File getInstallFolder(String osSdkRoot, SdkManager sdkManager) { + File folder = new File(osSdkRoot, SdkConstants.FD_SYSTEM_IMAGES); + folder = new File(folder, AndroidTargetHash.getPlatformHashString(mVersion)); + + // Computes a folder directory using the sanitized tag & abi strings. + String tag = mTag.getId(); + tag = tag.toLowerCase(Locale.US); + tag = tag.replaceAll("[^a-z0-9_-]+", "_"); //$NON-NLS-1$ //$NON-NLS-2$ + tag = tag.replaceAll("_+", "_"); //$NON-NLS-1$ //$NON-NLS-2$ + tag = tag.replaceAll("-+", "-"); //$NON-NLS-1$ //$NON-NLS-2$ + + folder = new File(folder, tag); + + String abi = mAbi; + abi = abi.toLowerCase(Locale.US); + abi = abi.replaceAll("[^a-z0-9_-]+", "_"); //$NON-NLS-1$ //$NON-NLS-2$ + abi = abi.replaceAll("_+", "_"); //$NON-NLS-1$ //$NON-NLS-2$ + abi = abi.replaceAll("-+", "-"); //$NON-NLS-1$ //$NON-NLS-2$ + + folder = new File(folder, abi); + return folder; + } + + @Override + public boolean sameItemAs(Package pkg) { + if (pkg instanceof SystemImagePackage) { + SystemImagePackage newPkg = (SystemImagePackage)pkg; + + // check they are the same tag, abi and version. + return getTag().equals(newPkg.getTag()) && + getAbi().equals(newPkg.getAbi()) && + getAndroidVersion().equals(newPkg.getAndroidVersion()) && + (mAddonVendor == newPkg.mAddonVendor || + (mAddonVendor != null && mAddonVendor.equals(newPkg.mAddonVendor))); + } + + return false; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((mAddonVendor == null) ? 0 : mAddonVendor.hashCode()); + result = prime * result + ((mTag == null) ? 0 : mTag.hashCode()); + result = prime * result + ((mAbi == null) ? 0 : mAbi.hashCode()); + result = prime * result + ((mVersion == null) ? 0 : mVersion.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (!(obj instanceof SystemImagePackage)) { + return false; + } + SystemImagePackage other = (SystemImagePackage) obj; + if (mAddonVendor == null) { + if (other.mAddonVendor != null) { + return false; + } + } else if (!mAddonVendor.equals(other.mAddonVendor)) { + return false; + } + if (mTag == null) { + if (other.mTag != null) { + return false; + } + } else if (!mTag.equals(other.mTag)) { + return false; + } + if (mAbi == null) { + if (other.mAbi != null) { + return false; + } + } else if (!mAbi.equals(other.mAbi)) { + return false; + } + if (mVersion == null) { + if (other.mVersion != null) { + return false; + } + } else if (!mVersion.equals(other.mVersion)) { + return false; + } + return true; + } + + /** + * For sys img packages, we want to add tag/abi to the sorting key + * before the revision number. + *

+ * {@inheritDoc} + */ + @Override + protected String comparisonKey() { + String s = super.comparisonKey(); + int pos = s.indexOf("|r:"); //$NON-NLS-1$ + assert pos > 0; + s = s.substring(0, pos) + + "|vend:" + (mAddonVendor == null ? "" : mAddonVendor.getId()) + //$NON-NLS-1$ //$NON-NLS-2$ + "|tag:" + getTag().getId() + //$NON-NLS-1$ + "|abi:" + getAbiDisplayName() + //$NON-NLS-1$ + s.substring(pos); + return s; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/ToolPackage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/ToolPackage.java new file mode 100644 index 00000000..1b8b3b83 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/ToolPackage.java @@ -0,0 +1,384 @@ +/* + * 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.sdklib.internal.repository.packages; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.annotations.VisibleForTesting; +import com.android.annotations.VisibleForTesting.Visibility; +import com.android.sdklib.SdkManager; +import com.android.sdklib.repository.IDescription; +import com.android.sdklib.internal.repository.ITaskMonitor; +import com.android.sdklib.internal.repository.archives.Archive; +import com.android.sdklib.internal.repository.sources.SdkSource; +import com.android.sdklib.repository.FullRevision; +import com.android.sdklib.repository.FullRevision.PreviewComparison; +import com.android.sdklib.repository.PkgProps; +import com.android.sdklib.repository.SdkRepoConstants; +import com.android.sdklib.repository.descriptors.IPkgDesc; +import com.android.sdklib.repository.descriptors.PkgDesc; +import com.android.utils.GrabProcessOutput; +import com.android.utils.GrabProcessOutput.IProcessOutput; +import com.android.utils.GrabProcessOutput.Wait; + +import org.w3c.dom.Node; + +import java.io.File; +import java.util.Map; +import java.util.Properties; + +/** + * Represents a tool XML node in an SDK repository. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class ToolPackage extends FullRevisionPackage implements IMinPlatformToolsDependency { + + /** The value returned by {@link ToolPackage#installId()}. */ + public static final String INSTALL_ID = "tools"; //$NON-NLS-1$ + /** The value returned by {@link ToolPackage#installId()}. */ + private static final String INSTALL_ID_PREVIEW = "tools-preview"; //$NON-NLS-1$ + + /** + * The minimal revision of the platform-tools package required by this package + * or {@link #MIN_PLATFORM_TOOLS_REV_INVALID} if the value was missing. + */ + private final FullRevision mMinPlatformToolsRevision; + + private final IPkgDesc mPkgDesc; + + /** + * Creates a new tool package from the attributes and elements of the given XML node. + * This constructor should throw an exception if the package cannot be created. + * + * @param source The {@link SdkSource} where this is loaded from. + * @param packageNode The XML element being parsed. + * @param nsUri The namespace URI of the originating XML document, to be able to deal with + * parameters that vary according to the originating XML schema. + * @param licenses The licenses loaded from the XML originating document. + */ + public ToolPackage(SdkSource source, + Node packageNode, + String nsUri, + Map licenses) { + super(source, packageNode, nsUri, licenses); + + mMinPlatformToolsRevision = PackageParserUtils.parseFullRevisionElement( + PackageParserUtils.findChildElement(packageNode, + SdkRepoConstants.NODE_MIN_PLATFORM_TOOLS_REV)); + + if (mMinPlatformToolsRevision.equals(MIN_PLATFORM_TOOLS_REV_INVALID)) { + // This revision number is mandatory starting with sdk-repository-3.xsd + // and did not exist before. Complain if the URI has level >= 3. + if (SdkRepoConstants.versionGreaterOrEqualThan(nsUri, 3)) { + throw new IllegalArgumentException( + String.format("Missing %1$s element in %2$s package", + SdkRepoConstants.NODE_MIN_PLATFORM_TOOLS_REV, + SdkRepoConstants.NODE_PLATFORM_TOOL)); + } + } + + mPkgDesc = setDescriptions(PkgDesc.Builder + .newTool(getRevision(), mMinPlatformToolsRevision)) + .create(); + } + + /** + * Manually create a new package with one archive and the given attributes or properties. + * This is used to create packages from local directories in which case there must be + * one archive which URL is the actual target location. + *

+ * By design, this creates a package with one and only one archive. + */ + public static Package create( + SdkSource source, + Properties props, + int revision, + String license, + String description, + String descUrl, + String archiveOsPath) { + return new ToolPackage(source, props, revision, license, description, + descUrl, archiveOsPath); + } + + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected ToolPackage( + SdkSource source, + Properties props, + int revision, + String license, + String description, + String descUrl, + String archiveOsPath) { + super(source, + props, + revision, + license, + description, + descUrl, + archiveOsPath); + + // Setup min-platform-tool + String revStr = getProperty(props, PkgProps.MIN_PLATFORM_TOOLS_REV, null); + + FullRevision rev = MIN_PLATFORM_TOOLS_REV_INVALID; + if (revStr != null) { + try { + rev = FullRevision.parseRevision(revStr); + } catch (NumberFormatException ignore) {} + } + + mMinPlatformToolsRevision = rev; + + mPkgDesc = setDescriptions(PkgDesc.Builder + .newTool(getRevision(), mMinPlatformToolsRevision)) + .create(); + } + + @Override + @NonNull + public IPkgDesc getPkgDesc() { + return mPkgDesc; + } + + @Override + public FullRevision getMinPlatformToolsRevision() { + return mMinPlatformToolsRevision; + } + + /** + * Returns a string identifier to install this package from the command line. + * For tools, we use "tools" or "tools-preview" since this package is unique. + *

+ * {@inheritDoc} + */ + @Override + public String installId() { + if (getRevision().isPreview()) { + return INSTALL_ID_PREVIEW; + } else { + return INSTALL_ID; + } + } + + /** + * Returns a description of this package that is suitable for a list display. + *

+ * {@inheritDoc} + */ + @Override + public String getListDescription() { + String ld = getListDisplay(); + return String.format("%1$s%2$s", + ld.isEmpty() ? "Android SDK Tools" : ld, + isObsolete() ? " (Obsolete)" : ""); + } + + /** + * Returns a short description for an {@link IDescription}. + */ + @Override + public String getShortDescription() { + String ld = getListDisplay(); + if (!ld.isEmpty()) { + return String.format("%1$s, revision %2$s%3$s", + ld, + getRevision().toShortString(), + isObsolete() ? " (Obsolete)" : ""); + } + + return String.format("Android SDK Tools, revision %1$s%2$s", + getRevision().toShortString(), + isObsolete() ? " (Obsolete)" : ""); + } + + /** Returns a long description for an {@link IDescription}. */ + @Override + public String getLongDescription() { + String s = getDescription(); + if (s == null || s.isEmpty()) { + s = getShortDescription(); + } + + if (s.indexOf("revision") == -1) { + s += String.format("\nRevision %1$s%2$s", + getRevision().toShortString(), + isObsolete() ? " (Obsolete)" : ""); + } + + return s; + } + + /** + * Computes a potential installation folder if an archive of this package were + * to be installed right away in the given SDK root. + *

+ * A "tool" package should always be located in SDK/tools. + * + * @param osSdkRoot The OS path of the SDK root folder. + * @param sdkManager An existing SDK manager to list current platforms and addons. + * @return A new {@link File} corresponding to the directory to use to install this package. + */ + @Override + public File getInstallFolder(String osSdkRoot, SdkManager sdkManager) { + return new File(osSdkRoot, SdkConstants.FD_TOOLS); + } + + /** + * Check whether 2 tool packages are the same and have the + * same preview bit. + */ + @Override + public boolean sameItemAs(Package pkg) { + return sameItemAs(pkg, PreviewComparison.COMPARE_TYPE); + } + + @Override + public boolean sameItemAs(Package pkg, PreviewComparison comparePreview) { + // only one tool package so any platform-tool package is the same item. + if (pkg instanceof ToolPackage) { + switch (comparePreview) { + case IGNORE: + return true; + + case COMPARE_NUMBER: + // Voluntary break-through. + case COMPARE_TYPE: + // There's only one tool so the preview number doesn't matter; + // however previews can only match previews by default so both cases + // are treated the same. + return pkg.getRevision().isPreview() == getRevision().isPreview(); + } + } + return false; + } + + @Override + public void saveProperties(Properties props) { + super.saveProperties(props); + + if (!getMinPlatformToolsRevision().equals(MIN_PLATFORM_TOOLS_REV_INVALID)) { + props.setProperty(PkgProps.MIN_PLATFORM_TOOLS_REV, + getMinPlatformToolsRevision().toShortString()); + } + } + + /** + * The tool package executes tools/lib/post_tools_install[.bat|.sh] + * {@inheritDoc} + */ + @Override + public void postInstallHook(Archive archive, final ITaskMonitor monitor, File installFolder) { + super.postInstallHook(archive, monitor, installFolder); + + if (installFolder == null) { + return; + } + + File libDir = new File(installFolder, SdkConstants.FD_LIB); + if (!libDir.isDirectory()) { + return; + } + + String scriptName = "post_tools_install"; //$NON-NLS-1$ + String shell = ""; //$NON-NLS-1$ + if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS) { + shell = "cmd.exe /c "; //$NON-NLS-1$ + scriptName += ".bat"; //$NON-NLS-1$ + } else { + scriptName += ".sh"; //$NON-NLS-1$ + } + + File scriptFile = new File(libDir, scriptName); + if (!scriptFile.isFile()) { + return; + } + + int status = -1; + + try { + Process proc = Runtime.getRuntime().exec( + shell + scriptName, // command + null, // environment + libDir); // working dir + + final String tag = scriptName; + status = GrabProcessOutput.grabProcessOutput( + proc, + Wait.WAIT_FOR_PROCESS, + new IProcessOutput() { + @Override + public void out(@Nullable String line) { + if (line != null) { + monitor.log("[%1$s] %2$s", tag, line); + } + } + + @Override + public void err(@Nullable String line) { + if (line != null) { + monitor.logError("[%1$s] Error: %2$s", tag, line); + } + } + }); + + } catch (Exception e) { + monitor.logError("Exception: %s", e.toString()); + } + + if (status != 0) { + monitor.logError("Failed to execute %s", scriptName); + return; + } + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + + ((mMinPlatformToolsRevision == null) ? 0 : mMinPlatformToolsRevision.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + if (!(obj instanceof ToolPackage)) { + return false; + } + ToolPackage other = (ToolPackage) obj; + if (mMinPlatformToolsRevision == null) { + if (other.mMinPlatformToolsRevision != null) { + return false; + } + } else if (!mMinPlatformToolsRevision.equals(other.mMinPlatformToolsRevision)) { + return false; + } + return true; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkAddonSource.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkAddonSource.java new file mode 100644 index 00000000..a77b128e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkAddonSource.java @@ -0,0 +1,113 @@ +/* + * 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.sdklib.internal.repository.sources; + +import com.android.annotations.Nullable; +import com.android.sdklib.internal.repository.packages.Package; +import com.android.sdklib.repository.SdkAddonConstants; + +import org.w3c.dom.Document; + +import java.io.InputStream; + + +/** + * An sdk-addon source, i.e. a download site for addons and extra packages. + * A repository describes one or more {@link Package}s available for download. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class SdkAddonSource extends SdkSource { + + /** + * Constructs a new source for the given repository URL. + * @param url The source URL. Cannot be null. If the URL ends with a /, the default + * addon.xml filename will be appended automatically. + * @param uiName The UI-visible name of the source. Can be null. + */ + public SdkAddonSource(String url, String uiName) { + super(url, uiName); + } + + /** + * Returns true if this is an addon source. + * We only load addons and extras from these sources. + */ + @Override + public boolean isAddonSource() { + return true; + } + + /** + * Returns true if this is a system-image source. + * We only load system-images from these sources. + */ + @Override + public boolean isSysImgSource() { + return false; + } + + @Override + protected String[] getDefaultXmlFileUrls() { + return new String[] { SdkAddonConstants.URL_DEFAULT_FILENAME }; + } + + @Override + protected int getNsLatestVersion() { + return SdkAddonConstants.NS_LATEST_VERSION; + } + + @Override + protected String getNsUri() { + return SdkAddonConstants.NS_URI; + } + + @Override + protected String getNsPattern() { + return SdkAddonConstants.NS_PATTERN; + } + + @Override + protected String getSchemaUri(int version) { + return SdkAddonConstants.getSchemaUri(version); + } + + @Override + protected String getRootElementName() { + return SdkAddonConstants.NODE_SDK_ADDON; + } + + @Override + protected InputStream getXsdStream(int version) { + return SdkAddonConstants.getXsdStream(version); + } + + /** + * This kind of schema does not support forward-evolution of the <tool> element. + * + * @param xml The input XML stream. Can be null. + * @return Always null. + * @null This implementation always return null. + */ + @Override + protected Document findAlternateToolsXml(@Nullable InputStream xml) { + return null; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkRepoSource.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkRepoSource.java new file mode 100644 index 00000000..3d2efb50 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkRepoSource.java @@ -0,0 +1,528 @@ +/* + * 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.sdklib.internal.repository.sources; + +import com.android.annotations.Nullable; +import com.android.sdklib.internal.repository.archives.ArchFilter; +import com.android.sdklib.internal.repository.packages.Package; +import com.android.sdklib.internal.repository.packages.PackageParserUtils; +import com.android.sdklib.repository.RepoConstants; +import com.android.sdklib.repository.SdkRepoConstants; + +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.Text; +import org.xml.sax.ErrorHandler; + +import java.io.IOException; +import java.io.InputStream; +import java.util.regex.Pattern; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + + +/** + * An sdk-repository source, i.e. a download site. + * A repository describes one or more {@link Package}s available for download. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class SdkRepoSource extends SdkSource { + + /** + * Constructs a new source for the given repository URL. + * @param url The source URL. Cannot be null. If the URL ends with a /, the default + * repository.xml filename will be appended automatically. + * @param uiName The UI-visible name of the source. Can be null. + */ + public SdkRepoSource(String url, String uiName) { + super(url, uiName); + } + + /** + * Returns true if this is an addon source. + * We only load addons and extras from these sources. + */ + @Override + public boolean isAddonSource() { + return false; + } + + /** + * Returns true if this is a system-image source. + * We only load system-images from these sources. + */ + @Override + public boolean isSysImgSource() { + return false; + } + + private static String[] sDefaults = null; // lazily allocated in getDefaultXmlFileUrls + + @Override + protected String[] getDefaultXmlFileUrls() { + if (sDefaults == null) { + String[] values = new String[SdkRepoConstants.NS_LATEST_VERSION + - SdkRepoConstants.NS_SERVER_MIN_VERSION + + 2]; + int k = 0; + for (int i = SdkRepoConstants.NS_LATEST_VERSION; + i >= SdkRepoConstants.NS_SERVER_MIN_VERSION; + i--) { + values[k++] = String.format(SdkRepoConstants.URL_FILENAME_PATTERN, i); + } + values[k++] = SdkRepoConstants.URL_DEFAULT_FILENAME; + assert k == values.length; + sDefaults = values; + } + + return sDefaults; + } + + @Override + protected int getNsLatestVersion() { + return SdkRepoConstants.NS_LATEST_VERSION; + } + + @Override + protected String getNsUri() { + return SdkRepoConstants.NS_URI; + } + + @Override + protected String getNsPattern() { + return SdkRepoConstants.NS_PATTERN; + } + + @Override + protected String getSchemaUri(int version) { + return SdkRepoConstants.getSchemaUri(version); + } + + @Override + protected String getRootElementName() { + return SdkRepoConstants.NODE_SDK_REPOSITORY; + } + + @Override + protected InputStream getXsdStream(int version) { + return SdkRepoConstants.getXsdStream(version); + } + + /** + * The purpose of this method is to support forward evolution of our schema. + *

+ * At this point, we know that xml does not point to any schema that this version of + * the tool knows how to process, so it's not one of the possible 1..N versions of our + * XSD schema. + *

+ * We thus try to interpret the byte stream as a possible XML stream. It may not be + * one at all in the first place. If it looks anything line an XML schema, we try to + * find its <tool> and the <platform-tools> elements. If we find any, + * we recreate a suitable document that conforms to what we expect from our XSD schema + * with only those elements. + *

+ * To be valid, the <tool> and the <platform-tools> elements must have at + * least one <archive> compatible with this platform. + *

+ * Starting the sdk-repository schema v3, <tools> has a <min-platform-tools-rev> + * node, so technically the corresponding XML schema will be usable only if there's a + * <platform-tools> with the request revision number. We don't enforce that here, as + * this is done at install time. + *

+ * If we don't find anything suitable, we drop the whole thing. + * + * @param xml The input XML stream. Can be null. + * @return Either a new XML document conforming to our schema with at least one <tool> + * and <platform-tools> element or null. + * @throws IOException if InputStream.reset() fails + * @null Can return null on failure. + */ + @Override + protected Document findAlternateToolsXml(@Nullable InputStream xml) throws IOException { + return findAlternateToolsXml(xml, null /*errorHandler*/); + } + + /** + * An alternate version of {@link #findAlternateToolsXml(InputStream)} that allows + * the caller to specify the XML error handler. The default from the underlying Java + * XML Xerces parser will dump to stdout/stderr, which is not convenient during unit tests. + * + * @param xml The input XML stream. Can be null. + * @param errorHandler An optional XML error handler. If null, the default will be used. + * @return Either a new XML document conforming to our schema with at least one <tool> + * and <platform-tools> element or null. + * @throws IOException if InputStream.reset() fails + * @null Can return null on failure. + * @see #findAlternateToolsXml(InputStream) findAlternateToolsXml() provides more details. + */ + protected Document findAlternateToolsXml( + @Nullable InputStream xml, + @Nullable ErrorHandler errorHandler) + throws IOException { + if (xml == null) { + return null; + } + + // Reset the stream if it supports that operation. + assert xml.markSupported(); + xml.reset(); + + // Get an XML document + + Document oldDoc = null; + Document newDoc = null; + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setIgnoringComments(false); + factory.setValidating(false); + + // Parse the old document using a non namespace aware builder + factory.setNamespaceAware(false); + DocumentBuilder builder = factory.newDocumentBuilder(); + + if (errorHandler != null) { + builder.setErrorHandler(errorHandler); + } + + oldDoc = builder.parse(xml); + + // Prepare a new document using a namespace aware builder + factory.setNamespaceAware(true); + builder = factory.newDocumentBuilder(); + newDoc = builder.newDocument(); + + } catch (Exception e) { + // Failed to get builder factor + // Failed to create XML document builder + // Failed to parse XML document + // Failed to read XML document + } + + if (oldDoc == null || newDoc == null) { + return null; + } + + + // Check the root element is an XML with at least the following properties: + // + // + // Note that we don't have namespace support enabled, we just do it manually. + + Pattern nsPattern = Pattern.compile(getNsPattern()); + + Node oldRoot = null; + String prefix = null; + for (Node child = oldDoc.getFirstChild(); child != null; child = child.getNextSibling()) { + if (child.getNodeType() == Node.ELEMENT_NODE) { + prefix = null; + String name = child.getNodeName(); + int pos = name.indexOf(':'); + if (pos > 0 && pos < name.length() - 1) { + prefix = name.substring(0, pos); + name = name.substring(pos + 1); + } + if (SdkRepoConstants.NODE_SDK_REPOSITORY.equals(name)) { + NamedNodeMap attrs = child.getAttributes(); + String xmlns = "xmlns"; //$NON-NLS-1$ + if (prefix != null) { + xmlns += ":" + prefix; //$NON-NLS-1$ + } + Node attr = attrs.getNamedItem(xmlns); + if (attr != null) { + String uri = attr.getNodeValue(); + if (uri != null && nsPattern.matcher(uri).matches()) { + oldRoot = child; + break; + } + } + } + } + } + + // we must have found the root node, and it must have an XML namespace prefix. + if (oldRoot == null || prefix == null || prefix.isEmpty()) { + return null; + } + + final String ns = getNsUri(); + Element newRoot = newDoc.createElementNS(ns, getRootElementName()); + newRoot.setPrefix(prefix); + newDoc.appendChild(newRoot); + int numTool = 0; + + // Find any inner or nodes and extract their required parameters + + String[] elementNames = { + SdkRepoConstants.NODE_TOOL, + SdkRepoConstants.NODE_PLATFORM_TOOL, + SdkRepoConstants.NODE_LICENSE + }; + + Element element = null; + while ((element = findChild(oldRoot, element, prefix, elementNames)) != null) { + boolean isElementValid = false; + + String name = element.getLocalName(); + if (name == null) { + name = element.getNodeName(); + + int pos = name.indexOf(':'); + if (pos > 0 && pos < name.length() - 1) { + name = name.substring(pos + 1); + } + } + + // To be valid, the tool or platform-tool element must have: + // - a element with a number + // - a element with a number for a element + // - an element with one or more elements inside + // - one of the elements must have an "os" and "arch" attributes + // compatible with the current platform. Only keep the first such element found. + // - the element must contain a , a and a . + // - none of the above for a license element + + if (SdkRepoConstants.NODE_LICENSE.equals(name)) { + isElementValid = true; + + } else { + try { + Node revision = findChild(element, null, prefix, RepoConstants.NODE_REVISION); + Node archives = findChild(element, null, prefix, RepoConstants.NODE_ARCHIVES); + + if (revision == null || archives == null) { + continue; + } + + // check revision contains a number + try { + String content = revision.getTextContent(); + content = content.trim(); + int rev = Integer.parseInt(content); + if (rev < 1) { + continue; + } + } catch (NumberFormatException ignore) { + continue; + } + + if (SdkRepoConstants.NODE_TOOL.equals(name)) { + Node minPTRev = findChild(element, null, prefix, + RepoConstants.NODE_MIN_PLATFORM_TOOLS_REV); + + if (minPTRev == null) { + continue; + } + + // check min-platform-tools-rev contains a number + try { + String content = minPTRev.getTextContent(); + content = content.trim(); + int rev = Integer.parseInt(content); + if (rev < 1) { + continue; + } + } catch (NumberFormatException ignore) { + continue; + } + } + + Node archive = null; + while ((archive = findChild(archives, + archive, + prefix, + RepoConstants.NODE_ARCHIVE)) != null) { + try { + ArchFilter af = PackageParserUtils.parseArchFilter(archive); + if (af == null || !af.isCompatibleWith(ArchFilter.getCurrent())) { + continue; + } + + Node node = findChild(archive, null, prefix, RepoConstants.NODE_URL); + String url = node == null ? null : node.getTextContent().trim(); + if (url == null || url.isEmpty()) { + continue; + } + + node = findChild(archive, null, prefix, RepoConstants.NODE_SIZE); + long size = 0; + try { + size = Long.parseLong(node.getTextContent()); + } catch (Exception e) { + // pass + } + if (size < 1) { + continue; + } + + node = findChild(archive, null, prefix, RepoConstants.NODE_CHECKSUM); + // double check that the checksum element contains a type=sha1 attribute + if (node == null) { + continue; + } + NamedNodeMap attrs = node.getAttributes(); + Node typeNode = attrs.getNamedItem(RepoConstants.ATTR_TYPE); + if (typeNode == null || + !RepoConstants.ATTR_TYPE.equals(typeNode.getNodeName()) || + !RepoConstants.SHA1_TYPE.equals(typeNode.getNodeValue())) { + continue; + } + String sha1 = node == null ? null : node.getTextContent().trim(); + if (sha1 == null || + sha1.length() != RepoConstants.SHA1_CHECKSUM_LEN) { + continue; + } + + isElementValid = true; + + } catch (Exception ignore1) { + // For debugging it is useful to re-throw the exception. + // For end-users, not so much. It would be nice to make it + // happen automatically during unit tests. + if (System.getenv("TESTING") != null || + System.getProperty("THROW_DEEP_EXCEPTION_DURING_TESTING") != null) { + throw new RuntimeException(ignore1); + } + } + } // while + } catch (Exception ignore2) { + // For debugging it is useful to re-throw the exception. + // For end-users, not so much. It would be nice to make it + // happen automatically during unit tests. + if (System.getenv("TESTING") != null || + System.getProperty("THROW_DEEP_EXCEPTION_DURING_TESTING") != null) { + throw new RuntimeException(ignore2); + } + } + } + + if (isElementValid) { + duplicateNode(newRoot, element, SdkRepoConstants.NS_URI, prefix); + numTool++; + } + } // while + + return numTool > 0 ? newDoc : null; + } + + /** + * Helper method used by {@link #findAlternateToolsXml(InputStream)} to find a given + * element child in a root XML node. + */ + private Element findChild(Node rootNode, Node after, String prefix, String[] nodeNames) { + for (int i = 0; i < nodeNames.length; i++) { + if (nodeNames[i].indexOf(':') < 0) { + nodeNames[i] = prefix + ":" + nodeNames[i]; + } + } + Node child = after == null ? rootNode.getFirstChild() : after.getNextSibling(); + for(; child != null; child = child.getNextSibling()) { + if (child.getNodeType() != Node.ELEMENT_NODE) { + continue; + } + for (String nodeName : nodeNames) { + if (nodeName.equals(child.getNodeName())) { + return (Element) child; + } + } + } + return null; + } + + /** + * Helper method used by {@link #findAlternateToolsXml(InputStream)} to find a given + * element child in a root XML node. + */ + private Node findChild(Node rootNode, Node after, String prefix, String nodeName) { + return findChild(rootNode, after, prefix, new String[] { nodeName }); + } + + /** + * Helper method used by {@link #findAlternateToolsXml(InputStream)} to duplicate a node + * and attach it to the given root in the new document. + */ + private Element duplicateNode(Element newRootNode, Element oldNode, + String namespaceUri, String prefix) { + // The implementation here is more or less equivalent to + // + // newRoot.appendChild(newDoc.importNode(oldNode, deep=true)) + // + // except we can't just use importNode() since we need to deal with the fact + // that the old document is not namespace-aware yet the new one is. + + Document newDoc = newRootNode.getOwnerDocument(); + Element newNode = null; + + String nodeName = oldNode.getNodeName(); + int pos = nodeName.indexOf(':'); + if (pos > 0 && pos < nodeName.length() - 1) { + nodeName = nodeName.substring(pos + 1); + newNode = newDoc.createElementNS(namespaceUri, nodeName); + newNode.setPrefix(prefix); + } else { + newNode = newDoc.createElement(nodeName); + } + + newRootNode.appendChild(newNode); + + // Merge in all the attributes + NamedNodeMap attrs = oldNode.getAttributes(); + for (int i = 0; i < attrs.getLength(); i++) { + Attr attr = (Attr) attrs.item(i); + Attr newAttr = null; + + String attrName = attr.getNodeName(); + pos = attrName.indexOf(':'); + if (pos > 0 && pos < attrName.length() - 1) { + attrName = attrName.substring(pos + 1); + newAttr = newDoc.createAttributeNS(namespaceUri, attrName); + newAttr.setPrefix(prefix); + } else { + newAttr = newDoc.createAttribute(attrName); + } + + newAttr.setNodeValue(attr.getNodeValue()); + + if (pos > 0) { + newNode.getAttributes().setNamedItemNS(newAttr); + } else { + newNode.getAttributes().setNamedItem(newAttr); + } + } + + // Merge all child elements and texts + for (Node child = oldNode.getFirstChild(); child != null; child = child.getNextSibling()) { + if (child.getNodeType() == Node.ELEMENT_NODE) { + duplicateNode(newNode, (Element) child, namespaceUri, prefix); + + } else if (child.getNodeType() == Node.TEXT_NODE) { + Text newText = newDoc.createTextNode(child.getNodeValue()); + newNode.appendChild(newText); + } + } + + return newNode; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSource.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSource.java new file mode 100644 index 00000000..54ebd340 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSource.java @@ -0,0 +1,999 @@ +/* + * 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.sdklib.internal.repository.sources; + +import com.android.annotations.Nullable; +import com.android.annotations.VisibleForTesting; +import com.android.annotations.VisibleForTesting.Visibility; +import com.android.io.NonClosingInputStream; +import com.android.io.NonClosingInputStream.CloseBehavior; +import com.android.sdklib.internal.repository.CanceledByUserException; +import com.android.sdklib.internal.repository.DownloadCache; +import com.android.sdklib.repository.IDescription; +import com.android.sdklib.internal.repository.ITaskMonitor; +import com.android.sdklib.internal.repository.packages.AddonPackage; +import com.android.sdklib.internal.repository.packages.BuildToolPackage; +import com.android.sdklib.internal.repository.packages.DocPackage; +import com.android.sdklib.internal.repository.packages.ExtraPackage; +import com.android.sdklib.internal.repository.packages.Package; +import com.android.sdklib.internal.repository.packages.PlatformPackage; +import com.android.sdklib.internal.repository.packages.PlatformToolPackage; +import com.android.sdklib.internal.repository.packages.SamplePackage; +import com.android.sdklib.internal.repository.packages.SourcePackage; +import com.android.sdklib.internal.repository.packages.SystemImagePackage; +import com.android.sdklib.internal.repository.packages.ToolPackage; +import com.android.sdklib.repository.RepoConstants; +import com.android.sdklib.repository.SdkAddonConstants; +import com.android.sdklib.repository.SdkRepoConstants; + +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.xml.sax.ErrorHandler; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.net.ssl.SSLKeyException; +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import javax.xml.validation.Validator; + +/** + * An sdk-addon or sdk-repository source, i.e. a download site. + * It may be a full repository or an add-on only repository. + * A repository describes one or {@link Package}s available for download. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public abstract class SdkSource implements IDescription, Comparable { + + private String mUrl; + + private Package[] mPackages; + private String mDescription; + private String mFetchError; + private final String mUiName; + + private static final SdkSourceProperties sSourcesProps = new SdkSourceProperties(); + + /** + * Constructs a new source for the given repository URL. + * @param url The source URL. Cannot be null. If the URL ends with a /, the default + * repository.xml filename will be appended automatically. + * @param uiName The UI-visible name of the source. Can be null. + */ + public SdkSource(String url, String uiName) { + + // URLs should not be null and should not have whitespace. + if (url == null) { + url = ""; + } + url = url.trim(); + + // if the URL ends with a /, it must be "directory" resource, + // in which case we automatically add the default file that will + // looked for. This way it will be obvious to the user which + // resource we are actually trying to fetch. + if (url.endsWith("/")) { //$NON-NLS-1$ + String[] names = getDefaultXmlFileUrls(); + if (names.length > 0) { + url += names[0]; + } + } + + if (uiName == null) { + uiName = sSourcesProps.getProperty(SdkSourceProperties.KEY_NAME, url, null); + } else { + sSourcesProps.setProperty(SdkSourceProperties.KEY_NAME, url, uiName); + } + + mUrl = url; + mUiName = uiName; + setDefaultDescription(); + } + + /** + * Returns true if this is an addon source. + * We only load addons and extras from these sources. + */ + public abstract boolean isAddonSource(); + + /** + * Returns true if this is a system-image source. + * We only load system-images from these sources. + */ + public abstract boolean isSysImgSource(); + + + /** + * Returns the basename of the default URLs to try to download the + * XML manifest. + * E.g. this is typically SdkRepoConstants.URL_DEFAULT_XML_FILE + * or SdkAddonConstants.URL_DEFAULT_XML_FILE + */ + protected abstract String[] getDefaultXmlFileUrls(); + + /** Returns SdkRepoConstants.NS_LATEST_VERSION or SdkAddonConstants.NS_LATEST_VERSION. */ + protected abstract int getNsLatestVersion(); + + /** Returns SdkRepoConstants.NS_URI or SdkAddonConstants.NS_URI. */ + protected abstract String getNsUri(); + + /** Returns SdkRepoConstants.NS_PATTERN or SdkAddonConstants.NS_PATTERN. */ + protected abstract String getNsPattern(); + + /** Returns SdkRepoConstants.getSchemaUri() or SdkAddonConstants.getSchemaUri(). */ + protected abstract String getSchemaUri(int version); + + /* Returns SdkRepoConstants.NODE_SDK_REPOSITORY or SdkAddonConstants.NODE_SDK_ADDON. */ + protected abstract String getRootElementName(); + + /** Returns SdkRepoConstants.getXsdStream() or SdkAddonConstants.getXsdStream(). */ + protected abstract InputStream getXsdStream(int version); + + /** + * In case we fail to load an XML, examine the XML to see if it matches a future + * schema that as at least a tools node that we could load to update the + * SDK Manager. + * + * @param xml The input XML stream. Can be null. + * @return Null on failure, otherwise returns an XML DOM with just the tools we + * need to update this SDK Manager. + * @null Can return null on failure. + */ + protected abstract Document findAlternateToolsXml(@Nullable InputStream xml) + throws IOException; + + /** + * Two repo source are equal if they have the same URL. + */ + @Override + public boolean equals(Object obj) { + if (obj instanceof SdkSource) { + SdkSource rs = (SdkSource) obj; + return rs.getUrl().equals(this.getUrl()); + } + return false; + } + + @Override + public int hashCode() { + return mUrl.hashCode(); + } + + /** + * Implementation of the {@link Comparable} interface. + * Simply compares the URL using the string's default ordering. + */ + @Override + public int compareTo(SdkSource rhs) { + return this.getUrl().compareTo(rhs.getUrl()); + } + + /** + * Returns the UI-visible name of the source. Can be null. + */ + public String getUiName() { + return mUiName; + } + + /** Returns the URL of the XML file for this source. */ + public String getUrl() { + return mUrl; + } + + /** + * Returns the list of known packages found by the last call to load(). + * This is null when the source hasn't been loaded yet -- caller should + * then call {@link #load} to load the packages. + */ + public Package[] getPackages() { + return mPackages; + } + + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected void setPackages(Package[] packages) { + mPackages = packages; + + if (mPackages != null) { + // Order the packages. + Arrays.sort(mPackages, null); + } + } + + /** + * Clear the internal packages list. After this call, {@link #getPackages()} will return + * null till load() is called. + */ + public void clearPackages() { + setPackages(null); + } + + /** + * Indicates if the source is enabled. + *

+ * A 3rd-party add-on source can be disabled by the user to prevent from loading it. + * + * @return True if the source is enabled (default is true). + */ + public boolean isEnabled() { + // A URL is enabled if it's not in the disabled list. + return sSourcesProps.getProperty(SdkSourceProperties.KEY_DISABLED, mUrl, null) == null; + } + + /** + * Changes whether the source is marked as enabled. + *

+ * When changing the enable state, the current package list is purged + * and the next {@code load} will either return an empty list (if disabled) or + * the actual package list (if enabled.) + * + * @param enabled True for the source to be enabled (can be loaded), false otherwise. + */ + public void setEnabled(boolean enabled) { + if (enabled != isEnabled()) { + // First we clear the current package list, which will force the + // next load() to actually set the package list as desired. + clearPackages(); + + sSourcesProps.setProperty(SdkSourceProperties.KEY_DISABLED, mUrl, + enabled ? null /*remove*/ : "disabled"); //$NON-NLS-1$ + } + } + + /** + * Returns the short description of the source, if not null. + * Otherwise returns the default Object toString result. + *

+ * This is mostly helpful for debugging. + * For UI display, use the {@link IDescription} interface. + */ + @Override + public String toString() { + String s = getShortDescription(); + if (s != null) { + return s; + } + return super.toString(); + } + + @Override + public String getShortDescription() { + + if (mUiName != null && !mUiName.isEmpty()) { + + String host = "malformed URL"; + + try { + URL u = new URL(mUrl); + host = u.getHost(); + } catch (MalformedURLException e) { + } + + return String.format("%1$s (%2$s)", mUiName, host); + + } + return mUrl; + } + + @Override + public String getLongDescription() { + // Note: in a normal workflow, mDescription is filled by setDefaultDescription(). + // However for packages made by unit tests or such, this can be null. + return mDescription == null ? "" : mDescription; //$NON-NLS-1$ + } + + /** + * Returns the last fetch error description. + * If there was no error, returns null. + */ + public String getFetchError() { + return mFetchError; + } + + /** + * Tries to fetch the repository index for the given URL and updates the package list. + * When a source is disabled, this create an empty non-null package list. + *

+ * Callers can get the package list using {@link #getPackages()} after this. It will be + * null in case of error, in which case {@link #getFetchError()} can be used to an + * error message. + */ + public void load(DownloadCache cache, ITaskMonitor monitor, boolean forceHttp) { + + setDefaultDescription(); + monitor.setProgressMax(7); + + if (!isEnabled()) { + setPackages(new Package[0]); + mDescription += "\nSource is disabled."; + monitor.incProgress(7); + return; + } + + String url = mUrl; + if (forceHttp) { + url = url.replaceAll("https://", "http://"); //$NON-NLS-1$ //$NON-NLS-2$ + } + + monitor.setDescription("Fetching URL: %1$s", url); + monitor.incProgress(1); + + mFetchError = null; + Boolean[] validatorFound = new Boolean[] { Boolean.FALSE }; + String[] validationError = new String[] { null }; + Exception[] exception = new Exception[] { null }; + Document validatedDoc = null; + boolean usingAlternateXml = false; + boolean usingAlternateUrl = false; + String validatedUri = null; + + String[] defaultNames = getDefaultXmlFileUrls(); + String firstDefaultName = defaultNames.length > 0 ? defaultNames[0] : ""; + + InputStream xml = fetchXmlUrl(url, cache, monitor.createSubMonitor(1), exception); + if (xml != null) { + int version = getXmlSchemaVersion(xml); + if (version == 0) { + closeStream(xml); + xml = null; + } + } + + // FIXME: this is a quick fix to support an alternate upgrade path. + // The whole logic below needs to be updated. + if (xml == null && defaultNames.length > 0) { + ITaskMonitor subMonitor = monitor.createSubMonitor(1); + subMonitor.setProgressMax(defaultNames.length); + + String baseUrl = url; + if (!baseUrl.endsWith("/")) { + int pos = baseUrl.lastIndexOf('/'); + if (pos > 0) { + baseUrl = baseUrl.substring(0, pos + 1); + } + } + + for (String name : defaultNames) { + String newUrl = baseUrl + name; + if (newUrl.equals(url)) { + continue; + } + xml = fetchXmlUrl(newUrl, cache, subMonitor.createSubMonitor(1), exception); + if (xml != null) { + int version = getXmlSchemaVersion(xml); + if (version == 0) { + closeStream(xml); + xml = null; + } else { + url = newUrl; + subMonitor.incProgress( + subMonitor.getProgressMax() - subMonitor.getProgress()); + break; + } + } + } + } else { + monitor.incProgress(1); + } + + // If the original URL can't be fetched + // and the URL doesn't explicitly end with our filename + // and it wasn't an HTTP authentication operation canceled by the user + // then make another tentative after changing the URL. + if (xml == null + && !url.endsWith(firstDefaultName) + && !(exception[0] instanceof CanceledByUserException)) { + if (!url.endsWith("/")) { //$NON-NLS-1$ + url += "/"; //$NON-NLS-1$ + } + url += firstDefaultName; + + xml = fetchXmlUrl(url, cache, monitor.createSubMonitor(1), exception); + usingAlternateUrl = true; + } else { + monitor.incProgress(1); + } + + // FIXME this needs to revisited. + if (xml != null) { + monitor.setDescription("Validate XML: %1$s", url); + + ITaskMonitor subMonitor = monitor.createSubMonitor(2); + subMonitor.setProgressMax(2); + for (int tryOtherUrl = 0; tryOtherUrl < 2; tryOtherUrl++) { + // Explore the XML to find the potential XML schema version + int version = getXmlSchemaVersion(xml); + + if (version >= 1 && version <= getNsLatestVersion()) { + // This should be a version we can handle. Try to validate it + // and report any error as invalid XML syntax, + + String uri = validateXml(xml, url, version, validationError, validatorFound); + if (uri != null) { + // Validation was successful + validatedDoc = getDocument(xml, monitor); + validatedUri = uri; + + if (usingAlternateUrl && validatedDoc != null) { + // If the second tentative succeeded, indicate it in the console + // with the URL that worked. + monitor.log("Repository found at %1$s", url); + + // Keep the modified URL + mUrl = url; + } + } else if (validatorFound[0].equals(Boolean.FALSE)) { + // Validation failed because this JVM lacks a proper XML Validator + mFetchError = validationError[0]; + } else { + // We got a validator but validation failed. We know there's + // what looks like a suitable root element with a suitable XMLNS + // so it must be a genuine error of an XML not conforming to the schema. + } + } else if (version > getNsLatestVersion()) { + // The schema used is more recent than what is supported by this tool. + // Tell the user to upgrade, pointing him to the right version of the tool + // package. + + try { + validatedDoc = findAlternateToolsXml(xml); + } catch (IOException e) { + // Failed, will be handled below. + } + if (validatedDoc != null) { + validationError[0] = null; // remove error from XML validation + validatedUri = getNsUri(); + usingAlternateXml = true; + } + + } else if (version < 1 && tryOtherUrl == 0 && !usingAlternateUrl) { + // This is obviously not one of our documents. + mFetchError = String.format( + "Failed to validate the XML for the repository at URL '%1$s'", + url); + + // If we haven't already tried the alternate URL, let's do it now. + // We don't capture any fetch exception that happen during the second + // fetch in order to avoid hiding any previous fetch errors. + if (!url.endsWith(firstDefaultName)) { + if (!url.endsWith("/")) { //$NON-NLS-1$ + url += "/"; //$NON-NLS-1$ + } + url += firstDefaultName; + + closeStream(xml); + xml = fetchXmlUrl(url, cache, subMonitor.createSubMonitor(1), + null /* outException */); + subMonitor.incProgress(1); + // Loop to try the alternative document + if (xml != null) { + usingAlternateUrl = true; + continue; + } + } + } else if (version < 1 && usingAlternateUrl && mFetchError == null) { + // The alternate URL is obviously not a valid XML either. + // We only report the error if we failed to produce one earlier. + mFetchError = String.format( + "Failed to validate the XML for the repository at URL '%1$s'", + url); + } + + // If we get here either we succeeded or we ran out of alternatives. + break; + } + } + + // If any exception was handled during the URL fetch, display it now. + if (exception[0] != null) { + mFetchError = "Failed to fetch URL"; + + String reason = null; + if (exception[0] instanceof FileNotFoundException) { + // FNF has no useful getMessage, so we need to special handle it. + reason = "File not found"; + mFetchError += ": " + reason; + } else if (exception[0] instanceof SSLKeyException) { + // That's a common error and we have a pref for it. + reason = "HTTPS SSL error. You might want to force download through HTTP in the settings."; + mFetchError += ": HTTPS SSL error"; + } else if (exception[0].getMessage() != null) { + reason = + exception[0].getClass().getSimpleName().replace("Exception", "") //$NON-NLS-1$ //$NON-NLS-2$ + + ' ' + + exception[0].getMessage(); + } else { + reason = exception[0].toString(); + } + + monitor.logError("Failed to fetch URL %1$s, reason: %2$s", url, reason); + } + + if (validationError[0] != null) { + monitor.logError("%s", validationError[0]); //$NON-NLS-1$ + } + + // Stop here if we failed to validate the XML. We don't want to load it. + if (validatedDoc == null) { + return; + } + + if (usingAlternateXml) { + // We found something using the "alternate" XML schema (that is the one made up + // to support schema upgrades). That means the user can only install the tools + // and needs to upgrade them before it download more stuff. + + // Is the manager running from inside ADT? + // We check that com.android.ide.eclipse.adt.AdtPlugin exists using reflection. + + boolean isADT = false; + try { + Class adt = Class.forName("com.android.ide.eclipse.adt.AdtPlugin"); //$NON-NLS-1$ + isADT = (adt != null); + } catch (ClassNotFoundException e) { + // pass + } + + String info; + if (isADT) { + info = "This repository requires a more recent version of ADT. Please update the Eclipse Android plugin."; + mDescription = "This repository requires a more recent version of ADT, the Eclipse Android plugin.\nYou must update it before you can see other new packages."; + + } else { + info = "This repository requires a more recent version of the Tools. Please update."; + mDescription = "This repository requires a more recent version of the Tools.\nYou must update it before you can see other new packages."; + } + + mFetchError = mFetchError == null ? info : mFetchError + ". " + info; + } + + monitor.incProgress(1); + + if (xml != null) { + monitor.setDescription("Parse XML: %1$s", url); + monitor.incProgress(1); + parsePackages(validatedDoc, validatedUri, monitor); + if (mPackages == null || mPackages.length == 0) { + mDescription += "\nNo packages found."; + } else if (mPackages.length == 1) { + mDescription += "\nOne package found."; + } else { + mDescription += String.format("\n%1$d packages found.", mPackages.length); + } + } + + // done + monitor.incProgress(1); + closeStream(xml); + } + + private void setDefaultDescription() { + if (isAddonSource()) { + String desc = ""; + + if (mUiName != null) { + desc += "Add-on Provider: " + mUiName; + desc += "\n"; + } + desc += "Add-on URL: " + mUrl; + + mDescription = desc; + } else { + mDescription = String.format("SDK Source: %1$s", mUrl); + } + } + + /** + * Fetches the document at the given URL and returns it as a string. Returns + * null if anything wrong happens and write errors to the monitor. + * + * @param urlString The URL to load, as a string. + * @param monitor {@link ITaskMonitor} related to this URL. + * @param outException If non null, where to store any exception that + * happens during the fetch. + */ + private InputStream fetchXmlUrl(String urlString, + DownloadCache cache, + ITaskMonitor monitor, + Exception[] outException) { + try { + InputStream xml = cache.openCachedUrl(urlString, monitor); + if (xml != null) { + xml.mark(500000); + xml = new NonClosingInputStream(xml); + ((NonClosingInputStream) xml).setCloseBehavior(CloseBehavior.RESET); + } + return xml; + } catch (Exception e) { + if (outException != null) { + outException[0] = e; + } + } + + return null; + } + + /** + * Closes the stream, ignore any exception from InputStream.close(). + * If the stream is a NonClosingInputStream, sets it to CloseBehavior.CLOSE first. + */ + private void closeStream(InputStream is) { + if (is != null) { + if (is instanceof NonClosingInputStream) { + ((NonClosingInputStream) is).setCloseBehavior(CloseBehavior.CLOSE); + } + try { + is.close(); + } catch (IOException ignore) {} + } + } + + /** + * Validates this XML against one of the requested SDK Repository schemas. + * If the XML was correctly validated, returns the schema that worked. + * If it doesn't validate, returns null and stores the error in outError[0]. + * If we can't find a validator, returns null and set validatorFound[0] to false. + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected String validateXml(InputStream xml, String url, int version, + String[] outError, Boolean[] validatorFound) { + + if (xml == null) { + return null; + } + + try { + Validator validator = getValidator(version); + + if (validator == null) { + validatorFound[0] = Boolean.FALSE; + outError[0] = String.format( + "XML verification failed for %1$s.\nNo suitable XML Schema Validator could be found in your Java environment. Please consider updating your version of Java.", + url); + return null; + } + + validatorFound[0] = Boolean.TRUE; + + // Reset the stream if it supports that operation. + assert xml.markSupported(); + xml.reset(); + + // Validation throws a bunch of possible Exceptions on failure. + validator.validate(new StreamSource(xml)); + return getSchemaUri(version); + + } catch (SAXParseException e) { + outError[0] = String.format( + "XML verification failed for %1$s.\nLine %2$d:%3$d, Error: %4$s", + url, + e.getLineNumber(), + e.getColumnNumber(), + e.toString()); + + } catch (Exception e) { + outError[0] = String.format( + "XML verification failed for %1$s.\nError: %2$s", + url, + e.toString()); + } + return null; + } + + /** + * Manually parses the root element of the XML to extract the schema version + * at the end of the xmlns:sdk="http://schemas.android.com/sdk/android/repository/$N" + * declaration. + * + * @return 1..{@link SdkRepoConstants#NS_LATEST_VERSION} for a valid schema version + * or 0 if no schema could be found. + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected int getXmlSchemaVersion(InputStream xml) { + if (xml == null) { + return 0; + } + + // Get an XML document + Document doc = null; + try { + assert xml.markSupported(); + xml.reset(); + + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setIgnoringComments(false); + factory.setValidating(false); + + // Parse the old document using a non namespace aware builder + factory.setNamespaceAware(false); + DocumentBuilder builder = factory.newDocumentBuilder(); + + // We don't want the default handler which prints errors to stderr. + builder.setErrorHandler(new ErrorHandler() { + @Override + public void warning(SAXParseException e) throws SAXException { + // pass + } + @Override + public void fatalError(SAXParseException e) throws SAXException { + throw e; + } + @Override + public void error(SAXParseException e) throws SAXException { + throw e; + } + }); + + doc = builder.parse(xml); + + // Prepare a new document using a namespace aware builder + factory.setNamespaceAware(true); + builder = factory.newDocumentBuilder(); + + } catch (Exception e) { + // Failed to reset XML stream + // Failed to get builder factor + // Failed to create XML document builder + // Failed to parse XML document + // Failed to read XML document + } + + if (doc == null) { + return 0; + } + + // Check the root element is an XML with at least the following properties: + // + // + // Note that we don't have namespace support enabled, we just do it manually. + + Pattern nsPattern = Pattern.compile(getNsPattern()); + + String prefix = null; + for (Node child = doc.getFirstChild(); child != null; child = child.getNextSibling()) { + if (child.getNodeType() == Node.ELEMENT_NODE) { + prefix = null; + String name = child.getNodeName(); + int pos = name.indexOf(':'); + if (pos > 0 && pos < name.length() - 1) { + prefix = name.substring(0, pos); + name = name.substring(pos + 1); + } + if (getRootElementName().equals(name)) { + NamedNodeMap attrs = child.getAttributes(); + String xmlns = "xmlns"; //$NON-NLS-1$ + if (prefix != null) { + xmlns += ":" + prefix; //$NON-NLS-1$ + } + Node attr = attrs.getNamedItem(xmlns); + if (attr != null) { + String uri = attr.getNodeValue(); + if (uri != null) { + Matcher m = nsPattern.matcher(uri); + if (m.matches()) { + String version = m.group(1); + try { + return Integer.parseInt(version); + } catch (NumberFormatException e) { + return 0; + } + } + } + } + } + } + } + + return 0; + } + + /** + * Helper method that returns a validator for our XSD, or null if the current Java + * implementation can't process XSD schemas. + * + * @param version The version of the XML Schema. + * See {@link SdkRepoConstants#getXsdStream(int)} + */ + private Validator getValidator(int version) throws SAXException { + InputStream xsdStream = getXsdStream(version); + SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + + if (factory == null) { + return null; + } + + // This may throw a SAX Exception if the schema itself is not a valid XSD + Schema schema = factory.newSchema(new StreamSource(xsdStream)); + + Validator validator = schema == null ? null : schema.newValidator(); + + // We don't want the default handler, which by default dumps errors to stderr. + validator.setErrorHandler(new ErrorHandler() { + @Override + public void warning(SAXParseException e) throws SAXException { + // pass + } + @Override + public void fatalError(SAXParseException e) throws SAXException { + throw e; + } + @Override + public void error(SAXParseException e) throws SAXException { + throw e; + } + }); + + return validator; + } + + /** + * Parse all packages defined in the SDK Repository XML and creates + * a new mPackages array with them. + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected boolean parsePackages(Document doc, String nsUri, ITaskMonitor monitor) { + + Node root = getFirstChild(doc, nsUri, getRootElementName()); + if (root != null) { + + ArrayList packages = new ArrayList(); + + // Parse license definitions + HashMap licenses = new HashMap(); + for (Node child = root.getFirstChild(); + child != null; + child = child.getNextSibling()) { + if (child.getNodeType() == Node.ELEMENT_NODE && + nsUri.equals(child.getNamespaceURI()) && + child.getLocalName().equals(RepoConstants.NODE_LICENSE)) { + Node id = child.getAttributes().getNamedItem(RepoConstants.ATTR_ID); + if (id != null) { + licenses.put(id.getNodeValue(), child.getTextContent()); + } + } + } + + // Parse packages + for (Node child = root.getFirstChild(); + child != null; + child = child.getNextSibling()) { + if (child.getNodeType() == Node.ELEMENT_NODE && + nsUri.equals(child.getNamespaceURI())) { + String name = child.getLocalName(); + Package p = null; + + try { + // We can load add-on and extra packages from all sources, either + // internal or user sources. + if (SdkAddonConstants.NODE_ADD_ON.equals(name)) { + p = new AddonPackage(this, child, nsUri, licenses); + + } else if (SdkAddonConstants.NODE_EXTRA.equals(name)) { + p = new ExtraPackage(this, child, nsUri, licenses); + + } else if (!isAddonSource()) { + // We only load platform, doc and tool packages from internal + // sources, never from user sources. + if (SdkRepoConstants.NODE_PLATFORM.equals(name)) { + p = new PlatformPackage(this, child, nsUri, licenses); + } else if (SdkRepoConstants.NODE_DOC.equals(name)) { + p = new DocPackage(this, child, nsUri, licenses); + } else if (SdkRepoConstants.NODE_TOOL.equals(name)) { + p = new ToolPackage(this, child, nsUri, licenses); + } else if (SdkRepoConstants.NODE_PLATFORM_TOOL.equals(name)) { + p = new PlatformToolPackage(this, child, nsUri, licenses); + } else if (SdkRepoConstants.NODE_BUILD_TOOL.equals(name)) { + p = new BuildToolPackage(this, child, nsUri, licenses); + } else if (SdkRepoConstants.NODE_SAMPLE.equals(name)) { + p = new SamplePackage(this, child, nsUri, licenses); + } else if (SdkRepoConstants.NODE_SYSTEM_IMAGE.equals(name)) { + p = new SystemImagePackage(this, child, nsUri, licenses); + } else if (SdkRepoConstants.NODE_SOURCE.equals(name)) { + p = new SourcePackage(this, child, nsUri, licenses); + } + } + + if (p != null) { + packages.add(p); + monitor.logVerbose("Found %1$s", p.getShortDescription()); + } + } catch (Exception e) { + // Ignore invalid packages + monitor.logError("Ignoring invalid %1$s element: %2$s", name, e.toString()); + } + } + } + + setPackages(packages.toArray(new Package[packages.size()])); + + return true; + } + + return false; + } + + /** + * Returns the first child element with the given XML local name. + * If xmlLocalName is null, returns the very first child element. + */ + private Node getFirstChild(Node node, String nsUri, String xmlLocalName) { + + for(Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) { + if (child.getNodeType() == Node.ELEMENT_NODE && + nsUri.equals(child.getNamespaceURI())) { + if (xmlLocalName == null || child.getLocalName().equals(xmlLocalName)) { + return child; + } + } + } + + return null; + } + + /** + * Takes an XML document as a string as parameter and returns a DOM for it. + * + * On error, returns null and prints a (hopefully) useful message on the monitor. + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected Document getDocument(InputStream xml, ITaskMonitor monitor) { + try { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setIgnoringComments(true); + factory.setNamespaceAware(true); + + DocumentBuilder builder = factory.newDocumentBuilder(); + assert xml.markSupported(); + xml.reset(); + Document doc = builder.parse(new InputSource(xml)); + + return doc; + } catch (ParserConfigurationException e) { + monitor.logError("Failed to create XML document builder"); + + } catch (SAXException e) { + monitor.logError("Failed to parse XML document"); + + } catch (IOException e) { + monitor.logError("Failed to read XML document"); + } + + return null; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSourceCategory.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSourceCategory.java new file mode 100644 index 00000000..263e9d5f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSourceCategory.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2010 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.sdklib.internal.repository.sources; + +import com.android.sdklib.repository.IDescription; + + +/** + * The category of a given {@link SdkSource} (which represents a download site). + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public enum SdkSourceCategory implements IDescription { + + /** + * The default canonical and official Android repository. + */ + ANDROID_REPO("Android Repository", true), + + /** + * Repositories contributed by the SDK_UPDATER_URLS env var, + * only used for local debugging. + */ + GETENV_REPOS("Custom Repositories", false), + + /** + * All third-party add-ons fetched from the Android repository. + */ + ADDONS_3RD_PARTY("Third party Add-ons", true), + + /** + * All add-ons contributed locally by the user via the "Add Add-on Site" button. + */ + USER_ADDONS("User Add-ons", false), + + /** + * Add-ons contributed by the SDK_UPDATER_USER_URLS env var, + * only used for local debugging. + */ + GETENV_ADDONS("Custom Add-ons", false); + + + private final String mUiName; + private final boolean mAlwaysDisplay; + + SdkSourceCategory(String uiName, boolean alwaysDisplay) { + mUiName = uiName; + mAlwaysDisplay = alwaysDisplay; + } + + /** + * Returns the UI-visible name of the category. Displayed in the available package tree. + * Cannot be null nor empty. + */ + public String getUiName() { + return mUiName; + } + + /** + * True if this category must always be displayed by the available package tree, even + * if empty. + * When false, the category must not be displayed when empty. + */ + public boolean getAlwaysDisplay() { + return mAlwaysDisplay; + } + + @Override + public String getLongDescription() { + return getUiName(); + } + + @Override + public String getShortDescription() { + return getUiName(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSourceProperties.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSourceProperties.java new file mode 100644 index 00000000..fb8014ce --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSourceProperties.java @@ -0,0 +1,254 @@ +/* + * Copyright (C) 2012 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.sdklib.internal.repository.sources; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.annotations.VisibleForTesting; +import com.android.annotations.VisibleForTesting.Visibility; +import com.android.prefs.AndroidLocation; +import com.android.prefs.AndroidLocation.AndroidLocationException; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Properties; + +/** + * Properties for individual sources which are persisted by a local settings file. + *

+ * All instances of {@link SdkSourceProperties} share the same singleton storage. + * The persisted setting file is loaded as necessary, however callers must persist + * it at some point by calling {@link #save()}. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class SdkSourceProperties { + + /** + * An internal file version number, in case we want to change the format later. + */ + private static final String KEY_VERSION = "@version@"; //$NON-NLS-1$ + /** + * The last known UI name of the source. + */ + public static final String KEY_NAME = "@name@"; //$NON-NLS-1$ + /** + * A non-null string if the source is disabled. Null if the source is enabled. + */ + public static final String KEY_DISABLED = "@disabled@"; //$NON-NLS-1$ + + private static final Properties sSourcesProperties = new Properties(); + private static final String SRC_FILENAME = "sites-settings.cfg"; //$NON-NLS-1$ + + private static boolean sModified = false; + + public SdkSourceProperties() { + } + + public void save() { + synchronized (sSourcesProperties) { + if (sModified && !sSourcesProperties.isEmpty()) { + saveLocked(); + sModified = false; + } + } + } + + /** + * Retrieves a property for the given source URL and the given key type. + *

+ * Implementation detail: this loads the persistent settings file as needed. + * + * @param key The kind of property to retrieve for that source URL. + * @param sourceUrl The source URL. + * @param defaultValue The default value to return, if the property isn't found. Can be null. + * @return The non-null string property for the key/sourceUrl or the default value. + */ + @Nullable + public String getProperty(@NonNull String key, + @NonNull String sourceUrl, + @Nullable String defaultValue) { + String value = defaultValue; + + synchronized (sSourcesProperties) { + if (sSourcesProperties.isEmpty()) { + loadLocked(); + } + + value = sSourcesProperties.getProperty(key + sourceUrl, defaultValue); + } + + return value; + } + + /** + * Sets or remove a property for the given source URL and the given key type. + *

+ * Implementation detail: this does not save the persistent settings file. + * Somehow the caller will need to call the {@link #save()} method later. + * + * @param key The kind of property to retrieve for that source URL. + * @param sourceUrl The source URL. + * @param value The new value to set (if non null) or null to remove an existing property. + */ + public void setProperty(String key, String sourceUrl, String value) { + synchronized (sSourcesProperties) { + if (sSourcesProperties.isEmpty()) { + loadLocked(); + } + + key += sourceUrl; + + String old = sSourcesProperties.getProperty(key); + if (value == null) { + if (old != null) { + sSourcesProperties.remove(key); + sModified = true; + } + } else if (old == null || !old.equals(value)) { + sSourcesProperties.setProperty(key, value); + sModified = true; + } + } + } + + /** + * Returns an internal string representation of the underlying Properties map, + * sorted by ascending keys. Useful for debugging and testing purposes only. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(" keys = Collections.list(sSourcesProperties.keys()); + Collections.sort(keys, new Comparator() { + @Override + public int compare(Object o1, Object o2) { + return o1.toString().compareTo(o2.toString()); + }}); + + for (Object key : keys) { + sb.append('\n').append(key) + .append(" = ").append(sSourcesProperties.get(key)); //$NON-NLS-1$ + } + } + sb.append('>'); + return sb.toString(); + } + + /** Load state from persistent file. Expects sSourcesProperties to be synchronized. */ + private void loadLocked() { + // Load state from persistent file + if (loadProperties()) { + // If it lacks our magic version key, don't use it + if (sSourcesProperties.getProperty(KEY_VERSION) == null) { + sSourcesProperties.clear(); + } + + sModified = false; + } + + if (sSourcesProperties.isEmpty()) { + // Nothing was loaded. Initialize the storage with a version + // identified. This isn't currently checked back, but we might + // want it later if we decide to change the way this works. + // The version key is chosen on purpose to not match any valid URL. + sSourcesProperties.setProperty(KEY_VERSION, "1"); //$NON-NLS-1$ //$NON-NLS-2$ + } + } + + /** + * Load properties from default file. Extracted so that it can be mocked in tests. + * + * @return True if actually loaded the file. False if there was an IO error or no + * file and nothing was loaded. + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected boolean loadProperties() { + try { + String folder = AndroidLocation.getFolder(); + File f = new File(folder, SRC_FILENAME); + if (f.exists()) { + FileInputStream fis = null; + try { + fis = new FileInputStream(f); + sSourcesProperties.load(fis); + } catch (IOException ignore) { + // nop + } finally { + if (fis != null) { + try { + fis.close(); + } catch (IOException ignore) {} + } + } + + return true; + } + } catch (AndroidLocationException ignore) { + // nop + } + return false; + } + + /** + * Save file to disk. Expects sSourcesProperties to be synchronized. + * Made accessible for testing purposes. + * For public usage, please use {@link #save()} instead. + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected void saveLocked() { + // Persist it to the file + FileOutputStream fos = null; + try { + String folder = AndroidLocation.getFolder(); + File f = new File(folder, SRC_FILENAME); + + fos = new FileOutputStream(f); + + sSourcesProperties.store(fos,"## Sites Settings for Android SDK Manager");//$NON-NLS-1$ + + } catch (AndroidLocationException ignore) { + // nop + } catch (IOException ignore) { + // nop + } finally { + if (fos != null) { + try { + fos.close(); + } catch (IOException ignore) {} + } + } + } + + /** Empty current property list. Made accessible for testing purposes. */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected void clear() { + synchronized (sSourcesProperties) { + sSourcesProperties.clear(); + sModified = false; + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSources.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSources.java new file mode 100644 index 00000000..7c663cad --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSources.java @@ -0,0 +1,444 @@ +/* + * 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.sdklib.internal.repository.sources; + +import com.android.prefs.AndroidLocation; +import com.android.prefs.AndroidLocation.AndroidLocationException; +import com.android.sdklib.repository.SdkSysImgConstants; +import com.android.utils.ILogger; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.Iterator; +import java.util.Properties; +import java.util.Map.Entry; + +/** + * A list of sdk-repository and sdk-addon sources, sorted by {@link SdkSourceCategory}. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class SdkSources { + + private static final String KEY_COUNT = "count"; + + private static final String KEY_SRC = "src"; + + private static final String SRC_FILENAME = "repositories.cfg"; //$NON-NLS-1$ + + private final EnumMap> mSources = + new EnumMap>(SdkSourceCategory.class); + + private ArrayList mChangeListeners; // lazily initialized + + + public SdkSources() { + } + + /** + * Adds a new source to the Sources list. + *

+ * Implementation detail: {@link SdkSources} doesn't invoke {@link #notifyChangeListeners()} + * directly. Callers who use {@code add()} are responsible for notifying the listeners once + * they are done modifying the sources list. The intent is to notify the listeners only once + * at the end, not for every single addition. + */ + public void add(SdkSourceCategory category, SdkSource source) { + synchronized (mSources) { + ArrayList list = mSources.get(category); + if (list == null) { + list = new ArrayList(); + mSources.put(category, list); + } + + list.add(source); + } + } + + /** + * Removes a source from the Sources list. + *

+ * Callers who remove entries are responsible for notifying the listeners using + * {@link #notifyChangeListeners()} once they are done modifying the sources list. + */ + public void remove(SdkSource source) { + synchronized (mSources) { + Iterator>> it = + mSources.entrySet().iterator(); + while (it.hasNext()) { + Entry> entry = it.next(); + ArrayList list = entry.getValue(); + + if (list.remove(source)) { + if (list.isEmpty()) { + // remove the entry since the source list became empty + it.remove(); + } + } + } + } + } + + /** + * Removes all the sources in the given category. + *

+ * Callers who remove entries are responsible for notifying the listeners using + * {@link #notifyChangeListeners()} once they are done modifying the sources list. + */ + public void removeAll(SdkSourceCategory category) { + synchronized (mSources) { + mSources.remove(category); + } + } + + /** + * Returns a set of all categories that must be displayed. This includes all + * categories that are to be always displayed as well as all categories which + * have at least one source. + * Might return a empty array, but never returns null. + */ + public SdkSourceCategory[] getCategories() { + ArrayList cats = new ArrayList(); + + for (SdkSourceCategory cat : SdkSourceCategory.values()) { + if (cat.getAlwaysDisplay()) { + cats.add(cat); + } else { + synchronized (mSources) { + ArrayList list = mSources.get(cat); + if (list != null && !list.isEmpty()) { + cats.add(cat); + } + } + } + } + + return cats.toArray(new SdkSourceCategory[cats.size()]); + } + + /** + * Returns a new array of sources attached to the given category. + * Might return an empty array, but never returns null. + */ + public SdkSource[] getSources(SdkSourceCategory category) { + synchronized (mSources) { + ArrayList list = mSources.get(category); + if (list == null) { + return new SdkSource[0]; + } else { + return list.toArray(new SdkSource[list.size()]); + } + } + } + + /** + * Returns true if there are sources for the given category. + */ + public boolean hasSources(SdkSourceCategory category) { + synchronized (mSources) { + ArrayList list = mSources.get(category); + return list != null && !list.isEmpty(); + } + } + + /** + * Returns an array of the sources across all categories. This is never null. + */ + public SdkSource[] getAllSources() { + synchronized (mSources) { + int n = 0; + + for (ArrayList list : mSources.values()) { + n += list.size(); + } + + SdkSource[] sources = new SdkSource[n]; + + int i = 0; + for (ArrayList list : mSources.values()) { + for (SdkSource source : list) { + sources[i++] = source; + } + } + + return sources; + } + } + + /** + * Each source keeps a local cache of whatever it loaded recently. + * This calls {@link SdkSource#clearPackages()} on all the available sources, + * and the next call to {@link SdkSource#getPackages()} will actually reload + * the remote package list. + */ + public void clearAllPackages() { + synchronized (mSources) { + for (ArrayList list : mSources.values()) { + for (SdkSource source : list) { + source.clearPackages(); + } + } + } + } + + /** + * Returns the category of a given source, or null if the source is unknown. + *

+ * Note that this method uses object identity to find a given source, and does + * not identify sources by their URL like {@link #hasSourceUrl(SdkSource)} does. + *

+ * The search is O(N), which should be acceptable on the expectedly small source list. + */ + public SdkSourceCategory getCategory(SdkSource source) { + if (source != null) { + synchronized (mSources) { + for (Entry> entry : mSources.entrySet()) { + if (entry.getValue().contains(source)) { + return entry.getKey(); + } + } + } + } + return null; + } + + /** + * Returns true if there's already a similar source in the sources list + * under any category. + *

+ * Important: The match is NOT done on object identity. + * Instead, this searches for a similar source, based on + * {@link SdkSource#equals(Object)} which compares the source URLs. + *

+ * The search is O(N), which should be acceptable on the expectedly small source list. + */ + public boolean hasSourceUrl(SdkSource source) { + synchronized (mSources) { + for (ArrayList list : mSources.values()) { + for (SdkSource s : list) { + if (s.equals(source)) { + return true; + } + } + } + return false; + } + } + + /** + * Returns true if there's already a similar source in the sources list + * under the specified category. + *

+ * Important: The match is NOT done on object identity. + * Instead, this searches for a similar source, based on + * {@link SdkSource#equals(Object)} which compares the source URLs. + *

+ * The search is O(N), which should be acceptable on the expectedly small source list. + */ + public boolean hasSourceUrl(SdkSourceCategory category, SdkSource source) { + synchronized (mSources) { + ArrayList list = mSources.get(category); + if (list != null) { + for (SdkSource s : list) { + if (s.equals(source)) { + return true; + } + } + } + return false; + } + } + + /** + * Loads all user sources. This replaces all existing user sources + * by the ones from the property file. + *

+ * This calls {@link #notifyChangeListeners()} at the end of the operation. + */ + public void loadUserAddons(ILogger log) { + // Implementation detail: synchronize on the sources list to make sure that + // a- the source list doesn't change while we load/save it, and most important + // b- to make sure it's not being saved while loaded or the reverse. + // In most cases we do these operation from the UI thread so it's not really + // that necessary. This is more a protection in case of someone calls this + // from a worker thread by mistake. + synchronized (mSources) { + // Remove all existing user sources + removeAll(SdkSourceCategory.USER_ADDONS); + + // Load new user sources from property file + FileInputStream fis = null; + try { + String folder = AndroidLocation.getFolder(); + File f = new File(folder, SRC_FILENAME); + if (f.exists()) { + fis = new FileInputStream(f); + + Properties props = new Properties(); + props.load(fis); + + int count = Integer.parseInt(props.getProperty(KEY_COUNT, "0")); + + for (int i = 0; i < count; i++) { + String url = props.getProperty(String.format("%s%02d", KEY_SRC, i)); //$NON-NLS-1$ + if (url != null) { + // FIXME: this code originally only dealt with add-on XML sources. + // Now we'd like it to deal with system-image sources too, but we + // don't know which kind of object it is (at least not without + // trying to fetch it.) As a temporary workaround, just take a + // guess based on the leaf URI name. However ideally what we can + // simply do is add a checkbox "is system-image XML" in the user + // dialog and pass this info down here. Another alternative is to + // make a "dynamic" source object that tries to guess its type once + // the URI has been fetched. + SdkSource s; + if (url.endsWith(SdkSysImgConstants.URL_DEFAULT_FILENAME)) { + s = new SdkSysImgSource(url, null/*uiName*/); + } else { + s = new SdkAddonSource(url, null/*uiName*/); + } + if (!hasSourceUrl(s)) { + add(SdkSourceCategory.USER_ADDONS, s); + } + } + } + } + + } catch (NumberFormatException e) { + log.error(e, null); + + } catch (AndroidLocationException e) { + log.error(e, null); + + } catch (IOException e) { + log.error(e, null); + + } finally { + if (fis != null) { + try { + fis.close(); + } catch (IOException e) { + } + } + } + } + notifyChangeListeners(); + } + + /** + * Saves all the user sources. + * @param log Logger. Cannot be null. + */ + public void saveUserAddons(ILogger log) { + // See the implementation detail note in loadUserAddons() about the synchronization. + synchronized (mSources) { + FileOutputStream fos = null; + try { + String folder = AndroidLocation.getFolder(); + File f = new File(folder, SRC_FILENAME); + + fos = new FileOutputStream(f); + + Properties props = new Properties(); + + int count = 0; + for (SdkSource s : getSources(SdkSourceCategory.USER_ADDONS)) { + props.setProperty(String.format("%s%02d", KEY_SRC, count), //$NON-NLS-1$ + s.getUrl()); + count++; + } + props.setProperty(KEY_COUNT, Integer.toString(count)); + + props.store( fos, "## User Sources for Android SDK Manager"); //$NON-NLS-1$ + + } catch (AndroidLocationException e) { + log.error(e, null); + + } catch (IOException e) { + log.error(e, null); + + } finally { + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + } + } + } + } + } + + /** + * Adds a listener that will be notified when the sources list has changed. + * + * @param changeListener A non-null listener to add. Ignored if already present. + * @see SdkSources#notifyChangeListeners() + */ + public void addChangeListener(Runnable changeListener) { + assert changeListener != null; + if (mChangeListeners == null) { + mChangeListeners = new ArrayList(); + } + synchronized (mChangeListeners) { + if (changeListener != null && !mChangeListeners.contains(changeListener)) { + mChangeListeners.add(changeListener); + } + } + } + + /** + * Removes a listener from the list of listeners to notify when the sources change. + * + * @param changeListener A listener to remove. Ignored if not previously added. + */ + public void removeChangeListener(Runnable changeListener) { + if (mChangeListeners != null && changeListener != null) { + synchronized (mChangeListeners) { + mChangeListeners.remove(changeListener); + } + } + } + + /** + * Invoke all the registered change listeners, if any. + *

+ * This may be called from a worker thread, in which case the runnable + * should take care of only updating UI from a main thread. + */ + public void notifyChangeListeners() { + if (mChangeListeners == null) { + return; + } + synchronized (mChangeListeners) { + for (Runnable runnable : mChangeListeners) { + try { + runnable.run(); + } catch (Throwable ignore) { + assert ignore == null : + "A SdkSource.ChangeListener failed with an exception: " + ignore.toString(); + } + } + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSysImgSource.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSysImgSource.java new file mode 100644 index 00000000..1c90b4cd --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSysImgSource.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2012 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.sdklib.internal.repository.sources; + +import com.android.annotations.Nullable; +import com.android.sdklib.internal.repository.packages.Package; +import com.android.sdklib.repository.SdkSysImgConstants; + +import org.w3c.dom.Document; + +import java.io.InputStream; + + +/** + * An sdk-sys-img source, i.e. a download site for system-image packages. + * A repository describes one or more {@link Package}s available for download. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class SdkSysImgSource extends SdkSource { + + /** + * Constructs a new source for the given repository URL. + * @param url The source URL. Cannot be null. If the URL ends with a /, the default + * sys-img.xml filename will be appended automatically. + * @param uiName The UI-visible name of the source. Can be null. + */ + public SdkSysImgSource(String url, String uiName) { + super(url, uiName); + } + + /** + * Returns true if this is an addon source. + * We only load addons and extras from these sources. + */ + @Override + public boolean isAddonSource() { + return false; + } + + /** + * Returns true if this is a system-image source. + * We only load system-images from these sources. + */ + @Override + public boolean isSysImgSource() { + return true; + } + + + @Override + protected String[] getDefaultXmlFileUrls() { + return new String[] { SdkSysImgConstants.URL_DEFAULT_FILENAME }; + } + + @Override + protected int getNsLatestVersion() { + return SdkSysImgConstants.NS_LATEST_VERSION; + } + + @Override + protected String getNsUri() { + return SdkSysImgConstants.NS_URI; + } + + @Override + protected String getNsPattern() { + return SdkSysImgConstants.NS_PATTERN; + } + + @Override + protected String getSchemaUri(int version) { + return SdkSysImgConstants.getSchemaUri(version); + } + + @Override + protected String getRootElementName() { + return SdkSysImgConstants.NODE_SDK_SYS_IMG; + } + + @Override + protected InputStream getXsdStream(int version) { + return SdkSysImgConstants.getXsdStream(version); + } + + /** + * This kind of schema does not support forward-evolution of the <tool> element. + * + * @param xml The input XML stream. Can be null. + * @return Always null. + * @null This implementation always return null. + */ + @Override + protected Document findAlternateToolsXml(@Nullable InputStream xml) { + return null; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/ArchiveInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/ArchiveInfo.java new file mode 100644 index 00000000..c2098389 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/ArchiveInfo.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.sdklib.internal.repository.updater; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdklib.internal.repository.archives.Archive; +import com.android.sdklib.internal.repository.archives.ArchiveReplacement; + +import java.util.ArrayList; +import java.util.Collection; + +/** + * Represents an archive that we want to install. + * Note that the installer deals with archives whereas the user mostly sees packages + * but as far as we are concerned for installation there's a 1-to-1 mapping. + *

+ * A new archive is always a remote archive that needs to be downloaded and then + * installed. It can replace an existing local one. It can also depends on another + * (new or local) archive, which means the dependent archive needs to be successfully + * installed first. Finally this archive can also be a dependency for another one. + *

+ * The accepted and rejected flags are used by {@code SdkUpdaterChooserDialog} to follow + * user choices. The installer should never install something that is not accepted. + *

+ * Note: There is currently no logic to support more than one level of + * dependency, either here or in the {@code SdkUpdaterChooserDialog}, since we currently + * have no need for it. + * + * @see ArchiveInfo#ArchiveInfo(Archive, Archive, ArchiveInfo[]) + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class ArchiveInfo extends ArchiveReplacement implements Comparable { + + private final ArchiveInfo[] mDependsOn; + private final ArrayList mDependencyFor = new ArrayList(); + private boolean mAccepted; + private boolean mRejected; + + /** + * Creates a new replacement where the {@code newArchive} will replace the + * currently installed {@code replaced} archive. + * When {@code newArchive} is not intended to replace anything (e.g. because + * the user is installing a new package not present on her system yet), then + * {@code replace} shall be null. + * + * @param newArchive A "new archive" to be installed. This is always an archive + * that comes from a remote site. This may be null. + * @param replaced An optional local archive that the new one will replace. + * Can be null if this archive does not replace anything. + * @param dependsOn An optional new or local dependency, that is an archive that + * this archive depends upon. In other words, we can only install + * this archive if the dependency has been successfully installed. It also + * means we need to install the dependency first. Can be null or empty. + * However it cannot contain nulls. + */ + public ArchiveInfo( + @Nullable Archive newArchive, + @Nullable Archive replaced, + @Nullable ArchiveInfo[] dependsOn) { + super(newArchive, replaced); + mDependsOn = dependsOn; + } + + /** + * Returns an optional new or local dependency, that is an archive that this + * archive depends upon. In other words, we can only install this archive if the + * dependency has been successfully installed. It also means we need to install the + * dependency first. + *

+ * This array can be null or empty. It can't contain nulls though. + */ + @Nullable + public ArchiveInfo[] getDependsOn() { + return mDependsOn; + } + + /** + * Returns true if this new archive is a dependency for another one that we + * want to install. + */ + public boolean isDependencyFor() { + return !mDependencyFor.isEmpty(); + } + + /** + * Adds an {@link ArchiveInfo} for which this package is a dependency. + * This means the package added here depends on this package. + */ + @NonNull + public ArchiveInfo addDependencyFor(ArchiveInfo dependencyFor) { + if (!mDependencyFor.contains(dependencyFor)) { + mDependencyFor.add(dependencyFor); + } + + return this; + } + + /** + * Returns the list of {@link ArchiveInfo} for which this package is a dependency. + * This means the packages listed here depend on this package. + *

+ * Implementation detail: this is the internal mutable list. Callers should not modify it. + * This list can be empty but is never null. + */ + @NonNull + public Collection getDependenciesFor() { + return mDependencyFor; + } + + /** + * Sets whether this archive was accepted (either manually by the user or + * automatically if it doesn't have a license) for installation. + */ + public void setAccepted(boolean accepted) { + mAccepted = accepted; + } + + /** + * Returns whether this archive was accepted (either manually by the user or + * automatically if it doesn't have a license) for installation. + */ + public boolean isAccepted() { + return mAccepted; + } + + /** + * Sets whether this archive was rejected manually by the user. + * An archive can neither accepted nor rejected. + */ + public void setRejected(boolean rejected) { + mRejected = rejected; + } + + /** + * Returns whether this archive was rejected manually by the user. + * An archive can neither accepted nor rejected. + */ + public boolean isRejected() { + return mRejected; + } + + /** + * ArchiveInfos are compared using ther "new archive" ordering. + * + * @see Archive#compareTo(Archive) + */ + @Override + public int compareTo(ArchiveInfo rhs) { + if (getNewArchive() != null && rhs != null) { + return getNewArchive().compareTo(rhs.getNewArchive()); + } + return 0; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/ISettingsPage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/ISettingsPage.java new file mode 100644 index 00000000..1da1b01c --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/ISettingsPage.java @@ -0,0 +1,113 @@ +/* + * 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.sdklib.internal.repository.updater; + +import com.android.sdklib.internal.repository.DownloadCache; + +import java.net.URL; +import java.util.Properties; + +/** + * Interface that a settings page must implement. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public interface ISettingsPage { + + /** + * Java system setting picked up by {@link URL} for http proxy port. + * Type: String. + */ + String KEY_HTTP_PROXY_PORT = "http.proxyPort"; //$NON-NLS-1$ + + /** + * Java system setting picked up by {@link URL} for http proxy host. + * Type: String. + */ + String KEY_HTTP_PROXY_HOST = "http.proxyHost"; //$NON-NLS-1$ + + /** + * Setting to force using http:// instead of https:// connections. + * Type: Boolean. + * Default: False. + */ + String KEY_FORCE_HTTP = "sdkman.force.http"; //$NON-NLS-1$ + + /** + * Setting to display only packages that are new or updates. + * Type: Boolean. + * Default: True. + */ + String KEY_SHOW_UPDATE_ONLY = "sdkman.show.update.only"; //$NON-NLS-1$ + + /** + * Setting to ask for permission before restarting ADB. + * Type: Boolean. + * Default: False. + */ + String KEY_ASK_ADB_RESTART = "sdkman.ask.adb.restart"; //$NON-NLS-1$ + + /** + * Setting to use the {@link DownloadCache}, for small manifest XML files. + * Type: Boolean. + * Default: True. + */ + String KEY_USE_DOWNLOAD_CACHE = "sdkman.use.dl.cache"; //$NON-NLS-1$ + + /** + * Setting to enabling previews in the package list + * Type: Boolean. + * Default: True. + */ + String KEY_ENABLE_PREVIEWS = "sdkman.enable.previews2"; //$NON-NLS-1$ + + /** + * Setting to set the density of the monitor. + * Type: Integer. + * Default: -1 + */ + String KEY_MONITOR_DENSITY = "sdkman.monitor.density"; //$NON-NLS-1$ + + /** Loads settings from the given {@link Properties} container and update the page UI. */ + void loadSettings(Properties inSettings); + + /** Called by the application to retrieve settings from the UI and store them in + * the given {@link Properties} container. */ + void retrieveSettings(Properties outSettings); + + /** + * Called by the application to give a callback that the page should invoke when + * settings have changed. + */ + void setOnSettingsChanged(SettingsChangedCallback settingsChangedCallback); + + /** + * Callback used to notify the application that settings have changed and need to be + * applied. + */ + interface SettingsChangedCallback { + /** + * Invoked by the settings page when settings have changed and need to be + * applied. The application will call {@link ISettingsPage#retrieveSettings(Properties)} + * and apply the new settings. + */ + void onSettingsChanged(ISettingsPage page); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/IUpdaterData.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/IUpdaterData.java new file mode 100644 index 00000000..b3a74f2f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/IUpdaterData.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2010 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.sdklib.internal.repository.updater; + +import com.android.sdklib.SdkManager; +import com.android.sdklib.internal.avd.AvdManager; +import com.android.sdklib.internal.repository.DownloadCache; +import com.android.sdklib.internal.repository.ITaskFactory; +import com.android.utils.ILogger; + + +/** + * Interface used to retrieve some parameters from an {@link UpdaterData} instance. + * Useful mostly for unit tests purposes. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public interface IUpdaterData { + + ITaskFactory getTaskFactory(); + + ILogger getSdkLog(); + + DownloadCache getDownloadCache(); + + SdkManager getSdkManager(); + + AvdManager getAvdManager(); + + SettingsController getSettingsController(); + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/PackageLoader.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/PackageLoader.java new file mode 100644 index 00000000..c0314d4b --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/PackageLoader.java @@ -0,0 +1,502 @@ +/* + * Copyright (C) 2011 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.sdklib.internal.repository.updater; + +import com.android.annotations.NonNull; +import com.android.sdklib.internal.repository.AddonsListFetcher; +import com.android.sdklib.internal.repository.AddonsListFetcher.Site; +import com.android.sdklib.internal.repository.DownloadCache; +import com.android.sdklib.internal.repository.ITask; +import com.android.sdklib.internal.repository.ITaskMonitor; +import com.android.sdklib.internal.repository.NullTaskMonitor; +import com.android.sdklib.internal.repository.archives.Archive; +import com.android.sdklib.internal.repository.packages.Package; +import com.android.sdklib.internal.repository.packages.Package.UpdateInfo; +import com.android.sdklib.internal.repository.sources.SdkAddonSource; +import com.android.sdklib.internal.repository.sources.SdkSource; +import com.android.sdklib.internal.repository.sources.SdkSourceCategory; +import com.android.sdklib.internal.repository.sources.SdkSources; +import com.android.sdklib.internal.repository.sources.SdkSysImgSource; +import com.android.sdklib.repository.SdkAddonsListConstants; +import com.android.sdklib.repository.SdkRepoConstants; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Loads packages fetched from the remote SDK Repository and keeps track + * of their state compared with the current local SDK installation. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class PackageLoader { + + /** The update data context. Never null. */ + private final UpdaterData mUpdaterData; + + /** + * The {@link DownloadCache} override. Can be null, in which case the one from + * {@link UpdaterData} is used instead. + * @see #getDownloadCache() + */ + private final DownloadCache mOverrideCache; + + /** + * 0 = need to fetch remote addons list once.. + * 1 = fetch succeeded, don't need to do it any more. + * -1= fetch failed, do it again only if the user requests a refresh + * or changes the force-http setting. + */ + private int mStateFetchRemoteAddonsList; + + + /** + * Interface for the callback called by + * {@link PackageLoader#loadPackages(boolean, ISourceLoadedCallback)}. + *

+ * After processing each source, the package loader calls {@link #onUpdateSource} + * with the list of packages found in that source. + * By returning true from {@link #onUpdateSource}, the client tells the loader to + * continue and process the next source. By returning false, it tells to stop loading. + *

+ * The {@link #onLoadCompleted()} method is guaranteed to be called at the end, no + * matter how the loader stopped, so that the client can clean up or perform any + * final action. + */ + public interface ISourceLoadedCallback { + /** + * After processing each source, the package loader calls this method with the + * list of packages found in that source. + * By returning true from {@link #onUpdateSource}, the client tells + * the loader to continue and process the next source. + * By returning false, it tells to stop loading. + *

+ * Important: This method is called from a sub-thread, so clients which + * try to access any UI widgets must wrap their calls into + * {@code Display.syncExec(Runnable)} or {@code Display.asyncExec(Runnable)}. + * + * @param packages All the packages loaded from the source. Never null. + * @return True if the load operation should continue, false if it should stop. + */ + boolean onUpdateSource(SdkSource source, Package[] packages); + + /** + * This method is guaranteed to be called at the end, no matter how the + * loader stopped, so that the client can clean up or perform any final action. + */ + void onLoadCompleted(); + } + + /** + * Interface describing the task of installing a specific package. + * For details on the operation, + * see {@link PackageLoader#loadPackagesWithInstallTask(int, IAutoInstallTask)}. + * + * @see PackageLoader#loadPackagesWithInstallTask(int, IAutoInstallTask) + */ + public interface IAutoInstallTask { + /** + * Invoked by the loader once a source has been loaded and its package + * definitions are known. The method should return the {@code packages} + * array and can modify it if necessary. + * The loader will call {@link #acceptPackage(Package)} on all the packages returned. + * + * @param source The source of the packages. Null for the locally installed packages. + * @param packages The packages found in the source. + */ + Package[] filterLoadedSource(SdkSource source, Package[] packages); + + /** + * Called by the install task for every package available (new ones, updates as well + * as existing ones that don't have a potential update.) + * The method should return true if this is a package that should be installed. + *

+ * Important: This method is called from a sub-thread, so clients who try + * to access any UI widgets must wrap their calls into {@code Display.syncExec(Runnable)} + * or {@code Display.asyncExec(Runnable)}. + */ + boolean acceptPackage(Package pkg); + + /** + * Called when the accepted package has been installed, successfully or not. + * If an already installed (aka existing) package has been accepted, this will + * be called with a 'true' success and the actual install paths. + *

+ * Important: This method is called from a sub-thread, so clients who try + * to access any UI widgets must wrap their calls into {@code Display.syncExec(Runnable)} + * or {@code Display.asyncExec(Runnable)}. + */ + void setResult(boolean success, Map installPaths); + + /** + * Called when the task is done iterating and completed. + */ + void taskCompleted(); + } + + /** + * Creates a new PackageManager associated with the given {@link UpdaterData} + * and using the {@link UpdaterData}'s default {@link DownloadCache}. + * + * @param updaterData The {@link UpdaterData}. Must not be null. + */ + public PackageLoader(UpdaterData updaterData) { + mUpdaterData = updaterData; + mOverrideCache = null; + } + + /** + * Creates a new PackageManager associated with the given {@link UpdaterData} + * but using the specified {@link DownloadCache} instead of the one from + * {@link UpdaterData}. + * + * @param updaterData The {@link UpdaterData}. Must not be null. + * @param cache The {@link DownloadCache} to use instead of the one from {@link UpdaterData}. + */ + public PackageLoader(UpdaterData updaterData, DownloadCache cache) { + mUpdaterData = updaterData; + mOverrideCache = cache; + } + + public UpdaterData getUpdaterData() { + return mUpdaterData; + } + + /** + * Runs a runnable on the UI thread. + * The base implementation just runs the runnable right away. + * + * @param r Non-null runnable. + */ + protected void runOnUiThread(@NonNull Runnable r) { + r.run(); + } + + /** + * Loads all packages from the remote repository. + * This runs in an {@link ITask}. The call is blocking. + *

+ * The callback is called with each set of {@link PkgItem} found in each source. + * The caller is responsible to accumulate the packages given to the callback + * after each source is finished loaded. In return the callback tells the loader + * whether to continue loading sources. + *

+ * Normally this method doesn't access the remote source if it's already + * been loaded in the in-memory source (e.g. don't fetch twice). + * + * @param overrideExisting Set this to true when the caller wants to + * check for updates and discard any existing source already + * loaded in memory. It should be false for normal use. + * @param sourceLoadedCallback The callback to invoke for each loaded source. + */ + public void loadPackages( + final boolean overrideExisting, + final ISourceLoadedCallback sourceLoadedCallback) { + try { + if (mUpdaterData == null) { + return; + } + + mUpdaterData.getTaskFactory().start("Loading Sources", new ITask() { + @Override + public void run(ITaskMonitor monitor) { + monitor.setProgressMax(10); + + // get local packages and offer them to the callback + Package[] localPkgs = + mUpdaterData.getInstalledPackages(monitor.createSubMonitor(1)); + if (localPkgs == null) { + localPkgs = new Package[0]; + } + if (!sourceLoadedCallback.onUpdateSource(null, localPkgs)) { + return; + } + + // get remote packages + boolean forceHttp = + mUpdaterData.getSettingsController().getSettings().getForceHttp(); + loadRemoteAddonsList(monitor.createSubMonitor(1)); + + SdkSource[] sources = mUpdaterData.getSources().getAllSources(); + try { + if (sources != null && sources.length > 0) { + ITaskMonitor subMonitor = monitor.createSubMonitor(8); + subMonitor.setProgressMax(sources.length); + for (SdkSource source : sources) { + Package[] pkgs = source.getPackages(); + if (pkgs == null || overrideExisting) { + source.load(getDownloadCache(), + subMonitor.createSubMonitor(1), + forceHttp); + pkgs = source.getPackages(); + } + if (pkgs == null) { + continue; + } + + // Notify the callback a new source has finished loading. + // If the callback requests so, stop right away. + if (!sourceLoadedCallback.onUpdateSource(source, pkgs)) { + return; + } + } + } + } catch(Exception e) { + monitor.logError("Loading source failed: %1$s", e.toString()); + } finally { + monitor.setDescription("Done loading packages."); + } + } + }); + } finally { + sourceLoadedCallback.onLoadCompleted(); + } + } + + /** + * Load packages, source by source using + * {@link #loadPackages(boolean, ISourceLoadedCallback)}, + * and executes the given {@link IAutoInstallTask} on the current package list. + * That is for each package known, the install task is queried to find if + * the package is the one to be installed or updated. + *

+ * - If an already installed package is accepted by the task, it is returned.
+ * - If a new package (remotely available but not installed locally) is accepted, + * the user will be prompted for permission to install it.
+ * - If an existing package has updates, the install task will be accept if it + * accepts one of the updating packages, and if yes the the user will be + * prompted for permission to install it.
+ *

+ * Only one package can be accepted, after which the task is completed. + * There is no direct return value, {@link IAutoInstallTask#setResult} is called on the + * result of the accepted package. + * When the task is completed, {@link IAutoInstallTask#taskCompleted()} is called. + *

+ * The call is blocking. Although the name says "Task", this is not an {@link ITask} + * running in its own thread but merely a synchronous call. + * + * @param installFlags Flags for installation such as + * {@link UpdaterData#TOOLS_MSG_UPDATED_FROM_ADT}. + * @param installTask The task to perform. + */ + public void loadPackagesWithInstallTask( + final int installFlags, + final IAutoInstallTask installTask) { + + loadPackages(false /*overrideExisting*/, new ISourceLoadedCallback() { + List mArchivesToInstall = new ArrayList(); + Map mInstallPaths = new HashMap(); + + @Override + public boolean onUpdateSource(SdkSource source, Package[] packages) { + packages = installTask.filterLoadedSource(source, packages); + if (packages == null || packages.length == 0) { + // Tell loadPackages() to process the next source. + return true; + } + + for (Package pkg : packages) { + if (pkg.isLocal()) { + // This is a local (aka installed) package + if (installTask.acceptPackage(pkg)) { + // If the caller is accepting an installed package, + // return a success and give the package's install path + Archive[] a = pkg.getArchives(); + // an installed package should have one local compatible archive + if (a.length == 1 && a[0].isCompatible()) { + mInstallPaths.put(pkg, new File(a[0].getLocalOsPath())); + } + } + + } else { + // This is a remote package + if (installTask.acceptPackage(pkg)) { + // The caller is accepting this remote package. We'll install it. + for (Archive archive : pkg.getArchives()) { + if (archive.isCompatible()) { + mArchivesToInstall.add(archive); + break; + } + } + } + } + } + + // Tell loadPackages() to process the next source. + return true; + } + + @Override + public void onLoadCompleted() { + if (!mArchivesToInstall.isEmpty()) { + installArchives(mArchivesToInstall); + } + if (mInstallPaths == null) { + installTask.setResult(false, null); + } else { + installTask.setResult(true, mInstallPaths); + } + + installTask.taskCompleted(); + } + + /** + * Shows the UI of the install selector. + * If the package is then actually installed, refresh the local list and + * notify the install task of the installation path. + * + * @param archivesToInstall The archives to install. + */ + private void installArchives(final List archivesToInstall) { + // Actually install the new archives that we just found. + // This will display some UI so we need a shell's sync exec. + + final List installedArchives = new ArrayList(); + + runOnUiThread(new Runnable() { + @Override + public void run() { + List archives = + mUpdaterData.updateOrInstallAll_WithGUI( + archivesToInstall, + true /* includeObsoletes */, + installFlags); + + if (archives != null) { + installedArchives.addAll(archives); + } + } + }); + + if (installedArchives.isEmpty()) { + // We failed to install anything. + mInstallPaths = null; + return; + } + + // The local package list has changed, make sure to refresh it + mUpdaterData.getSdkManager().reloadSdk(mUpdaterData.getSdkLog()); + mUpdaterData.getLocalSdkParser().clearPackages(); + final Package[] localPkgs = mUpdaterData.getInstalledPackages( + new NullTaskMonitor(mUpdaterData.getSdkLog())); + + // Scan the installed package list to find the install paths. + for (Archive installedArchive : installedArchives) { + Package pkg = installedArchive.getParentPackage(); + + for (Package localPkg : localPkgs) { + if (localPkg.canBeUpdatedBy(pkg) == UpdateInfo.NOT_UPDATE) { + Archive[] localArchive = localPkg.getArchives(); + if (localArchive.length == 1 && localArchive[0].isCompatible()) { + mInstallPaths.put( + localPkg, + new File(localArchive[0].getLocalOsPath())); + } + } + } + } + } + }); + } + + + /** + * Loads the remote add-ons list. + */ + public void loadRemoteAddonsList(ITaskMonitor monitor) { + + if (mStateFetchRemoteAddonsList != 0) { + return; + } + + mUpdaterData.getTaskFactory().start("Load Add-ons List", monitor, new ITask() { + @Override + public void run(ITaskMonitor subMonitor) { + loadRemoteAddonsListInTask(subMonitor); + } + }); + } + + private void loadRemoteAddonsListInTask(ITaskMonitor monitor) { + mStateFetchRemoteAddonsList = -1; + + String url = SdkAddonsListConstants.URL_ADDON_LIST; + + // We override SdkRepoConstants.URL_GOOGLE_SDK_SITE if this is defined + String baseUrl = System.getenv("SDK_TEST_BASE_URL"); //$NON-NLS-1$ + if (baseUrl != null) { + if (!baseUrl.isEmpty() && baseUrl.endsWith("/")) { //$NON-NLS-1$ + if (url.startsWith(SdkRepoConstants.URL_GOOGLE_SDK_SITE)) { + url = baseUrl + url.substring(SdkRepoConstants.URL_GOOGLE_SDK_SITE.length()); + } + } else { + monitor.logError("Ignoring invalid SDK_TEST_BASE_URL: %1$s", baseUrl); //$NON-NLS-1$ + } + } + + if (mUpdaterData.getSettingsController().getSettings().getForceHttp()) { + url = url.replaceAll("https://", "http://"); //$NON-NLS-1$ //$NON-NLS-2$ + } + + // Hook to bypass loading 3rd party addons lists. + boolean fetch3rdParties = System.getenv("SDK_SKIP_3RD_PARTIES") == null; + + AddonsListFetcher fetcher = new AddonsListFetcher(); + Site[] sites = fetcher.fetch(url, getDownloadCache(), monitor); + if (sites != null) { + SdkSources sources = mUpdaterData.getSources(); + sources.removeAll(SdkSourceCategory.ADDONS_3RD_PARTY); + + if (fetch3rdParties) { + for (Site s : sites) { + switch (s.getType()) { + case ADDON_SITE: + sources.add(SdkSourceCategory.ADDONS_3RD_PARTY, + new SdkAddonSource(s.getUrl(), s.getUiName())); + break; + case SYS_IMG_SITE: + sources.add(SdkSourceCategory.ADDONS_3RD_PARTY, + new SdkSysImgSource(s.getUrl(), s.getUiName())); + break; + } + } + } + + sources.notifyChangeListeners(); + + mStateFetchRemoteAddonsList = 1; + } + + monitor.setDescription("Fetched Add-ons List successfully"); + } + + /** + * Returns the {@link DownloadCache} to use. + * + * @return Returns {@link #mOverrideCache} if not null; otherwise returns the + * one from {@link UpdaterData} is used instead. + */ + private DownloadCache getDownloadCache() { + return mOverrideCache != null ? mOverrideCache : mUpdaterData.getDownloadCache(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/PkgItem.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/PkgItem.java new file mode 100644 index 00000000..60f7aa04 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/PkgItem.java @@ -0,0 +1,290 @@ +/* + * Copyright (C) 2011 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.sdklib.internal.repository.updater; + +import com.android.annotations.Nullable; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.repository.IDescription; +import com.android.sdklib.internal.repository.archives.Archive; +import com.android.sdklib.internal.repository.packages.IAndroidVersionProvider; +import com.android.sdklib.internal.repository.packages.Package; +import com.android.sdklib.internal.repository.packages.Package.UpdateInfo; +import com.android.sdklib.internal.repository.sources.SdkSource; +import com.android.sdklib.repository.FullRevision; + +/** + * A {@link PkgItem} represents one main {@link Package} combined with its state + * and an optional update package. + *

+ * The main package is final and cannot change since it's what "defines" this PkgItem. + * The state or update package can change later. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class PkgItem implements Comparable { + private final PkgState mState; + private final Package mMainPkg; + private Package mUpdatePkg; + private boolean mChecked; + + /** + * The state of the a given {@link PkgItem}, that is the relationship between + * a given remote package and the local repository. + */ + public enum PkgState { + // Implementation detail: if this is changed then PackageDiffLogic#STATES + // and PackageDiffLogic#processSource() need to be changed accordingly. + + /** + * Package is locally installed and may or may not have an update. + */ + INSTALLED, + + /** + * There's a new package available on the remote site that isn't installed locally. + */ + NEW + } + + /** + * Create a new {@link PkgItem} for this main package. + * The main package is final and cannot change since it's what "defines" this PkgItem. + * The state or update package can change later. + */ + public PkgItem(Package mainPkg, PkgState state) { + mMainPkg = mainPkg; + mState = state; + assert mMainPkg != null; + } + + public boolean isObsolete() { + return mMainPkg.isObsolete(); + } + + public boolean isChecked() { + return mChecked; + } + + public void setChecked(boolean checked) { + mChecked = checked; + } + + public Package getUpdatePkg() { + return mUpdatePkg; + } + + public boolean hasUpdatePkg() { + return mUpdatePkg != null; + } + + public String getName() { + return mMainPkg.getListDescription(); + } + + public FullRevision getRevision() { + return mMainPkg.getRevision(); + } + + /** + * @deprecated Use {@link #getMainPackage()} with the {@link IDescription} interface instead. + */ + @Deprecated + public String getDescription() { + return mMainPkg.getLongDescription(); + } + + public Package getMainPackage() { + return mMainPkg; + } + + public PkgState getState() { + return mState; + } + + public SdkSource getSource() { + return mMainPkg.getParentSource(); + } + + @Nullable + public AndroidVersion getAndroidVersion() { + return mMainPkg instanceof IAndroidVersionProvider ? + ((IAndroidVersionProvider) mMainPkg).getAndroidVersion() : + null; + } + + public Archive[] getArchives() { + return mMainPkg.getArchives(); + } + + @Override + public int compareTo(PkgItem pkg) { + return getMainPackage().compareTo(pkg.getMainPackage()); + } + + /** + * Returns true if this package or its updating packages contains + * the exact given archive. + * Important: This compares object references, not object equality. + */ + public boolean hasArchive(Archive archive) { + if (mMainPkg.hasArchive(archive)) { + return true; + } + if (mUpdatePkg != null && mUpdatePkg.hasArchive(archive)) { + return true; + } + return false; + } + + /** + * Returns true if the main package has at least one archive + * compatible with the current platform. + */ + public boolean hasCompatibleArchive() { + return mMainPkg.hasCompatibleArchive(); + } + + /** + * Checks whether the main packages are of the same type and are + * not an update of each other and have the same revision number. + */ + public boolean isSameMainPackageAs(Package pkg) { + if (mMainPkg.canBeUpdatedBy(pkg) == UpdateInfo.NOT_UPDATE) { + // package revision numbers must match + return mMainPkg.getRevision().equals(pkg.getRevision()); + } + return false; + } + + /** + * Checks whether the update packages are of the same type and are + * not an update of each other and have the same revision numbers. + */ + public boolean isSameUpdatePackageAs(Package pkg) { + if (mUpdatePkg != null && mUpdatePkg.canBeUpdatedBy(pkg) == UpdateInfo.NOT_UPDATE) { + // package revision numbers must match + return mUpdatePkg.getRevision().equals(pkg.getRevision()); + } + return false; + } + + /** + * Checks whether too {@link PkgItem} are the same. + * This checks both items have the same state, both main package are similar + * and that they have the same updating packages. + */ + public boolean isSameItemAs(PkgItem item) { + if (this == item) { + return true; + } + boolean same = this.mState == item.mState; + if (same) { + same = isSameMainPackageAs(item.getMainPackage()); + } + + if (same) { + // check updating packages are the same + Package p1 = this.mUpdatePkg; + Package p2 = item.getUpdatePkg(); + same = (p1 == p2) || (p1 == null && p2 == null) || (p1 != null && p2 != null); + + if (same && p1 != null) { + same = p1.canBeUpdatedBy(p2) == UpdateInfo.NOT_UPDATE; + } + } + + return same; + } + + /** + * Equality is defined as {@link #isSameItemAs(PkgItem)}: state, main package + * and update package must be the similar. + */ + @Override + public boolean equals(Object obj) { + return (obj instanceof PkgItem) && this.isSameItemAs((PkgItem) obj); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((mState == null) ? 0 : mState.hashCode()); + result = prime * result + ((mMainPkg == null) ? 0 : mMainPkg.hashCode()); + result = prime * result + ((mUpdatePkg == null) ? 0 : mUpdatePkg.hashCode()); + return result; + } + + /** + * Check whether the 'pkg' argument is an update for this package. + * If it is, record it as an updating package. + * If there's already an updating package, only keep the most recent update. + * Returns true if it is update (even if there was already an update and this + * ended up not being the most recent), false if incompatible or not an update. + * + * This should only be used for installed packages. + */ + public boolean mergeUpdate(Package pkg) { + if (mUpdatePkg == pkg) { + return true; + } + if (mMainPkg.canBeUpdatedBy(pkg) == UpdateInfo.UPDATE) { + if (mUpdatePkg == null) { + mUpdatePkg = pkg; + } else if (mUpdatePkg.canBeUpdatedBy(pkg) == UpdateInfo.UPDATE) { + // If we have more than one, keep only the most recent update + mUpdatePkg = pkg; + } + return true; + } + + return false; + } + + public void removeUpdate() { + mUpdatePkg = null; + } + + /** Returns a string representation of this item, useful when debugging. */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append('<'); + + if (mChecked) { + sb.append(" * "); //$NON-NLS-1$ + } + + sb.append(mState.toString()); + + if (mMainPkg != null) { + sb.append(", pkg:"); //$NON-NLS-1$ + sb.append(mMainPkg.toString()); + } + + if (mUpdatePkg != null) { + sb.append(", updated by:"); //$NON-NLS-1$ + sb.append(mUpdatePkg.toString()); + } + + sb.append('>'); + return sb.toString(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/SdkUpdaterLogic.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/SdkUpdaterLogic.java new file mode 100644 index 00000000..ff2000fa --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/SdkUpdaterLogic.java @@ -0,0 +1,1568 @@ +/* + * 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.sdklib.internal.repository.updater; + +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.internal.repository.ITask; +import com.android.sdklib.internal.repository.ITaskMonitor; +import com.android.sdklib.internal.repository.archives.Archive; +import com.android.sdklib.internal.repository.packages.AddonPackage; +import com.android.sdklib.internal.repository.packages.BuildToolPackage; +import com.android.sdklib.internal.repository.packages.DocPackage; +import com.android.sdklib.internal.repository.packages.ExtraPackage; +import com.android.sdklib.internal.repository.packages.IAndroidVersionProvider; +import com.android.sdklib.internal.repository.packages.IExactApiLevelDependency; +import com.android.sdklib.internal.repository.packages.IMinApiLevelDependency; +import com.android.sdklib.internal.repository.packages.IMinPlatformToolsDependency; +import com.android.sdklib.internal.repository.packages.IMinToolsDependency; +import com.android.sdklib.internal.repository.packages.IPlatformDependency; +import com.android.sdklib.internal.repository.packages.MinToolsPackage; +import com.android.sdklib.internal.repository.packages.Package; +import com.android.sdklib.internal.repository.packages.Package.UpdateInfo; +import com.android.sdklib.internal.repository.packages.PlatformPackage; +import com.android.sdklib.internal.repository.packages.PlatformToolPackage; +import com.android.sdklib.internal.repository.packages.SamplePackage; +import com.android.sdklib.internal.repository.packages.SystemImagePackage; +import com.android.sdklib.internal.repository.packages.ToolPackage; +import com.android.sdklib.internal.repository.sources.SdkSource; +import com.android.sdklib.internal.repository.sources.SdkSources; +import com.android.sdklib.repository.FullRevision; +import com.android.sdklib.repository.descriptors.IdDisplay; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * The logic to compute which packages to install, based on the choices + * made by the user. This adds required packages as needed. + *

+ * When the user doesn't provide a selection, looks at local package to find + * those that can be updated and compute dependencies too. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class SdkUpdaterLogic { + + private final IUpdaterData mUpdaterData; + + public SdkUpdaterLogic(IUpdaterData updaterData) { + mUpdaterData = updaterData; + } + + /** + * Retrieves an unfiltered list of all remote archives. + * The archives are guaranteed to be compatible with the current platform. + */ + public List getAllRemoteArchives( + SdkSources sources, + Package[] localPkgs, + boolean includeAll) { + + List remotePkgs = new ArrayList(); + SdkSource[] remoteSources = sources.getAllSources(); + fetchRemotePackages(remotePkgs, remoteSources); + + ArrayList archives = new ArrayList(); + for (Package remotePkg : remotePkgs) { + // Only look for non-obsolete updates unless requested to include them + if (includeAll || !remotePkg.isObsolete()) { + // Found a suitable update. Only accept the remote package + // if it provides at least one compatible archive + + addArchives: + for (Archive a : remotePkg.getArchives()) { + if (a.isCompatible()) { + + // If we're trying to add a package for revision N, + // make sure we don't also have a package for revision N-1. + for (int i = archives.size() - 1; i >= 0; i--) { + Package pkgFound = archives.get(i).getParentPackage(); + if (pkgFound.canBeUpdatedBy(remotePkg) == UpdateInfo.UPDATE) { + // This package can update one we selected earlier. + // Remove the one that can be updated by this new one. + archives.remove(i); + } else if (remotePkg.canBeUpdatedBy(pkgFound) == UpdateInfo.UPDATE) { + // There is a package in the list that is already better + // than the one we want to add, so don't add it. + break addArchives; + } + } + + archives.add(a); + break; + } + } + } + } + + ArrayList result = new ArrayList(); + + ArchiveInfo[] localArchives = createLocalArchives(localPkgs); + + for (Archive a : archives) { + insertArchive(a, + result, + archives, + remotePkgs, + remoteSources, + localArchives, + false /*automated*/); + } + + return result; + } + + /** + * Compute which packages to install by taking the user selection + * and adding required packages as needed. + * + * When the user doesn't provide a selection, looks at local packages to find + * those that can be updated and compute dependencies too. + */ + public List computeUpdates( + Collection selectedArchives, + SdkSources sources, + Package[] localPkgs, + boolean includeAll) { + + List archives = new ArrayList(); + List remotePkgs = new ArrayList(); + SdkSource[] remoteSources = sources.getAllSources(); + + // Create ArchiveInfos out of local (installed) packages. + ArchiveInfo[] localArchives = createLocalArchives(localPkgs); + + // If we do not have a specific list of archives to install (that is the user + // selected "update all" rather than request specific packages), then we try to + // find updates based on the *existing* packages. + if (selectedArchives == null) { + selectedArchives = findUpdates( + localArchives, + remotePkgs, + remoteSources, + includeAll); + } + + // Once we have a list of packages to install, we try to solve all their + // dependencies by automatically adding them to the list of things to install. + // This works on the list provided either by the user directly or the list + // computed from potential updates. + for (Archive a : selectedArchives) { + insertArchive(a, + archives, + selectedArchives, + remotePkgs, + remoteSources, + localArchives, + false /*automated*/); + } + + // Finally we need to look at *existing* packages which are not being updated + // and check if they have any missing dependencies and suggest how to fix + // these dependencies. + fixMissingLocalDependencies( + archives, + selectedArchives, + remotePkgs, + remoteSources, + localArchives); + + return archives; + } + + private double getRevisionRank(FullRevision rev) { + int p = rev.isPreview() ? 999 : 999 - rev.getPreview(); + return rev.getMajor() + + rev.getMinor() / 1000.d + + rev.getMicro() / 1000000.d + + p / 1000000000.d; + } + + /** + * Finds new packages that the user does not have in his/her local SDK + * and adds them to the list of archives to install. + *

+ * The default is to only find "new" platforms, that is anything more + * recent than the highest platform currently installed. + * A side effect is that for an empty SDK install this will list *all* + * platforms available (since there's no "highest" installed platform.) + *

+ * This also adds "silent" dependencies. For example the user probably + * needs to have at least one version of the build-tools package although + * there is nothing that directly depends on it. So if the user doesn't have + * any installed or selected version, selected the most recent one as a + * candidate for install. + * + * @param archives The in-out list of archives to install. Typically the + * list is not empty at first as it should contain any archives that is + * already scheduled for install. This method will add to the list. + * @param sources The list of all sources, to fetch them as necessary. + * @param localPkgs The list of all currently installed packages. + * @param includeAll When true, this will list all platforms. + * (included these lower than the highest installed one) as well as + * all obsolete packages of these platforms. + */ + public void addNewPlatforms( + Collection archives, + SdkSources sources, + Package[] localPkgs, + boolean includeAll) { + + // Create ArchiveInfos out of local (installed) packages. + ArchiveInfo[] localArchives = createLocalArchives(localPkgs); + + // Find the highest platform installed + double currentBuildToolScore = 0; + double currentPlatformScore = 0; + double currentSampleScore = 0; + double currentAddonScore = 0; + double currentDocScore = 0; + HashMap currentExtraScore = new HashMap(); + if (!includeAll) { + if (localPkgs != null) { + for (Package p : localPkgs) { + double rev = getRevisionRank(p.getRevision()); + int api = 0; + boolean isPreview = false; + if (p instanceof IAndroidVersionProvider) { + AndroidVersion vers = ((IAndroidVersionProvider) p).getAndroidVersion(); + api = vers.getApiLevel(); + isPreview = vers.isPreview(); + } + + // The score is 1000*api + (999 if preview) + rev + // This allows previews to rank above a non-preview and + // allows revisions to rank appropriately. + double score = api * 1000 + (isPreview ? 999 : 0) + rev; + + if (p instanceof BuildToolPackage) { + currentBuildToolScore = Math.max(currentBuildToolScore, score); + } else if (p instanceof PlatformPackage) { + currentPlatformScore = Math.max(currentPlatformScore, score); + } else if (p instanceof SamplePackage) { + currentSampleScore = Math.max(currentSampleScore, score); + } else if (p instanceof AddonPackage) { + currentAddonScore = Math.max(currentAddonScore, score); + } else if (p instanceof ExtraPackage) { + currentExtraScore.put(((ExtraPackage) p).getPath(), score); + } else if (p instanceof DocPackage) { + currentDocScore = Math.max(currentDocScore, score); + } + } + } + } + + SdkSource[] remoteSources = sources.getAllSources(); + ArrayList remotePkgs = new ArrayList(); + fetchRemotePackages(remotePkgs, remoteSources); + + Package suggestedDoc = null; + Package suggestedBuildTool = null; + + for (Package p : remotePkgs) { + // Skip obsolete packages unless requested to include them. + if (p.isObsolete() && !includeAll) { + continue; + } + + double rev = getRevisionRank(p.getRevision()); + int api = 0; + boolean isPreview = false; + if (p instanceof IAndroidVersionProvider) { + AndroidVersion vers = ((IAndroidVersionProvider) p).getAndroidVersion(); + api = vers.getApiLevel(); + isPreview = vers.isPreview(); + } + + double score = api * 1000 + (isPreview ? 999 : 0) + rev; + + boolean shouldAdd = false; + if (p instanceof BuildToolPackage) { + // We don't want all the build-tool packages, only the most recent one + // if not is currently installed. + if (currentBuildToolScore == 0 && score > currentBuildToolScore) { + suggestedBuildTool = p; + currentBuildToolScore = score; + } + } else if (p instanceof PlatformPackage) { + shouldAdd = score > currentPlatformScore; + } else if (p instanceof SamplePackage) { + shouldAdd = score > currentSampleScore; + } else if (p instanceof AddonPackage) { + shouldAdd = score > currentAddonScore; + } else if (p instanceof ExtraPackage) { + String key = ((ExtraPackage) p).getPath(); + shouldAdd = !currentExtraScore.containsKey(key) || + score > currentExtraScore.get(key).doubleValue(); + } else if (p instanceof DocPackage) { + // We don't want all the doc, only the most recent one + if (score > currentDocScore) { + suggestedDoc = p; + currentDocScore = score; + } + } + + if (shouldAdd) { + // We should suggest this package for installation. + for (Archive a : p.getArchives()) { + if (a.isCompatible()) { + insertArchive(a, + archives, + null /*selectedArchives*/, + remotePkgs, + remoteSources, + localArchives, + true /*automated*/); + } + } + } + + if (p instanceof PlatformPackage && (score >= currentPlatformScore)) { + // We just added a new platform *or* we are visiting the highest currently + // installed platform. In either case we want to make sure it either has + // its own system image or that we provide one by default. + PlatformPackage pp = (PlatformPackage) p; + if (pp.getIncludedAbi() == null) { + for (Package p2 : remotePkgs) { + if (!(p2 instanceof SystemImagePackage) || + ((SystemImagePackage)p2).isPlatform() || + (p2.isObsolete() && !includeAll)) { + continue; + } + SystemImagePackage sip = (SystemImagePackage) p2; + if (sip.getAndroidVersion().equals(pp.getAndroidVersion())) { + for (Archive a : sip.getArchives()) { + if (a.isCompatible()) { + insertArchive(a, + archives, + null /*selectedArchives*/, + remotePkgs, + remoteSources, + localArchives, + true /*automated*/); + } + } + } + } + } + } + } + + if (suggestedDoc != null) { + // We should suggest this package for installation. + for (Archive a : suggestedDoc.getArchives()) { + if (a.isCompatible()) { + insertArchive(a, + archives, + null /*selectedArchives*/, + remotePkgs, + remoteSources, + localArchives, + true /*automated*/); + } + } + } + + if (suggestedBuildTool != null) { + // We should suggest this package for installation. + for (Archive a : suggestedBuildTool.getArchives()) { + if (a.isCompatible()) { + insertArchive(a, + archives, + null /*selectedArchives*/, + remotePkgs, + remoteSources, + localArchives, + true /*automated*/); + } + } + } + } + + /** + * Create a array of {@link ArchiveInfo} based on all local (already installed) + * packages. The array is always non-null but may be empty. + *

+ * The local {@link ArchiveInfo} are guaranteed to have one non-null archive + * that you can retrieve using {@link ArchiveInfo#getNewArchive()}. + */ + public ArchiveInfo[] createLocalArchives(Package[] localPkgs) { + + if (localPkgs != null) { + ArrayList list = new ArrayList(); + for (Package p : localPkgs) { + // Only accept packages that have one compatible archive. + // Local package should have 1 and only 1 compatible archive anyway. + for (Archive a : p.getArchives()) { + if (a != null && a.isCompatible()) { + // We create an "installed" archive info to wrap the local package. + // Note that dependencies are not computed since right now we don't + // deal with more than one level of dependencies and installed archives + // are deemed implicitly accepted anyway. + list.add(new LocalArchiveInfo(a)); + } + } + } + + return list.toArray(new ArchiveInfo[list.size()]); + } + + return new ArchiveInfo[0]; + } + + /** + * Find suitable updates to all current local packages. + *

+ * Returns a list of potential updates for *existing* packages. This does NOT solve + * dependencies for the new packages. + *

+ * Always returns a non-null collection, which can be empty. + */ + private Collection findUpdates( + ArchiveInfo[] localArchives, + Collection remotePkgs, + SdkSource[] remoteSources, + boolean includeAll) { + ArrayList updates = new ArrayList(); + + fetchRemotePackages(remotePkgs, remoteSources); + + for (ArchiveInfo ai : localArchives) { + Archive na = ai.getNewArchive(); + if (na == null) { + continue; + } + Package localPkg = na.getParentPackage(); + + for (Package remotePkg : remotePkgs) { + // Only look for non-obsolete updates unless requested to include them + if ((includeAll || !remotePkg.isObsolete()) && + localPkg.canBeUpdatedBy(remotePkg) == UpdateInfo.UPDATE) { + // Found a suitable update. Only accept the remote package + // if it provides at least one compatible archive + + addArchives: + for (Archive a : remotePkg.getArchives()) { + if (a.isCompatible()) { + + // If we're trying to add a package for revision N, + // make sure we don't also have a package for revision N-1. + for (int i = updates.size() - 1; i >= 0; i--) { + Package pkgFound = updates.get(i).getParentPackage(); + if (pkgFound.canBeUpdatedBy(remotePkg) == UpdateInfo.UPDATE) { + // This package can update one we selected earlier. + // Remove the one that can be updated by this new one. + updates.remove(i); + } else if (remotePkg.canBeUpdatedBy(pkgFound) == + UpdateInfo.UPDATE) { + // There is a package in the list that is already better + // than the one we want to add, so don't add it. + break addArchives; + } + } + + updates.add(a); + break; + } + } + } + } + } + + return updates; + } + + /** + * Check all local archives which are NOT being updated and see if they + * miss any dependency. If they do, try to fix that dependency by selecting + * an appropriate package. + */ + private void fixMissingLocalDependencies( + Collection outArchives, + Collection selectedArchives, + Collection remotePkgs, + SdkSource[] remoteSources, + ArchiveInfo[] localArchives) { + + nextLocalArchive: for (ArchiveInfo ai : localArchives) { + Archive a = ai.getNewArchive(); + Package p = a == null ? null : a.getParentPackage(); + if (p == null) { + continue; + } + + // Is this local archive being updated? + for (ArchiveInfo ai2 : outArchives) { + if (ai2.getReplaced() == a) { + // this new archive will replace the current local one, + // so we don't have to care about fixing dependencies (since the + // new archive should already have had its dependencies resolved) + continue nextLocalArchive; + } + } + + // find dependencies for the local archive and add them as needed + // to the outArchives collection. + ArchiveInfo[] deps = findDependency(p, + outArchives, + selectedArchives, + remotePkgs, + remoteSources, + localArchives); + + if (deps != null) { + // The already installed archive has a missing dependency, which we + // just selected for install. Make sure we remember the dependency + // so that we can enforce it later in the UI. + for (ArchiveInfo aid : deps) { + aid.addDependencyFor(ai); + } + } + } + } + + private ArchiveInfo insertArchive(Archive archive, + Collection outArchives, + Collection selectedArchives, + Collection remotePkgs, + SdkSource[] remoteSources, + ArchiveInfo[] localArchives, + boolean automated) { + Package p = archive.getParentPackage(); + + // Is this an update? + Archive updatedArchive = null; + for (ArchiveInfo ai : localArchives) { + Archive a = ai.getNewArchive(); + if (a != null) { + Package lp = a.getParentPackage(); + + if (lp.canBeUpdatedBy(p) == UpdateInfo.UPDATE) { + updatedArchive = a; + } + } + } + + // Find dependencies and adds them as needed to outArchives + ArchiveInfo[] deps = findDependency(p, + outArchives, + selectedArchives, + remotePkgs, + remoteSources, + localArchives); + + // Make sure it's not a dup + ArchiveInfo ai = null; + + for (ArchiveInfo ai2 : outArchives) { + Archive a2 = ai2.getNewArchive(); + if (a2 != null && a2.getParentPackage().sameItemAs(archive.getParentPackage())) { + ai = ai2; + break; + } + } + + if (ai == null) { + ai = new ArchiveInfo( + archive, //newArchive + updatedArchive, //replaced + deps //dependsOn + ); + outArchives.add(ai); + } + + if (deps != null) { + for (ArchiveInfo d : deps) { + d.addDependencyFor(ai); + } + } + + return ai; + } + + /** + * Resolves dependencies for a given package. + * + * Returns null if no dependencies were found. + * Otherwise return an array of {@link ArchiveInfo}, which is guaranteed to have + * at least size 1 and contain no null elements. + */ + private ArchiveInfo[] findDependency(Package pkg, + Collection outArchives, + Collection selectedArchives, + Collection remotePkgs, + SdkSource[] remoteSources, + ArchiveInfo[] localArchives) { + + // Current dependencies can be: + // - addon: *always* depends on platform of same API level + // - platform: *might* depends on tools of rev >= min-tools-rev + // - extra: *might* depends on platform with api >= min-api-level + + Set aiFound = new HashSet(); + + if (pkg instanceof IPlatformDependency) { + ArchiveInfo ai = findPlatformDependency( + (IPlatformDependency) pkg, + outArchives, + selectedArchives, + remotePkgs, + remoteSources, + localArchives); + + if (ai != null) { + aiFound.add(ai); + } + } + + if (pkg instanceof IMinToolsDependency) { + + ArchiveInfo ai = findToolsDependency( + (IMinToolsDependency) pkg, + outArchives, + selectedArchives, + remotePkgs, + remoteSources, + localArchives); + + if (ai != null) { + aiFound.add(ai); + } + } + + if (pkg instanceof IMinPlatformToolsDependency) { + + ArchiveInfo ai = findPlatformToolsDependency( + (IMinPlatformToolsDependency) pkg, + outArchives, + selectedArchives, + remotePkgs, + remoteSources, + localArchives); + + if (ai != null) { + aiFound.add(ai); + } + } + + if (pkg instanceof IMinApiLevelDependency) { + + ArchiveInfo ai = findMinApiLevelDependency( + (IMinApiLevelDependency) pkg, + outArchives, + selectedArchives, + remotePkgs, + remoteSources, + localArchives); + + if (ai != null) { + aiFound.add(ai); + } + } + + if (pkg instanceof IExactApiLevelDependency) { + + ArchiveInfo ai = findExactApiLevelDependency( + (IExactApiLevelDependency) pkg, + outArchives, + selectedArchives, + remotePkgs, + remoteSources, + localArchives); + + if (ai != null) { + aiFound.add(ai); + } + } + + if (!aiFound.isEmpty()) { + ArchiveInfo[] result = aiFound.toArray(new ArchiveInfo[aiFound.size()]); + Arrays.sort(result); + return result; + } + + return null; + } + + /** + * Resolves dependencies on tools. + * + * A platform or an extra package can both have a min-tools-rev, in which case it + * depends on having a tools package of the requested revision. + * Finds the tools dependency. If found, add it to the list of things to install. + * Returns the archive info dependency, if any. + */ + public ArchiveInfo findToolsDependency( + IMinToolsDependency pkg, + Collection outArchives, + Collection selectedArchives, + Collection remotePkgs, + SdkSource[] remoteSources, + ArchiveInfo[] localArchives) { + // This is the requirement to match. + FullRevision rev = pkg.getMinToolsRevision(); + + if (rev.equals(MinToolsPackage.MIN_TOOLS_REV_NOT_SPECIFIED)) { + // Well actually there's no requirement. + return null; + } + + // First look in locally installed packages. + for (ArchiveInfo ai : localArchives) { + Archive a = ai.getNewArchive(); + if (a != null) { + Package p = a.getParentPackage(); + if (p instanceof ToolPackage) { + if (((ToolPackage) p).getRevision().compareTo(rev) >= 0) { + // We found one already installed. + return null; + } + } + } + } + + // Look in archives already scheduled for install + for (ArchiveInfo ai : outArchives) { + Archive a = ai.getNewArchive(); + if (a != null) { + Package p = a.getParentPackage(); + if (p instanceof ToolPackage) { + if (((ToolPackage) p).getRevision().compareTo(rev) >= 0) { + // The dependency is already scheduled for install, nothing else to do. + return ai; + } + } + } + } + + // Otherwise look in the selected archives. + if (selectedArchives != null) { + for (Archive a : selectedArchives) { + Package p = a.getParentPackage(); + if (p instanceof ToolPackage) { + if (((ToolPackage) p).getRevision().compareTo(rev) >= 0) { + // It's not already in the list of things to install, so add it now + return insertArchive(a, + outArchives, + selectedArchives, + remotePkgs, + remoteSources, + localArchives, + true /*automated*/); + } + } + } + } + + // Finally nothing matched, so let's look at all available remote packages + fetchRemotePackages(remotePkgs, remoteSources); + FullRevision localRev = rev; + Archive localArch = null; + for (Package p : remotePkgs) { + if (p instanceof ToolPackage) { + FullRevision r = ((ToolPackage) p).getRevision(); + if (r.compareTo(localRev) >= 0) { + // It's not already in the list of things to install, so add the + // first compatible archive we can find. + for (Archive a : p.getArchives()) { + if (a.isCompatible()) { + localRev = r; + localArch = a; + break; + } + } + } + } + } + if (localArch != null) { + return insertArchive(localArch, + outArchives, + selectedArchives, + remotePkgs, + remoteSources, + localArchives, + true /*automated*/); + + } + + // We end up here if nothing matches. We don't have a good platform to match. + // We need to indicate this extra depends on a missing platform archive + // so that it can be impossible to install later on. + return new MissingArchiveInfo(MissingArchiveInfo.TITLE_TOOL, rev); + } + + /** + * Resolves dependencies on platform-tools. + * + * A tool package can have a min-platform-tools-rev, in which case it depends on + * having a platform-tool package of the requested revision. + * Finds the platform-tool dependency. If found, add it to the list of things to install. + * Returns the archive info dependency, if any. + */ + public ArchiveInfo findPlatformToolsDependency( + IMinPlatformToolsDependency pkg, + Collection outArchives, + Collection selectedArchives, + Collection remotePkgs, + SdkSource[] remoteSources, + ArchiveInfo[] localArchives) { + // This is the requirement to match. + FullRevision rev = pkg.getMinPlatformToolsRevision(); + boolean findMax = false; + int compareThreshold = 0; + ArchiveInfo aiMax = null; + Archive aMax = null; + + if (rev.equals(IMinPlatformToolsDependency.MIN_PLATFORM_TOOLS_REV_INVALID)) { + // The requirement is invalid, which is not supposed to happen since this + // property is mandatory. However in a typical upgrade scenario we can end + // up with the previous updater managing a new package and not dealing + // correctly with the new unknown property. + // So instead we parse all the existing and remote packages and try to find + // the max available revision and we'll use it. + findMax = true; + // When findMax is false, we want r.compareTo(rev) >= 0. + // When findMax is true, we want r.compareTo(rev) > 0 (so >= 1). + compareThreshold = 1; + } + + // First look in locally installed packages. + for (ArchiveInfo ai : localArchives) { + Archive a = ai.getNewArchive(); + if (a != null) { + Package p = a.getParentPackage(); + if (p instanceof PlatformToolPackage) { + FullRevision r = ((PlatformToolPackage) p).getRevision(); + if (findMax && r.compareTo(rev) > compareThreshold) { + rev = r; + aiMax = ai; + } else if (!findMax && r.compareTo(rev) >= compareThreshold) { + // We found one already installed. + return null; + } + } + } + } + + // Because of previews, we can have more than 1 choice, so get the local max. + FullRevision localRev = rev; + ArchiveInfo localAiMax = null; + // Look in archives already scheduled for install + for (ArchiveInfo ai : outArchives) { + Archive a = ai.getNewArchive(); + if (a != null) { + Package p = a.getParentPackage(); + if (p instanceof PlatformToolPackage) { + FullRevision r = ((PlatformToolPackage) p).getRevision(); + // If computing dependencies for a non-preview package, don't offer preview dependencies + if (r.isPreview() && !rev.isPreview()) { + continue; + } + if (r.compareTo(localRev) >= compareThreshold) { + localRev = r; + localAiMax = ai; + } + } + } + } + if (localAiMax != null) { + if (findMax) { + rev = localRev; + aiMax = localAiMax; + } else { + // The dependency is already scheduled for install, nothing else to do. + return localAiMax; + } + } + + + // Otherwise look in the selected archives. + localRev = rev; + Archive localAMax = null; + if (selectedArchives != null) { + for (Archive a : selectedArchives) { + Package p = a.getParentPackage(); + if (p instanceof PlatformToolPackage) { + FullRevision r = ((PlatformToolPackage) p).getRevision(); + // If computing dependencies for a non-preview package, don't offer preview dependencies + if (r.isPreview() && !rev.isPreview()) { + continue; + } + + if (r.compareTo(localRev) >= compareThreshold) { + localRev = r; + localAiMax = null; + localAMax = a; + } + } + } + if (localAMax != null) { + if (findMax) { + rev = localRev; + aiMax = null; + aMax = localAMax; + } else { + // It's not already in the list of things to install, so add it now + return insertArchive(localAMax, + outArchives, + selectedArchives, + remotePkgs, + remoteSources, + localArchives, + true /*automated*/); + } + } + } + + // Finally nothing matched, so let's look at all available remote packages + fetchRemotePackages(remotePkgs, remoteSources); + localRev = rev; + localAMax = null; + for (Package p : remotePkgs) { + if (p instanceof PlatformToolPackage) { + FullRevision r = ((PlatformToolPackage) p).getRevision(); + // If computing dependencies for a non-preview package, don't offer preview dependencies + if (r.isPreview() && !rev.isPreview()) { + continue; + } + + if (r.compareTo(rev) >= 0) { + // Make sure there's at least one valid archive here + for (Archive a : p.getArchives()) { + if (a.isCompatible()) { + if (r.compareTo(localRev) >= compareThreshold) { + localRev = r; + localAiMax = null; + localAMax = a; + break; + } + } + } + } + } + } + if (localAMax != null) { + if (findMax) { + rev = localRev; + aiMax = null; + aMax = localAMax; + } else { + // It's not already in the list of things to install, so add the + // first compatible archive we can find. + return insertArchive(localAMax, + outArchives, + selectedArchives, + remotePkgs, + remoteSources, + localArchives, + true /*automated*/); + } + } + + if (findMax) { + if (aMax != null) { + return insertArchive(aMax, + outArchives, + selectedArchives, + remotePkgs, + remoteSources, + localArchives, + true /*automated*/); + } else if (aiMax != null) { + return aiMax; + } + } + + // We end up here if nothing matches. We don't have a good platform to match. + // We need to indicate this package depends on a missing platform archive + // so that it can be impossible to install later on. + return new MissingArchiveInfo(MissingArchiveInfo.TITLE_PLATFORM_TOOL, rev); + } + + /** + * Resolves dependencies on platform for an add-on. + * Resolves dependencies on a system-image on its base platform or add-on. + * + * An add-on depends on having a platform with the same API level. + * + * Finds the platform dependency. If found, add it to the list of things to install. + * Returns the archive info dependency, if any. + */ + public ArchiveInfo findPlatformDependency( + IPlatformDependency pkg, + Collection outArchives, + Collection selectedArchives, + Collection remotePkgs, + SdkSource[] remoteSources, + ArchiveInfo[] localArchives) { + // This is the requirement to match. + AndroidVersion v = pkg.getAndroidVersion(); + + + // The dependency package must be a PlatformPackage or an AddonPackage. + // For an add-on, we also need to have the same vendor and name-id. + Class expectedClass = PlatformPackage.class; + IdDisplay addonVendor = null; + IdDisplay addonTag = null; + if (pkg instanceof SystemImagePackage && !((SystemImagePackage) pkg).isPlatform()) { + expectedClass = AddonPackage.class; + addonVendor = ((SystemImagePackage) pkg).getAddonVendor(); + addonTag = ((SystemImagePackage) pkg).getTag(); + } + + // Find a platform or addon that would satisfy the requirement. + + // First look in locally installed packages. + for (ArchiveInfo ai : localArchives) { + Archive a = ai.getNewArchive(); + if (a != null) { + Package p = a.getParentPackage(); + if (expectedClass.isInstance(p)) { + if (v.equals(((IAndroidVersionProvider) p).getAndroidVersion())) { + if (addonVendor != null && addonTag != null && p instanceof AddonPackage) { + if (!((AddonPackage) p).getVendorId().equals(addonVendor.getId()) || + !((AddonPackage) p).getNameId().equals(addonTag.getId())) { + continue; + } + } + // We found one already installed. + return null; + } + } + } + } + + // Look in archives already scheduled for install + for (ArchiveInfo ai : outArchives) { + Archive a = ai.getNewArchive(); + if (a != null) { + Package p = a.getParentPackage(); + if (expectedClass.isInstance(p)) { + if (v.equals(((IAndroidVersionProvider) p).getAndroidVersion())) { + if (addonVendor != null && addonTag != null && p instanceof AddonPackage) { + if (!((AddonPackage) p).getVendorId().equals(addonVendor.getId()) || + !((AddonPackage) p).getNameId().equals(addonTag.getId())) { + continue; + } + } + // The dependency is already scheduled for install, nothing else to do. + return ai; + } + } + } + } + + // Otherwise look in the selected archives. + if (selectedArchives != null) { + for (Archive a : selectedArchives) { + Package p = a.getParentPackage(); + if (expectedClass.isInstance(p)) { + if (v.equals(((IAndroidVersionProvider) p).getAndroidVersion())) { + if (addonVendor != null && addonTag != null && p instanceof AddonPackage) { + if (!((AddonPackage) p).getVendorId().equals(addonVendor.getId()) || + !((AddonPackage) p).getNameId().equals(addonTag.getId())) { + continue; + } + } + // It's not already in the list of things to install, so add it now + return insertArchive(a, + outArchives, + selectedArchives, + remotePkgs, + remoteSources, + localArchives, + true /*automated*/); + } + } + } + } + + // Finally nothing matched, so let's look at all available remote packages + fetchRemotePackages(remotePkgs, remoteSources); + for (Package p : remotePkgs) { + if (expectedClass.isInstance(p)) { + if (v.equals(((IAndroidVersionProvider) p).getAndroidVersion())) { + if (addonVendor != null && addonTag != null && p instanceof AddonPackage) { + if (!((AddonPackage) p).getVendorId().equals(addonVendor.getId()) || + !((AddonPackage) p).getNameId().equals(addonTag.getId())) { + continue; + } + } + // It's not already in the list of things to install, so add the + // first compatible archive we can find. + for (Archive a : p.getArchives()) { + if (a.isCompatible()) { + return insertArchive(a, + outArchives, + selectedArchives, + remotePkgs, + remoteSources, + localArchives, + true /*automated*/); + } + } + } + } + } + + // We end up here if nothing matches. We don't have a good platform to match. + // We need to indicate this addon depends on a missing platform archive + // so that it can be impossible to install later on. + return new MissingPlatformArchiveInfo(pkg.getAndroidVersion()); + } + + /** + * Resolves platform dependencies for extras. + * An extra depends on having a platform with a minimun API level. + * + * We try to return the highest API level available above the specified minimum. + * Note that installed packages have priority so if one installed platform satisfies + * the dependency, we'll use it even if there's a higher API platform available but + * not installed yet. + * + * Finds the platform dependency. If found, add it to the list of things to install. + * Returns the archive info dependency, if any. + */ + protected ArchiveInfo findMinApiLevelDependency( + IMinApiLevelDependency pkg, + Collection outArchives, + Collection selectedArchives, + Collection remotePkgs, + SdkSource[] remoteSources, + ArchiveInfo[] localArchives) { + + int api = pkg.getMinApiLevel(); + + if (api == IMinApiLevelDependency.MIN_API_LEVEL_NOT_SPECIFIED) { + return null; + } + + // Find a platform that would satisfy the requirement. + + // First look in locally installed packages. + for (ArchiveInfo ai : localArchives) { + Archive a = ai.getNewArchive(); + if (a != null) { + Package p = a.getParentPackage(); + if (p instanceof PlatformPackage) { + if (((PlatformPackage) p).getAndroidVersion().isGreaterOrEqualThan(api)) { + // We found one already installed. + return null; + } + } + } + } + + // Look in archives already scheduled for install + int foundApi = 0; + ArchiveInfo foundAi = null; + + for (ArchiveInfo ai : outArchives) { + Archive a = ai.getNewArchive(); + if (a != null) { + Package p = a.getParentPackage(); + if (p instanceof PlatformPackage) { + if (((PlatformPackage) p).getAndroidVersion().isGreaterOrEqualThan(api)) { + if (api > foundApi) { + foundApi = api; + foundAi = ai; + } + } + } + } + } + + if (foundAi != null) { + // The dependency is already scheduled for install, nothing else to do. + return foundAi; + } + + // Otherwise look in the selected archives *or* available remote packages + // and takes the best out of the two sets. + foundApi = 0; + Archive foundArchive = null; + if (selectedArchives != null) { + for (Archive a : selectedArchives) { + Package p = a.getParentPackage(); + if (p instanceof PlatformPackage) { + if (((PlatformPackage) p).getAndroidVersion().isGreaterOrEqualThan(api)) { + if (api > foundApi) { + foundApi = api; + foundArchive = a; + } + } + } + } + } + + // Finally nothing matched, so let's look at all available remote packages + fetchRemotePackages(remotePkgs, remoteSources); + for (Package p : remotePkgs) { + if (p instanceof PlatformPackage) { + if (((PlatformPackage) p).getAndroidVersion().isGreaterOrEqualThan(api)) { + if (api > foundApi) { + // It's not already in the list of things to install, so add the + // first compatible archive we can find. + for (Archive a : p.getArchives()) { + if (a.isCompatible()) { + foundApi = api; + foundArchive = a; + } + } + } + } + } + } + + if (foundArchive != null) { + // It's not already in the list of things to install, so add it now + return insertArchive(foundArchive, + outArchives, + selectedArchives, + remotePkgs, + remoteSources, + localArchives, + true /*automated*/); + } + + // We end up here if nothing matches. We don't have a good platform to match. + // We need to indicate this extra depends on a missing platform archive + // so that it can be impossible to install later on. + return new MissingPlatformArchiveInfo(new AndroidVersion(api, null /*codename*/)); + } + + /** + * Resolves platform dependencies for add-ons. + * An add-ons depends on having a platform with an exact specific API level. + * + * Finds the platform dependency. If found, add it to the list of things to install. + * Returns the archive info dependency, if any. + */ + public ArchiveInfo findExactApiLevelDependency( + IExactApiLevelDependency pkg, + Collection outArchives, + Collection selectedArchives, + Collection remotePkgs, + SdkSource[] remoteSources, + ArchiveInfo[] localArchives) { + + int api = pkg.getExactApiLevel(); + + if (api == IExactApiLevelDependency.API_LEVEL_INVALID) { + return null; + } + + // Find a platform that would satisfy the requirement. + + // First look in locally installed packages. + for (ArchiveInfo ai : localArchives) { + Archive a = ai.getNewArchive(); + if (a != null) { + Package p = a.getParentPackage(); + if (p instanceof PlatformPackage) { + if (((PlatformPackage) p).getAndroidVersion().equals(api)) { + // We found one already installed. + return null; + } + } + } + } + + // Look in archives already scheduled for install + + for (ArchiveInfo ai : outArchives) { + Archive a = ai.getNewArchive(); + if (a != null) { + Package p = a.getParentPackage(); + if (p instanceof PlatformPackage) { + if (((PlatformPackage) p).getAndroidVersion().equals(api)) { + return ai; + } + } + } + } + + // Otherwise look in the selected archives. + if (selectedArchives != null) { + for (Archive a : selectedArchives) { + Package p = a.getParentPackage(); + if (p instanceof PlatformPackage) { + if (((PlatformPackage) p).getAndroidVersion().equals(api)) { + // It's not already in the list of things to install, so add it now + return insertArchive(a, + outArchives, + selectedArchives, + remotePkgs, + remoteSources, + localArchives, + true /*automated*/); + } + } + } + } + + // Finally nothing matched, so let's look at all available remote packages + fetchRemotePackages(remotePkgs, remoteSources); + for (Package p : remotePkgs) { + if (p instanceof PlatformPackage) { + if (((PlatformPackage) p).getAndroidVersion().equals(api)) { + // It's not already in the list of things to install, so add the + // first compatible archive we can find. + for (Archive a : p.getArchives()) { + if (a.isCompatible()) { + return insertArchive(a, + outArchives, + selectedArchives, + remotePkgs, + remoteSources, + localArchives, + true /*automated*/); + } + } + } + } + } + + // We end up here if nothing matches. We don't have a good platform to match. + // We need to indicate this extra depends on a missing platform archive + // so that it can be impossible to install later on. + return new MissingPlatformArchiveInfo(new AndroidVersion(api, null /*codename*/)); + } + + /** + * Fetch all remote packages only if really needed. + *

+ * This method takes a list of sources. Each source is only fetched once -- that is each + * source keeps the list of packages that we fetched from the remote XML file. If the list + * is null, it means this source has never been fetched so we'll do it once here. Otherwise + * we rely on the cached list of packages from this source. + *

+ * This method also takes a remote package list as input, which it will fill out. + * If a source has already been fetched, we'll add its packages to the remote package list + * if they are not already present. Otherwise, the source will be fetched and the packages + * added to the list. + * + * @param remotePkgs An in-out list of packages available from remote sources. + * This list must not be null. + * It can be empty or already contain some packages. + * @param remoteSources A list of available remote sources to fetch from. + */ + protected void fetchRemotePackages( + final Collection remotePkgs, + final SdkSource[] remoteSources) { + if (!remotePkgs.isEmpty()) { + return; + } + + // First check if there's any remote source we need to fetch. + // This will bring the task window, so we rather not display it unless + // necessary. + boolean needsFetch = false; + for (final SdkSource remoteSrc : remoteSources) { + Package[] pkgs = remoteSrc.getPackages(); + if (pkgs == null) { + // This source has never been fetched. We'll do it below. + needsFetch = true; + } else { + // This source has already been fetched and we know its package list. + // We still need to make sure all of its packages are present in the + // remotePkgs list. + + nextPackage: for (Package pkg : pkgs) { + for (Archive a : pkg.getArchives()) { + // Only add a package if it contains at least one compatible archive + // and is not already in the remote package list. + if (a.isCompatible()) { + if (!remotePkgs.contains(pkg)) { + remotePkgs.add(pkg); + continue nextPackage; + } + } + } + } + } + } + + if (!needsFetch) { + return; + } + + final boolean forceHttp = mUpdaterData.getSettingsController().getSettings().getForceHttp(); + + mUpdaterData.getTaskFactory().start("Refresh Sources", new ITask() { + @Override + public void run(ITaskMonitor monitor) { + for (SdkSource remoteSrc : remoteSources) { + Package[] pkgs = remoteSrc.getPackages(); + + if (pkgs == null) { + remoteSrc.load(mUpdaterData.getDownloadCache(), monitor, forceHttp); + pkgs = remoteSrc.getPackages(); + } + + if (pkgs != null) { + nextPackage: for (Package pkg : pkgs) { + for (Archive a : pkg.getArchives()) { + // Only add a package if it contains at least one compatible archive + // and is not already in the remote package list. + if (a.isCompatible()) { + if (!remotePkgs.contains(pkg)) { + remotePkgs.add(pkg); + continue nextPackage; + } + } + } + } + } + } + } + }); + } + + + /** + * A {@link LocalArchiveInfo} is an {@link ArchiveInfo} that wraps an already installed + * "local" package/archive. + *

+ * In this case, the "new Archive" is still expected to be non null and the + * "replaced Archive" is null. Installed archives are always accepted and never + * rejected. + *

+ * Dependencies are not set. + */ + private static class LocalArchiveInfo extends ArchiveInfo { + + public LocalArchiveInfo(Archive localArchive) { + super(localArchive, null /*replaced*/, null /*dependsOn*/); + } + + /** Installed archives are always accepted. */ + @Override + public boolean isAccepted() { + return true; + } + + /** Installed archives are never rejected. */ + @Override + public boolean isRejected() { + return false; + } + } + + /** + * A {@link MissingPlatformArchiveInfo} is an {@link ArchiveInfo} that represents a + * package/archive that we really need as a dependency but that we don't have. + *

+ * This is currently used for addons and extras in case we can't find a matching base platform. + *

+ * This kind of archive has specific properties: the new archive to install is null, + * there are no dependencies and no archive is being replaced. The info can never be + * accepted and is always rejected. + */ + private static class MissingPlatformArchiveInfo extends ArchiveInfo { + + private final AndroidVersion mVersion; + + /** + * Constructs a {@link MissingPlatformArchiveInfo} that will indicate the + * given platform version is missing. + */ + public MissingPlatformArchiveInfo(AndroidVersion version) { + super(null /*newArchive*/, null /*replaced*/, null /*dependsOn*/); + mVersion = version; + } + + /** Missing archives are never accepted. */ + @Override + public boolean isAccepted() { + return false; + } + + /** Missing archives are always rejected. */ + @Override + public boolean isRejected() { + return true; + } + + @Override + public String getShortDescription() { + return String.format("Missing SDK Platform Android%1$s, API %2$d", + mVersion.isPreview() ? " Preview" : "", + mVersion.getApiLevel()); + } + } + + /** + * A {@link MissingArchiveInfo} is an {@link ArchiveInfo} that represents a + * package/archive that we really need as a dependency but that we don't have. + *

+ * This is currently used for extras in case we can't find a matching tool revision + * or when a platform-tool is missing. + *

+ * This kind of archive has specific properties: the new archive to install is null, + * there are no dependencies and no archive is being replaced. The info can never be + * accepted and is always rejected. + */ + private static class MissingArchiveInfo extends ArchiveInfo { + + private final FullRevision mRevision; + private final String mTitle; + + public static final String TITLE_TOOL = "Tools"; + public static final String TITLE_PLATFORM_TOOL = "Platform-tools"; + + /** + * Constructs a {@link MissingPlatformArchiveInfo} that will indicate the + * given platform version is missing. + * + * @param title Typically "Tools" or "Platform-tools". + * @param revision The required revision. + */ + public MissingArchiveInfo(String title, FullRevision revision) { + super(null /*newArchive*/, null /*replaced*/, null /*dependsOn*/); + mTitle = title; + mRevision = revision; + } + + /** Missing archives are never accepted. */ + @Override + public boolean isAccepted() { + return false; + } + + /** Missing archives are always rejected. */ + @Override + public boolean isRejected() { + return true; + } + + @Override + public String getShortDescription() { + return String.format("Missing Android SDK %1$s, revision %2$s", + mTitle, + mRevision.toShortString()); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/SdkUpdaterNoWindow.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/SdkUpdaterNoWindow.java new file mode 100644 index 00000000..8183e1bc --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/SdkUpdaterNoWindow.java @@ -0,0 +1,715 @@ +/* + * Copyright (C) 2010 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.sdklib.internal.repository.updater; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.SdkManager; +import com.android.sdklib.internal.repository.ITask; +import com.android.sdklib.internal.repository.ITaskFactory; +import com.android.sdklib.internal.repository.ITaskMonitor; +import com.android.sdklib.internal.repository.NullTaskMonitor; +import com.android.sdklib.internal.repository.UserCredentials; +import com.android.sdklib.internal.repository.archives.Archive; +import com.android.sdklib.repository.SdkRepoConstants; +import com.android.utils.ILogger; +import com.android.utils.IReaderLogger; +import com.android.utils.NullLogger; +import com.android.utils.Pair; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +/** + * Performs an update using only a non-interactive console output with no GUI. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class SdkUpdaterNoWindow { + + /** The {@link UpdaterData} to use. */ + private final UpdaterData mUpdaterData; + /** The {@link ILogger} logger to use. */ + private final ILogger mSdkLog; + /** The reply to any question asked by the update process. Currently this will + * be yes/no for ability to replace modified samples or restart ADB. */ + private final boolean mForce; + + /** + * Creates an UpdateNoWindow object that will update using the given SDK root + * and outputs to the given SDK logger. + * + * @param osSdkRoot The OS path of the SDK folder to update. + * @param sdkManager An existing SDK manager to list current platforms and addons. + * @param sdkLog A logger object, that should ideally output to a write-only console. + * @param force The reply to any question asked by the update process. Currently this will + * be yes/no for ability to replace modified samples or restart ADB. + * @param useHttp True to force using HTTP instead of HTTPS for downloads. + * @param proxyPort An optional HTTP/HTTPS proxy port. Can be null. + * @param proxyHost An optional HTTP/HTTPS proxy host. Can be null. + */ + public SdkUpdaterNoWindow(String osSdkRoot, + SdkManager sdkManager, + ILogger sdkLog, + boolean force, + boolean useHttp, + String proxyHost, + String proxyPort) { + mSdkLog = sdkLog; + mForce = force; + mUpdaterData = new UpdaterData(osSdkRoot, sdkLog); + + // Read and apply settings from settings file, so that http/https proxy is set + // and let the command line args override them as necessary. + SettingsController settingsController = mUpdaterData.getSettingsController(); + settingsController.loadSettings(); + settingsController.applySettings(); + setupProxy(proxyHost, proxyPort); + + // Change the in-memory settings to force the http/https mode + settingsController.setSetting(ISettingsPage.KEY_FORCE_HTTP, useHttp); + + // Use a factory that only outputs to the given ILogger. + mUpdaterData.setTaskFactory(new ConsoleTaskFactory()); + + // Check that the AVD Manager has been correctly initialized. This is done separately + // from the constructor in the GUI-based UpdaterWindowImpl to give time to the UI to + // initialize before displaying a message box. Since we don't have any GUI here + // we can call it whenever we want. + if (mUpdaterData.checkIfInitFailed()) { + return; + } + + // Setup the default sources including the getenv overrides. + mUpdaterData.setupDefaultSources(); + + mUpdaterData.getLocalSdkParser().parseSdk( + osSdkRoot, + sdkManager, + new NullTaskMonitor(sdkLog)); + } + + /** + * Performs the actual update. + * + * @param pkgFilter A list of {@link SdkRepoConstants#NODES} to limit the type of packages + * we can update. A null or empty list means to update everything possible. + * @param includeAll True to list and install all packages, including obsolete ones. + * @param dryMode True to check what would be updated/installed but do not actually + * download or install anything. + * @param acceptLicense SDK licenses to automatically accept. + * @deprecated Use {@link #updateAll(java.util.ArrayList, boolean, boolean, String, boolean)} + * instead + */ + @Deprecated + public void updateAll( + ArrayList pkgFilter, + boolean includeAll, + boolean dryMode, + String acceptLicense) { + updateAll(pkgFilter, includeAll, dryMode, acceptLicense, false); + } + + /** + * Performs the actual update. + * + * @param pkgFilter A list of {@link SdkRepoConstants#NODES} to limit the type of packages + * we can update. A null or empty list means to update everything possible. + * @param includeAll True to list and install all packages, including obsolete ones. + * @param dryMode True to check what would be updated/installed but do not actually + * download or install anything. + * @param acceptLicense SDK licenses to automatically accept. + * @param includeDependencies If true, also include any required dependencies + */ + public void updateAll( + ArrayList pkgFilter, + boolean includeAll, + boolean dryMode, + String acceptLicense, + boolean includeDependencies) { + mUpdaterData.updateOrInstallAll_NoGUI(pkgFilter, includeAll, dryMode, acceptLicense, + includeDependencies); + } + + /** + * Lists remote packages available for install using 'android update sdk --no-ui'. + * + * @param includeAll True to list and install all packages, including obsolete ones. + * @param extendedOutput True to display more details on each package. + */ + public void listRemotePackages(boolean includeAll, boolean extendedOutput) { + mUpdaterData.listRemotePackages_NoGUI(includeAll, extendedOutput); + } + + /** + * Installs a platform package given its target hash string. + *

+ * This does not work for add-ons right now, just basic platforms. + * + * @param hashString The hash string of the platform to install. + * @return A boolean indicating whether the installation was successful (meaning the package + * was either already present, or got installed or updated properly) and a {@link File} + * with the path to the root folder of the package. The file is null when the boolean + * is false, otherwise it should point to an existing valid folder. + */ + public Pair installPlatformPackage(String hashString) { + + // TODO right now we really need the caller to use a reader-logger to + // handle license confirmations. This isn't optimal and should be addressed + // when we provide a proper UI for this. + assert mSdkLog instanceof IReaderLogger; + + SdkManager sm = mUpdaterData.getSdkManager(); + IAndroidTarget target = sm.getTargetFromHashString(hashString); + + if (target == null) { + // Otherwise try to install it. + // This currently only works for platforms since the package's "install id" + // is an exactly match with the IAndroidTarget hash string. + ArrayList filter = new ArrayList(); + filter.add(hashString); + List installed = mUpdaterData.updateOrInstallAll_NoGUI( + filter, + true, //includeAll + false, //dryMode + null); //acceptLicense + + if (installed != null) { + sm.reloadSdk(new NullLogger()); + target = sm.getTargetFromHashString(hashString); + } + } + + if (target != null) { + // Return existing target + return Pair.of(Boolean.TRUE, new File(target.getLocation())); + } + + return null; + } + + // ----- + + /** + * Sets both the HTTP and HTTPS proxy system properties, overriding the ones + * from the settings with these values if they are defined. + */ + private void setupProxy(String proxyHost, String proxyPort) { + + // The system property constants can be found in the Java SE documentation at + // http://download.oracle.com/javase/6/docs/technotes/guides/net/proxies.html + final String JAVA_PROP_HTTP_PROXY_HOST = "http.proxyHost"; //$NON-NLS-1$ + final String JAVA_PROP_HTTP_PROXY_PORT = "http.proxyPort"; //$NON-NLS-1$ + final String JAVA_PROP_HTTPS_PROXY_HOST = "https.proxyHost"; //$NON-NLS-1$ + final String JAVA_PROP_HTTPS_PROXY_PORT = "https.proxyPort"; //$NON-NLS-1$ + + Properties props = System.getProperties(); + + if (proxyHost != null && !proxyHost.isEmpty()) { + props.setProperty(JAVA_PROP_HTTP_PROXY_HOST, proxyHost); + props.setProperty(JAVA_PROP_HTTPS_PROXY_HOST, proxyHost); + } + if (proxyPort != null && !proxyPort.isEmpty()) { + props.setProperty(JAVA_PROP_HTTP_PROXY_PORT, proxyPort); + props.setProperty(JAVA_PROP_HTTPS_PROXY_PORT, proxyPort); + } + } + + /** + * A custom implementation of {@link ITaskFactory} that + * provides {@link ConsoleTaskMonitor} objects. + */ + private class ConsoleTaskFactory implements ITaskFactory { + @Override + public void start(String title, ITask task) { + start(title, null /*parentMonitor*/, task); + } + + @Override + public void start(String title, ITaskMonitor parentMonitor, ITask task) { + if (parentMonitor == null) { + task.run(new ConsoleTaskMonitor(title, task)); + } else { + // Use all the reminder of the parent monitor. + if (parentMonitor.getProgressMax() == 0) { + parentMonitor.setProgressMax(1); + } + + ITaskMonitor sub = parentMonitor.createSubMonitor( + parentMonitor.getProgressMax() - parentMonitor.getProgress()); + try { + task.run(sub); + } finally { + int delta = + sub.getProgressMax() - sub.getProgress(); + if (delta > 0) { + sub.incProgress(delta); + } + } + } + } + } + + /** + * A custom implementation of {@link ITaskMonitor} that defers all output to the + * super {@link SdkUpdaterNoWindow#mSdkLog}. + */ + private class ConsoleTaskMonitor implements ITaskMonitor { + + private static final double MAX_COUNT = 10000.0; + private double mIncCoef = 0; + private double mValue = 0; + private String mLastDesc = null; + private String mLastProgressBase = null; + + /** + * Creates a new {@link ConsoleTaskMonitor} with the given title. + */ + public ConsoleTaskMonitor(String title, ITask task) { + mSdkLog.info("%s:\n", title); + } + + /** + * Sets the description in the current task dialog. + */ + @Override + public void setDescription(String format, Object...args) { + + String last = mLastDesc; + String line = String.format(" " + format, args); //$NON-NLS-1$ + + // If the description contains a %, it generally indicates a recurring + // progress so we want a \r at the end. + int pos = line.indexOf('%'); + if (pos > -1) { + String base = line.trim(); + if (mLastProgressBase != null && base.startsWith(mLastProgressBase)) { + line = " " + base.substring(mLastProgressBase.length()); //$NON-NLS-1$ + } + line += '\r'; + } else { + mLastProgressBase = line.trim(); + line += '\n'; + } + + // Skip line if it's the same as the last one. + if (last != null && last.equals(line.trim())) { + return; + } + mLastDesc = line.trim(); + + // If the last line terminated with a \r but the new one doesn't, we need to + // insert a \n to avoid erasing the previous line. + if (last != null && + last.endsWith("\r") && //$NON-NLS-1$ + !line.endsWith("\r")) { //$NON-NLS-1$ + line = '\n' + line; + } + + mSdkLog.info("%s", line); //$NON-NLS-1$ + } + + @Override + public void log(String format, Object...args) { + setDescription(" " + format, args); //$NON-NLS-1$ + } + + @Override + public void logError(String format, Object...args) { + setDescription(format, args); + } + + @Override + public void logVerbose(String format, Object...args) { + // The ConsoleTask does not display verbose log messages. + } + + // --- ILogger --- + + @Override + public void error(@Nullable Throwable t, @Nullable String errorFormat, Object... args) { + mSdkLog.error(t, errorFormat, args); + } + + @Override + public void warning(@NonNull String warningFormat, Object... args) { + mSdkLog.warning(warningFormat, args); + } + + @Override + public void info(@NonNull String msgFormat, Object... args) { + mSdkLog.info(msgFormat, args); + } + + @Override + public void verbose(@NonNull String msgFormat, Object... args) { + mSdkLog.verbose(msgFormat, args); + } + + /** + * Sets the max value of the progress bar. + * + * Weird things will happen if setProgressMax is called multiple times + * *after* {@link #incProgress(int)}: we don't try to adjust it on the + * fly. + */ + @Override + public void setProgressMax(int max) { + assert max > 0; + // Always set the dialog's progress max to 10k since it only handles + // integers and we want to have a better inner granularity. Instead + // we use the max to compute a coefficient for inc deltas. + mIncCoef = max > 0 ? MAX_COUNT / max : 0; + assert mIncCoef > 0; + } + + @Override + public int getProgressMax() { + return mIncCoef > 0 ? (int) (MAX_COUNT / mIncCoef) : 0; + } + + /** + * Increments the current value of the progress bar. + */ + @Override + public void incProgress(int delta) { + if (delta > 0 && mIncCoef > 0) { + internalIncProgress(delta * mIncCoef); + } + } + + private void internalIncProgress(double realDelta) { + mValue += realDelta; + // max value is 10k, so 10k/100 == 100%. + // Experimentation shows that it is not really useful to display this + // progression since during download the description line will change. + // mSdkLog.printf(" [%3d%%]\r", ((int)mValue) / 100); + } + + /** + * Returns the current value of the progress bar, + * between 0 and up to {@link #setProgressMax(int)} - 1. + */ + @Override + public int getProgress() { + assert mIncCoef > 0; + return mIncCoef > 0 ? (int)(mValue / mIncCoef) : 0; + } + + /** + * Returns true if the "Cancel" button was selected. + */ + @Override + public boolean isCancelRequested() { + return false; + } + + /** + * Display a yes/no question dialog box. + * + * This implementation allow this to be called from any thread, it + * makes sure the dialog is opened synchronously in the ui thread. + * + * @param title The title of the dialog box + * @param message The error message + * @return true if YES was clicked. + */ + @Override + public boolean displayPrompt(final String title, final String message) { + // TODO Make it interactive if mForce==false + mSdkLog.info("\n%1$s\n%2$s\n%3$s", //$NON-NLS-1$ + title, + message, + mForce ? "--force used, will reply yes\n" : + "Note: you can use --force to override to yes.\n"); + if (mForce) { + return true; + } + + while (true) { + mSdkLog.info("%1$s", "[y/n] =>"); //$NON-NLS-1$ + try { + byte[] readBuffer = new byte[2048]; + String reply = readLine(readBuffer).trim(); + mSdkLog.info("\n"); //$NON-NLS-1$ + if (!reply.isEmpty() && reply.length() <= 3) { + char c = reply.charAt(0); + if (c == 'y' || c == 'Y') { + return true; + } else if (c == 'n' || c == 'N') { + return false; + } + } + mSdkLog.info("Unknown reply '%s'. Please use y[es]/n[o].\n"); //$NON-NLS-1$ + + } catch (IOException e) { + // Exception. Be conservative and say no. + mSdkLog.info("\n"); //$NON-NLS-1$ + return false; + } + } + } + + /** + * Displays a prompt message to the user and read two values, + * login/password. + *

+ * Asks user for login/password information. + *

+ * This method shows a question in the standard output, asking for login + * and password.
+ * Method Output:
+ * Title
+ * Message
+ * Login: (Wait for user input)
+ * Password: (Wait for user input)
+ *

+ * + * @param title The title of the iteration. + * @param message The message to be displayed. + * @return A {@link Pair} holding the entered login and password. The + * first element is always the Login, and the + * second element is always the Password. This + * method will never return null, in case of error the pair will + * be filled with empty strings. + * @see ITaskMonitor#displayLoginCredentialsPrompt(String, String) + */ + @Override + public UserCredentials displayLoginCredentialsPrompt(String title, String message) { + String login = ""; //$NON-NLS-1$ + String password = ""; //$NON-NLS-1$ + String workstation = ""; //$NON-NLS-1$ + String domain = ""; //$NON-NLS-1$ + + mSdkLog.info("\n%1$s\n%2$s", title, message); + byte[] readBuffer = new byte[2048]; + try { + mSdkLog.info("\nLogin: "); + login = readLine(readBuffer); + mSdkLog.info("\nPassword: "); + password = readLine(readBuffer); + mSdkLog.info("\nIf your proxy uses NTLM authentication, provide the following information. Leave blank otherwise."); + mSdkLog.info("\nWorkstation: "); + workstation = readLine(readBuffer); + mSdkLog.info("\nDomain: "); + domain = readLine(readBuffer); + + /* + * TODO: Implement a way to don't echo the typed password On + * Java 5 there's no simple way to do this. There's just a + * workaround which is output backspaces on each keystroke. + * A good alternative is to use Java 6 java.io.Console + */ + } catch (IOException e) { + // Reset login/pass to empty Strings. + login = ""; //$NON-NLS-1$ + password = ""; //$NON-NLS-1$ + workstation = ""; //$NON-NLS-1$ + domain = ""; //$NON-NLS-1$ + //Just print the error to console. + mSdkLog.info("\nError occurred during login/pass query: %s\n", e.getMessage()); + } + + return new UserCredentials(login, password, workstation, domain); + } + + /** + * Reads current console input in the given buffer. + * + * @param buffer Buffer to hold the user input. Must be larger than the largest + * expected input. Cannot be null. + * @return A new string. May be empty but not null. + * @throws IOException in case the buffer isn't long enough. + */ + private String readLine(byte[] buffer) throws IOException { + + int count; + if (mSdkLog instanceof IReaderLogger) { + count = ((IReaderLogger) mSdkLog).readLine(buffer); + } else { + count = System.in.read(buffer); + } + + // is the input longer than the buffer? + if (count == buffer.length && buffer[count-1] != 10) { + throw new IOException(String.format( + "Input is longer than the buffer size, (%1$s) bytes", buffer.length)); + } + + // ignore end whitespace + while (count > 0 && (buffer[count-1] == '\r' || buffer[count-1] == '\n')) { + count--; + } + + return new String(buffer, 0, count); + } + + /** + * Creates a sub-monitor that will use up to tickCount on the progress bar. + * tickCount must be 1 or more. + */ + @Override + public ITaskMonitor createSubMonitor(int tickCount) { + assert mIncCoef > 0; + assert tickCount > 0; + return new ConsoleSubTaskMonitor(this, null, mValue, tickCount * mIncCoef); + } + } + + private interface IConsoleSubTaskMonitor extends ITaskMonitor { + void subIncProgress(double realDelta); + } + + private static class ConsoleSubTaskMonitor implements IConsoleSubTaskMonitor { + + private final ConsoleTaskMonitor mRoot; + private final IConsoleSubTaskMonitor mParent; + private final double mStart; + private final double mSpan; + private double mSubValue; + private double mSubCoef; + + /** + * Creates a new sub task monitor which will work for the given range [start, start+span] + * in its parent. + * + * @param root The ProgressTask root + * @param parent The immediate parent. Can be the null or another sub task monitor. + * @param start The start value in the root's coordinates + * @param span The span value in the root's coordinates + */ + public ConsoleSubTaskMonitor(ConsoleTaskMonitor root, + IConsoleSubTaskMonitor parent, + double start, + double span) { + mRoot = root; + mParent = parent; + mStart = start; + mSpan = span; + mSubValue = start; + } + + @Override + public boolean isCancelRequested() { + return mRoot.isCancelRequested(); + } + + @Override + public void setDescription(String format, Object... args) { + mRoot.setDescription(format, args); + } + + @Override + public void log(String format, Object... args) { + mRoot.log(format, args); + } + + @Override + public void logError(String format, Object... args) { + mRoot.logError(format, args); + } + + @Override + public void logVerbose(String format, Object... args) { + mRoot.logVerbose(format, args); + } + + @Override + public void setProgressMax(int max) { + assert max > 0; + mSubCoef = max > 0 ? mSpan / max : 0; + assert mSubCoef > 0; + } + + @Override + public int getProgressMax() { + return mSubCoef > 0 ? (int) (mSpan / mSubCoef) : 0; + } + + @Override + public int getProgress() { + assert mSubCoef > 0; + return mSubCoef > 0 ? (int)((mSubValue - mStart) / mSubCoef) : 0; + } + + @Override + public void incProgress(int delta) { + if (delta > 0 && mSubCoef > 0) { + subIncProgress(delta * mSubCoef); + } + } + + @Override + public void subIncProgress(double realDelta) { + mSubValue += realDelta; + if (mParent != null) { + mParent.subIncProgress(realDelta); + } else { + mRoot.internalIncProgress(realDelta); + } + } + + @Override + public boolean displayPrompt(String title, String message) { + return mRoot.displayPrompt(title, message); + } + + @Override + public UserCredentials displayLoginCredentialsPrompt(String title, String message) { + return mRoot.displayLoginCredentialsPrompt(title, message); + } + + @Override + public ITaskMonitor createSubMonitor(int tickCount) { + assert mSubCoef > 0; + assert tickCount > 0; + return new ConsoleSubTaskMonitor(mRoot, + this, + mSubValue, + tickCount * mSubCoef); + } + + // --- ILogger --- + + @Override + public void error(@Nullable Throwable t, @Nullable String errorFormat, Object... args) { + mRoot.error(t, errorFormat, args); + } + + @Override + public void warning(@NonNull String warningFormat, Object... args) { + mRoot.warning(warningFormat, args); + } + + @Override + public void info(@NonNull String msgFormat, Object... args) { + mRoot.info(msgFormat, args); + } + + @Override + public void verbose(@NonNull String msgFormat, Object... args) { + mRoot.verbose(msgFormat, args); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/SettingsController.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/SettingsController.java new file mode 100644 index 00000000..f0204a0a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/SettingsController.java @@ -0,0 +1,392 @@ +/* + * 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.sdklib.internal.repository.updater; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.annotations.VisibleForTesting; +import com.android.annotations.VisibleForTesting.Visibility; +import com.android.prefs.AndroidLocation; +import com.android.prefs.AndroidLocation.AndroidLocationException; +import com.android.sdklib.io.LegacyFileOp; +import com.android.sdklib.io.IFileOp; +import com.android.utils.ILogger; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map.Entry; +import java.util.Properties; + +/** + * Controller class to get settings values. Settings are kept in-memory. + * Users of this class must first load the settings before changing them and save + * them when modified. + *

+ * Settings are enumerated by constants in {@link ISettingsPage}. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class SettingsController { + + private static final String SETTINGS_FILENAME = "androidtool.cfg"; //$NON-NLS-1$ + + private final IFileOp mFileOp; + private final ILogger mSdkLog; + private final Settings mSettings; + + public interface OnChangedListener { + void onSettingsChanged(@NonNull SettingsController controller, + @NonNull Settings oldSettings); + } + private final List mChangedListeners = new ArrayList(1); + + /** The currently associated {@link ISettingsPage}. Can be null. */ + private ISettingsPage mSettingsPage; + + + /** + * Constructs a new default {@link SettingsController}. + * + * @param sdkLog A non-null logger to use. + */ + public SettingsController(@NonNull ILogger sdkLog) { + this(new LegacyFileOp(), sdkLog); + } + + /** + * Constructs a new default {@link SettingsController}. + * + * @param LegacyFileOp A non-null {@link LegacyFileOp} to perform file operations (to load/save settings.) + * @param sdkLog A non-null logger to use. + */ + public SettingsController(@NonNull IFileOp LegacyFileOp, @NonNull ILogger sdkLog) { + this(LegacyFileOp, sdkLog, new Settings()); + } + + /** + * Specialized constructor that wraps an existing {@link Settings} instance. + * This is mostly used in unit-tests to override settings that are being used. + * Normal usage should NOT need to call this constructor. + * + * @param LegacyFileOp A non-null {@link LegacyFileOp} to perform file operations (to load/save settings) + * @param sdkLog A non-null logger to use. + * @param settings A non-null {@link Settings} to use as-is. It is not duplicated. + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected SettingsController(@NonNull IFileOp LegacyFileOp, + @NonNull ILogger sdkLog, + @NonNull Settings settings) { + mFileOp = LegacyFileOp; + mSdkLog = sdkLog; + mSettings = settings; + } + + @NonNull + public Settings getSettings() { + return mSettings; + } + + public void registerOnChangedListener(@Nullable OnChangedListener listener) { + if (listener != null && !mChangedListeners.contains(listener)) { + mChangedListeners.add(listener); + } + } + + public void unregisterOnChangedListener(@Nullable OnChangedListener listener) { + if (listener != null) { + mChangedListeners.remove(listener); + } + } + + //--- Access to settings ------------ + + + public static class Settings { + private final Properties mProperties; + + /** Initialize an empty set of settings. */ + public Settings() { + mProperties = new Properties(); + } + + /** Duplicates a set of settings. */ + public Settings(@NonNull Settings settings) { + this(); + for (Entry entry : settings.mProperties.entrySet()) { + mProperties.put(entry.getKey(), entry.getValue()); + } + } + + /** + * Specialized constructor for unit-tests that wraps an existing + * {@link Properties} instance. The properties instance is not duplicated, + * it's merely used as-is and changes will be reflected directly. + */ + protected Settings(@NonNull Properties properties) { + mProperties = properties; + } + + /** + * Returns the value of the {@link ISettingsPage#KEY_FORCE_HTTP} setting. + * + * @see ISettingsPage#KEY_FORCE_HTTP + */ + public boolean getForceHttp() { + return Boolean.parseBoolean(mProperties.getProperty(ISettingsPage.KEY_FORCE_HTTP)); + } + + /** + * Returns the value of the {@link ISettingsPage#KEY_ASK_ADB_RESTART} setting. + * + * @see ISettingsPage#KEY_ASK_ADB_RESTART + */ + public boolean getAskBeforeAdbRestart() { + return Boolean.parseBoolean(mProperties.getProperty(ISettingsPage.KEY_ASK_ADB_RESTART)); + } + + /** + * Returns the value of the {@link ISettingsPage#KEY_USE_DOWNLOAD_CACHE} setting. + * + * @see ISettingsPage#KEY_USE_DOWNLOAD_CACHE + */ + public boolean getUseDownloadCache() { + return Boolean.parseBoolean( + mProperties.getProperty( + ISettingsPage.KEY_USE_DOWNLOAD_CACHE, + Boolean.TRUE.toString())); + } + + /** + * Returns the value of the {@link ISettingsPage#KEY_SHOW_UPDATE_ONLY} setting. + * + * @see ISettingsPage#KEY_SHOW_UPDATE_ONLY + */ + public boolean getShowUpdateOnly() { + return Boolean.parseBoolean( + mProperties.getProperty( + ISettingsPage.KEY_SHOW_UPDATE_ONLY, + Boolean.TRUE.toString())); + } + + /** + * Returns the value of the {@link ISettingsPage#KEY_ENABLE_PREVIEWS} setting. + * + * @see ISettingsPage#KEY_ENABLE_PREVIEWS + */ + public boolean getEnablePreviews() { + return Boolean.parseBoolean( + mProperties.getProperty( + ISettingsPage.KEY_ENABLE_PREVIEWS, + Boolean.TRUE.toString())); + } + + /** + * Returns the value of the {@link ISettingsPage#KEY_MONITOR_DENSITY} setting + * @see ISettingsPage#KEY_MONITOR_DENSITY + */ + public int getMonitorDensity() { + String value = mProperties.getProperty(ISettingsPage.KEY_MONITOR_DENSITY, null); + if (value == null) { + return -1; + } + + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return -1; + } + } + } + + /** + * Sets the value of the {@link ISettingsPage#KEY_SHOW_UPDATE_ONLY} setting. + * + * @param enabled True if only compatible non-obsolete update items should be shown. + * @see ISettingsPage#KEY_SHOW_UPDATE_ONLY + */ + public void setShowUpdateOnly(boolean enabled) { + setSetting(ISettingsPage.KEY_SHOW_UPDATE_ONLY, enabled); + } + + /** + * Sets the value of the {@link ISettingsPage#KEY_MONITOR_DENSITY} setting. + * + * @param density the density of the monitor + * @see ISettingsPage#KEY_MONITOR_DENSITY + */ + public void setMonitorDensity(int density) { + mSettings.mProperties.setProperty( + ISettingsPage.KEY_MONITOR_DENSITY, Integer.toString(density)); + } + + /** + * Internal helper to set a boolean setting. + */ + void setSetting(@NonNull String key, boolean value) { + mSettings.mProperties.setProperty(key, Boolean.toString(value)); + } + + //--- Controller methods ------------- + + /** + * Associate the given {@link ISettingsPage} with this {@link SettingsController}. + *

+ * This loads the current properties into the setting page UI. + * It then associates the SettingsChanged callback with this controller. + *

+ * If the setting page given is null, it will be unlinked from controller. + * + * @param settingsPage An {@link ISettingsPage} to associate with the controller. + */ + public void setSettingsPage(@Nullable ISettingsPage settingsPage) { + mSettingsPage = settingsPage; + + if (settingsPage != null) { + settingsPage.loadSettings(mSettings.mProperties); + + settingsPage.setOnSettingsChanged(new ISettingsPage.SettingsChangedCallback() { + @Override + public void onSettingsChanged(ISettingsPage page) { + SettingsController.this.onSettingsChanged(); + } + }); + } + } + + /** + * Load settings from the settings file. + */ + public void loadSettings() { + + String path = null; + try { + String folder = AndroidLocation.getFolder(); + File f = new File(folder, SETTINGS_FILENAME); + path = f.getPath(); + + Properties props = mFileOp.loadProperties(f); + mSettings.mProperties.clear(); + mSettings.mProperties.putAll(props); + + // Properly reformat some settings to enforce their default value when missing. + setShowUpdateOnly(mSettings.getShowUpdateOnly()); + setSetting(ISettingsPage.KEY_ASK_ADB_RESTART, mSettings.getAskBeforeAdbRestart()); + setSetting(ISettingsPage.KEY_USE_DOWNLOAD_CACHE, mSettings.getUseDownloadCache()); + setSetting(ISettingsPage.KEY_ENABLE_PREVIEWS, mSettings.getEnablePreviews()); + + } catch (Exception e) { + if (mSdkLog != null) { + mSdkLog.error(e, + "Failed to load settings from .android folder. Path is '%1$s'.", + path); + } + } + } + + /** + * Saves settings to the settings file. + */ + public void saveSettings() { + + String path = null; + try { + String folder = AndroidLocation.getFolder(); + File f = new File(folder, SETTINGS_FILENAME); + path = f.getPath(); + mFileOp.saveProperties(f, mSettings.mProperties, "## Settings for Android Tool"); //$NON-NLS-1$ + + } catch (Exception e) { + if (mSdkLog != null) { + // This is important enough that we want to really nag the user about it + String reason = null; + + if (e instanceof FileNotFoundException) { + reason = "File not found"; + } else if (e instanceof AndroidLocationException) { + reason = ".android folder not found, please define ANDROID_SDK_HOME"; + } else if (e.getMessage() != null) { + reason = String.format("%1$s: %2$s", + e.getClass().getSimpleName(), + e.getMessage()); + } else { + reason = e.getClass().getName(); + } + + mSdkLog.error(e, "Failed to save settings file '%1$s': %2$s", path, reason); + } + } + } + + /** + * When settings have changed: retrieve the new settings, apply them, save them + * and notify on-settings-changed listeners. + */ + private void onSettingsChanged() { + if (mSettingsPage == null) { + return; + } + + Settings oldSettings = new Settings(mSettings); + mSettingsPage.retrieveSettings(mSettings.mProperties); + applySettings(); + saveSettings(); + + for (OnChangedListener listener : mChangedListeners) { + try { + listener.onSettingsChanged(this, oldSettings); + } catch (Throwable ignore) {} + } + } + + /** + * Applies the current settings. + */ + public void applySettings() { + Properties props = System.getProperties(); + + // Get the configured HTTP proxy settings + String proxyHost = mSettings.mProperties.getProperty(ISettingsPage.KEY_HTTP_PROXY_HOST, + ""); //$NON-NLS-1$ + String proxyPort = mSettings.mProperties.getProperty(ISettingsPage.KEY_HTTP_PROXY_PORT, + ""); //$NON-NLS-1$ + + // Set both the HTTP and HTTPS proxy system properties. + // The system property constants can be found in the Java SE documentation at + // http://download.oracle.com/javase/6/docs/technotes/guides/net/proxies.html + final String JAVA_PROP_HTTP_PROXY_HOST = "http.proxyHost"; //$NON-NLS-1$ + final String JAVA_PROP_HTTP_PROXY_PORT = "http.proxyPort"; //$NON-NLS-1$ + final String JAVA_PROP_HTTPS_PROXY_HOST = "https.proxyHost"; //$NON-NLS-1$ + final String JAVA_PROP_HTTPS_PROXY_PORT = "https.proxyPort"; //$NON-NLS-1$ + + // Only change the proxy if have something in the preferences. + // Do not erase the default settings by empty values. + if (proxyHost != null && !proxyHost.isEmpty()) { + props.setProperty(JAVA_PROP_HTTP_PROXY_HOST, proxyHost); + props.setProperty(JAVA_PROP_HTTPS_PROXY_HOST, proxyHost); + } + if (proxyPort != null && !proxyPort.isEmpty()) { + props.setProperty(JAVA_PROP_HTTP_PROXY_PORT, proxyPort); + props.setProperty(JAVA_PROP_HTTPS_PROXY_PORT, proxyPort); + } + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/UpdaterData.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/UpdaterData.java new file mode 100644 index 00000000..c1b691c6 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/UpdaterData.java @@ -0,0 +1,1336 @@ +/* + * 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.sdklib.internal.repository.updater; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.VisibleForTesting; +import com.android.annotations.VisibleForTesting.Visibility; +import com.android.prefs.AndroidLocation.AndroidLocationException; +import com.android.sdklib.SdkManager; +import com.android.sdklib.internal.avd.AvdManager; +import com.android.sdklib.internal.repository.AdbWrapper; +import com.android.sdklib.internal.repository.DownloadCache; +import com.android.sdklib.internal.repository.ITask; +import com.android.sdklib.internal.repository.ITaskFactory; +import com.android.sdklib.internal.repository.ITaskMonitor; +import com.android.sdklib.internal.repository.LocalSdkParser; +import com.android.sdklib.internal.repository.NullTaskMonitor; +import com.android.sdklib.internal.repository.archives.Archive; +import com.android.sdklib.internal.repository.archives.ArchiveInstaller; +import com.android.sdklib.internal.repository.packages.AddonPackage; +import com.android.sdklib.repository.License; +import com.android.sdklib.internal.repository.packages.Package; +import com.android.sdklib.internal.repository.packages.PlatformToolPackage; +import com.android.sdklib.internal.repository.packages.ToolPackage; +import com.android.sdklib.internal.repository.sources.SdkRepoSource; +import com.android.sdklib.internal.repository.sources.SdkSource; +import com.android.sdklib.internal.repository.sources.SdkSourceCategory; +import com.android.sdklib.internal.repository.sources.SdkSources; +import com.android.sdklib.internal.repository.updater.SettingsController.OnChangedListener; +import com.android.sdklib.repository.ISdkChangeListener; +import com.android.sdklib.repository.SdkAddonConstants; +import com.android.sdklib.repository.SdkRepoConstants; +import com.android.sdklib.util.LineUtil; +import com.android.utils.ILogger; +import com.android.utils.IReaderLogger; +import com.android.utils.SparseIntArray; +import com.google.common.base.Charsets; +import com.google.common.base.Joiner; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +/** + * Data shared by the SDK Manager updaters. + * + * @deprecated + * com.android.sdklib.internal.repository has moved into Studio as + * com.android.tools.idea.sdk.remote.internal. + */ +@Deprecated +public class UpdaterData implements IUpdaterData { + + public static final int NO_TOOLS_MSG = 0; + public static final int TOOLS_MSG_UPDATED_FROM_ADT = 1; + public static final int TOOLS_MSG_UPDATED_FROM_SDKMAN = 2; + + private String mOsSdkRoot; + + private final LocalSdkParser mLocalSdkParser = new LocalSdkParser(); + /** Holds all sources. Do not use this directly. + * Instead use {@link #getSources()} so that unit tests can override this as needed. */ + private final SdkSources mSources = new SdkSources(); + /** Holds settings. Do not use this directly. + * Instead use {@link #getSettingsController()} so that unit tests can override this. */ + private final SettingsController mSettingsController; + private final ArrayList mListeners = new ArrayList(); + private final ILogger mSdkLog; + private ITaskFactory mTaskFactory; + + private SdkManager mSdkManager; + private AvdManager mAvdManager; + /** + * The current {@link PackageLoader} to use. + * Lazily created in {@link #getPackageLoader()}. + */ + private PackageLoader mPackageLoader; + /** + * The current {@link DownloadCache} to use. + * Lazily created in {@link #getDownloadCache()}. + */ + private DownloadCache mDownloadCache; + private AndroidLocationException mAvdManagerInitError; + + /** + * Creates a new updater data. + * + * @param sdkLog Logger. Cannot be null. + * @param osSdkRoot The OS path to the SDK root. + */ + public UpdaterData(String osSdkRoot, ILogger sdkLog) { + mOsSdkRoot = osSdkRoot; + mSdkLog = sdkLog; + + mSettingsController = initSettingsController(); + initSdk(); + } + + // ----- getters, setters ---- + + public String getOsSdkRoot() { + return mOsSdkRoot; + } + + @Override + public DownloadCache getDownloadCache() { + if (mDownloadCache == null) { + mDownloadCache = new DownloadCache( + getSettingsController().getSettings().getUseDownloadCache() ? + DownloadCache.Strategy.FRESH_CACHE : + DownloadCache.Strategy.DIRECT); + } + return mDownloadCache; + } + + public void setTaskFactory(ITaskFactory taskFactory) { + mTaskFactory = taskFactory; + } + + @Override + public ITaskFactory getTaskFactory() { + return mTaskFactory; + } + + public SdkSources getSources() { + return mSources; + } + + public LocalSdkParser getLocalSdkParser() { + return mLocalSdkParser; + } + + @Override + public ILogger getSdkLog() { + return mSdkLog; + } + + @Override + public SdkManager getSdkManager() { + return mSdkManager; + } + + @Override + public AvdManager getAvdManager() { + return mAvdManager; + } + + @Override + public SettingsController getSettingsController() { + return mSettingsController; + } + + /** Adds a listener ({@link ISdkChangeListener}) that is notified when the SDK is reloaded. */ + public void addListeners(ISdkChangeListener listener) { + if (mListeners.contains(listener) == false) { + mListeners.add(listener); + } + } + + /** Removes a listener ({@link ISdkChangeListener}) that is notified when the SDK is reloaded. */ + public void removeListener(ISdkChangeListener listener) { + mListeners.remove(listener); + } + + public PackageLoader getPackageLoader() { + // The package loader is lazily initialized here. + if (mPackageLoader == null) { + mPackageLoader = new PackageLoader(this); + } + return mPackageLoader; + } + + /** + * Check if any error occurred during initialization. + * If it did, display an error message. + * + * @return True if an error occurred, false if we should continue. + */ + public boolean checkIfInitFailed() { + if (mAvdManagerInitError != null) { + String example; + if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS) { + example = "%USERPROFILE%"; //$NON-NLS-1$ + } else { + example = "~"; //$NON-NLS-1$ + } + + String error = String.format( + "The AVD manager normally uses the user's profile directory to store " + + "AVD files. However it failed to find the default profile directory. " + + "\n" + + "To fix this, please set the environment variable ANDROID_SDK_HOME to " + + "a valid path such as \"%s\".", + example); + + displayInitError(error); + + return true; + } + return false; + } + + protected void displayInitError(String error) { + mSdkLog.error(null /* Throwable */, "%s", error); //$NON-NLS-1$ + } + + // ----- + + /** + * Runs a runnable on the UI thread. + * The base implementation just runs the runnable right away. + * + * @param r Non-null runnable. + */ + protected void runOnUiThread(@NonNull Runnable r) { + r.run(); + } + + /** + * Initializes the {@link SdkManager} and the {@link AvdManager}. + * Extracted so that we can override this in unit tests. + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected void initSdk() { + setSdkManager(SdkManager.createManager(mOsSdkRoot, mSdkLog)); + try { + mAvdManager = null; + mAvdManager = AvdManager.getInstance(mSdkManager.getLocalSdk(), mSdkLog); + } catch (AndroidLocationException e) { + mSdkLog.error(e, "Unable to read AVDs: " + e.getMessage()); //$NON-NLS-1$ + + // Note: we used to continue here, but the thing is that + // mAvdManager==null so nothing is really going to work as + // expected. Let's just display an error later in checkIfInitFailed() + // and abort right there. This step is just too early in the SWT + // setup process to display a message box yet. + + mAvdManagerInitError = e; + } + + // notify listeners. + broadcastOnSdkReload(); + } + + /** + * Initializes the {@link SettingsController} + * Extracted so that we can override this in unit tests. + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected SettingsController initSettingsController() { + SettingsController settingsController = new SettingsController(mSdkLog); + settingsController.registerOnChangedListener(new OnChangedListener() { + @Override + public void onSettingsChanged( + SettingsController controller, + SettingsController.Settings oldSettings) { + + // Reset the download cache if it doesn't match the right strategy. + // The cache instance gets lazily recreated later in getDownloadCache(). + if (mDownloadCache != null) { + if (controller.getSettings().getUseDownloadCache() && + mDownloadCache.getStrategy() != DownloadCache.Strategy.FRESH_CACHE) { + mDownloadCache = null; + } else if (!controller.getSettings().getUseDownloadCache() && + mDownloadCache.getStrategy() != DownloadCache.Strategy.DIRECT) { + mDownloadCache = null; + } + } + } + }); + return settingsController; + } + + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected void setSdkManager(SdkManager sdkManager) { + mSdkManager = sdkManager; + } + + /** + * Reloads the SDK content (targets). + *

+ * This also reloads the AVDs in case their status changed. + *

+ * This does not notify the listeners ({@link ISdkChangeListener}). + */ + public void reloadSdk() { + // reload SDK + mSdkManager.reloadSdk(mSdkLog); + + // reload AVDs + if (mAvdManager != null) { + try { + mAvdManager.reloadAvds(mSdkLog); + } catch (AndroidLocationException e) { + // FIXME + } + } + + mLocalSdkParser.clearPackages(); + + // notify listeners + broadcastOnSdkReload(); + } + + /** + * Reloads the AVDs. + *

+ * This does not notify the listeners. + */ + public void reloadAvds() { + // reload AVDs + if (mAvdManager != null) { + try { + mAvdManager.reloadAvds(mSdkLog); + } catch (AndroidLocationException e) { + mSdkLog.error(e, null); + } + } + } + + /** + * Sets up the default sources:
+ * - the default google SDK repository,
+ * - the user sources from prefs
+ * - the extra repo URLs from the environment,
+ * - and finally the extra user repo URLs from the environment. + */ + public void setupDefaultSources() { + SdkSources sources = getSources(); + + // Load the conventional sources. + // For testing, the env var can be set to replace the default root download URL. + // It must end with a / and its the location where the updater will look for + // the repository.xml, addons_list.xml and such files. + + String baseUrl = System.getenv("SDK_TEST_BASE_URL"); //$NON-NLS-1$ + if (baseUrl == null || baseUrl.length() <= 0 || !baseUrl.endsWith("/")) { //$NON-NLS-1$ + baseUrl = SdkRepoConstants.URL_GOOGLE_SDK_SITE; + } + + sources.add(SdkSourceCategory.ANDROID_REPO, + new SdkRepoSource(baseUrl, + SdkSourceCategory.ANDROID_REPO.getUiName())); + + // Load user sources (this will also notify change listeners but this operation is + // done early enough that there shouldn't be any anyway.) + sources.loadUserAddons(getSdkLog()); + } + + /** + * Returns the list of installed packages, parsing them if this has not yet been done. + *

+ * The package list is cached in the {@link LocalSdkParser} and will be reset when + * {@link #reloadSdk()} is invoked. + */ + public Package[] getInstalledPackages(ITaskMonitor monitor) { + LocalSdkParser parser = getLocalSdkParser(); + + Package[] packages = parser.getPackages(); + + if (packages == null) { + // load on demand the first time + packages = parser.parseSdk(getOsSdkRoot(), getSdkManager(), monitor); + } + + return packages; + } + /** + * Install the list of given {@link Archive}s. This is invoked by the user selecting some + * packages in the remote page and then clicking "install selected". + * + * @param archives The archives to install. Incompatible ones will be skipped. + * @param flags Optional flags for the installer, such as {@link #NO_TOOLS_MSG}. + * @return A list of archives that have been installed. Can be empty but not null. + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected List installArchives(final List archives, final int flags) { + if (mTaskFactory == null) { + throw new IllegalArgumentException("Task Factory is null"); + } + + // this will accumulate all the packages installed. + final List newlyInstalledArchives = new ArrayList(); + + final boolean forceHttp = getSettingsController().getSettings().getForceHttp(); + + // sort all archives based on their dependency level. + Collections.sort(archives, new InstallOrderComparator()); + + mTaskFactory.start("Installing Archives", new ITask() { + @Override + public void run(ITaskMonitor monitor) { + + final int progressPerArchive = 2 * ArchiveInstaller.NUM_MONITOR_INC; + monitor.setProgressMax(1 + archives.size() * progressPerArchive); + monitor.setDescription("Preparing to install archives"); + + boolean installedAddon = false; + boolean installedTools = false; + boolean installedPlatformTools = false; + boolean preInstallHookInvoked = false; + + // Mark all current local archives as already installed. + HashSet installedArchives = new HashSet(); + for (Package p : getInstalledPackages(monitor.createSubMonitor(1))) { + for (Archive a : p.getArchives()) { + installedArchives.add(a); + } + } + + int numInstalled = 0; + nextArchive: for (ArchiveInfo ai : archives) { + Archive archive = ai.getNewArchive(); + if (archive == null) { + // This is not supposed to happen. + continue nextArchive; + } + + int nextProgress = monitor.getProgress() + progressPerArchive; + try { + if (monitor.isCancelRequested()) { + break; + } + + ArchiveInfo[] adeps = ai.getDependsOn(); + if (adeps != null) { + for (ArchiveInfo adep : adeps) { + Archive na = adep.getNewArchive(); + if (na == null) { + // This archive depends on a missing archive. + // We shouldn't get here. + // Skip it. + monitor.log("Skipping '%1$s'; it depends on a missing package.", + archive.getParentPackage().getShortDescription()); + continue nextArchive; + } else if (!installedArchives.contains(na)) { + // This archive depends on another one that was not installed. + // We shouldn't get here. + // Skip it. + monitor.logError("Skipping '%1$s'; it depends on '%2$s' which was not installed.", + archive.getParentPackage().getShortDescription(), + adep.getShortDescription()); + continue nextArchive; + } + } + } + + if (!preInstallHookInvoked) { + preInstallHookInvoked = true; + broadcastPreInstallHook(); + } + + ArchiveInstaller installer = createArchiveInstaler(); + if (installer.install(ai, + mOsSdkRoot, + forceHttp, + mSdkManager, + getDownloadCache(), + monitor)) { + // We installed this archive. + newlyInstalledArchives.add(archive); + installedArchives.add(archive); + numInstalled++; + + // If this package was replacing an existing one, the old one + // is no longer installed. + installedArchives.remove(ai.getReplaced()); + + // Check if we successfully installed a platform-tool or add-on package. + if (archive.getParentPackage() instanceof AddonPackage) { + installedAddon = true; + } else if (archive.getParentPackage() instanceof ToolPackage) { + installedTools = true; + } else if (archive.getParentPackage() instanceof PlatformToolPackage) { + installedPlatformTools = true; + } + } + + } catch (Throwable t) { + // Display anything unexpected in the monitor. + String msg = t.getMessage(); + if (msg != null) { + msg = String.format("Unexpected Error installing '%1$s': %2$s: %3$s", + archive.getParentPackage().getShortDescription(), + t.getClass().getCanonicalName(), msg); + } else { + // no error info? get the stack call to display it + // At least that'll give us a better bug report. + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + t.printStackTrace(new PrintStream(baos)); + + msg = String.format("Unexpected Error installing '%1$s'\n%2$s", + archive.getParentPackage().getShortDescription(), + baos.toString()); + } + + monitor.log( "%1$s", msg); //$NON-NLS-1$ + mSdkLog.error(t, "%1$s", msg); //$NON-NLS-1$ + } finally { + + // Always move the progress bar to the desired position. + // This allows internal methods to not have to care in case + // they abort early + monitor.incProgress(nextProgress - monitor.getProgress()); + } + } + + if (installedAddon) { + // Update the USB vendor ids for adb + try { + mSdkManager.updateAdb(); + monitor.log("Updated ADB to support the USB devices declared in the SDK add-ons."); + } catch (Exception e) { + mSdkLog.error(e, "Update ADB failed"); + monitor.logError("failed to update adb to support the USB devices declared in the SDK add-ons."); + } + } + + if (preInstallHookInvoked) { + broadcastPostInstallHook(); + } + + if (installedAddon || installedPlatformTools) { + // We need to restart ADB. Actually since we don't know if it's even + // running, maybe we should just kill it and not start it. + // Note: it turns out even under Windows we don't need to kill adb + // before updating the tools folder, as adb.exe is (surprisingly) not + // locked. + + askForAdbRestart(monitor); + } + + if (installedTools) { + notifyToolsNeedsToBeRestarted(flags); + } + + if (numInstalled == 0) { + monitor.setDescription("Done. Nothing was installed."); + } else { + monitor.setDescription("Done. %1$d %2$s installed.", + numInstalled, + numInstalled == 1 ? "package" : "packages"); + + //notify listeners something was installed, so that they can refresh + reloadSdk(); + } + } + }); + + return newlyInstalledArchives; + } + + /** + * A comparator to sort all the {@link ArchiveInfo} based on their + * dependency level. This forces the installer to install first all packages + * with no dependency, then those with one level of dependency, etc. + */ + private static class InstallOrderComparator implements Comparator { + + private final Map mOrders = new HashMap(); + + @Override + public int compare(ArchiveInfo o1, ArchiveInfo o2) { + int n1 = getDependencyOrder(o1); + int n2 = getDependencyOrder(o2); + + return n1 - n2; + } + + private int getDependencyOrder(ArchiveInfo ai) { + if (ai == null) { + return 0; + } + + // reuse cached value, if any + Integer cached = mOrders.get(ai); + if (cached != null) { + return cached.intValue(); + } + + ArchiveInfo[] deps = ai.getDependsOn(); + if (deps == null) { + return 0; + } + + // compute dependencies, recursively + int n = deps.length; + + for (ArchiveInfo dep : deps) { + n += getDependencyOrder(dep); + } + + // cache it + mOrders.put(ai, Integer.valueOf(n)); + + return n; + } + + } + + /** + * Attempts to restart ADB. + *

+ * If the "ask before restart" setting is set (the default), prompt the user whether + * now is a good time to restart ADB. + */ + protected void askForAdbRestart(ITaskMonitor monitor) { + // Restart ADB if we don't need to ask. + if (!getSettingsController().getSettings().getAskBeforeAdbRestart()) { + AdbWrapper adb = new AdbWrapper(getOsSdkRoot(), monitor); + adb.stopAdb(); + adb.startAdb(); + } + } + + protected void notifyToolsNeedsToBeRestarted(int flags) { + + String msg = null; + if ((flags & TOOLS_MSG_UPDATED_FROM_ADT) == TOOLS_MSG_UPDATED_FROM_ADT) { + msg = + "The Android SDK and AVD Manager that you are currently using has been updated. " + + "Please also run Eclipse > Help > Check for Updates to see if the Android " + + "plug-in needs to be updated."; + + } else if ((flags & TOOLS_MSG_UPDATED_FROM_SDKMAN) == TOOLS_MSG_UPDATED_FROM_SDKMAN) { + msg = + "The Android SDK and AVD Manager that you are currently using has been updated. " + + "It is recommended that you now close the manager window and re-open it. " + + "If you use Eclipse, please run Help > Check for Updates to see if the Android " + + "plug-in needs to be updated."; + } else if ((flags & NO_TOOLS_MSG) == NO_TOOLS_MSG) { + return; + } + mSdkLog.info("%s", msg); //$NON-NLS-1$ + } + + /** + * Fetches all archives available on the known remote sources. + * + * Used by {@link UpdaterData#listRemotePackages_NoGUI} and + * {@link UpdaterData#updateOrInstallAll_NoGUI}. + * + * @param includeAll True to list and install all packages, including obsolete ones. + * @return A list of potential {@link ArchiveInfo} to install. + */ + private List getRemoteArchives_NoGUI(boolean includeAll) { + refreshSources(true); + getPackageLoader().loadRemoteAddonsList(new NullTaskMonitor(getSdkLog())); + + List archives; + SdkUpdaterLogic ul = new SdkUpdaterLogic(this); + + if (includeAll) { + archives = ul.getAllRemoteArchives( + getSources(), + getLocalSdkParser().getPackages(), + includeAll); + + } else { + archives = ul.computeUpdates( + null /*selectedArchives*/, + getSources(), + getLocalSdkParser().getPackages(), + includeAll); + + ul.addNewPlatforms( + archives, + getSources(), + getLocalSdkParser().getPackages(), + includeAll); + } + + Collections.sort(archives); + return archives; + } + + /** + * Lists remote packages available for install using + * {@link UpdaterData#updateOrInstallAll_NoGUI}. + * + * @param includeAll True to list and install all packages, including obsolete ones. + * @param extendedOutput True to display more details on each package. + */ + public void listRemotePackages_NoGUI(boolean includeAll, boolean extendedOutput) { + + List archives = getRemoteArchives_NoGUI(includeAll); + + mSdkLog.info("Packages available for installation or update: %1$d\n", archives.size()); + + int index = 1; + for (ArchiveInfo ai : archives) { + Archive a = ai.getNewArchive(); + if (a != null) { + Package p = a.getParentPackage(); + if (p != null) { + if (extendedOutput) { + mSdkLog.info("----------\n"); + mSdkLog.info("id: %1$d or \"%2$s\"\n", index, p.installId()); + mSdkLog.info(" Type: %1$s\n", + p.getClass().getSimpleName().replaceAll("Package", "")); //$NON-NLS-1$ //$NON-NLS-2$ + String desc = LineUtil.reformatLine(" Desc: %s\n", + p.getLongDescription()); + mSdkLog.info("%s", desc); //$NON-NLS-1$ + } else { + mSdkLog.info("%1$ 4d- %2$s\n", + index, + p.getShortDescription()); + } + index++; + } + } + } + } + + /** + * Tries to update all the *existing* local packages. + * This version *requires* to be run with a GUI. + *

+ * There are two modes of operation: + *

    + *
  • If selectedArchives is null, refreshes all sources, compares the available remote + * packages with the current local ones and suggest updates to be done to the user (including + * new platforms that the users doesn't have yet). + *
  • If selectedArchives is not null, this represents a list of archives/packages that + * the user wants to install or update, so just process these. + *
+ * + * @param selectedArchives The list of remote archives to consider for the update. + * This can be null, in which case a list of remote archive is fetched from all + * available sources. + * @param includeObsoletes True if obsolete packages should be used when resolving what + * to update. + * @param flags Optional flags for the installer, such as {@link #NO_TOOLS_MSG}. + * @return A list of archives that have been installed. Can be null if nothing was done. + */ + public List updateOrInstallAll_WithGUI( + Collection selectedArchives, + boolean includeObsoletes, + int flags) { + // FIXME revisit this logic. This is just an transitional implementation + // while I refactor the way the sdk manager works internally. + + SdkUpdaterLogic ul = new SdkUpdaterLogic(this); + List archives = ul.computeUpdates( + selectedArchives, + getSources(), + getLocalSdkParser().getPackages(), + includeObsoletes); + + if (selectedArchives == null) { + getPackageLoader().loadRemoteAddonsList(new NullTaskMonitor(getSdkLog())); + ul.addNewPlatforms( + archives, + getSources(), + getLocalSdkParser().getPackages(), + includeObsoletes); + } + + Collections.sort(archives); + + if (!archives.isEmpty()) { + return installArchives(archives, flags); + } + return null; + } + + /** + * Tries to update all the *existing* local packages. + * This version is intended to run without a GUI and + * only outputs to the current {@link ILogger}. + * + * @param pkgFilter A list of {@link SdkRepoConstants#NODES} or {@link Package#installId()} + * or package indexes to limit the packages we can update or install. + * A null or empty list means to update everything possible. + * @param includeAll True to list and install all packages, including obsolete ones. + * @param dryMode True to check what would be updated/installed but do not actually + * download or install anything. + * @param acceptLicense SDK licenses to automatically accept. + * @return A list of archives that have been installed. Can be null if nothing was done. + * @deprecated Use {@link #updateOrInstallAll_NoGUI(java.util.Collection, boolean, boolean, String, boolean)} + * instead + */ + @Deprecated + public List updateOrInstallAll_NoGUI( + Collection pkgFilter, + boolean includeAll, + boolean dryMode, + String acceptLicense) { + return updateOrInstallAll_NoGUI(pkgFilter, includeAll, dryMode, acceptLicense, false); + } + + /** + * Tries to update all the *existing* local packages. + * This version is intended to run without a GUI and + * only outputs to the current {@link ILogger}. + * + * @param pkgFilter A list of {@link SdkRepoConstants#NODES} or {@link Package#installId()} + * or package indexes to limit the packages we can update or install. + * A null or empty list means to update everything possible. + * @param includeAll True to list and install all packages, including obsolete ones. + * @param dryMode True to check what would be updated/installed but do not actually + * download or install anything. + * @param acceptLicense SDK licenses to automatically accept. + * @param includeDependencies If true, also include any required dependencies + * @return A list of archives that have been installed. Can be null if nothing was done. + */ + public List updateOrInstallAll_NoGUI( + Collection pkgFilter, + boolean includeAll, + boolean dryMode, + String acceptLicense, + boolean includeDependencies) { + + List archives = getRemoteArchives_NoGUI(includeAll); + + // Filter the selected archives to only keep the ones matching the filter + if (pkgFilter != null && !pkgFilter.isEmpty() && archives != null && !archives.isEmpty()) { + // Map filter types to an SdkRepository Package type, + // e.g. create a map "platform" => PlatformPackage.class + HashMap> pkgMap = + new HashMap>(); + + mapFilterToPackageClass(pkgMap, SdkRepoConstants.NODES); + mapFilterToPackageClass(pkgMap, SdkAddonConstants.NODES); + + // Prepare a map install-id => package instance + HashMap installIdMap = new HashMap(); + for (ArchiveInfo ai : archives) { + Archive a = ai.getNewArchive(); + if (a != null) { + Package p = a.getParentPackage(); + if (p != null) { + String iid = p.installId().toLowerCase(Locale.US); + if (iid != null && !iid.isEmpty() && !installIdMap.containsKey(iid)) { + installIdMap.put(iid, p); + } + } + } + } + + // Now intersect this with the pkgFilter requested by the user, in order to + // only keep the classes that the user wants to install. + // We also create a set with the package indices requested by the user + // and a set of install-ids requested by the user. + + HashSet> userFilteredClasses = + new HashSet>(); + SparseIntArray userFilteredIndices = new SparseIntArray(); + Set userFilteredInstallIds = new HashSet(); + + for (String iid : pkgFilter) { + // The install-id is not case-sensitive. + iid = iid.toLowerCase(Locale.US); + + if (installIdMap.containsKey(iid)) { + userFilteredInstallIds.add(iid); + + } else if (iid.replaceAll("[0-9]+", "").isEmpty()) {//$NON-NLS-1$ //$NON-NLS-2$ + // An all-digit number is a package index requested by the user. + int index = Integer.parseInt(iid); + userFilteredIndices.put(index, index); + + } else if (pkgMap.containsKey(iid)) { + userFilteredClasses.add(pkgMap.get(iid)); + + } else { + // This should not happen unless there's a mismatch in the package map. + mSdkLog.error(null, "Ignoring unknown package filter '%1$s'", iid); + } + } + + // we don't need the maps anymore + pkgMap = null; + installIdMap = null; + + // Now filter the remote archives list to keep: + // - any package which class matches userFilteredClasses + // - any package index which matches userFilteredIndices + // - any package install id which matches userFilteredInstallIds + + int index = 1; + for (Iterator it = archives.iterator(); it.hasNext(); ) { + boolean keep = false; + ArchiveInfo ai = it.next(); + Archive a = ai.getNewArchive(); + if (a != null) { + Package p = a.getParentPackage(); + if (p != null) { + if (userFilteredInstallIds.contains(p.installId().toLowerCase(Locale.US)) || + userFilteredClasses.contains(p.getClass()) || + userFilteredIndices.get(index) > 0) { + keep = true; + } + + index++; + } + } + + if (!keep) { + it.remove(); + } + } + + if (archives.isEmpty()) { + mSdkLog.info(LineUtil.reflowLine( + "Warning: The package filter removed all packages. There is nothing to install.\nPlease consider trying to update again without a package filter.\n")); + return null; + } + } + + if (archives != null && !archives.isEmpty()) { + if (includeDependencies) { + List dependencies = getDependencies(archives); + if (!dependencies.isEmpty()) { + List combined = Lists.newArrayList(); + combined.addAll(dependencies); + combined.addAll(archives); + archives = combined; + } + } + if (dryMode) { + mSdkLog.info("Packages selected for install:\n"); + for (ArchiveInfo ai : archives) { + Archive a = ai.getNewArchive(); + if (a != null) { + Package p = a.getParentPackage(); + if (p != null) { + mSdkLog.info("- %1$s\n", p.getShortDescription()); + } + } + } + mSdkLog.info("\nDry mode is on so nothing is actually being installed.\n"); + } else { + if (acceptLicense(archives, acceptLicense, 100 /* numRetries */)) { + return installArchives(archives, NO_TOOLS_MSG); + } + } + } else { + mSdkLog.info("There is nothing to install or update.\n"); + } + + return null; + } + + /** + * Computes the transitive dependencies of the given list of archives. This will only + * include dependencies that also need to be installed, not satisfied dependencies. + */ + private static List getDependencies(@NonNull List archives) { + List dependencies = Lists.newArrayList(); + for (ArchiveInfo archive : archives) { + addDependencies(dependencies, archive, Sets.newHashSet()); + } + return dependencies; + } + + private static void addDependencies(@NonNull List dependencies, + @NonNull ArchiveInfo archive, + @NonNull Set visited) { + if (visited.contains(archive)) { + return; + } + visited.add(archive); + + ArchiveInfo[] dependsOn = archive.getDependsOn(); + if (dependsOn != null) { + for (ArchiveInfo dependency : dependsOn) { + if (!dependencies.contains(dependency)) { + dependencies.add(dependency); + addDependencies(dependencies, dependency, visited); + } + } + } + } + + /** + * Validates that all archive licenses are accepted. + *

+ * There are 2 cases:
+ * - When {@code acceptLicenses} is given, the licenses specified are automatically + * accepted and all those not specified are automatically rejected.
+ * - When {@code acceptLicenses} is empty or null, licenses are collected and there's + * an input prompt on StdOut to ask a yes/no question. To output, this uses the + * current {@link #mSdkLog} which should be configured to send + * {@link ILogger#info(String, Object...)} directly to {@link System#out}.
+ * + * Finally only accepted licenses are kept in the archive list. + * + * @param archives The archives to validate. + * @param acceptLicenseIds A comma-separated list of licenses ids already approved. + * @param numRetries The number of times the command-line will ask to accept a given + * license when the input doesn't match the expected y/n/yes/no answer. + * Use 0 for infinite. Useful for unit-tests. Once the number of retries + * is reached, the license is assumed as rejected. + * @return True if there are any archives left to install. + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + boolean acceptLicense( + List archives, + String acceptLicenseIds, + final int numRetries) { + TreeSet acceptedLids = new TreeSet(); + if (acceptLicenseIds != null) { + acceptedLids.addAll(Arrays.asList(acceptLicenseIds.split(","))); //$NON-NLS-1$ + } + boolean automated = !acceptedLids.isEmpty(); + + TreeSet rejectedLids = new TreeSet(); + TreeMap lidToAccept = new TreeMap(); + TreeMap> lidPkgNames = new TreeMap>(); + + // Find the licenses needed. Include those already accepted. + for (ArchiveInfo ai : archives) { + License lic = getArchiveInfoLicense(ai); + if (lic == null) { + continue; + } + String lid = getLicenseId(lic); + if (!acceptedLids.contains(lid)) { + if (automated) { + // Automatically reject those not already accepted + rejectedLids.add(lid); + } else { + // Queue it to ask for it to be accepted + lidToAccept.put(lid, lic); + List list = lidPkgNames.get(lid); + if (list == null) { + list = new ArrayList(); + lidPkgNames.put(lid, list); + } + list.add(ai.getShortDescription()); + } + } + } + + // Ask for each license that needs to be asked manually for confirmation + nextEntry: for (Map.Entry entry : lidToAccept.entrySet()) { + String lid = entry.getKey(); + License lic = entry.getValue(); + mSdkLog.info("-------------------------------\n"); + mSdkLog.info("License id: %1$s\n", lid); + mSdkLog.info("Used by: \n - %1$s\n", + Joiner.on("\n - ").skipNulls().join(lidPkgNames.get(lid))); + mSdkLog.info("-------------------------------\n\n"); + mSdkLog.info("%1$s\n", lic.getLicense()); + + int retries = numRetries; + tryAgain: while(true) { + try { + mSdkLog.info("Do you accept the license '%1$s' [y/n]: ", lid); + + byte[] buffer = new byte[256]; + if (mSdkLog instanceof IReaderLogger) { + ((IReaderLogger) mSdkLog).readLine(buffer); + } else { + System.in.read(buffer); + } + mSdkLog.info("\n"); + + String reply = new String(buffer, Charsets.UTF_8); + reply = reply.trim().toLowerCase(Locale.US); + + if ("y".equals(reply) || "yes".equals(reply)) { + acceptedLids.add(lid); + continue nextEntry; + + } else if ("n".equals(reply) || "no".equals(reply)) { + break tryAgain; + + } else { + mSdkLog.info("Unknown response '%1$s'.\n", reply); + if (--retries == 0) { + mSdkLog.info("Max number of retries exceeded. Rejecting '%1$s'\n", lid); + break tryAgain; + } + continue tryAgain; + } + + } catch (IOException e) { + // Panic. Don't install anything. + e.printStackTrace(); + return false; + } + } + rejectedLids.add(lid); + } + + // Finally remove all archive which license is rejected or not accepted. + for (Iterator it = archives.iterator(); it.hasNext(); ) { + ArchiveInfo ai = it.next(); + License lic = getArchiveInfoLicense(ai); + if (lic == null) { + continue; + } + String lid = getLicenseId(lic); + if (rejectedLids.contains(lid) || !acceptedLids.contains(lid)) { + mSdkLog.info("Package %1$s not installed due to rejected license '%2$s'.\n", + ai.getShortDescription(), + lid); + it.remove(); + } + } + + + return !archives.isEmpty(); + } + + private License getArchiveInfoLicense(ArchiveInfo ai) { + Archive a = ai.getNewArchive(); + if (a != null) { + Package p = a.getParentPackage(); + if (p != null) { + License lic = p.getLicense(); + if (lic != null && + lic.getLicenseRef() != null && + !lic.getLicense().isEmpty() && + lic.getLicense() != null && + !lic.getLicense().isEmpty()) { + return lic; + } + } + } + + return null; + } + + private String getLicenseId(License lic) { + return String.format("%1$s-%2$08x", //$NON-NLS-1$ + lic.getLicenseRef(), + lic.getLicense().hashCode()); + } + + @SuppressWarnings("unchecked") + private void mapFilterToPackageClass( + HashMap> inOutPkgMap, + String[] nodes) { + + // Automatically find the classes matching the node names + ClassLoader classLoader = getClass().getClassLoader(); + String basePackage = Package.class.getPackage().getName(); + + for (String node : nodes) { + // Capitalize the name + String name = node.substring(0, 1).toUpperCase() + node.substring(1); + + // We can have one dash at most in a name. If it's present, we'll try + // with the dash or with the next letter capitalized. + int dash = name.indexOf('-'); + if (dash > 0) { + name = name.replaceFirst("-", ""); + } + + for (int alternatives = 0; alternatives < 2; alternatives++) { + + String fqcn = basePackage + '.' + name + "Package"; //$NON-NLS-1$ + try { + Class clazz = + (Class) classLoader.loadClass(fqcn); + if (clazz != null) { + inOutPkgMap.put(node, clazz); + continue; + } + } catch (Throwable ignore) { + } + + if (alternatives == 0 && dash > 0) { + // Try an alternative where the next letter after the dash + // is converted to an upper case. + name = name.substring(0, dash) + + name.substring(dash, dash + 1).toUpperCase() + + name.substring(dash + 1); + } else { + break; + } + } + } + } + + /** + * Refresh all sources. This is invoked either internally (reusing an existing monitor) + * or as a UI callback on the remote page "Refresh" button (in which case the monitor is + * null and a new task should be created.) + * + * @param forceFetching When true, load sources that haven't been loaded yet. + * When false, only refresh sources that have been loaded yet. + */ + public void refreshSources(final boolean forceFetching) { + assert mTaskFactory != null; + + final boolean forceHttp = getSettingsController().getSettings().getForceHttp(); + + mTaskFactory.start("Refresh Sources", new ITask() { + @Override + public void run(ITaskMonitor monitor) { + + getPackageLoader().loadRemoteAddonsList(monitor); + + SdkSource[] sources = getSources().getAllSources(); + monitor.setDescription("Refresh Sources"); + monitor.setProgressMax(monitor.getProgress() + sources.length); + for (SdkSource source : sources) { + if (forceFetching || + source.getPackages() != null || + source.getFetchError() != null) { + source.load(getDownloadCache(), monitor.createSubMonitor(1), forceHttp); + } + monitor.incProgress(1); + } + } + }); + } + + /** + * Safely invoke all the registered {@link ISdkChangeListener#onSdkLoaded()}. + * This can be called from any thread. + */ + public void broadcastOnSdkLoaded() { + if (!mListeners.isEmpty()) { + runOnUiThread(new Runnable() { + @Override + public void run() { + for (ISdkChangeListener listener : mListeners) { + try { + listener.onSdkLoaded(); + } catch (Throwable t) { + mSdkLog.error(t, null); + } + } + } + }); + } + } + + /** + * Safely invoke all the registered {@link ISdkChangeListener#onSdkReload()}. + * This can be called from any thread. + */ + private void broadcastOnSdkReload() { + if (!mListeners.isEmpty()) { + runOnUiThread(new Runnable() { + @Override + public void run() { + for (ISdkChangeListener listener : mListeners) { + try { + listener.onSdkReload(); + } catch (Throwable t) { + mSdkLog.error(t, null); + } + } + } + }); + } + } + + /** + * Safely invoke all the registered {@link ISdkChangeListener#preInstallHook()}. + * This can be called from any thread. + */ + private void broadcastPreInstallHook() { + if (!mListeners.isEmpty()) { + runOnUiThread(new Runnable() { + @Override + public void run() { + for (ISdkChangeListener listener : mListeners) { + try { + listener.preInstallHook(); + } catch (Throwable t) { + mSdkLog.error(t, null); + } + } + } + }); + } + } + + /** + * Safely invoke all the registered {@link ISdkChangeListener#postInstallHook()}. + * This can be called from any thread. + */ + private void broadcastPostInstallHook() { + if (!mListeners.isEmpty()) { + runOnUiThread(new Runnable() { + @Override + public void run() { + for (ISdkChangeListener listener : mListeners) { + try { + listener.postInstallHook(); + } catch (Throwable t) { + mSdkLog.error(t, null); + } + } + } + }); + } + } + + /** + * Internal helper to return a new {@link ArchiveInstaller}. + * This allows us to override the installer for unit-testing. + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected ArchiveInstaller createArchiveInstaler() { + return new ArchiveInstaller(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/io/IFileOp.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/io/IFileOp.java new file mode 100644 index 00000000..4ed4ab1c --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/io/IFileOp.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2011 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.sdklib.io; + +import com.android.annotations.NonNull; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Properties; + + +/** + * Wraps some common {@link File} operations on files and folders. + *

+ * This makes it possible to override/mock/stub some file operations in unit tests. + */ +public interface IFileOp { + + /** + * Helper to delete a file or a directory. + * For a directory, recursively deletes all of its content. + * Files that cannot be deleted right away are marked for deletion on exit. + * It's ok for the file or folder to not exist at all. + * The argument can be null. + */ + void deleteFileOrFolder(@NonNull File fileOrFolder); + + /** + * Sets the executable Unix permission (+x) on a file or folder. + *

+ * This attempts to use File#setExecutable through reflection if + * it's available. + * If this is not available, this invokes a chmod exec instead, + * so there is no guarantee of it being fast. + *

+ * Caller must make sure to not invoke this under Windows. + * + * @param file The file to set permissions on. + * @throws IOException If an I/O error occurs + */ + void setExecutablePermission(@NonNull File file) throws IOException; + + /** + * Sets the file or directory as read-only. + * + * @param file The file or directory to set permissions on. + */ + void setReadOnly(@NonNull File file); + + /** + * Copies a binary file. + * + * @param source the source file to copy. + * @param dest the destination file to write. + * @throws FileNotFoundException if the source file doesn't exist. + * @throws IOException if there's a problem reading or writing the file. + */ + void copyFile(@NonNull File source, @NonNull File dest) throws IOException; + + /** + * Checks whether 2 binary files are the same. + * + * @param file1 the source file to copy + * @param file2 the destination file to write + * @throws FileNotFoundException if the source files don't exist. + * @throws IOException if there's a problem reading the files. + */ + boolean isSameFile(@NonNull File file1, @NonNull File file2) + throws IOException; + + /** Invokes {@link File#exists()} on the given {@code file}. */ + boolean exists(@NonNull File file); + + /** Invokes {@link File#isFile()} on the given {@code file}. */ + boolean isFile(@NonNull File file); + + /** Invokes {@link File#isDirectory()} on the given {@code file}. */ + boolean isDirectory(@NonNull File file); + + /** Invokes {@link File#length()} on the given {@code file}. */ + long length(@NonNull File file); + + /** + * Invokes {@link File#delete()} on the given {@code file}. + * Note: for a recursive folder version, consider {@link #deleteFileOrFolder(File)}. + */ + boolean delete(@NonNull File file); + + /** Invokes {@link File#mkdirs()} on the given {@code file}. */ + boolean mkdirs(@NonNull File file); + + /** + * Invokes {@link File#listFiles()} on the given {@code file}. + * Contrary to the Java API, this returns an empty array instead of null when the + * directory does not exist. + */ + @NonNull + File[] listFiles(@NonNull File file); + + /** Invokes {@link File#renameTo(File)} on the given files. */ + boolean renameTo(@NonNull File oldDir, @NonNull File newDir); + + /** Creates a new {@link OutputStream} for the given {@code file}. */ + @NonNull + OutputStream newFileOutputStream(@NonNull File file) + throws FileNotFoundException; + + /** Creates a new {@link InputStream} for the given {@code file}. */ + @NonNull + InputStream newFileInputStream(@NonNull File file) + throws FileNotFoundException; + + /** + * Load {@link Properties} from a file. Returns an empty property set on error. + * + * @param file A non-null file to load from. File may not exist. + * @return A new {@link Properties} with the properties loaded from the file, + * or an empty property set in case of error. + */ + @NonNull + Properties loadProperties(@NonNull File file); + + /** + * Saves (write, store) the given {@link Properties} into the given {@link File}. + * + * @param file A non-null file to write to. + * @param props The properties to write. + * @param comments A non-null description of the properly list, written in the file. + * @throws IOException if the write operation failed. + */ + void saveProperties( + @NonNull File file, + @NonNull Properties props, + @NonNull String comments) throws IOException; + + /** + * Returns the lastModified attribute of the file. + * + * @see File#lastModified() + * @param file The non-null file of which to retrieve the lastModified attribute. + * @return The last-modified attribute of the file, in milliseconds since The Epoch. + */ + long lastModified(@NonNull File file); +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/io/LegacyFileOp.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/io/LegacyFileOp.java new file mode 100644 index 00000000..95b9ff72 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/io/LegacyFileOp.java @@ -0,0 +1,488 @@ +/* + * Copyright (C) 2010 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.sdklib.io; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.google.common.io.Closer; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Properties; +import java.util.regex.Pattern; + + +/** + * Wraps some common {@link File} operations on files and folders. + *

+ * This makes it possible to override/mock/stub some file operations in unit tests. + */ +public class LegacyFileOp implements IFileOp { + + public static final File[] EMPTY_FILE_ARRAY = new File[0]; + + /** + * Reflection method for File.setExecutable(boolean, boolean). Only present in Java 6. + */ + private static Method sFileSetExecutable = null; + + /** + * Parameters to call File.setExecutable through reflection. + */ + private static final Object[] sFileSetExecutableParams = new Object[] { + Boolean.TRUE, Boolean.FALSE }; + + // static initialization of sFileSetExecutable. + static { + try { + sFileSetExecutable = File.class.getMethod("setExecutable", //$NON-NLS-1$ + boolean.class, boolean.class); + + } catch (SecurityException e) { + // do nothing we'll use chmod instead + } catch (NoSuchMethodException e) { + // do nothing we'll use chmod instead + } + } + + /** + * Appends the given {@code segments} to the {@code base} file. + * + * @param base A base file, non-null. + * @param segments Individual folder or filename segments to append to the base file. + * @return A new file representing the concatenation of the base path with all the segments. + */ + public static File append(@NonNull File base, @NonNull String...segments) { + for (String segment : segments) { + base = new File(base, segment); + } + return base; + } + + /** + * Appends the given {@code segments} to the {@code base} file. + * + * @param base A base file path, non-empty and non-null. + * @param segments Individual folder or filename segments to append to the base path. + * @return A new file representing the concatenation of the base path with all the segments. + */ + public static File append(@NonNull String base, @NonNull String...segments) { + return append(new File(base), segments); + } + + /** + * Helper to delete a file or a directory. + * For a directory, recursively deletes all of its content. + * Files that cannot be deleted right away are marked for deletion on exit. + * It's ok for the file or folder to not exist at all. + * The argument can be null. + */ + @Override + public void deleteFileOrFolder(@NonNull File fileOrFolder) { + if (fileOrFolder != null) { + if (isDirectory(fileOrFolder)) { + // Must delete content recursively first + File[] files = fileOrFolder.listFiles(); + if (files != null) { + for (File item : files) { + deleteFileOrFolder(item); + } + } + } + + // Don't try to delete it if it doesn't exist. + if (!exists(fileOrFolder)) { + return; + } + + if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) { + // Trying to delete a resource on windows might fail if there's a file + // indexer locking the resource. Generally retrying will be enough to + // make it work. + // + // Try for half a second before giving up. + + for (int i = 0; i < 5; i++) { + if (fileOrFolder.delete()) { + return; + } + + try { + Thread.sleep(100 /*ms*/); + } catch (InterruptedException e) { + // Ignore. + } + } + + fileOrFolder.deleteOnExit(); + + } else { + // On Linux or Mac, just straight deleting it should just work. + + if (!fileOrFolder.delete()) { + fileOrFolder.deleteOnExit(); + } + } + } + } + + /** + * Sets the executable Unix permission (+x) on a file or folder. + *

+ * This attempts to use File#setExecutable through reflection if + * it's available. + * If this is not available, this invokes a chmod exec instead, + * so there is no guarantee of it being fast. + *

+ * Caller must make sure to not invoke this under Windows. + * + * @param file The file to set permissions on. + * @throws IOException If an I/O error occurs + */ + @Override + public void setExecutablePermission(@NonNull File file) throws IOException { + + if (sFileSetExecutable != null) { + try { + sFileSetExecutable.invoke(file, sFileSetExecutableParams); + return; + } catch (IllegalArgumentException e) { + // we'll run chmod below + } catch (IllegalAccessException e) { + // we'll run chmod below + } catch (InvocationTargetException e) { + // we'll run chmod below + } + } + + Runtime.getRuntime().exec(new String[] { + "chmod", "+x", file.getAbsolutePath() //$NON-NLS-1$ //$NON-NLS-2$ + }); + } + + @Override + public void setReadOnly(@NonNull File file) { + file.setReadOnly(); + } + + /** + * Copies a binary file. + * + * @param source the source file to copy. + * @param dest the destination file to write. + * @throws FileNotFoundException if the source file doesn't exist. + * @throws IOException if there's a problem reading or writing the file. + */ + @Override + public void copyFile(@NonNull File source, @NonNull File dest) throws IOException { + byte[] buffer = new byte[8192]; + + FileInputStream fis = null; + FileOutputStream fos = null; + try { + fis = new FileInputStream(source); + fos = new FileOutputStream(dest); + + int read; + while ((read = fis.read(buffer)) != -1) { + fos.write(buffer, 0, read); + } + + } finally { + if (fis != null) { + try { + fis.close(); + } catch (IOException e) { + // Ignore. + } + } + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + // Ignore. + } + } + } + } + + /** + * Checks whether 2 binary files are the same. + * + * @param file1 the source file to copy + * @param file2 the destination file to write + * @throws FileNotFoundException if the source files don't exist. + * @throws IOException if there's a problem reading the files. + */ + @Override + public boolean isSameFile(@NonNull File file1, @NonNull File file2) throws IOException { + + if (file1.length() != file2.length()) { + return false; + } + + FileInputStream fis1 = null; + FileInputStream fis2 = null; + + try { + fis1 = new FileInputStream(file1); + fis2 = new FileInputStream(file2); + + byte[] buffer1 = new byte[8192]; + byte[] buffer2 = new byte[8192]; + + int read1; + while ((read1 = fis1.read(buffer1)) != -1) { + int read2 = 0; + while (read2 < read1) { + int n = fis2.read(buffer2, read2, read1 - read2); + if (n == -1) { + break; + } + } + + if (read2 != read1) { + return false; + } + + if (!Arrays.equals(buffer1, buffer2)) { + return false; + } + } + } finally { + if (fis2 != null) { + try { + fis2.close(); + } catch (IOException e) { + // ignore + } + } + if (fis1 != null) { + try { + fis1.close(); + } catch (IOException e) { + // ignore + } + } + } + + return true; + } + + /** Invokes {@link File#isFile()} on the given {@code file}. */ + @Override + public boolean isFile(@NonNull File file) { + return file.isFile(); + } + + /** Invokes {@link File#isDirectory()} on the given {@code file}. */ + @Override + public boolean isDirectory(@NonNull File file) { + return file.isDirectory(); + } + + /** Invokes {@link File#exists()} on the given {@code file}. */ + @Override + public boolean exists(@NonNull File file) { + return file.exists(); + } + + /** Invokes {@link File#length()} on the given {@code file}. */ + @Override + public long length(@NonNull File file) { + return file.length(); + } + + /** + * Invokes {@link File#delete()} on the given {@code file}. + * Note: for a recursive folder version, consider {@link #deleteFileOrFolder(File)}. + */ + @Override + public boolean delete(@NonNull File file) { + return file.delete(); + } + + /** Invokes {@link File#mkdirs()} on the given {@code file}. */ + @Override + public boolean mkdirs(@NonNull File file) { + return file.mkdirs(); + } + + /** + * Invokes {@link File#listFiles()} on the given {@code file}. + * Contrary to the Java API, this returns an empty array instead of null when the + * directory does not exist. + */ + @Override + @NonNull + public File[] listFiles(@NonNull File file) { + File[] r = file.listFiles(); + if (r == null) { + return EMPTY_FILE_ARRAY; + } else { + return r; + } + } + + /** Invokes {@link File#renameTo(File)} on the given files. */ + @Override + public boolean renameTo(@NonNull File oldFile, @NonNull File newFile) { + return oldFile.renameTo(newFile); + } + + /** Creates a new {@link OutputStream} for the given {@code file}. */ + @Override + @NonNull + public OutputStream newFileOutputStream(@NonNull File file) throws FileNotFoundException { + return new FileOutputStream(file); + } + + /** Creates a new {@link InputStream} for the given {@code file}. */ + @Override + @NonNull + public InputStream newFileInputStream(@NonNull File file) throws FileNotFoundException { + return new FileInputStream(file); + } + + @Override + @NonNull + public Properties loadProperties(@NonNull File file) { + Properties props = new Properties(); + Closer closer = Closer.create(); + try { + FileInputStream fis = closer.register(new FileInputStream(file)); + props.load(fis); + } catch (IOException ignore) { + } finally { + try { + closer.close(); + } catch (IOException e) { + } + } + return props; + } + + @Override + public void saveProperties( + @NonNull File file, + @NonNull Properties props, + @NonNull String comments) throws IOException { + Closer closer = Closer.create(); + try { + OutputStream fos = closer.register(newFileOutputStream(file)); + props.store(fos, comments); + } catch (Throwable e) { + throw closer.rethrow(e); + } finally { + closer.close(); + } + } + + @Override + public long lastModified(@NonNull File file) { + return file.lastModified(); + } + + /** + * Computes a relative path from "toBeRelative" relative to "baseDir". + * + * Rule: + * - let relative2 = makeRelative(path1, path2) + * - then pathJoin(path1 + relative2) == path2 after canonicalization. + * + * Principle: + * - let base = /c1/c2.../cN/a1/a2../aN + * - let toBeRelative = /c1/c2.../cN/b1/b2../bN + * - result is removes the common paths, goes back from aN to cN then to bN: + * - result = ../..../../1/b2../bN + * + * @param baseDir The base directory to be relative to. + * @param toBeRelative The file or directory to make relative to the base. + * @return A path that makes toBeRelative relative to baseDir. + * @throws IOException If drive letters don't match on Windows or path canonicalization fails. + */ + @NonNull + public static String makeRelative(@NonNull File baseDir, @NonNull File toBeRelative) + throws IOException { + return makeRelativeImpl( + baseDir.getCanonicalPath(), + toBeRelative.getCanonicalPath(), + SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS, + File.separator); + } + + /** + * Implementation detail of makeRelative to make it testable + * Independently of the platform. + */ + @NonNull + static String makeRelativeImpl(@NonNull String path1, + @NonNull String path2, + boolean isWindows, + @NonNull String dirSeparator) + throws IOException { + if (isWindows) { + // Check whether both path are on the same drive letter, if any. + String p1 = path1; + String p2 = path2; + char drive1 = (p1.length() >= 2 && p1.charAt(1) == ':') ? p1.charAt(0) : 0; + char drive2 = (p2.length() >= 2 && p2.charAt(1) == ':') ? p2.charAt(0) : 0; + if (drive1 != drive2) { + // Either a mix of UNC vs drive or not the same drives. + throw new IOException("makeRelative: incompatible drive letters"); + } + } + + String[] segments1 = path1.split(Pattern.quote(dirSeparator)); + String[] segments2 = path2.split(Pattern.quote(dirSeparator)); + + int len1 = segments1.length; + int len2 = segments2.length; + int len = Math.min(len1, len2); + int start = 0; + for (; start < len; start++) { + // On Windows should compare in case-insensitive. + // Mac & Linux file systems can be both type, although their default + // is generally to have a case-sensitive file system. + if (( isWindows && !segments1[start].equalsIgnoreCase(segments2[start])) || + (!isWindows && !segments1[start].equals(segments2[start]))) { + break; + } + } + + StringBuilder result = new StringBuilder(); + for (int i = start; i < len1; i++) { + result.append("..").append(dirSeparator); + } + while (start < len2) { + result.append(segments2[start]); + if (++start < len2) { + result.append(dirSeparator); + } + } + + return result.toString(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/AddonManifestIniProps.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/AddonManifestIniProps.java new file mode 100644 index 00000000..de82b2e8 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/AddonManifestIniProps.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2014 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.sdklib.repository; + +/** + * This class describes the properties that can appear in an add-on's manifest.ini file. + *

+ * These constants are public and part of the SDK Manager public API. + * Once published we can't change them arbitrarily since various parts + * of our build process depend on them. + */ +public class AddonManifestIniProps { + + /** + * The display name of the add-on. Always present.
+ * In source.properties, this matches {@link PkgProps#ADDON_NAME_DISPLAY}. + */ + public static final String ADDON_NAME = "name"; //$NON-NLS-1$ + + /** + * The optional "name id" of the add-on.
+ * In source.properties, this matches {@link PkgProps#ADDON_NAME_ID}. + *

+ * Historically the manifest used to have only a 'name' property for both internal unique id + * and display, in which case the internal id was synthesized using the display name and + * matching a {@code [a-zA-Z0-9_-]+} pattern (see {@code Addonpackage#sanitizeDisplayToNameId} + * for details.) + */ + public static final String ADDON_NAME_ID = "name-id"; //$NON-NLS-1$ + + /** + * The display vendor of the add-on. Always present.
+ * In source.properties, this matches {@link PkgProps#ADDON_VENDOR_DISPLAY}. + */ + public static final String ADDON_VENDOR = "vendor"; //$NON-NLS-1$ + + /** + * The optional vendor id of the add-on.
+ * In source.properties, this matches {@link PkgProps#ADDON_VENDOR_ID}. + *

+ * Historically the manifest used to have only a 'vendor' property for both internal unique id + * and display, in which case the internal id was synthesized using the display name and + * matching a {@code [a-zA-Z0-9_-]+} pattern (see {@code Addonpackage#sanitizeDisplayToNameId} + * for details.) + */ + public static final String ADDON_VENDOR_ID = "vendor-id"; //$NON-NLS-1$ + + /** + * The free description string of the add-on.
+ * Not saved in source.properties. + */ + public static final String ADDON_DESCRIPTION = "description"; //$NON-NLS-1$ + + /** + * The revision of the add-on.
+ * In source.properties, this matches {@link PkgProps#PKG_REVISION}. + */ + public static final String ADDON_REVISION = "revision"; //$NON-NLS-1$ + + /** + * An older/obsolete attribute for the revision of the add-on.
+ * The name was changed as it is ambiguous (platform version vs platform revision.) + */ + public static final String ADDON_REVISION_OLD = "version"; //$NON-NLS-1$ + + /** + * The API level of the add-on, always an integer.
+ * Note: add-ons do not currently support API codenames.
+ * In source.properties, this matches {@link PkgProps#VERSION_API_LEVEL}. + */ + public static final String ADDON_API = "api"; //$NON-NLS-1$ + + /** + * The list of libraries of the add-on.
+ * This is a string in the format "java.package1;java.package2;...java.packageN". + * For each library's java package name, the manifest.ini contains a key with + * value "library.jar;Jar Description String". Example: + *

+     * libraries=com.example.foo;com.example.bar
+     * com.example.foo=foo.jar;Foo Library
+     * com.example.bar=bar.jar;Bar Library
+     * 
+ * Not saved in source.properties. + */ + public static final String ADDON_LIBRARIES = "libraries"; //$NON-NLS-1$ + + /** + * An optional default skin string of the add-on.
+ * Not saved in source.properties. + */ + public static final String ADDON_DEFAULT_SKIN = "skin"; //$NON-NLS-1$ + + /** + * An optional USB vendor string for the add-on.
+ * Not saved in source.properties. + */ + public static final String ADDON_USB_VENDOR = "usb-vendor"; //$NON-NLS-1$ + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/FullRevision.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/FullRevision.java new file mode 100644 index 00000000..e71499f8 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/FullRevision.java @@ -0,0 +1,426 @@ +/* + * Copyright (C) 2012 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.sdklib.repository; + +import com.android.annotations.NonNull; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +/** + * Package multi-part revision number composed of a tuple + * (major.minor.micro) and an optional preview revision + * (the lack of a preview number indicates it's not a preview + * but a final package.) + * + * @see MajorRevision + */ +public class FullRevision implements Comparable { + + public static final int MISSING_MAJOR_REV = 0; + public static final int IMPLICIT_MINOR_REV = 0; + public static final int IMPLICIT_MICRO_REV = 0; + public static final int NOT_A_PREVIEW = 0; + + /** Only major revision specified: 1 term */ + protected static final int PRECISION_MAJOR = 1; + /** Only major and minor revisions specified: 2 terms (x.y) */ + protected static final int PRECISION_MINOR = 2; + /** Major, minor and micro revisions specified: 3 terms (x.y.z) */ + protected static final int PRECISION_MICRO = 3; + /** Major, minor, micro and preview revisions specified: 4 terms (x.y.z-rcN) */ + protected static final int PRECISION_PREVIEW = 4; + + public static final FullRevision NOT_SPECIFIED = new FullRevision(MISSING_MAJOR_REV); + + private static final Pattern FULL_REVISION_PATTERN = + // 1=major 2=minor 3=micro 4=separator 5=previewType 6=preview + Pattern.compile("\\s*([0-9]+)(?:\\.([0-9]+)(?:\\.([0-9]+))?)?([\\s-]*)?(?:(rc|alpha|beta)([0-9]+))?\\s*"); + + protected static final String DEFAULT_SEPARATOR = " "; + + private final int mMajor; + private final int mMinor; + private final int mMicro; + private final int mPreview; + private final String mPreviewSeparator; + private final PreviewType mPreviewType; + + public enum PreviewType { + ALPHA("alpha"), + BETA("beta"), + RC("rc") + ; + + final String name; + + PreviewType(String name) { + this.name = name; + } + } + + public FullRevision(int major) { + this(major, IMPLICIT_MINOR_REV, IMPLICIT_MICRO_REV); + } + + public FullRevision(int major, int minor, int micro) { + this(major, minor, micro, NOT_A_PREVIEW); + } + + public FullRevision(int major, int minor, int micro, int preview) { + this(major, minor, micro, PreviewType.RC, preview, DEFAULT_SEPARATOR); + } + + public FullRevision(int major, int minor, int micro, @NonNull PreviewType previewType, + int preview, @NonNull String previewSeparator) { + mMajor = major; + mMinor = minor; + mMicro = micro; + mPreview = preview; + mPreviewSeparator = previewSeparator; + mPreviewType = previewType; + } + + public int getMajor() { + return mMajor; + } + + public int getMinor() { + return mMinor; + } + + public int getMicro() { + return mMicro; + } + + @NonNull + protected String getSeparator() { + return mPreviewSeparator; + } + + public boolean isPreview() { + return mPreview > NOT_A_PREVIEW; + } + + public int getPreview() { + return mPreview; + } + + /** + * Parses a string of format "major.minor.micro rcPreview" and returns + * a new {@link FullRevision} for it. All the fields except major are + * optional. + *

+ * The parsing is equivalent to the pseudo-BNF/regexp: + *

+     *   Major/Minor/Micro/Preview := [0-9]+
+     *   Revision := Major ('.' Minor ('.' Micro)? )? \s* ('rc'Preview)?
+     * 
+ * + * @param revision A non-null revision to parse. + * @return A new non-null {@link FullRevision}. + * @throws NumberFormatException if the parsing failed. + */ + @NonNull + public static FullRevision parseRevision(@NonNull String revision) + throws NumberFormatException { + return parseRevisionImpl(revision, true /*supportMinorMicro*/, true /*supportPreview*/, + false /*keepPrevision*/); + } + + @NonNull + protected static FullRevision parseRevisionImpl(@NonNull String revision, + boolean supportMinorMicro, + boolean supportPreview, + boolean keepPrecision) + throws NumberFormatException { + if (revision == null) { + throw new NumberFormatException("revision is "); //$NON-NLS-1$ + } + + Throwable cause = null; + String error = null; + try { + Matcher m = FULL_REVISION_PATTERN.matcher(revision); + if (m != null && m.matches()) { + int major = Integer.parseInt(m.group(1)); + + int minor = IMPLICIT_MINOR_REV; + int micro = IMPLICIT_MICRO_REV; + int preview = NOT_A_PREVIEW; + int precision = PRECISION_MAJOR; + String previewSeparator = " "; + PreviewType previewType = PreviewType.RC; + + String s = m.group(2); + if (s != null) { + if (!supportMinorMicro) { + error = " -- Minor number not supported"; //$NON-NLS-1$ + } else { + minor = Integer.parseInt(s); + precision = PRECISION_MINOR; + } + } + + s = m.group(3); + if (s != null) { + if (!supportMinorMicro) { + error = " -- Micro number not supported"; //$NON-NLS-1$ + } else { + micro = Integer.parseInt(s); + precision = PRECISION_MICRO; + } + } + + s = m.group(6); + if (s != null) { + if (!supportPreview) { + error = " -- Preview number not supported"; //$NON-NLS-1$ + } else { + preview = Integer.parseInt(s); + previewSeparator = m.group(4); + precision = PRECISION_PREVIEW; + + String previewTypeName = m.group(5); + for (PreviewType pt : PreviewType.values()) { + if (pt.name.equals(previewTypeName)) { + previewType = pt; + break; + } + } + } + } + + if (error == null) { + if (keepPrecision) { + return new PreciseRevision(major, minor, micro, preview, precision, + previewSeparator); + } else { + return new FullRevision(major, minor, micro, previewType, preview, previewSeparator); + } + } + } + } catch (Throwable t) { + cause = t; + } + + NumberFormatException n = new NumberFormatException( + "Invalid revision: " //$NON-NLS-1$ + + revision + + (error == null ? "" : error)); + if (cause != null) { + n.initCause(cause); + } + throw n; + } + + /** + * Returns the version in a fixed format major.minor.micro + * with an optional "rc preview#". For example it would + * return "18.0.0", "18.1.0" or "18.1.2 rc5". + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(mMajor) + .append('.').append(mMinor) + .append('.').append(mMicro); + + if (mPreview != NOT_A_PREVIEW) { + sb.append(mPreviewSeparator).append(mPreviewType.name).append(mPreview); + } + + return sb.toString(); + } + + /** + * Returns the version in a dynamic format "major.minor.micro rc#". + * This is similar to {@link #toString()} except it omits minor, micro + * or preview versions when they are zero. + * For example it would return "18 rc1" instead of "18.0.0 rc1", + * or "18.1 rc2" instead of "18.1.0 rc2". + */ + public String toShortString() { + StringBuilder sb = new StringBuilder(); + sb.append(mMajor); + if (mMinor > 0 || mMicro > 0) { + sb.append('.').append(mMinor); + } + if (mMicro > 0) { + sb.append('.').append(mMicro); + } + if (mPreview != NOT_A_PREVIEW) { + sb.append(mPreviewSeparator).append(mPreviewType.name).append(mPreview); + } + + return sb.toString(); + } + + /** + * Returns the version number as an integer array, in the form + * [major, minor, micro] or [major, minor, micro, preview]. + * + * This is useful to initialize an instance of + * {@code org.apache.tools.ant.util.DeweyDecimal} using a + * {@link FullRevision}. + * + * @param includePreview If true the output will contain 4 fields + * to include the preview number (even if 0.) If false the output + * will contain only 3 fields (major, minor and micro.) + * @return A new int array, never null, with either 3 or 4 fields. + */ + public int[] toIntArray(boolean includePreview) { + int size = includePreview ? 4 : 3; + int[] result = new int[size]; + result[0] = mMajor; + result[1] = mMinor; + result[2] = mMicro; + if (result.length > 3) { + result[3] = mPreview; + } + return result; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + mMajor; + result = prime * result + mMinor; + result = prime * result + mMicro; + result = prime * result + mPreview; + result = prime * result + mPreviewType.hashCode(); + return result; + } + + @Override + public boolean equals(Object rhs) { + if (this == rhs) { + return true; + } + if (rhs == null) { + return false; + } + if (!(rhs instanceof FullRevision)) { + return false; + } + FullRevision other = (FullRevision) rhs; + if (mMajor != other.mMajor) { + return false; + } + if (mMinor != other.mMinor) { + return false; + } + if (mMicro != other.mMicro) { + return false; + } + if (mPreview != other.mPreview) { + return false; + } + if (mPreviewType != other.mPreviewType) { + return false; + } + return true; + } + + /** + * Trivial comparison of a version, e.g 17.1.2 < 18.0.0. + * + * Note that preview/release candidate are released before their final version, + * so "18.0.0 rc1" comes below "18.0.0". The best way to think of it as if the + * lack of preview number was "+inf": + * "18.1.2 rc5" => "18.1.2.5" so its less than "18.1.2.+INF" but more than "18.1.1.0" + * and more than "18.1.2.4" + * + * @param rhs The right-hand side {@link FullRevision} to compare with. + * @return <0 if lhs < rhs; 0 if lhs==rhs; >0 if lhs > rhs. + */ + @Override + public int compareTo(FullRevision rhs) { + return compareTo(rhs, PreviewComparison.COMPARE_NUMBER); + } + + /** + * Trivial comparison of a version, e.g 17.1.2 < 18.0.0. + * + * Note that preview/release candidate are released before their final version, + * so "18.0.0 rc1" comes below "18.0.0". The best way to think of it as if the + * lack of preview number was "+inf": + * "18.1.2 rc5" => "18.1.2.5" so its less than "18.1.2.+INF" but more than "18.1.1.0" + * and more than "18.1.2.4" + * + * @param rhs The right-hand side {@link FullRevision} to compare with. + * @param comparePreview How to compare the preview value. + * @return <0 if lhs < rhs; 0 if lhs==rhs; >0 if lhs > rhs. + */ + public int compareTo(FullRevision rhs, PreviewComparison comparePreview) { + int delta = mMajor - rhs.mMajor; + if (delta != 0) { + return delta; + } + + delta = mMinor - rhs.mMinor; + if (delta != 0) { + return delta; + } + + delta = mMicro - rhs.mMicro; + if (delta != 0) { + return delta; + } + + int p1, p2; + switch (comparePreview) { + case IGNORE: + // Nothing to compare. + break; + + case COMPARE_NUMBER: + if (!mPreviewType.equals(rhs.mPreviewType)) { + return mPreviewType.compareTo(rhs.mPreviewType); + } + + p1 = mPreview == NOT_A_PREVIEW ? Integer.MAX_VALUE : mPreview; + p2 = rhs.mPreview == NOT_A_PREVIEW ? Integer.MAX_VALUE : rhs.mPreview; + delta = p1 - p2; + break; + + case COMPARE_TYPE: + p1 = mPreview == NOT_A_PREVIEW ? 1 : 0; + p2 = rhs.mPreview == NOT_A_PREVIEW ? 1 : 0; + delta = p1 - p2; + break; + } + return delta; + } + + /** Indicates how to compare the preview field in + * {@link FullRevision#compareTo(FullRevision, PreviewComparison)} */ + public enum PreviewComparison { + /** Both revisions must have exactly the same preview number. */ + COMPARE_NUMBER, + /** Both revisions must have the same preview type (both must be previews + * or both must not be previews, but the actual number is irrelevant.) + * This is the most typical choice used to find updates of the same type. */ + COMPARE_TYPE, + /** The preview field is ignored and not used in the comparison. */ + IGNORE + } + + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/IDescription.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/IDescription.java new file mode 100644 index 00000000..1e44964d --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/IDescription.java @@ -0,0 +1,40 @@ +/* + * 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.sdklib.repository; + +/** + * Interface for elements that can provide a description of themselves. + */ +public interface IDescription { + + /** + * Returns a description of the given element. Cannot be null. + *

+ * A description is a multi-line of text, typically much more + * elaborate than what {@link Object#toString()} would provide. + */ + String getShortDescription(); + + /** + * Returns a description of the given element. Cannot be null. + *

+ * A description is a multi-line of text, typically much more + * elaborate than what {@link Object#toString()} would provide. + */ + String getLongDescription(); + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/IListDescription.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/IListDescription.java new file mode 100644 index 00000000..c3ae3653 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/IListDescription.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2013 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.sdklib.repository; + +/** + * Interface for elements that can provide a description of themselves. + */ +public interface IListDescription { + + /** + * Returns a description of this package that is suitable for a list display. + * Should not be empty. Must never be null. + *

+ * Note that this is the "base" name for the package + * with no specific revision nor API mentioned. + * In contrast, {@link IDescription#getShortDescription()} should be used if you + * want more details such as the package revision number or the API, if applicable. + */ + String getListDescription(); +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/ISdkChangeListener.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/ISdkChangeListener.java new file mode 100644 index 00000000..5c0cab86 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/ISdkChangeListener.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2010 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.sdklib.repository; + + +/** + * Interface for listeners on SDK modifications by the SDK Manager UI. + * This notifies when the SDK manager is first loading the SDK or before/after it installed + * a package. + */ +public interface ISdkChangeListener { + /** + * Invoked when the content of the SDK is being loaded by the SDK Manager UI + * for the first time. + * This is generally followed by a call to {@link #onSdkReload()} + * or by a call to {@link #preInstallHook()}. + */ + void onSdkLoaded(); + + /** + * Invoked when the SDK Manager UI is about to start installing packages. + * This will be followed by a call to {@link #postInstallHook()}. + */ + void preInstallHook(); + + /** + * Invoked when the SDK Manager UI is done installing packages. + * Some new packages might have been installed or the user might have cancelled the operation. + * This is generally followed by a call to {@link #onSdkReload()}. + */ + void postInstallHook(); + + /** + * Invoked when the content of the SDK is being reloaded by the SDK Manager UI, + * typically after a package was installed. The SDK content might or might not + * have changed. + */ + void onSdkReload(); +} + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/License.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/License.java new file mode 100644 index 00000000..7040ec11 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/License.java @@ -0,0 +1,176 @@ +/* + * 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.sdklib.repository; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdklib.SdkManager; +import com.android.utils.FileUtils; +import com.google.common.base.Charsets; +import com.google.common.hash.Hashing; +import com.google.common.io.Files; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; + +/** + * License text, with an optional license XML reference. + */ +public class License { + private final String mLicense; + private final String mLicenseRef; + private final String mLicenseHash; + + private static final String LICENSE_DIR = "licenses"; + + public License(@NonNull String license, @Nullable String licenseRef) { + mLicense = license; + mLicenseRef = licenseRef; + mLicenseHash = Hashing.sha1().hashBytes(mLicense.getBytes()).toString(); + } + + /** Returns the license text. Never null. */ + @NonNull + public String getLicense() { + return mLicense; + } + + /** Returns the hash of the license text. Never null. */ + @NonNull + public String getLicenseHash() { + return mLicenseHash; + } + + /** + * Returns the license XML reference. + * Could be null, e.g. in tests or synthetic packages + * recreated from local source.properties. + */ + @Nullable + public String getLicenseRef() { + return mLicenseRef; + } + + /** + * Returns a string representation of the license, useful for debugging. + * This is not designed to be shown to the user. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(""); + return sb.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + ((mLicense == null) ? 0 : mLicense.hashCode()); + result = prime * result + + ((mLicenseRef == null) ? 0 : mLicenseRef.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof License)) { + return false; + } + License other = (License) obj; + if (mLicense == null) { + if (other.mLicense != null) { + return false; + } + } else if (!mLicense.equals(other.mLicense)) { + return false; + } + if (mLicenseRef == null) { + if (other.mLicenseRef != null) { + return false; + } + } else if (!mLicenseRef.equals(other.mLicenseRef)) { + return false; + } + return true; + } + + /** + * Checks whether this license has previously been accepted. + * @param sdkRoot The root directory of the Android SDK + * @return true if this license has already been accepted + */ + public boolean checkAccepted(@Nullable File sdkRoot) { + if (sdkRoot == null) { + return false; + } + File licenseDir = new File(sdkRoot, LICENSE_DIR); + File licenseFile = new File(licenseDir, mLicenseRef == null ? mLicenseHash : mLicenseRef); + if (!licenseFile.exists()) { + return false; + } + try { + String hash = Files.readFirstLine(licenseFile, Charsets.UTF_8); + return hash.equals(mLicenseHash); + } catch (IOException e) { + return false; + } + } + + /** + * Marks this license as accepted. + * + * @param sdkRoot The root directory of the Android SDK + * @return true if the acceptance was persisted successfully. + */ + public boolean setAccepted(@Nullable File sdkRoot) { + if (sdkRoot == null) { + return false; + } + if (checkAccepted(sdkRoot)) { + return true; + } + File licenseDir = new File(sdkRoot, LICENSE_DIR); + if (licenseDir.exists() && !licenseDir.isDirectory()) { + return false; + } + if (!licenseDir.exists()) { + licenseDir.mkdir(); + } + File licenseFile = new File(licenseDir, mLicenseRef == null ? mLicenseHash : mLicenseRef); + try { + Files.write(mLicenseHash, licenseFile, Charsets.UTF_8); + } + catch (IOException e) { + return false; + } + return true; + } +} + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/MajorRevision.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/MajorRevision.java new file mode 100644 index 00000000..b6d25573 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/MajorRevision.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2012 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.sdklib.repository; + +import com.android.annotations.NonNull; + + +/** + * Package revision number composed of a single major revision. + *

+ * Contrary to a {@link FullRevision}, a {@link MajorRevision} does not + * provide minor, micro and preview revision numbers -- these are all + * set to zero. + */ +public class MajorRevision extends FullRevision { + + public MajorRevision(FullRevision fullRevision) { + super(fullRevision.getMajor(), IMPLICIT_MINOR_REV, IMPLICIT_MICRO_REV); + } + + public MajorRevision(int major) { + super(major, IMPLICIT_MINOR_REV, IMPLICIT_MICRO_REV); + } + + @Override + public String toString() { + return super.toShortString(); + } + + /** + * Parses a single-integer string and returns a new {@link MajorRevision} for it. + * + * @param revision A non-null revision to parse. + * @return A new non-null {@link MajorRevision}. + * @throws NumberFormatException if the parsing failed. + */ + @NonNull + public static MajorRevision parseRevision(@NonNull String revision) + throws NumberFormatException { + FullRevision r = parseRevisionImpl( + revision, false /*supportMinorMicro*/, false /*supportPreview*/, + false /*keepPrecision*/); + return new MajorRevision(r.getMajor()); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/NoPreviewRevision.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/NoPreviewRevision.java new file mode 100644 index 00000000..0280cb28 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/NoPreviewRevision.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2013 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.sdklib.repository; + +import com.android.annotations.NonNull; + + +/** + * Package multi-part revision number composed of a tuple + * (major.minor.micro) but without support for any optional preview number. + * + * @see FullRevision + */ +public class NoPreviewRevision extends FullRevision { + + public NoPreviewRevision(int major) { + this(major, IMPLICIT_MINOR_REV, IMPLICIT_MICRO_REV); + } + + public NoPreviewRevision(int major, int minor, int micro) { + super(major, minor, micro, NOT_A_PREVIEW); + } + + /** + * Parses a string of format "major.minor.micro" and returns + * a new {@link NoPreviewRevision} for it. All the fields except major are + * optional. + *

+ * The parsing is equivalent to the pseudo-BNF/regexp: + *

+     *   Major/Minor/Micro/Preview := [0-9]+
+     *   Revision := Major ('.' Minor ('.' Micro)? )? \s*
+     * 
+ * + * @param revision A non-null revision to parse. + * @return A new non-null {@link NoPreviewRevision}. + * @throws NumberFormatException if the parsing failed. + */ + @NonNull + public static NoPreviewRevision parseRevision(@NonNull String revision) + throws NumberFormatException { + FullRevision r = parseRevisionImpl( + revision, true /*supportMinorMicro*/, false /*supportPreview*/, + false /*keepPrecision*/); + return new NoPreviewRevision(r.getMajor(), r.getMinor(), r.getMicro()); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/PkgProps.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/PkgProps.java new file mode 100644 index 00000000..3126984f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/PkgProps.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2011 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.sdklib.repository; + + + +/** + * Public constants used by the repository when saving {@code source.properties} + * files in local packages. + *

+ * These constants are public and part of the SDK Manager public API. + * Once published we can't change them arbitrarily since various parts + * of our build process depend on them. + */ +public class PkgProps { + + // Base Package + public static final String PKG_REVISION = "Pkg.Revision"; //$NON-NLS-1$ + public static final String PKG_LICENSE = "Pkg.License"; //$NON-NLS-1$ + public static final String PKG_LICENSE_REF = "Pkg.LicenseRef"; //$NON-NLS-1$ + public static final String PKG_DESC = "Pkg.Desc"; //$NON-NLS-1$ + public static final String PKG_DESC_URL = "Pkg.DescUrl"; //$NON-NLS-1$ + public static final String PKG_RELEASE_NOTE = "Pkg.RelNote"; //$NON-NLS-1$ + public static final String PKG_RELEASE_URL = "Pkg.RelNoteUrl"; //$NON-NLS-1$ + public static final String PKG_SOURCE_URL = "Pkg.SourceUrl"; //$NON-NLS-1$ + public static final String PKG_OBSOLETE = "Pkg.Obsolete"; //$NON-NLS-1$ + public static final String PKG_LIST_DISPLAY = "Pkg.ListDisplay"; //$NON-NLS-1$ + + // AndroidVersion + + public static final String VERSION_API_LEVEL = "AndroidVersion.ApiLevel";//$NON-NLS-1$ + /** Code name of the platform if the platform is not final */ + public static final String VERSION_CODENAME = "AndroidVersion.CodeName";//$NON-NLS-1$ + + + // AddonPackage + + public static final String ADDON_NAME = "Addon.Name"; //$NON-NLS-1$ + public static final String ADDON_NAME_ID = "Addon.NameId"; //$NON-NLS-1$ + public static final String ADDON_NAME_DISPLAY = "Addon.NameDisplay"; //$NON-NLS-1$ + + public static final String ADDON_VENDOR = "Addon.Vendor"; //$NON-NLS-1$ + public static final String ADDON_VENDOR_ID = "Addon.VendorId"; //$NON-NLS-1$ + public static final String ADDON_VENDOR_DISPLAY = "Addon.VendorDisplay"; //$NON-NLS-1$ + + // DocPackage + + // ExtraPackage + + public static final String EXTRA_PATH = "Extra.Path"; //$NON-NLS-1$ + public static final String EXTRA_OLD_PATHS = "Extra.OldPaths"; //$NON-NLS-1$ + public static final String EXTRA_MIN_API_LEVEL = "Extra.MinApiLevel"; //$NON-NLS-1$ + public static final String EXTRA_PROJECT_FILES = "Extra.ProjectFiles"; //$NON-NLS-1$ + public static final String EXTRA_VENDOR = "Extra.Vendor"; //$NON-NLS-1$ + public static final String EXTRA_VENDOR_ID = "Extra.VendorId"; //$NON-NLS-1$ + public static final String EXTRA_VENDOR_DISPLAY = "Extra.VendorDisplay"; //$NON-NLS-1$ + public static final String EXTRA_NAME_DISPLAY = "Extra.NameDisplay"; //$NON-NLS-1$ + + // ILayoutlibVersion + + public static final String LAYOUTLIB_API = "Layoutlib.Api"; //$NON-NLS-1$ + public static final String LAYOUTLIB_REV = "Layoutlib.Revision"; //$NON-NLS-1$ + + // MinToolsPackage + + public static final String MIN_TOOLS_REV = "Platform.MinToolsRev"; //$NON-NLS-1$ + + // PlatformPackage + + public static final String PLATFORM_VERSION = "Platform.Version"; //$NON-NLS-1$ + /** Code name of the platform. This has no bearing on the package being a preview or not. */ + public static final String PLATFORM_CODENAME = "Platform.CodeName"; //$NON-NLS-1$ + public static final String PLATFORM_INCLUDED_ABI = "Platform.Included.Abi"; //$NON-NLS-1$ + + // ToolPackage + + public static final String MIN_PLATFORM_TOOLS_REV = "Platform.MinPlatformToolsRev";//$NON-NLS-1$ + public static final String MIN_BUILD_TOOLS_REV = "Platform.MinBuildToolsRev"; //$NON-NLS-1$ + + + // SamplePackage + + public static final String SAMPLE_MIN_API_LEVEL = "Sample.MinApiLevel"; //$NON-NLS-1$ + + // SystemImagePackage + + public static final String SYS_IMG_ABI = "SystemImage.Abi"; //$NON-NLS-1$ + public static final String SYS_IMG_TAG_ID = "SystemImage.TagId"; //$NON-NLS-1$ + public static final String SYS_IMG_TAG_DISPLAY = "SystemImage.TagDisplay"; //$NON-NLS-1$ +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/PreciseRevision.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/PreciseRevision.java new file mode 100644 index 00000000..81e97443 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/PreciseRevision.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2014 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.sdklib.repository; + +import com.android.annotations.NonNull; + +/** + * A {@link FullRevision} which distinguishes between x and x.0, x.0.0, x.y.0, etc; it basically + * keeps track of the precision of the revision string. + *

+ * This is vital when referencing Gradle artifact numbers, + * since versions x.y.0 and version x.y are not the same. + */ +public class PreciseRevision extends FullRevision { + private final int mPrecision; + + /** + * Parses a string of format "major.minor.micro rcPreview" and returns + * a new {@link com.android.sdklib.repository.PreciseRevision} for it. + * + * All the fields except major are optional. + *

+ * @param revision A non-null revision to parse. + * @return A new non-null {@link com.android.sdklib.repository.PreciseRevision}. + * @throws NumberFormatException if the parsing failed. + */ + @NonNull + public static PreciseRevision parseRevision(@NonNull String revision) + throws NumberFormatException { + return (PreciseRevision) parseRevisionImpl(revision, true /*supportMinorMicro*/, + true /*supportPreview*/, true /*keepPrevision*/); + } + + public PreciseRevision(int major) { + this(major, IMPLICIT_MINOR_REV, IMPLICIT_MICRO_REV, NOT_A_PREVIEW, PRECISION_MAJOR, + DEFAULT_SEPARATOR); + } + + public PreciseRevision(int major, int minor) { + this(major, minor, IMPLICIT_MICRO_REV, NOT_A_PREVIEW, PRECISION_MINOR, DEFAULT_SEPARATOR); + } + + public PreciseRevision(int major, int minor, int micro) { + this(major, minor, micro, NOT_A_PREVIEW, PRECISION_MICRO, DEFAULT_SEPARATOR); + } + + public PreciseRevision(int major, int minor, int micro, int preview) { + this(major, minor, micro, preview, PRECISION_PREVIEW, DEFAULT_SEPARATOR); + } + + PreciseRevision(int major, int minor, int micro, int preview, int precision, + String separator) { + this(major, minor, micro, preview, precision, separator, PreviewType.RC); + } + + PreciseRevision(int major, int minor, int micro, int preview, int precision, + String separator, FullRevision.PreviewType previewType) { + super(major, minor, micro, previewType, preview, separator); + mPrecision = precision; + } + + /** + * Returns the version in a fixed format major.minor.micro + * with an optional "rc preview#". For example it would + * return "18.0.0", "18.1.0" or "18.1.2 rc5". + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(getMajor()); + + if (mPrecision >= PRECISION_MINOR) { + sb.append('.').append(getMinor()); + if (mPrecision >= PRECISION_MICRO) { + sb.append('.').append(getMicro()); + if (mPrecision >= PRECISION_PREVIEW && isPreview()) { + sb.append(getSeparator()).append("rc").append(getPreview()); + } + } + } + + return sb.toString(); + } + + @Override + public String toShortString() { + return toString(); + } + + @Override + public int[] toIntArray(boolean includePreview) { + int[] result; + if (mPrecision >= PRECISION_PREVIEW) { + if (includePreview) { + result = new int[mPrecision]; + result[3] = getPreview(); + } else { + result = new int[mPrecision - 1]; + } + } else { + result = new int[mPrecision]; + } + result[0] = getMajor(); + if (mPrecision >= PRECISION_MINOR) { + result[1] = getMinor(); + if (mPrecision >= PRECISION_MICRO) { + result[2] = getMicro(); + } + } + + return result; + } + + @Override + public int hashCode() { + return 31 * super.hashCode() + mPrecision; + } + + @Override + public boolean equals(Object rhs) { + boolean equals = super.equals(rhs); + if (equals) { + if (!(rhs instanceof PreciseRevision)) { + return false; + } + PreciseRevision other = (PreciseRevision) rhs; + return mPrecision == other.mPrecision; + } + return false; + } + + public int compareTo(PreciseRevision rhs, PreviewComparison comparePreview) { + int delta = super.compareTo(rhs, comparePreview); + if (delta == 0) { + return mPrecision - rhs.mPrecision; + } + return delta; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/README.txt b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/README.txt new file mode 100644 index 00000000..e6e0f633 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/README.txt @@ -0,0 +1,25 @@ +This directory contains the XML Schemas (XSD) used by the Android SDK Repository. + +The repository exports all the packages that compose the SDK as well as +various manifest that define what is available in the repository. +The XML schemas available here allows clients to validate the manifests. + +TODO: +- overview of schemas +- principles of design +- principles of evolution vs revision numbers +- naming convention +- using by "make sdk_repo" + + +Naming Convention +----------------- + +Repository schemas are named sdk-type-N.xsd where +- type is either addon, addons-list or repository. +- N is the schema revision number, starting at 1 and increment with each revision. + +Schemas can also be named -sdk-type-N.xsd. +The dash prefix means this schema is a *future* schema that is not yet +used in production. This allows the repository to test future schemas +before they are deployed. diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/RepoConstants.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/RepoConstants.java new file mode 100644 index 00000000..44f6f15c --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/RepoConstants.java @@ -0,0 +1,213 @@ +/* + * 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.sdklib.repository; + +import java.io.InputStream; + + + +/** + * Public constants common to the sdk-repository and sdk-addon XML Schemas. + * @deprecated moved to studio + */ +public class RepoConstants { + + /** The license definition. */ + public static final String NODE_LICENSE = "license"; //$NON-NLS-1$ + /** The optional uses-license for all packages or for a lib. */ + public static final String NODE_USES_LICENSE = "uses-license"; //$NON-NLS-1$ + /** The revision, an int > 0, for all packages. */ + public static final String NODE_REVISION = "revision"; //$NON-NLS-1$ + /** The optional description for all packages or for a lib. */ + public static final String NODE_DESCRIPTION = "description"; //$NON-NLS-1$ + /** The optional description URL for all packages. */ + public static final String NODE_DESC_URL = "desc-url"; //$NON-NLS-1$ + /** The optional release note for all packages. */ + public static final String NODE_RELEASE_NOTE = "release-note"; //$NON-NLS-1$ + /** The optional release note URL for all packages. */ + public static final String NODE_RELEASE_URL = "release-url"; //$NON-NLS-1$ + /** The optional obsolete qualifier for all packages. */ + public static final String NODE_OBSOLETE = "obsolete"; //$NON-NLS-1$ + /** The optional project-files provided by extra packages. */ + public static final String NODE_PROJECT_FILES = "project-files"; //$NON-NLS-1$ + + /** A system-image package. */ + public static final String NODE_SYSTEM_IMAGE = "system-image"; //$NON-NLS-1$ + + /* An included-ABI element for a system-image package. */ + public static final String NODE_ABI_INCLUDED = "included-abi"; //$NON-NLS-1$ + /* An ABI element for a system-image package. */ + public static final String NODE_ABI = "abi"; //$NON-NLS-1$ + + /** The optional minimal tools revision required by platform & extra packages. */ + public static final String NODE_MIN_TOOLS_REV = "min-tools-rev"; //$NON-NLS-1$ + /** The optional minimal platform-tools revision required by tool packages. */ + public static final String NODE_MIN_PLATFORM_TOOLS_REV = "min-platform-tools-rev"; //$NON-NLS-1$ + /** The optional minimal API level required by extra packages. */ + public static final String NODE_MIN_API_LEVEL = "min-api-level"; //$NON-NLS-1$ + + /** The version, a string, for platform packages. */ + public static final String NODE_VERSION = "version"; //$NON-NLS-1$ + /** The api-level, an int > 0, for platform, add-on and doc packages. */ + public static final String NODE_API_LEVEL = "api-level"; //$NON-NLS-1$ + /** The codename, a string, for platform packages. */ + public static final String NODE_CODENAME = "codename"; //$NON-NLS-1$ + /** The *old* vendor, a string, for add-on and extra packages. + * Replaced by {@link #NODE_VENDOR_DISPLAY} and {@link #NODE_VENDOR_ID} in addon-v4.xsd. */ + public static final String NODE_VENDOR = "vendor"; //$NON-NLS-1$ + /** The vendor display string, for add-on and extra packages. */ + public static final String NODE_VENDOR_DISPLAY = "vendor-display"; //$NON-NLS-1$ + /** The unique vendor id string, for add-on and extra packages. */ + public static final String NODE_VENDOR_ID = "vendor-id"; //$NON-NLS-1$ + /** The name, a string, for add-on packages or for libraries. + * Replaced by {@link #NODE_NAME_DISPLAY} and {@link #NODE_NAME_ID} in addon-v4.xsd. */ + public static final String NODE_NAME = "name"; //$NON-NLS-1$ + /** The name display string, for add-on packages or for libraries. */ + public static final String NODE_NAME_DISPLAY = "name-display"; //$NON-NLS-1$ + /** The unique name id string, for add-on packages or for libraries. */ + public static final String NODE_NAME_ID = "name-id"; //$NON-NLS-1$ + /** The optional string used to display a package in a list view. */ + public static final String NODE_LIST_DISPLAY = "list-display"; //$NON-NLS-1$ + + /** A layoutlib package. */ + public static final String NODE_LAYOUT_LIB = "layoutlib"; //$NON-NLS-1$ + /** The API integer for a layoutlib element. */ + public static final String NODE_API = "api"; //$NON-NLS-1$ + + /** The libs container, optional for an add-on. */ + public static final String NODE_LIBS = "libs"; //$NON-NLS-1$ + /** A lib element in a libs container. */ + public static final String NODE_LIB = "lib"; //$NON-NLS-1$ + + /** The path segment, a string, for extra packages. */ + public static final String NODE_PATH = "path"; //$NON-NLS-1$ + + /** The old_path segments, a string, for extra packages. */ + public static final String NODE_OLD_PATHS = "old-paths"; //$NON-NLS-1$ + + /** The archives container, for all packages. */ + public static final String NODE_ARCHIVES = "archives"; //$NON-NLS-1$ + /** An archive element, for the archives container. */ + public static final String NODE_ARCHIVE = "archive"; //$NON-NLS-1$ + + /** An archive size, an int > 0. */ + public static final String NODE_SIZE = "size"; //$NON-NLS-1$ + /** A sha1 archive checksum, as a 40-char hex. */ + public static final String NODE_CHECKSUM = "checksum"; //$NON-NLS-1$ + /** A download archive URL, either absolute or relative to the repository xml. */ + public static final String NODE_URL = "url"; //$NON-NLS-1$ + + /** + * Optional element to indicate an archive is only suitable for the specified OS.
+ * Values: windows | macosx | linux. + * @since repo-10, addon-7 and sys-img-3. + * @replaces {@link #LEGACY_ATTR_OS} + */ + public static final String NODE_HOST_OS = "host-os"; //$NON-NLS-1$ + /** + * Optional element to indicate an archive is only suitable for the specified host bit size.
+ * Values: 32 | 64. + * @since repo-10, addon-7 and sys-img-3. + */ + public static final String NODE_HOST_BITS = "host-bits"; //$NON-NLS-1$ + /** + * Optional element to indicate an archive is only suitable for the specified JVM bit size.
+ * Values: 32 | 64. + * @since repo-10, addon-7 and sys-img-3. + * @replaces {@link #LEGACY_ATTR_ARCH} + */ + public static final String NODE_JVM_BITS = "jvm-bits"; //$NON-NLS-1$ + /** + * Optional element to indicate an archive is only suitable for a JVM equal or greater than + * the specified value.
+ * Value format: [1-9](\.[0-9]{1,2}){0,2}, e.g. "1.6", "1.7.0", "1.10" or "2" + * @since repo-10, addon-7 and sys-img-3. + */ + public static final String NODE_MIN_JVM_VERSION = "min-jvm-version"; //$NON-NLS-1$ + + + /** An archive checksum type, mandatory. */ + public static final String ATTR_TYPE = "type"; //$NON-NLS-1$ + /** + * An archive OS attribute, mandatory.
+ * Use {@link #NODE_HOST_OS} instead in repo-10, addon-7 and sys-img-3. + */ + public static final String LEGACY_ATTR_OS = "os"; //$NON-NLS-1$ + /** + * An optional archive Architecture attribute.
+ * Use {@link #NODE_JVM_BITS} instead in repo-10, addon-7 and sys-img-3. + */ + public static final String LEGACY_ATTR_ARCH = "arch"; //$NON-NLS-1$ + + /** A license definition ID. */ + public static final String ATTR_ID = "id"; //$NON-NLS-1$ + /** A license reference. */ + public static final String ATTR_REF = "ref"; //$NON-NLS-1$ + + + /** Type of a sha1 checksum. */ + public static final String SHA1_TYPE = "sha1"; //$NON-NLS-1$ + + /** Length of a string representing a SHA1 checksum; always 40 characters long. */ + public static final int SHA1_CHECKSUM_LEN = 40; + + /** + * Temporary folder used to hold downloads and extract archives during installation. + * This folder will be located in the SDK. + */ + public static final String FD_TEMP = "temp"; //$NON-NLS-1$ + + /** + * Returns a stream to the requested XML Schema. + * This is an internal helper. Users of the library should call + * {@link SdkRepoConstants#getXsdStream(String, int)} or + * {@link SdkAddonConstants#getXsdStream(String, int)}. + * + * @param rootElement The root of the filename of the XML schema. + * This is by convention the same as the root element declared by the schema. + * @param version The XML schema revision number, an integer >= 1. + * @return An {@link InputStream} object for the local XSD file or + * null if there is no schema for the requested version. + * @see SdkRepoConstants#getXsdStream(int) + * @see SdkAddonConstants#getXsdStream(int) + */ + protected static InputStream getXsdStream(String rootElement, int version) { + String filename = String.format("%1$s-%2$02d.xsd", rootElement, version); //$NON-NLS-1$ + + InputStream stream = null; + try { + stream = RepoConstants.class.getResourceAsStream(filename); + } catch (Exception e) { + // Some implementations seem to return null on failure, + // others throw an exception. We want to return null. + } + if (stream == null) { + // Try the alternate schemas that are not published yet. + // This allows us to internally test with new schemas before the + // public repository uses it. + filename = String.format("-%1$s-%2$02d.xsd", rootElement, version); //$NON-NLS-1$ + try { + stream = RepoConstants.class.getResourceAsStream(filename); + } catch (Exception e) { + // Some implementations seem to return null on failure, + // others throw an exception. We want to return null. + } + } + + return stream; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/RepoXsdUtil.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/RepoXsdUtil.java new file mode 100644 index 00000000..805fa8a9 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/RepoXsdUtil.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2015 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.sdklib.repository; + +import com.google.common.collect.Lists; + +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.List; +import java.util.logging.Logger; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParserFactory; +import javax.xml.transform.stream.StreamSource; + +/** + * Utilities related to the respository XSDs. + */ +public class RepoXsdUtil { + + public static final String NODE_IMPORT = "import"; + public static final String NODE_INCLUDE = "include"; + + public static final String ATTR_SCHEMA_LOCATION = "schemaLocation"; + + + /** + * Gets StreamSources for the given xsd (implied by the name and version), as well as any xsds imported or included by the main one. + * + * @param rootElement The root of the filename of the XML schema. This is by convention the same + * as the root element declared by the schema. + * @param version The XML schema revision number, an integer >= 1. + */ + public static StreamSource[] getXsdStream(final String rootElement, int version) { + String filename = String.format("%1$s-%2$02d.xsd", rootElement, version); //$NON-NLS-1$ + final List streams = Lists.newArrayList(); + InputStream stream = null; + try { + stream = RepoXsdUtil.class.getResourceAsStream(filename); + if (stream == null) { + filename = String.format("%1$s-%2$d.xsd", rootElement, version); //$NON-NLS-1$ + stream = RepoXsdUtil.class.getResourceAsStream(filename); + } + if (stream == null) { + // Try the alternate schemas that are not published yet. + // This allows us to internally test with new schemas before the + // public repository uses it. + filename = String.format("-%1$s-%2$02d.xsd", rootElement, version); //$NON-NLS-1$ + stream = RepoXsdUtil.class.getResourceAsStream(filename); + } + + // Parse the schema and find any imports or includes so we can return them as well. + // Currently transitive includes are not supported. + SAXParserFactory.newInstance().newSAXParser().parse(stream, new DefaultHandler() { + @Override + public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException { + name = name.substring(name.indexOf(':') + 1); + if (name.equals(NODE_IMPORT) || name.equals(NODE_INCLUDE)) { + String importFile = attributes.getValue(ATTR_SCHEMA_LOCATION); + streams.add(new StreamSource(RepoXsdUtil.class.getResourceAsStream(importFile))); + } + } + }); + // create and add the first stream again, since SaxParser closes the original one + streams.add(new StreamSource(RepoXsdUtil.class.getResourceAsStream(filename))); + } catch (Exception e) { + // Some implementations seem to return null on failure, + // others throw an exception. We want to return null. + return null; + } + return streams.toArray(new StreamSource[streams.size()]); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkAddonConstants.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkAddonConstants.java new file mode 100644 index 00000000..805f85b2 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkAddonConstants.java @@ -0,0 +1,89 @@ +/* + * 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.sdklib.repository; + + +import java.io.InputStream; + +/** + * Public constants for the sdk-addon XML Schema. + * @deprecated moved to studio + */ +public class SdkAddonConstants extends RepoConstants { + + /** + * The latest version of the sdk-addon XML Schema. + * Valid version numbers are between 1 and this number, included. + */ + public static final int NS_LATEST_VERSION = 7; + + /** + * The default name looked for by SdkSource when trying to load an + * sdk-addon XML if the URL doesn't match an existing resource. + */ + public static final String URL_DEFAULT_FILENAME = "addon.xml"; //$NON-NLS-1$ + + /** The base of our sdk-addon XML namespace. */ + private static final String NS_BASE = + "http://schemas.android.com/sdk/android/addon/"; //$NON-NLS-1$ + + /** + * The pattern of our sdk-addon XML namespace. + * Matcher's group(1) is the schema version (integer). + */ + public static final String NS_PATTERN = NS_BASE + "([0-9]+)"; //$NON-NLS-1$ + + /** The XML namespace of the latest sdk-addon XML. */ + public static final String NS_URI = getSchemaUri(NS_LATEST_VERSION); + + /** The root sdk-addon element */ + public static final String NODE_SDK_ADDON = "sdk-addon"; //$NON-NLS-1$ + + /** An add-on package. */ + public static final String NODE_ADD_ON = "add-on"; //$NON-NLS-1$ + + /** An extra package. */ + public static final String NODE_EXTRA = "extra"; //$NON-NLS-1$ + + /** + * List of possible nodes in a repository XML. Used to populate options automatically + * in the no-GUI mode. + */ + public static final String[] NODES = { + NODE_ADD_ON, + NODE_EXTRA + }; + + /** + * Returns a stream to the requested {@code sdk-addon} XML Schema. + * + * @param version Between 1 and {@link #NS_LATEST_VERSION}, included. + * @return An {@link InputStream} object for the local XSD file or + * null if there is no schema for the requested version. + */ + public static InputStream getXsdStream(int version) { + return getXsdStream(NODE_SDK_ADDON, version); + } + + /** + * Returns the URI of the sdk-addon schema for the given version number. + * @param version Between 1 and {@link #NS_LATEST_VERSION} included. + */ + public static String getSchemaUri(int version) { + return String.format(NS_BASE + "%d", version); //$NON-NLS-1$ + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkAddonsListConstants.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkAddonsListConstants.java new file mode 100644 index 00000000..3468b778 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkAddonsListConstants.java @@ -0,0 +1,109 @@ +/* + * 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.sdklib.repository; + + +import java.io.InputStream; + +/** + * Public constants for the sdk-addons-list XML Schema. + * @deprecated moved to studio + */ +public class SdkAddonsListConstants { + + /** The base of our sdk-addons-list XML namespace. */ + private static final String NS_BASE = + "http://schemas.android.com/sdk/android/addons-list/"; //$NON-NLS-1$ + + /** + * The pattern of our sdk-addons-list XML namespace. + * Matcher's group(1) is the schema version (integer). + */ + public static final String NS_PATTERN = NS_BASE + "([1-9][0-9]*)"; //$NON-NLS-1$ + + /** The latest version of the sdk-addons-list XML Schema. + * Valid version numbers are between 1 and this number, included. */ + public static final int NS_LATEST_VERSION = 2; + + /** The XML namespace of the latest sdk-addons-list XML. */ + public static final String NS_URI = getSchemaUri(NS_LATEST_VERSION); + + + /** The canonical URL filename for addons-list XML files. */ + public static final String URL_DEFAULT_FILENAME = getDefaultName(NS_LATEST_VERSION); + + /** The URL where to find the official addons list fle. */ + public static final String URL_ADDON_LIST = + SdkRepoConstants.URL_GOOGLE_SDK_SITE + URL_DEFAULT_FILENAME; + + + + /** The root sdk-addons-list element */ + public static final String NODE_SDK_ADDONS_LIST = "sdk-addons-list"; //$NON-NLS-1$ + + /** An add-on site. */ + public static final String NODE_ADDON_SITE = "addon-site"; //$NON-NLS-1$ + + /** A system image site. */ + public static final String NODE_SYS_IMG_SITE = "sys-img-site"; //$NON-NLS-1$ + + /** The UI-visible name of the add-on site. */ + public static final String NODE_NAME = "name"; //$NON-NLS-1$ + + /** + * The URL of the site. + *

+ * This can be either the exact URL of the an XML resource conforming + * to the latest sdk-addon-N.xsd schema, or it can be the URL of a + * 'directory', in which case the manager will look for a resource + * named 'addon.xml' at this location. + *

+ * Examples: + *

+     *    http://www.example.com/android/my_addons.xml
+     *  or
+     *    http://www.example.com/android/
+     * 
+ * In the second example, the manager will actually look for + * http://www.example.com/android/addon.xml + */ + public static final String NODE_URL = "url"; //$NON-NLS-1$ + + /** + * Returns a stream to the requested sdk-addon XML Schema. + * + * @param version Between 1 and {@link #NS_LATEST_VERSION}, included. + * @return An {@link InputStream} object for the local XSD file or + * null if there is no schema for the requested version. + */ + public static InputStream getXsdStream(int version) { + String filename = String.format("sdk-addons-list-%d.xsd", version); //$NON-NLS-1$ + return SdkAddonsListConstants.class.getResourceAsStream(filename); + } + + /** + * Returns the URI of the sdk-addon schema for the given version number. + * @param version Between 1 and {@link #NS_LATEST_VERSION} included. + */ + public static String getSchemaUri(int version) { + return String.format(NS_BASE + "%d", version); //$NON-NLS-1$ + } + + public static String getDefaultName(int version) { + return String.format("addons_list-%1$d.xml", version); //$NON-NLS-1$ + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkRepoConstants.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkRepoConstants.java new file mode 100644 index 00000000..7b325432 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkRepoConstants.java @@ -0,0 +1,171 @@ +/* + * 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.sdklib.repository; + + +import com.android.annotations.NonNull; + +import java.io.InputStream; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Public constants for the sdk-repository XML Schema. + * @deprecated moved to studio + */ +public class SdkRepoConstants extends RepoConstants { + + /** + * The latest version of the sdk-repository XML Schema. + * Valid version numbers are between 1 and this number, included. + */ + public static final int NS_LATEST_VERSION = 11; + + /** + * The min version of the sdk-repository XML Schema we'll try to load. + * When looking for a repository-N.xml on the server, we'll check from + * {@link #NS_LATEST_VERSION} down to this revision. + * We only introduced the "repository-N.xml" pattern start with revision + * 5, so we know that our server will never contain a repository + * XML with a schema version lower than this one. + */ + public static final int NS_SERVER_MIN_VERSION = 5; + + /** + * The URL of the official Google sdk-repository site. + * The URL ends with a /, allowing easy concatenation. + * */ + public static final String URL_GOOGLE_SDK_SITE = + "https://dl.google.com/android/repository/"; //$NON-NLS-1$ + + /** + * The default name looked for by SdkSource when trying to load an + * sdk-repository XML if the URL doesn't match an existing resource. + */ + public static final String URL_DEFAULT_FILENAME = "repository.xml"; //$NON-NLS-1$ + + /** + * The pattern name looked by {@link SdkSource} when trying to load + * an sdk-repository XML that is specific to a given XSD revision. + *

+ * This must be used with {@link String#format(String, Object...)} with + * one integer parameter between 1 and {@link #NS_LATEST_VERSION}. + */ + public static final String URL_FILENAME_PATTERN = "repository-%1$d.xml"; //$NON-NLS-1$ + + /** The base of our sdk-repository XML namespace. */ + private static final String NS_BASE = + "http://schemas.android.com/sdk/android/repository/"; //$NON-NLS-1$ + + /** + * The pattern of our sdk-repository XML namespace. + * Matcher's group(1) is the schema version (integer). + */ + public static final String NS_PATTERN = NS_BASE + "([0-9]+)"; //$NON-NLS-1$ + + /** The XML namespace of the latest sdk-repository XML. */ + public static final String NS_URI = getSchemaUri(NS_LATEST_VERSION); + + /** The root sdk-repository element */ + public static final String NODE_SDK_REPOSITORY = "sdk-repository"; //$NON-NLS-1$ + + /* The major revision for tool and platform-tool package + * (the full revision number is revision.minor.micro + preview#.) + * Mandatory int > 0. 0 when missing, which should not happen in + * a valid document. */ + public static final String NODE_MAJOR_REV = "major"; //$NON-NLS-1$ + /* The minor revision for tool and platform-tool package + * (the full revision number is revision.minor.micro + preview#.) + * Optional int >= 0. Implied to be 0 when missing. */ + public static final String NODE_MINOR_REV = "minor"; //$NON-NLS-1$ + /* The micro revision for tool and platform-tool package + * (the full revision number is revision.minor.micro + preview#.) + * Optional int >= 0. Implied to be 0 when missing. */ + public static final String NODE_MICRO_REV = "micro"; //$NON-NLS-1$ + /* The preview revision for tool and platform-tool package. + * Int > 0, only present for "preview / release candidate" packages. */ + public static final String NODE_PREVIEW = "preview"; //$NON-NLS-1$ + + /** A platform package. */ + public static final String NODE_PLATFORM = "platform"; //$NON-NLS-1$ + /** A tool package. */ + public static final String NODE_TOOL = "tool"; //$NON-NLS-1$ + /** A platform-tool package. */ + public static final String NODE_PLATFORM_TOOL = "platform-tool"; //$NON-NLS-1$ + /** A build-tool package. */ + public static final String NODE_BUILD_TOOL = "build-tool"; //$NON-NLS-1$ + /** A doc package. */ + public static final String NODE_DOC = "doc"; //$NON-NLS-1$ + /** A sample package. */ + public static final String NODE_SAMPLE = "sample"; //$NON-NLS-1$ + /** A source package. */ + public static final String NODE_SOURCE = "source"; //$NON-NLS-1$ + + /** + * List of possible nodes in a repository XML. Used to populate options automatically + * in the no-GUI mode. + */ + public static final String[] NODES = { + NODE_PLATFORM, + NODE_SYSTEM_IMAGE, + NODE_TOOL, + NODE_PLATFORM_TOOL, + NODE_DOC, + NODE_SAMPLE, + NODE_SOURCE, + }; + + /** + * Returns a stream to the requested {@code sdk-repository} XML Schema. + * + * @param version Between 1 and {@link #NS_LATEST_VERSION}, included. + * @return An {@link InputStream} object for the local XSD file or + * null if there is no schema for the requested version. + */ + public static InputStream getXsdStream(int version) { + return getXsdStream(NODE_SDK_REPOSITORY, version); + } + + /** + * Returns the URI of the SDK Repository schema for the given version number. + * @param version Between 1 and {@link #NS_LATEST_VERSION} included. + */ + public static String getSchemaUri(int version) { + return String.format(NS_BASE + "%d", version); //$NON-NLS-1$ + } + + /** + * Checks whether the schema version is greater or equal to the specified one. + * + * @param nsUri A non-null sdk-repository schema URI. + * @param minVersion The minimum version accepted. + * @return True if the URI is valid and has at least the required version. False otherwise. + */ + public static boolean versionGreaterOrEqualThan(@NonNull String nsUri, int minVersion) { + Pattern nsPattern = Pattern.compile(SdkRepoConstants.NS_PATTERN); + Matcher m = nsPattern.matcher(nsUri); + if (m.matches()) { + String version = m.group(1); + try { + return Integer.parseInt(version) >= minVersion; + } catch (NumberFormatException e) { + } + } + return false; + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkStatsConstants.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkStatsConstants.java new file mode 100644 index 00000000..df3e5d6d --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkStatsConstants.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2012 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.sdklib.repository; + + +import java.io.InputStream; + +/** + * Public constants for the sdk-stats XML Schema. + * @deprecated moved to studio + */ +public class SdkStatsConstants { + + /** The canonical URL filename for addons-list XML files. */ + public static final String URL_DEFAULT_FILENAME = "stats-1.xml"; //$NON-NLS-1$ + + /** The URL where to find the official addons list fle. */ + public static final String URL_STATS = + SdkRepoConstants.URL_GOOGLE_SDK_SITE + URL_DEFAULT_FILENAME; + + /** The base of our sdk-addons-list XML namespace. */ + private static final String NS_BASE = + "http://schemas.android.com/sdk/android/stats/"; //$NON-NLS-1$ + + /** + * The pattern of our sdk-stats XML namespace. + * Matcher's group(1) is the schema version (integer). + */ + public static final String NS_PATTERN = NS_BASE + "([1-9][0-9]*)"; //$NON-NLS-1$ + + /** The latest version of the sdk-stats XML Schema. + * Valid version numbers are between 1 and this number, included. */ + public static final int NS_LATEST_VERSION = 1; + + /** The XML namespace of the latest sdk-stats XML. */ + public static final String NS_URI = getSchemaUri(NS_LATEST_VERSION); + + /** The root sdk-stats element */ + public static final String NODE_SDK_STATS = "sdk-stats"; //$NON-NLS-1$ + + /** A platform stat. */ + public static final String NODE_PLATFORM = "platform"; //$NON-NLS-1$ + + /** The Android API Level for the platform. An int > 0. */ + public static final String NODE_API_LEVEL = "api-level"; //$NON-NLS-1$ + + /** The official codename for this platform, for example "Cupcake". */ + public static final String NODE_CODENAME = "codename"; //$NON-NLS-1$ + + /** The official version name of this platform, for example "Android 1.5". */ + public static final String NODE_VERSION = "version"; //$NON-NLS-1$ + + /** + * The approximate share percentage of that platform. + * See the caveat in sdk-stats-1.xsd about value freshness and accuracy. + */ + public static final String NODE_SHARE = "share"; //$NON-NLS-1$ + + /** + * Returns a stream to the requested sdk-stats XML Schema. + * + * @param version Between 1 and {@link #NS_LATEST_VERSION}, included. + * @return An {@link InputStream} object for the local XSD file or + * null if there is no schema for the requested version. + */ + public static InputStream getXsdStream(int version) { + String filename = String.format("sdk-stats-%d.xsd", version); //$NON-NLS-1$ + return SdkStatsConstants.class.getResourceAsStream(filename); + } + + /** + * Returns the URI of the sdk-stats schema for the given version number. + * @param version Between 1 and {@link #NS_LATEST_VERSION} included. + */ + public static String getSchemaUri(int version) { + return String.format(NS_BASE + "%d", version); //$NON-NLS-1$ + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkSysImgConstants.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkSysImgConstants.java new file mode 100644 index 00000000..d3e5752e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkSysImgConstants.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2012 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.sdklib.repository; + + +import java.io.InputStream; + +/** + * Public constants for the sdk-sys-img XML Schema. + * @deprecated moved to studio + */ +public class SdkSysImgConstants extends RepoConstants { + + /** + * The default name looked for by SdkSource when trying to load an + * sdk-sys-img XML if the URL doesn't match an existing resource. + */ + public static final String URL_DEFAULT_FILENAME = "sys-img.xml"; //$NON-NLS-1$ + + /** The base of our sdk-sys-img XML namespace. */ + private static final String NS_BASE = + "http://schemas.android.com/sdk/android/sys-img/"; //$NON-NLS-1$ + + /** + * The pattern of our sdk-sys-img XML namespace. + * Matcher's group(1) is the schema version (integer). + */ + public static final String NS_PATTERN = NS_BASE + "([0-9]+)"; //$NON-NLS-1$ + + /** + * The latest version of the sdk-sys-img XML Schema. + * Valid version numbers are between 1 and this number, included. + */ + public static final int NS_LATEST_VERSION = 3; + + /** The XML namespace of the latest sdk-sys-img XML. */ + public static final String NS_URI = getSchemaUri(NS_LATEST_VERSION); + + /** The root sdk-sys-img element */ + public static final String NODE_SDK_SYS_IMG = "sdk-sys-img"; //$NON-NLS-1$ + + /** A system-image tag id. */ + public static final String ATTR_TAG_ID = "tag-id"; //$NON-NLS-1$ + /** The user-visible display part of a system-image tag id. Optional. */ + public static final String ATTR_TAG_DISPLAY = "tag-display"; //$NON-NLS-1$ + + /** An add-on sub-element, indicating this is an add-on system image. */ + public static final String NODE_ADD_ON = SdkAddonConstants.NODE_ADD_ON; + + /** + * List of possible nodes in a repository XML. Used to populate options automatically + * in the no-GUI mode. + */ + public static final String[] NODES = { + NODE_SYSTEM_IMAGE, + }; + + /** + * Returns a stream to the requested {@code sdk-sys-img} XML Schema. + * + * @param version Between 1 and {@link #NS_LATEST_VERSION}, included. + * @return An {@link InputStream} object for the local XSD file or + * null if there is no schema for the requested version. + */ + public static InputStream getXsdStream(int version) { + return getXsdStream(NODE_SDK_SYS_IMG, version); + } + + /** + * Returns the URI of the sdk-sys-img schema for the given version number. + * @param version Between 1 and {@link #NS_LATEST_VERSION} included. + */ + public static String getSchemaUri(int version) { + return String.format(NS_BASE + "%d", version); //$NON-NLS-1$ + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IPkgCapabilities.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IPkgCapabilities.java new file mode 100644 index 00000000..b481e6a6 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IPkgCapabilities.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2013 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.sdklib.repository.descriptors; + +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.repository.FullRevision; +import com.android.sdklib.repository.MajorRevision; + +/** + * {@link IPkgCapabilities} describe which attributes are available for each kind of + * SDK Manager package type. + *

+ * To query packages capabilities, rely on {@code PkgType.hasXxx()} or {@code PkgDesc.hasXxx()}. + * + * @see PkgType + * @see PkgDesc + */ +public interface IPkgCapabilities { + + /** + * Indicates whether this package type has a {@link FullRevision}. + * @return True if this package type has a {@link FullRevision}. + */ + boolean hasFullRevision(); + + /** + * Indicates whether this package type has a {@link MajorRevision}. + * @return True if this package type has a {@link MajorRevision}. + */ + boolean hasMajorRevision(); + + /** + * Indicates whether this package type has a {@link AndroidVersion}. + * @return True if this package type has a {@link AndroidVersion}. + */ + boolean hasAndroidVersion(); + + /** + * Indicates whether this package type has a path. + * @return True if this package type has a path. + */ + boolean hasPath(); + + /** + * Indicates whether this package type has a tag. + * @return True if this package type has a tag id-display tuple. + */ + boolean hasTag(); + + /** + * Indicates whether this package type has a vendor id. + * @return True if this package type has a vendor id. + */ + boolean hasVendor(); + + /** + * Indicates whether this package type has a {@code min-tools-rev} attribute. + * @return True if this package type has a {@code min-tools-rev} attribute. + */ + boolean hasMinToolsRev(); + + /** + * Indicates whether this package type has a {@code min-platform-tools-rev} attribute. + * @return True if this package type has a {@code min-platform-tools-rev} attribute. + */ + boolean hasMinPlatformToolsRev(); +} + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IPkgDesc.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IPkgDesc.java new file mode 100644 index 00000000..5effda3a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IPkgDesc.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2013 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.sdklib.repository.descriptors; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.repository.IListDescription; +import com.android.sdklib.repository.License; +import com.android.sdklib.repository.FullRevision; +import com.android.sdklib.repository.MajorRevision; +import com.android.sdklib.repository.PreciseRevision; + +import java.io.File; + +/** + * {@link IPkgDesc} keeps information on individual SDK packages + * (both local or remote packages definitions.) + *
+ * Packages have different attributes depending on their type. + *

+ * To create a new {@link IPkgDesc}, use one of the package-specific constructors + * provided by {@code PkgDesc.Builder.newXxx()}. + *

+ * To query packages capabilities, rely on {@link #getType()} and the {@code IPkgDesc.hasXxx()} + * methods provided by {@link IPkgDesc}. + */ +public interface IPkgDesc extends Comparable, IPkgCapabilities, IListDescription { + + /** + * Returns the type of the package. + * @return Returns one of the {@link PkgType} constants. + */ + @NonNull + PkgType getType(); + + /** + * Returns the list-display meta data of this package. + * @return The list-display data, if available, or null. + */ + @Nullable + String getListDisplay(); + + @Nullable + String getDescriptionShort(); + + @Nullable + String getDescriptionUrl(); + + @Nullable + License getLicense(); + + boolean isObsolete(); + + /** + * Returns the package's {@link FullRevision} or null. + * @return A non-null value if {@link #hasFullRevision()} is true; otherwise a null value. + */ + @Nullable + FullRevision getFullRevision(); + + /** + * Returns the package's {@link MajorRevision} or null. + * @return A non-null value if {@link #hasMajorRevision()} is true; otherwise a null value. + */ + @Nullable + MajorRevision getMajorRevision(); + + /** + * Returns the package's revision or null. This will come from the {@link FullRevision} or + * {@link MajorRevision}, with the precision set as appropriate. + * @return A representation of {@link #getMajorRevision()} or {@link #getFullRevision()}, + * depending on which one exists. + */ + @NonNull + PreciseRevision getPreciseRevision(); + + /** + * Returns the package's {@link AndroidVersion} or null. + * @return A non-null value if {@link #hasAndroidVersion()} is true; otherwise a null value. + */ + @Nullable + AndroidVersion getAndroidVersion(); + + /** + * Returns the package's path string or null. + *

+ * For {@link PkgType#PKG_SYS_IMAGE}, the path is the system-image ABI.
+ * For {@link PkgType#PKG_PLATFORM}, the path is the platform hash string.
+ * For {@link PkgType#PKG_ADDON}, the path is the platform hash string.
+ * For {@link PkgType#PKG_EXTRA}, the path is the extra-path string.
+ * + * @return A non-null value if {@link #hasPath()} is true; otherwise a null value. + */ + @Nullable + String getPath(); + + /** + * Returns the package's tag id-display tuple or null. + * + * @return A non-null tag if {@link #hasTag()} is true; otherwise a null value. + */ + @Nullable + IdDisplay getTag(); + + /** + * Returns the package's vendor-id string or null. + * @return A non-null value if {@link #hasVendor()} is true; otherwise a null value. + */ + @Nullable + IdDisplay getVendor(); + + /** + * Returns the package's {@code min-tools-rev} or null. + * @return A non-null value if {@link #hasMinToolsRev()} is true; otherwise a null value. + */ + @Nullable + FullRevision getMinToolsRev(); + + /** + * Returns the package's {@code min-platform-tools-rev} or null. + * @return A non-null value if {@link #hasMinPlatformToolsRev()} is true; otherwise null. + */ + @Nullable + FullRevision getMinPlatformToolsRev(); + + /** + * Indicates whether this package descriptor is an update for the given + * existing descriptor. Preview versions are never considered updates for non- + * previews, and vice versa. + * + * @param existingDesc A non-null existing descriptor. + * @return True if this package is an update for the given one. + */ + boolean isUpdateFor(@NonNull IPkgDesc existingDesc); + + /** + * Indicates whether this package descriptor is an update for the given + * existing descriptor, using the given comparison method. + * + * @param existingDesc A non-null existing descriptor. + * @param previewComparison The {@link FullRevision.PreviewComparison} method to use + * when comparing the packages. + * @return True if this package is an update for the given one. + */ + boolean isUpdateFor(@NonNull IPkgDesc existingDesc, + @NonNull FullRevision.PreviewComparison previewComparison); + + /** + * Returns a stable string id that can be used to reference this package, including + * a suffix indicating that this package is a preview if it is. + */ + @NonNull + String getInstallId(); + + /** + * Returns a stable string id that can be used to reference this package, which + * excludes the preview suffix. + */ + String getBaseInstallId(); + + /** + * Returns the canonical location where such a package would be installed. + * @param sdkLocation The root of the SDK. + * @return the canonical location where such a package would be installed. + */ + @NonNull + File getCanonicalInstallFolder(@NonNull File sdkLocation); + + /** + * @return True if the revision of this package is a preview. + */ + boolean isPreview(); +} + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IPkgDescAddon.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IPkgDescAddon.java new file mode 100644 index 00000000..02c0ad5a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IPkgDescAddon.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2014 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.sdklib.repository.descriptors; + +import com.android.annotations.NonNull; + +/** + * {@link IPkgDescAddon} keeps information on individual add-on SDK packages + * (both local or remote packages definitions.) The base {@link IPkgDesc} tries + * to present a unified interface to package attributes and this interface + * adds methods specific to extras. + *

+ * To create a new {@link IPkgDescAddon}, + * use {@link PkgDesc.Builder#newAddon(com.android.sdklib.AndroidVersion, com.android.sdklib.repository.MajorRevision, IdDisplay, IdDisplay)}. + *

+ * To query generic packages capabilities, rely on {@link #getType()} and the + * {@code IPkgDesc.hasXxx()} methods provided by {@link IPkgDesc}. + */ +public interface IPkgDescAddon extends IPkgDesc { + + /** + * Returns the id/display name of the add-on. + * @return A non-null id/display name for the add-on + */ + @NonNull IdDisplay getName(); +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IPkgDescExtra.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IPkgDescExtra.java new file mode 100644 index 00000000..68c4bd72 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IPkgDescExtra.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2013 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.sdklib.repository.descriptors; + +import com.android.annotations.NonNull; +import com.android.sdklib.repository.NoPreviewRevision; + +/** + * {@link IPkgDescExtra} keeps information on individual extra SDK packages + * (both local or remote packages definitions.) The base {@link IPkgDesc} tries + * to present a unified interface to package attributes and this interface + * adds methods specific to extras. + *

+ * To create a new {@link IPkgDescExtra}, + * use {@link PkgDesc.Builder#newExtra(IdDisplay, String, String, String[], NoPreviewRevision)}. + *

+ * The extra's revision is a {@link NoPreviewRevision}; the attribute is however + * accessed via {@link IPkgDesc#getFullRevision()} instead of introducing a new + * custom method. + *

+ * To query generic packages capabilities, rely on {@link #getType()} and the + * {@code IPkgDesc.hasXxx()} methods provided by {@link IPkgDesc}. + */ +public interface IPkgDescExtra extends IPkgDesc { + /** + * Returns an optional list of older paths for this extra package. + * @return A non-null, possibly empty, for old paths previously used for the same extra. + */ + @NonNull String[] getOldPaths(); + + /** + * Returns the display name of the Extra. + * @return A non-null name for the Extra, used for display purposes. + */ + @NonNull String getNameDisplay(); + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IdDisplay.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IdDisplay.java new file mode 100644 index 00000000..0278ba5b --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IdDisplay.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2014 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.sdklib.repository.descriptors; + +import com.android.annotations.NonNull; + +/** + * Immutable structure that represents a tuple (id-string + display-string.) + */ +public final class IdDisplay implements Comparable { + + private final String mId; + private final String mDisplay; + + /** + * Creates a new immutable tuple (id-string + display-string.) + * + * @param id The non-null id string. + * @param display The non-null display string. + */ + public IdDisplay(@NonNull String id, @NonNull String display) { + mId = id; + mDisplay = display; + } + + @NonNull + public String getId() { + return mId; + } + + @NonNull + public String getDisplay() { + return mDisplay; + } + + /** + * {@link IdDisplay} instances are the same if they have the same id. + * The display value is not used for comparison or ordering. + */ + @Override + public int compareTo(IdDisplay tag) { + return mId.compareTo(tag.mId); + } + + /** + * Hash code of {@link IdDisplay} instances only rely on the id hash code. + */ + @Override + public int hashCode() { + return mId.hashCode(); + } + + /** + * Equality of {@link IdDisplay} instances only rely on the id equality. + * The display value is not used for comparison or ordering. + */ + @Override + public boolean equals(Object obj) { + return (obj instanceof IdDisplay) && mId.equals(((IdDisplay)obj).mId); + } + + /** + * Returns a string representation for *debug* purposes only, not for UI display. + */ + @Override + public String toString() { + return String.format("%1$s [%2$s]", mId, mDisplay); + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/PkgDesc.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/PkgDesc.java new file mode 100644 index 00000000..eda9838d --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/PkgDesc.java @@ -0,0 +1,1206 @@ +/* + * Copyright (C) 2013 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.sdklib.repository.descriptors; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.annotations.VisibleForTesting; +import com.android.annotations.VisibleForTesting.Visibility; +import com.android.sdklib.AndroidTargetHash; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.SystemImage; +import com.android.sdklib.io.LegacyFileOp; +import com.android.sdklib.repository.FullRevision; +import com.android.sdklib.repository.FullRevision.PreviewComparison; +import com.android.sdklib.repository.License; +import com.android.sdklib.repository.MajorRevision; +import com.android.sdklib.repository.NoPreviewRevision; +import com.android.sdklib.repository.PreciseRevision; + +import java.io.File; +import java.util.Locale; + +/** + * {@link PkgDesc} keeps information on individual SDK packages + * (both local or remote packages definitions.) + *
+ * Packages have different attributes depending on their type. + *

+ * To create a new {@link PkgDesc}, use one of the package-specific constructors + * provided here. + *

+ * To query packages capabilities, rely on {@link #getType()} and the {@code PkgDesc.hasXxx()} + * methods provided in the base {@link PkgDesc}. + */ +public class PkgDesc implements IPkgDesc { + public static final String PREVIEW_SUFFIX = "-preview"; + private final PkgType mType; + private final FullRevision mFullRevision; + private final MajorRevision mMajorRevision; + private final AndroidVersion mAndroidVersion; + private final String mPath; + private final IdDisplay mTag; + private final IdDisplay mVendor; + private final FullRevision mMinToolsRev; + private final FullRevision mMinPlatformToolsRev; + private final IIsUpdateFor mCustomIsUpdateFor; + private final IGetPath mCustomPath; + + private final License mLicense; + private final String mListDisplay; + private final String mDescriptionShort; + private final String mDescriptionUrl; + private final boolean mIsObsolete; + + protected PkgDesc(@NonNull PkgType type, + @Nullable License license, + @Nullable String listDisplay, + @Nullable String descriptionShort, + @Nullable String descriptionUrl, + boolean isObsolete, + @Nullable FullRevision fullRevision, + @Nullable MajorRevision majorRevision, + @Nullable AndroidVersion androidVersion, + @Nullable String path, + @Nullable IdDisplay tag, + @Nullable IdDisplay vendor, + @Nullable FullRevision minToolsRev, + @Nullable FullRevision minPlatformToolsRev, + @Nullable IIsUpdateFor customIsUpdateFor, + @Nullable IGetPath customPath) { + mType = type; + mIsObsolete = isObsolete; + mLicense = license; + mListDisplay = listDisplay; + mDescriptionShort = descriptionShort; + mDescriptionUrl = descriptionUrl; + mFullRevision = fullRevision; + mMajorRevision = majorRevision; + mAndroidVersion = androidVersion; + mPath = path; + mTag = tag; + mVendor = vendor; + mMinToolsRev = minToolsRev; + mMinPlatformToolsRev = minPlatformToolsRev; + mCustomIsUpdateFor = customIsUpdateFor; + mCustomPath = customPath; + } + + @NonNull + @Override + public PkgType getType() { + return mType; + } + + @Override + @Nullable + public String getListDisplay() { + return mListDisplay; + } + + @Override + @Nullable + public String getDescriptionShort() { + return mDescriptionShort; + } + + @Override + @Nullable + public String getDescriptionUrl() { + return mDescriptionUrl; + } + + @Override + @Nullable + public License getLicense() { + return mLicense; + } + + @Override + public boolean isObsolete() { + return mIsObsolete; + } + + @Override + public final boolean hasFullRevision() { + return getType().hasFullRevision(); + } + + @Override + public final boolean hasMajorRevision() { + return getType().hasMajorRevision(); + } + + @Override + public final boolean hasAndroidVersion() { + return getType().hasAndroidVersion(); + } + + @Override + public final boolean hasPath() { + return getType().hasPath(); + } + + @Override + public final boolean hasTag() { + return getType().hasTag(); + } + + @Override + public boolean hasVendor() { + return getType().hasVendor(); + } + + @Override + public final boolean hasMinToolsRev() { + return getType().hasMinToolsRev(); + } + + @Override + public final boolean hasMinPlatformToolsRev() { + return getType().hasMinPlatformToolsRev(); + } + + @Nullable + @Override + public FullRevision getFullRevision() { + return mFullRevision; + } + + @Nullable + @Override + public MajorRevision getMajorRevision() { + return mMajorRevision; + } + + @NonNull + @Override + public final PreciseRevision getPreciseRevision() { + if (mMajorRevision == null) { + return new PreciseRevision(mFullRevision.getMajor(), mFullRevision.getMinor(), + mFullRevision.getMicro(), mFullRevision.getPreview()); + } + return new PreciseRevision(mMajorRevision.getMajor()); + } + + @Nullable + @Override + public AndroidVersion getAndroidVersion() { + return mAndroidVersion; + } + + @Override + public boolean isPreview() { + return getPreciseRevision().isPreview(); + } + + @Nullable + @Override + public String getPath() { + if (mCustomPath != null) { + return mCustomPath.getPath(this); + } else { + return mPath; + } + } + + @Nullable + @Override + public IdDisplay getTag() { + return mTag; + } + + @Nullable + @Override + public IdDisplay getVendor() { + return mVendor; + } + + @Nullable + @Override + public FullRevision getMinToolsRev() { + return mMinToolsRev; + } + + @Nullable + @Override + public FullRevision getMinPlatformToolsRev() { + return mMinPlatformToolsRev; + } + + @Override + public String getInstallId() { + String id = getBaseInstallId(); + if (getPreciseRevision().isPreview()) { + return id + PREVIEW_SUFFIX; + } + return id; + } + + @Override + public String getBaseInstallId() { + StringBuilder sb = new StringBuilder(); + + /* iid patterns: + tools, platform-tools => FOLDER + build-tools => FOLDER-REV + doc, sample, source => ENUM-API + extra => ENUM-VENDOR.id-PATH + platform => android-API + add-on => addon-NAME.id-VENDOR.id-API + platform sys-img => sys-img-ABI-TAG|android-API + add-on sys-img => sys-img-ABI-addon-NAME.id-VENDOR.id-API + */ + + switch (mType) { + case PKG_TOOLS: + case PKG_PLATFORM_TOOLS: + sb.append(mType.getFolderName()); + break; + + case PKG_BUILD_TOOLS: + sb.append(mType.getFolderName()).append('-'); + // Add version number without the preview revision number. This is to make preview + // packages be updatable to the next revision. + int[] version = getPreciseRevision().toIntArray(false); + for (int i = 0; i < version.length; i++) { + sb.append(version[i]); + if (i != version.length - 1) { + sb.append('.'); + } + } + break; + + case PKG_DOC: + sb.append("doc"); + break; + + case PKG_SAMPLE: + case PKG_SOURCE: + sb.append(mType.toString().toLowerCase(Locale.US).replace("pkg_", "")); + sb.append('-').append(getAndroidVersion().getApiString()); + break; + + case PKG_EXTRA: + sb.append("extra-") + .append(getVendor().getId()) + .append('-') + .append(getPath()); + break; + + case PKG_PLATFORM: + sb.append(AndroidTargetHash.PLATFORM_HASH_PREFIX) + .append(getAndroidVersion().getApiString()); + break; + + case PKG_ADDON: + sb.append("addon-") + .append(((IPkgDescAddon)this).getName().getId()) + .append('-') + .append(getVendor().getId()) + .append('-') + .append(getAndroidVersion().getApiString()); + break; + + case PKG_SYS_IMAGE: + sb.append("sys-img-") + .append(getPath()) // path==ABI for sys-img + .append('-') + .append(SystemImage.DEFAULT_TAG.equals(getTag()) ? "android" : getTag().getId()) + .append('-') + .append(getAndroidVersion().getApiString()); + break; + + case PKG_ADDON_SYS_IMAGE: + sb.append("sys-img-") + .append(getPath()) // path==ABI for sys-img + .append("-addon-") + .append(SystemImage.DEFAULT_TAG.equals(getTag()) ? "android" : getTag().getId()) + .append('-') + .append(getVendor().getId()) + .append('-') + .append(getAndroidVersion().getApiString()); + break; + + case PKG_NDK: + sb.append("ndk"); + break; + + default: + throw new IllegalArgumentException("IID not defined for type " + mType.toString()); + } + + return sanitize(sb.toString()); + } + + @Override + public File getCanonicalInstallFolder(@NonNull File sdkLocation) { + File f = LegacyFileOp.append(sdkLocation, mType.getFolderName()); + + /* folder patterns: + tools, platform-tools, doc => FOLDER + build-tools, add-on => FOLDER/IID + platform, sample, source => FOLDER/android-API + platform sys-img => FOLDER/android-API/TAG/ABI + add-on sys-img => FOLDER/addon-NAME.id-VENDOR.id-API/ABI + extra => FOLDER/VENDOR.id/PATH + */ + + switch (mType) { + case PKG_TOOLS: + case PKG_PLATFORM_TOOLS: + case PKG_DOC: + // no-op, top-folder is all what is needed here + break; + + case PKG_BUILD_TOOLS: + case PKG_ADDON: + f = LegacyFileOp.append(f, getInstallId()); + break; + + case PKG_PLATFORM: + case PKG_SAMPLE: + case PKG_SOURCE: + f = LegacyFileOp.append(f, AndroidTargetHash.PLATFORM_HASH_PREFIX + sanitize( + getAndroidVersion().getApiString())); + break; + + case PKG_SYS_IMAGE: + f = LegacyFileOp.append(f, + AndroidTargetHash.PLATFORM_HASH_PREFIX + sanitize( + getAndroidVersion().getApiString()), + sanitize(SystemImage.DEFAULT_TAG.equals(getTag()) ? "android" + : getTag().getId()), + sanitize(getPath())); // path==abi + break; + + case PKG_ADDON_SYS_IMAGE: + String name = "addon-" + + (SystemImage.DEFAULT_TAG.equals(getTag()) ? "android" : getTag().getId()) + + '-' + + getVendor().getId() + + '-' + + getAndroidVersion().getApiString(); + f = LegacyFileOp.append(f, + sanitize(name), + sanitize(getPath())); // path==abi + break; + + case PKG_EXTRA: + f = LegacyFileOp.append(f, + sanitize(getVendor().getId()), + sanitize(getPath())); + break; + + default: + throw new IllegalArgumentException( + "CanonicalFolder not defined for type " + mType.toString()); + } + + return f; + } + + //---- Updating ---- + + /** + * Computes the most general case of {@link #isUpdateFor(IPkgDesc)}. + * Individual package types use this and complement with their own specific cases + * as needed. + * + * @param existingDesc A non-null package descriptor to compare with. + * @return True if this package descriptor would generally update the given one. + */ + @Override + public boolean isUpdateFor(@NonNull IPkgDesc existingDesc) { + return isUpdateFor(existingDesc, PreviewComparison.COMPARE_NUMBER); + } + + @Override + public boolean isUpdateFor(@NonNull IPkgDesc existingDesc, + @NonNull PreviewComparison previewComparison) { + if (mCustomIsUpdateFor != null) { + return mCustomIsUpdateFor.isUpdateFor(this, existingDesc); + } else { + return isGenericUpdateFor(existingDesc, previewComparison); + } + } + + /** + * Computes the most general case of {@link #isUpdateFor(IPkgDesc)}. + * Individual package types use this and complement with their own specific cases + * as needed. + * + * @param existingDesc A non-null package descriptor to compare with. + * @param previewComparison The type of preview comparison to do. + * @return True if this package descriptor would generally update the given one. + */ + private boolean isGenericUpdateFor(@NonNull IPkgDesc existingDesc, + PreviewComparison previewComparison) { + + if (existingDesc == null || !getType().equals(existingDesc.getType())) { + return false; + } + + // Packages that have an Android version can generally only be updated + // for the same Android version (otherwise it's a new artifact.) + if (hasAndroidVersion() && !getAndroidVersion().equals(existingDesc.getAndroidVersion())) { + return false; + } + + // Packages that have a vendor id need the same vendor id on both sides + if (hasVendor() && !getVendor().equals(existingDesc.getVendor())) { + return false; + } + + // Packages that have a tag id need the same tag id on both sides + if (hasTag() && !getTag().getId().equals(existingDesc.getTag().getId())) { + return false; + } + + // Packages that have a path can generally only be updated if both use the same path + if (hasPath()) { + if (this instanceof IPkgDescExtra) { + // Extra package handle paths differently, they need to use the old_path + // to allow upgrade compatibility. + if (!PkgDescExtra.compatibleVendorAndPath((IPkgDescExtra) this, + (IPkgDescExtra) existingDesc)) { + return false; + } + } else if (!getPath().equals(existingDesc.getPath())) { + return false; + } + } + + // Packages that have a major version are generally updates if it increases. + if (hasMajorRevision() && + getMajorRevision().compareTo(existingDesc.getMajorRevision()) > 0) { + return true; + } + + // Packages that have a full revision are generally updates if it increases + // but keeps the same kind of preview (e.g. previews are only updates by previews.) + if (hasFullRevision() && + (previewComparison == PreviewComparison.IGNORE + || existingDesc.isPreview() == isPreview())) { + // If both packages match in their preview type (both previews or both not previews) + // then is the RC/preview number an update? + return getFullRevision().compareTo(existingDesc.getFullRevision(), + PreviewComparison.COMPARE_NUMBER) > 0; + } + + return false; + } + + + //---- Ordering ---- + + /** + * Compares this descriptor to another one. + * All fields must match for equality. + *

+ * This is must not be used an indication that a package is a suitable update for another one. + * The comparison order is however suitable for sorting packages for display purposes. + */ + @Override + public int compareTo(@NonNull IPkgDesc o) { + int t1 = getType().getIntValue(); + int t2 = o.getType().getIntValue(); + if (t1 != t2) { + return t1 - t2; + } + + if (hasAndroidVersion() && o.hasAndroidVersion()) { + t1 = getAndroidVersion().compareTo(o.getAndroidVersion()); + if (t1 != 0) { + return t1; + } + } + + if (hasVendor() && o.hasVendor()) { + t1 = getVendor().compareTo(o.getVendor()); + if (t1 != 0) { + return t1; + } + } + + if (hasTag() && o.hasTag()) { + t1 = getTag().compareTo(o.getTag()); + if (t1 != 0) { + return t1; + } + } + + if (hasPath() && o.hasPath()) { + t1 = getPath().compareTo(o.getPath()); + if (t1 != 0) { + return t1; + } + } + + if (hasFullRevision() && o.hasFullRevision()) { + t1 = getFullRevision().compareTo(o.getFullRevision()); + if (t1 != 0) { + return t1; + } + } + + if (hasMajorRevision() && o.hasMajorRevision()) { + t1 = getMajorRevision().compareTo(o.getMajorRevision()); + if (t1 != 0) { + return t1; + } + } + + if (hasMinToolsRev() && o.hasMinToolsRev()) { + t1 = getMinToolsRev().compareTo(o.getMinToolsRev()); + if (t1 != 0) { + return t1; + } + } + + if (hasMinPlatformToolsRev() && o.hasMinPlatformToolsRev()) { + t1 = getMinPlatformToolsRev().compareTo(o.getMinPlatformToolsRev()); + if (t1 != 0) { + return t1; + } + } + + return 0; + } + + // --- display description ---- + + @NonNull + @Override + public String getListDescription() { + if (mListDisplay != null && !mListDisplay.isEmpty()) { + return mListDisplay; + } + + return patternReplaceImpl(getType().getListDisplayPattern()); + } + + @VisibleForTesting(visibility=Visibility.PRIVATE) + protected String patternReplaceImpl(String result) { + // Flags for list description pattern string, used in PkgType: + // $MAJ $FULL $API $PATH $TAG $VEND $NAME (for extras) + + result = result + .replace("$MAJ", hasMajorRevision() ? getMajorRevision().toShortString() : ""); + result = result + .replace("$FULL", hasFullRevision() ? getFullRevision().toShortString() : ""); + result = result + .replace("$API", hasAndroidVersion() ? getAndroidVersion().getApiString() : ""); + result = result.replace("$PATH", hasPath() ? getPath() : ""); + result = result.replace("$TAG", hasTag() && !getTag().equals(SystemImage.DEFAULT_TAG) ? + getTag().getDisplay() : ""); + result = result.replace("$VEND", hasVendor() ? getVendor().getDisplay() : ""); + String name = ""; + if (this instanceof IPkgDescExtra) { + name = ((IPkgDescExtra) this).getNameDisplay(); + } else if (this instanceof IPkgDescAddon) { + name = ((IPkgDescAddon) this).getName().getDisplay(); + } + result = result.replace("$NAME", name); + + // Evaluate replacements. + // {|choice1|choice2|...|choiceN|} gets replaced by the first non-empty choice. + for (int start = result.indexOf("{|"); + start >= 0; + start = result.indexOf("{|")) { + int end = result.indexOf('}', start); + int last = start + 1; + for (int pipe = result.indexOf('|', last+1); + pipe > start; + last = pipe, pipe = result.indexOf('|', last+1)) { + if (pipe - last > 1) { + result = result.substring(0, start) + + result.substring(last+1, pipe) + + result.substring(end+1); + break; + } + } + } + + // Evaluate conditions. + // {?value>1:text to use} -- uses the text if value is greater than 1. + // Simplification: this is our only test right now so hard-code it instead of + // using a generic expression evaluation. + for (int start = result.indexOf("{?"); + start >= 0; + start = result.indexOf("{?")) { + int end = result.indexOf('}', start); + int op = result.indexOf(">1:"); + if (op > start) { + String value = ""; + try { + FullRevision i = FullRevision.parseRevision(result.substring(start+2, op)); + if (i.compareTo(new FullRevision(1)) > 0) { + value = result.substring(op+3, end); + } + } catch (NumberFormatException e) { + value = "ERROR " + e.getMessage() + " in " + result.substring(start, end+1); + } + result = result.substring(0, start) + + value + + result.substring(end+1); + } + } + + + return result; + } + + /** String representation for debugging purposes. */ + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("'); + return builder.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (hasAndroidVersion() ? getAndroidVersion().hashCode() : 0); + result = prime * result + (hasVendor() ? getVendor() .hashCode() : 0); + result = prime * result + (hasTag() ? getTag() .hashCode() : 0); + result = prime * result + (hasPath() ? getPath() .hashCode() : 0); + result = prime * result + (hasFullRevision() ? getFullRevision() .hashCode() : 0); + result = prime * result + (hasMajorRevision() ? getMajorRevision() .hashCode() : 0); + result = prime * result + (hasMinToolsRev() ? getMinToolsRev() .hashCode() : 0); + result = prime * result + (hasMinPlatformToolsRev() ? + getMinPlatformToolsRev().hashCode() : 0); + + return result; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof IPkgDesc)) return false; + IPkgDesc rhs = (IPkgDesc) obj; + + if (hasAndroidVersion() != rhs.hasAndroidVersion()) { + return false; + } + if (hasAndroidVersion() && !getAndroidVersion().equals(rhs.getAndroidVersion())) { + return false; + } + + if (hasTag() != rhs.hasTag()) { + return false; + } + if (hasTag() && !getTag().equals(rhs.getTag())) { + return false; + } + + if (hasPath() != rhs.hasPath()) { + return false; + } + if (hasPath() && !getPath().equals(rhs.getPath())) { + return false; + } + + if (hasFullRevision() != rhs.hasFullRevision()) { + return false; + } + if (hasFullRevision() && !getFullRevision().equals(rhs.getFullRevision())) { + return false; + } + + if (hasMajorRevision() != rhs.hasMajorRevision()) { + return false; + } + if (hasMajorRevision() && !getMajorRevision().equals(rhs.getMajorRevision())) { + return false; + } + + if (hasMinToolsRev() != rhs.hasMinToolsRev()) { + return false; + } + if (hasMinToolsRev() && !getMinToolsRev().equals(rhs.getMinToolsRev())) { + return false; + } + + if (hasMinPlatformToolsRev() != rhs.hasMinPlatformToolsRev()) { + return false; + } + if (hasMinPlatformToolsRev() && + !getMinPlatformToolsRev().equals(rhs.getMinPlatformToolsRev())) { + return false; + } + + return true; + } + + + // ---- Constructors ----- + + public interface IIsUpdateFor { + boolean isUpdateFor(@NonNull PkgDesc thisPkgDesc, @NonNull IPkgDesc existingDesc); + } + + public interface IGetPath { + String getPath(@NonNull PkgDesc thisPkgDesc); + } + + public static class Builder { + private final PkgType mType; + private FullRevision mFullRevision; + private MajorRevision mMajorRevision; + private AndroidVersion mAndroidVersion; + private String mPath; + private IdDisplay mTag; + private IdDisplay mVendor; + private FullRevision mMinToolsRev; + private FullRevision mMinPlatformToolsRev; + private IIsUpdateFor mCustomIsUpdateFor; + private IGetPath mCustomPath; + private String[] mOldPaths; + private String mNameDisplay; + private IdDisplay mNameIdDisplay; + + private License mLicense; + private String mListDisplay; + private String mDescriptionShort; + private String mDescriptionUrl; + private boolean mIsObsolete; + + + private Builder(PkgType type) { + mType = type; + } + + /** + * Creates a new tool package descriptor. + * + * @param revision The revision of the tool package. + * @param minPlatformToolsRev The {@code min-platform-tools-rev}. + * Use {@link FullRevision#NOT_SPECIFIED} to indicate there is no requirement. + * @return A {@link PkgDesc} describing this tool package. + */ + @NonNull + public static Builder newTool(@NonNull FullRevision revision, + @NonNull FullRevision minPlatformToolsRev) { + Builder p = new Builder(PkgType.PKG_TOOLS); + p.mFullRevision = revision; + p.mMinPlatformToolsRev = minPlatformToolsRev; + return p; + } + + /** + * Creates a new platform-tool package descriptor. + * + * @param revision The revision of the platform-tool package. + * @return A {@link PkgDesc} describing this platform-tool package. + */ + @NonNull + public static Builder newPlatformTool(@NonNull FullRevision revision) { + Builder p = new Builder(PkgType.PKG_PLATFORM_TOOLS); + p.mFullRevision = revision; + return p; + } + + /** + * Creates a new build-tool package descriptor. + * + * @param revision The revision of the build-tool package. + * @return A {@link PkgDesc} describing this build-tool package. + */ + @NonNull + public static Builder newBuildTool(@NonNull FullRevision revision) { + Builder p = new Builder(PkgType.PKG_BUILD_TOOLS); + p.mFullRevision = revision; + p.mCustomIsUpdateFor = new IIsUpdateFor() { + @Override + public boolean isUpdateFor(PkgDesc thisPkgDesc, IPkgDesc existingDesc) { + // Generic test checks that the preview type is the same (both previews or not). + // Build tool is different in that the full revision must be an exact match + // and not an increase. + return thisPkgDesc + .isGenericUpdateFor(existingDesc, PreviewComparison.COMPARE_NUMBER) && + thisPkgDesc.getFullRevision().compareTo( + existingDesc.getFullRevision(), + PreviewComparison.COMPARE_TYPE) == 0; + } + }; + return p; + } + + /** + * Creates a new doc package descriptor. + * + * @param revision The revision of the doc package. + * @return A {@link PkgDesc} describing this doc package. + */ + @NonNull + public static Builder newDoc(@NonNull AndroidVersion version, + @NonNull MajorRevision revision) { + Builder p = new Builder(PkgType.PKG_DOC); + p.mAndroidVersion = version; + p.mMajorRevision = revision; + p.mCustomIsUpdateFor = new IIsUpdateFor() { + @Override + public boolean isUpdateFor(PkgDesc thisPkgDesc, IPkgDesc existingDesc) { + if (existingDesc == null || + !thisPkgDesc.getType().equals(existingDesc.getType())) { + return false; + } + + // This package is unique in the SDK. It's an update if the API is newer + // or the revision is newer for the same API. + int diff = thisPkgDesc.getAndroidVersion().compareTo( + existingDesc.getAndroidVersion()); + return diff > 0 || + (diff == 0 && thisPkgDesc.getMajorRevision().compareTo( + existingDesc.getMajorRevision()) > 0); + } + }; + return p; + } + + /** + * Creates a new extra package descriptor. + * + * @param vendor The vendor id string of the extra package. + * @param path The path id string of the extra package. + * @param displayName The display name. If missing, caller should build one using the path. + * @param oldPaths An optional list of older paths for this extra package. + * @param revision The revision of the extra package. + * @return A {@link PkgDesc} describing this extra package. + */ + @NonNull + public static Builder newExtra(@NonNull IdDisplay vendor, + @NonNull String path, + @Nullable String displayName, + @Nullable String[] oldPaths, + @NonNull NoPreviewRevision revision) { + Builder p = new Builder(PkgType.PKG_EXTRA); + p.mFullRevision = revision; + p.mVendor = vendor; + p.mPath = path; + p.mNameDisplay = displayName; + p.mOldPaths = oldPaths; + return p; + } + + /** + * Creates a new platform package descriptor. + * + * @param version The android version of the platform package. + * @param revision The revision of the extra package. + * @param minToolsRev An optional {@code min-tools-rev}. + * Use {@link FullRevision#NOT_SPECIFIED} to indicate + * there is no requirement. + * @return A {@link PkgDesc} describing this platform package. + */ + @NonNull + public static Builder newPlatform(@NonNull AndroidVersion version, + @NonNull MajorRevision revision, + @NonNull FullRevision minToolsRev) { + Builder p = new Builder(PkgType.PKG_PLATFORM); + p.mAndroidVersion = version; + p.mMajorRevision = revision; + p.mMinToolsRev = minToolsRev; + p.mCustomPath = new IGetPath() { + @Override + public String getPath(PkgDesc thisPkgDesc) { + /** The "path" of a Platform is its Target Hash. */ + return AndroidTargetHash.getPlatformHashString(thisPkgDesc.getAndroidVersion()); + } + }; + return p; + } + + /** + * Create a new add-on package descriptor. + *

+ * The vendor id and the name id provided are used to compute the add-on's + * target hash. + * + * @param version The android version of the add-on package. + * @param revision The revision of the add-on package. + * @param addonVendor The vendor id/display of the add-on package. + * @param addonName The name id/display of the add-on package. + * @return A {@link PkgDesc} describing this add-on package. + */ + @NonNull + public static Builder newAddon(@NonNull AndroidVersion version, + @NonNull MajorRevision revision, + @NonNull IdDisplay addonVendor, + @NonNull IdDisplay addonName) { + Builder p = new Builder(PkgType.PKG_ADDON); + p.mAndroidVersion = version; + p.mMajorRevision = revision; + p.mVendor = addonVendor; + p.mNameIdDisplay = addonName; + return p; + } + + /** + * Create a new platform system-image package descriptor. + *

+ * For system-images, {@link PkgDesc#getPath()} returns the ABI. + * + * @param version The android version of the system-image package. + * @param tag The tag of the system-image package. + * @param abi The ABI of the system-image package. + * @param revision The revision of the system-image package. + * @return A {@link PkgDesc} describing this system-image package. + */ + @NonNull + public static Builder newSysImg(@NonNull AndroidVersion version, + @NonNull IdDisplay tag, + @NonNull String abi, + @NonNull MajorRevision revision) { + Builder p = new Builder(PkgType.PKG_SYS_IMAGE); + p.mAndroidVersion = version; + p.mMajorRevision = revision; + p.mTag = tag; + p.mPath = abi; + p.mVendor = null; + return p; + } + + /** + * Create a new add-on system-image package descriptor. + *

+ * For system-images, {@link PkgDesc#getPath()} returns the ABI. + * + * @param version The android version of the system-image package. + * @param addonVendor The vendor id/display of an associated add-on. + * @param addonName The tag of the system-image package is the add-on name. + * @param abi The ABI of the system-image package. + * @param revision The revision of the system-image package. + * @return A {@link PkgDesc} describing this system-image package. + */ + @NonNull + public static Builder newAddonSysImg(@NonNull AndroidVersion version, + @NonNull IdDisplay addonVendor, + @NonNull IdDisplay addonName, + @NonNull String abi, + @NonNull MajorRevision revision) { + Builder p = new Builder(PkgType.PKG_ADDON_SYS_IMAGE); + p.mAndroidVersion = version; + p.mMajorRevision = revision; + p.mTag = addonName; + p.mPath = abi; + p.mVendor = addonVendor; + return p; + } + + /** + * Create a new source package descriptor. + * + * @param version The android version of the source package. + * @param revision The revision of the source package. + * @return A {@link PkgDesc} describing this source package. + */ + @NonNull + public static Builder newSource(@NonNull AndroidVersion version, + @NonNull MajorRevision revision) { + Builder p = new Builder(PkgType.PKG_SOURCE); + p.mAndroidVersion = version; + p.mMajorRevision = revision; + return p; + } + + /** + * Create a new sample package descriptor. + * + * @param version The android version of the sample package. + * @param revision The revision of the sample package. + * @param minToolsRev An optional {@code min-tools-rev}. + * Use {@link FullRevision#NOT_SPECIFIED} to indicate + * there is no requirement. + * @return A {@link PkgDesc} describing this sample package. + */ + @NonNull + public static Builder newSample(@NonNull AndroidVersion version, + @NonNull MajorRevision revision, + @NonNull FullRevision minToolsRev) { + Builder p = new Builder(PkgType.PKG_SAMPLE); + p.mAndroidVersion = version; + p.mMajorRevision = revision; + p.mMinToolsRev = minToolsRev; + return p; + } + + /** + * Creates a new NDK package descriptor. + * + * @param revision The revision of the NDK package. + * @return A {@link PkgDesc} describing this NDK package. + */ + @NonNull + public static Builder newNdk(@NonNull FullRevision revision) { + Builder p = new Builder(PkgType.PKG_NDK); + p.mFullRevision = revision; + return p; + } + + public Builder setLicense(@Nullable License license) { + mLicense = license; + return this; + } + + public Builder setListDisplay(@Nullable String text) { + mListDisplay = text; + return this; + } + + public Builder setDescriptionShort(@Nullable String text) { + mDescriptionShort = text; + return this; + } + + public Builder setDescriptionUrl(@Nullable String text) { + mDescriptionUrl = text; + return this; + } + + public Builder setIsObsolete(boolean isObsolete) { + mIsObsolete = isObsolete; + return this; + } + + public IPkgDesc create() { + if (mType == PkgType.PKG_ADDON) { + return new PkgDescAddon( + mType, + mLicense, + mListDisplay, + mDescriptionShort, + mDescriptionUrl, + mIsObsolete, + mMajorRevision, + mAndroidVersion, + mVendor, + mNameIdDisplay); + } + + if (mType == PkgType.PKG_EXTRA) { + return new PkgDescExtra( + mType, + mLicense, + mListDisplay, + mDescriptionShort, + mDescriptionUrl, + mIsObsolete, + mFullRevision, + mMajorRevision, + mAndroidVersion, + mPath, + mTag, + mVendor, + mMinToolsRev, + mMinPlatformToolsRev, + mNameDisplay, + mOldPaths); + } + + return new PkgDesc( + mType, + mLicense, + mListDisplay, + mDescriptionShort, + mDescriptionUrl, + mIsObsolete, + mFullRevision, + mMajorRevision, + mAndroidVersion, + mPath, + mTag, + mVendor, + mMinToolsRev, + mMinPlatformToolsRev, + mCustomIsUpdateFor, + mCustomPath); + } + } + + // ---- Helpers ----- + + @NonNull + private static String sanitize(@NonNull String str) { + str = str.toLowerCase(Locale.US).replaceAll("[^a-z0-9_.-]+", "_").replaceAll("_+", "_"); + return str; + } +} + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/PkgDescAddon.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/PkgDescAddon.java new file mode 100644 index 00000000..9694d1f8 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/PkgDescAddon.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2013 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.sdklib.repository.descriptors; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdklib.AndroidTargetHash; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.repository.License; +import com.android.sdklib.repository.MajorRevision; + +/** + * Implementation detail of {@link PkgDesc} for add-ons. + * Do not use this class directly. + * To create an instance use {@link PkgDesc.Builder#newAddon} instead. + */ +final class PkgDescAddon extends PkgDesc implements IPkgDescAddon { + + private final IdDisplay mAddonName; + + /** + * Add-on descriptor. + * The following attributes are mandatory: + */ + PkgDescAddon(@NonNull PkgType type, + @Nullable License license, + @Nullable String listDisplay, + @Nullable String descriptionShort, + @Nullable String descriptionUrl, + boolean isObsolete, + @NonNull MajorRevision majorRevision, + @NonNull AndroidVersion androidVersion, + @NonNull IdDisplay addonVendor, + @NonNull IdDisplay addonName) { + super(type, + license, + listDisplay, + descriptionShort, + descriptionUrl, + isObsolete, + null, //fullRevision + majorRevision, + androidVersion, + AndroidTargetHash.getAddonHashString(addonVendor.getDisplay(), + addonName.getDisplay(), + androidVersion), + null, //tag + addonVendor, + null, //minToolsRev + null, //minPlatformToolsRev + null, //customIsUpdateFor + null); //customPath + + mAddonName = addonName; + } + + @NonNull + @Override + public IdDisplay getName() { + return mAddonName; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/PkgDescExtra.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/PkgDescExtra.java new file mode 100644 index 00000000..b5f2efbf --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/PkgDescExtra.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2013 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.sdklib.repository.descriptors; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.repository.License; +import com.android.sdklib.repository.FullRevision; +import com.android.sdklib.repository.MajorRevision; + +/** + * Implementation detail of {@link IPkgDescExtra} for extra packages. + */ +public final class PkgDescExtra extends PkgDesc implements IPkgDescExtra { + + private final String[] mOldPaths; + private final String mNameDisplay; + + PkgDescExtra(@NonNull PkgType type, + @Nullable License license, + @Nullable String listDisplay, + @Nullable String descriptionShort, + @Nullable String descriptionUrl, + boolean isObsolete, + @Nullable FullRevision fullRevision, + @Nullable MajorRevision majorRevision, + @Nullable AndroidVersion androidVersion, + @Nullable String path, + @Nullable IdDisplay tag, + @Nullable IdDisplay vendor, + @Nullable FullRevision minToolsRev, + @Nullable FullRevision minPlatformToolsRev, + @Nullable String nameDisplay, + @Nullable final String[] oldPaths) { + super(type, + license, + listDisplay, + descriptionShort, + descriptionUrl, + isObsolete, + fullRevision, + majorRevision, + androidVersion, + path, + tag, + vendor, + minToolsRev, + minPlatformToolsRev, + null, //customIsUpdateFor + null); //customPath + mNameDisplay = nameDisplay; + mOldPaths = oldPaths != null ? oldPaths : new String[0]; + } + + @NonNull + @Override + public String[] getOldPaths() { + return mOldPaths; + } + + @NonNull + @Override + public String getNameDisplay() { + return mNameDisplay == null ? String.format("Unknown (%s)", getInstallId()) : mNameDisplay; + } + + // ---- Helpers ---- + + /** + * Helper method that converts the old_paths property string into the + * an old paths array. + * + * @param oldPathsProperty A possibly-null old_path property string. + * @return A list of old paths split by their separator. Can be empty but not null. + */ + @NonNull + public static String[] convertOldPaths(@Nullable String oldPathsProperty) { + if (oldPathsProperty == null || oldPathsProperty.isEmpty()) { + return new String[0]; + } + return oldPathsProperty.split(";"); //$NON-NLS-1$ + } + + /** + * Helper to computhe whether the extra path of both {@link IPkgDescExtra}s + * are compatible with each other, which means they are either equal or are + * matched between existing path and the potential old paths list. + *

+ * This also covers backward compatibility -- in earlier schemas the vendor id was + * merged into the path string when reloading installed extras. + * + * @param lhs A non-null {@link IPkgDescExtra}. + * @param rhs Another non-null {@link IPkgDescExtra}. + * @return true if the paths are compatible. + */ + public static boolean compatibleVendorAndPath( + @NonNull IPkgDescExtra lhs, + @NonNull IPkgDescExtra rhs) { + String[] epOldPaths = rhs.getOldPaths(); + int lenEpOldPaths = epOldPaths.length; + for (int indexEp = -1; indexEp < lenEpOldPaths; indexEp++) { + if (sameVendorAndPath( + lhs.getVendor().getId(), lhs.getPath(), + rhs.getVendor().getId(), indexEp < 0 ? rhs.getPath() : epOldPaths[indexEp])) { + return true; + } + } + + String[] thisOldPaths = lhs.getOldPaths(); + int lenThisOldPaths = thisOldPaths.length; + for (int indexThis = -1; indexThis < lenThisOldPaths; indexThis++) { + if (sameVendorAndPath( + lhs.getVendor().getId(), indexThis < 0 ? lhs.getPath() : thisOldPaths[indexThis], + rhs.getVendor().getId(), rhs.getPath())) { + return true; + } + } + + return false; + } + + private static boolean sameVendorAndPath( + @Nullable String thisVendor, @Nullable String thisPath, + @Nullable String otherVendor, @Nullable String otherPath) { + // To be backward compatible, we need to support the old vendor-path form + // in either the current or the remote package. + // + // The vendor test below needs to account for an old installed package + // (e.g. with an install path of vendor-name) that has then been updated + // in-place and thus when reloaded contains the vendor name in both the + // path and the vendor attributes. + if (otherPath != null && thisPath != null && thisVendor != null) { + if (otherPath.equals(thisVendor + '-' + thisPath) && + (otherVendor == null || + otherVendor.isEmpty() || + otherVendor.equals(thisVendor))) { + return true; + } + } + if (thisPath != null && otherPath != null && otherVendor != null) { + if (thisPath.equals(otherVendor + '-' + otherPath) && + (thisVendor == null || + thisVendor.isEmpty() || + thisVendor.equals(otherVendor))) { + return true; + } + } + + + if (thisPath != null && thisPath.equals(otherPath)) { + if ((thisVendor == null && otherVendor == null) || + (thisVendor != null && thisVendor.equals(otherVendor))) { + return true; + } + } + + return false; + } + +} + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/PkgType.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/PkgType.java new file mode 100644 index 00000000..9f0ccc27 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/PkgType.java @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2013 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.sdklib.repository.descriptors; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.internal.repository.LocalSdkParser; +import com.android.sdklib.repository.FullRevision; +import com.android.sdklib.repository.MajorRevision; +import com.android.sdklib.repository.local.LocalSdk; + +import java.util.EnumSet; + +/** + * Package types handled by the {@link LocalSdk}. + *

+ * Integer bit values are provided via {@link #getIntValue()} for backward + * compatibility with the older {@link LocalSdkParser} class. + * The integer bit values also indicate the natural ordering of the packages. + */ +public enum PkgType implements IPkgCapabilities { + + // Boolean attributes below, in that order: + // maj-r, full-r, api, path, tag, vend, min-t-r, min-pt-r + // + // Corresponding flags for list description pattern string: + // $MAJ $FULL $API $PATH $TAG $VEND $NAME (for extras & add-ons) + // + // + + /** Filter the SDK/tools folder. + * Has {@link FullRevision}. */ + PKG_TOOLS(0x0001, SdkConstants.FD_TOOLS, + "Android SDK Tools $FULL", + false, true /*full-r*/, false, false, false, false, false, true /*min-pt-r*/), + + /** Filter the SDK/platform-tools folder. + * Has {@link FullRevision}. */ + PKG_PLATFORM_TOOLS(0x0002, SdkConstants.FD_PLATFORM_TOOLS, + "Android SDK Platform-Tools $FULL", + false, true /*full-r*/, false, false, false, false, false, false), + + /** Filter the SDK/build-tools folder. + * Has {@link FullRevision}. */ + PKG_BUILD_TOOLS(0x0004, SdkConstants.FD_BUILD_TOOLS, + "Android SDK Build-Tools $FULL", + false, true /*full-r*/, false, false, false, false, false, false), + + /** Filter the SDK/docs folder. + * Has {@link MajorRevision}. */ + PKG_DOC(0x0010, SdkConstants.FD_DOCS, + "Documentation for Android SDK", + true /*maj-r*/, false, true /*api*/, false, false, false, false, false), + + /** Filter the SDK/platforms. + * Has {@link AndroidVersion}. Has {@link MajorRevision}. + * Path returns the platform's target hash. */ + PKG_PLATFORM(0x0100, SdkConstants.FD_PLATFORMS, + "Android SDK Platform $API{?$MAJ>1:, rev $MAJ}", + true /*maj-r*/, false, true /*api*/, true /*path*/, false, false, true /*min-t-r*/, false), + + /** Filter the SDK/system-images/android. + * Has {@link AndroidVersion}. Has {@link MajorRevision}. Has tag. + * Path returns the system image ABI. */ + PKG_SYS_IMAGE(0x0200, SdkConstants.FD_SYSTEM_IMAGES, + "$PATH System Image, Android $API{?$MAJ>1:, rev $MAJ}", + true /*maj-r*/, false, true /*api*/, true /*path*/, true /*tag*/, false /*vend*/, false, false), + + /** Filter the SDK/addons. + * Has {@link AndroidVersion}. Has {@link MajorRevision}. + * Path returns the add-on's target hash. */ + PKG_ADDON(0x0400, SdkConstants.FD_ADDONS, + "{|$NAME|$VEND $PATH|}, Android $API{?$MAJ>1:, rev $MAJ}", + true /*maj-r*/, false, true /*api*/, true /*path*/, false, true /*vend*/, false, false), + + /** Filter the SDK/system-images/addons. + * Has {@link AndroidVersion}. Has {@link MajorRevision}. Has tag. + * Path returns the system image ABI. */ + PKG_ADDON_SYS_IMAGE(0x0800, SdkConstants.FD_SYSTEM_IMAGES, + "{|$NAME|$VEND $PATH|} System Image, Android $API{?$MAJ>1:, rev $MAJ}", + true /*maj-r*/, false, true /*api*/, true /*path*/, true /*tag*/, true /*vend*/, false, false), + + /** Filter the SDK/samples folder. + * Note: this will not detect samples located in the SDK/extras packages. + * Has {@link AndroidVersion}. Has {@link MajorRevision}. */ + PKG_SAMPLE(0x1000, SdkConstants.FD_SAMPLES, + "Samples for Android $API{?$MAJ>1:, rev $MAJ}", + true /*maj-r*/, false, true /*api*/, false, false, false, true /*min-t-r*/, false), + + /** Filter the SDK/sources folder. + * Has {@link AndroidVersion}. Has {@link MajorRevision}. */ + PKG_SOURCE(0x2000, SdkConstants.FD_ANDROID_SOURCES, + "Sources for Android $API{?$MAJ>1:, rev $MAJ}", + true /*maj-r*/, false, true /*api*/, false, false, false, false, false), + + /** Filter the SDK/extras folder. + * Has {@code Path}. Has {@link MajorRevision}. + * Path returns the combined vendor id + extra path. + * Cast the descriptor to {@link IPkgDescExtra} to get extra's specific attributes. */ + PKG_EXTRA(0x4000, SdkConstants.FD_EXTRAS, + "{|$NAME|$VEND $PATH|}{?$FULL>1:, rev $FULL}", + false, true /*full-r*/, false, true /*path*/, false, true /*vend*/, false, false), + + /** The SDK/ndk folder. */ + PKG_NDK(0x8000, SdkConstants.FD_NDK, "", + false, true, false, false, false, false, false, false); + + /** A collection of all the known PkgTypes. */ + public static final EnumSet PKG_ALL = EnumSet.allOf(PkgType.class); + + /** Integer value matching all available pkg types, for the old LocalSdkParer. */ + public static final int PKG_ALL_INT = 0xFFFF; + + private int mIntValue; + private String mFolderName; + + private final boolean mHasMajorRevision; + private final boolean mHasFullRevision; + private final boolean mHasAndroidVersion; + private final boolean mHasPath; + private final boolean mHasTag; + private final boolean mHasVendor; + private final boolean mHasMinToolsRev; + private final boolean mHasMinPlatformToolsRev; + private final String mListDisplayPattern; + + PkgType(int intValue, + @NonNull String folderName, + @NonNull String listDisplayPattern, + boolean hasMajorRevision, + boolean hasFullRevision, + boolean hasAndroidVersion, + boolean hasPath, + boolean hasTag, + boolean hasVendor, + boolean hasMinToolsRev, + boolean hasMinPlatformToolsRev) { + mIntValue = intValue; + mFolderName = folderName; + mListDisplayPattern = listDisplayPattern; + mHasMajorRevision = hasMajorRevision; + mHasFullRevision = hasFullRevision; + mHasAndroidVersion = hasAndroidVersion; + mHasPath = hasPath; + mHasTag = hasTag; + mHasVendor = hasVendor; + mHasMinToolsRev = hasMinToolsRev; + mHasMinPlatformToolsRev = hasMinPlatformToolsRev; + } + + /** Returns the integer value matching the type, compatible with the old LocalSdkParer. */ + public int getIntValue() { + return mIntValue; + } + + /** Returns the name of SDK top-folder where this type of package is stored. */ + @NonNull + public String getFolderName() { + return mFolderName; + } + + @Override + public boolean hasMajorRevision() { + return mHasMajorRevision; + } + + @Override + public boolean hasFullRevision() { + return mHasFullRevision; + } + + @Override + public boolean hasAndroidVersion() { + return mHasAndroidVersion; + } + + @Override + public boolean hasPath() { + return mHasPath; + } + + @Override + public boolean hasTag() { + return mHasTag; + } + + @Override + public boolean hasVendor() { + return mHasVendor; + } + + @Override + public boolean hasMinToolsRev() { + return mHasMinToolsRev; + } + + @Override + public boolean hasMinPlatformToolsRev() { + return mHasMinPlatformToolsRev; + } + + /* + * Returns a pattern string used by {@link PkgDesc#getListDescription()} to + * compute a default list-display representation string for this package. + */ + public String getListDisplayPattern() { + return mListDisplayPattern; + } +} + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalAddonPkgInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalAddonPkgInfo.java new file mode 100644 index 00000000..bce325e2 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalAddonPkgInfo.java @@ -0,0 +1,503 @@ +/* + * Copyright (C) 2013 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.sdklib.repository.local; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.ISystemImage; +import com.android.sdklib.ISystemImage.LocationType; +import com.android.sdklib.SystemImage; +import com.android.sdklib.internal.androidTarget.AddOnTarget; +import com.android.sdklib.internal.androidTarget.PlatformTarget; +import com.android.sdklib.internal.project.ProjectProperties; +import com.android.sdklib.io.LegacyFileOp; +import com.android.sdklib.io.IFileOp; +import com.android.sdklib.repository.AddonManifestIniProps; +import com.android.sdklib.repository.FullRevision; +import com.android.sdklib.repository.MajorRevision; +import com.android.sdklib.repository.descriptors.*; +import com.android.utils.Pair; +import com.google.common.base.Objects; +import com.google.common.collect.SetMultimap; +import com.google.common.collect.TreeMultimap; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@SuppressWarnings("MethodMayBeStatic") +public class LocalAddonPkgInfo extends LocalPlatformPkgInfo { + + private static final Pattern PATTERN_LIB_DATA = Pattern.compile( + "^([a-zA-Z0-9._-]+\\.jar);(.*)$", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$ + + // usb ids are 16-bit hexadecimal values. + private static final Pattern PATTERN_USB_IDS = Pattern.compile( + "^0x[a-f0-9]{4}$", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$ + + @NonNull + private final IPkgDescAddon mAddonDesc; + + public LocalAddonPkgInfo(@NonNull LocalSdk localSdk, + @NonNull File localDir, + @NonNull Properties sourceProps, + @NonNull AndroidVersion version, + @NonNull MajorRevision revision, + @NonNull IdDisplay vendor, + @NonNull IdDisplay name) { + super(localSdk, localDir, sourceProps, version, revision, FullRevision.NOT_SPECIFIED); + mAddonDesc = (IPkgDescAddon) PkgDesc.Builder.newAddon(version, revision, vendor, name) + .create(); + } + + @NonNull + @Override + public IPkgDesc getDesc() { + return mAddonDesc; + } + + /** The "path" of an add-on is its Target Hash. */ + @Override + @NonNull + public String getTargetHash() { + return getDesc().getPath(); + } + + //----- + + /** + * Computes a sanitized name-id based on an addon name-display. + * This is used to provide compatibility with older add-ons that lacks the new fields. + * + * @param displayName A name-display field or a old-style name field. + * @return A non-null sanitized name-id that fits in the {@code [a-zA-Z0-9_-]+} pattern. + */ + public static String sanitizeDisplayToNameId(@NonNull String displayName) { + String name = displayName.toLowerCase(Locale.US); + name = name.replaceAll("[^a-z0-9_-]+", "_"); //$NON-NLS-1$ //$NON-NLS-2$ + name = name.replaceAll("_+", "_"); //$NON-NLS-1$ //$NON-NLS-2$ + + // Trim leading and trailing underscores + if (name.length() > 1) { + name = name.replaceAll("^_+", ""); //$NON-NLS-1$ //$NON-NLS-2$ + } + if (name.length() > 1) { + name = name.replaceAll("_+$", ""); //$NON-NLS-1$ //$NON-NLS-2$ + } + return name; + } + + //----- + + /** + * Creates the AddOnTarget. Invoked by {@link #getAndroidTarget()}. + */ + @Override + @Nullable + protected IAndroidTarget createAndroidTarget() { + LocalSdk sdk = getLocalSdk(); + IFileOp fileOp = sdk.getFileOp(); + + // Parse the addon properties to ensure we can load it. + Pair, String> infos = parseAddonProperties(); + + Map propertyMap = infos.getFirst(); + String error = infos.getSecond(); + + if (error != null) { + appendLoadError("Ignoring add-on '%1$s': %2$s", getLocalDir().getName(), error); + return null; + } + + // Since error==null we're not supposed to encounter any issues loading this add-on. + try { + assert propertyMap != null; + + String api = propertyMap.get(AddonManifestIniProps.ADDON_API); + String name = propertyMap.get(AddonManifestIniProps.ADDON_NAME); + String vendor = propertyMap.get(AddonManifestIniProps.ADDON_VENDOR); + + assert api != null; + assert name != null; + assert vendor != null; + + PlatformTarget baseTarget = null; + + // Look for a platform that has a matching api level or codename. + LocalPkgInfo plat = sdk.getPkgInfo(PkgType.PKG_PLATFORM, + getDesc().getAndroidVersion()); + if (plat instanceof LocalPlatformPkgInfo) { + baseTarget = (PlatformTarget) ((LocalPlatformPkgInfo) plat).getAndroidTarget(); + } + assert baseTarget != null; + + // get the optional description + String description = propertyMap.get(AddonManifestIniProps.ADDON_DESCRIPTION); + + // get the add-on revision + int revisionValue = 1; + String revision = propertyMap.get(AddonManifestIniProps.ADDON_REVISION); + if (revision == null) { + revision = propertyMap.get(AddonManifestIniProps.ADDON_REVISION_OLD); + } + if (revision != null) { + revisionValue = Integer.parseInt(revision); + } + + // get the optional libraries + String librariesValue = propertyMap.get(AddonManifestIniProps.ADDON_LIBRARIES); + Map libMap = null; + + if (librariesValue != null) { + librariesValue = librariesValue.trim(); + if (!librariesValue.isEmpty()) { + // split in the string into the libraries name + String[] libraries = librariesValue.split(";"); //$NON-NLS-1$ + if (libraries.length > 0) { + libMap = new HashMap(); + for (String libName : libraries) { + libName = libName.trim(); + + // get the library data from the properties + String libData = propertyMap.get(libName); + + if (libData != null) { + // split the jar file from the description + Matcher m = PATTERN_LIB_DATA.matcher(libData); + if (m.matches()) { + libMap.put(libName, new String[] { + m.group(1), m.group(2) }); + } else { + appendLoadError( + "Ignoring library '%1$s', property value has wrong format\n\t%2$s", + libName, libData); + } + } else { + appendLoadError( + "Ignoring library '%1$s', missing property value", + libName, libData); + } + } + } + } + } + + // get the abi list. + ISystemImage[] systemImages = getAddonSystemImages(fileOp); + + // check whether the add-on provides its own rendering info/library. + boolean hasRenderingLibrary = false; + boolean hasRenderingResources = false; + + File dataFolder = new File(getLocalDir(), SdkConstants.FD_DATA); + if (fileOp.isDirectory(dataFolder)) { + hasRenderingLibrary = + fileOp.isFile(new File(dataFolder, SdkConstants.FN_LAYOUTLIB_JAR)); + hasRenderingResources = + fileOp.isDirectory(new File(dataFolder, SdkConstants.FD_RES)) && + fileOp.isDirectory(new File(dataFolder, SdkConstants.FD_FONTS)); + } + + AddOnTarget target = new AddOnTarget( + getLocalDir().getAbsolutePath(), + name, + vendor, + revisionValue, + description, + systemImages, + libMap, + hasRenderingLibrary, + hasRenderingResources, + baseTarget); + + // parse the legacy skins, located under SDK/addons/addon-name/skins/[skin-name] + // and merge with the system-image skins, if any, merging them by name. + File targetSkinFolder = target.getFile(IAndroidTarget.SKINS); + + Map skinsMap = new TreeMap(); + + for (File f : PackageParserUtils.parseSkinFolder(targetSkinFolder, fileOp)) { + skinsMap.put(f.getName().toLowerCase(Locale.US), f); + } + for (ISystemImage si : systemImages) { + for (File f : si.getSkins()) { + skinsMap.put(f.getName().toLowerCase(Locale.US), f); + } + } + + List skins = new ArrayList(skinsMap.values()); + Collections.sort(skins); + + // get the default skin + File defaultSkin = null; + String defaultSkinName = propertyMap.get(AddonManifestIniProps.ADDON_DEFAULT_SKIN); + if (defaultSkinName != null) { + defaultSkin = new File(targetSkinFolder, defaultSkinName); + } else { + // No default skin name specified, use the first one from the addon + // or the default from the platform. + if (skins.size() == 1) { + defaultSkin = skins.get(0); + } else { + defaultSkin = baseTarget.getDefaultSkin(); + } + } + + // get the USB ID (if available) + int usbVendorId = convertId(propertyMap.get(AddonManifestIniProps.ADDON_USB_VENDOR)); + if (usbVendorId != IAndroidTarget.NO_USB_ID) { + target.setUsbVendorId(usbVendorId); + } + + target.setSkins(skins.toArray(new File[skins.size()]), defaultSkin); + + return target; + + } catch (Exception e) { + appendLoadError("Ignoring add-on '%1$s': error %2$s.", + getLocalDir().getName(), e.toString()); + } + + return null; + + } + + /** + * Parses the add-on properties and decodes any error that occurs when loading an addon. + * + * @return A pair with the property map and an error string. Both can be null but not at the + * same time. If a non-null error is present then the property map must be ignored. The error + * should be translatable as it might show up in the SdkManager UI. + */ + @NonNull + private Pair, String> parseAddonProperties() { + Map propertyMap = null; + String error = null; + + IFileOp fileOp = getLocalSdk().getFileOp(); + File addOnManifest = new File(getLocalDir(), SdkConstants.FN_MANIFEST_INI); + + do { + if (!fileOp.isFile(addOnManifest)) { + error = String.format("File not found: %1$s", SdkConstants.FN_MANIFEST_INI); + break; + } + + try { + propertyMap = ProjectProperties.parsePropertyStream( + fileOp.newFileInputStream(addOnManifest), + addOnManifest.getPath(), + null /*log*/); + if (propertyMap == null) { + error = String.format("Failed to parse properties from %1$s", + SdkConstants.FN_MANIFEST_INI); + break; + } + } catch (FileNotFoundException e) { + // this can happen if the system fails to open the file because of too many + // open files. + error = String.format("Failed to parse properties from %1$s: %2$s", + SdkConstants.FN_MANIFEST_INI, e.getMessage()); + break; + } + + // look for some specific values in the map. + // we require name, vendor, and api + String name = propertyMap.get(AddonManifestIniProps.ADDON_NAME); + if (name == null) { + error = addonManifestWarning(AddonManifestIniProps.ADDON_NAME); + break; + } + + String vendor = propertyMap.get(AddonManifestIniProps.ADDON_VENDOR); + if (vendor == null) { + error = addonManifestWarning(AddonManifestIniProps.ADDON_VENDOR); + break; + } + + String api = propertyMap.get(AddonManifestIniProps.ADDON_API); + if (api == null) { + error = addonManifestWarning(AddonManifestIniProps.ADDON_API); + break; + } + + // Look for a platform that has a matching api level or codename. + IAndroidTarget baseTarget = null; + LocalPkgInfo plat = getLocalSdk().getPkgInfo(PkgType.PKG_PLATFORM, + getDesc().getAndroidVersion()); + if (plat instanceof LocalPlatformPkgInfo) { + baseTarget = ((LocalPlatformPkgInfo) plat).getAndroidTarget(); + } + + if (baseTarget == null) { + error = String.format("Unable to find base platform with API level '%1$s'", api); + break; + } + + // get the add-on revision + String revision = propertyMap.get(AddonManifestIniProps.ADDON_REVISION); + if (revision == null) { + revision = propertyMap.get(AddonManifestIniProps.ADDON_REVISION_OLD); + } + if (revision != null) { + try { + Integer.parseInt(revision); + } catch (NumberFormatException e) { + // looks like revision does not parse to a number. + error = String.format("%1$s is not a valid number in %2$s.", + AddonManifestIniProps.ADDON_REVISION, SdkConstants.FN_BUILD_PROP); + break; + } + } + + } while(false); + + return Pair.of(propertyMap, error); + } + + /** + * Prepares a warning about the addon being ignored due to a missing manifest value. + * This string will show up in the SdkManager UI. + * + * @param valueName The missing manifest value, for display. + */ + @NonNull + private static String addonManifestWarning(@NonNull String valueName) { + return String.format("'%1$s' is missing from %2$s.", + valueName, SdkConstants.FN_MANIFEST_INI); + } + + /** + * Converts a string representation of an hexadecimal ID into an int. + * @param value the string to convert. + * @return the int value, or {@link IAndroidTarget#NO_USB_ID} if the conversion failed. + */ + private int convertId(@Nullable String value) { + if (value != null && !value.isEmpty()) { + if (PATTERN_USB_IDS.matcher(value).matches()) { + String v = value.substring(2); + try { + return Integer.parseInt(v, 16); + } catch (NumberFormatException e) { + // this shouldn't happen since we check the pattern above, but this is safer. + // the method will return 0 below. + } + } + } + + return IAndroidTarget.NO_USB_ID; + } + + /** + * Get all the system images supported by an add-on target. + * For an add-on, we first look in the new sdk/system-images folders then we look + * for sub-folders in the addon/images directory. + * If none are found but the directory exists and is not empty, assume it's a legacy + * arm eabi system image. + * If any given API appears twice or more, the first occurrence wins. + *

+ * Note that it's OK for an add-on to have no system-images at all, since it can always + * rely on the ones from its base platform. + * + * @param fileOp File operation wrapper. + * @return an array of ISystemImage containing all the system images for the target. + * The list can be empty but not null. + */ + @NonNull + private ISystemImage[] getAddonSystemImages(IFileOp fileOp) { + Set found = new TreeSet(); + SetMultimap tagToAbiFound = TreeMultimap.create(); + + + // Look in the system images folders: + // - SDK/system-image/platform/addon-id-tag/abi + // - SDK/system-image/addon-id-tag/abi (many abi possible) + // Optional: look for skins under + // - SDK/system-image/platform/addon-id-tag/abi/skins/skin-name + // - SDK/system-image/addon-id-tag/abi/skins/skin-name + // If we find multiple occurrences of the same platform/abi, the first one read wins. + + LocalPkgInfo[] sysImgInfos = getLocalSdk().getPkgsInfos(PkgType.PKG_ADDON_SYS_IMAGE); + for (LocalPkgInfo pkg : sysImgInfos) { + IPkgDesc d = pkg.getDesc(); + if (pkg instanceof LocalAddonSysImgPkgInfo && + d.hasVendor() && + mAddonDesc.getVendor().equals(d.getVendor()) && + mAddonDesc.getName().equals(d.getTag()) && + Objects.equal(mAddonDesc.getAndroidVersion(), pkg.getDesc().getAndroidVersion())) { + final IdDisplay tag = mAddonDesc.getName(); + final String abi = d.getPath(); + if (abi != null && !tagToAbiFound.containsEntry(tag, abi)) { + found.add(((LocalAddonSysImgPkgInfo)pkg).getSystemImage()); + tagToAbiFound.put(tag, abi); + } + } + } + + // Look for sub-directories: + // - SDK/addons/addon-name/images/abi (multiple abi possible) + // - SDK/addons/addon-name/armeabi (legacy support) + boolean useLegacy = true; + boolean hasImgFiles = false; + final IdDisplay defaultTag = SystemImage.DEFAULT_TAG; + + File imagesDir = new File(getLocalDir(), SdkConstants.OS_IMAGES_FOLDER); + File[] files = fileOp.listFiles(imagesDir); + for (File file : files) { + if (fileOp.isDirectory(file)) { + useLegacy = false; + String abi = file.getName(); + if (!tagToAbiFound.containsEntry(defaultTag, abi)) { + found.add(new SystemImage( + file, + LocationType.IN_IMAGES_SUBFOLDER, + SystemImage.DEFAULT_TAG, + mAddonDesc.getVendor(), + abi, + LegacyFileOp.EMPTY_FILE_ARRAY)); + tagToAbiFound.put(defaultTag, abi); + } + } else if (!hasImgFiles && fileOp.isFile(file)) { + if (file.getName().endsWith(".img")) { //$NON-NLS-1$ + // The legacy images folder is only valid if it contains some .img files + hasImgFiles = true; + } + } + } + + if (useLegacy && + hasImgFiles && + fileOp.isDirectory(imagesDir) && + !tagToAbiFound.containsEntry(defaultTag, SdkConstants.ABI_ARMEABI)) { + // We found no sub-folder system images but it looks like the top directory + // has some img files in it. It must be a legacy ARM EABI system image folder. + found.add(new SystemImage( + imagesDir, + LocationType.IN_LEGACY_FOLDER, + SystemImage.DEFAULT_TAG, + SdkConstants.ABI_ARMEABI, + LegacyFileOp.EMPTY_FILE_ARRAY)); + } + + return found.toArray(new ISystemImage[found.size()]); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalAddonSysImgPkgInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalAddonSysImgPkgInfo.java new file mode 100644 index 00000000..e02c8e95 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalAddonSysImgPkgInfo.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2014 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.sdklib.repository.local; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.ISystemImage; +import com.android.sdklib.repository.MajorRevision; +import com.android.sdklib.repository.descriptors.IPkgDesc; +import com.android.sdklib.repository.descriptors.IdDisplay; +import com.android.sdklib.repository.descriptors.PkgDesc; + +import java.io.File; +import java.util.Properties; + +/** + * Local add-on system-image package, for a given addon's {@link AndroidVersion} and given ABI. + * The system-image tag is the add-on name. + * The package itself has a major revision. + * There should be only one for a given android platform version & ABI. + */ +public class LocalAddonSysImgPkgInfo extends LocalPkgInfo { + + + @NonNull + private final IPkgDesc mDesc; + + public LocalAddonSysImgPkgInfo(@NonNull LocalSdk localSdk, + @NonNull File localDir, + @NonNull Properties sourceProps, + @NonNull AndroidVersion version, + @Nullable IdDisplay addonVendor, + @Nullable IdDisplay addonName, + @NonNull String abi, + @NonNull MajorRevision revision) { + super(localSdk, localDir, sourceProps); + mDesc = PkgDesc.Builder.newAddonSysImg(version, addonVendor, addonName, abi, revision) + .create(); + } + + @NonNull + @Override + public IPkgDesc getDesc() { + return mDesc; + } + + public ISystemImage getSystemImage() { + return LocalSysImgPkgInfo.getSystemImage(mDesc, getLocalDir(), getLocalSdk().getFileOp()); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalBuildToolPkgInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalBuildToolPkgInfo.java new file mode 100644 index 00000000..0251cc3c --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalBuildToolPkgInfo.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2013 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.sdklib.repository.local; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdklib.BuildToolInfo; +import com.android.sdklib.repository.FullRevision; +import com.android.sdklib.repository.descriptors.IPkgDesc; +import com.android.sdklib.repository.descriptors.PkgDesc; + +import java.io.File; +import java.util.Properties; + +public class LocalBuildToolPkgInfo extends LocalPkgInfo { + + + @Nullable + private final BuildToolInfo mBuildToolInfo; + @NonNull + private final IPkgDesc mDesc; + + public LocalBuildToolPkgInfo(@NonNull LocalSdk localSdk, + @NonNull File localDir, + @NonNull Properties sourceProps, + @NonNull FullRevision revision, + @Nullable BuildToolInfo btInfo) { + super(localSdk, localDir, sourceProps); + mDesc = PkgDesc.Builder.newBuildTool(revision).create(); + mBuildToolInfo = btInfo; + } + + @NonNull + @Override + public IPkgDesc getDesc() { + return mDesc; + } + + @Nullable + public BuildToolInfo getBuildToolInfo() { + return mBuildToolInfo; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalDirInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalDirInfo.java new file mode 100644 index 00000000..4414fc6c --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalDirInfo.java @@ -0,0 +1,275 @@ +/* + * Copyright (C) 2014 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.sdklib.repository.local; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.sdklib.io.LegacyFileOp; +import com.android.sdklib.io.IFileOp; + +import java.io.File; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Map; +import java.util.zip.Adler32; + +/** + * Keeps information on a visited directory to quickly determine if it + * has changed later. A directory has changed if its timestamp has been + * modified, or if an underlying source.properties file has changed in + * timestamp or checksum. + *

+ * Note that depending on the filesystem & OS, the content of the files in + * a directory can change without the directory's last-modified property + * changing. To have a consistent behavior between OSes, we compute a quick + * checksum using all the files & directories modified timestamps. + * The content of files is not included though, except for the checksum on + * the source.property file since this one is the most important for the SDK. + *

+ * The {@link #hashCode()} and {@link #equals(Object)} methods directly + * defer to the underlying File object. This allows the DirInfo to be placed + * into a map and still call {@link Map#containsKey(Object)} with a File + * object to check whether there's a corresponding DirInfo in the map. + */ +class LocalDirInfo { + @NonNull + private final IFileOp mFileOp; + @NonNull + private final File mDir; + private final long mDirModifiedTS; + private final long mDirChecksum; + private final long mPropsModifiedTS; + private final long mPropsChecksum; + + /** + * Creates a new immutable {@link LocalDirInfo}. + * + * @param LegacyFileOp The {@link LegacyFileOp} to use for all file-based interactions. + * @param dir The platform/addon directory of the target. It should be a directory. + */ + public LocalDirInfo(@NonNull IFileOp LegacyFileOp, @NonNull File dir) { + mFileOp = LegacyFileOp; + mDir = dir; + mDirModifiedTS = mFileOp.lastModified(dir); + + // Capture some info about the source.properties file if it exists. + // We use propsModifiedTS == 0 to mean there is no props file. + long propsChecksum = 0; + long propsModifiedTS = 0; + File props = new File(dir, SdkConstants.FN_SOURCE_PROP); + if (mFileOp.isFile(props)) { + propsModifiedTS = mFileOp.lastModified(props); + propsChecksum = getFileChecksum(props); + } + mPropsModifiedTS = propsModifiedTS; + mPropsChecksum = propsChecksum; + mDirChecksum = getDirChecksum(mDir); + } + + /** + * Checks whether the directory/source.properties attributes have changed. + * + * @return True if the directory modified timestamp or + * its source.property files have changed. + */ + public boolean hasChanged() { + // Does platform directory still exist? + if (!mFileOp.isDirectory(mDir)) { + return true; + } + // Has platform directory modified-timestamp changed? + if (mDirModifiedTS != mFileOp.lastModified(mDir)) { + return true; + } + + File props = new File(mDir, SdkConstants.FN_SOURCE_PROP); + + // The directory did not have a props file if target was null or + // if mPropsModifiedTS is 0. + boolean hadProps = mPropsModifiedTS != 0; + + // Was there a props file and it vanished, or there wasn't and there's one now? + if (hadProps != mFileOp.isFile(props)) { + return true; + } + + if (hadProps) { + // Has source.props file modified-timestamp changed? + if (mPropsModifiedTS != mFileOp.lastModified(props)) { + return true; + } + // Had the content of source.props changed? + if (mPropsChecksum != getFileChecksum(props)) { + return true; + } + } + + // Has the deep directory checksum changed? + if (mDirChecksum != getDirChecksum(mDir)) { + return true; + } + + return false; + } + + /** + * Computes an adler32 checksum (source.props are small files, so this + * should be OK with an acceptable collision rate.) + */ + private long getFileChecksum(@NonNull File file) { + InputStream fis = null; + try { + fis = mFileOp.newFileInputStream(file); + Adler32 a = new Adler32(); + byte[] buf = new byte[1024]; + int n; + while ((n = fis.read(buf)) > 0) { + a.update(buf, 0, n); + } + return a.getValue(); + } catch (Exception ignore) { + } finally { + try { + if (fis != null) { + fis.close(); + } + } catch(Exception ignore) {} + } + return 0; + } + + /** + * Computes a checksum using the last-modified attributes of all + * the files and first-leveldirectories in this root directory. + *

+ * Heuristic: the SDK Manager updates package by replacing whole directories + * so we don't need to do a recursive deep-first checksum of all files. Only + * the top-level of the package directory should be sufficient to detect + * SDK updates. + */ + private long getDirChecksum(@NonNull File dir) { + long checksum = mFileOp.lastModified(dir); + + // Get the file & directory list sorted by case-insensitive name + // to make the checksum more consistent. + File[] files = mFileOp.listFiles(dir); + Arrays.sort(files, new Comparator() { + @Override + public int compare(File o1, File o2) { + return o1.getName().compareToIgnoreCase(o2.getName()); + } + }); + for (File file : files) { + checksum = 31 * checksum | mFileOp.lastModified(file); + } + return checksum; + } + + /** Returns a visual representation of this object for debugging. */ + @Override + public String toString() { + String s = String.format(""; //$NON-NLS-1$ + } + + /** + * Returns the hashCode of the underlying File object. + *

+ * When a {@link LocalDirInfo} is placed in a map, what matters is to use the underlying + * File object as the key so {@link #hashCode()} and {@link #equals(Object)} both + * return the properties of the underlying File object. + * + * @see File#hashCode() + */ + @Override + public int hashCode() { + return mDir.hashCode(); + } + + /** + * Checks equality of the underlying File object. + *

+ * When a {@link LocalDirInfo} is placed in a map, what matters is to use the underlying + * File object as the key so {@link #hashCode()} and {@link #equals(Object)} both + * return the properties of the underlying File object. + * + * @see File#equals(Object) + */ + @Override + public boolean equals(Object obj) { + if (obj instanceof File) { + return mDir.equals(obj); + } else if (obj instanceof LocalDirInfo) { + return mDir.equals(((LocalDirInfo) obj).mDir); + } else if (obj instanceof MapComparator) { + return mDir.equals(((MapComparator) obj).mDir); + } + return false; + } + + /** + * Helper for Map.contains() to make sure we're comparing the inner directory File + * object and not the outer wrapper itself. + */ + public static class MapComparator { + private final File mDir; + + public MapComparator(File dir) { + mDir = dir; + } + + /** + * Returns the hashCode of the underlying File object. + *

+ * When a {@link LocalDirInfo} is placed in a map, what matters is to use the underlying + * File object as the key so {@link #hashCode()} and {@link #equals(Object)} both + * return the properties of the underlying File object. + * + * @see File#hashCode() + */ + @Override + public int hashCode() { + return mDir.hashCode(); + } + + /** + * Checks equality of the underlying File object. + *

+ * When a {@link LocalDirInfo} is placed in a map, what matters is to use the underlying + * File object as the key so {@link #hashCode()} and {@link #equals(Object)} both + * return the properties of the underlying File object. + * + * @see File#equals(Object) + */ + @Override + public boolean equals(Object obj) { + if (obj instanceof File) { + return mDir.equals(obj); + } else if (obj instanceof LocalDirInfo) { + return mDir.equals(((LocalDirInfo) obj).mDir); + } else if (obj instanceof MapComparator) { + return mDir.equals(((MapComparator) obj).mDir); + } + return false; + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalDocPkgInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalDocPkgInfo.java new file mode 100644 index 00000000..004f4503 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalDocPkgInfo.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2013 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.sdklib.repository.local; + +import com.android.annotations.NonNull; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.repository.MajorRevision; +import com.android.sdklib.repository.descriptors.IPkgDesc; +import com.android.sdklib.repository.descriptors.PkgDesc; + +import java.io.File; +import java.util.Properties; + +public class LocalDocPkgInfo extends LocalPkgInfo { + + @NonNull + private final IPkgDesc mDesc; + + public LocalDocPkgInfo(@NonNull LocalSdk localSdk, + @NonNull File localDir, + @NonNull Properties sourceProps, + @NonNull AndroidVersion version, + @NonNull MajorRevision revision) { + super(localSdk, localDir, sourceProps); + mDesc = PkgDesc.Builder.newDoc(version, revision).create(); + } + + @NonNull + @Override + public IPkgDesc getDesc() { + return mDesc; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalExtraPkgInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalExtraPkgInfo.java new file mode 100644 index 00000000..5ec811bd --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalExtraPkgInfo.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2013 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.sdklib.repository.local; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdklib.repository.NoPreviewRevision; +import com.android.sdklib.repository.descriptors.IPkgDesc; +import com.android.sdklib.repository.descriptors.IPkgDescExtra; +import com.android.sdklib.repository.descriptors.IdDisplay; +import com.android.sdklib.repository.descriptors.PkgDesc; + +import java.io.File; +import java.util.Properties; + +public class LocalExtraPkgInfo extends LocalPkgInfo { + + @NonNull + private final IPkgDescExtra mDesc; + + public LocalExtraPkgInfo(@NonNull LocalSdk localSdk, + @NonNull File localDir, + @NonNull Properties sourceProps, + @NonNull IdDisplay vendor, + @NonNull String path, + @Nullable String displayName, + @NonNull String[] oldPaths, + @NonNull NoPreviewRevision revision) { + super(localSdk, localDir, sourceProps); + mDesc = (IPkgDescExtra) PkgDesc.Builder.newExtra( + vendor, + path, + displayName, + oldPaths, + revision).create(); + } + + @NonNull + @Override + public IPkgDesc getDesc() { + return mDesc; + } + + @NonNull + public String[] getOldPaths() { + return mDesc.getOldPaths(); + } + + // --- helpers --- + + /** + * Used to produce a suitable name-display based on the extra's path + * and vendor display string in addon-3 schemas. + * + * @param vendor The vendor id of the extra. + * @param extraPath The non-null path of the extra. + * @return A non-null display name based on the extra's path id. + */ + public static String getPrettyName(@Nullable IdDisplay vendor, @NonNull String extraPath) { + String name = extraPath; + + // In the past, we used to save the extras in a folder vendor-path, + // and that "vendor" would end up in the path when we reload the extra from + // disk. Detect this and compensate. + String disp = vendor == null ? null : vendor.getDisplay(); + if (disp != null && !disp.isEmpty()) { + if (name.startsWith(disp + "-")) { //$NON-NLS-1$ + name = name.substring(disp.length() + 1); + } + } + + // Uniformize all spaces in the name + if (name != null) { + name = name.replaceAll("[ _\t\f-]+", " ").trim(); //$NON-NLS-1$ //$NON-NLS-2$ + } + if (name == null || name.isEmpty()) { + name = "Unknown Extra"; + } + + if (disp != null && !disp.isEmpty()) { + name = disp + " " + name; //$NON-NLS-1$ + name = name.replaceAll("[ _\t\f-]+", " ").trim(); //$NON-NLS-1$ //$NON-NLS-2$ + } + + // Look at all lower case characters in range [1..n-1] and replace them by an upper + // case if they are preceded by a space. Also upper cases the first character of the + // string. + boolean changed = false; + char[] chars = name.toCharArray(); + for (int n = chars.length - 1, i = 0; i < n; i++) { + if (Character.isLowerCase(chars[i]) && (i == 0 || chars[i - 1] == ' ')) { + chars[i] = Character.toUpperCase(chars[i]); + changed = true; + } + } + if (changed) { + name = new String(chars); + } + + // Special case: reformat a few typical acronyms. + name = name.replaceAll(" Usb ", " USB "); //$NON-NLS-1$ + name = name.replaceAll(" Api ", " API "); //$NON-NLS-1$ + + return name; + } +} + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalNdkPkgInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalNdkPkgInfo.java new file mode 100644 index 00000000..88583005 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalNdkPkgInfo.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2015 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.sdklib.repository.local; + +import com.android.annotations.NonNull; +import com.android.sdklib.repository.FullRevision; +import com.android.sdklib.repository.descriptors.IPkgDesc; +import com.android.sdklib.repository.descriptors.PkgDesc; + +import java.io.File; +import java.util.Properties; + +/** + * Local package representing the Android NDK + */ +public class LocalNdkPkgInfo extends LocalPkgInfo { + @NonNull + private final IPkgDesc mDesc; + + protected LocalNdkPkgInfo(@NonNull LocalSdk localSdk, + @NonNull File localDir, + @NonNull Properties sourceProps, + @NonNull FullRevision revision) { + super(localSdk, localDir, sourceProps); + mDesc = PkgDesc.Builder.newNdk(revision).setDescriptionShort("Android NDK").setListDisplay("Android NDK").create(); + } + + @NonNull + @Override + public IPkgDesc getDesc() { + return mDesc; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalPkgInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalPkgInfo.java new file mode 100644 index 00000000..4f8982e7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalPkgInfo.java @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2013 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.sdklib.repository.local; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdklib.repository.IDescription; +import com.android.sdklib.repository.IListDescription; +import com.android.sdklib.io.LegacyFileOp; +import com.android.sdklib.repository.descriptors.IPkgDesc; + +import java.io.File; +import java.util.Properties; + +/** + * Information about a locally installed package. + *

+ * Local package information is retrieved via the {@link LocalSdk} object. + * Clients should not need to create instances of {@link LocalPkgInfo} directly. + * Instead please use the {@link LocalSdk} methods to parse and retrieve packages. + *

+ */ +public abstract class LocalPkgInfo + implements IDescription, IListDescription, Comparable { + + private final LocalSdk mLocalSdk; + private final File mLocalDir; + private final Properties mSourceProperties; + + private String mLoadError; + + protected LocalPkgInfo(@NonNull LocalSdk localSdk, @NonNull File localDir, @NonNull Properties sourceProps) { + mLocalSdk = localSdk; + mLocalDir = localDir; + mSourceProperties = sourceProps; + } + + //---- Attributes ---- + + @NonNull + public LocalSdk getLocalSdk() { + return mLocalSdk; + } + + @NonNull + public File getLocalDir() { + return mLocalDir; + } + + @NonNull + public Properties getSourceProperties() { + return mSourceProperties; + } + + @Nullable + public String getLoadError() { + return mLoadError; + } + + // ---- + + /** + * Returns the {@link IPkgDesc} describing this package. + */ + @NonNull + public abstract IPkgDesc getDesc(); + + + //---- Ordering ---- + + /** + * Comparison is solely done based on the {@link IPkgDesc}. + *

+ * Other local attributes (local directory, source properties) + * are not used in the comparison. Consequently {@link #compareTo(LocalPkgInfo)} + * does not match {@link #equals(Object)} and the {@link #hashCode()} properties. + */ + @Override + public int compareTo(@NonNull LocalPkgInfo o) { + return getDesc().compareTo(o.getDesc()); + } + + /** + * String representation for debugging purposes. + */ + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append('<').append(this.getClass().getSimpleName()).append(' '); + builder.append(getDesc().toString()); + builder.append('>'); + return builder.toString(); + } + + /** + * Computes a hash code specific to this instance based on the underlying + * {@link IPkgDesc} but also specific local properties such a local directory, + * and actual source properties. + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((getDesc() == null) ? 0 : getDesc().hashCode()); + result = prime * result + ((mLocalDir == null) ? 0 : mLocalDir.hashCode()); + result = prime * result + ((mSourceProperties == null) ? 0 : mSourceProperties.hashCode()); + return result; + } + + /** + * Computes object equality to this instance based on the underlying + * {@link IPkgDesc} but also specific local properties such a local directory, + * update available and actual source properties. This is different from + * the behavior of {@link #compareTo(LocalPkgInfo)} which only uses the + * {@link IPkgDesc} for ordering. + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof LocalPkgInfo)) { + return false; + } + LocalPkgInfo other = (LocalPkgInfo)obj; + + if (!getDesc().equals(other.getDesc())) { + return false; + } + if (mLocalDir == null) { + if (other.mLocalDir != null) { + return false; + } + } + else if (!mLocalDir.equals(other.mLocalDir)) { + return false; + } + if (mSourceProperties == null) { + if (other.mSourceProperties != null) { + return false; + } + } + else if (!mSourceProperties.equals(other.mSourceProperties)) { + return false; + } + return true; + } + + + //---- Package Management ---- + + /** + * A "broken" package is installed but is not fully operational. + *

+ * For example an addon that lacks its underlying platform or a tool package + * that lacks some of its binaries or essentially files. + *

+ * Operational code should generally ignore broken packages. + * Only the SDK Updater cares about displaying them so that they can be fixed. + */ + public boolean hasLoadError() { + return mLoadError != null; + } + + void appendLoadError(@NonNull String format, Object... params) { + String loadError = String.format(format, params); + if (mLoadError == null) { + mLoadError = loadError; + } + else { + mLoadError = mLoadError + '\n' + loadError; + } + } + + @NonNull + @Override + public String getListDescription() { + return getDesc().getListDescription(); + } + + @Override + public String getShortDescription() { + // TODO revisit to differentiate from list-description depending + // on how we'll use it in the sdkman UI. + return getListDescription(); + } + + @Override + public String getLongDescription() { + StringBuilder sb = new StringBuilder(); + IPkgDesc desc = getDesc(); + + sb.append(desc.getListDescription()).append('\n'); + + if (desc.hasVendor()) { + assert desc.getVendor() != null; + sb.append("By ").append(desc.getVendor().getDisplay()).append('\n'); + } + + if (desc.hasMinPlatformToolsRev()) { + assert desc.getMinPlatformToolsRev() != null; + sb.append("Requires Platform-Tools revision ").append(desc.getMinPlatformToolsRev().toShortString()).append('\n'); + } + + if (desc.hasMinToolsRev()) { + assert desc.getMinToolsRev() != null; + sb.append("Requires Tools revision ").append(desc.getMinToolsRev().toShortString()).append('\n'); + } + + sb.append("Location: ").append(mLocalDir.getAbsolutePath()); + + return sb.toString(); + } + + /** + * Deletes the files in the SDK corresponding to this package. + */ + public void delete() { + new LegacyFileOp().deleteFileOrFolder(getLocalDir()); + } + +} + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalPlatformPkgInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalPlatformPkgInfo.java new file mode 100644 index 00000000..1ccb2be7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalPlatformPkgInfo.java @@ -0,0 +1,453 @@ +/* + * Copyright (C) 2013 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.sdklib.repository.local; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.annotations.VisibleForTesting; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.IAndroidTarget.OptionalLibrary; +import com.android.sdklib.ISystemImage; +import com.android.sdklib.ISystemImage.LocationType; +import com.android.sdklib.SdkManager.LayoutlibVersion; +import com.android.sdklib.SystemImage; +import com.android.sdklib.internal.androidTarget.OptionalLibraryImpl; +import com.android.sdklib.internal.androidTarget.PlatformTarget; +import com.android.sdklib.internal.project.ProjectProperties; +import com.android.sdklib.io.LegacyFileOp; +import com.android.sdklib.io.IFileOp; +import com.android.sdklib.repository.FullRevision; +import com.android.sdklib.repository.MajorRevision; +import com.android.sdklib.repository.PkgProps; +import com.android.sdklib.repository.descriptors.IPkgDesc; +import com.android.sdklib.repository.descriptors.IdDisplay; +import com.android.sdklib.repository.descriptors.PkgDesc; +import com.android.sdklib.repository.descriptors.PkgType; +import com.google.common.base.Charsets; +import com.google.common.collect.Lists; +import com.google.common.collect.SetMultimap; +import com.google.common.collect.TreeMultimap; +import com.google.common.io.Files; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +import java.io.File; +import java.io.FileNotFoundException; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.TreeSet; + +@SuppressWarnings("ConstantConditions") +public class LocalPlatformPkgInfo extends LocalPkgInfo { + + public static final String PROP_VERSION_SDK = "ro.build.version.sdk"; //$NON-NLS-1$ + public static final String PROP_VERSION_CODENAME = "ro.build.version.codename"; //$NON-NLS-1$ + public static final String PROP_VERSION_RELEASE = "ro.build.version.release"; //$NON-NLS-1$ + + @NonNull + private final IPkgDesc mDesc; + + /** Android target, lazyly loaded from #getAndroidTarget */ + private IAndroidTarget mTarget; + private boolean mLoaded; + + public LocalPlatformPkgInfo(@NonNull LocalSdk localSdk, + @NonNull File localDir, + @NonNull Properties sourceProps, + @NonNull AndroidVersion version, + @NonNull MajorRevision revision, + @NonNull FullRevision minToolsRev) { + super(localSdk, localDir, sourceProps); + mDesc = PkgDesc.Builder.newPlatform(version, revision, minToolsRev).create(); + } + + @NonNull + @Override + public IPkgDesc getDesc() { + return mDesc; + } + + /** The "path" of a Platform is its Target Hash. */ + @NonNull + public String getTargetHash() { + return getDesc().getPath(); + } + + @Nullable + public IAndroidTarget getAndroidTarget() { + if (!mLoaded) { + mTarget = createAndroidTarget(); + mLoaded = true; + } + return mTarget; + } + + public boolean isLoaded() { + return mLoaded; + } + + //----- + + /** + * Creates the PlatformTarget. Invoked by {@link #getAndroidTarget()}. + */ + @SuppressWarnings("ConstantConditions") + @Nullable + protected IAndroidTarget createAndroidTarget() { + LocalSdk sdk = getLocalSdk(); + IFileOp fileOp = sdk.getFileOp(); + File platformFolder = getLocalDir(); + File buildProp = new File(platformFolder, SdkConstants.FN_BUILD_PROP); + File sourcePropFile = new File(platformFolder, SdkConstants.FN_SOURCE_PROP); + + if (!fileOp.isFile(buildProp) || !fileOp.isFile(sourcePropFile)) { + appendLoadError("Ignoring platform '%1$s': %2$s is missing.", //$NON-NLS-1$ + platformFolder.getName(), + SdkConstants.FN_BUILD_PROP); + return null; + } + + Map platformProp = new HashMap(); + + // add all the property files + Map map = null; + + try { + map = ProjectProperties.parsePropertyStream( + fileOp.newFileInputStream(buildProp), + buildProp.getPath(), + null /*log*/); + if (map != null) { + platformProp.putAll(map); + } + } catch (FileNotFoundException ignore) {} + + try { + map = ProjectProperties.parsePropertyStream( + fileOp.newFileInputStream(sourcePropFile), + sourcePropFile.getPath(), + null /*log*/); + if (map != null) { + platformProp.putAll(map); + } + } catch (FileNotFoundException ignore) {} + + File sdkPropFile = new File(platformFolder, SdkConstants.FN_SDK_PROP); + if (fileOp.isFile(sdkPropFile)) { // obsolete platforms don't have this. + try { + map = ProjectProperties.parsePropertyStream( + fileOp.newFileInputStream(sdkPropFile), + sdkPropFile.getPath(), + null /*log*/); + if (map != null) { + platformProp.putAll(map); + } + } catch (FileNotFoundException ignore) {} + } + + // look for some specific values in the map. + + // api level + int apiNumber; + String stringValue = platformProp.get(PROP_VERSION_SDK); + if (stringValue == null) { + appendLoadError("Ignoring platform '%1$s': %2$s is missing from '%3$s'", + platformFolder.getName(), PROP_VERSION_SDK, + SdkConstants.FN_BUILD_PROP); + return null; + } else { + try { + apiNumber = Integer.parseInt(stringValue); + } catch (NumberFormatException e) { + // looks like apiNumber does not parse to a number. + // Ignore this platform. + appendLoadError( + "Ignoring platform '%1$s': %2$s is not a valid number in %3$s.", + platformFolder.getName(), PROP_VERSION_SDK, + SdkConstants.FN_BUILD_PROP); + return null; + } + } + + // Codename must be either null or a platform codename. + // REL means it's a release version and therefore the codename should be null. + AndroidVersion apiVersion = + new AndroidVersion(apiNumber, platformProp.get(PROP_VERSION_CODENAME)); + + // version string + String apiName = platformProp.get(PkgProps.PLATFORM_VERSION); + if (apiName == null) { + apiName = platformProp.get(PROP_VERSION_RELEASE); + } + if (apiName == null) { + appendLoadError( + "Ignoring platform '%1$s': %2$s is missing from '%3$s'", + platformFolder.getName(), PROP_VERSION_RELEASE, + SdkConstants.FN_BUILD_PROP); + return null; + } + + // platform rev number & layoutlib version are extracted from the source.properties + // saved by the SDK Manager when installing the package. + + int revision = 1; + LayoutlibVersion layoutlibVersion = null; + try { + revision = Integer.parseInt(platformProp.get(PkgProps.PKG_REVISION)); + } catch (NumberFormatException e) { + // do nothing, we'll keep the default value of 1. + } + + try { + String propApi = platformProp.get(PkgProps.LAYOUTLIB_API); + String propRev = platformProp.get(PkgProps.LAYOUTLIB_REV); + int llApi = propApi == null ? LayoutlibVersion.NOT_SPECIFIED : + Integer.parseInt(propApi); + int llRev = propRev == null ? LayoutlibVersion.NOT_SPECIFIED : + Integer.parseInt(propRev); + if (llApi > LayoutlibVersion.NOT_SPECIFIED && + llRev >= LayoutlibVersion.NOT_SPECIFIED) { + layoutlibVersion = new LayoutlibVersion(llApi, llRev); + } + } catch (NumberFormatException e) { + // do nothing, we'll ignore the layoutlib version if it's invalid + } + + // api number and name look valid, perform a few more checks + String err = checkPlatformContent(fileOp, platformFolder); + if (err != null) { + appendLoadError("%s", err); //$NLN-NLS-1$ + return null; + } + + ISystemImage[] systemImages = getPlatformSystemImages(fileOp, platformFolder, apiVersion); + + // create the target. + PlatformTarget pt = new PlatformTarget( + sdk.getLocation().getPath(), + platformFolder.getAbsolutePath(), + apiVersion, + apiName, + revision, + layoutlibVersion, + systemImages, + platformProp, + getOptionalLibraries(platformFolder), + sdk.getLatestBuildTool()); + + // add the skins from the platform. Make a copy to not modify the original collection. + List skins = new ArrayList(PackageParserUtils.parseSkinFolder(pt.getFile(IAndroidTarget.SKINS), fileOp)); + + // add the system-image specific skins, if any. + for (ISystemImage systemImage : systemImages) { + skins.addAll(Arrays.asList(systemImage.getSkins())); + } + + pt.setSkins(skins.toArray(new File[skins.size()])); + + // add path to the non-legacy samples package if it exists + LocalPkgInfo samples = sdk.getPkgInfo(PkgType.PKG_SAMPLE, getDesc().getAndroidVersion()); + if (samples != null) { + pt.setSamplesPath(samples.getLocalDir().getAbsolutePath()); + } + + // add path to the non-legacy sources package if it exists + LocalPkgInfo sources = sdk.getPkgInfo(PkgType.PKG_SOURCE, getDesc().getAndroidVersion()); + if (sources != null) { + pt.setSourcesPath(sources.getLocalDir().getAbsolutePath()); + } + + return pt; + } + + /** + * Get all the system images supported by a platform target. + * For a platform, we first look in the new sdk/system-images folders then we + * look for sub-folders in the platform/images directory and/or the one legacy + * folder. + * If any given API appears twice or more, the first occurrence wins. + * + * @param fileOp File operation wrapper. + * @param platformDir Root of the platform target being loaded. + * @param apiVersion API level + codename of platform being loaded. + * @return an array of ISystemImage containing all the system images for the target. + * The list can be empty but not null. + */ + @NonNull + private ISystemImage[] getPlatformSystemImages(IFileOp fileOp, + File platformDir, + AndroidVersion apiVersion) { + Set found = new TreeSet(); + SetMultimap tagToAbiFound = TreeMultimap.create(); + + + // Look in the SDK/system-image/platform-n/tag/abi folders. + // Look in the SDK/system-image/platform-n/abi folders. + // If we find multiple occurrences of the same platform/abi, the first one read wins. + + LocalPkgInfo[] sysImgInfos = getLocalSdk().getPkgsInfos(PkgType.PKG_SYS_IMAGE); + for (LocalPkgInfo pkg : sysImgInfos) { + IPkgDesc d = pkg.getDesc(); + if (pkg instanceof LocalSysImgPkgInfo && + !d.hasVendor() && + apiVersion.equals(d.getAndroidVersion())) { + IdDisplay tag = d.getTag(); + String abi = d.getPath(); + if (tag != null && abi != null && !tagToAbiFound.containsEntry(tag, abi)) { + found.add(((LocalSysImgPkgInfo)pkg).getSystemImage()); + tagToAbiFound.put(tag, abi); + } + } + } + + // Look in either the platform/images/abi or the legacy folder + File imgDir = new File(platformDir, SdkConstants.OS_IMAGES_FOLDER); + File[] files = fileOp.listFiles(imgDir); + boolean useLegacy = true; + boolean hasImgFiles = false; + final IdDisplay defaultTag = SystemImage.DEFAULT_TAG; + + // Look for sub-directories + for (File file : files) { + if (fileOp.isDirectory(file)) { + useLegacy = false; + String abi = file.getName(); + if (!tagToAbiFound.containsEntry(defaultTag, abi)) { + found.add(new SystemImage( + file, + LocationType.IN_IMAGES_SUBFOLDER, + defaultTag, + abi, + LegacyFileOp.EMPTY_FILE_ARRAY)); + tagToAbiFound.put(defaultTag, abi); + } + } else if (!hasImgFiles && fileOp.isFile(file)) { + if (file.getName().endsWith(".img")) { //$NON-NLS-1$ + hasImgFiles = true; + } + } + } + + if (useLegacy && + hasImgFiles && + fileOp.isDirectory(imgDir) && + !tagToAbiFound.containsEntry(defaultTag, SdkConstants.ABI_ARMEABI)) { + // We found no sub-folder system images but it looks like the top directory + // has some img files in it. It must be a legacy ARM EABI system image folder. + found.add(new SystemImage( + imgDir, + LocationType.IN_LEGACY_FOLDER, + defaultTag, + SdkConstants.ABI_ARMEABI, + LegacyFileOp.EMPTY_FILE_ARRAY)); + } + + return found.toArray(new ISystemImage[found.size()]); + } + + private List getOptionalLibraries(@NonNull File platformDir) { + File optionalDir = new File(platformDir, "optional"); + if (!optionalDir.isDirectory()) { + return Collections.emptyList(); + } + + File optionalJson = new File(optionalDir, "optional.json"); + if (!optionalJson.isFile()) { + return Collections.emptyList(); + } + + return getLibsFromJson(optionalJson); + } + + public static class Library { + String name; + String jar; + boolean manifest; + } + + + @VisibleForTesting + static List getLibsFromJson(@NonNull File jsonFile) { + + Gson gson = new Gson(); + + try { + Type collectionType = new TypeToken>() { + }.getType(); + Collection libs = gson + .fromJson(Files.newReader(jsonFile, Charsets.UTF_8), collectionType); + + // convert into the right format. + List optionalLibraries = Lists.newArrayListWithCapacity(libs.size()); + + File rootFolder = jsonFile.getParentFile(); + for (Library lib : libs) { + optionalLibraries.add(new OptionalLibraryImpl( + lib.name, + new File(rootFolder, lib.jar), + lib.name, + lib.manifest)); + } + + return optionalLibraries; + } catch (FileNotFoundException e) { + // shouldn't happen since we've checked the file is here, but can happen in + // some cases (too many files open). + return Collections.emptyList(); + } + } + + /** List of items in the platform to check when parsing it. These paths are relative to the + * platform root folder. */ + private static final String[] sPlatformContentList = new String[] { + SdkConstants.FN_FRAMEWORK_LIBRARY, + SdkConstants.FN_FRAMEWORK_AIDL, + }; + + /** + * Checks the given platform has all the required files, and returns true if they are all + * present. + *

This checks the presence of the following files: android.jar, framework.aidl, aapt(.exe), + * aidl(.exe), dx(.bat), and dx.jar + * + * @param fileOp File operation wrapper. + * @param platform The folder containing the platform. + * @return An error description if platform is rejected; null if no error is detected. + */ + @NonNull + private static String checkPlatformContent(IFileOp fileOp, @NonNull File platform) { + for (String relativePath : sPlatformContentList) { + File f = new File(platform, relativePath); + if (!fileOp.exists(f)) { + return String.format( + "Ignoring platform '%1$s': %2$s is missing.", //$NON-NLS-1$ + platform.getName(), relativePath); + } + } + return null; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalPlatformToolPkgInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalPlatformToolPkgInfo.java new file mode 100644 index 00000000..281c64e4 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalPlatformToolPkgInfo.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2013 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.sdklib.repository.local; + +import com.android.annotations.NonNull; +import com.android.sdklib.repository.FullRevision; +import com.android.sdklib.repository.descriptors.IPkgDesc; +import com.android.sdklib.repository.descriptors.PkgDesc; + +import java.io.File; +import java.util.Properties; + +public class LocalPlatformToolPkgInfo extends LocalPkgInfo { + + @NonNull + private final IPkgDesc mDesc; + + public LocalPlatformToolPkgInfo(@NonNull LocalSdk localSdk, + @NonNull File localDir, + @NonNull Properties sourceProps, + @NonNull FullRevision revision) { + super(localSdk, localDir, sourceProps); + mDesc = PkgDesc.Builder.newPlatformTool(revision).create(); + } + + @NonNull + @Override + public IPkgDesc getDesc() { + return mDesc; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalSamplePkgInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalSamplePkgInfo.java new file mode 100644 index 00000000..d8159d61 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalSamplePkgInfo.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2013 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.sdklib.repository.local; + +import com.android.annotations.NonNull; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.repository.FullRevision; +import com.android.sdklib.repository.MajorRevision; +import com.android.sdklib.repository.descriptors.IPkgDesc; +import com.android.sdklib.repository.descriptors.PkgDesc; + +import java.io.File; +import java.util.Properties; + +/** + * Local sample package, for a given platform's {@link AndroidVersion}. + * The package itself has a major revision. + * There should be only one for a given android platform version. + */ +public class LocalSamplePkgInfo extends LocalPkgInfo { + + @NonNull + private final IPkgDesc mDesc; + + public LocalSamplePkgInfo(@NonNull LocalSdk localSdk, + @NonNull File localDir, + @NonNull Properties sourceProps, + @NonNull AndroidVersion version, + @NonNull MajorRevision revision, + @NonNull FullRevision minToolsRev) { + super(localSdk, localDir, sourceProps); + mDesc = PkgDesc.Builder.newSample(version, revision, minToolsRev).create(); + } + + @NonNull + @Override + public IPkgDesc getDesc() { + return mDesc; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalSdk.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalSdk.java new file mode 100644 index 00000000..72597cd3 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalSdk.java @@ -0,0 +1,1239 @@ +/* + * Copyright (C) 2013 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.sdklib.repository.local; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.annotations.VisibleForTesting; +import com.android.annotations.VisibleForTesting.Visibility; +import com.android.annotations.concurrency.GuardedBy; +import com.android.sdklib.*; +import com.android.sdklib.AndroidVersion.AndroidVersionException; +import com.android.sdklib.internal.androidTarget.MissingTarget; +import com.android.sdklib.io.LegacyFileOp; +import com.android.sdklib.io.IFileOp; +import com.android.sdklib.repository.FullRevision; +import com.android.sdklib.repository.MajorRevision; +import com.android.sdklib.repository.NoPreviewRevision; +import com.android.sdklib.repository.PkgProps; +import com.android.sdklib.repository.descriptors.IPkgDesc; +import com.android.sdklib.repository.descriptors.IdDisplay; +import com.android.sdklib.repository.descriptors.PkgDescExtra; +import com.android.sdklib.repository.descriptors.PkgType; +import com.google.common.collect.*; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.*; + +/** + * This class keeps information on the current locally installed SDK. + * It tries to lazily load information as much as possible. + *

+ * Packages are accessed by their type and a main query attribute, depending on the + * package type. There are different versions of {@link #getPkgInfo} which depend on the + * query attribute. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
TypeQuery parameterGetter
ToolsUnique instance{@code getPkgInfo(PkgType.PKG_TOOLS)} => {@link LocalPkgInfo}
Platform-ToolsUnique instance{@code getPkgInfo(PkgType.PKG_PLATFORM_TOOLS)} => {@link LocalPkgInfo}
DocsUnique instance{@code getPkgInfo(PkgType.PKG_DOCS)} => {@link LocalPkgInfo}
Build-Tools{@link FullRevision}{@code getLatestBuildTool()} => {@link BuildToolInfo},
+ * or {@code getBuildTool(FullRevision)} => {@link BuildToolInfo},
+ * or {@code getPkgInfo(PkgType.PKG_BUILD_TOOLS, FullRevision)} => {@link LocalPkgInfo},
+ * or {@code getPkgsInfos(PkgType.PKG_BUILD_TOOLS)} => {@link LocalPkgInfo}[]
ExtrasString vendor/path{@code getExtra(String)} => {@link LocalExtraPkgInfo},
+ * or {@code getPkgInfo(PkgType.PKG_EXTRAS, String)} => {@link LocalPkgInfo},
+ * or {@code getPkgsInfos(PkgType.PKG_EXTRAS)} => {@link LocalPkgInfo}[]
Sources{@link AndroidVersion}{@code getPkgInfo(PkgType.PKG_SOURCES, AndroidVersion)} => {@link LocalPkgInfo},
+ * or {@code getPkgsInfos(PkgType.PKG_SOURCES)} => {@link LocalPkgInfo}[]
Samples{@link AndroidVersion}{@code getPkgInfo(PkgType.PKG_SAMPLES, AndroidVersion)} => {@link LocalPkgInfo},
+ * or {@code getPkgsInfos(PkgType.PKG_SAMPLES)} => {@link LocalPkgInfo}[]
Platforms{@link AndroidVersion}{@code getPkgInfo(PkgType.PKG_PLATFORMS, AndroidVersion)} => {@link LocalPkgInfo},
+ * or {@code getPkgInfo(PkgType.PKG_ADDONS, String)} => {@link LocalPkgInfo},
+ * or {@code getPkgsInfos(PkgType.PKG_PLATFORMS)} => {@link LocalPkgInfo}[],
+ * or {@code getTargetFromHashString(String)} => {@link IAndroidTarget}
Add-ons{@link AndroidVersion} x String vendor/path{@code getPkgInfo(PkgType.PKG_ADDONS, String)} => {@link LocalPkgInfo},
+ * or {@code getPkgsInfos(PkgType.PKG_ADDONS)} => {@link LocalPkgInfo}[],
+ * or {@code getTargetFromHashString(String)} => {@link IAndroidTarget}
System images{@link AndroidVersion} x {@link String} ABI{@code getPkgsInfos(PkgType.PKG_SYS_IMAGES)} => {@link LocalPkgInfo}[]
+ * + * Apps/libraries that use it are encouraged to keep an existing instance around + * (using a singleton or similar mechanism). + *

+ * Threading: All accessor methods are synchronized on the same internal lock so + * it's safe to call them from any thread, even concurrently.
+ * A method like {@code getPkgsInfos} returns a copy of its data array, which objects are + * not altered after creation, so its value is not influenced by the internal state after + * it returns. + *

+ * + * Implementation Background: + *

    + *
  • The sdk manager has a set of "Package" classes that cover both local + * and remote SDK operations. + *
  • Goal was to split it in 2 cleanly separated parts: {@link LocalSdk} parses sdk on disk, + * and a separate class wraps the downloaded manifest (this is now handled within Studio only) + *
  • The local SDK should be a singleton accessible somewhere, so there will be one in ADT + * (via the Sdk instance), one in Studio, and one in the command line tool.
    + * Right now there's a bit of mess with some classes creating a temp LocalSdkParser, + * some others using an SdkManager instance, and that needs to be sorted out. + *
  • As a transition, the SdkManager instance wraps a LocalSdk and uses this. Eventually the + * SdkManager.java class will go away (its name is totally misleading, for starters.) + *
  • The current LocalSdkParser stays as-is for compatibility purposes and the goal is also + * to totally remove it when the SdkManager class goes away. + *
+ * @version 2 of the {@code SdkManager} class, essentially. + */ +public class LocalSdk { + + /** Location of the SDK. Maybe null. Can be changed. */ + private File mSdkRoot; + /** File operation object. (Used for overriding in mock testing.) */ + private final IFileOp mFileOp; + /** List of package information loaded so far. Lazily populated. */ + @GuardedBy(value="mLocalPackages") + private final Multimap mLocalPackages = TreeMultimap.create(); + /** Directories already parsed into {@link #mLocalPackages}. */ + @GuardedBy(value="mLocalPackages") + private final Multimap mVisitedDirs = HashMultimap.create(); + /** A legacy build-tool for older platform-tools < 17. */ + private BuildToolInfo mLegacyBuildTools; + /** Cache of targets from local sdk. See {@link #getTargets()}. */ + @GuardedBy(value="mLocalPackages") + private List mCachedTargets = null; + private Set mCachedMissingTargets = null; + + /** + * Creates an initial LocalSdk instance with an unknown location. + */ + public LocalSdk() { + mFileOp = new LegacyFileOp(); + } + + /** + * Creates an initial LocalSdk instance for a known SDK location. + * + * @param sdkRoot The location of the SDK root folder. + */ + public LocalSdk(@NonNull File sdkRoot) { + this(); + setLocation(sdkRoot); + } + + /** + * Creates an initial LocalSdk instance with an unknown location. + * This is designed for unit tests to override the {@link LegacyFileOp} being used. + * + * @param LegacyFileOp The alternate {@link LegacyFileOp} to use for all file-based interactions. + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + public LocalSdk(@NonNull IFileOp LegacyFileOp) { + mFileOp = LegacyFileOp; + } + + /* + * Returns the current IFileOp being used. + */ + @NonNull + public IFileOp getFileOp() { + return mFileOp; + } + + /** + * Sets or changes the SDK root location. This also clears any cached information. + * + * @param sdkRoot The location of the SDK root folder. + */ + public void setLocation(@NonNull File sdkRoot) { + assert sdkRoot != null; + mSdkRoot = sdkRoot; + clearLocalPkg(PkgType.PKG_ALL); + } + + /** + * Location of the SDK. Maybe null. Can be changed. + * + * @return The location of the SDK. Null if not initialized yet. + */ + @Nullable + public File getLocation() { + return mSdkRoot; + } + + /** + * Location of the SDK. Maybe null. Can be changed. + * The getLocation() API replaces this function. + * @return The location of the SDK. Null if not initialized yet. + */ + @Deprecated + @Nullable + public String getPath() { + return mSdkRoot != null ? mSdkRoot.getPath() : null; + } + + /** + * Clear the tracked visited folders & the cached {@link LocalPkgInfo} for the + * given filter types. + * + * @param filters A set of PkgType constants or {@link PkgType#PKG_ALL} to clear everything. + */ + public void clearLocalPkg(@NonNull EnumSet filters) { + mLegacyBuildTools = null; + + synchronized (mLocalPackages) { + for (PkgType filter : filters) { + mVisitedDirs.removeAll(filter); + mLocalPackages.removeAll(filter); + } + + // Clear the targets if the platforms or addons are being cleared + if (filters.contains(PkgType.PKG_PLATFORM) || filters.contains(PkgType.PKG_ADDON)) { + mCachedMissingTargets = null; + mCachedTargets = null; + } + } + } + + /** + * Check the tracked visited folders to see if anything has changed for the + * requested filter types. + * This does not refresh or reload any package information. + * + * @param filters A set of PkgType constants or {@link PkgType#PKG_ALL} to clear everything. + */ + public boolean hasChanged(@NonNull EnumSet filters) { + for (PkgType filter : filters) { + Collection dirInfos; + synchronized (mLocalPackages) { + dirInfos = mVisitedDirs.get(filter); + for(LocalDirInfo dirInfo : dirInfos) { + if (dirInfo.hasChanged()) { + return true; + } + } + } + } + + return false; + } + + //--------- Generic querying --------- + + + /** + * Retrieves information on a package identified by an {@link IPkgDesc}. + * + * @param descriptor {@link IPkgDesc} describing a package. + * @return The first package found with the same descriptor or null. + */ + @Nullable + public LocalPkgInfo getPkgInfo(@NonNull IPkgDesc descriptor) { + + for (LocalPkgInfo pkg : getPkgsInfos(EnumSet.of(descriptor.getType()))) { + IPkgDesc d = pkg.getDesc(); + if (d.equals(descriptor)) { + return pkg; + } + } + + return null; + } + + /** + * Retrieves information on a package identified by an {@link AndroidVersion}. + * + * Note: don't use this for {@link PkgType#PKG_SYS_IMAGE} since there can be more than + * one ABI and this method only returns a single package per filter type. + * + * @param filter {@link PkgType#PKG_PLATFORM}, {@link PkgType#PKG_SAMPLE} + * or {@link PkgType#PKG_SOURCE}. + * @param version The {@link AndroidVersion} specific for this package type. + * @return An existing package information or null if not found. + */ + @Nullable + public LocalPkgInfo getPkgInfo(@NonNull PkgType filter, @NonNull AndroidVersion version) { + assert filter == PkgType.PKG_PLATFORM || + filter == PkgType.PKG_SAMPLE || + filter == PkgType.PKG_SOURCE; + + for (LocalPkgInfo pkg : getPkgsInfos(filter)) { + IPkgDesc d = pkg.getDesc(); + if (d.hasAndroidVersion() && d.getAndroidVersion().equals(version)) { + return pkg; + } + } + + return null; + } + + /** + * Retrieves information on a package identified by its {@link FullRevision}. + *

+ * Note that {@link PkgType#PKG_TOOLS} and {@link PkgType#PKG_PLATFORM_TOOLS} + * are unique in a local SDK so you'll want to use {@link #getPkgInfo(PkgType)} + * to retrieve them instead. + * + * @param filter {@link PkgType#PKG_BUILD_TOOLS}. + * @param revision The {@link FullRevision} uniquely identifying this package. + * @return An existing package information or null if not found. + */ + @Nullable + public LocalPkgInfo getPkgInfo(@NonNull PkgType filter, @NonNull FullRevision revision) { + + assert filter == PkgType.PKG_BUILD_TOOLS; + + for (LocalPkgInfo pkg : getPkgsInfos(filter)) { + IPkgDesc d = pkg.getDesc(); + if (d.hasFullRevision() && d.getFullRevision().equals(revision)) { + return pkg; + } + } + return null; + } + + /** + * Retrieves information on a package identified by its {@link String} path. + *

+ * For add-ons and platforms, the path is the target hash string + * (see {@link AndroidTargetHash} for helpers methods to generate this string.) + * + * @param filter {@link PkgType#PKG_ADDON}, {@link PkgType#PKG_PLATFORM}. + * @param path The vendor/path uniquely identifying this package. + * @return An existing package information or null if not found. + */ + @Nullable + public LocalPkgInfo getPkgInfo(@NonNull PkgType filter, @NonNull String path) { + + assert filter == PkgType.PKG_ADDON || + filter == PkgType.PKG_PLATFORM; + + for (LocalPkgInfo pkg : getPkgsInfos(filter)) { + IPkgDesc d = pkg.getDesc(); + if (d.hasPath() && path.equals(d.getPath())) { + return pkg; + } + } + return null; + } + + /** + * Retrieves information on a package identified by both vendor and path strings. + *

+ * For add-ons the path is target hash string + * (see {@link AndroidTargetHash} for helpers methods to generate this string.) + * + * @param filter {@link PkgType#PKG_EXTRA}, {@link PkgType#PKG_ADDON}. + * @param vendor The vendor id of the extra package. + * @param path The path uniquely identifying this package for its vendor. + * @return An existing package information or null if not found. + */ + @Nullable + public LocalPkgInfo getPkgInfo(@NonNull PkgType filter, + @NonNull String vendor, + @NonNull String path) { + + assert filter == PkgType.PKG_EXTRA || + filter == PkgType.PKG_ADDON; + + for (LocalPkgInfo pkg : getPkgsInfos(filter)) { + IPkgDesc d = pkg.getDesc(); + if (d.hasVendor() && vendor.equals(d.getVendor().getId())) { + if (d.hasPath() && path.equals(d.getPath())) { + return pkg; + } + } + } + return null; + } + + /** + * Retrieves information on an extra package identified by its {@link String} vendor/path. + * + * @param vendor The vendor id of the extra package. + * @param path The path uniquely identifying this package for its vendor. + * @return An existing extra package information or null if not found. + */ + @Nullable + public LocalExtraPkgInfo getExtra(@NonNull String vendor, @NonNull String path) { + return (LocalExtraPkgInfo) getPkgInfo(PkgType.PKG_EXTRA, vendor, path); + } + + /** + * For unique local packages. + * Returns the cached LocalPkgInfo for the requested type. + * Loads it from disk if not cached. + * + * @param filter {@link PkgType#PKG_TOOLS} or {@link PkgType#PKG_PLATFORM_TOOLS} + * or {@link PkgType#PKG_DOC}. + * @return null if the package is not installed. + */ + @Nullable + public LocalPkgInfo getPkgInfo(@NonNull PkgType filter) { + if (filter != PkgType.PKG_TOOLS && + filter != PkgType.PKG_PLATFORM_TOOLS && + filter != PkgType.PKG_DOC && + filter != PkgType.PKG_NDK) { + assert false; + return null; + } + + LocalPkgInfo info = null; + synchronized (mLocalPackages) { + Collection existing = mLocalPackages.get(filter); + assert existing.size() <= 1; + if (!existing.isEmpty()) { + return existing.iterator().next(); + } + + File uniqueDir = new File(mSdkRoot, filter.getFolderName()); + + if (!mVisitedDirs.containsEntry(filter, new LocalDirInfo.MapComparator(uniqueDir))) { + switch(filter) { + case PKG_TOOLS: + info = scanTools(uniqueDir); + break; + case PKG_PLATFORM_TOOLS: + info = scanPlatformTools(uniqueDir); + break; + case PKG_DOC: + info = scanDoc(uniqueDir); + break; + case PKG_NDK: + info = scanNdk(uniqueDir); + default: + break; + } + } + + // Whether we have found a valid pkg or not, this directory has been visited. + mVisitedDirs.put(filter, new LocalDirInfo(mFileOp, uniqueDir)); + + if (info != null) { + mLocalPackages.put(filter, info); + } + } + + return info; + } + + /** + * Retrieve all the info about the requested package type. + * This is used for the package types that have one or more instances, each with different + * versions. + * The resulting array is sorted according to the PkgInfo's sort order. + *

+ * Note: you can use this with {@link PkgType#PKG_TOOLS}, {@link PkgType#PKG_PLATFORM_TOOLS} and + * {@link PkgType#PKG_DOC} but since there can only be one package of these types, it is + * more efficient to use {@link #getPkgInfo(PkgType)} to query them. + * + * @param filter One of {@link PkgType} constants. + * @return A list (possibly empty) of matching installed packages. Never returns null. + */ + @NonNull + public LocalPkgInfo[] getPkgsInfos(@NonNull PkgType filter) { + return getPkgsInfos(EnumSet.of(filter)); + } + + /** + * Retrieve all the info about the requested package types. + * This is used for the package types that have one or more instances, each with different + * versions. + * The resulting array is sorted according to the PkgInfo's sort order. + *

+ * To force the LocalSdk parser to load everything, simply call this method + * with the {@link PkgType#PKG_ALL} argument to load all the known package types. + *

+ * Note: you can use this with {@link PkgType#PKG_TOOLS}, {@link PkgType#PKG_PLATFORM_TOOLS} and + * {@link PkgType#PKG_DOC} but since there can only be one package of these types, it is + * more efficient to use {@link #getPkgInfo(PkgType)} to query them. + * + * @param filters One or more of {@link PkgType#PKG_ADDON}, {@link PkgType#PKG_PLATFORM}, + * {@link PkgType#PKG_BUILD_TOOLS}, {@link PkgType#PKG_EXTRA}, + * {@link PkgType#PKG_SOURCE}, {@link PkgType#PKG_SYS_IMAGE} + * @return A list (possibly empty) of matching installed packages. Never returns null. + */ + @NonNull + public LocalPkgInfo[] getPkgsInfos(@NonNull EnumSet filters) { + List list = Lists.newArrayList(); + + for (PkgType filter : filters) { + if (filter == PkgType.PKG_TOOLS || + filter == PkgType.PKG_PLATFORM_TOOLS || + filter == PkgType.PKG_DOC || + filter == PkgType.PKG_NDK) { + LocalPkgInfo info = getPkgInfo(filter); + if (info != null) { + list.add(info); + } + } else { + synchronized (mLocalPackages) { + Collection existing = mLocalPackages.get(filter); + assert existing != null; // Multimap returns an empty set if not found + + if (!existing.isEmpty()) { + list.addAll(existing); + continue; + } + + File subDir = new File(mSdkRoot, filter.getFolderName()); + + if (!mVisitedDirs.containsEntry(filter, new LocalDirInfo.MapComparator(subDir))) { + switch(filter) { + case PKG_BUILD_TOOLS: + scanBuildTools(subDir, existing); + break; + + case PKG_PLATFORM: + scanPlatforms(subDir, existing); + break; + + case PKG_SYS_IMAGE: + scanSysImages(subDir, existing, false); + break; + + case PKG_ADDON_SYS_IMAGE: + scanSysImages(subDir, existing, true); + break; + + case PKG_ADDON: + scanAddons(subDir, existing); + break; + + case PKG_SAMPLE: + scanSamples(subDir, existing); + break; + + case PKG_SOURCE: + scanSources(subDir, existing); + break; + + case PKG_EXTRA: + scanExtras(subDir, existing); + break; + + case PKG_TOOLS: + case PKG_PLATFORM_TOOLS: + case PKG_DOC: + case PKG_NDK: + break; + default: + throw new IllegalArgumentException( + "Unsupported pkg type " + filter.toString()); + } + mVisitedDirs.put(filter, new LocalDirInfo(mFileOp, subDir)); + list.addAll(existing); + } + } + } + } + + Collections.sort(list); + return list.toArray(new LocalPkgInfo[list.size()]); + } + + //---------- Package-specific querying -------- + + /** + * Returns the {@link BuildToolInfo} for the given revision. + * + * @param revision The requested revision. + * @return A {@link BuildToolInfo}. Can be null if {@code revision} is null or is + * not part of the known set returned by {@code getPkgsInfos(PkgType.PKG_BUILD_TOOLS)}. + */ + @Nullable + public BuildToolInfo getBuildTool(@Nullable FullRevision revision) { + LocalPkgInfo pkg = getPkgInfo(PkgType.PKG_BUILD_TOOLS, revision); + if (pkg instanceof LocalBuildToolPkgInfo) { + return ((LocalBuildToolPkgInfo) pkg).getBuildToolInfo(); + } + return null; + } + + /** + * Returns the highest build-tool revision known, or null if there are are no build-tools. + *

+ * If no specific build-tool package is installed but the platform-tools is lower than 17, + * then this creates and returns a "legacy" built-tool package using platform-tools. + * (We only split build-tools out of platform-tools starting with revision 17, + * before they were both the same thing.) + * + * @return The highest build-tool revision known, or null. + */ + @Nullable + public BuildToolInfo getLatestBuildTool() { + if (mLegacyBuildTools != null) { + return mLegacyBuildTools; + } + + LocalPkgInfo[] pkgs = getPkgsInfos(PkgType.PKG_BUILD_TOOLS); + + if (pkgs.length == 0) { + LocalPkgInfo ptPkg = getPkgInfo(PkgType.PKG_PLATFORM_TOOLS); + if (ptPkg instanceof LocalPlatformToolPkgInfo && + ptPkg.getDesc().getFullRevision().compareTo(new FullRevision(17)) < 0) { + // older SDK, create a compatible build-tools + mLegacyBuildTools = createLegacyBuildTools((LocalPlatformToolPkgInfo) ptPkg); + return mLegacyBuildTools; + } + return null; + } + + assert pkgs.length > 0; + + // Note: the pkgs come from a TreeMultimap so they should already be sorted. + // Just in case, sort them again. + Arrays.sort(pkgs); + + // LocalBuildToolPkgInfo's comparator sorts on its FullRevision so we just + // need to take the latest element. + LocalPkgInfo pkg = pkgs[pkgs.length - 1]; + if (pkg instanceof LocalBuildToolPkgInfo) { + return ((LocalBuildToolPkgInfo) pkg).getBuildToolInfo(); + } + + return null; + } + + @NonNull + private BuildToolInfo createLegacyBuildTools(@NonNull LocalPlatformToolPkgInfo ptInfo) { + File platformTools = new File(getLocation(), SdkConstants.FD_PLATFORM_TOOLS); + File platformToolsLib = ptInfo.getLocalDir(); + File platformToolsRs = new File(platformTools, SdkConstants.FN_FRAMEWORK_RENDERSCRIPT); + + return new BuildToolInfo( + ptInfo.getDesc().getFullRevision(), + platformTools, + new File(platformTools, SdkConstants.FN_AAPT), + new File(platformTools, SdkConstants.FN_AIDL), + new File(platformTools, SdkConstants.FN_DX), + new File(platformToolsLib, SdkConstants.FN_DX_JAR), + new File(platformTools, SdkConstants.FN_RENDERSCRIPT), + new File(platformToolsRs, SdkConstants.FN_FRAMEWORK_INCLUDE), + new File(platformToolsRs, SdkConstants.FN_FRAMEWORK_INCLUDE_CLANG), + null, + null, + null, + null, + new File(platformTools, SdkConstants.FN_ZIPALIGN)); + } + + /** + * Returns the targets (platforms & addons) that are available in the SDK. + * The target list is created on demand the first time then cached. + * It will not refreshed unless {@link #clearLocalPkg} is called to clear platforms + * and/or add-ons. + *

+ * The array can be empty but not null. + */ + @NonNull + public IAndroidTarget[] getTargets() { + synchronized (mLocalPackages) { + if (mCachedTargets == null) { + List result = Lists.newArrayList(); + LocalPkgInfo[] pkgsInfos = getPkgsInfos(EnumSet.of(PkgType.PKG_PLATFORM, + PkgType.PKG_ADDON)); + for (LocalPkgInfo info : pkgsInfos) { + assert info instanceof LocalPlatformPkgInfo; + IAndroidTarget target = ((LocalPlatformPkgInfo) info).getAndroidTarget(); + if (target != null) { + result.add(target); + } + } + mCachedTargets = result; + } + return mCachedTargets.toArray(new IAndroidTarget[mCachedTargets.size()]); + } + } + + public IAndroidTarget[] getTargets(boolean includeMissing) { + IAndroidTarget[] result = getTargets(); + if (includeMissing) { + result = ObjectArrays.concat(result, getMissingTargets(), IAndroidTarget.class); + } + return result; + } + + @NonNull + public IAndroidTarget[] getMissingTargets() { + synchronized (mLocalPackages) { + if (mCachedMissingTargets == null) { + Map result = Maps.newHashMap(); + Set seen = Sets.newHashSet(); + for (IAndroidTarget target : getTargets()) { + Collections.addAll(seen, target.getSystemImages()); + } + for (LocalPkgInfo local : getPkgsInfos(PkgType.PKG_ADDON_SYS_IMAGE)) { + LocalAddonSysImgPkgInfo info = (LocalAddonSysImgPkgInfo)local; + ISystemImage image = info.getSystemImage(); + if (!seen.contains(image)) { + addOrphanedSystemImage(image, info.getDesc(), result); + } + } + for (LocalPkgInfo local : getPkgsInfos(PkgType.PKG_SYS_IMAGE)) { + LocalSysImgPkgInfo info = (LocalSysImgPkgInfo)local; + ISystemImage image = info.getSystemImage(); + if (!seen.contains(image)) { + addOrphanedSystemImage(image, info.getDesc(), result); + } + } + mCachedMissingTargets = result.keySet(); + } + return mCachedMissingTargets.toArray(new IAndroidTarget[mCachedMissingTargets.size()]); + } + } + + private static void addOrphanedSystemImage(ISystemImage image, IPkgDesc desc, Map targets) { + IdDisplay vendor = desc.getVendor(); + MissingTarget target = new MissingTarget(vendor == null ? null : vendor.getDisplay(), desc.getTag().getDisplay(), desc.getAndroidVersion()); + MissingTarget existing = targets.get(target); + if (existing == null) { + existing = target; + targets.put(target, target); + } + existing.addSystemImage(image); + } + + /** + * Returns a target from a hash that was generated by {@link IAndroidTarget#hashString()}. + * + * @param hash the {@link IAndroidTarget} hash string. + * @return The matching {@link IAndroidTarget} or null. + */ + @Nullable + public IAndroidTarget getTargetFromHashString(@Nullable String hash) { + if (hash != null) { + IAndroidTarget[] targets = getTargets(true); + for (IAndroidTarget target : targets) { + if (target != null && hash.equals(AndroidTargetHash.getTargetHashString(target))) { + return target; + } + } + } + return null; + } + + // ------------- + + /** + * Try to find a tools package at the given location. + * Returns null if not found. + */ + private LocalToolPkgInfo scanTools(File toolFolder) { + // Can we find some properties? + Properties props = parseProperties(new File(toolFolder, SdkConstants.FN_SOURCE_PROP)); + FullRevision rev = PackageParserUtils.getPropertyFull(props, PkgProps.PKG_REVISION); + if (rev == null) { + return null; + } + + FullRevision minPlatToolsRev = + PackageParserUtils.getPropertyFull(props, PkgProps.MIN_PLATFORM_TOOLS_REV); + if (minPlatToolsRev == null) { + minPlatToolsRev = FullRevision.NOT_SPECIFIED; + } + + LocalToolPkgInfo info = new LocalToolPkgInfo(this, toolFolder, props, rev, minPlatToolsRev); + + // We're not going to check that all tools are present. At the very least + // we should expect to find android and an emulator adapted to the current OS. + boolean hasEmulator = false; + boolean hasAndroid = false; + String android1 = SdkConstants.androidCmdName().replace(".bat", ".exe"); + String android2 = android1.indexOf('.') == -1 ? null : android1.replace(".exe", ".bat"); + File[] files = mFileOp.listFiles(toolFolder); + for (File file : files) { + String name = file.getName(); + if (SdkConstants.FN_EMULATOR.equals(name)) { + hasEmulator = true; + } + if (android1.equals(name) || (android2 != null && android2.equals(name))) { + hasAndroid = true; + } + } + if (!hasAndroid) { + info.appendLoadError("Missing %1$s", SdkConstants.androidCmdName()); + } + if (!hasEmulator) { + info.appendLoadError("Missing %1$s", SdkConstants.FN_EMULATOR); + } + + return info; + } + + /** + * Try to find a platform-tools package at the given location. + * Returns null if not found. + */ + private LocalPlatformToolPkgInfo scanPlatformTools(File ptFolder) { + // Can we find some properties? + Properties props = parseProperties(new File(ptFolder, SdkConstants.FN_SOURCE_PROP)); + FullRevision rev = PackageParserUtils.getPropertyFull(props, PkgProps.PKG_REVISION); + if (rev == null) { + return null; + } + + LocalPlatformToolPkgInfo info = new LocalPlatformToolPkgInfo(this, ptFolder, props, rev); + return info; + } + + /** + * Try to find a docs package at the given location. + * Returns null if not found. + */ + private LocalDocPkgInfo scanDoc(File docFolder) { + // Can we find some properties? + Properties props = parseProperties(new File(docFolder, SdkConstants.FN_SOURCE_PROP)); + MajorRevision rev = PackageParserUtils.getPropertyMajor(props, PkgProps.PKG_REVISION); + if (rev == null) { + return null; + } + + try { + AndroidVersion vers = new AndroidVersion(props); + LocalDocPkgInfo info = new LocalDocPkgInfo(this, docFolder, props, vers, rev); + + // To start with, a doc folder should have an "index.html" to be acceptable. + // We don't actually check the content of the file. + if (!mFileOp.isFile(new File(docFolder, "index.html"))) { + info.appendLoadError("Missing index.html"); + } + return info; + + } catch (AndroidVersionException e) { + return null; // skip invalid or missing android version. + } + } + + /** + * Try to find an NDK package at the given location. + * Returns null if not found. + */ + @Nullable + private LocalNdkPkgInfo scanNdk(@NonNull File ndkFolder) { + // Can we find some properties? + Properties props = parseProperties(new File(ndkFolder, SdkConstants.FN_SOURCE_PROP)); + FullRevision rev = PackageParserUtils.getPropertyFull(props, PkgProps.PKG_REVISION); + if (rev == null) { + return null; + } + + return new LocalNdkPkgInfo(this, ndkFolder, props, rev); + } + + + /** + * Helper used by scanXyz methods below to check whether a directory should be visited. + * It can be skipped if it's not a directory or if it's already marked as visited in + * mVisitedDirs for the given package type -- in which case the directory is added to + * the visited map. + * + * @param pkgType The package type being scanned. + * @param directory The file or directory to check. + * @return False if directory can/should be skipped. + * True if directory should be visited, in which case it's registered in mVisitedDirs. + */ + private boolean shouldVisitDir(@NonNull PkgType pkgType, @NonNull File directory) { + if (!mFileOp.isDirectory(directory)) { + return false; + } + synchronized (mLocalPackages) { + if (mVisitedDirs.containsEntry(pkgType, new LocalDirInfo.MapComparator(directory))) { + return false; + } + mVisitedDirs.put(pkgType, new LocalDirInfo(mFileOp, directory)); + } + return true; + } + + private void scanBuildTools(File collectionDir, Collection outCollection) { + // The build-tool root folder contains a list of per-revision folders. + for (File buildToolDir : mFileOp.listFiles(collectionDir)) { + if (!shouldVisitDir(PkgType.PKG_BUILD_TOOLS, buildToolDir)) { + continue; + } + + Properties props = parseProperties(new File(buildToolDir, SdkConstants.FN_SOURCE_PROP)); + FullRevision rev = PackageParserUtils.getPropertyFull(props, PkgProps.PKG_REVISION); + if (rev == null) { + continue; // skip, no revision + } + + BuildToolInfo btInfo = new BuildToolInfo(rev, buildToolDir); + LocalBuildToolPkgInfo pkgInfo = + new LocalBuildToolPkgInfo(this, buildToolDir, props, rev, btInfo); + outCollection.add(pkgInfo); + } + } + + private void scanPlatforms(File collectionDir, Collection outCollection) { + for (File platformDir : mFileOp.listFiles(collectionDir)) { + if (!shouldVisitDir(PkgType.PKG_PLATFORM, platformDir)) { + continue; + } + + Properties props = parseProperties(new File(platformDir, SdkConstants.FN_SOURCE_PROP)); + MajorRevision rev = PackageParserUtils.getPropertyMajor(props, PkgProps.PKG_REVISION); + if (rev == null) { + continue; // skip, no revision + } + + FullRevision minToolsRev = + PackageParserUtils.getPropertyFull(props, PkgProps.MIN_TOOLS_REV); + if (minToolsRev == null) { + minToolsRev = FullRevision.NOT_SPECIFIED; + } + + try { + AndroidVersion vers = new AndroidVersion(props); + + LocalPlatformPkgInfo pkgInfo = + new LocalPlatformPkgInfo(this, platformDir, props, vers, rev, minToolsRev); + outCollection.add(pkgInfo); + + } catch (AndroidVersionException e) { + continue; // skip invalid or missing android version. + } + } + } + + private void scanAddons(File collectionDir, Collection outCollection) { + for (File addonDir : mFileOp.listFiles(collectionDir)) { + if (!shouldVisitDir(PkgType.PKG_ADDON, addonDir)) { + continue; + } + + Properties props = parseProperties(new File(addonDir, SdkConstants.FN_SOURCE_PROP)); + MajorRevision rev = PackageParserUtils.getPropertyMajor(props, PkgProps.PKG_REVISION); + if (rev == null) { + continue; // skip, no revision + } + + try { + AndroidVersion vers = new AndroidVersion(props); + + // Starting with addon-4.xsd, we have vendor-id and name-id available + // in the add-on source properties so we'll use that directly. + + String nameId = props.getProperty(PkgProps.ADDON_NAME_ID); + String nameDisp = props.getProperty(PkgProps.ADDON_NAME_DISPLAY); + String vendorId = props.getProperty(PkgProps.ADDON_VENDOR_ID); + String vendorDisp = props.getProperty(PkgProps.ADDON_VENDOR_DISPLAY); + + if (nameId == null) { + // Support earlier add-ons that only had a name display attribute + nameDisp = props.getProperty(PkgProps.ADDON_NAME, "Unknown"); + nameId = LocalAddonPkgInfo.sanitizeDisplayToNameId(nameDisp); + } + + if (nameId != null && nameDisp == null) { + nameDisp = LocalExtraPkgInfo.getPrettyName(null, nameId); + } + + if (vendorId != null && vendorDisp == null) { + vendorDisp = LocalExtraPkgInfo.getPrettyName(null, nameId); + } + + if (vendorId == null) { + // Support earlier add-ons that only had a vendor display attribute + vendorDisp = props.getProperty(PkgProps.ADDON_VENDOR, "Unknown"); + vendorId = LocalAddonPkgInfo.sanitizeDisplayToNameId(vendorDisp); + } + + LocalAddonPkgInfo pkgInfo = new LocalAddonPkgInfo( + this, addonDir, props, vers, rev, + new IdDisplay(vendorId, vendorDisp), + new IdDisplay(nameId, nameDisp)); + outCollection.add(pkgInfo); + + } catch (AndroidVersionException e) { + continue; // skip invalid or missing android version. + } + } + } + + private void scanSysImages( + File collectionDir, + Collection outCollection, + boolean scanAddons) { + List propFiles = Lists.newArrayList(); + PkgType type = scanAddons ? PkgType.PKG_ADDON_SYS_IMAGE : PkgType.PKG_SYS_IMAGE; + + // Create a list of folders that contains a source.properties file matching these patterns: + // sys-img/target/tag/abi + // sys-img/target/abis + // sys-img/add-on-target/abi + // sys-img/target/add-on/abi + for (File platformDir : mFileOp.listFiles(collectionDir)) { + if (!shouldVisitDir(type, platformDir)) { + continue; + } + + for (File dir1 : mFileOp.listFiles(platformDir)) { + // dir1 might be either a tag or an abi folder. + if (!shouldVisitDir(type, dir1)) { + continue; + } + + File prop1 = new File(dir1, SdkConstants.FN_SOURCE_PROP); + if (mFileOp.isFile(prop1)) { + // dir1 was a legacy abi folder. + if (!propFiles.contains(prop1)) { + propFiles.add(prop1); + } + } else { + File[] dir1Files = mFileOp.listFiles(dir1); + for (File dir2 : dir1Files) { + // dir2 should be an abi folder in a tag folder. + if (!shouldVisitDir(type, dir2)) { + continue; + } + + File prop2 = new File(dir2, SdkConstants.FN_SOURCE_PROP); + if (mFileOp.isFile(prop2)) { + if (!propFiles.contains(prop2)) { + propFiles.add(prop2); + } + } + } + } + } + } + + for (File propFile : propFiles) { + Properties props = parseProperties(propFile); + MajorRevision rev = PackageParserUtils.getPropertyMajor(props, PkgProps.PKG_REVISION); + if (rev == null) { + continue; // skip, no revision + } + + try { + AndroidVersion vers = new AndroidVersion(props); + + IdDisplay tag = LocalSysImgPkgInfo.extractTagFromProps(props); + String vendorId = props.getProperty(PkgProps.ADDON_VENDOR_ID, null); + File abiDir = propFile.getParentFile(); + + if (vendorId == null && !scanAddons) { + LocalSysImgPkgInfo pkgInfo = + new LocalSysImgPkgInfo(this, abiDir, props, vers, tag, abiDir.getName(), rev); + outCollection.add(pkgInfo); + + } else if (vendorId != null && scanAddons) { + String vendorDisp = props.getProperty(PkgProps.ADDON_VENDOR_DISPLAY, vendorId); + IdDisplay vendor = new IdDisplay(vendorId, vendorDisp); + + LocalAddonSysImgPkgInfo pkgInfo = + new LocalAddonSysImgPkgInfo( + this, abiDir, props, vers, vendor, tag, abiDir.getName(), rev); + outCollection.add(pkgInfo); + } + + } catch (AndroidVersionException e) { + continue; // skip invalid or missing android version. + } + } + } + + private void scanSamples(File collectionDir, Collection outCollection) { + for (File platformDir : mFileOp.listFiles(collectionDir)) { + if (!shouldVisitDir(PkgType.PKG_SAMPLE, platformDir)) { + continue; + } + + Properties props = parseProperties(new File(platformDir, SdkConstants.FN_SOURCE_PROP)); + MajorRevision rev = PackageParserUtils.getPropertyMajor(props, PkgProps.PKG_REVISION); + if (rev == null) { + continue; // skip, no revision + } + + FullRevision minToolsRev = + PackageParserUtils.getPropertyFull(props, PkgProps.MIN_TOOLS_REV); + if (minToolsRev == null) { + minToolsRev = FullRevision.NOT_SPECIFIED; + } + + try { + AndroidVersion vers = new AndroidVersion(props); + + LocalSamplePkgInfo pkgInfo = + new LocalSamplePkgInfo(this, platformDir, props, vers, rev, minToolsRev); + outCollection.add(pkgInfo); + } catch (AndroidVersionException e) { + continue; // skip invalid or missing android version. + } + } + } + + private void scanSources(File collectionDir, Collection outCollection) { + // The build-tool root folder contains a list of per-revision folders. + for (File platformDir : mFileOp.listFiles(collectionDir)) { + if (!shouldVisitDir(PkgType.PKG_SOURCE, platformDir)) { + continue; + } + + Properties props = parseProperties(new File(platformDir, SdkConstants.FN_SOURCE_PROP)); + MajorRevision rev = PackageParserUtils.getPropertyMajor(props, PkgProps.PKG_REVISION); + if (rev == null) { + continue; // skip, no revision + } + + try { + AndroidVersion vers = new AndroidVersion(props); + + LocalSourcePkgInfo pkgInfo = + new LocalSourcePkgInfo(this, platformDir, props, vers, rev); + outCollection.add(pkgInfo); + } catch (AndroidVersionException e) { + continue; // skip invalid or missing android version. + } + } + } + + private void scanExtras(File collectionDir, Collection outCollection) { + for (File vendorDir : mFileOp.listFiles(collectionDir)) { + if (!shouldVisitDir(PkgType.PKG_EXTRA, vendorDir)) { + continue; + } + + for (File extraDir : mFileOp.listFiles(vendorDir)) { + if (!shouldVisitDir(PkgType.PKG_EXTRA, extraDir)) { + continue; + } + + Properties props = parseProperties(new File(extraDir, SdkConstants.FN_SOURCE_PROP)); + NoPreviewRevision rev = + PackageParserUtils.getPropertyNoPreview(props, PkgProps.PKG_REVISION); + if (rev == null) { + continue; // skip, no revision + } + + String oldPaths = + PackageParserUtils.getProperty(props, PkgProps.EXTRA_OLD_PATHS, null); + + String vendorId = vendorDir.getName(); + String vendorDisp = props.getProperty(PkgProps.EXTRA_VENDOR_DISPLAY); + if (vendorDisp == null || vendorDisp.isEmpty()) { + vendorDisp = vendorId; + } + + String displayName = props.getProperty(PkgProps.EXTRA_NAME_DISPLAY, null); + + LocalExtraPkgInfo pkgInfo = new LocalExtraPkgInfo( + this, + extraDir, + props, + new IdDisplay(vendorId, vendorDisp), + extraDir.getName(), + displayName, + PkgDescExtra.convertOldPaths(oldPaths), + rev); + outCollection.add(pkgInfo); + } + } + } + + /** + * Parses the given file as properties file if it exists. + * Returns null if the file does not exist, cannot be parsed or has no properties. + */ + private Properties parseProperties(File propsFile) { + InputStream fis = null; + try { + if (mFileOp.exists(propsFile)) { + fis = mFileOp.newFileInputStream(propsFile); + + Properties props = new Properties(); + props.load(fis); + + // To be valid, there must be at least one property in it. + if (!props.isEmpty()) { + return props; + } + } + } catch (IOException e) { + // Ignore + } finally { + if (fis != null) { + try { + fis.close(); + } catch (IOException e) {} + } + } + return null; + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalSourcePkgInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalSourcePkgInfo.java new file mode 100644 index 00000000..eb822de1 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalSourcePkgInfo.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2013 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.sdklib.repository.local; + +import com.android.annotations.NonNull; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.repository.MajorRevision; +import com.android.sdklib.repository.descriptors.IPkgDesc; +import com.android.sdklib.repository.descriptors.PkgDesc; + +import java.io.File; +import java.util.Properties; + +/** + * Local source package, for a given platform's {@link AndroidVersion}. + * The package itself has a major revision. + * There should be only one for a given android platform version. + */ +public class LocalSourcePkgInfo extends LocalPkgInfo { + + @NonNull + private final IPkgDesc mDesc; + + public LocalSourcePkgInfo(@NonNull LocalSdk localSdk, + @NonNull File localDir, + @NonNull Properties sourceProps, + @NonNull AndroidVersion version, + @NonNull MajorRevision revision) { + super(localSdk, localDir, sourceProps); + mDesc = PkgDesc.Builder.newSource(version, revision).create(); + } + + @NonNull + @Override + public IPkgDesc getDesc() { + return mDesc; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalSysImgPkgInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalSysImgPkgInfo.java new file mode 100644 index 00000000..0f6cdad5 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalSysImgPkgInfo.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2013 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.sdklib.repository.local; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.ISystemImage; +import com.android.sdklib.SystemImage; +import com.android.sdklib.io.LegacyFileOp; +import com.android.sdklib.io.IFileOp; +import com.android.sdklib.repository.MajorRevision; +import com.android.sdklib.repository.PkgProps; +import com.android.sdklib.repository.descriptors.IPkgDesc; +import com.android.sdklib.repository.descriptors.IdDisplay; +import com.android.sdklib.repository.descriptors.PkgDesc; +import com.google.common.base.Objects; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Properties; + +/** + * Local system-image package, for a given platform's {@link AndroidVersion} + * and given ABI. + * The package itself has a major revision. + * There should be only one for a given android platform version & ABI. + */ +public class LocalSysImgPkgInfo extends LocalPkgInfo { + + + @NonNull + private final IPkgDesc mDesc; + + public LocalSysImgPkgInfo(@NonNull LocalSdk localSdk, + @NonNull File localDir, + @NonNull Properties sourceProps, + @NonNull AndroidVersion version, + @Nullable IdDisplay tag, + @NonNull String abi, + @NonNull MajorRevision revision) { + super(localSdk, localDir, sourceProps); + mDesc = PkgDesc.Builder.newSysImg(version, tag, abi, revision).create(); + } + + @NonNull + @Override + public IPkgDesc getDesc() { + return mDesc; + } + + /** + * Extracts the tag id & display from the properties. + * If missing, uses the "default" tag id. + */ + @NonNull + public static IdDisplay extractTagFromProps(Properties props) { + if (props != null) { + String tagId = props.getProperty(PkgProps.SYS_IMG_TAG_ID, SystemImage.DEFAULT_TAG.getId()); + String tagDisp = props.getProperty(PkgProps.SYS_IMG_TAG_DISPLAY, ""); //$NON-NLS-1$ + if (tagDisp == null || tagDisp.isEmpty()) { + tagDisp = tagIdToDisplay(tagId); + } + assert tagId != null; + assert tagDisp != null; + return new IdDisplay(tagId, tagDisp); + } + return SystemImage.DEFAULT_TAG; + } + + /** + * Computes a display-friendly tag string based on the tag id. + * This is typically used when there's no tag-display attribute. + * + * @param tagId A non-null tag id to sanitize for display. + * @return The tag id with all non-alphanum symbols replaced by spaces and trimmed. + */ + @NonNull + public static String tagIdToDisplay(@NonNull String tagId) { + String name; + name = tagId.replaceAll("[^A-Za-z0-9]+", " "); //$NON-NLS-1$ //$NON-NLS-2$ + name = name.replaceAll(" +", " "); //$NON-NLS-1$ //$NON-NLS-2$ + name = name.trim(); + + if (!name.isEmpty()) { + char c = name.charAt(0); + if (!Character.isUpperCase(c)) { + StringBuilder sb = new StringBuilder(name); + sb.replace(0, 1, String.valueOf(c).toUpperCase(Locale.US)); + name = sb.toString(); + } + } + return name; + } + + public SystemImage getSystemImage() { + return getSystemImage(mDesc, getLocalDir(), getLocalSdk().getFileOp()); + } + + static SystemImage getSystemImage(IPkgDesc desc, File localDir, @NonNull IFileOp fileOp) { + final IdDisplay tag = desc.getTag(); + final String abi = desc.getPath(); + List parsedSkins = PackageParserUtils.parseSkinFolder(new File(localDir, SdkConstants.FD_SKINS), fileOp); + File[] skins = LegacyFileOp.EMPTY_FILE_ARRAY; + if (!parsedSkins.isEmpty()) { + skins = parsedSkins.toArray(new File[parsedSkins.size()]); + } + + return new SystemImage( + localDir, + ISystemImage.LocationType.IN_SYSTEM_IMAGE, + tag, + desc.getVendor(), + abi, + skins); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalToolPkgInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalToolPkgInfo.java new file mode 100644 index 00000000..91306ea3 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalToolPkgInfo.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2013 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.sdklib.repository.local; + +import com.android.annotations.NonNull; +import com.android.sdklib.repository.FullRevision; +import com.android.sdklib.repository.descriptors.IPkgDesc; +import com.android.sdklib.repository.descriptors.PkgDesc; + +import java.io.File; +import java.util.Properties; + +public class LocalToolPkgInfo extends LocalPkgInfo { + + @NonNull + private final IPkgDesc mDesc; + + public LocalToolPkgInfo(@NonNull LocalSdk localSdk, + @NonNull File localDir, + @NonNull Properties sourceProps, + @NonNull FullRevision revision, + @NonNull FullRevision minPlatformToolsRev) { + super(localSdk, localDir, sourceProps); + mDesc = PkgDesc.Builder.newTool(revision, minPlatformToolsRev).create(); + } + + @NonNull + @Override + public IPkgDesc getDesc() { + return mDesc; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/PackageParserUtils.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/PackageParserUtils.java new file mode 100644 index 00000000..e8cc4b2e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/PackageParserUtils.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2015 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.sdklib.repository.local; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdklib.io.IFileOp; +import com.android.sdklib.repository.FullRevision; +import com.android.sdklib.repository.MajorRevision; +import com.android.sdklib.repository.NoPreviewRevision; +import com.android.sdklib.repository.PkgProps; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +/** + * Misc utilities to help extracting elements and attributes out of a repository XML document. + */ +class PackageParserUtils { + /** + * Utility method to parse the {@link PkgProps#PKG_REVISION} property as a full + * revision (major.minor.micro.preview). + * + * @param props The properties to parse. + * @return A {@link FullRevision} or null if there is no such property or it couldn't be parsed. + * @param propKey The name of the property. Must not be null. + */ + @Nullable + public static FullRevision getPropertyFull( + @Nullable Properties props, + @NonNull String propKey) { + String revStr = getProperty(props, propKey, null); + + FullRevision rev = null; + if (revStr != null) { + try { + rev = FullRevision.parseRevision(revStr); + } catch (NumberFormatException ignore) {} + } + + return rev; + } + + /** + * Utility method to parse the {@link PkgProps#PKG_REVISION} property as a major + * revision (major integer, no minor/micro/preview parts.) + * + * @param props The properties to parse. + * @return A {@link MajorRevision} or null if there is no such property or it couldn't be parsed. + * @param propKey The name of the property. Must not be null. + */ + @Nullable + public static MajorRevision getPropertyMajor( + @Nullable Properties props, + @NonNull String propKey) { + String revStr = getProperty(props, propKey, null); + + MajorRevision rev = null; + if (revStr != null) { + try { + rev = MajorRevision.parseRevision(revStr); + } catch (NumberFormatException ignore) {} + } + + return rev; + } + + /** + * Utility method to parse the {@link PkgProps#PKG_REVISION} property as a no-preview + * revision (major.minor.micro integers but no preview part.) + * + * @param props The properties to parse. + * @return A {@link NoPreviewRevision} or + * null if there is no such property or it couldn't be parsed. + * @param propKey The name of the property. Must not be null. + */ + @Nullable + public static NoPreviewRevision getPropertyNoPreview( + @Nullable Properties props, + @NonNull String propKey) { + String revStr = getProperty(props, propKey, null); + + NoPreviewRevision rev = null; + if (revStr != null) { + try { + rev = NoPreviewRevision.parseRevision(revStr); + } catch (NumberFormatException ignore) {} + } + + return rev; + } + + + /** + * Utility method that returns a property from a {@link Properties} object. + * Returns the default value if props is null or if the property is not defined. + * + * @param props The {@link Properties} to search into. + * If null, the default value is returned. + * @param propKey The name of the property. Must not be null. + * @param defaultValue The default value to return if {@code props} is null or if the + * key is not found. Can be null. + * @return The string value of the given key in the properties, or null if the key + * isn't found or if {@code props} is null. + */ + @Nullable + public static String getProperty( + @Nullable Properties props, + @NonNull String propKey, + @Nullable String defaultValue) { + if (props == null) { + return defaultValue; + } + return props.getProperty(propKey, defaultValue); + } + + + /** + * Parses the skin folder and builds the skin list. + * @param skinRootFolder The path to the skin root folder. + */ + @NonNull + public static List parseSkinFolder(@NonNull File skinRootFolder, @NonNull IFileOp fileOp) { + if (fileOp.isDirectory(skinRootFolder)) { + ArrayList skinList = new ArrayList(); + + File[] files = fileOp.listFiles(skinRootFolder); + + for (File skinFolder : files) { + if (fileOp.isDirectory(skinFolder)) { + // check for layout file + File layout = new File(skinFolder, SdkConstants.FN_SKIN_LAYOUT); + + if (fileOp.isFile(layout)) { + // for now we don't parse the content of the layout and + // simply add the directory to the list. + skinList.add(skinFolder); + } + } + } + + Collections.sort(skinList); + return skinList; + } + + return Collections.emptyList(); + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-01.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-01.xsd new file mode 100644 index 00000000..1d533132 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-01.xsd @@ -0,0 +1,295 @@ + + + + + + + + + + + + The repository contains a collection of downloadable packages. + + + + + + + + + + + + + + An SDK add-on package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK extra package. This kind of package is for "free" content. + Such packages are installed in SDK/vendor/path. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + One path segment for the install path of an extra element. + It must be a single-segment path. It must not be empty. + + + + + + + + + + + + + + A license definition. Such a license must be used later as a reference + using a uses-license element in one of the package elements. + + + + + + + + + + + + + + + + + Describes the license used by a package. The license MUST be defined + using a license node and referenced using the ref attribute of the + license element inside a package. + + + + + + + + + + + + A collection of files that can be downloaded for a given architecture. + The <archives> node is mandatory in the repository packages and the + collection must have at least one <archive> declared. + Each archive is a zip file that will be unzipped in a location that depends + on its package type. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A SHA1 checksum. + + + + + + + + + A file checksum, currently only SHA1. + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-02.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-02.xsd new file mode 100644 index 00000000..27fae8b1 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-02.xsd @@ -0,0 +1,361 @@ + + + + + + + + + + + + The repository contains a collection of downloadable packages. + + + + + + + + + + + + + + An SDK add-on package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Version information for a layoutlib included in an addon. + . + + + + + + + + + + + + + + + + An SDK extra package. This kind of package is for "free" content. + Such packages are installed in SDK/vendor/path. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + One path segment for the install path of an extra element. + It must be a single-segment path. It must not be empty. + + + + + + + + + + + + + + A license definition. Such a license must be used later as a reference + using a uses-license element in one of the package elements. + + + + + + + + + + + + + + + + + Describes the license used by a package. The license MUST be defined + using a license node and referenced using the ref attribute of the + license element inside a package. + + + + + + + + + + + + A collection of files that can be downloaded for a given architecture. + The <archives> node is mandatory in the repository packages and the + collection must have at least one <archive> declared. + Each archive is a zip file that will be unzipped in a location that depends + on its package type. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A collection of file paths available in an <extra> package + that can be installed in an Android project. + If present, the <project-files> collection must contain at least one path. + Each path is relative to the root directory of the package. + + + + + + + + + + + + + + A SHA1 checksum. + + + + + + + + + A file checksum, currently only SHA1. + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-03.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-03.xsd new file mode 100644 index 00000000..ccd00c2a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-03.xsd @@ -0,0 +1,381 @@ + + + + + + + + + + + + The repository contains a collection of downloadable packages. + + + + + + + + + + + + + + An SDK add-on package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Version information for a layoutlib included in an addon. + . + + + + + + + + + + + + + + + + An SDK extra package. This kind of package is for "free" content. + Such packages are installed in SDK/vendor/path. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + One path segment for the install path of an extra element. + It must be a single-segment path. It must not be empty. + + + + + + + + + + + A semi-colon separated list of a segmentTypes. + + + + + + + + + + + + + + A license definition. Such a license must be used later as a reference + using a uses-license element in one of the package elements. + + + + + + + + + + + + + + + + + Describes the license used by a package. The license MUST be defined + using a license node and referenced using the ref attribute of the + license element inside a package. + + + + + + + + + + + + A collection of files that can be downloaded for a given architecture. + The <archives> node is mandatory in the repository packages and the + collection must have at least one <archive> declared. + Each archive is a zip file that will be unzipped in a location that depends + on its package type. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A collection of file paths available in an <extra> package + that can be installed in an Android project. + If present, the <project-files> collection must contain at least one path. + Each path is relative to the root directory of the package. + + + + + + + + + + + + + + A SHA1 checksum. + + + + + + + + + A file checksum, currently only SHA1. + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-04.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-04.xsd new file mode 100644 index 00000000..c31efbf5 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-04.xsd @@ -0,0 +1,417 @@ + + + + + + + + + + + + The repository contains a collection of downloadable packages. + + + + + + + + + + + + + + An SDK add-on package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An ID string for an addon/extra name-id or vendor-id + can only be simple alphanumeric string. + + + + + + + + + + + + + + Version information for a layoutlib included in an addon. + . + + + + + + + + + + + + + + + + An SDK extra package. This kind of package is for "free" content. + Such packages are installed in SDK/vendor/path. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + One path segment for the install path of an extra element. + It must be a single-segment path. It must not be empty. + + + + + + + + + + + A semi-colon separated list of a segmentTypes. + + + + + + + + + + + + + + A license definition. Such a license must be used later as a reference + using a uses-license element in one of the package elements. + + + + + + + + + + + + + + + + + Describes the license used by a package. The license MUST be defined + using a license node and referenced using the ref attribute of the + license element inside a package. + + + + + + + + + + + + A collection of files that can be downloaded for a given architecture. + The <archives> node is mandatory in the repository packages and the + collection must have at least one <archive> declared. + Each archive is a zip file that will be unzipped in a location that depends + on its package type. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A collection of file paths available in an <extra> package + that can be installed in an Android project. + If present, the <project-files> collection must contain at least one path. + Each path is relative to the root directory of the package. + + + + + + + + + + + + + + A SHA1 checksum. + + + + + + + + + A file checksum, currently only SHA1. + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-05.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-05.xsd new file mode 100644 index 00000000..546b00d3 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-05.xsd @@ -0,0 +1,442 @@ + + + + + + + + + + + + The repository contains a collection of downloadable packages. + + + + + + + + + + + + + + + An SDK add-on package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An ID string for an addon/extra name-id or vendor-id + can only be simple alphanumeric string. + + + + + + + + + + + + + + Version information for a layoutlib included in an addon. + . + + + + + + + + + + + + + + + + An SDK extra package. This kind of package is for "free" content. + Such packages are installed in SDK/vendor/path. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A full revision, with a major.minor.micro and an + optional preview number. The major number is mandatory. + + + + + + + + + + + + + + + + + + + + + One path segment for the install path of an extra element. + It must be a single-segment path. It must not be empty. + + + + + + + + + + + A semi-colon separated list of a segmentTypes. + + + + + + + + + + + + + + A license definition. Such a license must be used later as a reference + using a uses-license element in one of the package elements. + + + + + + + + + + + + + + + + + Describes the license used by a package. The license MUST be defined + using a license node and referenced using the ref attribute of the + license element inside a package. + + + + + + + + + + + + A collection of files that can be downloaded for a given architecture. + The <archives> node is mandatory in the repository packages and the + collection must have at least one <archive> declared. + Each archive is a zip file that will be unzipped in a location that depends + on its package type. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A collection of file paths available in an <extra> package + that can be installed in an Android project. + If present, the <project-files> collection must contain at least one path. + Each path is relative to the root directory of the package. + + + + + + + + + + + + + + A SHA1 checksum. + + + + + + + + + A file checksum, currently only SHA1. + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-06.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-06.xsd new file mode 100644 index 00000000..3457aada --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-06.xsd @@ -0,0 +1,472 @@ + + + + + + + + + + + + The repository contains a collection of downloadable packages. + + + + + + + + + + + + + + + An SDK add-on package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An ID string for an addon/extra name-id or vendor-id + can only be simple alphanumeric string. + + + + + + + + + + + + + + Version information for a layoutlib included in an addon. + . + + + + + + + + + + + + + + + + An SDK extra package. This kind of package is for "free" content. + Such packages are installed in SDK/vendor/path. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A full revision, with a major.minor.micro and an + optional preview number. The major number is mandatory. + + + + + + + + + + + + + + + + + + + + + A full revision, with a major.minor.micro and no support for + the optional preview number. The major number is mandatory. + + + + + + + + + + + + + + + + + + + One path segment for the install path of an extra element. + It must be a single-segment path. It must not be empty. + + + + + + + + + + + A semi-colon separated list of a segmentTypes. + + + + + + + + + + + + + + A license definition. Such a license must be used later as a reference + using a uses-license element in one of the package elements. + + + + + + + + + + + + + + + + + Describes the license used by a package. The license MUST be defined + using a license node and referenced using the ref attribute of the + license element inside a package. + + + + + + + + + + + + A collection of files that can be downloaded for a given architecture. + The <archives> node is mandatory in the repository packages and the + collection must have at least one <archive> declared. + Each archive is a zip file that will be unzipped in a location that depends + on its package type. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A collection of file paths available in an <extra> package + that can be installed in an Android project. + If present, the <project-files> collection must contain at least one path. + Each path is relative to the root directory of the package. + + + + + + + + + + + + + + A SHA1 checksum. + + + + + + + + + A file checksum, currently only SHA1. + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-07.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-07.xsd new file mode 100644 index 00000000..3c2c13a4 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-07.xsd @@ -0,0 +1,499 @@ + + + + + + + + + + + + The repository contains a collection of downloadable packages. + + + + + + + + + + + + + + + An SDK add-on package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An ID string for an addon/extra name-id or vendor-id + can only be simple alphanumeric string. + + + + + + + + + + + + + + Version information for a layoutlib included in an addon. + . + + + + + + + + + + + + + + + + An SDK extra package. This kind of package is for "free" content. + Such packages are installed in SDK/vendor/path. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A full revision, with a major.minor.micro and an + optional preview number. The major number is mandatory. + + + + + + + + + + + + + + + + + + + + + A full revision, with a major.minor.micro and no support for + the optional preview number. The major number is mandatory. + + + + + + + + + + + + + + + + + + + One path segment for the install path of an extra element. + It must be a single-segment path. It must not be empty. + + + + + + + + + + + A semi-colon separated list of a segmentTypes. + + + + + + + + + + + + + + A license definition. Such a license must be used later as a reference + using a uses-license element in one of the package elements. + + + + + + + + + + + + + + + + + Describes the license used by a package. The license MUST be defined + using a license node and referenced using the ref attribute of the + license element inside a package. + + + + + + + + + + + + A collection of files that can be downloaded for a given architecture. + The <archives> node is mandatory in the repository packages and the + collection must have at least one <archive> declared. + Each archive is a zip file that will be unzipped in a location that depends + on its package type. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A collection of file paths available in an <extra> package + that can be installed in an Android project. + If present, the <project-files> collection must contain at least one path. + Each path is relative to the root directory of the package. + + + + + + + + + + + + + + A CPU bit size filter. + + + + + + + + + + A host OS filter. + + + + + + + + + + + A JVM version number, e.g. "1" or "1.6" or "1.14.15". + + + + + + + + + + + + A SHA1 checksum. + + + + + + + + + A file checksum, currently only SHA1. + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addons-list-1.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addons-list-1.xsd new file mode 100644 index 00000000..176fb608 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addons-list-1.xsd @@ -0,0 +1,71 @@ + + + + + + + + + + + + A simple list of add-ons site. + + + + + + + + + + + + An SDK add-on site. + + + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addons-list-2.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addons-list-2.xsd new file mode 100644 index 00000000..dde72148 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addons-list-2.xsd @@ -0,0 +1,106 @@ + + + + + + + + + + + + A simple list of add-ons site. + + + + + + + + + + + + + An SDK add-on site. + + + + + + + + + + + + + + + + An SDK sys-img site. + + + + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-01.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-01.xsd new file mode 100644 index 00000000..38ec3096 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-01.xsd @@ -0,0 +1,381 @@ + + + + + + + + + + The repository contains collections of downloadable packages. + + + + + + + + + + + An SDK platform package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK add-on package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK tool package. + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK doc package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK extra package. This kind of package is for "free" + content and specifies in which fixed root directory it must be + installed. + The paths "add-ons", "platforms", "tools" and "docs" are + reserved and cannot be used. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A license definition. Such a license must be used later as a reference + using a uses-license element in one of the package elements. + + + + + + + + + + + + + + + + + + + + + + Describes the license used by a package. The license MUST be defined + using a license node and referenced using the ref attribute of the + license element inside a package. + + + + + + + + + + + + A collection of files that can be downloaded for a given architecture. + The <archives> node is mandatory in the repository packages and the + collection must have at least one <archive> declared. + Each archive is a zip file that will be unzipped in a location that depends + on its package type. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A SHA1 checksum. + + + + + + + + + A file checksum, currently only SHA1. + + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-02.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-02.xsd new file mode 100644 index 00000000..ecadc3f9 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-02.xsd @@ -0,0 +1,438 @@ + + + + + + + + + + + + The repository contains a collection of downloadable packages. + + + + + + + + + + + + + + + + + + An SDK platform package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK add-on package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK tool package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK doc package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK sample package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK extra package. This kind of package is for "free" + content and specifies in which fixed root directory it must be + installed. + The paths "add-ons", "platforms", "tools" and "docs" are + reserved and cannot be used. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A license definition. Such a license must be used later as a reference + using a uses-license element in one of the package elements. + + + + + + + + + + + + + + + + + Describes the license used by a package. The license MUST be defined + using a license node and referenced using the ref attribute of the + license element inside a package. + + + + + + + + + + + + A collection of files that can be downloaded for a given architecture. + The <archives> node is mandatory in the repository packages and the + collection must have at least one <archive> declared. + Each archive is a zip file that will be unzipped in a location that depends + on its package type. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A SHA1 checksum. + + + + + + + + + A file checksum, currently only SHA1. + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-03.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-03.xsd new file mode 100644 index 00000000..75d8541f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-03.xsd @@ -0,0 +1,436 @@ + + + + + + + + + + + + The repository contains a collection of downloadable packages. + + + + + + + + + + + + + + + + + + An SDK platform package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK tool package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK platform-tool package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK doc package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK sample package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK extra package. This kind of package is for "free" content. + Such packages are installed in SDK/vendor/path. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + One path segment for the install path of an extra element. + It must be a single-segment path. + + + + + + + + + + + + + + A license definition. Such a license must be used later as a reference + using a uses-license element in one of the package elements. + + + + + + + + + + + + + + + + + Describes the license used by a package. The license MUST be defined + using a license node and referenced using the ref attribute of the + license element inside a package. + + + + + + + + + + + + A collection of files that can be downloaded for a given architecture. + The <archives> node is mandatory in the repository packages and the + collection must have at least one <archive> declared. + Each archive is a zip file that will be unzipped in a location that depends + on its package type. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A SHA1 checksum. + + + + + + + + + A file checksum, currently only SHA1. + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-04.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-04.xsd new file mode 100644 index 00000000..9b14772a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-04.xsd @@ -0,0 +1,500 @@ + + + + + + + + + + + + The repository contains a collection of downloadable packages. + + + + + + + + + + + + + + + + + + An SDK platform package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Version information for a layoutlib included in a platform. + + + + + + + + + + + + + + + + An SDK tool package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK platform-tool package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK doc package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK sample package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK extra package. This kind of package is for "free" content. + Such packages are installed in SDK/vendor/path. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + One path segment for the install path of an extra element. + It must be a single-segment path. + + + + + + + + + + + + + + A license definition. Such a license must be used later as a reference + using a uses-license element in one of the package elements. + + + + + + + + + + + + + + + + + Describes the license used by a package. The license MUST be defined + using a license node and referenced using the ref attribute of the + license element inside a package. + + + + + + + + + + + + A collection of files that can be downloaded for a given architecture. + The <archives> node is mandatory in the repository packages and the + collection must have at least one <archive> declared. + Each archive is a zip file that will be unzipped in a location that depends + on its package type. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A collection of file paths available in an <extra> package + that can be installed in an Android project. + If present, the <project-files> collection must contain at least one path. + Each path is relative to the root directory of the package. + + + + + + + + + + + + + + A SHA1 checksum. + + + + + + + + + A file checksum, currently only SHA1. + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-05.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-05.xsd new file mode 100644 index 00000000..ae8275f0 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-05.xsd @@ -0,0 +1,624 @@ + + + + + + + + + + + + The repository contains a collection of downloadable packages. + + + + + + + + + + + + + + + + + + + + An SDK platform package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Version information for a layoutlib included in a platform. + + + + + + + + + + + + + + + + + System Image for a platform. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The ABI of a platform's system image. + + + + + + + + + + + + + + + Sources for a platform. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK tool package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK platform-tool package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK doc package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK sample package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK extra package. This kind of package is for "free" content. + Such packages are installed in SDK/vendor/path. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + One path segment for the install path of an extra element. + It must be a single-segment path. + + + + + + + + + + + A semi-colon separated list of a segmentTypes. + + + + + + + + + + + + + + A license definition. Such a license must be used later as a reference + using a uses-license element in one of the package elements. + + + + + + + + + + + + + + + + + Describes the license used by a package. The license MUST be defined + using a license node and referenced using the ref attribute of the + license element inside a package. + + + + + + + + + + + + A collection of files that can be downloaded for a given architecture. + The <archives> node is mandatory in the repository packages and the + collection must have at least one <archive> declared. + Each archive is a zip file that will be unzipped in a location that depends + on its package type. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A collection of file paths available in an <extra> package + that can be installed in an Android project. + If present, the <project-files> collection must contain at least one path. + Each path is relative to the root directory of the package. + + + + + + + + + + + + + + A SHA1 checksum. + + + + + + + + + A file checksum, currently only SHA1. + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-06.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-06.xsd new file mode 100644 index 00000000..bccce69d --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-06.xsd @@ -0,0 +1,608 @@ + + + + + + + + + + + + The repository contains a collection of downloadable packages. + + + + + + + + + + + + + + + + + + + An SDK platform package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Version information for a layoutlib included in a platform. + + + + + + + + + + + + + + + + + System Image for a platform. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The ABI of a platform's system image. + + + + + + + + + + + + + + + + Sources for a platform. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK tool package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK platform-tool package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK doc package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK sample package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + One path segment for the install path of an extra element. + It must be a single-segment path. + + + + + + + + + + + A semi-colon separated list of a segmentTypes. + + + + + + + + + + + + + + A license definition. Such a license must be used later as a reference + using a uses-license element in one of the package elements. + + + + + + + + + + + + + + + + + Describes the license used by a package. The license MUST be defined + using a license node and referenced using the ref attribute of the + license element inside a package. + + + + + + + + + + + + A collection of files that can be downloaded for a given architecture. + The <archives> node is mandatory in the repository packages and the + collection must have at least one <archive> declared. + Each archive is a zip file that will be unzipped in a location that depends + on its package type. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A collection of file paths available in an <extra> package + that can be installed in an Android project. + If present, the <project-files> collection must contain at least one path. + Each path is relative to the root directory of the package. + + + + + + + + + + + + + + A SHA1 checksum. + + + + + + + + + A file checksum, currently only SHA1. + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-07.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-07.xsd new file mode 100644 index 00000000..ea180709 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-07.xsd @@ -0,0 +1,612 @@ + + + + + + + + + + + + The repository contains a collection of downloadable packages. + + + + + + + + + + + + + + + + + + + An SDK platform package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Version information for a layoutlib included in a platform. + + + + + + + + + + + + + + + + + System Image for a platform. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The ABI of a platform's system image. + + + + + + + + + + + + + + + + Sources for a platform. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK tool package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK platform-tool package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK doc package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK sample package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + One path segment for the install path of an extra element. + It must be a single-segment path. + + + + + + + + + + + A semi-colon separated list of a segmentTypes. + + + + + + + + + + + + + + A license definition. Such a license must be used later as a reference + using a uses-license element in one of the package elements. + + + + + + + + + + + + + + + + + Describes the license used by a package. The license MUST be defined + using a license node and referenced using the ref attribute of the + license element inside a package. + + + + + + + + + + + + A collection of files that can be downloaded for a given architecture. + The <archives> node is mandatory in the repository packages and the + collection must have at least one <archive> declared. + Each archive is a zip file that will be unzipped in a location that depends + on its package type. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A full revision, with a major.minor.micro and an + optional preview number. The major number is mandatory. + + + + + + + + + + + + + + + + + + + + + A collection of file paths available in an <extra> package + that can be installed in an Android project. + If present, the <project-files> collection must contain at least one path. + Each path is relative to the root directory of the package. + + + + + + + + + + + + + + A SHA1 checksum. + + + + + + + + + A file checksum, currently only SHA1. + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-08.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-08.xsd new file mode 100644 index 00000000..0c4ca630 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-08.xsd @@ -0,0 +1,652 @@ + + + + + + + + + + + + The repository contains a collection of downloadable packages. + + + + + + + + + + + + + + + + + + + + An SDK platform package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Version information for a layoutlib included in a platform. + + + + + + + + + + + + + + + + + System Image for a platform. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The ABI of a platform's system image. + + + + + + + + + + + + + + + + Sources for a platform. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK tool package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK platform-tool package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK build-tool package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK doc package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK sample package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + One path segment for the install path of an extra element. + It must be a single-segment path. + + + + + + + + + + + A semi-colon separated list of a segmentTypes. + + + + + + + + + + + + + + A license definition. Such a license must be used later as a reference + using a uses-license element in one of the package elements. + + + + + + + + + + + + + + + + + Describes the license used by a package. The license MUST be defined + using a license node and referenced using the ref attribute of the + license element inside a package. + + + + + + + + + + + + A collection of files that can be downloaded for a given architecture. + The <archives> node is mandatory in the repository packages and the + collection must have at least one <archive> declared. + Each archive is a zip file that will be unzipped in a location that depends + on its package type. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A full revision, with a major.minor.micro and an + optional preview number. The major number is mandatory. + + + + + + + + + + + + + + + + + + + + + A collection of file paths available in an <extra> package + that can be installed in an Android project. + If present, the <project-files> collection must contain at least one path. + Each path is relative to the root directory of the package. + + + + + + + + + + + + + + A SHA1 checksum. + + + + + + + + + A file checksum, currently only SHA1. + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-09.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-09.xsd new file mode 100644 index 00000000..1b9d68d4 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-09.xsd @@ -0,0 +1,677 @@ + + + + + + + + + + + + The repository contains a collection of downloadable packages. + + + + + + + + + + + + + + + + + + + + + An SDK platform package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Version information for a layoutlib included in a platform. + + + + + + + + + + + + + + + + + System Image for a platform. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A tag string for a system image can only be a simple alphanumeric string. + + + + + + + + + + + + + The ABI of a platform's system image. + + + + + + + + + + + + + + + + Sources for a platform. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK tool package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK platform-tool package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK build-tool package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK doc package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK sample package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + One path segment for the install path of an extra element. + It must be a single-segment path. + + + + + + + + + + + A semi-colon separated list of a segmentTypes. + + + + + + + + + + + + + + A license definition. Such a license must be used later as a reference + using a uses-license element in one of the package elements. + + + + + + + + + + + + + + + + + Describes the license used by a package. The license MUST be defined + using a license node and referenced using the ref attribute of the + license element inside a package. + + + + + + + + + + + + A collection of files that can be downloaded for a given architecture. + The <archives> node is mandatory in the repository packages and the + collection must have at least one <archive> declared. + Each archive is a zip file that will be unzipped in a location that depends + on its package type. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A full revision, with a major.minor.micro and an + optional preview number. The major number is mandatory. + + + + + + + + + + + + + + + + + + + + + A collection of file paths available in an <extra> package + that can be installed in an Android project. + If present, the <project-files> collection must contain at least one path. + Each path is relative to the root directory of the package. + + + + + + + + + + + + + + A SHA1 checksum. + + + + + + + + + A file checksum, currently only SHA1. + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-10.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-10.xsd new file mode 100644 index 00000000..3fc1d929 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-10.xsd @@ -0,0 +1,653 @@ + + + + + + + + + + + + The repository contains a collection of downloadable packages. + + + + + + + + + + + + + + + + + + + + An SDK platform package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Version information for a layoutlib included in a platform. + + + + + + + + + + + + + + + + The ABI of a platform's system image. + + + + + + + + + + + + + + + + Sources for a platform. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK tool package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK platform-tool package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK build-tool package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK doc package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK sample package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + One path segment for the install path of an extra element. + It must be a single-segment path. + + + + + + + + + + + A semi-colon separated list of a segmentTypes. + + + + + + + + + + + + + + A license definition. Such a license must be used later as a reference + using a uses-license element in one of the package elements. + + + + + + + + + + + + + + + + + Describes the license used by a package. The license MUST be defined + using a license node and referenced using the ref attribute of the + license element inside a package. + + + + + + + + + + + + A collection of files that can be downloaded for a given architecture. + The <archives> node is mandatory in the repository packages and the + collection must have at least one <archive> declared. + Each archive is a zip file that will be unzipped in a location that depends + on its package type. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A full revision, with a major.minor.micro and an + optional preview number. The major number is mandatory. + + + + + + + + + + + + + + + + + + + + + A collection of file paths available in an <extra> package + that can be installed in an Android project. + If present, the <project-files> collection must contain at least one path. + Each path is relative to the root directory of the package. + + + + + + + + + + + + + + A CPU bit size filter. + + + + + + + + + + A host OS filter. + + + + + + + + + + + A JVM version number, e.g. "1" or "1.6" or "1.14.15". + + + + + + + + + + + + A SHA1 checksum. + + + + + + + + + A file checksum, currently only SHA1. + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-11.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-11.xsd new file mode 100644 index 00000000..8e9f736a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-11.xsd @@ -0,0 +1,680 @@ + + + + + + + + + + + + The repository contains a collection of downloadable packages. + + + + + + + + + + + + + + + + + + + + An NDK package. + + + + + + + + + + + + + + + + + + + + + + + + An SDK platform package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Version information for a layoutlib included in a platform. + + + + + + + + + + + + + + + + The ABI of a platform's system image. + + + + + + + + + + + + + + + + Sources for a platform. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK tool package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK platform-tool package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK build-tool package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK doc package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An SDK sample package. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + One path segment for the install path of an extra element. + It must be a single-segment path. + + + + + + + + + + + A semi-colon separated list of a segmentTypes. + + + + + + + + + + + + + + A license definition. Such a license must be used later as a reference + using a uses-license element in one of the package elements. + + + + + + + + + + + + + + + + + Describes the license used by a package. The license MUST be defined + using a license node and referenced using the ref attribute of the + license element inside a package. + + + + + + + + + + + + A collection of files that can be downloaded for a given architecture. + The <archives> node is mandatory in the repository packages and the + collection must have at least one <archive> declared. + Each archive is a zip file that will be unzipped in a location that depends + on its package type. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A full revision, with a major.minor.micro and an + optional preview number. The major number is mandatory. + + + + + + + + + + + + + + + + + + + + + A collection of file paths available in an <extra> package + that can be installed in an Android project. + If present, the <project-files> collection must contain at least one path. + Each path is relative to the root directory of the package. + + + + + + + + + + + + + + A CPU bit size filter. + + + + + + + + + + A host OS filter. + + + + + + + + + + + A JVM version number, e.g. "1" or "1.6" or "1.14.15". + + + + + + + + + + + + A SHA1 checksum. + + + + + + + + + A file checksum, currently only SHA1. + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-stats-1.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-stats-1.xsd new file mode 100644 index 00000000..2944b124 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-stats-1.xsd @@ -0,0 +1,96 @@ + + + + + + + + + + + + A simple list of platform stats. + + + + + + + + + + + + + Stats information for a given Android platform. + The api-level acts as a key, and it is epxected there should only + be one platform listed with the same API-level. + + + + + + + + + + + + + + + + + + + + + + + A decimal percentage, between 0.0 and 100.0%. + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-sys-img-01.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-sys-img-01.xsd new file mode 100644 index 00000000..005b4316 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-sys-img-01.xsd @@ -0,0 +1,229 @@ + + + + + + + + + + + + The repository contains a collection of downloadable system images. + + + + + + + + + + + + + + + System Image for a platform. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The ABI of a platform's system image. + + + + + + + + + + + + + + + + A license definition. Such a license must be used later as a reference + using a uses-license element in one of the package elements. + + + + + + + + + + + + + + + + + Describes the license used by a package. The license MUST be defined + using a license node and referenced using the ref attribute of the + license element inside a package. + + + + + + + + + + + + A collection of files that can be downloaded for a given architecture. + The <archives> node is mandatory in the repository packages and the + collection must have at least one <archive> declared. + Each archive is a zip file that will be unzipped in a location that depends + on its package type. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A SHA1 checksum. + + + + + + + + + A file checksum, currently only SHA1. + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-sys-img-02.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-sys-img-02.xsd new file mode 100644 index 00000000..a1c16a32 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-sys-img-02.xsd @@ -0,0 +1,249 @@ + + + + + + + + + + + + The repository contains a collection of downloadable system images. + + + + + + + + + + + + + + + System Image for a platform. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + An tag string for a system image can only be simple alphanumeric string. + + + + + + + + + + + + + The ABI of a platform's system image. + + + + + + + + + + + + + + + + A license definition. Such a license must be used later as a reference + using a uses-license element in one of the package elements. + + + + + + + + + + + + + + + + + Describes the license used by a package. The license MUST be defined + using a license node and referenced using the ref attribute of the + license element inside a package. + + + + + + + + + + + + A collection of files that can be downloaded for a given architecture. + The <archives> node is mandatory in the repository packages and the + collection must have at least one <archive> declared. + Each archive is a zip file that will be unzipped in a location that depends + on its package type. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A SHA1 checksum. + + + + + + + + + A file checksum, currently only SHA1. + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-sys-img-03.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-sys-img-03.xsd new file mode 100644 index 00000000..9b63c010 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-sys-img-03.xsd @@ -0,0 +1,309 @@ + + + + + + + + + + + + The repository contains a collection of downloadable system images. + + + + + + + + + + + + + + + System Image for a platform. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Indicates a system-image is tied to an add-on and which one + (the combo tag-id + vendor-id uniquely identifies the add-on.) + Mandatory for add-on system-images. + Must not be present for platform system-images. + + + + + + + + + + + + + + + An tag string for a system image can only be simple alphanumeric string. + + + + + + + + + + + + + The ABI of a platform's system image. + + + + + + + + + + + + + + + + + + + A license definition. Such a license must be used later as a reference + using a uses-license element in one of the package elements. + + + + + + + + + + + + + + + + + Describes the license used by a package. The license MUST be defined + using a license node and referenced using the ref attribute of the + license element inside a package. + + + + + + + + + + + + A collection of files that can be downloaded for a given architecture. + The <archives> node is mandatory in the repository packages and the + collection must have at least one <archive> declared. + Each archive is a zip file that will be unzipped in a location that depends + on its package type. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A CPU bit size filter. + + + + + + + + + + A host OS filter. + + + + + + + + + + + A JVM version number, e.g. "1" or "1.6" or "1.14.15". + + + + + + + + + + + + A SHA1 checksum. + + + + + + + + + A file checksum, currently only SHA1. + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/util/CommandLineParser.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/util/CommandLineParser.java new file mode 100644 index 00000000..28918dac --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/util/CommandLineParser.java @@ -0,0 +1,976 @@ +/* + * 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.sdklib.util; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.utils.ILogger; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map.Entry; + +/** + * Parses the command-line and stores flags needed or requested. + *

+ * This is a base class. To be useful you want to: + *

    + *
  • override it. + *
  • pass an action array to the constructor. + *
  • define flags for your actions. + *
+ *

+ * To use, call {@link #parseArgs(String[])} and then + * call {@link #getValue(String, String, String)}. + */ +public class CommandLineParser { + + /* + * Steps needed to add a new action: + * - Each action is defined as a "verb object" followed by parameters. + * - Either reuse a VERB_ constant or define a new one. + * - Either reuse an OBJECT_ constant or define a new one. + * - Add a new entry to mAction with a one-line help summary. + * - In the constructor, add a define() call for each parameter (either mandatory + * or optional) for the given action. + */ + + /** Internal verb name for internally hidden flags. */ + public static final String GLOBAL_FLAG_VERB = "@@internal@@"; //$NON-NLS-1$ + + /** String to use when the verb doesn't need any object. */ + public static final String NO_VERB_OBJECT = ""; //$NON-NLS-1$ + + /** The global help flag. */ + public static final String KEY_HELP = "help"; + /** The global verbose flag. */ + public static final String KEY_VERBOSE = "verbose"; + /** The global silent flag. */ + public static final String KEY_SILENT = "silent"; + + /** Verb requested by the user. Null if none specified, which will be an error. */ + private String mVerbRequested; + /** Direct object requested by the user. Can be null. */ + private String mDirectObjectRequested; + + /** + * Action definitions. + *

+ * This list serves two purposes: first it is used to know which verb/object + * actions are acceptable on the command-line; second it provides a summary + * for each action that is printed in the help. + *

+ * Each entry is a string array with: + *

    + *
  • the verb. + *
  • a direct object (use {@link #NO_VERB_OBJECT} if there's no object). + *
  • a description. + *
  • an alternate form for the object (e.g. plural). + *
+ */ + private final String[][] mActions; + + private static final int ACTION_VERB_INDEX = 0; + private static final int ACTION_OBJECT_INDEX = 1; + private static final int ACTION_DESC_INDEX = 2; + private static final int ACTION_ALT_OBJECT_INDEX = 3; + + /** + * The map of all defined arguments. + *

+ * The key is a string "verb/directObject/longName". + */ + private final HashMap mArguments = new HashMap(); + /** Logger */ + private final ILogger mLog; + + /** + * Constructs a new command-line processor. + * + * @param logger An SDK logger object. Must not be null. + * @param actions The list of actions recognized on the command-line. + * See the javadoc of {@link #mActions} for more details. + * + * @see #mActions + */ + public CommandLineParser(ILogger logger, String[][] actions) { + mLog = logger; + mActions = actions; + + /* + * usage should fit in 80 columns, including the space to print the options: + * " -v --verbose 7890123456789012345678901234567890123456789012345678901234567890" + */ + + define(Mode.BOOLEAN, false, GLOBAL_FLAG_VERB, NO_VERB_OBJECT, "v", KEY_VERBOSE, + "Verbose mode, shows errors, warnings and all messages.", + false); + define(Mode.BOOLEAN, false, GLOBAL_FLAG_VERB, NO_VERB_OBJECT, "s", KEY_SILENT, + "Silent mode, shows errors only.", + false); + define(Mode.BOOLEAN, false, GLOBAL_FLAG_VERB, NO_VERB_OBJECT, "h", KEY_HELP, + "Help on a specific command.", + false); + } + + /** + * Indicates if this command-line can work when no verb is specified. + * The default is false, which generates an error when no verb/object is specified. + * Derived implementations can set this to true if they can deal with a lack + * of verb/action. + */ + public boolean acceptLackOfVerb() { + return false; + } + + + //------------------ + // Helpers to get flags values + + /** Helper that returns true if --verbose was requested. */ + public boolean isVerbose() { + return ((Boolean) getValue(GLOBAL_FLAG_VERB, NO_VERB_OBJECT, KEY_VERBOSE)).booleanValue(); + } + + /** Helper that returns true if --silent was requested. */ + public boolean isSilent() { + return ((Boolean) getValue(GLOBAL_FLAG_VERB, NO_VERB_OBJECT, KEY_SILENT)).booleanValue(); + } + + /** Helper that returns true if --help was requested. */ + public boolean isHelpRequested() { + return ((Boolean) getValue(GLOBAL_FLAG_VERB, NO_VERB_OBJECT, KEY_HELP)).booleanValue(); + } + + /** Returns the verb name from the command-line. Can be null. */ + public String getVerb() { + return mVerbRequested; + } + + /** Returns the direct object name from the command-line. Can be null. */ + public String getDirectObject() { + return mDirectObjectRequested; + } + + //------------------ + + /** + * Raw access to parsed parameter values. + *

+ * The default is to scan all parameters. Parameters that have been explicitly set on the + * command line are returned first. Otherwise one with a non-null value is returned. + *

+ * Both a verb and a direct object filter can be specified. When they are non-null they limit + * the scope of the search. + *

+ * If nothing has been found, return the last default value seen matching the filter. + * + * @param verb The verb name, including {@link #GLOBAL_FLAG_VERB}. If null, all possible + * verbs that match the direct object condition will be examined and the first + * value set will be used. + * @param directObject The direct object name, including {@link #NO_VERB_OBJECT}. If null, + * all possible direct objects that match the verb condition will be examined and + * the first value set will be used. + * @param longFlagName The long flag name for the given action. Mandatory. Cannot be null. + * @return The current value object stored in the parameter, which depends on the argument mode. + */ + public Object getValue(String verb, String directObject, String longFlagName) { + + if (verb != null && directObject != null) { + String key = verb + '/' + directObject + '/' + longFlagName; + Arg arg = mArguments.get(key); + return arg.getCurrentValue(); + } + + Object lastDefault = null; + for (Arg arg : mArguments.values()) { + if (arg.getLongArg().equals(longFlagName)) { + if (verb == null || arg.getVerb().equals(verb)) { + if (directObject == null || arg.getDirectObject().equals(directObject)) { + if (arg.isInCommandLine()) { + return arg.getCurrentValue(); + } + if (arg.getCurrentValue() != null) { + lastDefault = arg.getCurrentValue(); + } + } + } + } + } + + return lastDefault; + } + + /** + * Internal setter for raw parameter value. + * @param verb The verb name, including {@link #GLOBAL_FLAG_VERB}. + * @param directObject The direct object name, including {@link #NO_VERB_OBJECT}. + * @param longFlagName The long flag name for the given action. + * @param value The new current value object stored in the parameter, which depends on the + * argument mode. + */ + protected void setValue(String verb, String directObject, String longFlagName, Object value) { + String key = verb + '/' + directObject + '/' + longFlagName; + Arg arg = mArguments.get(key); + arg.setCurrentValue(value); + } + + /** + * Parses the command-line arguments. + *

+ * This method will exit and not return if a parsing error arise. + * + * @param args The arguments typically received by a main method. + */ + public void parseArgs(String[] args) { + String errorMsg = null; + String verb = null; + String directObject = null; + + try { + int n = args.length; + for (int i = 0; i < n; i++) { + Arg arg = null; + String a = args[i]; + if (a.startsWith("--")) { //$NON-NLS-1$ + arg = findLongArg(verb, directObject, a.substring(2)); + } else if (a.startsWith("-")) { //$NON-NLS-1$ + arg = findShortArg(verb, directObject, a.substring(1)); + } + + // No matching argument name found + if (arg == null) { + // Does it looks like a dashed parameter? + if (a.startsWith("-")) { //$NON-NLS-1$ + if (verb == null || directObject == null) { + // It looks like a dashed parameter and we don't have a a verb/object + // set yet, the parameter was just given too early. + + errorMsg = String.format( + "Flag '%1$s' is not a valid global flag. Did you mean to specify it after the verb/object name?", + a); + return; + } else { + // It looks like a dashed parameter but it is unknown by this + // verb-object combination + + errorMsg = String.format( + "Flag '%1$s' is not valid for '%2$s %3$s'.", + a, verb, directObject); + return; + } + } + + if (verb == null) { + // Fill verb first. Find it. + for (String[] actionDesc : mActions) { + if (actionDesc[ACTION_VERB_INDEX].equals(a)) { + verb = a; + break; + } + } + + // Error if it was not a valid verb + if (verb == null) { + errorMsg = String.format( + "Expected verb after global parameters but found '%1$s' instead.", + a); + return; + } + + } else if (directObject == null) { + // Then fill the direct object. Find it. + for (String[] actionDesc : mActions) { + if (actionDesc[ACTION_VERB_INDEX].equals(verb)) { + if (actionDesc[ACTION_OBJECT_INDEX].equals(a)) { + directObject = a; + break; + } else if (actionDesc.length > ACTION_ALT_OBJECT_INDEX && + actionDesc[ACTION_ALT_OBJECT_INDEX].equals(a)) { + // if the alternate form exist and is used, we internally + // only memorize the default direct object form. + directObject = actionDesc[ACTION_OBJECT_INDEX]; + break; + } + } + } + + // Error if it was not a valid object for that verb + if (directObject == null) { + errorMsg = String.format( + "Expected verb after global parameters but found '%1$s' instead.", + a); + return; + + } + } else { + // The argument is not a dashed parameter and we already + // have a verb/object. Must be some extra unknown argument. + errorMsg = String.format( + "Argument '%1$s' is not recognized.", + a); + } + } else if (arg != null) { + // This argument was present on the command line + arg.setInCommandLine(true); + + // Process keyword + Object error = null; + if (arg.getMode().needsExtra()) { + if (i+1 >= n) { + errorMsg = String.format("Missing argument for flag %1$s.", a); + return; + } + + while (i+1 < n) { + String b = args[i+1]; + + if (arg.getMode() != Mode.STRING_ARRAY) { + // We never accept something that looks like a valid argument + // unless we see -- first + Arg dummyArg = null; + if (b.startsWith("--")) { //$NON-NLS-1$ + dummyArg = findLongArg(verb, directObject, b.substring(2)); + } else if (b.startsWith("-")) { //$NON-NLS-1$ + dummyArg = findShortArg(verb, directObject, b.substring(1)); + } + if (dummyArg != null) { + errorMsg = String.format( + "Oops, it looks like you didn't provide an argument for '%1$s'.\n'%2$s' was found instead.", + a, b); + return; + } + } + + error = arg.getMode().process(arg, b); + if (error == Accept.CONTINUE) { + i++; + } else if (error == Accept.ACCEPT_AND_STOP) { + i++; + break; + } else if (error == Accept.REJECT_AND_STOP) { + break; + } else if (error instanceof String) { + // We stop because of an error + break; + } + } + } else { + error = arg.getMode().process(arg, null); + + if (isHelpRequested()) { + // The --help flag was requested. We'll continue the usual processing + // so that we can find the optional verb/object words. Those will be + // used to print specific help. + // Setting a non-null error message triggers printing the help, however + // there is no specific error to print. + errorMsg = ""; //$NON-NLS-1$ + } + } + + if (error instanceof String) { + errorMsg = String.format("Invalid usage for flag %1$s: %2$s.", a, error); + return; + } + } + } + + if (errorMsg == null) { + if (verb == null && !acceptLackOfVerb()) { + errorMsg = "Missing verb name."; + } else if (verb != null) { + if (directObject == null) { + // Make sure this verb has an optional direct object + for (String[] actionDesc : mActions) { + if (actionDesc[ACTION_VERB_INDEX].equals(verb) && + actionDesc[ACTION_OBJECT_INDEX].equals(NO_VERB_OBJECT)) { + directObject = NO_VERB_OBJECT; + break; + } + } + + if (directObject == null) { + errorMsg = String.format("Missing object name for verb '%1$s'.", verb); + return; + } + } + + // Validate that all mandatory arguments are non-null for this action + String missing = null; + boolean plural = false; + for (Entry entry : mArguments.entrySet()) { + Arg arg = entry.getValue(); + if (arg.getVerb().equals(verb) && + arg.getDirectObject().equals(directObject)) { + if (arg.isMandatory() && arg.getCurrentValue() == null) { + if (missing == null) { + missing = "--" + arg.getLongArg(); //$NON-NLS-1$ + } else { + missing += ", --" + arg.getLongArg(); //$NON-NLS-1$ + plural = true; + } + } + } + } + + if (missing != null) { + errorMsg = String.format( + "The %1$s %2$s must be defined for action '%3$s %4$s'", + plural ? "parameters" : "parameter", + missing, + verb, + directObject); + } + + mVerbRequested = verb; + mDirectObjectRequested = directObject; + } + } + } finally { + if (errorMsg != null) { + printHelpAndExitForAction(verb, directObject, errorMsg); + } + } + } + + /** + * Finds an {@link Arg} given an action name and a long flag name. + * @return The {@link Arg} found or null. + */ + protected Arg findLongArg(String verb, String directObject, String longName) { + if (verb == null) { + verb = GLOBAL_FLAG_VERB; + } + if (directObject == null) { + directObject = NO_VERB_OBJECT; + } + String key = verb + '/' + directObject + '/' + longName; //$NON-NLS-1$ + return mArguments.get(key); + } + + /** + * Finds an {@link Arg} given an action name and a short flag name. + * @return The {@link Arg} found or null. + */ + protected Arg findShortArg(String verb, String directObject, String shortName) { + if (verb == null) { + verb = GLOBAL_FLAG_VERB; + } + if (directObject == null) { + directObject = NO_VERB_OBJECT; + } + + for (Entry entry : mArguments.entrySet()) { + Arg arg = entry.getValue(); + if (arg.getVerb().equals(verb) && arg.getDirectObject().equals(directObject)) { + if (shortName.equals(arg.getShortArg())) { + return arg; + } + } + } + + return null; + } + + /** + * Prints the help/usage and exits. + * + * @param errorFormat Optional error message to print prior to usage using String.format + * @param args Arguments for String.format + */ + public void printHelpAndExit(String errorFormat, Object... args) { + printHelpAndExitForAction(null /*verb*/, null /*directObject*/, errorFormat, args); + } + + /** + * Prints the help/usage and exits. + * + * @param verb If null, displays help for all verbs. If not null, display help only + * for that specific verb. In all cases also displays general usage and action list. + * @param directObject If null, displays help for all verb objects. + * If not null, displays help only for that specific action + * In all cases also display general usage and action list. + * @param errorFormat Optional error message to print prior to usage using String.format + * @param args Arguments for String.format + */ + public void printHelpAndExitForAction(String verb, String directObject, + String errorFormat, Object... args) { + if (errorFormat != null && !errorFormat.isEmpty()) { + stderr(errorFormat, args); + } + + /* + * usage should fit in 80 columns + * 12345678901234567890123456789012345678901234567890123456789012345678901234567890 + */ + stdout("\n" + + "Usage:\n" + + " android [global options] %s [action options]\n" + + "\n" + + "Global options:", + verb == null ? "action" : + verb + (directObject == null ? "" : " " + directObject)); //$NON-NLS-1$ + listOptions(GLOBAL_FLAG_VERB, NO_VERB_OBJECT); + + if (verb == null || directObject == null) { + stdout("\nValid actions are composed of a verb and an optional direct object:"); + for (String[] action : mActions) { + if (verb == null || verb.equals(action[ACTION_VERB_INDEX])) { + stdout("- %1$6s %2$-13s: %3$s", + action[ACTION_VERB_INDEX], + action[ACTION_OBJECT_INDEX], + action[ACTION_DESC_INDEX]); + } + } + } + + // Only print details if a verb/object is requested + if (verb != null) { + for (String[] action : mActions) { + if (verb == null || verb.equals(action[ACTION_VERB_INDEX])) { + if (directObject == null || directObject.equals(action[ACTION_OBJECT_INDEX])) { + stdout("\nAction \"%1$s %2$s\":", + action[ACTION_VERB_INDEX], + action[ACTION_OBJECT_INDEX]); + stdout(" %1$s", action[ACTION_DESC_INDEX]); + stdout("Options:"); + listOptions(action[ACTION_VERB_INDEX], action[ACTION_OBJECT_INDEX]); + } + } + } + } + + exit(); + } + + /** + * Internal helper to print all the option flags for a given action name. + */ + protected void listOptions(String verb, String directObject) { + int numOptions = 0; + int longArgLen = 8; + + for (Entry entry : mArguments.entrySet()) { + Arg arg = entry.getValue(); + if (arg.getVerb().equals(verb) && arg.getDirectObject().equals(directObject)) { + int n = arg.getLongArg().length(); + if (n > longArgLen) { + longArgLen = n; + } + } + } + + for (Entry entry : mArguments.entrySet()) { + Arg arg = entry.getValue(); + if (arg.getVerb().equals(verb) && arg.getDirectObject().equals(directObject)) { + + String value = ""; //$NON-NLS-1$ + String required = ""; //$NON-NLS-1$ + if (arg.isMandatory()) { + required = " [required]"; + + } else { + if (arg.getDefaultValue() instanceof String[]) { + for (String v : (String[]) arg.getDefaultValue()) { + if (!value.isEmpty()) { + value += ", "; + } + value += v; + } + } else if (arg.getDefaultValue() != null) { + Object v = arg.getDefaultValue(); + if (arg.getMode() != Mode.BOOLEAN || v.equals(Boolean.TRUE)) { + value = v.toString(); + } + } + if (!value.isEmpty()) { + value = " [Default: " + value + "]"; + } + } + + // Java doesn't support * for printf variable width, so we'll insert the long arg + // width "manually" in the printf format string. + String longArgWidth = Integer.toString(longArgLen + 2); + + // Print a line in the form " -1_letter_arg --long_arg description" + // where either the 1-letter arg or the long arg are optional. + String output = String.format( + " %1$-2s %2$-" + longArgWidth + "s: %3$s%4$s%5$s", //$NON-NLS-1$ //$NON-NLS-2$ + !arg.getShortArg().isEmpty() ? + "-" + arg.getShortArg() : //$NON-NLS-1$ + "", //$NON-NLS-1$ + !arg.getLongArg().isEmpty() ? + "--" + arg.getLongArg() : //$NON-NLS-1$ + "", //$NON-NLS-1$ + arg.getDescription(), + value, + required); + stdout(output); + numOptions++; + } + } + + if (numOptions == 0) { + stdout(" No options"); + } + } + + //---- + + protected enum Accept { + CONTINUE, + ACCEPT_AND_STOP, + REJECT_AND_STOP, + } + + /** + * The mode of an argument specifies the type of variable it represents, + * whether an extra parameter is required after the flag and how to parse it. + */ + protected enum Mode { + /** Argument value is a Boolean. Default value is a Boolean. */ + BOOLEAN { + @Override + public boolean needsExtra() { + return false; + } + @Override + public Object process(Arg arg, String extra) { + // Toggle the current value + arg.setCurrentValue(! ((Boolean) arg.getCurrentValue()).booleanValue()); + return Accept.ACCEPT_AND_STOP; + } + }, + + /** Argument value is an Integer. Default value is an Integer. */ + INTEGER { + @Override + public boolean needsExtra() { + return true; + } + @Override + public Object process(Arg arg, String extra) { + try { + arg.setCurrentValue(Integer.parseInt(extra)); + return null; + } catch (NumberFormatException e) { + return String.format("Failed to parse '%1$s' as an integer: %2$s", extra, + e.getMessage()); + } + } + }, + + /** Argument value is a String. Default value is a String[]. */ + ENUM { + @Override + public boolean needsExtra() { + return true; + } + @Override + public Object process(Arg arg, String extra) { + StringBuilder desc = new StringBuilder(); + String[] values = (String[]) arg.getDefaultValue(); + for (String value : values) { + if (value.equals(extra)) { + arg.setCurrentValue(extra); + return Accept.ACCEPT_AND_STOP; + } + + if (desc.length() != 0) { + desc.append(", "); + } + desc.append(value); + } + + return String.format("'%1$s' is not one of %2$s", extra, desc.toString()); + } + }, + + /** Argument value is a String. Default value is a null. */ + STRING { + @Override + public boolean needsExtra() { + return true; + } + @Override + public Object process(Arg arg, String extra) { + arg.setCurrentValue(extra); + return Accept.ACCEPT_AND_STOP; + } + }, + + /** Argument value is a {@link List}<String>. Default value is an empty list. */ + STRING_ARRAY { + @Override + public boolean needsExtra() { + return true; + } + @Override + public Object process(Arg arg, String extra) { + // For simplification, a string array doesn't accept something that + // starts with a dash unless a pure -- was seen before. + if (extra != null) { + Object v = arg.getCurrentValue(); + if (v == null) { + ArrayList a = new ArrayList(); + arg.setCurrentValue(a); + v = a; + } + if (v instanceof List) { + @SuppressWarnings("unchecked") List a = (List) v; + + if (extra.equals("--") || + !extra.startsWith("-") || + (extra.startsWith("-") && a.contains("--"))) { + a.add(extra); + return Accept.CONTINUE; + } else if (a.isEmpty()) { + return "No values provided"; + } + } + } + return Accept.REJECT_AND_STOP; + } + }; + + /** + * Returns true if this mode requires an extra parameter. + */ + public abstract boolean needsExtra(); + + /** + * Processes the flag for this argument. + * + * @param arg The argument being processed. + * @param extra The extra parameter. Null if {@link #needsExtra()} returned false. + * @return {@link Accept#CONTINUE} if this argument can use multiple values and + * wishes to receive more. + * Or {@link Accept#ACCEPT_AND_STOP} if this was the last value accepted by the argument. + * Or {@link Accept#REJECT_AND_STOP} if this was value was reject and the argument + * stops accepting new values with no error. + * Or a string in case of error. + * Never returns null. + */ + public abstract Object process(Arg arg, String extra); + } + + /** + * An argument accepted by the command-line, also called "a flag". + * Arguments must have a short version (one letter), a long version name and a description. + * They can have a default value, or it can be null. + * Depending on the {@link Mode}, the default value can be a Boolean, an Integer, a String + * or a String array (in which case the first item is the current by default.) + */ + protected static class Arg { + /** Verb for that argument. Never null. */ + private final String mVerb; + /** Direct Object for that argument. Never null, but can be empty string. */ + private final String mDirectObject; + /** The 1-letter short name of the argument, e.g. -v. */ + private final String mShortName; + /** The long name of the argument, e.g. --verbose. */ + private final String mLongName; + /** A description. Never null. */ + private final String mDescription; + /** A default value. Can be null. */ + private final Object mDefaultValue; + /** The argument mode (type + process method). Never null. */ + private final Mode mMode; + /** True if this argument is mandatory for this verb/directobject. */ + private final boolean mMandatory; + /** Current value. Initially set to the default value. */ + private Object mCurrentValue; + /** True if the argument has been used on the command line. */ + private boolean mInCommandLine; + + /** + * Creates a new argument flag description. + * + * @param mode The {@link Mode} for the argument. + * @param mandatory True if this argument is mandatory for this action. + * @param verb The verb name. Never null. Can be {@link CommandLineParser#GLOBAL_FLAG_VERB}. + * @param directObject The action name. Can be {@link CommandLineParser#NO_VERB_OBJECT}. + * @param shortName The one-letter short argument name. Can be empty but not null. + * @param longName The long argument name. Can be empty but not null. + * @param description The description. Cannot be null. + * @param defaultValue The default value (or values), which depends on the selected + * {@link Mode}. Can be null. + */ + public Arg(Mode mode, + boolean mandatory, + @NonNull String verb, + @NonNull String directObject, + @NonNull String shortName, + @NonNull String longName, + @NonNull String description, + @Nullable Object defaultValue) { + mMode = mode; + mMandatory = mandatory; + mVerb = verb; + mDirectObject = directObject; + mShortName = shortName; + mLongName = longName; + mDescription = description; + mDefaultValue = defaultValue; + mInCommandLine = false; + if (defaultValue instanceof String[]) { + mCurrentValue = ((String[])defaultValue)[0]; + } else { + mCurrentValue = mDefaultValue; + } + } + + /** Return true if this argument is mandatory for this verb/directobject. */ + public boolean isMandatory() { + return mMandatory; + } + + /** Returns the 1-letter short name of the argument, e.g. -v. */ + public String getShortArg() { + return mShortName; + } + + /** Returns the long name of the argument, e.g. --verbose. */ + public String getLongArg() { + return mLongName; + } + + /** Returns the description. Never null. */ + public String getDescription() { + return mDescription; + } + + /** Returns the verb for that argument. Never null. */ + public String getVerb() { + return mVerb; + } + + /** Returns the direct Object for that argument. Never null, but can be empty string. */ + public String getDirectObject() { + return mDirectObject; + } + + /** Returns the default value. Can be null. */ + public Object getDefaultValue() { + return mDefaultValue; + } + + /** Returns the current value. Initially set to the default value. Can be null. */ + public Object getCurrentValue() { + return mCurrentValue; + } + + /** Sets the current value. Can be null. */ + public void setCurrentValue(Object currentValue) { + mCurrentValue = currentValue; + } + + /** Returns the argument mode (type + process method). Never null. */ + public Mode getMode() { + return mMode; + } + + /** Returns true if the argument has been used on the command line. */ + public boolean isInCommandLine() { + return mInCommandLine; + } + + /** Sets if the argument has been used on the command line. */ + public void setInCommandLine(boolean inCommandLine) { + mInCommandLine = inCommandLine; + } + } + + /** + * Internal helper to define a new argument for a give action. + * + * @param mode The {@link Mode} for the argument. + * @param mandatory The argument is required (never if {@link Mode#BOOLEAN}) + * @param verb The verb name. Never null. Can be {@link CommandLineParser#GLOBAL_FLAG_VERB}. + * @param directObject The action name. Can be {@link CommandLineParser#NO_VERB_OBJECT}. + * @param shortName The one-letter short argument name. Can be empty but not null. + * @param longName The long argument name. Can be empty but not null. + * @param description The description. Cannot be null. + * @param defaultValue The default value (or values), which depends on the selected + * {@link Mode}. + */ + protected void define(Mode mode, + boolean mandatory, + @NonNull String verb, + @NonNull String directObject, + @NonNull String shortName, + @NonNull String longName, + @NonNull String description, + @Nullable Object defaultValue) { + assert verb != null; + assert(!(mandatory && mode == Mode.BOOLEAN)); // a boolean mode cannot be mandatory + + // We should always have at least a short or long name, ideally both but never none. + assert shortName != null; + assert longName != null; + assert !shortName.isEmpty() || !longName.isEmpty(); + + if (directObject == null) { + directObject = NO_VERB_OBJECT; + } + + String key = verb + '/' + directObject + '/' + longName; + mArguments.put(key, new Arg(mode, mandatory, + verb, directObject, shortName, longName, description, defaultValue)); + } + + /** + * Exits in case of error. + * This is protected so that it can be overridden in unit tests. + */ + protected void exit() { + System.exit(1); + } + + /** + * Prints a line to stdout. + * This is protected so that it can be overridden in unit tests. + * + * @param format The string to be formatted. Cannot be null. + * @param args Format arguments. + */ + protected void stdout(String format, Object...args) { + String output = String.format(format, args); + output = LineUtil.reflowLine(output); + mLog.info("%s\n", output); //$NON-NLS-1$ + } + + /** + * Prints a line to stderr. + * This is protected so that it can be overridden in unit tests. + * + * @param format The string to be formatted. Cannot be null. + * @param args Format arguments. + */ + protected void stderr(String format, Object...args) { + mLog.error(null, format, args); + } + + /** + * Returns the logger object. + * @return the logger object. + */ + protected ILogger getLog() { + return mLog; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/util/FormatUtils.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/util/FormatUtils.java new file mode 100644 index 00000000..0ff5e693 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/util/FormatUtils.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2012 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.sdklib.util; + +import com.android.annotations.NonNull; + +/** + * Helper methods to do some format conversions. + */ +public abstract class FormatUtils { + + /** + * Converts a byte size to a human readable string, + * for example "3 MiB", "1020 Bytes" or "1.2 GiB". + * + * @param size The byte size to convert. + * @return A new non-null string, with the size expressed in either Bytes + * or KiB or MiB or GiB. + */ + @NonNull + public static String byteSizeToString(long size) { + String sizeStr; + + if (size < 1024) { + sizeStr = String.format("%d Bytes", size); + } else if (size < 1024 * 1024) { + sizeStr = String.format("%d KiB", Math.round(size / 1024.0)); + } else if (size < 1024 * 1024 * 1024) { + sizeStr = String.format("%.1f MiB", + Math.round(10.0 * size / (1024 * 1024.0))/ 10.0); + } else { + sizeStr = String.format("%.1f GiB", + Math.round(10.0 * size / (1024 * 1024 * 1024.0))/ 10.0); + } + + return sizeStr; + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/util/LineUtil.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/util/LineUtil.java new file mode 100644 index 00000000..c42bd0dd --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/util/LineUtil.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2011 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.sdklib.util; + + +public abstract class LineUtil { + + /** + * Reformats a line so that it fits in 78 characters max. + *

+ * When wrapping the second line and following, prefix the string with a number of + * spaces. This will use the first colon (:) to determine the prefix size + * or use 4 as a minimum if there are no colons in the string. + * + * @param line The line to reflow. Must be non-null. + * @return A new line to print as-is, that contains \n as needed. + */ + public static String reflowLine(String line) { + final int maxLen = 78; + + // Most of time the line will fit in the given length and this will be a no-op + int n = line.length(); + int cr = line.indexOf('\n'); + if (n <= maxLen && (cr == -1 || cr == n - 1)) { + return line; + } + + int prefixSize = line.indexOf(':') + 1; + // If there' some spacing after the colon, use the same when wrapping + if (prefixSize > 0 && prefixSize < maxLen) { + while(prefixSize < n && line.charAt(prefixSize) == ' ') { + prefixSize++; + } + } else { + prefixSize = 4; + } + String prefix = String.format( + "%-" + Integer.toString(prefixSize) + "s", //$NON-NLS-1$ //$NON-NLS-2$ + " "); //$NON-NLS-1$ + + StringBuilder output = new StringBuilder(n + prefixSize); + + while (n > 0) { + cr = line.indexOf('\n'); + if (n <= maxLen && (cr == -1 || cr == n - 1)) { + output.append(line); + break; + } + + // Line is longer than the max length, find the first character before and after + // the whitespace where we want to break the line. + int posNext = maxLen; + if (cr != -1 && cr != n - 1 && cr <= posNext) { + posNext = cr + 1; + while (posNext < n && line.charAt(posNext) == '\n') { + posNext++; + } + } + while (posNext < n && line.charAt(posNext) == ' ') { + posNext++; + } + while (posNext > 0) { + char c = line.charAt(posNext - 1); + if (c != ' ' && c != '\n') { + posNext--; + } else { + break; + } + } + + if (posNext == 0 || (posNext >= n && maxLen < n)) { + // We found no whitespace separator. This should generally not occur. + posNext = maxLen; + } + int posPrev = posNext; + while (posPrev > 0) { + char c = line.charAt(posPrev - 1); + if (c == ' ' || c == '\n') { + posPrev--; + } else { + break; + } + } + + output.append(line.substring(0, posPrev)).append('\n'); + line = prefix + line.substring(posNext); + n = line.length(); + } + + return output.toString(); + } + + /** + * Formats the string using {@link String#format(String, Object...)} + * and then returns the result of {@link #reflowLine(String)}. + * + * @param format The string format. + * @param params The parameters for the string format. + * @return The result of {@link #reflowLine(String)} on the formatted string. + */ + public static String reformatLine(String format, Object...params) { + return reflowLine(String.format(format, params)); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/AboutDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/AboutDialog.java new file mode 100644 index 00000000..9220305a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/AboutDialog.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2012 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.sdkuilib.internal.repository; + + +import com.android.SdkConstants; +import com.android.sdklib.io.LegacyFileOp; +import com.android.sdklib.repository.PkgProps; +import com.android.sdklib.repository.SdkAddonConstants; +import com.android.sdklib.repository.SdkRepoConstants; +import com.android.sdkuilib.internal.repository.icons.ImageFactory; +import com.android.sdkuilib.ui.GridDataBuilder; +import com.android.sdkuilib.ui.GridLayoutBuilder; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Properties; + +public class AboutDialog extends UpdaterBaseDialog { + + public AboutDialog(Shell parentShell, SwtUpdaterData swtUpdaterData) { + super(parentShell, swtUpdaterData, "About" /*title*/); + assert swtUpdaterData != null; + } + + @Override + protected void createContents() { + super.createContents(); + Shell shell = getShell(); + shell.setMinimumSize(new Point(450, 150)); + shell.setSize(450, 150); + + GridLayoutBuilder.create(shell).columns(3); + + Label logo = new Label(shell, SWT.NONE); + ImageFactory imgf = getSwtUpdaterData() == null ? null + : getSwtUpdaterData().getImageFactory(); + Image image = imgf == null ? null : imgf.getImageByName("sdkman_logo_128.png"); + if (image != null) logo.setImage(image); + + Label label = new Label(shell, SWT.NONE); + GridDataBuilder.create(label).hFill().hGrab().hSpan(2);; + label.setText(String.format( + "Android SDK Manager.\n" + + "Revision %1$s\n" + + "Add-on XML Schema #%2$d\n" + + "Repository XML Schema #%3$d\n" + + // TODO: update with new year date (search this to find other occurrences to update) + "Copyright (C) 2009-2012 The Android Open Source Project.", + getRevision(), + SdkAddonConstants.NS_LATEST_VERSION, + SdkRepoConstants.NS_LATEST_VERSION)); + + Label filler = new Label(shell, SWT.NONE); + GridDataBuilder.create(filler).fill().grab().hSpan(2); + + createCloseButton(); + } + + @Override + protected void checkSubclass() { + // Disable the check that prevents subclassing of SWT components + } + + // -- Start of internal part ---------- + // Hide everything down-below from SWT designer + //$hide>>$ + + // End of hiding from SWT Designer + //$hide<<$ + + private String getRevision() { + Properties p = new Properties(); + try{ + File sourceProp = LegacyFileOp.append(getSwtUpdaterData().getOsSdkRoot(), + SdkConstants.FD_TOOLS, + SdkConstants.FN_SOURCE_PROP); + FileInputStream fis = null; + try { + fis = new FileInputStream(sourceProp); + p.load(fis); + } finally { + if (fis != null) { + try { + fis.close(); + } catch (IOException ignore) { + } + } + } + + String revision = p.getProperty(PkgProps.PKG_REVISION); + if (revision != null) { + return revision; + } + } catch (IOException e) { + } + + return "?"; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ISdkUpdaterWindow.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ISdkUpdaterWindow.java new file mode 100644 index 00000000..88a9ac13 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ISdkUpdaterWindow.java @@ -0,0 +1,42 @@ +/* + * 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.sdkuilib.internal.repository; + +import com.android.sdklib.repository.ISdkChangeListener; + +/** + * Interface for the actual implementation of the Update Window. + */ +public interface ISdkUpdaterWindow { + + /** + * Adds a new listener to be notified when a change is made to the content of the SDK. + */ + public abstract void addListener(ISdkChangeListener listener); + + /** + * Removes a new listener to be notified anymore when a change is made to the content of + * the SDK. + */ + public abstract void removeListener(ISdkChangeListener listener); + + /** + * Opens the window. + */ + public abstract void open(); + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ISwtUpdaterData.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ISwtUpdaterData.java new file mode 100644 index 00000000..916423e0 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ISwtUpdaterData.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2013 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.sdkuilib.internal.repository; + +import com.android.sdklib.internal.repository.updater.IUpdaterData; +import com.android.sdklib.internal.repository.updater.UpdaterData; +import com.android.sdkuilib.internal.repository.icons.ImageFactory; + +import org.eclipse.swt.widgets.Shell; + + +/** + * Interface used to retrieve some parameters from an {@link UpdaterData} instance. + * Useful mostly for unit tests purposes. + */ +interface ISwtUpdaterData extends IUpdaterData { + + public abstract ImageFactory getImageFactory(); + + public abstract Shell getWindowShell(); + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/MenuBarWrapper.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/MenuBarWrapper.java new file mode 100644 index 00000000..feea181f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/MenuBarWrapper.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2011 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.sdkuilib.internal.repository; + + +import com.android.menubar.IMenuBarCallback; +import com.android.menubar.MenuBarEnhancer; +import com.android.sdkuilib.internal.repository.ui.SdkUpdaterWindowImpl2; + +import org.eclipse.swt.widgets.Menu; + +/** + * A simple wrapper/delegate around the {@link MenuBarEnhancer}. + * + * The {@link MenuBarEnhancer} and {@link IMenuBarCallback} classes are only + * available when the SwtMenuBar library is available too. This wrapper helps + * {@link SdkUpdaterWindowImpl2} make the call conditional, otherwise the updater + * window class would fail to load when the SwtMenuBar library isn't found. + */ +public abstract class MenuBarWrapper { + + public MenuBarWrapper(String appName, Menu menu) { + MenuBarEnhancer.setupMenu(appName, menu, new IMenuBarCallback() { + @Override + public void onPreferencesMenuSelected() { + MenuBarWrapper.this.onPreferencesMenuSelected(); + } + + @Override + public void onAboutMenuSelected() { + MenuBarWrapper.this.onAboutMenuSelected(); + } + + @Override + public void printError(String format, Object... args) { + MenuBarWrapper.this.printError(format, args); + } + }); + } + + abstract public void onPreferencesMenuSelected(); + + abstract public void onAboutMenuSelected(); + + abstract public void printError(String format, Object... args); +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SdkUpdaterChooserDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SdkUpdaterChooserDialog.java new file mode 100644 index 00000000..002011ba --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SdkUpdaterChooserDialog.java @@ -0,0 +1,1130 @@ +/* + * 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.sdkuilib.internal.repository; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.internal.repository.archives.Archive; +import com.android.sdklib.internal.repository.packages.IAndroidVersionProvider; +import com.android.sdklib.internal.repository.packages.Package; +import com.android.sdklib.internal.repository.sources.SdkSource; +import com.android.sdklib.internal.repository.updater.ArchiveInfo; +import com.android.sdklib.internal.repository.updater.SdkUpdaterLogic; +import com.android.sdklib.repository.FullRevision; +import com.android.sdklib.repository.License; +import com.android.sdkuilib.internal.repository.icons.ImageFactory; +import com.android.sdkuilib.ui.GridDialog; + +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.LabelProvider; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.SashForm; +import org.eclipse.swt.custom.StyleRange; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.events.ControlAdapter; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +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.Control; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Link; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeColumn; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TreeMap; + + +/** + * Implements an {@link SdkUpdaterChooserDialog}. + */ +final class SdkUpdaterChooserDialog extends GridDialog { + + /** Last dialog size for this session. */ + private static Point sLastSize; + /** Precomputed flag indicating whether the "accept license" radio is checked. */ + private boolean mAcceptSameAllLicense; + private boolean mInternalLicenseRadioUpdate; + + // UI fields + private SashForm mSashForm; + private Composite mPackageRootComposite; + private TreeViewer mTreeViewPackage; + private Tree mTreePackage; + private TreeColumn mTreeColum; + private StyledText mPackageText; + private Button mLicenseRadioAccept; + private Button mLicenseRadioReject; + private Button mLicenseRadioAcceptLicense; + private Group mPackageTextGroup; + private final SwtUpdaterData mSwtUpdaterData; + private Group mTableGroup; + private Label mErrorLabel; + + /** + * List of all archives to be installed with dependency information. + *

+ * Note: in a lot of cases, we need to find the archive info for a given archive. This + * is currently done using a simple linear search, which is fine since we only have a very + * limited number of archives to deal with (e.g. < 10 now). We might want to revisit + * this later if it becomes an issue. Right now just do the simple thing. + *

+ * Typically we could add a map Archive=>ArchiveInfo later. + */ + private final Collection mArchives; + + + + /** + * Create the dialog. + * + * @param parentShell The shell to use, typically updaterData.getWindowShell() + * @param swtUpdaterData The updater data + * @param archives The archives to be installed + */ + public SdkUpdaterChooserDialog(Shell parentShell, + SwtUpdaterData swtUpdaterData, + Collection archives) { + super(parentShell, 3, false/*makeColumnsEqual*/); + mSwtUpdaterData = swtUpdaterData; + mArchives = archives; + } + + @Override + protected boolean isResizable() { + return true; + } + + /** + * Returns the results, i.e. the list of selected new archives to install. + * This is similar to the {@link ArchiveInfo} list instance given to the constructor + * except only accepted archives are present. + *

+ * An empty list is returned if cancel was chosen. + */ + public ArrayList getResult() { + ArrayList ais = new ArrayList(); + + if (getReturnCode() == Window.OK) { + for (ArchiveInfo ai : mArchives) { + if (ai.isAccepted()) { + ais.add(ai); + } + } + } + + return ais; + } + + /** + * Create the main content of the dialog. + * See also {@link #createButtonBar(Composite)} below. + */ + @Override + public void createDialogContent(Composite parent) { + // Sash form + mSashForm = new SashForm(parent, SWT.NONE); + mSashForm.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 3, 1)); + + + // Left part of Sash Form + + mTableGroup = new Group(mSashForm, SWT.NONE); + mTableGroup.setText("Packages"); + mTableGroup.setLayout(new GridLayout(1, false/*makeColumnsEqual*/)); + + mTreeViewPackage = new TreeViewer(mTableGroup, SWT.BORDER | SWT.V_SCROLL | SWT.SINGLE); + mTreePackage = mTreeViewPackage.getTree(); + mTreePackage.setHeaderVisible(false); + mTreePackage.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1)); + + mTreePackage.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + onPackageSelected(); //$hide$ + } + @Override + public void widgetDefaultSelected(SelectionEvent e) { + onPackageDoubleClick(); + } + }); + + mTreeColum = new TreeColumn(mTreePackage, SWT.NONE); + mTreeColum.setWidth(100); + mTreeColum.setText("Packages"); + + // Right part of Sash form + + mPackageRootComposite = new Composite(mSashForm, SWT.NONE); + mPackageRootComposite.setLayout(new GridLayout(4, false/*makeColumnsEqual*/)); + mPackageRootComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + + mPackageTextGroup = new Group(mPackageRootComposite, SWT.NONE); + mPackageTextGroup.setText("Package Description && License"); + mPackageTextGroup.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 4, 1)); + mPackageTextGroup.setLayout(new GridLayout(1, false/*makeColumnsEqual*/)); + + mPackageText = new StyledText(mPackageTextGroup, + SWT.MULTI | SWT.READ_ONLY | SWT.WRAP | SWT.V_SCROLL); + mPackageText.setBackground( + getParentShell().getDisplay().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND)); + mPackageText.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1)); + + mLicenseRadioAccept = new Button(mPackageRootComposite, SWT.RADIO); + mLicenseRadioAccept.setText("Accept"); + mLicenseRadioAccept.setToolTipText("Accept this package."); + mLicenseRadioAccept.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + onLicenseRadioSelected(); + } + }); + + mLicenseRadioReject = new Button(mPackageRootComposite, SWT.RADIO); + mLicenseRadioReject.setText("Reject"); + mLicenseRadioReject.setToolTipText("Reject this package."); + mLicenseRadioReject.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + onLicenseRadioSelected(); + } + }); + + Link link = new Link(mPackageRootComposite, SWT.NONE); + link.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, false, 1, 1)); + final String printAction = "Print"; // extracted for NLS, to compare with below. + link.setText(String.format("Copy to clipboard | %1$s", printAction)); + link.setToolTipText("Copies all text and license to clipboard | Print using system defaults."); + link.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + super.widgetSelected(e); + if (printAction.equals(e.text)) { + mPackageText.print(); + } else { + Point p = mPackageText.getSelection(); + mPackageText.selectAll(); + mPackageText.copy(); + mPackageText.setSelection(p); + } + } + }); + + + mLicenseRadioAcceptLicense = new Button(mPackageRootComposite, SWT.RADIO); + mLicenseRadioAcceptLicense.setText("Accept License"); + mLicenseRadioAcceptLicense.setToolTipText("Accept all packages that use the same license."); + mLicenseRadioAcceptLicense.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + onLicenseRadioSelected(); + } + }); + + mSashForm.setWeights(new int[] {200, 300}); + } + + /** + * Creates and returns the contents of this dialog's button bar. + *

+ * This reimplements most of the code from the base class with a few exceptions: + *

    + *
  • Enforces 3 columns. + *
  • Inserts a full-width error label. + *
  • Inserts a help label on the left of the first button. + *
  • Renames the OK button into "Install" + *
+ */ + @Override + protected Control createButtonBar(Composite parent) { + Composite composite = new Composite(parent, SWT.NONE); + GridLayout layout = new GridLayout(); + layout.numColumns = 0; // this is incremented by createButton + layout.makeColumnsEqualWidth = false; + layout.marginWidth = convertHorizontalDLUsToPixels(IDialogConstants.HORIZONTAL_MARGIN); + layout.marginHeight = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_MARGIN); + layout.horizontalSpacing = convertHorizontalDLUsToPixels(IDialogConstants.HORIZONTAL_SPACING); + layout.verticalSpacing = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_SPACING); + composite.setLayout(layout); + GridData data = new GridData(SWT.FILL, SWT.CENTER, true, false, 3, 1); + composite.setLayoutData(data); + composite.setFont(parent.getFont()); + + // Error message area + mErrorLabel = new Label(composite, SWT.NONE); + mErrorLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 3, 1)); + + // Label at the left of the install/cancel buttons + Label label = new Label(composite, SWT.NONE); + label.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + label.setText("[*] Something depends on this package"); + label.setEnabled(false); + layout.numColumns++; + + // Add the ok/cancel to the button bar. + createButtonsForButtonBar(composite); + + // the ok button should be an "install" button + Button button = getButton(IDialogConstants.OK_ID); + button.setText("Install"); + + return composite; + } + + // -- End of UI, Start of internal logic ---------- + // Hide everything down-below from SWT designer + //$hide>>$ + + @Override + public void create() { + super.create(); + + // set window title + getShell().setText("Choose Packages to Install"); + + setWindowImage(); + + // Automatically accept those with an empty license or no license + for (ArchiveInfo ai : mArchives) { + Archive a = ai.getNewArchive(); + if (a != null) { + License license = a.getParentPackage().getLicense(); + boolean hasLicense = license != null && + license.getLicense() != null && + license.getLicense().length() > 0; + ai.setAccepted(!hasLicense); + } + } + + // Fill the list with the replacement packages + mTreeViewPackage.setLabelProvider(new NewArchivesLabelProvider()); + mTreeViewPackage.setContentProvider(new NewArchivesContentProvider()); + mTreeViewPackage.setInput(createTreeInput(mArchives)); + mTreeViewPackage.expandAll(); + + adjustColumnsWidth(); + + // select first item + onPackageSelected(); + } + + /** + * Creates the icon of the window shell. + */ + private void setWindowImage() { + String imageName = "android_icon_16.png"; //$NON-NLS-1$ + if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_DARWIN) { + imageName = "android_icon_128.png"; //$NON-NLS-1$ + } + + if (mSwtUpdaterData != null) { + ImageFactory imgFactory = mSwtUpdaterData.getImageFactory(); + if (imgFactory != null) { + getShell().setImage(imgFactory.getImageByName(imageName)); + } + } + } + + /** + * Adds a listener to adjust the columns width when the parent is resized. + *

+ * If we need something more fancy, we might want to use this: + * http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet77.java?view=co + */ + private void adjustColumnsWidth() { + // Add a listener to resize the column to the full width of the table + ControlAdapter resizer = new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Rectangle r = mTreePackage.getClientArea(); + mTreeColum.setWidth(r.width); + } + }; + mTreePackage.addControlListener(resizer); + resizer.controlResized(null); + } + + /** + * Captures the window size before closing this. + * @see #getInitialSize() + */ + @Override + public boolean close() { + sLastSize = getShell().getSize(); + return super.close(); + } + + /** + * Tries to reuse the last window size during this session. + *

+ * Note: the alternative would be to implement {@link #getDialogBoundsSettings()} + * since the default {@link #getDialogBoundsStrategy()} is to persist both location + * and size. + */ + @Override + protected Point getInitialSize() { + if (sLastSize != null) { + return sLastSize; + } else { + // Arbitrary values that look good on my screen and fit on 800x600 + return new Point(740, 470); + } + } + + /** + * Callback invoked when a package item is selected in the list. + */ + private void onPackageSelected() { + Object item = getSelectedItem(); + + // Update mAcceptSameAllLicense : true if all items under the same license are accepted. + ArchiveInfo ai = null; + List list = null; + if (item instanceof ArchiveInfo) { + ai = (ArchiveInfo) item; + + Object p = + ((NewArchivesContentProvider) mTreeViewPackage.getContentProvider()).getParent(ai); + if (p instanceof LicenseEntry) { + list = ((LicenseEntry) p).getArchives(); + } + displayPackageInformation(ai); + + } else if (item instanceof LicenseEntry) { + LicenseEntry entry = (LicenseEntry) item; + list = entry.getArchives(); + displayLicenseInformation(entry); + + } else { + // Fallback, should not happen. + displayEmptyInformation(); + } + + // the "Accept License" radio is selected if there's a license with >= 0 items + // and they are all in "accepted" state. + mAcceptSameAllLicense = list != null && list.size() > 0; + if (mAcceptSameAllLicense) { + assert list != null; + License lic0 = getLicense(list.get(0)); + for (ArchiveInfo ai2 : list) { + License lic2 = getLicense(ai2); + if (ai2.isAccepted() && (lic0 == lic2 || lic0.equals(lic2))) { + continue; + } else { + mAcceptSameAllLicense = false; + break; + } + } + } + + displayMissingDependency(ai); + updateLicenceRadios(ai); + } + + /** Returns the currently selected tree item. + * @return Either {@link ArchiveInfo} or {@link LicenseEntry} or null. */ + private Object getSelectedItem() { + ISelection sel = mTreeViewPackage.getSelection(); + if (sel instanceof IStructuredSelection) { + Object elem = ((IStructuredSelection) sel).getFirstElement(); + if (elem instanceof ArchiveInfo || elem instanceof LicenseEntry) { + return elem; + } + } + return null; + } + + /** + * Information displayed when nothing valid is selected. + */ + private void displayEmptyInformation() { + mPackageText.setText("Please select a package or a license."); + } + + /** + * Updates the package description and license text depending on the selected package. + *

+ * Note that right now there is no logic to support more than one level of dependencies + * (e.g. A <- B <- C and A is disabled so C should be disabled; currently C's state depends + * solely on B's state). We currently don't need this. It would be straightforward to add + * if we had a need for it, though. This would require changes to {@link ArchiveInfo} and + * {@link SdkUpdaterLogic}. + */ + private void displayPackageInformation(ArchiveInfo ai) { + Archive aNew = ai == null ? null : ai.getNewArchive(); + Package pNew = aNew == null ? null : aNew.getParentPackage(); + + if (pNew == null) { + displayEmptyInformation(); + return; + } + assert ai != null; // make Eclipse null detector happy + assert aNew != null; + + mPackageText.setText(""); //$NON-NLS-1$ + + addSectionTitle("Package Description\n"); + addText(pNew.getLongDescription(), "\n\n"); //$NON-NLS-1$ + + Archive aOld = ai.getReplaced(); + if (aOld != null) { + Package pOld = aOld.getParentPackage(); + + FullRevision rOld = pOld.getRevision(); + FullRevision rNew = pNew.getRevision(); + + boolean showRev = true; + + if (pNew instanceof IAndroidVersionProvider && + pOld instanceof IAndroidVersionProvider) { + AndroidVersion vOld = ((IAndroidVersionProvider) pOld).getAndroidVersion(); + AndroidVersion vNew = ((IAndroidVersionProvider) pNew).getAndroidVersion(); + + if (!vOld.equals(vNew)) { + // Versions are different, so indicate more than just the revision. + addText(String.format("This update will replace API %1$s revision %2$s with API %3$s revision %4$s.\n\n", + vOld.getApiString(), rOld.toShortString(), + vNew.getApiString(), rNew.toShortString())); + showRev = false; + } + } + + if (showRev) { + addText(String.format("This update will replace revision %1$s with revision %2$s.\n\n", + rOld.toShortString(), + rNew.toShortString())); + } + } + + ArchiveInfo[] aDeps = ai.getDependsOn(); + if ((aDeps != null && aDeps.length > 0) || ai.isDependencyFor()) { + addSectionTitle("Dependencies\n"); + + if (aDeps != null && aDeps.length > 0) { + addText("Installing this package also requires installing:"); + for (ArchiveInfo aDep : aDeps) { + addText(String.format("\n- %1$s", + aDep.getShortDescription())); + } + addText("\n\n"); + } + + if (ai.isDependencyFor()) { + addText("This package is a dependency for:"); + for (ArchiveInfo ai2 : ai.getDependenciesFor()) { + addText(String.format("\n- %1$s", + ai2.getShortDescription())); + } + addText("\n\n"); + } + } + + addSectionTitle("Archive Description\n"); + addText(aNew.getLongDescription(), "\n\n"); //$NON-NLS-1$ + + License license = pNew.getLicense(); + if (license != null) { + String text = license.getLicense(); + if (text != null) { + addSectionTitle("License\n"); + addText(text.trim(), "\n\n"); //$NON-NLS-1$ + } + } + + addSectionTitle("Site\n"); + SdkSource source = pNew.getParentSource(); + if (source != null) { + addText(source.getShortDescription()); + } + } + + /** + * Updates the description for a license entry. + */ + private void displayLicenseInformation(LicenseEntry entry) { + List archives = entry == null ? null : entry.getArchives(); + if (archives == null) { + // There should not be a license entry without any package in it. + displayEmptyInformation(); + return; + } + assert entry != null; + + mPackageText.setText(""); //$NON-NLS-1$ + + License license = null; + addSectionTitle("Packages\n"); + for (ArchiveInfo ai : entry.getArchives()) { + Archive aNew = ai.getNewArchive(); + if (aNew != null) { + Package pNew = aNew.getParentPackage(); + if (pNew != null) { + if (license == null) { + license = pNew.getLicense(); + } else { + assert license.equals(pNew.getLicense()); // all items have the same license + } + addText("- ", pNew.getShortDescription(), "\n"); //$NON-NLS-1$ //$NON-NLS-2$ + } + } + } + + if (license != null) { + String text = license.getLicense(); + if (text != null) { + addSectionTitle("\nLicense\n"); + addText(text.trim(), "\n\n"); //$NON-NLS-1$ + } + } + } + + /** + * Computes and displays missing dependencies. + * + * If there's a selected package, check the dependency for that one. + * Otherwise display the first missing dependency of any other package. + */ + private void displayMissingDependency(ArchiveInfo ai) { + String error = null; + + try { + if (ai != null) { + if (ai.isAccepted()) { + // Case where this package is accepted but blocked by another non-accepted one + ArchiveInfo[] adeps = ai.getDependsOn(); + if (adeps != null) { + for (ArchiveInfo adep : adeps) { + if (!adep.isAccepted()) { + error = String.format("This package depends on '%1$s'.", + adep.getShortDescription()); + return; + } + } + } + } else { + // Case where this package blocks another one when not accepted + for (ArchiveInfo adep : ai.getDependenciesFor()) { + // It only matters if the blocked one is accepted + if (adep.isAccepted()) { + error = String.format("Package '%1$s' depends on this one.", + adep.getShortDescription()); + return; + } + } + } + } + + // If there is no missing dependency on the current selection, + // just find the first missing dependency of any other package. + for (ArchiveInfo ai2 : mArchives) { + if (ai2 == ai) { + // We already processed that one above. + continue; + } + if (ai2.isAccepted()) { + // The user requested to install this package. + // Check if all its dependencies are met. + ArchiveInfo[] adeps = ai2.getDependsOn(); + if (adeps != null) { + for (ArchiveInfo adep : adeps) { + if (!adep.isAccepted()) { + error = String.format("Package '%1$s' depends on '%2$s'", + ai2.getShortDescription(), + adep.getShortDescription()); + return; + } + } + } + } else { + // The user did not request to install this package. + // Check whether this package blocks another one when not accepted. + for (ArchiveInfo adep : ai2.getDependenciesFor()) { + // It only matters if the blocked one is accepted + // or if it's a local archive that is already installed (these + // are marked as implicitly accepted, so it's the same test.) + if (adep.isAccepted()) { + error = String.format("Package '%1$s' depends on '%2$s'", + adep.getShortDescription(), + ai2.getShortDescription()); + return; + } + } + } + } + } finally { + mErrorLabel.setText(error == null ? "" : error); //$NON-NLS-1$ + } + } + + private void addText(String...string) { + for (String s : string) { + mPackageText.append(s); + } + } + + private void addSectionTitle(String string) { + String s = mPackageText.getText(); + int start = (s == null ? 0 : s.length()); + mPackageText.append(string); + + StyleRange sr = new StyleRange(); + sr.start = start; + sr.length = string.length(); + sr.fontStyle = SWT.BOLD; + sr.underline = true; + mPackageText.setStyleRange(sr); + } + + private void updateLicenceRadios(ArchiveInfo ai) { + if (mInternalLicenseRadioUpdate) { + return; + } + mInternalLicenseRadioUpdate = true; + + boolean oneAccepted = false; + + mLicenseRadioAcceptLicense.setSelection(mAcceptSameAllLicense); + oneAccepted = ai != null && ai.isAccepted(); + mLicenseRadioAccept.setEnabled(ai != null); + mLicenseRadioReject.setEnabled(ai != null); + mLicenseRadioAccept.setSelection(oneAccepted); + mLicenseRadioReject.setSelection(ai != null && ai.isRejected()); + + // The install button is enabled if there's at least one package accepted. + // If the current one isn't, look for another one. + boolean missing = mErrorLabel.getText() != null && mErrorLabel.getText().length() > 0; + if (!missing && !oneAccepted) { + for(ArchiveInfo ai2 : mArchives) { + if (ai2.isAccepted()) { + oneAccepted = true; + break; + } + } + } + + getButton(IDialogConstants.OK_ID).setEnabled(!missing && oneAccepted); + + mInternalLicenseRadioUpdate = false; + } + + /** + * Callback invoked when one of the radio license buttons is selected. + * + * - accept/refuse: toggle, update item checkbox + * - accept all: set accept-all, check all items with the *same* license + */ + private void onLicenseRadioSelected() { + if (mInternalLicenseRadioUpdate) { + return; + } + mInternalLicenseRadioUpdate = true; + + Object item = getSelectedItem(); + ArchiveInfo ai = (item instanceof ArchiveInfo) ? (ArchiveInfo) item : null; + boolean needUpdate = true; + + if (!mAcceptSameAllLicense && mLicenseRadioAcceptLicense.getSelection()) { + // Accept all has been switched on. Mark all packages as accepted + + List list = null; + if (item instanceof LicenseEntry) { + list = ((LicenseEntry) item).getArchives(); + } else if (ai != null) { + Object p = ((NewArchivesContentProvider) mTreeViewPackage.getContentProvider()) + .getParent(ai); + if (p instanceof LicenseEntry) { + list = ((LicenseEntry) p).getArchives(); + } + } + + if (list != null && list.size() > 0) { + mAcceptSameAllLicense = true; + for(ArchiveInfo ai2 : list) { + ai2.setAccepted(true); + ai2.setRejected(false); + } + } + + } else if (ai != null && mLicenseRadioAccept.getSelection()) { + // Accept only this one + mAcceptSameAllLicense = false; + ai.setAccepted(true); + ai.setRejected(false); + + } else if (ai != null && mLicenseRadioReject.getSelection()) { + // Reject only this one + mAcceptSameAllLicense = false; + ai.setAccepted(false); + ai.setRejected(true); + + } else { + needUpdate = false; + } + + mInternalLicenseRadioUpdate = false; + + if (needUpdate) { + if (mAcceptSameAllLicense) { + mTreeViewPackage.refresh(); + } else { + mTreeViewPackage.refresh(ai); + mTreeViewPackage.refresh( + ((NewArchivesContentProvider) mTreeViewPackage.getContentProvider()). + getParent(ai)); + } + displayMissingDependency(ai); + updateLicenceRadios(ai); + } + } + + /** + * Callback invoked when a package item is double-clicked in the list. + */ + private void onPackageDoubleClick() { + Object item = getSelectedItem(); + + if (item instanceof ArchiveInfo) { + ArchiveInfo ai = (ArchiveInfo) item; + boolean wasAccepted = ai.isAccepted(); + ai.setAccepted(!wasAccepted); + ai.setRejected(wasAccepted); + + // update state + mAcceptSameAllLicense = false; + mTreeViewPackage.refresh(ai); + // refresh parent since its icon might have changed. + mTreeViewPackage.refresh( + ((NewArchivesContentProvider) mTreeViewPackage.getContentProvider()). + getParent(ai)); + + displayMissingDependency(ai); + updateLicenceRadios(ai); + + } else if (item instanceof LicenseEntry) { + mTreeViewPackage.setExpandedState(item, !mTreeViewPackage.getExpandedState(item)); + } + } + + /** + * Provides the labels for the tree view. + * Root branches are {@link LicenseEntry} elements. + * Leave nodes are {@link ArchiveInfo} which all have the same license. + */ + private class NewArchivesLabelProvider extends LabelProvider { + @Override + public Image getImage(Object element) { + if (element instanceof ArchiveInfo) { + // Archive icon: accepted (green), rejected (red), not set yet (question mark) + ArchiveInfo ai = (ArchiveInfo) element; + + ImageFactory imgFactory = mSwtUpdaterData.getImageFactory(); + if (imgFactory != null) { + if (ai.isAccepted()) { + return imgFactory.getImageByName("accept_icon16.png"); + } else if (ai.isRejected()) { + return imgFactory.getImageByName("reject_icon16.png"); + } + return imgFactory.getImageByName("unknown_icon16.png"); + } + return super.getImage(element); + + } else if (element instanceof LicenseEntry) { + // License icon: green if all below are accepted, red if all rejected, otherwise + // no icon. + ImageFactory imgFactory = mSwtUpdaterData.getImageFactory(); + if (imgFactory != null) { + boolean allAccepted = true; + boolean allRejected = true; + for (ArchiveInfo ai : ((LicenseEntry) element).getArchives()) { + allAccepted = allAccepted && ai.isAccepted(); + allRejected = allRejected && ai.isRejected(); + } + if (allAccepted && !allRejected) { + return imgFactory.getImageByName("accept_icon16.png"); + } else if (!allAccepted && allRejected) { + return imgFactory.getImageByName("reject_icon16.png"); + } + } + } + + return null; + } + + @Override + public String getText(Object element) { + if (element instanceof LicenseEntry) { + return ((LicenseEntry) element).getLicenseRef(); + + } else if (element instanceof ArchiveInfo) { + ArchiveInfo ai = (ArchiveInfo) element; + + String desc = ai.getShortDescription(); + + if (ai.isDependencyFor()) { + desc += " [*]"; + } + + return desc; + + } + + assert element instanceof String || element instanceof ArchiveInfo; + return null; + } + } + + /** + * Provides the content for the tree view. + * Root branches are {@link LicenseEntry} elements. + * Leave nodes are {@link ArchiveInfo} which all have the same license. + */ + private class NewArchivesContentProvider implements ITreeContentProvider { + private List mInput; + + @Override + public void dispose() { + // pass + } + + @SuppressWarnings("unchecked") + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // Input should be the result from createTreeInput. + if (newInput instanceof List && + ((List) newInput).size() > 0 && + ((List) newInput).get(0) instanceof LicenseEntry) { + mInput = (List) newInput; + } else { + mInput = null; + } + } + + @Override + public boolean hasChildren(Object parent) { + if (parent instanceof List) { + // This is the root of the tree. + return true; + + } else if (parent instanceof LicenseEntry) { + return ((LicenseEntry) parent).getArchives().size() > 0; + } + + return false; + } + + @Override + public Object[] getElements(Object parent) { + return getChildren(parent); + } + + @Override + public Object[] getChildren(Object parent) { + if (parent instanceof List) { + return ((List) parent).toArray(); + + } else if (parent instanceof LicenseEntry) { + return ((LicenseEntry) parent).getArchives().toArray(); + } + + return new Object[0]; + } + + @Override + public Object getParent(Object child) { + if (child instanceof LicenseEntry) { + return ((LicenseEntry) child).getRoot(); + + } else if (child instanceof ArchiveInfo && mInput != null) { + for (LicenseEntry entry : mInput) { + if (entry.getArchives().contains(child)) { + return entry; + } + } + } + + return null; + } + } + + /** + * Represents a branch in the view tree: an entry where all the sub-archive info + * share the same license. Contains a link to the share root list for convenience. + */ + private static class LicenseEntry { + private final List mRoot; + private final String mLicenseRef; + private final List mArchives; + + public LicenseEntry( + @NonNull List root, + @NonNull String licenseRef, + @NonNull List archives) { + mRoot = root; + mLicenseRef = licenseRef; + mArchives = archives; + } + + @NonNull + public List getRoot() { + return mRoot; + } + + @NonNull + public String getLicenseRef() { + return mLicenseRef; + } + + @NonNull + public List getArchives() { + return mArchives; + } + } + + /** + * Creates the tree structure based on the given archives. + * The current structure is to have a branch per license type, + * with all the archives sharing the same license under it. + * Elements with no license are left at the root. + * + * @param archives The non-null collection of archive info to display. Ideally non-empty. + * @return A list of {@link LicenseEntry}, each containing a list of {@link ArchiveInfo}. + */ + @NonNull + private List createTreeInput(@NonNull Collection archives) { + // Build an ordered map with all the licenses, ordered by license ref name. + final String noLicense = "No license"; //NLS + + Comparator comp = new Comparator() { + @Override + public int compare(String s1, String s2) { + boolean first1 = noLicense.equals(s1); + boolean first2 = noLicense.equals(s2); + if (first1 && first2) { + return 0; + } else if (first1) { + return -1; + } else if (first2) { + return 1; + } + return s1.compareTo(s2); + } + }; + + Map> map = new TreeMap>(comp); + + for (ArchiveInfo info : archives) { + String ref = noLicense; + License license = getLicense(info); + if (license != null && license.getLicenseRef() != null) { + ref = prettyLicenseRef(license.getLicenseRef()); + } + + List list = map.get(ref); + if (list == null) { + map.put(ref, list = new ArrayList()); + } + list.add(info); + } + + // Transform result into a list + List licensesList = new ArrayList(); + for (Map.Entry> entry : map.entrySet()) { + licensesList.add(new LicenseEntry(licensesList, entry.getKey(), entry.getValue())); + } + + return licensesList; + } + + /** + * Helper method to retrieve the {@link License} for a given {@link ArchiveInfo}. + * + * @param ai The archive info. Can be null. + * @return The license for the package owning the archive. Can be null. + */ + @Nullable + private License getLicense(@Nullable ArchiveInfo ai) { + if (ai != null) { + Archive aNew = ai.getNewArchive(); + if (aNew != null) { + Package pNew = aNew.getParentPackage(); + if (pNew != null) { + return pNew.getLicense(); + } + } + } + return null; + } + + /** + * Reformats the licenseRef to be more human-readable. + * It's an XML ref and in practice it looks like [oem-]android-[type]-license. + * If it's not a format we can deal with, leave it alone. + */ + private String prettyLicenseRef(String ref) { + // capitalize every word + StringBuilder sb = new StringBuilder(); + boolean capitalize = true; + for (char c : ref.toCharArray()) { + if (c >= 'a' && c <= 'z') { + if (capitalize) { + c = (char) (c + 'A' - 'a'); + capitalize = false; + } + } else { + if (c == '-') { + c = ' '; + } + capitalize = true; + } + sb.append(c); + } + + ref = sb.toString(); + + // A few acronyms should stay upper-case + for (String w : new String[] { "Sdk", "Mips", "Arm" }) { + ref = ref.replaceAll(w, w.toUpperCase(Locale.US)); + } + + return ref; + } + + // End of hiding from SWT Designer + //$hide<<$ +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SettingsDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SettingsDialog.java new file mode 100644 index 00000000..06a9df9f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SettingsDialog.java @@ -0,0 +1,286 @@ +/* + * Copyright (C) 2012 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.sdkuilib.internal.repository; + +import com.android.sdklib.internal.repository.DownloadCache; +import com.android.sdklib.internal.repository.DownloadCache.Strategy; +import com.android.sdklib.internal.repository.updater.ISettingsPage; +import com.android.sdklib.internal.repository.updater.SettingsController; +import com.android.sdklib.util.FormatUtils; +import com.android.sdkuilib.ui.GridDataBuilder; +import com.android.sdkuilib.ui.GridLayoutBuilder; + +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.Point; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +import java.util.Properties; + + +public class SettingsDialog extends UpdaterBaseDialog implements ISettingsPage { + + + // data members + private final DownloadCache mDownloadCache = new DownloadCache(Strategy.SERVE_CACHE); + private final SettingsController mSettingsController; + private SettingsChangedCallback mSettingsChangedCallback; + + // UI widgets + private Text mTextProxyServer; + private Text mTextProxyPort; + private Text mTextCacheSize; + private Button mCheckUseCache; + private Button mCheckForceHttp; + private Button mCheckAskAdbRestart; + private Button mCheckEnablePreviews; + + private SelectionAdapter mApplyOnSelected = new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + applyNewSettings(); //$hide$ + } + }; + + private ModifyListener mApplyOnModified = new ModifyListener() { + @Override + public void modifyText(ModifyEvent e) { + applyNewSettings(); //$hide$ + } + }; + + public SettingsDialog(Shell parentShell, SwtUpdaterData swtUpdaterData) { + super(parentShell, swtUpdaterData, "Settings" /*title*/); + assert swtUpdaterData != null; + mSettingsController = swtUpdaterData.getSettingsController(); + } + + @Override + protected void createShell() { + super.createShell(); + Shell shell = getShell(); + shell.setMinimumSize(new Point(450, 370)); + shell.setSize(450, 400); + } + + @Override + protected void createContents() { + super.createContents(); + Shell shell = getShell(); + + Group group = new Group(shell, SWT.NONE); + group.setText("Proxy Settings"); + GridDataBuilder.create(group).fill().grab().hSpan(2); + GridLayoutBuilder.create(group).columns(2); + + Label label = new Label(group, SWT.NONE); + GridDataBuilder.create(label).hRight().vCenter(); + label.setText("HTTP Proxy Server"); + String tooltip = "The hostname or IP of the HTTP & HTTPS proxy server to use (e.g. proxy.example.com).\n" + + "When empty, the default Java proxy setting is used."; + label.setToolTipText(tooltip); + + mTextProxyServer = new Text(group, SWT.BORDER); + GridDataBuilder.create(mTextProxyServer).hFill().hGrab().vCenter(); + mTextProxyServer.addModifyListener(mApplyOnModified); + mTextProxyServer.setToolTipText(tooltip); + + label = new Label(group, SWT.NONE); + GridDataBuilder.create(label).hRight().vCenter(); + label.setText("HTTP Proxy Port"); + tooltip = "The port of the HTTP & HTTPS proxy server to use (e.g. 3128).\n" + + "When empty, the default Java proxy setting is used."; + label.setToolTipText(tooltip); + + mTextProxyPort = new Text(group, SWT.BORDER); + GridDataBuilder.create(mTextProxyPort).hFill().hGrab().vCenter(); + mTextProxyPort.addModifyListener(mApplyOnModified); + mTextProxyPort.setToolTipText(tooltip); + + // ---- + group = new Group(shell, SWT.NONE); + group.setText("Manifest Cache"); + GridDataBuilder.create(group).fill().grab().hSpan(2); + GridLayoutBuilder.create(group).columns(3); + + label = new Label(group, SWT.NONE); + GridDataBuilder.create(label).hRight().vCenter(); + label.setText("Directory:"); + + Text text = new Text(group, SWT.NONE); + GridDataBuilder.create(text).hFill().hGrab().vCenter().hSpan(2); + text.setEnabled(false); + text.setText(mDownloadCache.getCacheRoot().getAbsolutePath()); + + label = new Label(group, SWT.NONE); + GridDataBuilder.create(label).hRight().vCenter(); + label.setText("Current Size:"); + + mTextCacheSize = new Text(group, SWT.NONE); + GridDataBuilder.create(mTextCacheSize).hFill().hGrab().vCenter().hSpan(2); + mTextCacheSize.setEnabled(false); + updateDownloadCacheSize(); + + mCheckUseCache = new Button(group, SWT.CHECK); + GridDataBuilder.create(mCheckUseCache).vCenter().hSpan(1); + mCheckUseCache.setText("Use download cache"); + mCheckUseCache.setToolTipText("When checked, small manifest files are cached locally.\n" + + "Large binary files are never cached locally."); + mCheckUseCache.addSelectionListener(mApplyOnSelected); + + label = new Label(group, SWT.NONE); + GridDataBuilder.create(label).hFill().hGrab().hSpan(1); + + Button button = new Button(group, SWT.PUSH); + GridDataBuilder.create(button).vCenter().hSpan(1); + button.setText("Clear Cache"); + button.setToolTipText("Deletes all cached files."); + button.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + mDownloadCache.clearCache(); + updateDownloadCacheSize(); + } + }); + + // ---- + group = new Group(shell, SWT.NONE); + group.setText("Others"); + GridDataBuilder.create(group).fill().grab().hSpan(2); + GridLayoutBuilder.create(group).columns(2); + + mCheckForceHttp = new Button(group, SWT.CHECK); + GridDataBuilder.create(mCheckForceHttp).hFill().hGrab().vCenter().hSpan(2); + mCheckForceHttp.setText("Force https://... sources to be fetched using http://..."); + mCheckForceHttp.setToolTipText( + "If you are not able to connect to the official Android repository using HTTPS,\n" + + "enable this setting to force accessing it via HTTP."); + mCheckForceHttp.addSelectionListener(mApplyOnSelected); + + mCheckAskAdbRestart = new Button(group, SWT.CHECK); + GridDataBuilder.create(mCheckAskAdbRestart).hFill().hGrab().vCenter().hSpan(2); + mCheckAskAdbRestart.setText("Ask before restarting ADB"); + mCheckAskAdbRestart.setToolTipText( + "When checked, the user will be asked for permission to restart ADB\n" + + "after updating an addon-on package or a tool package."); + mCheckAskAdbRestart.addSelectionListener(mApplyOnSelected); + + mCheckEnablePreviews = new Button(group, SWT.CHECK); + GridDataBuilder.create(mCheckEnablePreviews).hFill().hGrab().vCenter().hSpan(2); + mCheckEnablePreviews.setText("Enable Preview Tools"); + mCheckEnablePreviews.setToolTipText( + "When checked, the package list will also display preview versions of the tools.\n" + + "These are optional future release candidates that the Android tools team\n" + + "publishes from time to time for early feedback."); + mCheckEnablePreviews.addSelectionListener(mApplyOnSelected); + + Label filler = new Label(shell, SWT.NONE); + GridDataBuilder.create(filler).hFill().hGrab(); + + createCloseButton(); + } + + @Override + protected void postCreate() { + super.postCreate(); + // This tells the controller to load the settings into the page UI. + mSettingsController.setSettingsPage(this); + } + + @Override + protected void close() { + // Dissociate this page from the controller + mSettingsController.setSettingsPage(null); + super.close(); + } + + + // -- Start of internal part ---------- + // Hide everything down-below from SWT designer + //$hide>>$ + + /** Loads settings from the given {@link Properties} container and update the page UI. */ + @Override + public void loadSettings(Properties inSettings) { + mTextProxyServer.setText(inSettings.getProperty(KEY_HTTP_PROXY_HOST, "")); //$NON-NLS-1$ + mTextProxyPort.setText( inSettings.getProperty(KEY_HTTP_PROXY_PORT, "")); //$NON-NLS-1$ + mCheckForceHttp.setSelection( + Boolean.parseBoolean(inSettings.getProperty(KEY_FORCE_HTTP))); + mCheckAskAdbRestart.setSelection( + Boolean.parseBoolean(inSettings.getProperty(KEY_ASK_ADB_RESTART))); + mCheckUseCache.setSelection( + Boolean.parseBoolean(inSettings.getProperty(KEY_USE_DOWNLOAD_CACHE))); + mCheckEnablePreviews.setSelection( + Boolean.parseBoolean(inSettings.getProperty(KEY_ENABLE_PREVIEWS))); + + } + + /** Called by the application to retrieve settings from the UI and store them in + * the given {@link Properties} container. */ + @Override + public void retrieveSettings(Properties outSettings) { + outSettings.setProperty(KEY_HTTP_PROXY_HOST, mTextProxyServer.getText()); + outSettings.setProperty(KEY_HTTP_PROXY_PORT, mTextProxyPort.getText()); + outSettings.setProperty(KEY_FORCE_HTTP, + Boolean.toString(mCheckForceHttp.getSelection())); + outSettings.setProperty(KEY_ASK_ADB_RESTART, + Boolean.toString(mCheckAskAdbRestart.getSelection())); + outSettings.setProperty(KEY_USE_DOWNLOAD_CACHE, + Boolean.toString(mCheckUseCache.getSelection())); + outSettings.setProperty(KEY_ENABLE_PREVIEWS, + Boolean.toString(mCheckEnablePreviews.getSelection())); + + } + + /** + * Called by the application to give a callback that the page should invoke when + * settings must be applied. The page does not apply the settings itself, instead + * it notifies the application. + */ + @Override + public void setOnSettingsChanged(SettingsChangedCallback settingsChangedCallback) { + mSettingsChangedCallback = settingsChangedCallback; + } + + /** + * Callback invoked when user touches one of the settings. + * There is no "Apply" button, settings are applied immediately as they are changed. + * Notify the application that settings have changed. + */ + private void applyNewSettings() { + if (mSettingsChangedCallback != null) { + mSettingsChangedCallback.onSettingsChanged(this); + } + } + + private void updateDownloadCacheSize() { + long size = mDownloadCache.getCurrentSize(); + String str = FormatUtils.byteSizeToString(size); + mTextCacheSize.setText(str); + } + + + // End of hiding from SWT Designer + //$hide<<$ +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SwtUpdaterData.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SwtUpdaterData.java new file mode 100644 index 00000000..0d26a3f8 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SwtUpdaterData.java @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2013 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.sdkuilib.internal.repository; + +import com.android.annotations.NonNull; +import com.android.sdklib.internal.repository.AdbWrapper; +import com.android.sdklib.internal.repository.ITaskMonitor; +import com.android.sdklib.internal.repository.NullTaskMonitor; +import com.android.sdklib.internal.repository.archives.Archive; +import com.android.sdklib.internal.repository.updater.ArchiveInfo; +import com.android.sdklib.internal.repository.updater.SdkUpdaterLogic; +import com.android.sdklib.internal.repository.updater.UpdaterData; +import com.android.sdkuilib.internal.repository.icons.ImageFactory; +import com.android.sdkuilib.internal.repository.ui.SdkUpdaterWindowImpl2; +import com.android.utils.ILogger; + +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * Data shared between {@link SdkUpdaterWindowImpl2} and its pages. + */ +public class SwtUpdaterData extends UpdaterData { + + private Shell mWindowShell; + + /** + * The current {@link ImageFactory}. + * Set via {@link #setImageFactory(ImageFactory)} by the window implementation. + * It is null when invoked using the command-line interface. + */ + private ImageFactory mImageFactory; + + /** + * Creates a new updater data. + * + * @param sdkLog Logger. Cannot be null. + * @param osSdkRoot The OS path to the SDK root. + */ + public SwtUpdaterData(String osSdkRoot, ILogger sdkLog) { + super(osSdkRoot, sdkLog); + } + + // ----- getters, setters ---- + + public void setImageFactory(ImageFactory imageFactory) { + mImageFactory = imageFactory; + } + + public ImageFactory getImageFactory() { + return mImageFactory; + } + + public void setWindowShell(Shell windowShell) { + mWindowShell = windowShell; + } + + public Shell getWindowShell() { + return mWindowShell; + } + + @Override + protected void displayInitError(String error) { + // We may not have any UI. Only display a dialog if there's a window shell available. + if (mWindowShell != null && !mWindowShell.isDisposed()) { + MessageDialog.openError(mWindowShell, + "Android Virtual Devices Manager", + error); + } else { + super.displayInitError(error); + } + } + + // ----- + + /** + * Runs the runnable on the UI thread using {@link Display#syncExec(Runnable)}. + * + * @param r Non-null runnable. + */ + @Override + protected void runOnUiThread(@NonNull Runnable r) { + if (mWindowShell != null && !mWindowShell.isDisposed()) { + mWindowShell.getDisplay().syncExec(r); + } + } + + /** + * Attempts to restart ADB. + *

+ * If the "ask before restart" setting is set (the default), prompt the user whether + * now is a good time to restart ADB. + */ + @Override + protected void askForAdbRestart(ITaskMonitor monitor) { + final boolean[] canRestart = new boolean[] { true }; + + if (getWindowShell() != null && + getSettingsController().getSettings().getAskBeforeAdbRestart()) { + // need to ask for permission first + final Shell shell = getWindowShell(); + if (shell != null && !shell.isDisposed()) { + shell.getDisplay().syncExec(new Runnable() { + @Override + public void run() { + if (!shell.isDisposed()) { + canRestart[0] = MessageDialog.openQuestion(shell, + "ADB Restart", + "A package that depends on ADB has been updated. \n" + + "Do you want to restart ADB now?"); + } + } + }); + } + } + + if (canRestart[0]) { + AdbWrapper adb = new AdbWrapper(getOsSdkRoot(), monitor); + adb.stopAdb(); + adb.startAdb(); + } + } + + @Override + protected void notifyToolsNeedsToBeRestarted(int flags) { + String msg = null; + if ((flags & TOOLS_MSG_UPDATED_FROM_ADT) != 0) { + msg = + "The Android SDK and AVD Manager that you are currently using has been updated. " + + "Please also run Eclipse > Help > Check for Updates to see if the Android " + + "plug-in needs to be updated."; + + } else if ((flags & TOOLS_MSG_UPDATED_FROM_SDKMAN) != 0) { + msg = + "The Android SDK and AVD Manager that you are currently using has been updated. " + + "It is recommended that you now close the manager window and re-open it. " + + "If you use Eclipse, please run Help > Check for Updates to see if the Android " + + "plug-in needs to be updated."; + } + + final String msg2 = msg; + + final Shell shell = getWindowShell(); + if (msg2 != null && shell != null && !shell.isDisposed()) { + shell.getDisplay().syncExec(new Runnable() { + @Override + public void run() { + if (!shell.isDisposed()) { + MessageDialog.openInformation(shell, + "Android Tools Updated", + msg2); + } + } + }); + } + } + + /** + * Tries to update all the *existing* local packages. + * This version *requires* to be run with a GUI. + *

+ * There are two modes of operation: + *

    + *
  • If selectedArchives is null, refreshes all sources, compares the available remote + * packages with the current local ones and suggest updates to be done to the user (including + * new platforms that the users doesn't have yet). + *
  • If selectedArchives is not null, this represents a list of archives/packages that + * the user wants to install or update, so just process these. + *
+ * + * @param selectedArchives The list of remote archives to consider for the update. + * This can be null, in which case a list of remote archive is fetched from all + * available sources. + * @param includeObsoletes True if obsolete packages should be used when resolving what + * to update. + * @param flags Optional flags for the installer, such as {@link #NO_TOOLS_MSG}. + * @return A list of archives that have been installed. Can be null if nothing was done. + */ + @Override + public List updateOrInstallAll_WithGUI( + Collection selectedArchives, + boolean includeObsoletes, + int flags) { + + // Note: we no longer call refreshSources(true) here. This will be done + // automatically by computeUpdates() iif it needs to access sources to + // resolve missing dependencies. + + SdkUpdaterLogic ul = new SdkUpdaterLogic(this); + List archives = ul.computeUpdates( + selectedArchives, + getSources(), + getLocalSdkParser().getPackages(), + includeObsoletes); + + if (selectedArchives == null) { + getPackageLoader().loadRemoteAddonsList(new NullTaskMonitor(getSdkLog())); + ul.addNewPlatforms( + archives, + getSources(), + getLocalSdkParser().getPackages(), + includeObsoletes); + } + + // TODO if selectedArchives is null and archives.len==0, find if there are + // any new platform we can suggest to install instead. + + Collections.sort(archives); + + SdkUpdaterChooserDialog dialog = + new SdkUpdaterChooserDialog(getWindowShell(), this, archives); + dialog.open(); + + ArrayList result = dialog.getResult(); + if (result != null && result.size() > 0) { + return installArchives(result, flags); + } + return null; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/UpdaterBaseDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/UpdaterBaseDialog.java new file mode 100644 index 00000000..bc8d9d51 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/UpdaterBaseDialog.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2012 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.sdkuilib.internal.repository; + +import com.android.SdkConstants; +import com.android.sdkuilib.internal.repository.icons.ImageFactory; +import com.android.sdkuilib.internal.repository.ui.SdkUpdaterWindowImpl2; +import com.android.sdkuilib.ui.GridDataBuilder; +import com.android.sdkuilib.ui.GridLayoutBuilder; +import com.android.sdkuilib.ui.SwtBaseDialog; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Shell; + + + +/** + * Base class for auxiliary dialogs shown in the updater (for example settings, + * about box or add-on site.) + */ +public abstract class UpdaterBaseDialog extends SwtBaseDialog { + + private final SwtUpdaterData mSwtUpdaterData; + + protected UpdaterBaseDialog(Shell parentShell, SwtUpdaterData swtUpdaterData, String title) { + super(parentShell, + SWT.APPLICATION_MODAL, + String.format("%1$s - %2$s", SdkUpdaterWindowImpl2.APP_NAME, title)); //$NON-NLS-1$ + mSwtUpdaterData = swtUpdaterData; + } + + public SwtUpdaterData getSwtUpdaterData() { + return mSwtUpdaterData; + } + + /** + * Initializes the shell with a 2-column Grid layout. + * Caller should use {@link #createCloseButton()} to inject the + * close button at the bottom of the dialog. + */ + @Override + protected void createContents() { + Shell shell = getShell(); + setWindowImage(shell); + + GridLayoutBuilder.create(shell).columns(2); + } + + protected void createCloseButton() { + Button close = new Button(getShell(), SWT.PUSH); + close.setText("Close"); + GridDataBuilder.create(close).hFill().vBottom(); + close.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + close(); + } + }); + } + + @Override + protected void postCreate() { + // pass + } + + @Override + protected void close() { + super.close(); + } + + /** + * Creates the icon of the window shell. + * + * @param shell The shell on which to put the icon + */ + private void setWindowImage(Shell shell) { + String imageName = "android_icon_16.png"; //$NON-NLS-1$ + if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_DARWIN) { + imageName = "android_icon_128.png"; //$NON-NLS-1$ + } + + if (mSwtUpdaterData != null) { + ImageFactory imgFactory = mSwtUpdaterData.getImageFactory(); + if (imgFactory != null) { + shell.setImage(imgFactory.getImageByName(imageName)); + } + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PackagesDiffLogic.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PackagesDiffLogic.java new file mode 100644 index 00000000..5617898e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PackagesDiffLogic.java @@ -0,0 +1,859 @@ +/* + * Copyright (C) 2011 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.sdkuilib.internal.repository.core; + +import com.android.SdkConstants; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.internal.repository.packages.BuildToolPackage; +import com.android.sdklib.internal.repository.packages.ExtraPackage; +import com.android.sdklib.internal.repository.packages.IAndroidVersionProvider; +import com.android.sdklib.internal.repository.packages.IFullRevisionProvider; +import com.android.sdklib.internal.repository.packages.Package; +import com.android.sdklib.internal.repository.packages.Package.UpdateInfo; +import com.android.sdklib.internal.repository.packages.PlatformPackage; +import com.android.sdklib.internal.repository.packages.PlatformToolPackage; +import com.android.sdklib.internal.repository.packages.SystemImagePackage; +import com.android.sdklib.internal.repository.packages.ToolPackage; +import com.android.sdklib.internal.repository.sources.SdkSource; +import com.android.sdklib.internal.repository.updater.PkgItem; +import com.android.sdklib.internal.repository.updater.PkgItem.PkgState; +import com.android.sdklib.repository.FullRevision; +import com.android.sdklib.repository.FullRevision.PreviewComparison; +import com.android.sdkuilib.internal.repository.SwtUpdaterData; +import com.android.sdkuilib.internal.repository.ui.PackagesPageIcons; +import com.android.utils.Pair; +import com.android.utils.SparseArray; +import com.google.common.collect.Maps; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Helper class that separates the logic of package management from the UI + * so that we can test it using head-less unit tests. + */ +public class PackagesDiffLogic { + private final SwtUpdaterData mUpdaterData; + private boolean mFirstLoadComplete = true; + + public PackagesDiffLogic(SwtUpdaterData swtUpdaterData) { + mUpdaterData = swtUpdaterData; + } + + /** + * Removes all the internal state and resets the object. + * Useful for testing. + */ + public void clear() { + mFirstLoadComplete = true; + mOpApi.clear(); + } + + /** Return mFirstLoadComplete and resets it to false. + * All following calls will returns false. */ + public boolean isFirstLoadComplete() { + boolean b = mFirstLoadComplete; + mFirstLoadComplete = false; + return b; + } + + /** + * Mark all new and update PkgItems as checked. + * + * @param selectNew If true, select all new packages (except the rc/preview ones). + * @param selectUpdates If true, select all update packages. + * @param selectTop If true, select the top platform. + * If the top platform has nothing installed, select all items in it (except the rc/preview); + * If it is partially installed, at least select the platform and system images if none of + * the system images are installed. + * @param currentPlatform The {@link SdkConstants#currentPlatform()} value. + */ + public void checkNewUpdateItems( + boolean selectNew, + boolean selectUpdates, + boolean selectTop, + int currentPlatform) { + int maxApi = 0; + Set installedPlatforms = new HashSet(); + SparseArray> platformItems = new SparseArray>(); + + boolean hasTools = false; + Map, Pair> toolsCandidates = Maps.newHashMap(); + toolsCandidates.put(PlatformToolPackage.class, Pair.of((PkgItem)null, (FullRevision)null)); + toolsCandidates.put(BuildToolPackage.class, Pair.of((PkgItem)null, (FullRevision)null)); + + // sort items in platforms... directly deal with new/update items + List allItems = getAllPkgItems(); + for (PkgItem item : allItems) { + if (!item.hasCompatibleArchive()) { + // Ignore items that have no archive compatible with the current platform. + continue; + } + + // Get the main package's API level. We don't need to look at the updates + // since by definition they should target the same API level. + int api = 0; + Package p = item.getMainPackage(); + if (p instanceof IAndroidVersionProvider) { + api = ((IAndroidVersionProvider) p).getAndroidVersion().getApiLevel(); + } + + if (selectTop && api > 0) { + // Keep track of the max api seen + maxApi = Math.max(maxApi, api); + + // keep track of what platform is currently installed (that is, has at least + // one thing installed.) + if (item.getState() == PkgState.INSTALLED) { + installedPlatforms.add(api); + } + + // for each platform, collect all its related item for later use below. + List items = platformItems.get(api); + if (items == null) { + platformItems.put(api, items = new ArrayList()); + } + items.add(item); + } + + if ((selectUpdates || selectNew) && + item.getState() == PkgState.NEW && + !item.getRevision().isPreview()) { + boolean sameFound = false; + Package newPkg = item.getMainPackage(); + if (newPkg instanceof IFullRevisionProvider) { + // We have a potential new non-preview package; but this kind of package + // supports having previews, which means we want to make sure we're not + // offering an older "new" non-preview if there's a newer preview installed. + // + // We should get into this odd situation only when updating an RC/preview + // by a final release pkg. + + IFullRevisionProvider newPkg2 = (IFullRevisionProvider) newPkg; + for (PkgItem item2 : allItems) { + if (item2.getState() == PkgState.INSTALLED) { + Package installed = item2.getMainPackage(); + + if (installed.getRevision().isPreview() && + newPkg2.sameItemAs(installed, PreviewComparison.IGNORE)) { + sameFound = true; + + if (installed.canBeUpdatedBy(newPkg) == UpdateInfo.UPDATE) { + item.setChecked(true); + break; + } + } + } + } + } + + if (selectNew && !sameFound) { + item.setChecked(true); + } + + } else if (selectUpdates && item.hasUpdatePkg()) { + item.setChecked(true); + } + + // Keep track of the tools and offer to auto-select platform-tools/build-tools. + if (selectTop) { + if (p instanceof ToolPackage && p.isLocal()) { + hasTools = true; // main tool package is installed. + } else if (p instanceof PlatformToolPackage || p instanceof BuildToolPackage) { + for (Class clazz : toolsCandidates.keySet()) { + if (clazz.isInstance(p)) { // allow p to be a mock-derived class + if (p.isLocal()) { + // There's one such package installed, we don't need candidates. + toolsCandidates.remove(clazz); + } else if (toolsCandidates.containsKey(clazz)) { + Pair val = toolsCandidates.get(clazz); + FullRevision rev = p.getRevision(); + if (!rev.isPreview()) { + // Don't auto-select previews. + if (val.getSecond() == null || + rev.compareTo(val.getSecond()) > 0) { + // No revision: set the first candidate. + // Or we found a new higher revision + toolsCandidates.put(clazz, Pair.of(item, rev)); + } + } + } + break; + } + } + } + } + } + + // Select the top platform/build-tool found above if needed. + if (selectTop && hasTools) { + for (Pair candidate : toolsCandidates.values()) { + PkgItem item = candidate.getFirst(); + if (item != null) { + item.setChecked(true); + } + } + } + + + // Select top platform items. + + List items = platformItems.get(maxApi); + if (selectTop && maxApi > 0 && items != null) { + if (!installedPlatforms.contains(maxApi)) { + // If the top platform has nothing installed at all, select everything in it + for (PkgItem item : items) { + if ((item.getState() == PkgState.NEW && !item.getRevision().isPreview()) || + item.hasUpdatePkg()) { + item.setChecked(true); + } + } + + } else { + // The top platform has at least one thing installed. + + // First make sure the platform package itself is installed, or select it. + for (PkgItem item : items) { + Package p = item.getMainPackage(); + if (p instanceof PlatformPackage && + item.getState() == PkgState.NEW && !item.getRevision().isPreview()) { + item.setChecked(true); + break; + } + } + + // Check we have at least one system image installed, otherwise select them + boolean hasSysImg = false; + for (PkgItem item : items) { + Package p = item.getMainPackage(); + if (p instanceof PlatformPackage && item.getState() == PkgState.INSTALLED) { + if (item.hasUpdatePkg() && item.isChecked()) { + // If the installed platform is scheduled for update, look for the + // system image in the update package, not the current one. + p = item.getUpdatePkg(); + if (p instanceof PlatformPackage) { + hasSysImg = ((PlatformPackage) p).getIncludedAbi() != null; + } + } else { + // Otherwise look into the currently installed platform + hasSysImg = ((PlatformPackage) p).getIncludedAbi() != null; + } + if (hasSysImg) { + break; + } + } + if (p instanceof SystemImagePackage && + ((SystemImagePackage) p).isPlatform() && + item.getState() == PkgState.INSTALLED) { + hasSysImg = true; + break; + } + } + if (!hasSysImg) { + // No system image installed. + // Try whether the current platform or its update would bring one. + + for (PkgItem item : items) { + Package p = item.getMainPackage(); + if (p instanceof PlatformPackage) { + if (item.getState() == PkgState.NEW && + !item.getRevision().isPreview() && + ((PlatformPackage) p).getIncludedAbi() != null) { + item.setChecked(true); + hasSysImg = true; + } else if (item.hasUpdatePkg()) { + p = item.getUpdatePkg(); + if (p instanceof PlatformPackage && + ((PlatformPackage) p).getIncludedAbi() != null) { + item.setChecked(true); + hasSysImg = true; + } + } + } + } + } + if (!hasSysImg) { + // No system image in the platform, try a system image package + for (PkgItem item : items) { + Package p = item.getMainPackage(); + if (p instanceof SystemImagePackage && + ((SystemImagePackage) p).isPlatform() && + item.getState() == PkgState.NEW) { + item.setChecked(true); + } + } + } + } + } + + if (selectTop) { + for (PkgItem item : getAllPkgItems()) { + Package p = item.getMainPackage(); + if (p instanceof ExtraPackage && + item.getState() == PkgState.NEW && + !item.getRevision().isPreview()) { + ExtraPackage ep = (ExtraPackage) p; + + // On Windows, we'll also auto-select the USB driver + if (currentPlatform == SdkConstants.PLATFORM_WINDOWS) { + if (ep.getVendorId().equals("google") && //$NON-NLS-1$ + ep.getPath().equals("usb_driver")) { //$NON-NLS-1$ + item.setChecked(true); + continue; + } + } + + // On all platforms, we'll auto-select the support library. + if (ep.getVendorId().equals("android") && //$NON-NLS-1$ + ep.getPath().equals("support")) { //$NON-NLS-1$ + item.setChecked(true); + continue; + } + + } + } + } + } + + /** + * Mark all PkgItems as not checked. + */ + public void uncheckAllItems() { + for (PkgItem item : getAllPkgItems()) { + item.setChecked(false); + } + } + + /** + * An update operation, customized to either sort by API or sort by source. + */ + abstract class UpdateOp { + private final Set mVisitedSources = new HashSet(); + private final List mCategories = new ArrayList(); + private final Set mCatsToRemove = new HashSet(); + private final Set mItemsToRemove = new HashSet(); + private final Map mUpdatesToRemove = new HashMap(); + + /** Removes all internal state. */ + public void clear() { + mVisitedSources.clear(); + mCategories.clear(); + } + + /** Retrieve the sorted category list. */ + public List getCategories() { + return mCategories; + } + + /** Retrieve the category key for the given package, either local or remote. */ + public abstract Object getCategoryKey(Package pkg); + + /** Modified {@code currentCategories} to add default categories. */ + public abstract void addDefaultCategories(); + + /** Creates the category for the given key and returns it. */ + public abstract PkgCategory createCategory(Object catKey); + /** Adjust attributes of an existing category. */ + public abstract void adjustCategory(PkgCategory cat, Object catKey); + + /** Sorts the category list (but not the items within the categories.) */ + public abstract void sortCategoryList(); + + /** Called after items of a given category have changed. Used to sort the + * items and/or adjust the category name. */ + public abstract void postCategoryItemsChanged(); + + public void updateStart() { + mVisitedSources.clear(); + + // Note that default categories are created after the unused ones so that + // the callback can decide whether they should be marked as unused or not. + mCatsToRemove.clear(); + mItemsToRemove.clear(); + mUpdatesToRemove.clear(); + for (PkgCategory cat : mCategories) { + mCatsToRemove.add(cat); + List items = cat.getItems(); + mItemsToRemove.addAll(items); + for (PkgItem item : items) { + if (item.hasUpdatePkg()) { + mUpdatesToRemove.put(item.getUpdatePkg(), item); + } + } + } + + addDefaultCategories(); + } + + public boolean updateSourcePackages(SdkSource source, Package[] newPackages) { + mVisitedSources.add(source); + if (source == null) { + return processLocals(this, newPackages); + } else { + return processSource(this, source, newPackages); + } + } + + public boolean updateEnd() { + boolean hasChanged = false; + + // Remove unused categories & items at the end of the update + synchronized (mCategories) { + for (PkgCategory unusedCat : mCatsToRemove) { + if (mCategories.remove(unusedCat)) { + hasChanged = true; + } + } + } + + for (PkgCategory cat : mCategories) { + for (Iterator itemIt = cat.getItems().iterator(); itemIt.hasNext(); ) { + PkgItem item = itemIt.next(); + if (mItemsToRemove.contains(item)) { + itemIt.remove(); + hasChanged = true; + } else if (item.hasUpdatePkg() && + mUpdatesToRemove.containsKey(item.getUpdatePkg())) { + item.removeUpdate(); + hasChanged = true; + } + } + } + + mCatsToRemove.clear(); + mItemsToRemove.clear(); + mUpdatesToRemove.clear(); + + return hasChanged; + } + + public boolean isKeep(PkgItem item) { + return !mItemsToRemove.contains(item); + } + + public void keep(Package pkg) { + mUpdatesToRemove.remove(pkg); + } + + public void keep(PkgItem item) { + mItemsToRemove.remove(item); + } + + public void keep(PkgCategory cat) { + mCatsToRemove.remove(cat); + } + + public void dontKeep(PkgItem item) { + mItemsToRemove.add(item); + } + + public void dontKeep(PkgCategory cat) { + mCatsToRemove.add(cat); + } + } + + private final UpdateOpApi mOpApi = new UpdateOpApi(); + + public List getCategories() { + return mOpApi.getCategories(); + } + + public List getAllPkgItems() { + List items = new ArrayList(); + + List cats = getCategories(); + synchronized (cats) { + for (PkgCategory cat : cats) { + items.addAll(cat.getItems()); + } + } + + return items; + } + + public void updateStart() { + mOpApi.updateStart(); + } + + public boolean updateSourcePackages(SdkSource source, Package[] newPackages) { + + return mOpApi.updateSourcePackages(source, newPackages); + } + + public boolean updateEnd() { + return mOpApi.updateEnd(); + } + + + /** Process all local packages. Returns true if something changed. */ + private boolean processLocals(UpdateOp op, Package[] packages) { + boolean hasChanged = false; + List cats = op.getCategories(); + Set keep = new HashSet(); + + // For all locally installed packages, check they are either listed + // as installed or create new installed items for them. + + nextPkg: for (Package localPkg : packages) { + // Check to see if we already have the exact same package + // (type & revision) marked as installed. + for (PkgCategory cat : cats) { + for (PkgItem currItem : cat.getItems()) { + if (currItem.getState() == PkgState.INSTALLED && + currItem.isSameMainPackageAs(localPkg)) { + // This package is already listed as installed. + op.keep(currItem); + op.keep(cat); + keep.add(currItem); + continue nextPkg; + } + } + } + + // If not found, create a new installed package item + keep.add(addNewItem(op, localPkg, PkgState.INSTALLED)); + hasChanged = true; + } + + // Remove installed items that we don't want to keep anymore. They would normally be + // cleanup up in UpdateOp.updateEnd(); however it's easier to remove them before we + // run processSource() to avoid merging updates in items that would be removed later. + + for (PkgCategory cat : cats) { + for (Iterator itemIt = cat.getItems().iterator(); itemIt.hasNext(); ) { + PkgItem item = itemIt.next(); + if (item.getState() == PkgState.INSTALLED && !keep.contains(item)) { + itemIt.remove(); + hasChanged = true; + } + } + } + + if (hasChanged) { + op.postCategoryItemsChanged(); + } + + return hasChanged; + } + + /** + * {@link PkgState}s to check in {@link #processSource(UpdateOp, SdkSource, Package[])}. + * The order matters. + * When installing the diff will have both the new and the installed item and we + * need to merge with the installed one before the new one. + */ + private final static PkgState[] PKG_STATES = { PkgState.INSTALLED, PkgState.NEW }; + + /** Process all remote packages. Returns true if something changed. */ + private boolean processSource(UpdateOp op, SdkSource source, Package[] packages) { + boolean hasChanged = false; + List cats = op.getCategories(); + + boolean enablePreviews = + mUpdaterData.getSettingsController().getSettings().getEnablePreviews(); + + nextPkg: for (Package newPkg : packages) { + + if (!enablePreviews && newPkg.getRevision().isPreview()) { + // This is a preview and previews are not enabled. Ignore the package. + // Starting with Tools 23, we explicitly allows Build-Tools RC packages to + // always be visible so only RCs for Tools & Platform-Tools will be hidden. + if (!(newPkg instanceof BuildToolPackage)) { + continue nextPkg; + } + } + + for (PkgCategory cat : cats) { + for (PkgState state : PKG_STATES) { + for (Iterator currItemIt = cat.getItems().iterator(); + currItemIt.hasNext(); ) { + PkgItem currItem = currItemIt.next(); + // We need to merge with installed items first. When installing + // the diff will have both the new and the installed item and we + // need to merge with the installed one before the new one. + if (currItem.getState() != state) { + continue; + } + // Only process current items if they represent the same item (but + // with a different revision number) than the new package. + Package mainPkg = currItem.getMainPackage(); + if (!mainPkg.sameItemAs(newPkg)) { + continue; + } + + // Check to see if we already have the exact same package + // (type & revision) marked as main or update package. + if (currItem.isSameMainPackageAs(newPkg)) { + op.keep(currItem); + op.keep(cat); + continue nextPkg; + } else if (currItem.hasUpdatePkg() && + currItem.isSameUpdatePackageAs(newPkg)) { + op.keep(currItem.getUpdatePkg()); + op.keep(cat); + continue nextPkg; + } + + switch (currItem.getState()) { + case NEW: + if (newPkg.getRevision().compareTo(mainPkg.getRevision()) < 0) { + if (!op.isKeep(currItem)) { + // The new item has a lower revision than the current one, + // but the current one hasn't been marked as being kept so + // it's ok to downgrade it. + currItemIt.remove(); + addNewItem(op, newPkg, PkgState.NEW); + hasChanged = true; + } + } else if (newPkg.getRevision().compareTo(mainPkg.getRevision()) > 0) { + // We have a more recent new version, remove the current one + // and replace by a new one + currItemIt.remove(); + addNewItem(op, newPkg, PkgState.NEW); + hasChanged = true; + } + break; + case INSTALLED: + // if newPkg.revision<=mainPkg.revision: it's already installed, ignore. + if (newPkg.getRevision().compareTo(mainPkg.getRevision()) > 0) { + // This is a new update for the main package. + if (currItem.mergeUpdate(newPkg)) { + op.keep(currItem.getUpdatePkg()); + op.keep(cat); + hasChanged = true; + } + } + break; + } + continue nextPkg; + } + } + } + // If not found, create a new package item + addNewItem(op, newPkg, PkgState.NEW); + hasChanged = true; + } + + if (hasChanged) { + op.postCategoryItemsChanged(); + } + + return hasChanged; + } + + private PkgItem addNewItem(UpdateOp op, Package pkg, PkgState state) { + List cats = op.getCategories(); + Object catKey = op.getCategoryKey(pkg); + PkgCategory cat = findCurrentCategory(cats, catKey); + + if (cat == null) { + // This is a new category. Create it and add it to the list. + cat = op.createCategory(catKey); + synchronized (cats) { + cats.add(cat); + } + op.sortCategoryList(); + } else { + // Not a new category. Give op a chance to adjust the category attributes + op.adjustCategory(cat, catKey); + } + + PkgItem item = new PkgItem(pkg, state); + op.keep(item); + cat.getItems().add(item); + op.keep(cat); + return item; + } + + private PkgCategory findCurrentCategory( + List currentCategories, + Object categoryKey) { + for (PkgCategory cat : currentCategories) { + if (cat.getKey().equals(categoryKey)) { + return cat; + } + } + return null; + } + + /** + * {@link UpdateOp} describing the Sort-by-API operation. + */ + private class UpdateOpApi extends UpdateOp { + @Override + public Object getCategoryKey(Package pkg) { + // Sort by API + + if (pkg instanceof IAndroidVersionProvider) { + return ((IAndroidVersionProvider) pkg).getAndroidVersion(); + + } else if (pkg instanceof ToolPackage || + pkg instanceof PlatformToolPackage || + pkg instanceof BuildToolPackage) { + if (pkg.getRevision().isPreview()) { + return PkgCategoryApi.KEY_TOOLS_PREVIEW; + } else { + return PkgCategoryApi.KEY_TOOLS; + } + } else { + return PkgCategoryApi.KEY_EXTRA; + } + } + + @Override + public void addDefaultCategories() { + boolean needTools = true; + boolean needExtras = true; + + List cats = getCategories(); + for (PkgCategory cat : cats) { + if (cat.getKey().equals(PkgCategoryApi.KEY_TOOLS)) { + // Mark them as not unused to prevent their removal in updateEnd(). + keep(cat); + needTools = false; + } else if (cat.getKey().equals(PkgCategoryApi.KEY_EXTRA)) { + keep(cat); + needExtras = false; + } + } + + // Always add the tools & extras categories, even if empty (unlikely anyway) + if (needTools) { + PkgCategoryApi acat = new PkgCategoryApi( + PkgCategoryApi.KEY_TOOLS, + null, + mUpdaterData.getImageFactory().getImageByName(PackagesPageIcons.ICON_CAT_OTHER)); + synchronized (cats) { + cats.add(acat); + } + } + + if (needExtras) { + PkgCategoryApi acat = new PkgCategoryApi( + PkgCategoryApi.KEY_EXTRA, + null, + mUpdaterData.getImageFactory().getImageByName(PackagesPageIcons.ICON_CAT_OTHER)); + synchronized (cats) { + cats.add(acat); + } + } + } + + @Override + public PkgCategory createCategory(Object catKey) { + // Create API category. + PkgCategory cat = null; + + assert catKey instanceof AndroidVersion; + AndroidVersion key = (AndroidVersion) catKey; + + // We should not be trying to recreate the tools or extra categories. + assert !key.equals(PkgCategoryApi.KEY_TOOLS) && !key.equals(PkgCategoryApi.KEY_EXTRA); + + // We need a label for the category. + // If we have an API level, try to get the info from the SDK Manager. + // If we don't (e.g. when installing a new platform that isn't yet available + // locally in the SDK Manager), it's OK we'll try to find the first platform + // package available. + String platformName = null; + for (IAndroidTarget target : + mUpdaterData.getSdkManager().getTargets()) { + if (target.isPlatform() && key.equals(target.getVersion())) { + platformName = target.getVersionName(); + break; + } + } + + cat = new PkgCategoryApi( + key, + platformName, + mUpdaterData.getImageFactory().getImageByName(PackagesPageIcons.ICON_CAT_PLATFORM)); + + return cat; + } + + @Override + public void adjustCategory(PkgCategory cat, Object catKey) { + // Pass. Nothing to do for API-sorted categories + } + + @Override + public void sortCategoryList() { + // Sort the categories list. + // We always want categories in order tools..platforms..extras. + // For platform, we compare in descending order (o2-o1). + // This order is achieved by having the category keys ordered as + // needed for the sort to just do what we expect. + + synchronized (getCategories()) { + Collections.sort(getCategories(), new Comparator() { + @Override + public int compare(PkgCategory cat1, PkgCategory cat2) { + assert cat1 instanceof PkgCategoryApi; + assert cat2 instanceof PkgCategoryApi; + assert cat1.getKey() instanceof AndroidVersion; + assert cat2.getKey() instanceof AndroidVersion; + AndroidVersion v1 = (AndroidVersion) cat1.getKey(); + AndroidVersion v2 = (AndroidVersion) cat2.getKey(); + return v2.compareTo(v1); + } + }); + } + } + + @Override + public void postCategoryItemsChanged() { + // Sort the items + for (PkgCategory cat : getCategories()) { + Collections.sort(cat.getItems()); + + // When sorting by API, we can't always get the platform name + // from the package manager. In this case at the very end we + // look for a potential platform package we can use to extract + // the platform version name (e.g. '1.5') from the first suitable + // platform package we can find. + + assert cat instanceof PkgCategoryApi; + PkgCategoryApi pac = (PkgCategoryApi) cat; + if (pac.getPlatformName() == null) { + // Check whether we can get the actual platform version name (e.g. "1.5") + // from the first Platform package we find in this category. + + for (PkgItem item : cat.getItems()) { + Package p = item.getMainPackage(); + if (p instanceof PlatformPackage) { + String platformName = ((PlatformPackage) p).getVersionName(); + if (platformName != null) { + pac.setPlatformName(platformName); + break; + } + } + } + } + } + + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgCategory.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgCategory.java new file mode 100644 index 00000000..7eb57e67 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgCategory.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2011 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.sdkuilib.internal.repository.core; + + +import com.android.sdklib.internal.repository.updater.PkgItem; + +import java.util.ArrayList; +import java.util.List; + +public abstract class PkgCategory { + private final Object mKey; + private final Object mIconRef; + private final List mItems = new ArrayList(); + private String mLabel; + + public PkgCategory(Object key, String label, Object iconRef) { + mKey = key; + mLabel = label; + mIconRef = iconRef; + } + + public Object getKey() { + return mKey; + } + + public String getLabel() { + return mLabel; + } + + public void setLabel(String label) { + mLabel = label; + } + + public Object getIconRef() { + return mIconRef; + } + + public List getItems() { + return mItems; + } + + @Override + public String toString() { + return String.format("%s ", + this.getClass().getSimpleName(), + mKey == null ? "null" : mKey.toString(), + mLabel, + mItems.size()); + } + + /** {@link PkgCategory}s are equal if their internal keys are equal. */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((mKey == null) ? 0 : mKey.hashCode()); + return result; + } + + /** {@link PkgCategory}s are equal if their internal keys are equal. */ + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + PkgCategory other = (PkgCategory) obj; + if (mKey == null) { + if (other.mKey != null) return false; + } else if (!mKey.equals(other.mKey)) return false; + return true; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgCategoryApi.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgCategoryApi.java new file mode 100644 index 00000000..89a4914a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgCategoryApi.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2011 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.sdkuilib.internal.repository.core; + +import com.android.sdklib.AndroidVersion; + + +public class PkgCategoryApi extends PkgCategory { + + /** Platform name, in the form "Android 1.2". Can be null if we don't have the name. */ + private String mPlatformName; + + // When sorting by Source, key is the hash of the source's name. + // When storing by API, key is the AndroidVersion (API level >=1 + optional codename). + // We always want categories in order tools..platforms..extras; to achieve that tools + // and extras have the special values so they get "naturally" sorted the way we want + // them. + // (Note: don't use integer.max to avoid integers wrapping in comparisons. We can + // revisit the day we get 2^30 platforms.) + public final static AndroidVersion KEY_TOOLS = new AndroidVersion(Integer.MAX_VALUE / 2, null); + public final static AndroidVersion KEY_TOOLS_PREVIEW = + new AndroidVersion(Integer.MAX_VALUE / 2 - 1, null); + public final static AndroidVersion KEY_EXTRA = new AndroidVersion(-1, null); + + public PkgCategoryApi(AndroidVersion version, String platformName, Object iconRef) { + super(version, null /*label*/, iconRef); + setPlatformName(platformName); + } + + public String getPlatformName() { + return mPlatformName; + } + + public void setPlatformName(String platformName) { + if (platformName != null) { + // Normal case for actual platform categories + mPlatformName = String.format("Android %1$s", platformName); + super.setLabel(null); + } + } + + public String getApiLabel() { + AndroidVersion key = (AndroidVersion) getKey(); + if (key.equals(KEY_TOOLS)) { + return "TOOLS"; //$NON-NLS-1$ // for internal debug use only + } else if (key.equals(KEY_TOOLS_PREVIEW)) { + return "TOOLS-PREVIEW"; //$NON-NLS-1$ // for internal debug use only + } else if (key.equals(KEY_EXTRA)) { + return "EXTRAS"; //$NON-NLS-1$ // for internal debug use only + } else { + return key.toString(); + } + } + + @Override + public String getLabel() { + String label = super.getLabel(); + if (label == null) { + AndroidVersion key = (AndroidVersion) getKey(); + + if (key.equals(KEY_TOOLS)) { + label = "Tools"; + } else if (key.equals(KEY_TOOLS_PREVIEW)) { + label = "Tools (Preview Channel)"; + } else if (key.equals(KEY_EXTRA)) { + label = "Extras"; + } else { + if (mPlatformName != null) { + label = String.format("%1$s (%2$s)", mPlatformName, getApiLabel()); + } else { + label = getApiLabel(); + } + } + super.setLabel(label); + } + return label; + } + + @Override + public void setLabel(String label) { + throw new UnsupportedOperationException("Use setPlatformName() instead."); + } + + @Override + public String toString() { + return String.format("%s ", + this.getClass().getSimpleName(), + getApiLabel(), + getLabel(), + getItems().size()); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgCategorySource.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgCategorySource.java new file mode 100644 index 00000000..463049ce --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgCategorySource.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2011 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.sdkuilib.internal.repository.core; + +import com.android.sdklib.internal.repository.sources.SdkRepoSource; +import com.android.sdklib.internal.repository.sources.SdkSource; +import com.android.sdkuilib.internal.repository.SwtUpdaterData; +import com.android.sdkuilib.internal.repository.ui.PackagesPageIcons; + + +public class PkgCategorySource extends PkgCategory { + + /** + * A special {@link SdkSource} object that represents the locally installed + * items, or more exactly a lack of remote source. + */ + public final static SdkSource UNKNOWN_SOURCE = + new SdkRepoSource("http://no.source", "Local Packages"); + private final SdkSource mSource; + + /** + * Creates a new {@link PkgCategorySource}. + * This uses {@link SdkSource#toString()} to get the source's description. + * Note that if the name of the source isn't known, the description will use its URL. + */ + public PkgCategorySource(SdkSource source, SwtUpdaterData swtUpdaterData) { + super( + source, // the source is the key and it can be null + source == UNKNOWN_SOURCE ? "Local Packages" : source.toString(), + source == UNKNOWN_SOURCE ? + swtUpdaterData.getImageFactory() + .getImageByName(PackagesPageIcons.ICON_PKG_INSTALLED) : + source); + mSource = source; + } + + @Override + public String toString() { + return String.format("%s ", + this.getClass().getSimpleName(), + mSource.toString(), + getItems().size()); + } + + public SdkSource getSource() { + return mSource; + } + + /** Sets the label to match the source's UI name if the label wasn't already set. */ + public void adjustLabel(SdkSource source) { + if (getLabel() == null || getLabel().startsWith("http")) { //$NON-NLS-1$ + setLabel(source == UNKNOWN_SOURCE ? "Local Packages" : source.toString()); + } + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgContentProvider.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgContentProvider.java new file mode 100644 index 00000000..34bf17de --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgContentProvider.java @@ -0,0 +1,237 @@ +/* + * Copyright (C) 2012 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.sdkuilib.internal.repository.core; + +import com.android.sdklib.internal.repository.archives.Archive; +import com.android.sdklib.internal.repository.packages.Package; +import com.android.sdklib.internal.repository.sources.SdkSource; +import com.android.sdklib.internal.repository.updater.PkgItem; +import com.android.sdklib.repository.IDescription; +import com.android.sdkuilib.internal.repository.ui.PackagesPage; + +import org.eclipse.jface.viewers.IInputProvider; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.Viewer; + +import java.util.ArrayList; +import java.util.List; + +/** + * Content provider for the main tree view in {@link PackagesPage}. + */ +public class PkgContentProvider implements ITreeContentProvider { + + private final IInputProvider mViewer; + private boolean mDisplayArchives; + + public PkgContentProvider(IInputProvider viewer) { + mViewer = viewer; + } + + public void setDisplayArchives(boolean displayArchives) { + mDisplayArchives = displayArchives; + } + + @Override + public Object[] getChildren(Object parentElement) { + if (parentElement instanceof ArrayList) { + return ((ArrayList) parentElement).toArray(); + + } else if (parentElement instanceof PkgCategorySource) { + return getSourceChildren((PkgCategorySource) parentElement); + + } else if (parentElement instanceof PkgCategory) { + return ((PkgCategory) parentElement).getItems().toArray(); + + } else if (parentElement instanceof PkgItem) { + if (mDisplayArchives) { + + Package pkg = ((PkgItem) parentElement).getUpdatePkg(); + + // Display update packages as sub-items if the details mode is activated. + if (pkg != null) { + return new Object[] { pkg }; + } + + return ((PkgItem) parentElement).getArchives(); + } + + } else if (parentElement instanceof Package) { + if (mDisplayArchives) { + return ((Package) parentElement).getArchives(); + } + + } + + return new Object[0]; + } + + @Override + @SuppressWarnings("unchecked") + public Object getParent(Object element) { + // This operation is expensive, so we do the minimum + // and don't try to cover all cases. + + if (element instanceof PkgItem) { + Object input = mViewer.getInput(); + if (input != null) { + for (PkgCategory cat : (List) input) { + if (cat.getItems().contains(element)) { + return cat; + } + } + } + } + + return null; + } + + @Override + public boolean hasChildren(Object parentElement) { + if (parentElement instanceof ArrayList) { + return true; + + } else if (parentElement instanceof PkgCategory) { + return true; + + } else if (parentElement instanceof PkgItem) { + if (mDisplayArchives) { + Package pkg = ((PkgItem) parentElement).getUpdatePkg(); + + // Display update packages as sub-items if the details mode is activated. + if (pkg != null) { + return true; + } + + Archive[] archives = ((PkgItem) parentElement).getArchives(); + return archives.length > 0; + } + } else if (parentElement instanceof Package) { + if (mDisplayArchives) { + return ((Package) parentElement).getArchives().length > 0; + } + } + + return false; + } + + @Override + public Object[] getElements(Object inputElement) { + return getChildren(inputElement); + } + + @Override + public void dispose() { + // unused + + } + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // unused + } + + + private Object[] getSourceChildren(PkgCategorySource parentElement) { + List children = parentElement.getItems(); + + SdkSource source = parentElement.getSource(); + IDescription error = null; + IDescription empty = null; + + String errStr = source.getFetchError(); + if (errStr != null) { + error = new RepoSourceError(source); + } + if (!source.isEnabled() || children.isEmpty()) { + empty = new RepoSourceNotification(source); + } + + if (error != null || empty != null) { + ArrayList children2 = new ArrayList(); + if (error != null) { + children2.add(error); + } + if (empty != null) { + children2.add(empty); + } + children2.addAll(children); + children = children2; + } + + return children.toArray(); + } + + + /** + * A dummy entry returned for sources which had load errors. + * It displays a summary of the error as its short description or + * it displays the source's long description. + */ + public static class RepoSourceError implements IDescription { + + private final SdkSource mSource; + + public RepoSourceError(SdkSource source) { + mSource = source; + } + + @Override + public String getLongDescription() { + return mSource.getLongDescription(); + } + + @Override + public String getShortDescription() { + return mSource.getFetchError(); + } + } + + /** + * A dummy entry returned for sources with no packages. + * We need that to force the SWT tree to display an open/close triangle + * even for empty sources. + */ + public static class RepoSourceNotification implements IDescription { + + private final SdkSource mSource; + + public RepoSourceNotification(SdkSource source) { + mSource = source; + } + + @Override + public String getLongDescription() { + if (mSource.isEnabled()) { + return mSource.getLongDescription(); + } else { + return "Loading from this site has been disabled. " + + "To enable it, use Tools > Manage Add-ons Sites."; + } + } + + @Override + public String getShortDescription() { + if (mSource.isEnabled()) { + return "No packages found."; + } else { + return "This site is disabled. "; + } + } + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/SdkLogAdapter.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/SdkLogAdapter.java new file mode 100644 index 00000000..cbc33950 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/SdkLogAdapter.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2011 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.sdkuilib.internal.repository.core; + +import com.android.sdkuilib.internal.tasks.ILogUiProvider; +import com.android.utils.ILogger; + + +/** + * Adapter that transform log from an {@link ILogUiProvider} to an {@link ILogger}. + */ +public final class SdkLogAdapter implements ILogUiProvider { + + private ILogger mSdkLog; + private String mLastLogMsg; + + /** + * Creates a new adapter to output log on the given {@code sdkLog}. + * + * @param sdkLog The logger to output to. Must not be null. + */ + public SdkLogAdapter(ILogger sdkLog) { + mSdkLog = sdkLog; + } + + /** + * Sets the description in the current task dialog. + * This method can be invoked from a non-UI thread. + */ + @Override + public void setDescription(final String description) { + if (acceptLog(description)) { + mSdkLog.info("%1$s", description); //$NON-NLS-1$ + } + } + + /** + * Logs a "normal" information line. + * This method can be invoked from a non-UI thread. + */ + @Override + public void log(String log) { + if (acceptLog(log)) { + mSdkLog.info(" %1$s", log); //$NON-NLS-1$ + } + } + + /** + * Logs an "error" information line. + * This method can be invoked from a non-UI thread. + */ + @Override + public void logError(String log) { + if (acceptLog(log)) { + mSdkLog.error(null, " %1$s", log); //$NON-NLS-1$ + } + } + + /** + * Logs a "verbose" information line, that is extra details which are typically + * not that useful for the end-user and might be hidden until explicitly shown. + * This method can be invoked from a non-UI thread. + */ + @Override + public void logVerbose(String log) { + if (acceptLog(log)) { + mSdkLog.verbose(" %1$s", log); //$NON-NLS-1$ + } + } + + // ---- + + /** + * Filter messages displayed in the log:
+ * - Messages with a % are typical part of a progress update and shouldn't be in the log.
+ * - Messages that are the same as the same output message should be output a second time. + * + * @param msg The potential log line to print. + * @return True if the log line should be printed, false otherwise. + */ + private boolean acceptLog(String msg) { + if (msg == null) { + return false; + } + + msg = msg.trim(); + if (msg.indexOf('%') != -1) { + return false; + } + + if (msg.equals(mLastLogMsg)) { + return false; + } + + mLastLogMsg = msg; + return true; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/SwtPackageLoader.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/SwtPackageLoader.java new file mode 100644 index 00000000..d5e7982c --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/SwtPackageLoader.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2013 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.sdkuilib.internal.repository.core; + +import com.android.annotations.NonNull; +import com.android.sdklib.internal.repository.DownloadCache; +import com.android.sdklib.internal.repository.updater.PackageLoader; +import com.android.sdkuilib.internal.repository.SwtUpdaterData; + +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +/** + * Loads packages fetched from the remote SDK Repository and keeps track + * of their state compared with the current local SDK installation. + */ +public class SwtPackageLoader extends PackageLoader { + + /** + * Creates a new PackageManager associated with the given {@link SwtUpdaterData} + * and using the {@link SwtUpdaterData}'s default {@link DownloadCache}. + * + * @param swtUpdaterData The {@link SwtUpdaterData}. Must not be null. + */ + public SwtPackageLoader(SwtUpdaterData swtUpdaterData) { + super(swtUpdaterData); + } + + /** + * Creates a new PackageManager associated with the given {@link SwtUpdaterData} + * but using the specified {@link DownloadCache} instead of the one from + * {@link SwtUpdaterData}. + * + * @param swtUpdaterData The {@link SwtUpdaterData}. Must not be null. + * @param cache The {@link DownloadCache} to use instead of the one from {@link SwtUpdaterData}. + */ + public SwtPackageLoader(SwtUpdaterData swtUpdaterData, DownloadCache cache) { + super(swtUpdaterData, cache); + } + + /** + * Runs the runnable on the UI thread using {@link Display#syncExec(Runnable)}. + * + * @param r Non-null runnable. + */ + @Override + protected void runOnUiThread(@NonNull Runnable r) { + SwtUpdaterData swtUpdaterData = (SwtUpdaterData) getUpdaterData(); + Shell shell = swtUpdaterData.getWindowShell(); + + if (shell != null && !shell.isDisposed()) { + shell.getDisplay().syncExec(r); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/ImageFactory.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/ImageFactory.java new file mode 100644 index 00000000..d985162f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/ImageFactory.java @@ -0,0 +1,205 @@ +/* + * 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.sdkuilib.internal.repository.icons; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdklib.internal.repository.archives.Archive; +import com.android.sdklib.internal.repository.packages.Package; +import com.android.sdklib.internal.repository.sources.SdkSource; +import com.android.sdkuilib.internal.repository.core.PkgContentProvider; + +import org.eclipse.swt.SWTException; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; + +import java.io.InputStream; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; + + +/** + * An utility class to serve {@link Image} correspond to the various icons + * present in this package and dispose of them correctly at the end. + */ +public class ImageFactory { + + private final Display mDisplay; + private final Map mImages = new HashMap(); + + /** + * Filter an image when it's loaded by + * {@link ImageFactory#getImageByName(String, String, Filter)}. + */ + public interface Filter { + /** + * Invoked by {@link ImageFactory#getImageByName(String, String, Filter)} when + * a non-null {@link Image} object has been loaded. The filter should create a + * new image, modifying it as needed.
+ * If no modification is necessary, the filter can simply return the source image.
+ * The result will be cached and returned by {@link ImageFactory}. + *

+ * + * @param source A non-null source image. + * @return Either the source or a new, potentially modified, image. + */ + @NonNull public Image filter(@NonNull Image source); + } + + public ImageFactory(@NonNull Display display) { + mDisplay = display; + } + + /** + * Loads an image given its filename (with its extension). + * Might return null if the image cannot be loaded.
+ * The image is cached. Successive calls will return the same object.
+ * The image is automatically disposed when {@link ImageFactory} is disposed. + * + * @param imageName The filename (with extension) of the image to load. + * @return A new or existing {@link Image}. The caller must NOT dispose the image (the + * image will disposed by {@link #dispose()}). The returned image can be null if the + * expected file is missing. + */ + @Nullable + public Image getImageByName(@NonNull String imageName) { + return getImageByName(imageName, imageName, null); + } + + + /** + * Loads an image given its filename (with its extension), caches it using the given + * {@code KeyName} name and optionally applies a filter to it. + * Might return null if the image cannot be loaded. + * The image is cached. Successive calls using {@code KeyName} will return the same + * object directly (the filter is not re-applied in this case.)
+ * The image is automatically disposed when {@link ImageFactory} is disposed. + *

+ * + * @param imageName The filename (with extension) of the image to load. + * @return A new or existing {@link Image}. The caller must NOT dispose the image (the + * image will disposed by {@link #dispose()}). The returned image is null if the + * expected file is missing. + */ + @Nullable + public Image getImageByName(@NonNull String imageName, + @NonNull String keyName, + @Nullable Filter filter) { + + Image image = mImages.get(keyName); + if (image != null) { + return image; + } + + InputStream stream = getClass().getResourceAsStream(imageName); + if (stream != null) { + try { + image = new Image(mDisplay, stream); + if (image != null && filter != null) { + Image image2 = filter.filter(image); + if (image2 != image && !image.isDisposed()) { + image.dispose(); + } + image = image2; + } + } catch (SWTException e) { + // ignore + } catch (IllegalArgumentException e) { + // ignore + } + } + + // Store the image in the hash, even if this failed. If it fails now, it will fail later. + mImages.put(keyName, image); + + return image; + } + + /** + * Loads and returns the appropriate image for a given package, archive or source object. + * The image is cached. Successive calls will return the same object. + * + * @param object A {@link SdkSource} or {@link Package} or {@link Archive}. + * @return A new or existing {@link Image}. The caller must NOT dispose the image (the + * image will disposed by {@link #dispose()}). The returned image can be null if the + * object is of an unknown type. + */ + @Nullable + public Image getImageForObject(@Nullable Object object) { + + if (object == null) { + return null; + } + + if (object instanceof Image) { + return (Image) object; + } + + String clz = object.getClass().getSimpleName(); + if (clz.endsWith(Package.class.getSimpleName())) { + String name = clz.replaceFirst(Package.class.getSimpleName(), "") //$NON-NLS-1$ + .replace("SystemImage", "sysimg") //$NON-NLS-1$ //$NON-NLS-2$ + .toLowerCase(Locale.US); + name += "_pkg_16.png"; //$NON-NLS-1$ + return getImageByName(name); + } + + if (object instanceof PkgContentProvider.RepoSourceError) { + return getImageByName("error_icon_16.png"); //$NON-NLS-1$ + + } else if (object instanceof PkgContentProvider.RepoSourceNotification) { + return getImageByName("nopkg_icon_16.png"); //$NON-NLS-1$ + } + + if (object instanceof Archive) { + if (((Archive) object).isCompatible()) { + return getImageByName("archive_icon16.png"); //$NON-NLS-1$ + } else { + return getImageByName("incompat_icon16.png"); //$NON-NLS-1$ + } + } + + if (object instanceof String) { + return getImageByName((String) object); + } + + + if (object != null) { + // For debugging + // System.out.println("No image for object " + object.getClass().getSimpleName()); + } + + return null; + } + + /** + * Dispose all the images created by this factory so far. + */ + public void dispose() { + Iterator it = mImages.values().iterator(); + while(it.hasNext()) { + Image img = it.next(); + if (img != null && img.isDisposed() == false) { + img.dispose(); + } + it.remove(); + } + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/accept_icon16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/accept_icon16.png new file mode 100644 index 0000000000000000000000000000000000000000..ae61f7df993c36e0c20b51d0d7c1ef7d9d2800d1 GIT binary patch literal 3277 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 LNkvXXu0mjf76}mt literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/addon_pkg_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/addon_pkg_16.png new file mode 100644 index 0000000000000000000000000000000000000000..addef8ef3d92dbe4859f0095419a99c16058cde8 GIT binary patch literal 529 zcmV+s0`C2ZP)(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRZ-mq|oHRCwBy(?4z#K^O(_->lcMBO(w5f(p@4&>;H!k$eUm0iuXF0ir-oL(L)z z0^$NG5TH$poFEc%Ebb-JvtYf8CyNpY?g70B4t7pj2RkRN>aaX39{$T)C<9Rjc~o1UYvP~iBb@U+!><7VeHMNd T!P8!+00000NkvXXu0mjfffw7D literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/android_icon_128.png b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/android_icon_128.png new file mode 100644 index 0000000000000000000000000000000000000000..830c04b0beff37224ef4e0b32d68fae5db4daad1 GIT binary patch literal 17715 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

t3(1hb3FHT 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 ArT_o{ literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/android_icon_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/android_icon_16.png new file mode 100644 index 0000000000000000000000000000000000000000..08ffda85b67647a3a9871f813e025a93fcbab9e1 GIT binary patch literal 219 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`i#%N%Lo5W3PB0pn4*FPD(XOkD>*u9SLsJ5b5 z+J2prnF*;8P9IiGY;3E1>oALd(S4oE=h%Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_0004iNklHEU?j7kddOXU^OdcV{bAntYoKTp_E2eSXf$#Vlyd1v9J*v zmTERq4DXpY_q}5=%*-=m6sK<8bMHC-`TOsGm5A`4@D(RFyYnuz&gi206+uK05ssI~ zOcQE4d=;r?(zT6@9(IF^BFn%K{87=}UCOp{KhO9p?j^8q-H1O5O29)EzETA%@H zD=<$cViJk3D<6Pu+ep^6($K5%0yF)qm>HL9h zo!jNvX^jOyaP3FUCI2xG<}1lDRW%%lXt#e6fL}>Y4bhb5GY~{4c+SrRIwO*;o`3|C zNxtHw4gmBiPeXMxQNTM$;GzbiSjYynj&ZjbPFxl4ulHh~#vR1VUa^9sZ!3x5EkzX} zfzLqK1c3fD0BK#?0#%|lK-mZhV-EKwK@CVEC)ipUi}vgnccF=-oX#{^00000NkvXX Hu0mjfO6O;W literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/broken_pkg_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/broken_pkg_16.png new file mode 100644 index 0000000000000000000000000000000000000000..6daa67b87aab855155e38be57d1318b4eef031a3 GIT binary patch literal 281 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qucLCF%=h?3y^w370~qEv=}#LT=BJwMkFg)(D3 zQ$179u0IceiaI@A978Ppmrg#-+hD-sY|ZsM{~*`h7S0BRnARE{?~qrGt7^+0864aD zE!TQ#Zu#ld{(j4sZ3pxlIwdq5L)Z&uDrhNu%d`w+*yt`VF2^9cbmhs$003=u z0048K008{m004tz00404008W0001yP000n>N2bPDNB8b~7$DE;UJt{NMlp0SZY(K~y+Tjgmi0 zf>9L4--sHFRx=b7ksP8$@Dun26dc>6xv5X!;#Uf62pj}yigap73DL>LDN=t&o~y6v z&HKQQ_nzlG=f3}L6sp$qLj_)-3rX2w((o^g;Z}CI7J5l~z~5ZIY4g7onZwNS%f+uy;R|9VP~{@CkPJ&9My<1DDW-4%B5IQ53BJL8?ve literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/doc_pkg_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/doc_pkg_16.png new file mode 100644 index 0000000000000000000000000000000000000000..a2be37af0f8c0c0f3eb82f7c5c96f9064101afa7 GIT binary patch literal 290 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#S9GG!XV7ZFl&wkP*5S+ zBgmJ5A-9x)p?CoU!=Jkh49zJF3vBEC$x;#$5>knCo;*om0|7N5o$jQBgcZkMGN~NsaD8LX%-pQZ%zQXb zL;jtRTZF`cJq4|ivWFSw?l%wI!ybHjenRtARtbh>j?dqpO-S0)@Fu6E*CN@l* fbEZXPx#24YJ`L;(K)#sC1O1(pE-000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iXQ5 z1r#DXW@Zrp00HtzL_t(I%VpEMYZXBh$MMhXeI+4533h^534&r{ZDS)=B0lg@sc7K? zNsyquSokPJNMVseeDvZ!kj6r=6x6~_&_>Mey}Nh!-d&5Gn-mTl1`addbLMw`6PjC? z#{hkF5l}m7VpD0Na9r+_5k?rs$mp8G0_Gy5iv+cMO7nV1tl^9rQ&cF?n*nB!`8FxB z-_%(p3N%ieI$+}4?4n*CVJbQ?6%~J z1q?jW_gD`dmv!uw#3#l`geY1PsXaH$ThjGQ@CF|Q-_aMWY@d%S*%qc1r>}5ZiqdoE zY`E%`+9Ne;*VUHQXxd0jFo~+>&Kue*mCLN-5-zE{Huc-YqT0;}i+~bi^KUaH2qtk>iGrNYzqLq`0NAs!^b@ZY(vnN9CHT^+O97A+st) z>b=Yl*^W>~$ZaXSG{)GfQm+NHwmf9$gcO-)wvhe@Aoo;ZqO?!xf^r7PtjLi0EVUVR z*jZWhe(tB-J2?vX6+&oETGsbd&p^jT9dC(as^f&JDD)VGgK}SDSEoXA%tHeYqCLkF zP5g~5WCp0>zi1~m7q$D?HMC3RZ}fJMAP5>v1vM%`8TYn}JZf_IAN)Ge|D1L9YXATM M07*qoM6N<$f+N5YNB{r; literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/extra_pkg_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/extra_pkg_16.png new file mode 100644 index 0000000000000000000000000000000000000000..7ad8a669170bef10ab86adcc930e79c2338ad449 GIT binary patch literal 540 zcmV+%0^|LOP)(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRZ-qDe$SRCwB?lD%#eQ4B_peecX$z`!TI+%}r5)o?!fDD<2cl^MC z0Bm?;RFKEc$A{NLRq+VS1NUHDu!x{4Bo$NycW3$OyR3Hr$o~E%a#d5z=!#K_W9|SX zRdQCUoX9Crx^`3>Ye3G4x~_;;C>A)*t-CJ(IQj4d5hbNeU1tcp0KlzKx{kK(I9;`z zo^|UHe*anFO@$C73Eay6HUgL#NTKUGZr*uLcrcw^2H^JbD@28FAD>bs>1)pe!VBR_ zgbh1B7XcAA)Rk=jgD@25%i9O6&I*SQ-vjXZ^*#2Q%(ST>tgpQYa1=-abyA{AhzI~p zm8g@Fq(6)p+}Q+#3aCPrb+8Uj-rWaa+Vu8SSy2U*ai|Rd0;)nrQ4wPx#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_0007VNklY%R#<_D2o_{e z1VI`G9U_y0@=%CUq@*@=saekD=JwzAI?PsG>fnQSd5`zy`~C91N<>&q839nr0eGjH z_xhKr%Fv6f0BqTJN&Oc=L=ZR{%{PlU93puoHvNs6mI z*gZa6bv|4v8=D7aalT)q{6`M&PBczuJcw;rhnzfs8Xkek8JPS6?YEG+%kW_gb^j@R zoPg{s$jJty`rAZ_3R;5`I-23|h4kjl5bF4O2!tUk3(RIP9WxScFcPo`x7BoYaT zgq1)2fv<{q-rC;)%L&o|u@ID4KmmFJC4!on zC$FIiTUR&zks!)!g1l;WH+QOx1YOrbkHW*((D4`y8e%db$s}570jY08%y+etXSMRg z-C%gseapm1P<`W+V7Qv+qi0Q6ivB}SBT zRBkPY!7eI9Ln$CNRVy7=#DCFgSpStYiTe04wmw-aoX@zA{XuQeX%{eIZnAgRTLH z#L_I^4kMZkCEx(OD>u&(8UHgYz1L1>;%W)Zvi?&Ll@h*Il(MVso>Hrg^H1D49z>nH Ru=@Z2002ovPDHLkV1lmhI~o80 literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/log_off_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/log_off_16.png new file mode 100644 index 0000000000000000000000000000000000000000..ad2edff61586ff014db58cd556f8c78128de542c GIT binary patch literal 1218 zcmbVMTWr%-7)0PbcLaJ1b+ot28V$v+z z=bYdF_x*QIXL|OvZhw6{K@hE}{c;wsJG`g43BTWbu&Ust4JGoZ*BU@2#eqajvkDNT z3}p~zp`wi*`U!RtL}O9UJa*StQqZ1-$pf6baT; z(2z*3JC#poK-_X52nRy}m1Y>gaY3dd%yG=SfTbChqVdfIm<}P#32X#xTqL&UXaho4 zPHx!3Cy^{7WD68kDwTqzP|$J)DTe2HkAr0cSR>$$nn)=JOt-zxAVXJmbQ|fG2|Pxn zV2vP=#GbB)VA$#OCb8*mBnqdDDl0a{1Zm1Jytvl1U6h6Yy0N9Un;W$um4&V~;;6VD z1MPJ%&fW73d5YK@;XOykMNx)jOC2$wiBhsi;x9o>*94xUBauX0W_Xt7q6`zIqoF7p zPRcUJOA;HYb8Ll`q7oaEcVt*6r%p?go~7 zE>?&;P(hZHv#jBI2V{yCvfQF&gLp3o{AoqiO;7NSSD^KHW$5T1K`rT623X6mpl{&; zAN#M!*Rk6F$r^>7p}h8Zrd{eH+=JeBv-$C1b9|tQXU4(9@#wRuTX?W)DLIxa-!81I zu68y0CZ;PAU2FPgqB)sN&ds0W-OLLo$yN0{%blX>b2f(r4544@GoV|bR{4dpWXBTU` z{oC$^x}TiOViJ%>VZFhmEtBZ%@5F-ZC}Q xU6`-^v+vZ+<-;!*^TP{oO_EE|hoM)xCmM(s&*T^PjvC1OtE3V=@@1+2@Kdy8oCg2^ literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/log_on_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/log_on_16.png new file mode 100644 index 0000000000000000000000000000000000000000..ed27b5204cf6201a34dfeb1c1cb5430d093d2c97 GIT binary patch literal 468 zcmV;_0W1EAP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02y>eSaefwW^{L9 za%BKeVQFr3E>1;MAa*k@H7+$tiu~XJ00BfvL_t(IPtB0MN&`U@h4&GB2qDF$5y6)* zMyvyijlO|JBq`DuEG!m8H@I<8aQ%4!OT%gv(pUr~3-JXk6h*OdJ?AZB+Okf=;VRexJvQrr;aW|gS2RqY0j;y6%?}zE$8{56gn(Vt=E`MR*=L^% z1KRPdzyN(Caox#t;S-GD9jYg+dY%Mu<+;0$Crtgsa|YPJyYFAt-h#dDiJQY<>vNP; z#u7lGaRzknddm;#_2N7BFAM}X!>g6sQL^-i{S#|xQemwR>T2A4g%?MSSAu;@;hjXy z=QwIg;OS4j0T$Hww_xypY$&QZo{iLh2?c&hNgYMDmskIT9Onz+AklDQL70(Y)*K0-AbW|YuPgga4sKCl6Bnib)<9EuJzX3_EKXmY zZ0ODGDA2m!vR@-Td%n?B7RM&v4V>G;o}G`psTO*{?ah%x-L4+0D}&gMoO+XY?(><5 zHBNfdSryIvEZ_XD_?#9NGBsB#>GZGY11|z!omcjB?th=BBKu{j9Lt-xm%N3dHm?k6 zs;m-Ndo|;z;ohtJ*0fv9?o#7CV!fe$0h57b_byfm=C^(CSs59=U3H$roV@o#E}y%4 zYM}swLA{`^-F`(jsk5sj6!=)vbVOL6SU!<|sGCy))aNFsGd*t3^Lr(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRZ-A4x<(RCwB?kwHqsKp2I;jLD{<5wux&1UD8cJ%HA30=+hJWV0|Gj@4V+>krfKy-!MEr~mkN{a6$GB3obxd_P^|Z*^Q(=cl%h?9#~OJII5tiYbay~#1vXmi z_2+-~Ja6Rtoixvn!z!~<*Fn(T6-5yOIK~+MgZBXdSU^Zwr7)E900000NkvXXu0mjf D@N}W= literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkg_installed_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkg_installed_16.png new file mode 100644 index 0000000000000000000000000000000000000000..70295655505c76c48f1f42d1892f4516324a59b6 GIT binary patch literal 563 zcmV-30?hr1P)(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRZ-xk*GpRCwB?lg~?3Q545N@7}3*WgKwPe;|?~)-r+?gAi8Bf=s22R@lOI)*@#% z>Apon(IO%zTe_)?q{vNcp=qHTor;18$>+QCygyv{Mw}W{m@izooO{maa1ZAjc<*UN zf)jWJ{1dxQ{3DPI65 zYPH&E*K)K3yiSX^fIE9L@XlaOLLA3~-IeVs@Dv!SbZ5>BAugW$lFS0nfquXOL%`=f zfU}0ArinKH0%{I83%u+3YVyDgFb#|V9=HL_H>|RE4reXfNdhnq{02?~Z-62&ndaYH zXsbSJ;ss9-fRv)NnEUY!7yveaGr;{c_Yp87rFhWPOj1geQm4W&eEYur`wDO&g#eU+ ziH{w>ZY!lG|MNm!^#$3dbLq6N0Lj0Poux860{}T+womu-mMQ=M002ovPDHLkV1g~)xww({ zkb!{P!~TwWvs7<1UK8S8lEmUKRM`>|rx4V(=rha00}i`Xyc`dE=K0n6f4?ugEOXQH zz6n81!ci@(X)bP^hWl8IcRbjpkn#GohHq{g*K^U}=fBKrlpge0@0xm1{>AeqZ9VDl z<_oglthw7%&SH4XH^FxAefJ;S`_g5Cy$-vTwLE8daYXuU>@?*xpt~46UHx3vIVCg! E0GTpe-~a#s literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkg_update_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkg_update_16.png new file mode 100644 index 0000000000000000000000000000000000000000..4171ba639d32e06f83231660b77bd6262574c281 GIT binary patch literal 553 zcmV+^0@nSBP)(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRZ-uSrBfRCwB?l09e?K^TVL-I?9n;XHpq2_(dHsguG=5U~>NL`1N#@X&-v5K^Sd zMG{haV4*R+h_!;1SfzkG^lsuJvkD zK<8dQ`}w915GDxI0Dy9jC^D#?b+ESEFd{;lrey;Wh6oQyfe~0m6dS~G)UCQ#1eulr zDg>250~G>GUPQNUy>r>%SPIPicRSmzw@iy_u68#O3uY~>a;WChOU?0HMARw_1S-<~ zRTY%y6BGIP`XBduEVLLC5J(CD7@@^;8;AEu?#N> z9xNUTg0a{&7S~?A*#9wfZjCW;&f!9I@$sc!mp40|>=Tg(kw>SKeLgs7U06GaclGXO zZ^Ntu`9L>~e$h-ohF^@t1c@|!PK=XDoFHPWZs*;$PCqPs@RGad z`9IJ9_xrzJKH9mrp>BH}000f44z}A^ZRT5RHNLIC>722&sDVDUM@p($PUZoBLW=QV zD9sJ<-8`2VdHx%|9RMszp|?-%i-gIzly-2Yjzddl3^V|=Q(A_L5AiA(;|B!M2Q8j2 zK%kKDLHpbhB$8qHL7`(*=6gmvd*h=+aUuaxyTNviGzikX%7I!sB`T!mgI0J+V{abA z5V)eE4*8%}r}`pMkdb5_bUP4x9PuEa2X&xMm)Glk8pIG3fe{QwF+1udofwHA;MxNj z*5pKz>}G>&whYM!4XSE}gyC#9>&QACk~{#T1VNY_7-ly#?8=C!a++OK+NumJuf$~` zqY9D;nv7gb8diOf;pu7!=}aWDPAn>Gi84|KYg`6K9SEFGn{lmZD{43YuNxa`E4?Eb z9`5E9X;_XM^+>i=!A9=xZ^%?Mydk?}!6*usVx{w}CNM?y%D2#O;p>UAMr z4DsM7iem)M&@@Icv^z*MSe0W#tjo_jogTvPW>FLkV1&!K90LR0WTXsgR61VF3f}bQ8Y*s3`@9iXbqeAKRJU9XJE5A{?jbgBclV& z?Rx7Q!us&=qA@YDF&ZtGAEf|blNMtAz1r2--?~m&c8=!{(Zvrw@m46O(>YNpjSoj9fh%_SozQ2pPGHO zl`1-D>z?n>Bd^XUCo8%RTb|f?CUskHn%t4kO%$gpH_Dwq zx6D&Bg;R-zny$96{?^;yW-s@ZFBQM|YIAtx(=Gic`p;_nD;>9HTv4U|WbJ9-%Ux|V z*J>N)K8qgr-?TipHJobw{Mw*Zac?80rdsxlZvuL*oKC;8w4=-Xw}k?o>>2vN;eP;F C3*VCf literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkgcat_other_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkgcat_other_16.png new file mode 100644 index 0000000000000000000000000000000000000000..395a2403fe28aaa2576b530a2037ae15c5114858 GIT binary patch literal 335 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6;>1s;*b z3=G_YAk0{w5<>&kwUO-P)Tb7FbPbD)r9iEBhjaDG}zd16s2LqK9?UWuNc zYluRbv7V`(sb|-p2S7zTJzX3_Ec}xb7O3smwC|O@k=n^O&36<3Uu5*?(QwYo&;IlO z{aZPMf1hhE|3Cjd?BD-_x_kfA-IxEV-?3cwXsChsB8i$}@4x@+{#;W|NrBsJq~HG&52Y`da bOPh%yqCs3Y{6d^8&=U-vu6{1-oD!M<4Mv7} literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/platform_pkg_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/platform_pkg_16.png new file mode 100644 index 0000000000000000000000000000000000000000..56e10d0a0c1c85b7bfaf3a02acf93df135b80487 GIT binary patch literal 2981 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 IM6N<$g3iIZPyhe` literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/status_ok_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/status_ok_16.png new file mode 100644 index 0000000000000000000000000000000000000000..ae61f7df993c36e0c20b51d0d7c1ef7d9d2800d1 GIT binary patch literal 3277 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 LNkvXXu0mjf76}mt literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/stop_disabled_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/stop_disabled_16.png new file mode 100644 index 0000000000000000000000000000000000000000..721d18410b2e61af85381948ee9ad349f5879a82 GIT binary patch literal 809 zcmV+^1J?YBP)(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRZ;uSrBfRCwBSlTC{pRTPGw`_a`^T~*!b>4bcND8a3e!9*A;5+xznaUsUIFoMR7 zn{2Xj#qWuS$-e2R$l@Ivy z!kY1avFKv^xMOYteB6-eIgIo9iJV6cWOU zP2;3U;{=031p$OmmNX6=#CCfCG~CCSc6tzvJ9+2f~S6P zACw|GbOeCbbMu7B81atm`31(m{(@F8V004R= z004l4008;_004mL004C`008P>0026e000+nl3&F}00009a7bBm000XT000XT0n*)m z`~Uy|8FWQhbW?9;ba!ELWdKlNX>N2bPDNB8b~7$DE;UJt{NMlp0pm$TK~y+Tb&}0X zlu;PPZzVO_O!*a+B_Z^IVJY^36tf8;C*G4XRsV zc{#qvHo&PeMMl}A!Nl1H#lx9b6kJ<_sqtvwe`B-+_in)5iO49Mis|~t=k)du<+br_ zI=;^zz^_F(D((AMxG)eIWfOf`_)Pb2!ooK=u_^xl{)Uy6Xt1;d@87_q+c5JgbW>U1 zJ?s;Qb+*MH@Zw3mK_mXC$?7UxKOcTm*+jP%K887UIn2I+R`6Urqnck?Ut=+hf-ZiZ z&F&VM`&jpy$|g#>M(?60^IA2#Tj1r>y3ZM9)j|)cQoJna%6xn$CmP`G`!!m(*BNEw zHM&W=3gtow)5BZwjOV|=?AsWgHGAx>g=;99Y?jb2L&tw;&7S6P4MmeS8Meq+YNBh_ c)`e602QrOKrQOEs{r~^~07*qoM6N<$f*c_pp8x;= literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/sysimg_pkg_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/sysimg_pkg_16.png new file mode 100644 index 0000000000000000000000000000000000000000..942ce47a1bc4018bef51c2d444d526a1c7a3c84f GIT binary patch literal 1145 zcmbVMO=uKX93Nd9Bx(sL)>PE#h$86Dyf^zbGiFV8=ff;!W7sTo5f7Q2d7BK$d^q#s zPIA#G2-1r=`9-0WqUgbcA}E4UEEMq|dWuCbLK82gJ@isbbbXWTrac4?9hiCV|K9)i z`+v{*f&Rmr+P`k6C~8xoS1FOTBYM}jk?*d*8ai1zv0A}{ZVlIUA5l5ets=T$>%*vo zbaUdwQ`ALKv6@w`;7YMiGF+S0BONxhJ%XmFuI|v&jZuW@DjK$&Ec54s8HTpZEc0!u z2#TJJMy%dR9}Q0SmyO9$Lo}K0Lv&Xt5ds@woeu3WCy>G{v&1WreRRw*^pXl5%`(eQ zRf+?&?D~jKu^?`MG@#Rvg^6S)lQ}^100Iv19OUCLBPDnV0J`}w#F}r`q>`dFZ4oKU zj9~0Z99OT`*?NL?{b3G@q8M@Te4J>+g9!)gVcZFJw-^)@7{29U%XR38QLnn=ILi=E zmqW0CRY9|R+4?BW7jXc z?pUh>21Z=$1|zOV%Y!Mps2i3O$x(Thqg9Y^okXVQyEeU)U&&gfy7xTrb)(=4qc(t*)-rS*xhGCas36XTQ7*njnINXF_$ zLCKZFU#p9aMt5w_Y4peI`&&96{oM1-(A3M{{y97S#pSc|i&NF(RNKXe-z^T;YHfEK zA3r?%;g|R4rhfu+3vbcfR~_q&j_o@R9J{@7X2YdQrL*!%eDwR9Q;jv>Pu&#Qz3np_ zdahi6?A7V_``EufyyLb#nY}lrEo`1&`{R?j=WiNA8;u0m{AFxQtZ#NF+1?}P m&Fk9MwKpz!$F3z0#@2149{2Q@J5+T(`WY0|e&tqv=*%Z+ymy-b literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/tag_android-tv_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/tag_android-tv_16.png new file mode 100644 index 0000000000000000000000000000000000000000..8887bcd36302b18de4cc74053171acec0e584658 GIT binary patch literal 2948 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 Y08CUp0=SqWX8-^I07*qoM6N<$f<95e?f?J) literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/tag_android-wear_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/tag_android-wear_16.png new file mode 100644 index 0000000000000000000000000000000000000000..652c5efa9ed19578c4c4aec5107a3928a1b80859 GIT binary patch literal 3115 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 FV1hRu3A_LR literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/tag_android-wear_32.png b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/tag_android-wear_32.png new file mode 100644 index 0000000000000000000000000000000000000000..33560163b2792717cea886c7abdf91d5e1b5dfc7 GIT binary patch literal 3143 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 literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/tag_default_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/tag_default_16.png new file mode 100644 index 0000000000000000000000000000000000000000..e3ad2e60b2615147e41889e8b8b03786c69f3960 GIT binary patch literal 3097 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 literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/tag_default_32.png b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/tag_default_32.png new file mode 100644 index 0000000000000000000000000000000000000000..f4264e7b7dad591cdb1f5a3d3a378e7c064526ca GIT binary patch literal 3064 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 literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/warning_icon16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/warning_icon16.png new file mode 100644 index 0000000000000000000000000000000000000000..a2fcf7cff6d413cb0e598bd92fd546d7ed0be5b8 GIT binary patch literal 259 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qucL8%hgh?3y^w370~qEv=}#LT=BJwMkFg)(D3 zQ$0gN_s>q|KvhMaE{-7<{!4o=@*Ys&aJf4vbXTnUD;_Z)Rrb9z!tCOd*uUo}1vRxf zs7E%mIe1=J|JmSRkkjGzNeNTb&6e+Vc0Sr|-SokD-@{{-tX`fET6Ja^-{Cv7pvCFf yx0iMt8I7qIBDp2X*yn2eU=+=9JpA5cH{*_*T?X0PwCjMjGkCiCxvX + * - one tab with the list of extra add-ons sites defined by the user.
+ * - one tab with the list of 3rd-party add-ons currently available, which the user can + * deactivate to prevent from loading them. + */ +public class AddonSitesDialog extends UpdaterBaseDialog { + + private final SdkSources mSources; + private Table mUserTable; + private TableViewer mUserTableViewer; + private CheckboxTableViewer mSitesTableViewer; + private Button mUserButtonNew; + private Button mUserButtonDelete; + private Button mUserButtonEdit; + private Runnable mSourcesChangeListener; + + /** + * Create the dialog. + * + * @param parent The parent's shell + * @wbp.parser.entryPoint + */ + public AddonSitesDialog(Shell parent, SwtUpdaterData updaterData) { + super(parent, updaterData, "Add-on Sites"); + mSources = updaterData.getSources(); + assert mSources != null; + } + + /** + * Create contents of the dialog. + * @wbp.parser.entryPoint + */ + @Override + protected void createContents() { + super.createContents(); + Shell shell = getShell(); + shell.setMinimumSize(new Point(300, 300)); + shell.setSize(600, 400); + + TabFolder tabFolder = new TabFolder(shell, SWT.NONE); + GridDataBuilder.create(tabFolder).fill().grab().hSpan(2); + + TabItem sitesTabItem = new TabItem(tabFolder, SWT.NONE); + sitesTabItem.setText("Official Add-on Sites"); + createTabOfficialSites(tabFolder, sitesTabItem); + + TabItem userTabItem = new TabItem(tabFolder, SWT.NONE); + userTabItem.setText("User Defined Sites"); + createTabUserSites(tabFolder, userTabItem); + + // placeholder for aligning close button + Label label = new Label(shell, SWT.NONE); + GridDataBuilder.create(label).hFill().hGrab(); + + createCloseButton(); + } + + void createTabOfficialSites(TabFolder tabFolder, TabItem sitesTabItem) { + Composite root = new Composite(tabFolder, SWT.NONE); + sitesTabItem.setControl(root); + GridLayoutBuilder.create(root).columns(3); + + Label label = new Label(root, SWT.NONE); + GridDataBuilder.create(label).hGrab().vCenter().hSpan(3); + label.setText( + "This lets select which official 3rd-party sites you want to load.\n" + + "\n" + + "These sites are managed by non-Android vendors to provide add-ons and extra packages.\n" + + "They are by default all enabled. When you disable one, the SDK Manager will not check the site for new packages." + ); + + mSitesTableViewer = CheckboxTableViewer.newCheckList(root, SWT.BORDER | SWT.FULL_SELECTION); + mSitesTableViewer.setContentProvider(new SourcesContentProvider()); + + Table sitesTable = mSitesTableViewer.getTable(); + sitesTable.setToolTipText("Enable 3rd-Party Site"); + sitesTable.setLinesVisible(true); + sitesTable.setHeaderVisible(true); + GridDataBuilder.create(sitesTable).fill().grab().hSpan(3); + + TableViewerColumn columnViewer = new TableViewerColumn(mSitesTableViewer, SWT.NONE); + TableColumn column = columnViewer.getColumn(); + column.setResizable(true); + column.setWidth(150); + column.setText("Name"); + columnViewer.setLabelProvider(new ColumnLabelProvider() { + @Override + public String getText(Object element) { + if (element instanceof SdkSource) { + String name = ((SdkSource) element).getUiName(); + if (name != null) { + return name; + } + return ((SdkSource) element).getShortDescription(); + } + return super.getText(element); + } + }); + + columnViewer = new TableViewerColumn(mSitesTableViewer, SWT.NONE); + column = columnViewer.getColumn(); + column.setResizable(true); + column.setWidth(400); + column.setText("URL"); + columnViewer.setLabelProvider(new ColumnLabelProvider() { + @Override + public String getText(Object element) { + if (element instanceof SdkSource) { + return ((SdkSource) element).getUrl(); + } + return super.getText(element); + } + }); + + mSitesTableViewer.addCheckStateListener(new ICheckStateListener() { + @Override + public void checkStateChanged(CheckStateChangedEvent event) { + on_SitesTableViewer_checkStateChanged(event); + } + }); + + // "enable all" and "disable all" buttons under the table + Button selectAll = new Button(root, SWT.NONE); + selectAll.setText("Enable All"); + GridDataBuilder.create(selectAll).hLeft(); + selectAll.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent event) { + on_SitesTableViewer_selectAll(); + } + }); + + // placeholder between both buttons + label = new Label(root, SWT.NONE); + GridDataBuilder.create(label).hFill().hGrab(); + + Button deselectAll = new Button(root, SWT.NONE); + deselectAll.setText("Disable All"); + GridDataBuilder.create(deselectAll).hRight(); + deselectAll.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent event) { + on_SitesTableViewer_deselectAll(); + } + }); + } + + void createTabUserSites(TabFolder tabFolder, TabItem userTabItem) { + Composite root = new Composite(tabFolder, SWT.NONE); + userTabItem.setControl(root); + GridLayoutBuilder.create(root).columns(2); + + Label label = new Label(root, SWT.NONE); + GridDataBuilder.create(label).hLeft().vCenter().hSpan(2); + label.setText( + "This lets you manage a list of user-contributed external add-on sites URLs.\n" + + "\n" + + "Add-on sites can provide new add-ons and extra packages.\n" + + "They cannot provide standard Android platforms, system images or docs.\n" + + "Adding a URL here will not allow you to clone an official Android repository." + ); + + mUserTableViewer = new TableViewer(root, SWT.BORDER | SWT.FULL_SELECTION); + mUserTableViewer.setContentProvider(new SourcesContentProvider()); + + mUserTableViewer.addPostSelectionChangedListener(new ISelectionChangedListener() { + @Override + public void selectionChanged(SelectionChangedEvent event) { + on_UserTableViewer_selectionChanged(event); + } + }); + mUserTable = mUserTableViewer.getTable(); + mUserTable.setLinesVisible(true); + mUserTable.addMouseListener(new MouseAdapter() { + @Override + public void mouseUp(MouseEvent event) { + on_UserTable_mouseUp(event); + } + }); + GridDataBuilder.create(mUserTable).fill().grab().vSpan(5); + + TableViewerColumn tableViewerColumn = new TableViewerColumn(mUserTableViewer, SWT.NONE); + TableColumn userColumnUrl = tableViewerColumn.getColumn(); + userColumnUrl.setWidth(100); + + // Implementation detail: set the label provider on the table viewer *after* associating + // a column. This will set the label provider on the column for us. + mUserTableViewer.setLabelProvider(new LabelProvider()); + + + mUserButtonNew = new Button(root, SWT.NONE); + mUserButtonNew.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + userNewOrEdit(false /*isEdit*/); + } + }); + GridDataBuilder.create(mUserButtonNew).hFill().vCenter(); + mUserButtonNew.setText("New..."); + + mUserButtonEdit = new Button(root, SWT.NONE); + mUserButtonEdit.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + userNewOrEdit(true /*isEdit*/); + } + }); + GridDataBuilder.create(mUserButtonEdit).hFill().vCenter(); + mUserButtonEdit.setText("Edit..."); + + mUserButtonDelete = new Button(root, SWT.NONE); + mUserButtonDelete.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + on_UserButtonDelete_widgetSelected(e); + } + }); + GridDataBuilder.create(mUserButtonDelete).hFill().vCenter(); + mUserButtonDelete.setText("Delete..."); + + adjustColumnsWidth(mUserTable, userColumnUrl); + } + + @Override + protected void close() { + if (mSources != null && mSourcesChangeListener != null) { + mSources.removeChangeListener(mSourcesChangeListener); + } + SdkSourceProperties p = new SdkSourceProperties(); + p.save(); + super.close(); + } + + /** + * Adds a listener to adjust the column width when the parent is resized. + */ + private void adjustColumnsWidth(final Table table, final TableColumn column0) { + // Add a listener to resize the column to the full width of the table + table.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Rectangle r = table.getClientArea(); + column0.setWidth(r.width * 100 / 100); // 100% + } + }); + } + + private void userNewOrEdit(final boolean isEdit) { + final SdkSource[] knownSources = mSources.getAllSources(); + String title = isEdit ? "Edit Add-on Site URL" : "Add Add-on Site URL"; + String msg = "Please enter the URL of the addon.xml:"; + IStructuredSelection sel = (IStructuredSelection) mUserTableViewer.getSelection(); + final String initialValue = !isEdit || sel.isEmpty() ? null : + sel.getFirstElement().toString(); + + if (isEdit && initialValue == null) { + // Edit with no actual value is not supposed to happen. Ignore this case. + return; + } + + InputDialog dlg = new InputDialog( + getShell(), + title, + msg, + initialValue, + new IInputValidator() { + @Override + public String isValid(String newText) { + + newText = newText == null ? null : newText.trim(); + + if (newText == null || newText.length() == 0) { + return "Error: URL field is empty. Please enter a URL."; + } + + // A URL should have one of the following prefixes + if (!newText.startsWith("file://") && //$NON-NLS-1$ + !newText.startsWith("ftp://") && //$NON-NLS-1$ + !newText.startsWith("http://") && //$NON-NLS-1$ + !newText.startsWith("https://")) { //$NON-NLS-1$ + return "Error: The URL must start by one of file://, ftp://, http:// or https://"; + } + + if (isEdit && newText.equals(initialValue)) { + // Edited value hasn't changed. This isn't an error. + return null; + } + + // Reject URLs that are already in the source list. + // URLs are generally case-insensitive (except for file:// where it all depends + // on the current OS so we'll ignore this case.) + for (SdkSource s : knownSources) { + if (newText.equalsIgnoreCase(s.getUrl())) { + return "Error: This site is already listed."; + } + } + + return null; + } + }); + + if (dlg.open() == Window.OK) { + String url = dlg.getValue().trim(); + + if (!url.equals(initialValue)) { + if (isEdit && initialValue != null) { + // Remove the old value before we add the new one, which is we just + // asserted will be different. + for (SdkSource source : mSources.getSources(SdkSourceCategory.USER_ADDONS)) { + if (initialValue.equals(source.getUrl())) { + mSources.remove(source); + break; + } + } + + } + + // create the source, store it and update the list + SdkSource newSource; + // use url suffix to decide whether this is a SysImg or Addon; + // see SdkSources.loadUserAddons() for another check like this + if (url.endsWith(SdkSysImgConstants.URL_DEFAULT_FILENAME)) { + newSource = new SdkSysImgSource(url, null/*uiName*/); + } else { + newSource = new SdkAddonSource(url, null/*uiName*/); + } + mSources.add(SdkSourceCategory.USER_ADDONS, newSource); + setReturnValue(true); + // notify sources change listeners. This will invoke our own loadUserUrlsList(). + mSources.notifyChangeListeners(); + + // select the new source + IStructuredSelection newSel = new StructuredSelection(newSource); + mUserTableViewer.setSelection(newSel, true /*reveal*/); + } + } + } + + private void on_UserButtonDelete_widgetSelected(SelectionEvent e) { + IStructuredSelection sel = (IStructuredSelection) mUserTableViewer.getSelection(); + String selectedUrl = sel.isEmpty() ? null : sel.getFirstElement().toString(); + + if (selectedUrl == null) { + return; + } + + MessageBox mb = new MessageBox(getShell(), + SWT.YES | SWT.NO | SWT.ICON_QUESTION | SWT.APPLICATION_MODAL); + mb.setText("Delete add-on site"); + mb.setMessage(String.format("Do you want to delete the URL %1$s?", selectedUrl)); + if (mb.open() == SWT.YES) { + for (SdkSource source : mSources.getSources(SdkSourceCategory.USER_ADDONS)) { + if (selectedUrl.equals(source.getUrl())) { + mSources.remove(source); + setReturnValue(true); + mSources.notifyChangeListeners(); + break; + } + } + } + } + + private void on_UserTable_mouseUp(MouseEvent event) { + Point p = new Point(event.x, event.y); + if (mUserTable.getItem(p) == null) { + mUserTable.deselectAll(); + on_UserTableViewer_selectionChanged(null /*event*/); + } + } + + private void on_UserTableViewer_selectionChanged(SelectionChangedEvent event) { + ISelection sel = mUserTableViewer.getSelection(); + mUserButtonDelete.setEnabled(!sel.isEmpty()); + mUserButtonEdit.setEnabled(!sel.isEmpty()); + } + + private void on_SitesTableViewer_checkStateChanged(CheckStateChangedEvent event) { + Object element = event.getElement(); + if (element instanceof SdkSource) { + SdkSource source = (SdkSource) element; + boolean isChecked = event.getChecked(); + if (source.isEnabled() != isChecked) { + setReturnValue(true); + source.setEnabled(isChecked); + mSources.notifyChangeListeners(); + } + } + } + + private void on_SitesTableViewer_selectAll() { + for (Object item : (Object[]) mSitesTableViewer.getInput()) { + if (!mSitesTableViewer.getChecked(item)) { + mSitesTableViewer.setChecked(item, true); + on_SitesTableViewer_checkStateChanged( + new CheckStateChangedEvent(mSitesTableViewer, item, true)); + } + } + } + + private void on_SitesTableViewer_deselectAll() { + for (Object item : (Object[]) mSitesTableViewer.getInput()) { + if (mSitesTableViewer.getChecked(item)) { + mSitesTableViewer.setChecked(item, false); + on_SitesTableViewer_checkStateChanged( + new CheckStateChangedEvent(mSitesTableViewer, item, false)); + } + } + } + + + @Override + protected void postCreate() { + // A runnable to initially load and then update the user urls & sites lists. + final Runnable updateInUiThread = new Runnable() { + @Override + public void run() { + loadUserUrlsList(); + loadSiteUrlsList(); + } + }; + + // A listener that runs when the sources have changed. + // This is most likely called on a worker thread. + mSourcesChangeListener = new Runnable() { + @Override + public void run() { + Shell shell = getShell(); + if (shell != null) { + Display display = shell.getDisplay(); + if (display != null) { + display.syncExec(updateInUiThread); + } + } + } + }; + + mSources.addChangeListener(mSourcesChangeListener); + + // initialize the list + updateInUiThread.run(); + } + + private void loadUserUrlsList() { + SdkSource[] knownSources = mSources.getSources(SdkSourceCategory.USER_ADDONS); + Arrays.sort(knownSources); + + ISelection oldSelection = mUserTableViewer.getSelection(); + + mUserTableViewer.setInput(knownSources); + mUserTableViewer.refresh(); + // initialize buttons' state that depend on the list + on_UserTableViewer_selectionChanged(null /*event*/); + + if (oldSelection != null && !oldSelection.isEmpty()) { + mUserTableViewer.setSelection(oldSelection, true /*reveal*/); + } + } + + private void loadSiteUrlsList() { + SdkSource[] knownSources = mSources.getSources(SdkSourceCategory.ADDONS_3RD_PARTY); + Arrays.sort(knownSources); + + ISelection oldSelection = mSitesTableViewer.getSelection(); + + mSitesTableViewer.setInput(knownSources); + mSitesTableViewer.refresh(); + + if (oldSelection != null && !oldSelection.isEmpty()) { + mSitesTableViewer.setSelection(oldSelection, true /*reveal*/); + } + + // Check the sources which are currently enabled. + ArrayList disabled = new ArrayList(knownSources.length); + for (SdkSource source : knownSources) { + if (source.isEnabled()) { + disabled.add(source); + } + } + mSitesTableViewer.setCheckedElements(disabled.toArray()); + } + + + private static class SourcesContentProvider implements IStructuredContentProvider { + @Override + public void dispose() { + // pass + } + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } + + @Override + public Object[] getElements(Object inputElement) { + if (inputElement instanceof SdkSource[]) { + return (Object[]) inputElement; + } else { + return new Object[0]; + } + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AdtUpdateDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AdtUpdateDialog.java new file mode 100644 index 00000000..5fcd1f77 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AdtUpdateDialog.java @@ -0,0 +1,494 @@ +/* + * Copyright (C) 2011 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.sdkuilib.internal.repository.ui; + + +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.internal.repository.packages.ExtraPackage; +import com.android.sdklib.internal.repository.packages.Package; +import com.android.sdklib.internal.repository.packages.PlatformPackage; +import com.android.sdklib.internal.repository.packages.PlatformToolPackage; +import com.android.sdklib.internal.repository.packages.ToolPackage; +import com.android.sdklib.internal.repository.sources.SdkSource; +import com.android.sdklib.internal.repository.updater.PackageLoader; +import com.android.sdklib.internal.repository.updater.SettingsController; +import com.android.sdklib.internal.repository.updater.PackageLoader.IAutoInstallTask; +import com.android.sdkuilib.internal.repository.SwtUpdaterData; +import com.android.sdkuilib.internal.repository.core.SdkLogAdapter; +import com.android.sdkuilib.internal.tasks.ProgressView; +import com.android.sdkuilib.internal.tasks.ProgressViewFactory; +import com.android.sdkuilib.ui.GridDataBuilder; +import com.android.sdkuilib.ui.GridLayoutBuilder; +import com.android.sdkuilib.ui.SwtBaseDialog; +import com.android.utils.ILogger; +import com.android.utils.Pair; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.ProgressBar; +import org.eclipse.swt.widgets.Shell; + +import java.io.File; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +/** + * This is a private implementation of UpdateWindow for ADT, + * designed to install a very specific package. + *

+ * Example of usage: + *

+ * AdtUpdateDialog dialog = new AdtUpdateDialog(
+ *     AdtPlugin.getDisplay().getActiveShell(),
+ *     new AdtConsoleSdkLog(),
+ *     sdk.getSdkLocation());
+ *
+ * Pair result = dialog.installExtraPackage(
+ *     "android", "compatibility");  //$NON-NLS-1$ //$NON-NLS-2$
+ * or
+ * Pair result = dialog.installPlatformPackage(11);
+ * 
+ */ +public class AdtUpdateDialog extends SwtBaseDialog { + + public static final int USE_MAX_REMOTE_API_LEVEL = 0; + + private static final String APP_NAME = "Android SDK Manager"; + private final SwtUpdaterData mUpdaterData; + + private Boolean mResultCode = Boolean.FALSE; + private Map mResultPaths = null; + private SettingsController mSettingsController; + private PackageFilter mPackageFilter; + private PackageLoader mPackageLoader; + + private ProgressBar mProgressBar; + private Label mStatusText; + + /** + * Creates a new {@link AdtUpdateDialog}. + * Callers will want to call {@link #installExtraPackage} or + * {@link #installPlatformPackage} after this. + * + * @param parentShell The existing parent shell. Must not be null. + * @param sdkLog An SDK logger. Must not be null. + * @param osSdkRoot The current SDK root OS path. Must not be null or empty. + */ + public AdtUpdateDialog( + Shell parentShell, + ILogger sdkLog, + String osSdkRoot) { + super(parentShell, SWT.NONE, APP_NAME); + mUpdaterData = new SwtUpdaterData(osSdkRoot, sdkLog); + } + + /** + * Displays the update dialog and triggers installation of the requested {@code extra} + * package with the specified vendor and path attributes. + *

+ * Callers must not try to reuse this dialog after this call. + * + * @param vendor The extra package vendor string to match. + * @param path The extra package path string to match. + * @return A boolean indicating whether the installation was successful (meaning the package + * was either already present, or got installed or updated properly) and a {@link File} + * with the path to the root folder of the package. The file is null when the boolean + * is false, otherwise it should point to an existing valid folder. + * @wbp.parser.entryPoint + */ + public Pair installExtraPackage(String vendor, String path) { + mPackageFilter = createExtraFilter(vendor, path); + open(); + + File installPath = null; + if (mResultPaths != null) { + for (Entry entry : mResultPaths.entrySet()) { + if (entry.getKey() instanceof ExtraPackage) { + installPath = entry.getValue(); + break; + } + } + } + + return Pair.of(mResultCode, installPath); + } + + /** + * Displays the update dialog and triggers installation of platform-tools package. + *

+ * Callers must not try to reuse this dialog after this call. + * + * @return A boolean indicating whether the installation was successful (meaning the package + * was either already present, or got installed or updated properly) and a {@link File} + * with the path to the root folder of the package. The file is null when the boolean + * is false, otherwise it should point to an existing valid folder. + * @wbp.parser.entryPoint + */ + public Pair installPlatformTools() { + mPackageFilter = createPlatformToolsFilter(); + open(); + + File installPath = null; + if (mResultPaths != null) { + for (Entry entry : mResultPaths.entrySet()) { + if (entry.getKey() instanceof ExtraPackage) { + installPath = entry.getValue(); + break; + } + } + } + + return Pair.of(mResultCode, installPath); + } + + /** + * Displays the update dialog and triggers installation of the requested platform + * package with the specified API level. + *

+ * Callers must not try to reuse this dialog after this call. + * + * @param apiLevel The platform API level to match. + * The special value {@link #USE_MAX_REMOTE_API_LEVEL} means to use + * the highest API level available on the remote repository. + * @return A boolean indicating whether the installation was successful (meaning the package + * was either already present, or got installed or updated properly) and a {@link File} + * with the path to the root folder of the package. The file is null when the boolean + * is false, otherwise it should point to an existing valid folder. + */ + public Pair installPlatformPackage(int apiLevel) { + mPackageFilter = createPlatformFilter(apiLevel); + open(); + + File installPath = null; + if (mResultPaths != null) { + for (Entry entry : mResultPaths.entrySet()) { + if (entry.getKey() instanceof PlatformPackage) { + installPath = entry.getValue(); + break; + } + } + } + + return Pair.of(mResultCode, installPath); + } + + /** + * Displays the update dialog and triggers installation of a new SDK. This works by + * requesting a remote platform package with the specified API levels as well as + * the first tools or platform-tools packages available. + *

+ * Callers must not try to reuse this dialog after this call. + * + * @param apiLevels A set of platform API levels to match. + * The special value {@link #USE_MAX_REMOTE_API_LEVEL} means to use + * the highest API level available in the repository. + * @return A boolean indicating whether the installation was successful (meaning the packages + * were either already present, or got installed or updated properly). + */ + public boolean installNewSdk(Set apiLevels) { + mPackageFilter = createNewSdkFilter(apiLevels); + open(); + return mResultCode.booleanValue(); + } + + @Override + protected void createContents() { + Shell shell = getShell(); + shell.setMinimumSize(new Point(450, 100)); + shell.setSize(450, 100); + + mUpdaterData.setWindowShell(shell); + + GridLayoutBuilder.create(shell).columns(1); + + Composite composite1 = new Composite(shell, SWT.NONE); + composite1.setLayout(new GridLayout(1, false)); + GridDataBuilder.create(composite1).fill().grab(); + + mProgressBar = new ProgressBar(composite1, SWT.NONE); + GridDataBuilder.create(mProgressBar).hFill().hGrab(); + + mStatusText = new Label(composite1, SWT.NONE); + mStatusText.setText("Status Placeholder"); //$NON-NLS-1$ placeholder + GridDataBuilder.create(mStatusText).hFill().hGrab(); + } + + @Override + protected void postCreate() { + ProgressViewFactory factory = new ProgressViewFactory(); + factory.setProgressView(new ProgressView( + mStatusText, + mProgressBar, + null /*buttonStop*/, + new SdkLogAdapter(mUpdaterData.getSdkLog()))); + mUpdaterData.setTaskFactory(factory); + + setupSources(); + initializeSettings(); + + if (mUpdaterData.checkIfInitFailed()) { + close(); + return; + } + + mUpdaterData.broadcastOnSdkLoaded(); + + mPackageLoader = new PackageLoader(mUpdaterData); + } + + @Override + protected void eventLoop() { + mPackageLoader.loadPackagesWithInstallTask( + mPackageFilter.installFlags(), + new IAutoInstallTask() { + @Override + public Package[] filterLoadedSource(SdkSource source, Package[] packages) { + for (Package pkg : packages) { + mPackageFilter.visit(pkg); + } + return packages; + } + + @Override + public boolean acceptPackage(Package pkg) { + // Is this the package we want to install? + return mPackageFilter.accept(pkg); + } + + @Override + public void setResult(boolean success, Map installPaths) { + // Capture the result from the installation. + mResultCode = Boolean.valueOf(success); + mResultPaths = installPaths; + } + + @Override + public void taskCompleted() { + // We can close that window now. + close(); + } + }); + + super.eventLoop(); + } + + // -- Start of internal part ---------- + // Hide everything down-below from SWT designer + //$hide>>$ + + // --- Public API ----------- + + + // --- Internals & UI Callbacks ----------- + + /** + * Used to initialize the sources. + */ + private void setupSources() { + mUpdaterData.setupDefaultSources(); + } + + /** + * Initializes settings. + */ + private void initializeSettings() { + mSettingsController = mUpdaterData.getSettingsController(); + mSettingsController.loadSettings(); + mSettingsController.applySettings(); + } + + // ---- + + private static abstract class PackageFilter { + /** Returns the installer flags for the corresponding mode. */ + abstract int installFlags(); + + /** Visit a new package definition, in case we need to adjust the filter dynamically. */ + abstract void visit(Package pkg); + + /** Checks whether this is the package we've been looking for. */ + abstract boolean accept(Package pkg); + } + + public static PackageFilter createExtraFilter( + final String vendor, + final String path) { + return new PackageFilter() { + String mVendor = vendor; + String mPath = path; + + @Override + boolean accept(Package pkg) { + if (pkg instanceof ExtraPackage) { + ExtraPackage ep = (ExtraPackage) pkg; + if (ep.getVendorId().equals(mVendor)) { + // Check actual extra field first + if (ep.getPath().equals(mPath)) { + return true; + } + // If not, check whether this is one of the values. + for (String oldPath : ep.getOldPaths()) { + if (oldPath.equals(mPath)) { + return true; + } + } + } + } + return false; + } + + @Override + void visit(Package pkg) { + // nop + } + + @Override + int installFlags() { + return SwtUpdaterData.TOOLS_MSG_UPDATED_FROM_ADT; + } + }; + } + + private PackageFilter createPlatformToolsFilter() { + return new PackageFilter() { + @Override + boolean accept(Package pkg) { + return pkg instanceof PlatformToolPackage; + } + + @Override + void visit(Package pkg) { + // nop + } + + @Override + int installFlags() { + return SwtUpdaterData.TOOLS_MSG_UPDATED_FROM_ADT; + } + }; + } + + public static PackageFilter createPlatformFilter(final int apiLevel) { + return new PackageFilter() { + int mApiLevel = apiLevel; + boolean mFindMaxApi = apiLevel == USE_MAX_REMOTE_API_LEVEL; + + @Override + boolean accept(Package pkg) { + if (pkg instanceof PlatformPackage) { + PlatformPackage pp = (PlatformPackage) pkg; + AndroidVersion v = pp.getAndroidVersion(); + return !v.isPreview() && v.getApiLevel() == mApiLevel; + } + return false; + } + + @Override + void visit(Package pkg) { + // Try to find the max API in all remote packages + if (mFindMaxApi && + pkg instanceof PlatformPackage && + !pkg.isLocal()) { + PlatformPackage pp = (PlatformPackage) pkg; + AndroidVersion v = pp.getAndroidVersion(); + if (!v.isPreview()) { + int api = v.getApiLevel(); + if (api > mApiLevel) { + mApiLevel = api; + } + } + } + } + + @Override + int installFlags() { + return SwtUpdaterData.TOOLS_MSG_UPDATED_FROM_ADT; + } + }; + } + + public static PackageFilter createNewSdkFilter(final Set apiLevels) { + return new PackageFilter() { + int mMaxApiLevel; + boolean mFindMaxApi = apiLevels.contains(USE_MAX_REMOTE_API_LEVEL); + boolean mNeedTools = true; + boolean mNeedPlatformTools = true; + + @Override + boolean accept(Package pkg) { + if (!pkg.isLocal()) { + if (pkg instanceof PlatformPackage) { + PlatformPackage pp = (PlatformPackage) pkg; + AndroidVersion v = pp.getAndroidVersion(); + if (!v.isPreview()) { + int level = v.getApiLevel(); + if ((mFindMaxApi && level == mMaxApiLevel) || + (level > 0 && apiLevels.contains(level))) { + return true; + } + } + } else if (mNeedTools && pkg instanceof ToolPackage) { + // We want a tool package. There should be only one, + // but in case of error just take the first one. + mNeedTools = false; + return true; + } else if (mNeedPlatformTools && pkg instanceof PlatformToolPackage) { + // We want a platform-tool package. There should be only one, + // but in case of error just take the first one. + mNeedPlatformTools = false; + return true; + } + } + return false; + } + + @Override + void visit(Package pkg) { + // Try to find the max API in all remote packages + if (mFindMaxApi && + pkg instanceof PlatformPackage && + !pkg.isLocal()) { + PlatformPackage pp = (PlatformPackage) pkg; + AndroidVersion v = pp.getAndroidVersion(); + if (!v.isPreview()) { + int api = v.getApiLevel(); + if (api > mMaxApiLevel) { + mMaxApiLevel = api; + } + } + } + } + + @Override + int installFlags() { + return SwtUpdaterData.NO_TOOLS_MSG; + } + }; + } + + + + // End of hiding from SWT Designer + //$hide<<$ + + // ----- + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AvdManagerPage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AvdManagerPage.java new file mode 100644 index 00000000..ed0b933a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AvdManagerPage.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.sdkuilib.internal.repository.ui; + +import com.android.prefs.AndroidLocation.AndroidLocationException; +import com.android.sdklib.devices.DeviceManager; +import com.android.sdklib.devices.DeviceManager.DevicesChangedListener; +import com.android.sdklib.internal.avd.AvdInfo; +import com.android.sdklib.internal.avd.AvdManager; +import com.android.sdklib.repository.ISdkChangeListener; +import com.android.sdkuilib.internal.repository.SwtUpdaterData; +import com.android.sdkuilib.internal.widgets.AvdSelector; +import com.android.sdkuilib.internal.widgets.AvdSelector.DisplayMode; + +import org.eclipse.swt.SWT; +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.Label; + +/** + * An Update page displaying AVD Manager entries. + * This is the sole page displayed by {@link AvdManagerWindowImpl1}. + * + * Note: historically the SDK Manager was a single window with several sub-pages and a tab + * switcher. For simplicity each page was separated in its own window. The AVD Manager is + * thus composed of the {@link AvdManagerWindowImpl1} (the window shell itself) and this + * page displays the actually list of AVDs and various action buttons. + */ +public class AvdManagerPage extends Composite + implements ISdkChangeListener, DevicesChangedListener, DisposeListener { + + private AvdSelector mAvdSelector; + + private final SwtUpdaterData mSwtUpdaterData; + private final DeviceManager mDeviceManager; + /** + * Create the composite. + * @param parent The parent of the composite. + * @param swtUpdaterData An instance of {@link SwtUpdaterData}. + */ + public AvdManagerPage(Composite parent, + int swtStyle, + SwtUpdaterData swtUpdaterData, + DeviceManager deviceManager) { + super(parent, swtStyle); + + mSwtUpdaterData = swtUpdaterData; + mSwtUpdaterData.addListeners(this); + + mDeviceManager = deviceManager; + mDeviceManager.registerListener(this); + + createContents(this); + postCreate(); //$hide$ + } + + private void createContents(Composite parent) { + parent.setLayout(new GridLayout(1, false)); + + Label label = new Label(parent, SWT.NONE); + label.setLayoutData(new GridData()); + + try { + if (mSwtUpdaterData != null && mSwtUpdaterData.getAvdManager() != null) { + label.setText(String.format( + "List of existing Android Virtual Devices located at %s", + mSwtUpdaterData.getAvdManager().getBaseAvdFolder())); + } else { + label.setText("Error: cannot find the AVD folder location.\r\n Please set the 'ANDROID_SDK_HOME' env variable."); + } + } catch (AndroidLocationException e) { + label.setText(e.getMessage()); + } + + mAvdSelector = new AvdSelector(parent, + mSwtUpdaterData.getOsSdkRoot(), + mSwtUpdaterData.getAvdManager(), + DisplayMode.MANAGER, + mSwtUpdaterData.getSdkLog()); + mAvdSelector.setSettingsController(mSwtUpdaterData.getSettingsController()); + } + + @Override + public void widgetDisposed(DisposeEvent e) { + dispose(); + } + + @Override + public void dispose() { + mSwtUpdaterData.removeListener(this); + mDeviceManager.unregisterListener(this); + super.dispose(); + } + + @Override + protected void checkSubclass() { + // Disable the check that prevents subclassing of SWT components + } + + public void selectAvd(AvdInfo avdInfo, boolean reloadAvdList) { + if (reloadAvdList) { + mAvdSelector.refresh(true /*reload*/); + + // Reloading the AVDs created new objects, so the reference to avdInfo + // will never be selected. Instead reselect it based on its unique name. + AvdManager am = mSwtUpdaterData.getAvdManager(); + avdInfo = am.getAvd(avdInfo.getName(), false /*validAvdOnly*/); + } + mAvdSelector.setSelection(avdInfo); + } + + // -- Start of internal part ---------- + // Hide everything down-below from SWT designer + //$hide>>$ + + /** + * Called by the constructor right after {@link #createContents(Composite)}. + */ + private void postCreate() { + // nothing to be done for now. + } + + // --- Implementation of ISdkChangeListener --- + + @Override + public void onSdkLoaded() { + onSdkReload(); + } + + @Override + public void onSdkReload() { + mAvdSelector.refresh(false /*reload*/); + } + + @Override + public void preInstallHook() { + // nothing to be done for now. + } + + @Override + public void postInstallHook() { + // nothing to be done for now. + } + + // --- Implementation of DevicesChangeListener + + @Override + public void onDevicesChanged() { + mAvdSelector.refresh(false /*reload*/); + } + + + // End of hiding from SWT Designer + //$hide<<$ +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AvdManagerWindowImpl1.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AvdManagerWindowImpl1.java new file mode 100644 index 00000000..1f14d7f7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AvdManagerWindowImpl1.java @@ -0,0 +1,414 @@ +/* + * Copyright (C) 2011 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.sdkuilib.internal.repository.ui; + + +import com.android.SdkConstants; +import com.android.sdklib.devices.DeviceManager; +import com.android.sdklib.internal.avd.AvdInfo; +import com.android.sdklib.internal.repository.ITaskFactory; +import com.android.sdklib.internal.repository.updater.SettingsController; +import com.android.sdklib.repository.ISdkChangeListener; +import com.android.sdkuilib.internal.repository.AboutDialog; +import com.android.sdkuilib.internal.repository.MenuBarWrapper; +import com.android.sdkuilib.internal.repository.SettingsDialog; +import com.android.sdkuilib.internal.repository.SwtUpdaterData; +import com.android.sdkuilib.internal.repository.icons.ImageFactory; +import com.android.sdkuilib.internal.repository.ui.DeviceManagerPage.IAvdCreatedListener; +import com.android.sdkuilib.repository.AvdManagerWindow.AvdInvocationContext; +import com.android.sdkuilib.repository.SdkUpdaterWindow; +import com.android.sdkuilib.ui.GridDataBuilder; +import com.android.sdkuilib.ui.GridLayoutBuilder; +import com.android.utils.ILogger; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Menu; +import org.eclipse.swt.widgets.MenuItem; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.TabFolder; +import org.eclipse.swt.widgets.TabItem; + +import java.io.File; + +/** + * This is an intermediate version of the {@link AvdManagerPage} + * wrapped in its own standalone window for use from the SDK Manager 2. + */ +public class AvdManagerWindowImpl1 { + + private static final String APP_NAME = "Android Virtual Device (AVD) Manager"; + private static final String APP_NAME_MAC_MENU = "AVD Manager"; + private static final String SIZE_POS_PREFIX = "avdman1"; //$NON-NLS-1$ + + private final Shell mParentShell; + private final AvdInvocationContext mContext; + /** Internal data shared between the window and its pages. */ + private final SwtUpdaterData mSwtUpdaterData; + /** True if this window created the UpdaterData, in which case it needs to dispose it. */ + private final boolean mOwnUpdaterData; + private final DeviceManager mDeviceManager; + + + // --- UI members --- + + protected Shell mShell; + private AvdManagerPage mAvdPage; + private SettingsController mSettingsController; + private TabFolder mTabFolder; + + /** + * Creates a new window. Caller must call open(), which will block. + * + * @param parentShell Parent shell. + * @param sdkLog Logger. Cannot be null. + * @param osSdkRoot The OS path to the SDK root. + * @param context The {@link AvdInvocationContext} to change the behavior depending on who's + * opening the SDK Manager. + */ + public AvdManagerWindowImpl1( + Shell parentShell, + ILogger sdkLog, + String osSdkRoot, + AvdInvocationContext context) { + mParentShell = parentShell; + mContext = context; + mSwtUpdaterData = new SwtUpdaterData(osSdkRoot, sdkLog); + mOwnUpdaterData = true; + mDeviceManager = DeviceManager.createInstance(new File(osSdkRoot), sdkLog); + } + + /** + * Creates a new window. Caller must call open(), which will block. + *

+ * This is to be used when the window is opened from {@link SdkUpdaterWindowImpl2} + * to share the same {@link SwtUpdaterData} structure. + * + * @param parentShell Parent shell. + * @param swtUpdaterData The parent's updater data. + * @param context The {@link AvdInvocationContext} to change the behavior depending on who's + * opening the SDK Manager. + */ + public AvdManagerWindowImpl1( + Shell parentShell, + SwtUpdaterData swtUpdaterData, + AvdInvocationContext context) { + mParentShell = parentShell; + mContext = context; + mSwtUpdaterData = swtUpdaterData; + mOwnUpdaterData = false; + mDeviceManager = DeviceManager.createInstance(new File(mSwtUpdaterData.getOsSdkRoot()), + mSwtUpdaterData.getSdkLog()); + } + + /** + * Opens the window. + * @wbp.parser.entryPoint + */ + public void open() { + if (mParentShell == null) { + Display.setAppName(APP_NAME); //$hide$ (hide from SWT designer) + } + + createShell(); + preCreateContent(); + createContents(); + createMenuBar(); + mShell.open(); + mShell.layout(); + + boolean ok = postCreateContent(); + + if (ok && mContext == AvdInvocationContext.STANDALONE) { + Display display = Display.getDefault(); + while (!mShell.isDisposed()) { + if (!display.readAndDispatch()) { + display.sleep(); + } + } + + dispose(); //$hide$ + } + } + + private void createShell() { + // The AVD Manager must use a shell trim when standalone + // or a dialog trim when invoked from somewhere else. + int style = SWT.SHELL_TRIM; + if (mContext != AvdInvocationContext.STANDALONE) { + style |= SWT.APPLICATION_MODAL; + } + + mShell = new Shell(mParentShell, style); + mShell.addDisposeListener(new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + ShellSizeAndPos.saveSizeAndPos(mShell, SIZE_POS_PREFIX); //$hide$ + onAndroidSdkUpdaterDispose(); //$hide$ + mAvdPage.dispose(); //$hide$ + } + }); + + GridLayout glShell = new GridLayout(2, false); + mShell.setLayout(glShell); + + mShell.setMinimumSize(new Point(500, 300)); + mShell.setSize(700, 500); + mShell.setText(APP_NAME); + + ShellSizeAndPos.loadSizeAndPos(mShell, SIZE_POS_PREFIX); + } + + private void createContents() { + mTabFolder = new TabFolder(mShell, SWT.NONE); + GridDataBuilder.create(mTabFolder).fill().grab().hSpan(2); + + // avd tab + TabItem avdTabItem = new TabItem(mTabFolder, SWT.NONE); + avdTabItem.setText("Android Virtual Devices"); + avdTabItem.setToolTipText(avdTabItem.getText()); + createAvdTab(mTabFolder, avdTabItem); + + // device tab + TabItem devTabItem = new TabItem(mTabFolder, SWT.NONE); + devTabItem.setText("Device Definitions"); + devTabItem.setToolTipText(devTabItem.getText()); + createDeviceTab(mTabFolder, devTabItem); + } + + private void createAvdTab(TabFolder tabFolder, TabItem avdTabItem) { + Composite root = new Composite(tabFolder, SWT.NONE); + avdTabItem.setControl(root); + GridLayoutBuilder.create(root).columns(1); + + mAvdPage = new AvdManagerPage(root, SWT.NONE, mSwtUpdaterData, mDeviceManager); + GridDataBuilder.create(mAvdPage).fill().grab(); + } + + private void createDeviceTab(TabFolder tabFolder, TabItem devTabItem) { + Composite root = new Composite(tabFolder, SWT.NONE); + devTabItem.setControl(root); + GridLayoutBuilder.create(root).columns(1); + + DeviceManagerPage devicePage = + new DeviceManagerPage(root, SWT.NONE, mSwtUpdaterData, mDeviceManager); + GridDataBuilder.create(devicePage).fill().grab(); + + devicePage.setAvdCreatedListener(new IAvdCreatedListener() { + @Override + public void onAvdCreated(AvdInfo avdInfo) { + if (avdInfo != null) { + mTabFolder.setSelection(0); // display mAvdPage + mAvdPage.selectAvd(avdInfo, true /*reloadAvdList*/); + } + } + }); + } + + @SuppressWarnings("unused") + // MenuBarWrapper works using side effects + private void createMenuBar() { + Menu menuBar = new Menu(mShell, SWT.BAR); + mShell.setMenuBar(menuBar); + + // Only create the tools menu when running as standalone. + // We don't need the tools menu when invoked from the IDE, or the SDK Manager + // or from the AVD Chooser dialog. The only point of the tools menu is to + // get the about box, and invoke Tools > SDK Manager, which we don't + // need to do in these cases. + if (mContext == AvdInvocationContext.STANDALONE) { + + MenuItem menuBarTools = new MenuItem(menuBar, SWT.CASCADE); + menuBarTools.setText("Tools"); + + Menu menuTools = new Menu(menuBarTools); + menuBarTools.setMenu(menuTools); + + MenuItem manageSdk = new MenuItem(menuTools, SWT.NONE); + manageSdk.setText("Manage SDK..."); + manageSdk.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent event) { + onSdkManager(); + } + }); + + try { + new MenuBarWrapper(APP_NAME_MAC_MENU, menuTools) { + @Override + public void onPreferencesMenuSelected() { + SettingsDialog sd = new SettingsDialog(mShell, mSwtUpdaterData); + sd.open(); + } + + @Override + public void onAboutMenuSelected() { + AboutDialog ad = new AboutDialog(mShell, mSwtUpdaterData); + ad.open(); + } + + @Override + public void printError(String format, Object... args) { + if (mSwtUpdaterData != null) { + mSwtUpdaterData.getSdkLog().error(null, format, args); + } + } + }; + } catch (Throwable e) { + mSwtUpdaterData.getSdkLog().error(e, "Failed to setup menu bar"); + e.printStackTrace(); + } + } + } + + + // -- Start of internal part ---------- + // Hide everything down-below from SWT designer + //$hide>>$ + + // --- Public API ----------- + + /** + * Adds a new listener to be notified when a change is made to the content of the SDK. + */ + public void addListener(ISdkChangeListener listener) { + mSwtUpdaterData.addListeners(listener); + } + + /** + * Removes a new listener to be notified anymore when a change is made to the content of + * the SDK. + */ + public void removeListener(ISdkChangeListener listener) { + mSwtUpdaterData.removeListener(listener); + } + + // --- Internals & UI Callbacks ----------- + + /** + * Called before the UI is created. + */ + private void preCreateContent() { + mSwtUpdaterData.setWindowShell(mShell); + // We need the UI factory to create the UI + mSwtUpdaterData.setImageFactory(new ImageFactory(mShell.getDisplay())); + // Note: we can't create the TaskFactory yet because we need the UI + // to be created first, so this is done in postCreateContent(). + } + + /** + * Once the UI has been created, initializes the content. + * This creates the pages, selects the first one, setup sources and scan for local folders. + * + * Returns true if we should show the window. + */ + private boolean postCreateContent() { + setWindowImage(mShell); + + setupSources(); + initializeSettings(); + + if (mSwtUpdaterData.checkIfInitFailed()) { + return false; + } + + mSwtUpdaterData.broadcastOnSdkLoaded(); + + return true; + } + + /** + * Creates the icon of the window shell. + * + * @param shell The shell on which to put the icon + */ + private void setWindowImage(Shell shell) { + String imageName = "android_icon_16.png"; //$NON-NLS-1$ + if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_DARWIN) { + imageName = "android_icon_128.png"; + } + + if (mSwtUpdaterData != null) { + ImageFactory imgFactory = mSwtUpdaterData.getImageFactory(); + if (imgFactory != null) { + shell.setImage(imgFactory.getImageByName(imageName)); + } + } + } + + /** + * Called by the main loop when the window has been disposed. + */ + private void dispose() { + mSwtUpdaterData.getSources().saveUserAddons(mSwtUpdaterData.getSdkLog()); + } + + /** + * Callback called when the window shell is disposed. + */ + private void onAndroidSdkUpdaterDispose() { + if (mOwnUpdaterData && mSwtUpdaterData != null) { + ImageFactory imgFactory = mSwtUpdaterData.getImageFactory(); + if (imgFactory != null) { + imgFactory.dispose(); + } + } + } + + /** + * Used to initialize the sources. + */ + private void setupSources() { + mSwtUpdaterData.setupDefaultSources(); + } + + /** + * Initializes settings. + * This must be called after addExtraPages(), which created a settings page. + * Iterate through all the pages to find the first (and supposedly unique) setting page, + * and use it to load and apply these settings. + */ + private void initializeSettings() { + mSettingsController = mSwtUpdaterData.getSettingsController(); + mSettingsController.loadSettings(); + mSettingsController.applySettings(); + } + + private void onSdkManager() { + ITaskFactory oldFactory = mSwtUpdaterData.getTaskFactory(); + + try { + SdkUpdaterWindowImpl2 win = new SdkUpdaterWindowImpl2( + mShell, + mSwtUpdaterData, + SdkUpdaterWindow.SdkInvocationContext.AVD_MANAGER); + + win.open(); + } catch (Exception e) { + mSwtUpdaterData.getSdkLog().error(e, "SDK Manager window error"); + } finally { + mSwtUpdaterData.setTaskFactory(oldFactory); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/DeviceManagerPage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/DeviceManagerPage.java new file mode 100644 index 00000000..bd20ef82 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/DeviceManagerPage.java @@ -0,0 +1,833 @@ +/* + * Copyright (C) 2012 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.sdkuilib.internal.repository.ui; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdklib.SystemImage; +import com.android.sdklib.devices.Device; +import com.android.sdklib.devices.DeviceManager; +import com.android.sdklib.devices.DeviceManager.DeviceFilter; +import com.android.sdklib.devices.DeviceManager.DevicesChangedListener; +import com.android.sdklib.devices.Hardware; +import com.android.sdklib.devices.Screen; +import com.android.sdklib.devices.Storage; +import com.android.sdklib.devices.Storage.Unit; +import com.android.sdklib.internal.avd.AvdInfo; +import com.android.sdklib.repository.ISdkChangeListener; +import com.android.sdkuilib.internal.repository.SwtUpdaterData; +import com.android.sdkuilib.internal.repository.icons.ImageFactory; +import com.android.sdkuilib.internal.repository.icons.ImageFactory.Filter; +import com.android.sdkuilib.internal.widgets.AvdCreationDialog; +import com.android.sdkuilib.internal.widgets.AvdSelector; +import com.android.sdkuilib.internal.widgets.DeviceCreationDialog; +import com.android.sdkuilib.ui.GridDataBuilder; +import com.android.sdkuilib.ui.GridLayoutBuilder; + +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.window.Window; +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.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.PaletteData; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.graphics.Resource; +import org.eclipse.swt.graphics.TextLayout; +import org.eclipse.swt.graphics.TextStyle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Button; +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.Shell; +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.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A page displaying Device Manager entries. + *

+ * This is displayed as a second tab in the AVD Manager window. + * The layout purposely matches the one from {@link AvdManagerPage} and {@link AvdSelector} + * so that there's a good consistency when switching tabs. + * The table displays a few properties of each device as well as actions to edit/add/delete + * devices and a button to create an AVD from a given device. + * + * Non-goals: this tries to keep it simple for a first iteration. Possible enhancements: + * - a way to sort the device list by name, manufacturer or screen size. + * - possibly a tree organized by manufacturer. + * - a filter box to do a string search on any part of the display. + */ +public class DeviceManagerPage extends Composite + implements ISdkChangeListener, DevicesChangedListener, DisposeListener { + + public interface IAvdCreatedListener { + public void onAvdCreated(AvdInfo createdAvdInfo); + } + + private final SwtUpdaterData mSwtUpdaterData; + private final DeviceManager mDeviceManager; + private Table mTable; + private Button mNewButton; + private Button mEditButton; + private Button mDeleteButton; + private Button mNewAvdButton; + private Button mRefreshButton; + private ImageFactory mImageFactory; + private Image mUserImage; + private Image mDeviceImage; + private int mImageWidth; + private boolean mDisableRefresh; + private IAvdCreatedListener mAvdCreatedListener; + private final Filter mUserColorFilter = new Filter() { + @Override + public Image filter(Image source) { + ImageData srcData = source.getImageData(); + + // swap green and blue + PaletteData p = srcData.palette; + int b = p.blueMask; + p.blueMask = p.greenMask; + p.greenMask = b; + + b = p.blueShift; + p.blueShift = p.greenShift; + p.greenShift = b; + + return new Image(source.getDevice(), srcData); + } + }; + + /** + * Create the composite. + * @param parent The parent of the composite. + * @param swtUpdaterData An instance of {@link SwtUpdaterData}. + */ + public DeviceManagerPage(Composite parent, + int swtStyle, + SwtUpdaterData swtUpdaterData, + DeviceManager deviceManager) { + super(parent, swtStyle); + + mSwtUpdaterData = swtUpdaterData; + mSwtUpdaterData.addListeners(this); + + mDeviceManager = deviceManager; + mDeviceManager.registerListener(this); + + createContents(this); + postCreate(); //$hide$ + } + + public void setAvdCreatedListener(IAvdCreatedListener avdCreatedListener) { + mAvdCreatedListener = avdCreatedListener; + } + + private void createContents(Composite parent) { + + // get some bitmaps. + mImageFactory = new ImageFactory(parent.getDisplay()); + mUserImage = getTagImage(null /*tag*/, true /*isUser*/); + mDeviceImage = getTagImage(null /*tag*/, false /*isUser*/); + mImageWidth = Math.max(mDeviceImage.getImageData().width, + Math.max(mUserImage.getImageData().width, + mDeviceImage.getImageData().width)); + + // Layout has 2 columns + GridLayoutBuilder.create(parent).columns(2); + + // Insert a top label explanation. This matches the design in AvdManagerPage so + // that the table starts at the same height on both tabs. + Label label = new Label(parent, SWT.NONE); + label.setText("List of known device definitions. This can later be used to create Android Virtual Devices."); + GridDataBuilder.create(label).hSpan(2); + + // Device table. + mTable = new Table(parent, SWT.FULL_SELECTION | SWT.SINGLE | SWT.BORDER); + mTable.setHeaderVisible(true); + mTable.setLinesVisible(true); + mTable.setFont(parent.getFont()); + setTableHeightHint(30); + + // Buttons on the side. + Composite buttons = new Composite(parent, SWT.NONE); + GridLayoutBuilder.create(buttons).columns(1).noMargins(); + GridDataBuilder.create(buttons).vFill(); + buttons.setFont(parent.getFont()); + + mNewAvdButton = new Button(buttons, SWT.PUSH | SWT.FLAT); + mNewAvdButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mNewAvdButton.setText("Create AVD..."); + mNewAvdButton.setToolTipText("Creates a new AVD based on this device definition."); + mNewAvdButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + onCreateAvd(); + } + }); + + @SuppressWarnings("unused") + Label spacing = new Label(buttons, SWT.NONE); + + mNewButton = new Button(buttons, SWT.PUSH | SWT.FLAT); + mNewButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mNewButton.setText("Create Device..."); + mNewButton.setToolTipText("Creates a new user device definition."); + mNewButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + onNewDevice(); + } + }); + + mEditButton = new Button(buttons, SWT.PUSH | SWT.FLAT); + mEditButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mEditButton.setText("Edit..."); + mEditButton.setToolTipText("Edit an existing device definition."); + mEditButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + onEditDevice(); + } + }); + + mDeleteButton = new Button(buttons, SWT.PUSH | SWT.FLAT); + mDeleteButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mDeleteButton.setText("Delete..."); + mDeleteButton.setToolTipText("Deletes the selected AVD."); + mDeleteButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + onDeleteDevice(); + } + }); + + Composite padding = new Composite(buttons, SWT.NONE); + padding.setLayoutData(new GridData(GridData.FILL_VERTICAL)); + + mRefreshButton = new Button(buttons, SWT.PUSH | SWT.FLAT); + mRefreshButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mRefreshButton.setText("Refresh"); + mRefreshButton.setToolTipText("Reloads the list of devices."); + mRefreshButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + onRefresh(); + } + }); + + // Legend at the bottom. + // This matches the one on AvdSelector so that the table height in the tab be similar. + Composite legend = new Composite(parent, SWT.NONE); + GridLayoutBuilder.create(legend).columns(4).noMargins(); + GridDataBuilder.create(legend).hFill().vTop().hGrab().hSpan(2); + legend.setFont(parent.getFont()); + + new Label(legend, SWT.NONE).setImage(mUserImage); + new Label(legend, SWT.NONE).setText("A user-created device definition."); + new Label(legend, SWT.NONE).setImage(mDeviceImage); + new Label(legend, SWT.NONE).setText("A generic device definition."); + + // create the table columns + final TableColumn column0 = new TableColumn(mTable, SWT.NONE); + column0.setText("Device"); + + adjustColumnsWidth(mTable, column0); + setupSelectionListener(mTable); + fillTable(mTable); + updateButtonStates(); + setEnabled(true); + } + + private void adjustColumnsWidth(final Table table, final TableColumn column0) { + // Add a listener to resize the column to the full width of the table + table.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Rectangle r = table.getClientArea(); + column0.setWidth(r.width * 100 / 100 - 1); // 100% + } + }); + } + + private void setupSelectionListener(Table table) { + // TODO Auto-generated method stub + + } + + /** + * Sets the table grid layout data. + * + * @param heightHint If > 0, the height hint is set to the requested value. + */ + public void setTableHeightHint(int heightHint) { + GridData data = new GridData(); + if (heightHint > 0) { + data.heightHint = heightHint; + } + data.grabExcessVerticalSpace = true; + data.grabExcessHorizontalSpace = true; + data.horizontalAlignment = GridData.FILL; + data.verticalAlignment = GridData.FILL; + mTable.setLayoutData(data); + } + + @Override + public void widgetDisposed(DisposeEvent e) { + dispose(); + } + + @Override + public void dispose() { + mSwtUpdaterData.removeListener(this); + mDeviceManager.unregisterListener(this); + super.dispose(); + } + + @Override + protected void checkSubclass() { + // Disable the check that prevents subclassing of SWT components + } + + // -- Start of internal part ---------- + // Hide everything down-below from SWT designer + //$hide>>$ + + /** + * Called by the constructor right after {@link #createContents(Composite)}. + */ + private void postCreate() { + // nothing to be done for now. + } + + + // ------- + + private static class CellInfo { + final boolean mIsUser; + final Device mDevice; + final TextLayout mWidget; + Rectangle mBounds; + + CellInfo(boolean isUser, Device device, TextLayout widget) { + mIsUser = isUser; + mDevice = device; + mWidget = widget; + } + } + + private void fillTable(final Table table) { + + table.removeAll(); + disposeTableResources(table.getData("disposeResources")); + + final List disposables = new ArrayList(); + + Font boldFont = getBoldFont(table); + if (boldFont != null) { + disposables.add(boldFont); + } else { + boldFont = table.getFont(); + } + + try { + mDisableRefresh = true; + disposables.addAll(fillDevices(table, boldFont, true, + mDeviceManager.getDevices(DeviceFilter.USER))); + disposables.addAll(fillDevices(table, boldFont, false, + mDeviceManager.getDevices(EnumSet.of(DeviceFilter.DEFAULT, + DeviceFilter.VENDOR, + DeviceFilter.SYSTEM_IMAGES)))); + } finally { + mDisableRefresh = false; + } + + table.setData("disposeResources", disposables); + + if (!Boolean.TRUE.equals(table.getData("createdTableListeners"))) { + table.addListener(SWT.PaintItem, new Listener() { + @Override + public void handleEvent(Event event) { + if (event.item != null) { + Object info = event.item.getData(); + if (info instanceof CellInfo) { + ((CellInfo) info).mWidget.draw(event.gc, event.x, event.y + 1); + } + } + } + }); + + table.addListener(SWT.MeasureItem, new Listener() { + @Override + public void handleEvent(Event event) { + if (event.item != null) { + Object info = event.item.getData(); + if (info instanceof CellInfo) { + CellInfo ci = (CellInfo) info; + Rectangle bounds = ci.mBounds; + if (bounds == null) { + // TextLayout.getBounds() seems expensive, so let's cache it. + ci.mBounds = bounds = ci.mWidget.getBounds(); + } + event.width = bounds.width + 2; + event.height = bounds.height + 4; + } + } + } + }); + + table.addDisposeListener(new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent event) { + disposeTableResources(table.getData("disposeResources")); + } + }); + + table.addSelectionListener(new SelectionListener() { + /** Handles single clicks on a row. */ + @Override + public void widgetSelected(SelectionEvent event) { + updateButtonStates(); + } + + /** Handles double click on a row. */ + @Override + public void widgetDefaultSelected(SelectionEvent event) { + // FIXME: should double-click be to edit a device or create a new AVD? + onEditDevice(); + } + }); + } + + if (table.getItemCount() == 0) { + table.setEnabled(true); + TableItem item = new TableItem(table, SWT.NONE); + item.setData(null); + item.setText(0, "No devices available"); + return; + } + + table.setData("createdTableListeners", Boolean.TRUE); + } + + private void disposeTableResources(Object disposablesList) { + if (disposablesList instanceof List) { + for (Object obj : (List) disposablesList) { + if (obj instanceof Resource) { + ((Resource) obj).dispose(); + } + } + } + } + + private Font getBoldFont(Table table) { + Display display = table.getDisplay(); + FontData[] fds = table.getFont().getFontData(); + if (fds != null && fds.length > 0) { + fds[0].setStyle(SWT.BOLD); + return new Font(display, fds[0]); + } + return null; + } + + private List fillDevices( + Table table, + Font boldFont, + boolean isUser, + Collection devicesCollection) { + List disposables = new ArrayList(); + Display display = table.getDisplay(); + + TextStyle boldStyle = new TextStyle(); + boldStyle.font = boldFont; + + // We need the list to be be modifiable so that we can sort it. + ArrayList devices = new ArrayList(devicesCollection); + Collections.sort(devices, Device.getDisplayComparator()); + + // Generate a list of the AVD names using these devices + Map> device2avdMap = new HashMap>(); + for (AvdInfo avd : mSwtUpdaterData.getAvdManager().getAllAvds()) { + String n = avd.getDeviceName(); + String m = avd.getDeviceManufacturer(); + if (n == null || m == null || n.isEmpty() || m.isEmpty()) { + continue; + } + for (Device device : devices) { + if (m.equals(device.getManufacturer()) && n.equals(device.getName())) { + List list = device2avdMap.get(device); + if (list == null) { + list = new LinkedList(); + device2avdMap.put(device, list); + } + list.add(avd.getName()); + } + } + } + + final String prefix = "\n "; + + for (Device device : devices) { + TableItem item = new TableItem(table, SWT.NONE); + TextLayout widget = new TextLayout(display); + CellInfo ci = new CellInfo(isUser, device, widget); + item.setData(ci); + + widget.setIndent(mImageWidth * 2); + widget.setFont(table.getFont()); + + StringBuilder sb = new StringBuilder(); + String name = getPrettyName(device, false /*leadZeroes*/); + sb.append(name); + int pos1 = sb.length(); + + String manufacturer = device.getManufacturer(); + if (!manufacturer.contains(NEXUS)) { + sb.append(" by ").append(manufacturer); + } + + Image img = getTagImage(device.getTagId(), isUser); + item.setImage(img != null ? img : mDeviceImage); + + Hardware hw = device.getDefaultHardware(); + Screen screen = hw.getScreen(); + sb.append(prefix); + sb.append(String.format(java.util.Locale.US, + "Screen: %1$.1f\", %2$d \u00D7 %3$d, %4$s %5$s", // U+00D7: Unicode multiplication sign + screen.getDiagonalLength(), + screen.getXDimension(), + screen.getYDimension(), + screen.getSize().getShortDisplayValue(), + screen.getPixelDensity().getResourceValue() + )); + + Storage sto = hw.getRam(); + Unit unit = sto.getSizeAsUnit(Unit.GiB) > 1 ? Unit.GiB : Unit.MiB; + sb.append(prefix); + sb.append(String.format(java.util.Locale.US, + "RAM: %1$d %2$s", + sto.getSizeAsUnit(unit), + unit)); + + List avdList = device2avdMap.get(device); + if (avdList != null && !avdList.isEmpty()) { + sb.append(prefix); + sb.append("Used by: "); + boolean first = true; + for (String avd : avdList) { + if (!first) { + sb.append(", "); + } + sb.append(avd); + first = false; + } + } + + widget.setText(sb.toString()); + widget.setStyle(boldStyle, 0, pos1); + } + + return disposables; + } + + @Nullable + private Image getTagImage(@NonNull String tagId, boolean isUser) { + if (tagId == null) { + tagId = SystemImage.DEFAULT_TAG.getId(); + } + + String fname = String.format("tag_%s_32.png", tagId); + String kname = (isUser ? "user_" : "dev_") + fname; + return mImageFactory.getImageByName(fname, kname, isUser ? mUserColorFilter : null); + } + + // Constants extracted from DeviceMenuListerner -- TODO refactor somewhere else. + private static final String NEXUS = "Nexus"; //$NON-NLS-1$ + private static Pattern PATTERN = Pattern.compile( + "(\\d+\\.?\\d*)(?:in|\") (.+?)( \\(.*Nexus.*\\))?"); //$NON-NLS-1$ + /** + * Returns a pretty name for the device. + * + * Extracted from DeviceMenuListener. + * Modified to remove the leading space insertion as it doesn't render + * neatly in the avd manager. Instead added the option to add leading + * zeroes to make the string names sort properly. + * + * Replace "'in'" with '"' (e.g. 2.7" QVGA instead of 2.7in QVGA) + * Use the same precision for all devices (all but one specify decimals) + * Add in screen resolution and density + */ + private static String getPrettyName(Device d, boolean leadZeroes) { + if (d == null) { + return ""; + } + String name = d.getName(); + if (name.equals("3.7 FWVGA slider")) { //$NON-NLS-1$ + // Fix metadata: this one entry doesn't have "in" like the rest of them + name = "3.7in FWVGA slider"; //$NON-NLS-1$ + } + + Matcher matcher = PATTERN.matcher(name); + if (matcher.matches()) { + String size = matcher.group(1); + String n = matcher.group(2); + int dot = size.indexOf('.'); + if (dot == -1) { + size = size + ".0"; + dot = size.length() - 2; + } + if (leadZeroes && dot < 3) { + // Pad to have at least 3 digits before the dot, for sorting purposes. + // We can revisit this once we get devices that are more than 999 inches wide. + size = "000".substring(dot) + size; + } + name = size + "\" " + n; + } + + return name; + } + + /** + * Returns the currently selected cell info in the table or null + */ + private CellInfo getTableSelection() { + if (mTable.isDisposed()) { + return null; + } + int selIndex = mTable.getSelectionIndex(); + if (selIndex >= 0) { + return (CellInfo) mTable.getItem(selIndex).getData(); + } + + return null; + } + + private void updateButtonStates() { + CellInfo ci = getTableSelection(); + + mNewButton.setEnabled(true); + mEditButton.setEnabled(ci != null); + mEditButton.setText((ci != null && !ci.mIsUser) ? "Clone..." : "Edit..."); + mDeleteButton.setEnabled(ci != null && ci.mIsUser); + mNewAvdButton.setEnabled(ci != null); + mRefreshButton.setEnabled(true); + } + + private void onNewDevice() { + DeviceCreationDialog dlg = new DeviceCreationDialog( + getShell(), + mDeviceManager, + mSwtUpdaterData.getImageFactory(), + null /*device*/); + if (dlg.open() == Window.OK) { + onRefresh(); + + // Select the new device, if any. + selectCellByDevice(dlg.getCreatedDevice()); + updateButtonStates(); + } + } + + private void onEditDevice() { + CellInfo ci = getTableSelection(); + if (ci == null || ci.mDevice == null) { + return; + } + + DeviceCreationDialog dlg = new DeviceCreationDialog( + getShell(), + mDeviceManager, + mSwtUpdaterData.getImageFactory(), + ci.mDevice); + if (dlg.open() == Window.OK) { + onRefresh(); + + // Select the new device, if any. + selectCellByDevice(dlg.getCreatedDevice()); + updateButtonStates(); + } + } + + private void onDeleteDevice() { + CellInfo ci = getTableSelection(); + if (ci == null || ci.mDevice == null || !ci.mIsUser) { + return; + } + + final String name = getPrettyName(ci.mDevice, false /*leadZeroes*/); + final AtomicBoolean result = new AtomicBoolean(false); + getDisplay().syncExec(new Runnable() { + @Override + public void run() { + Shell shell = getDisplay().getActiveShell(); + boolean ok = MessageDialog.openQuestion(shell, + "Delete Device Definition", + String.format( + "Please confirm that you want to delete the device definition named '%s'. This operation cannot be reverted.", + name)); + result.set(ok); + } + }); + + if (result.get()) { + mDeviceManager.removeUserDevice(ci.mDevice); + mDeviceManager.saveUserDevices(); + onRefresh(); + } + } + + private void onCreateAvd() { + CellInfo ci = getTableSelection(); + if (ci == null || ci.mDevice == null) { + return; + } + + final AvdCreationDialog dlg = new AvdCreationDialog(mTable.getShell(), + mSwtUpdaterData.getAvdManager(), + mImageFactory, + mSwtUpdaterData.getSdkLog(), + null); + dlg.selectInitialDevice(ci.mDevice); + + if (dlg.open() == Window.OK) { + onRefresh(); + + if (mAvdCreatedListener != null) { + mAvdCreatedListener.onAvdCreated(dlg.getCreatedAvd()); + } + } + } + + private void onRefresh() { + if (mDisableRefresh || mTable.isDisposed()) { + return; + } + int selIndex = mTable.getSelectionIndex(); + CellInfo selected = getTableSelection(); + + fillTable(mTable); + + if (selected != null) { + if (selectCellByName(selected)) { + updateButtonStates(); + return; + } + } + // If not found by name, use the position if available. + if (selIndex >= 0 && selIndex < mTable.getItemCount()) { + mTable.select(selIndex); + } + } + + private boolean selectCellByName(CellInfo selected) { + if (mTable.isDisposed() || selected == null || selected.mDevice == null) { + return false; + } + String name = selected.mDevice.getName(); + for (int n = mTable.getItemCount() - 1; n >= 0; n--) { + TableItem item = mTable.getItem(n); + Object data = item.getData(); + if (data instanceof CellInfo) { + CellInfo ci = (CellInfo) data; + if (ci != null && ci.mDevice != null && name.equals(ci.mDevice.getName())) { + // Same cell object. Select it. + mTable.select(n); + return true; + } + } + } + return false; + } + + private boolean selectCellByDevice(Device selected) { + if (mTable.isDisposed() || selected == null) { + return false; + } + for (int n = mTable.getItemCount() - 1; n >= 0; n--) { + TableItem item = mTable.getItem(n); + Object data = item.getData(); + if (data instanceof CellInfo) { + CellInfo ci = (CellInfo) data; + if (ci != null && ci.mDevice == selected) { + // Same device object. Select it. + mTable.select(n); + return true; + } + } + } + return false; + } + + // ------- + + + // --- Implementation of ISdkChangeListener --- + + @Override + public void onSdkLoaded() { + onSdkReload(); + } + + @Override + public void onSdkReload() { + onRefresh(); + } + + @Override + public void preInstallHook() { + // nothing to be done for now. + } + + @Override + public void postInstallHook() { + // nothing to be done for now. + } + + // --- Implementation of DevicesChangeListener + + @Override + public void onDevicesChanged() { + onRefresh(); + } + + + // End of hiding from SWT Designer + //$hide<<$ +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/LogWindow.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/LogWindow.java new file mode 100644 index 00000000..5977f315 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/LogWindow.java @@ -0,0 +1,379 @@ +/* + * Copyright (C) 2011 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.sdkuilib.internal.repository.ui; + +import com.android.sdkuilib.internal.tasks.ILogUiProvider; +import com.android.sdkuilib.ui.GridDataBuilder; +import com.android.sdkuilib.ui.GridLayoutBuilder; +import com.android.utils.ILogger; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.StyleRange; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.ShellAdapter; +import org.eclipse.swt.events.ShellEvent; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Widget; + + +/** + * A floating log window that can be displayed or hidden by the main SDK Manager 2 window. + * It displays a log of the sdk manager operation (listing, install, delete) including + * any errors (e.g. network error or install/delete errors.) + *

+ * Since the SDK Manager will direct all log to this window, its purpose is to be + * opened by the main window at startup and left open all the time. When not needed + * the floating window is hidden but not closed. This way it can easily accumulate + * all the log. + */ +class LogWindow implements ILogUiProvider { + + private Shell mParentShell; + private Shell mShell; + private Composite mRootComposite; + private StyledText mStyledText; + private Label mLogDescription; + private Button mCloseButton; + + private final ILogger mSecondaryLog; + private boolean mCloseRequested; + private boolean mInitPosition = true; + private String mLastLogMsg = null; + + private enum TextStyle { + DEFAULT, + TITLE, + ERROR + } + + /** + * Creates the floating window. Callers should use {@link #open()} later. + * + * @param parentShell Parent container + * @param secondaryLog An optional logger where messages will also be output. + */ + public LogWindow(Shell parentShell, ILogger secondaryLog) { + mParentShell = parentShell; + mSecondaryLog = secondaryLog; + } + + /** + * For testing only. See {@link #open()} and {@link #close()} for normal usage. + * @wbp.parser.entryPoint + */ + void openBlocking() { + open(); + Display display = Display.getDefault(); + while (!mShell.isDisposed()) { + if (!display.readAndDispatch()) { + display.sleep(); + } + } + close(); + } + + /** + * Opens the window. + * This call does not block and relies on the fact that the main window is + * already running an SWT event dispatch loop. + * Caller should use {@link #close()} later. + */ + public void open() { + createShell(); + createContents(); + mShell.open(); + mShell.layout(); + mShell.setVisible(false); + } + + /** + * Closes and destroys the window. + * This must be called just before quitting the app. + *

+ * To simply hide/show the window, use {@link #setVisible(boolean)} instead. + */ + public void close() { + if (mShell != null && !mShell.isDisposed()) { + mCloseRequested = true; + mShell.close(); + mShell = null; + } + } + + /** + * Determines whether the window is currently shown or not. + * + * @return True if the window is shown. + */ + public boolean isVisible() { + return mShell != null && !mShell.isDisposed() && mShell.isVisible(); + } + + /** + * Toggles the window visibility. + * + * @param visible True to make the window visible, false to hide it. + */ + public void setVisible(boolean visible) { + if (mShell != null && !mShell.isDisposed()) { + mShell.setVisible(visible); + if (visible && mInitPosition) { + mInitPosition = false; + positionWindow(); + } + } + } + + private void createShell() { + mShell = new Shell(mParentShell, SWT.SHELL_TRIM | SWT.TOOL); + mShell.setMinimumSize(new Point(600, 300)); + mShell.setSize(450, 300); + mShell.setText("Android SDK Manager Log"); + GridLayoutBuilder.create(mShell); + + mShell.addShellListener(new ShellAdapter() { + @Override + public void shellClosed(ShellEvent e) { + if (!mCloseRequested) { + e.doit = false; + setVisible(false); + } + } + }); + } + + /** + * Create contents of the dialog. + */ + private void createContents() { + mRootComposite = new Composite(mShell, SWT.NONE); + GridLayoutBuilder.create(mRootComposite).columns(2); + GridDataBuilder.create(mRootComposite).fill().grab(); + + mStyledText = new StyledText(mRootComposite, + SWT.BORDER | SWT.MULTI | SWT.READ_ONLY | SWT.WRAP | SWT.V_SCROLL); + GridDataBuilder.create(mStyledText).hSpan(2).fill().grab(); + + mLogDescription = new Label(mRootComposite, SWT.NONE); + GridDataBuilder.create(mLogDescription).hFill().hGrab(); + + mCloseButton = new Button(mRootComposite, SWT.NONE); + mCloseButton.setText("Close"); + mCloseButton.setToolTipText("Closes the log window"); + mCloseButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + setVisible(false); //$hide$ + } + }); + } + + // --- Implementation of ILogUiProvider --- + + + /** + * Sets the description in the current task dialog. + * This method can be invoked from a non-UI thread. + */ + @Override + public void setDescription(final String description) { + syncExec(mLogDescription, new Runnable() { + @Override + public void run() { + mLogDescription.setText(description); + + if (acceptLog(description, true /*isDescription*/)) { + appendLine(TextStyle.TITLE, description); + + if (mSecondaryLog != null) { + mSecondaryLog.info("%1$s", description); //$NON-NLS-1$ + } + } + } + }); + } + + /** + * Logs a "normal" information line. + * This method can be invoked from a non-UI thread. + */ + @Override + public void log(final String log) { + if (acceptLog(log, false /*isDescription*/)) { + syncExec(mLogDescription, new Runnable() { + @Override + public void run() { + appendLine(TextStyle.DEFAULT, log); + } + }); + + if (mSecondaryLog != null) { + mSecondaryLog.info(" %1$s", log); //$NON-NLS-1$ + } + } + } + + /** + * Logs an "error" information line. + * This method can be invoked from a non-UI thread. + */ + @Override + public void logError(final String log) { + if (acceptLog(log, false /*isDescription*/)) { + syncExec(mLogDescription, new Runnable() { + @Override + public void run() { + appendLine(TextStyle.ERROR, log); + } + }); + + if (mSecondaryLog != null) { + mSecondaryLog.error(null, "%1$s", log); //$NON-NLS-1$ + } + } + } + + /** + * Logs a "verbose" information line, that is extra details which are typically + * not that useful for the end-user and might be hidden until explicitly shown. + * This method can be invoked from a non-UI thread. + */ + @Override + public void logVerbose(final String log) { + if (acceptLog(log, false /*isDescription*/)) { + syncExec(mLogDescription, new Runnable() { + @Override + public void run() { + appendLine(TextStyle.DEFAULT, " " + log); //$NON-NLS-1$ + } + }); + + if (mSecondaryLog != null) { + mSecondaryLog.info(" %1$s", log); //$NON-NLS-1$ + } + } + } + + + // ---- + + + /** + * Centers the dialog in its parent shell. + */ + private void positionWindow() { + // Centers the dialog in its parent shell + Shell child = mShell; + if (child != null && mParentShell != null) { + // get the parent client area with a location relative to the display + Rectangle parentArea = mParentShell.getClientArea(); + Point parentLoc = mParentShell.getLocation(); + int px = parentLoc.x; + int py = parentLoc.y; + int pw = parentArea.width; + int ph = parentArea.height; + + Point childSize = child.getSize(); + int cw = Math.max(childSize.x, pw); + int ch = childSize.y; + + int x = 30 + px + (pw - cw) / 2; + if (x < 0) x = 0; + + int y = py + (ph - ch) / 2; + if (y < py) y = py; + + child.setLocation(x, y); + child.setSize(cw, ch); + } + } + + private void appendLine(TextStyle style, String text) { + if (!text.endsWith("\n")) { //$NON-NLS-1$ + text += '\n'; + } + + int start = mStyledText.getCharCount(); + + if (style == TextStyle.DEFAULT) { + mStyledText.append(text); + + } else { + mStyledText.append(text); + + StyleRange sr = new StyleRange(); + sr.start = start; + sr.length = text.length(); + sr.fontStyle = SWT.BOLD; + if (style == TextStyle.ERROR) { + sr.foreground = mStyledText.getDisplay().getSystemColor(SWT.COLOR_DARK_RED); + } + sr.underline = false; + mStyledText.setStyleRange(sr); + } + + // Scroll caret if it was already at the end before we added new text. + // Ideally we would scroll if the scrollbar is at the bottom but we don't + // have direct access to the scrollbar without overriding the SWT impl. + if (mStyledText.getCaretOffset() >= start) { + mStyledText.setSelection(mStyledText.getCharCount()); + } + } + + + private void syncExec(final Widget widget, final Runnable runnable) { + if (widget != null && !widget.isDisposed()) { + widget.getDisplay().syncExec(runnable); + } + } + + /** + * Filter messages displayed in the log:
+ * - Messages with a % are typical part of a progress update and shouldn't be in the log.
+ * - Messages that are the same as the same output message should be output a second time. + * + * @param msg The potential log line to print. + * @return True if the log line should be printed, false otherwise. + */ + private boolean acceptLog(String msg, boolean isDescription) { + if (msg == null) { + return false; + } + + msg = msg.trim(); + + // Descriptions also have the download progress status (0..100%) which we want to avoid + if (isDescription && msg.indexOf('%') != -1) { + return false; + } + + if (msg.equals(mLastLogMsg)) { + return false; + } + + mLastLogMsg = msg; + return true; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPage.java new file mode 100644 index 00000000..bb72ea18 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPage.java @@ -0,0 +1,1215 @@ +/* + * Copyright (C) 2011 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.sdkuilib.internal.repository.ui; + +import com.android.sdklib.internal.repository.ITask; +import com.android.sdklib.internal.repository.ITaskMonitor; +import com.android.sdklib.internal.repository.archives.Archive; +import com.android.sdklib.internal.repository.archives.ArchiveInstaller; +import com.android.sdklib.internal.repository.packages.Package; +import com.android.sdklib.internal.repository.updater.PkgItem; +import com.android.sdklib.internal.repository.updater.PkgItem.PkgState; +import com.android.sdklib.repository.ISdkChangeListener; +import com.android.sdkuilib.internal.repository.SwtUpdaterData; +import com.android.sdkuilib.internal.repository.core.PkgCategory; +import com.android.sdkuilib.internal.repository.core.PkgCategoryApi; +import com.android.sdkuilib.internal.repository.core.PkgContentProvider; +import com.android.sdkuilib.internal.repository.icons.ImageFactory; +import com.android.sdkuilib.repository.SdkUpdaterWindow.SdkInvocationContext; +import com.android.sdkuilib.ui.GridDataBuilder; +import com.android.sdkuilib.ui.GridLayoutBuilder; + +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.viewers.CheckStateChangedEvent; +import org.eclipse.jface.viewers.CheckboxTreeViewer; +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.jface.viewers.ColumnViewerToolTipSupport; +import org.eclipse.jface.viewers.DoubleClickEvent; +import org.eclipse.jface.viewers.ICheckStateListener; +import org.eclipse.jface.viewers.IDoubleClickListener; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.ITreeSelection; +import org.eclipse.jface.viewers.TreeViewerColumn; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.jface.viewers.ViewerFilter; +import org.eclipse.jface.window.ToolTip; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +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.graphics.Image; +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.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Link; +import org.eclipse.swt.widgets.MenuItem; +import org.eclipse.swt.widgets.Text; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeColumn; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +/** + * Page that displays both locally installed packages as well as all known + * remote available packages. This gives an overview of what is installed + * vs what is available and allows the user to update or install packages. + */ +public final class PackagesPage extends Composite implements ISdkChangeListener { + + enum MenuAction { + RELOAD (SWT.NONE, "Reload"), + SHOW_ADDON_SITES (SWT.NONE, "Manage Add-on Sites..."), + TOGGLE_SHOW_ARCHIVES (SWT.CHECK, "Show Archives Details"), + TOGGLE_SHOW_INSTALLED_PKG (SWT.CHECK, "Show Installed Packages"), + TOGGLE_SHOW_OBSOLETE_PKG (SWT.CHECK, "Show Obsolete Packages"), + TOGGLE_SHOW_UPDATE_NEW_PKG (SWT.CHECK, "Show Updates/New Packages") + ; + + private final int mMenuStyle; + private final String mMenuTitle; + + MenuAction(int menuStyle, String menuTitle) { + mMenuStyle = menuStyle; + mMenuTitle = menuTitle; + } + + public int getMenuStyle() { + return mMenuStyle; + } + + public String getMenuTitle() { + return mMenuTitle; + } + }; + + private final Map mMenuActions = new HashMap(); + + private final PackagesPageImpl mImpl; + private final SdkInvocationContext mContext; + + private boolean mDisplayArchives = false; + private boolean mOperationPending; + + private Composite mGroupPackages; + private Text mTextSdkOsPath; + private Button mCheckFilterObsolete; + private Button mCheckFilterInstalled; + private Button mCheckFilterNew; + private Composite mGroupOptions; + private Composite mGroupSdk; + private Button mButtonDelete; + private Button mButtonInstall; + private Font mTreeFontItalic; + private TreeColumn mTreeColumnName; + private CheckboxTreeViewer mTreeViewer; + + public PackagesPage( + Composite parent, + int swtStyle, + SwtUpdaterData swtUpdaterData, + SdkInvocationContext context) { + super(parent, swtStyle); + mImpl = new PackagesPageImpl(swtUpdaterData) { + @Override + protected boolean isUiDisposed() { + return mGroupPackages == null || mGroupPackages.isDisposed(); + }; + @Override + protected void syncExec(Runnable runnable) { + if (!isUiDisposed()) { + mGroupPackages.getDisplay().syncExec(runnable); + } + }; + + @Override + protected void syncViewerSelection() { + PackagesPage.this.syncViewerSelection(); + } + + @Override + protected void refreshViewerInput() { + PackagesPage.this.refreshViewerInput(); + } + + @Override + protected Font getTreeFontItalic() { + return mTreeFontItalic; + } + + @Override + protected void loadPackages(boolean useLocalCache, boolean overrideExisting) { + PackagesPage.this.loadPackages(useLocalCache, overrideExisting); + } + }; + mContext = context; + + createContents(this); + postCreate(); //$hide$ + } + + public void performFirstLoad() { + mImpl.performFirstLoad(); + } + + @SuppressWarnings("unused") + private void createContents(Composite parent) { + GridLayoutBuilder.create(parent).noMargins().columns(2); + + mGroupSdk = new Composite(parent, SWT.NONE); + GridDataBuilder.create(mGroupSdk).hFill().vCenter().hGrab().hSpan(2); + GridLayoutBuilder.create(mGroupSdk).columns(2); + + Label label1 = new Label(mGroupSdk, SWT.NONE); + label1.setText("SDK Path:"); + + mTextSdkOsPath = new Text(mGroupSdk, SWT.NONE); + GridDataBuilder.create(mTextSdkOsPath).hFill().vCenter().hGrab(); + mTextSdkOsPath.setEnabled(false); + + Group groupPackages = new Group(parent, SWT.NONE); + mGroupPackages = groupPackages; + GridDataBuilder.create(mGroupPackages).fill().grab().hSpan(2); + groupPackages.setText("Packages"); + GridLayoutBuilder.create(groupPackages).columns(1); + + mTreeViewer = new CheckboxTreeViewer(groupPackages, SWT.BORDER); + mImpl.setITreeViewer(new PackagesPageImpl.ICheckboxTreeViewer() { + @Override + public Object getInput() { + return mTreeViewer.getInput(); + } + + @Override + public void setInput(List cats) { + mTreeViewer.setInput(cats); + } + + @Override + public void setContentProvider(PkgContentProvider pkgContentProvider) { + mTreeViewer.setContentProvider(pkgContentProvider); + } + + @Override + public void refresh() { + mTreeViewer.refresh(); + } + + @Override + public Object[] getCheckedElements() { + return mTreeViewer.getCheckedElements(); + } + }); + mTreeViewer.addFilter(new ViewerFilter() { + @Override + public boolean select(Viewer viewer, Object parentElement, Object element) { + return filterViewerItem(element); + } + }); + + mTreeViewer.addCheckStateListener(new ICheckStateListener() { + @Override + public void checkStateChanged(CheckStateChangedEvent event) { + onTreeCheckStateChanged(event); //$hide$ + } + }); + + mTreeViewer.addDoubleClickListener(new IDoubleClickListener() { + @Override + public void doubleClick(DoubleClickEvent event) { + onTreeDoubleClick(event); //$hide$ + } + }); + + Tree tree = mTreeViewer.getTree(); + tree.setLinesVisible(true); + tree.setHeaderVisible(true); + GridDataBuilder.create(tree).fill().grab(); + + // column name icon is set when loading depending on the current filter type + // (e.g. API level or source) + TreeViewerColumn columnName = new TreeViewerColumn(mTreeViewer, SWT.NONE); + mTreeColumnName = columnName.getColumn(); + mTreeColumnName.setText("Name"); + mTreeColumnName.setWidth(340); + + TreeViewerColumn columnApi = new TreeViewerColumn(mTreeViewer, SWT.NONE); + TreeColumn treeColumn2 = columnApi.getColumn(); + treeColumn2.setText("API"); + treeColumn2.setAlignment(SWT.CENTER); + treeColumn2.setWidth(50); + + TreeViewerColumn columnRevision = new TreeViewerColumn(mTreeViewer, SWT.NONE); + TreeColumn treeColumn3 = columnRevision.getColumn(); + treeColumn3.setText("Rev."); + treeColumn3.setToolTipText("Revision currently installed"); + treeColumn3.setAlignment(SWT.CENTER); + treeColumn3.setWidth(50); + + + TreeViewerColumn columnStatus = new TreeViewerColumn(mTreeViewer, SWT.NONE); + TreeColumn treeColumn4 = columnStatus.getColumn(); + treeColumn4.setText("Status"); + treeColumn4.setAlignment(SWT.LEAD); + treeColumn4.setWidth(190); + + mImpl.setIColumns( + wrapColumn(columnName), + wrapColumn(columnApi), + wrapColumn(columnRevision), + wrapColumn(columnStatus)); + + mGroupOptions = new Composite(groupPackages, SWT.NONE); + GridDataBuilder.create(mGroupOptions).hFill().vCenter().hGrab(); + GridLayoutBuilder.create(mGroupOptions).columns(7).noMargins(); + + // Options line 1, 7 columns + + Label label3 = new Label(mGroupOptions, SWT.NONE); + label3.setText("Show:"); + + mCheckFilterNew = new Button(mGroupOptions, SWT.CHECK); + mCheckFilterNew.setText("Updates/New"); + mCheckFilterNew.setToolTipText("Show Updates and New"); + mCheckFilterNew.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + refreshViewerInput(); + } + }); + mCheckFilterNew.setSelection(true); + + mCheckFilterInstalled = new Button(mGroupOptions, SWT.CHECK); + mCheckFilterInstalled.setToolTipText("Show Installed"); + mCheckFilterInstalled.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + refreshViewerInput(); + } + }); + mCheckFilterInstalled.setSelection(true); + mCheckFilterInstalled.setText("Installed"); + + new Label(mGroupOptions, SWT.NONE); + + Link linkSelectNew = new Link(mGroupOptions, SWT.NONE); + // Note for i18n: we need to identify which link is used, and this is done by using the + // text itself so for translation purposes we want to keep the
link strings separate. + final String strLinkNew = "New"; + final String strLinkUpdates = "Updates"; + linkSelectNew.setText( + String.format("Select %1$s or %2$s", strLinkNew, strLinkUpdates)); + linkSelectNew.setToolTipText("Selects all items that are either new or updates."); + GridDataBuilder.create(linkSelectNew).hFill(); + linkSelectNew.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + super.widgetSelected(e); + boolean selectNew = e.text == null || e.text.equals(strLinkNew); + onSelectNewUpdates(selectNew, !selectNew, false/*selectTop*/); + } + }); + + // placeholder between "select all" and "install" + Label placeholder = new Label(mGroupOptions, SWT.NONE); + GridDataBuilder.create(placeholder).hFill().hGrab(); + + mButtonInstall = new Button(mGroupOptions, SWT.NONE); + mButtonInstall.setText(""); //$NON-NLS-1$ placeholder, filled in updateButtonsState() + mButtonInstall.setToolTipText("Install one or more packages"); + GridDataBuilder.create(mButtonInstall).vCenter().wHint(150); + mButtonInstall.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + onButtonInstall(); //$hide$ + } + }); + + // Options line 2, 7 columns + + new Label(mGroupOptions, SWT.NONE); + + mCheckFilterObsolete = new Button(mGroupOptions, SWT.CHECK); + mCheckFilterObsolete.setText("Obsolete"); + mCheckFilterObsolete.setToolTipText("Also show obsolete packages"); + mCheckFilterObsolete.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + refreshViewerInput(); + } + }); + mCheckFilterObsolete.setSelection(false); + + // placeholder before "deselect" + new Label(mGroupOptions, SWT.NONE); + new Label(mGroupOptions, SWT.NONE); + + Link linkDeselect = new Link(mGroupOptions, SWT.NONE); + linkDeselect.setText("Deselect All"); + linkDeselect.setToolTipText("Deselects all the currently selected items"); + GridDataBuilder.create(linkDeselect).hFill(); + linkDeselect.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + super.widgetSelected(e); + onDeselectAll(); + } + }); + + // placeholder between "deselect" and "delete" + placeholder = new Label(mGroupOptions, SWT.NONE); + GridDataBuilder.create(placeholder).hFill().hGrab(); + + mButtonDelete = new Button(mGroupOptions, SWT.NONE); + mButtonDelete.setText(""); //$NON-NLS-1$ placeholder, filled in updateButtonsState() + mButtonDelete.setToolTipText("Delete one ore more installed packages"); + GridDataBuilder.create(mButtonDelete).vCenter().wHint(150); + mButtonDelete.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + onButtonDelete(); //$hide$ + } + }); + } + + private PackagesPageImpl.ITreeViewerColumn wrapColumn(final TreeViewerColumn column) { + return new PackagesPageImpl.ITreeViewerColumn() { + @Override + public void setLabelProvider(ColumnLabelProvider labelProvider) { + column.setLabelProvider(labelProvider); + } + }; + } + + private Image getImage(String filename) { + if (mImpl.mSwtUpdaterData != null) { + ImageFactory imgFactory = mImpl.mSwtUpdaterData.getImageFactory(); + if (imgFactory != null) { + return imgFactory.getImageByName(filename); + } + } + return null; + } + + + // -- Start of internal part ---------- + // Hide everything down-below from SWT designer + //$hide>>$ + + + // --- menu interactions --- + + public void registerMenuAction(final MenuAction action, MenuItem item) { + item.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + Button button = null; + + switch (action) { + case RELOAD: + mImpl.fullReload(); + break; + case SHOW_ADDON_SITES: + AddonSitesDialog d = new AddonSitesDialog(getShell(), mImpl.mSwtUpdaterData); + if (d.open()) { + mImpl.loadPackages(); + } + break; + case TOGGLE_SHOW_ARCHIVES: + mDisplayArchives = !mDisplayArchives; + // Force the viewer to be refreshed + ((PkgContentProvider) mTreeViewer.getContentProvider()). + setDisplayArchives(mDisplayArchives); + mTreeViewer.setInput(null); + refreshViewerInput(); + syncViewerSelection(); + break; + case TOGGLE_SHOW_INSTALLED_PKG: + button = mCheckFilterInstalled; + break; + case TOGGLE_SHOW_OBSOLETE_PKG: + button = mCheckFilterObsolete; + break; + case TOGGLE_SHOW_UPDATE_NEW_PKG: + button = mCheckFilterNew; + break; + } + + if (button != null && !button.isDisposed()) { + // Toggle this button (radio or checkbox) + + boolean value = button.getSelection(); + + // SWT doesn't automatically switch radio buttons when using the + // Widget#setSelection method, so we'll do it here manually. + if (!value && (button.getStyle() & SWT.RADIO) != 0) { + // we'll be selecting this radio button, so deselect all ther other ones + // in the parent group. + for (Control child : button.getParent().getChildren()) { + if (child instanceof Button && + child != button && + (child.getStyle() & SWT.RADIO) != 0) { + ((Button) child).setSelection(value); + } + } + } + + button.setSelection(!value); + + // SWT doesn't actually invoke the listeners when using Widget#setSelection + // so let's run the actual action. + button.notifyListeners(SWT.Selection, new Event()); + } + + updateMenuCheckmarks(); + } + }); + + mMenuActions.put(action, item); + } + + // --- internal methods --- + + private void updateMenuCheckmarks() { + + for (Entry entry : mMenuActions.entrySet()) { + MenuAction action = entry.getKey(); + MenuItem item = entry.getValue(); + + if (action.getMenuStyle() == SWT.NONE) { + continue; + } + + boolean value = false; + Button button = null; + + switch (action) { + case TOGGLE_SHOW_ARCHIVES: + value = mDisplayArchives; + break; + case TOGGLE_SHOW_INSTALLED_PKG: + button = mCheckFilterInstalled; + break; + case TOGGLE_SHOW_OBSOLETE_PKG: + button = mCheckFilterObsolete; + break; + case TOGGLE_SHOW_UPDATE_NEW_PKG: + button = mCheckFilterNew; + break; + case RELOAD: + case SHOW_ADDON_SITES: + // No checkmark to update + break; + } + + if (button != null && !button.isDisposed()) { + value = button.getSelection(); + } + + if (!item.isDisposed()) { + item.setSelection(value); + } + } + } + + private void postCreate() { + mImpl.postCreate(); + + if (mImpl.mSwtUpdaterData != null) { + mTextSdkOsPath.setText(mImpl.mSwtUpdaterData.getOsSdkRoot()); + } + + ((PkgContentProvider) mTreeViewer.getContentProvider()).setDisplayArchives( + mDisplayArchives); + + ColumnViewerToolTipSupport.enableFor(mTreeViewer, ToolTip.NO_RECREATE); + + Tree tree = mTreeViewer.getTree(); + FontData fontData = tree.getFont().getFontData()[0]; + fontData.setStyle(SWT.ITALIC); + mTreeFontItalic = new Font(tree.getDisplay(), fontData); + + tree.addDisposeListener(new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + mTreeFontItalic.dispose(); + mTreeFontItalic = null; + } + }); + } + + private void loadPackages(boolean useLocalCache, boolean overrideExisting) { + if (mImpl.mSwtUpdaterData == null) { + return; + } + + // LoadPackage is synchronous but does not block the UI. + // Consequently it's entirely possible for the user + // to request the app to close whilst the packages are loading. Any + // action done after loadPackages must check the UI hasn't been + // disposed yet. Otherwise hilarity ensues. + + if (mTreeColumnName.isDisposed()) { + // If the UI got disposed, don't try to load anything since we won't be + // able to display it anyway. + return; + } + + mTreeColumnName.setImage(getImage(PackagesPageIcons.ICON_SORT_BY_API)); + + mImpl.loadPackagesImpl(useLocalCache, overrideExisting); + } + + private void refreshViewerInput() { + // Dynamically update the table while we load after each source. + // Since the official Android source gets loaded first, it makes the + // window look non-empty a lot sooner. + if (!mGroupPackages.isDisposed()) { + try { + mImpl.setViewerInput(); + } catch (Exception ignore) {} + + // set the initial expanded state + expandInitial(mTreeViewer.getInput()); + + updateButtonsState(); + updateMenuCheckmarks(); + } + } + + /** + * Decide whether to keep an item in the current tree based on user-chosen filter options. + */ + private boolean filterViewerItem(Object treeElement) { + if (treeElement instanceof PkgCategory) { + PkgCategory cat = (PkgCategory) treeElement; + + if (!cat.getItems().isEmpty()) { + // A category is hidden if all of its content is hidden. + // However empty categories are always visible. + for (PkgItem item : cat.getItems()) { + if (filterViewerItem(item)) { + // We found at least one element that is visible. + return true; + } + } + return false; + } + } + + if (treeElement instanceof PkgItem) { + PkgItem item = (PkgItem) treeElement; + + if (!mCheckFilterObsolete.getSelection()) { + if (item.isObsolete()) { + return false; + } + } + + if (!mCheckFilterInstalled.getSelection()) { + if (item.getState() == PkgState.INSTALLED) { + return false; + } + } + + if (!mCheckFilterNew.getSelection()) { + if (item.getState() == PkgState.NEW || item.hasUpdatePkg()) { + return false; + } + } + } + + return true; + } + + /** + * Performs the initial expansion of the tree. This expands categories that contain + * at least one installed item and collapses the ones with nothing installed. + * + * TODO: change this to only change the expanded state on categories that have not + * been touched by the user yet. Once we do that, call this every time a new source + * is added or the list is reloaded. + */ + private void expandInitial(Object elem) { + if (elem == null) { + return; + } + if (mTreeViewer != null && !mTreeViewer.getTree().isDisposed()) { + + boolean enablePreviews = + mImpl.mSwtUpdaterData.getSettingsController().getSettings().getEnablePreviews(); + + mTreeViewer.setExpandedState(elem, true); + nextCategory: for (Object pkg : + ((ITreeContentProvider) mTreeViewer.getContentProvider()). + getChildren(elem)) { + if (pkg instanceof PkgCategory) { + PkgCategory cat = (PkgCategory) pkg; + + // Always expand the Tools category (and the preview one, if enabled) + if (cat.getKey().equals(PkgCategoryApi.KEY_TOOLS) || + (enablePreviews && + cat.getKey().equals(PkgCategoryApi.KEY_TOOLS_PREVIEW))) { + expandInitial(pkg); + continue nextCategory; + } + + + for (PkgItem item : cat.getItems()) { + if (item.getState() == PkgState.INSTALLED) { + expandInitial(pkg); + continue nextCategory; + } + } + } + } + } + } + + /** + * Handle checking and unchecking of the tree items. + * + * When unchecking, all sub-tree items checkboxes are cleared too. + * When checking a source, all of its packages are checked too. + * When checking a package, only its compatible archives are checked. + */ + private void onTreeCheckStateChanged(CheckStateChangedEvent event) { + boolean checked = event.getChecked(); + Object elem = event.getElement(); + + assert event.getSource() == mTreeViewer; + + // When selecting, we want to only select compatible archives and expand the super nodes. + checkAndExpandItem(elem, checked, true/*fixChildren*/, true/*fixParent*/); + updateButtonsState(); + } + + private void onTreeDoubleClick(DoubleClickEvent event) { + assert event.getSource() == mTreeViewer; + ISelection sel = event.getSelection(); + if (sel.isEmpty() || !(sel instanceof ITreeSelection)) { + return; + } + ITreeSelection tsel = (ITreeSelection) sel; + Object elem = tsel.getFirstElement(); + if (elem == null) { + return; + } + + ITreeContentProvider provider = + (ITreeContentProvider) mTreeViewer.getContentProvider(); + Object[] children = provider.getElements(elem); + if (children == null) { + return; + } + + if (children.length > 0) { + // If the element has children, expand/collapse it. + if (mTreeViewer.getExpandedState(elem)) { + mTreeViewer.collapseToLevel(elem, 1); + } else { + mTreeViewer.expandToLevel(elem, 1); + } + } else { + // If the element is a terminal one, select/deselect it. + checkAndExpandItem( + elem, + !mTreeViewer.getChecked(elem), + false /*fixChildren*/, + true /*fixParent*/); + updateButtonsState(); + } + } + + private void checkAndExpandItem( + Object elem, + boolean checked, + boolean fixChildren, + boolean fixParent) { + ITreeContentProvider provider = + (ITreeContentProvider) mTreeViewer.getContentProvider(); + + // fix the item itself + if (checked != mTreeViewer.getChecked(elem)) { + mTreeViewer.setChecked(elem, checked); + } + if (elem instanceof PkgItem) { + // update the PkgItem to reflect the selection + ((PkgItem) elem).setChecked(checked); + } + + if (!checked) { + if (fixChildren) { + // when de-selecting, we deselect all children too + mTreeViewer.setSubtreeChecked(elem, checked); + for (Object child : provider.getChildren(elem)) { + checkAndExpandItem(child, checked, fixChildren, false/*fixParent*/); + } + } + + // fix the parent when deselecting + if (fixParent) { + Object parent = provider.getParent(elem); + if (parent != null && mTreeViewer.getChecked(parent)) { + mTreeViewer.setChecked(parent, false); + } + } + return; + } + + // When selecting, we also select sub-items (for a category) + if (fixChildren) { + if (elem instanceof PkgCategory || elem instanceof PkgItem) { + Object[] children = provider.getChildren(elem); + for (Object child : children) { + checkAndExpandItem(child, true, fixChildren, false/*fixParent*/); + } + // only fix the parent once the last sub-item is set + if (elem instanceof PkgCategory) { + if (children.length > 0) { + checkAndExpandItem( + children[0], true, false/*fixChildren*/, true/*fixParent*/); + } else { + mTreeViewer.setChecked(elem, false); + } + } + } else if (elem instanceof Package) { + // in details mode, we auto-select compatible packages + selectCompatibleArchives(elem, provider); + } + } + + if (fixParent && checked && elem instanceof PkgItem) { + Object parent = provider.getParent(elem); + if (!mTreeViewer.getChecked(parent)) { + Object[] children = provider.getChildren(parent); + boolean allChecked = children.length > 0; + for (Object e : children) { + if (!mTreeViewer.getChecked(e)) { + allChecked = false; + break; + } + } + if (allChecked) { + mTreeViewer.setChecked(parent, true); + } + } + } + } + + private void selectCompatibleArchives(Object pkg, ITreeContentProvider provider) { + for (Object archive : provider.getChildren(pkg)) { + if (archive instanceof Archive) { + mTreeViewer.setChecked(archive, ((Archive) archive).isCompatible()); + } + } + } + + /** + * Checks all PkgItems that are either new or have updates or select top platform + * for initial run. + */ + private void onSelectNewUpdates(boolean selectNew, boolean selectUpdates, boolean selectTop) { + // This will update the tree's "selected" state and then invoke syncViewerSelection() + // which will in turn update tree. + mImpl.onSelectNewUpdates(selectNew, selectUpdates, selectTop); + } + + /** + * Deselect all checked PkgItems. + */ + private void onDeselectAll() { + // This does not update the tree itself, syncViewerSelection does it below. + mImpl.onDeselectAll(); + syncViewerSelection(); + } + + /** + * Synchronize the 'checked' state of PkgItems in the tree with their internal isChecked state. + */ + private void syncViewerSelection() { + ITreeContentProvider provider = (ITreeContentProvider) mTreeViewer.getContentProvider(); + + Object input = mTreeViewer.getInput(); + if (input != null) { + for (Object cat : provider.getElements(input)) { + Object[] children = provider.getElements(cat); + boolean allChecked = children.length > 0; + for (Object child : children) { + if (child instanceof PkgItem) { + PkgItem item = (PkgItem) child; + boolean checked = item.isChecked(); + allChecked &= checked; + + if (checked != mTreeViewer.getChecked(item)) { + if (checked) { + if (!mTreeViewer.getExpandedState(cat)) { + mTreeViewer.setExpandedState(cat, true); + } + } + checkAndExpandItem( + item, + checked, + true/*fixChildren*/, + false/*fixParent*/); + } + } + } + + if (allChecked != mTreeViewer.getChecked(cat)) { + mTreeViewer.setChecked(cat, allChecked); + } + } + } + + updateButtonsState(); + } + + /** + * Indicate an install/delete operation is pending. + * This disables the install/delete buttons. + * Use {@link #endOperationPending()} to revert, typically in a {@code try..finally} block. + */ + private void beginOperationPending() { + mOperationPending = true; + updateButtonsState(); + } + + private void endOperationPending() { + mOperationPending = false; + updateButtonsState(); + } + + /** + * Updates the Install and Delete Package buttons. + */ + private void updateButtonsState() { + if (!mButtonInstall.isDisposed()) { + int numPackages = getArchivesForInstall(null /*archives*/); + + mButtonInstall.setEnabled((numPackages > 0) && !mOperationPending); + mButtonInstall.setText( + numPackages == 0 ? "Install packages..." : // disabled button case + numPackages == 1 ? "Install 1 package..." : + String.format("Install %d packages...", numPackages)); + } + + if (!mButtonDelete.isDisposed()) { + // We can only delete local archives + int numPackages = getArchivesToDelete(null /*outMsg*/, null /*outArchives*/); + + mButtonDelete.setEnabled((numPackages > 0) && !mOperationPending); + mButtonDelete.setText( + numPackages == 0 ? "Delete packages..." : // disabled button case + numPackages == 1 ? "Delete 1 package..." : + String.format("Delete %d packages...", numPackages)); + } + } + + /** + * Called when the Install Package button is selected. + * Collects the packages to be installed and shows the installation window. + */ + private void onButtonInstall() { + ArrayList archives = new ArrayList(); + getArchivesForInstall(archives); + + if (mImpl.mSwtUpdaterData != null) { + boolean needsRefresh = false; + try { + beginOperationPending(); + + List installed = mImpl.mSwtUpdaterData.updateOrInstallAll_WithGUI( + archives, + mCheckFilterObsolete.getSelection() /* includeObsoletes */, + mContext == SdkInvocationContext.IDE ? + SwtUpdaterData.TOOLS_MSG_UPDATED_FROM_ADT : + SwtUpdaterData.TOOLS_MSG_UPDATED_FROM_SDKMAN); + needsRefresh = installed != null && !installed.isEmpty(); + } finally { + endOperationPending(); + + if (needsRefresh) { + // The local package list has changed, make sure to refresh it + mImpl.localReload(); + } + } + } + } + + /** + * Selects the archives that can be installed. + * This can be used with a null {@code outArchives} just to count the number of + * installable archives. + * + * @param outArchives An archive list where to add the archives that can be installed. + * This can be null. + * @return The number of archives that can be installed. + */ + private int getArchivesForInstall(List outArchives) { + if (mTreeViewer == null || + mTreeViewer.getTree() == null || + mTreeViewer.getTree().isDisposed()) { + return 0; + } + Object[] checked = mTreeViewer.getCheckedElements(); + if (checked == null) { + return 0; + } + + int count = 0; + + // Give us a way to force install of incompatible archives. + boolean checkIsCompatible = + System.getenv(ArchiveInstaller.ENV_VAR_IGNORE_COMPAT) == null; + + if (mDisplayArchives) { + // In detail mode, we display archives so we can install only the + // archives that are actually selected. + + for (Object c : checked) { + if (c instanceof Archive) { + Archive a = (Archive) c; + if (a != null) { + if (checkIsCompatible && !a.isCompatible()) { + continue; + } + count++; + if (outArchives != null) { + outArchives.add((Archive) c); + } + } + } + } + } else { + // In non-detail mode, we install all the compatible archives + // found in the selected pkg items. We also automatically + // select update packages rather than the root package if any. + + for (Object c : checked) { + Package p = null; + if (c instanceof Package) { + // This is an update package + p = (Package) c; + } else if (c instanceof PkgItem) { + p = ((PkgItem) c).getMainPackage(); + + PkgItem pi = (PkgItem) c; + if (pi.getState() == PkgState.INSTALLED) { + // We don't allow installing items that are already installed + // unless they have a pending update. + p = pi.getUpdatePkg(); + + } else if (pi.getState() == PkgState.NEW) { + p = pi.getMainPackage(); + } + } + if (p != null) { + for (Archive a : p.getArchives()) { + if (a != null) { + if (checkIsCompatible && !a.isCompatible()) { + continue; + } + count++; + if (outArchives != null) { + outArchives.add(a); + } + } + } + } + } + } + + return count; + } + + /** + * Called when the Delete Package button is selected. + * Collects the packages to be deleted, prompt the user for confirmation + * and actually performs the deletion. + */ + private void onButtonDelete() { + final String title = "Delete SDK Package"; + StringBuilder msg = new StringBuilder("Are you sure you want to delete:"); + + // A list of archives to delete + final ArrayList archives = new ArrayList(); + + getArchivesToDelete(msg, archives); + + if (!archives.isEmpty()) { + msg.append("\n").append("This cannot be undone."); //$NON-NLS-1$ + if (MessageDialog.openQuestion(getShell(), title, msg.toString())) { + try { + beginOperationPending(); + + mImpl.mSwtUpdaterData.getTaskFactory().start("Delete Package", new ITask() { + @Override + public void run(ITaskMonitor monitor) { + monitor.setProgressMax(archives.size() + 1); + for (Archive a : archives) { + monitor.setDescription("Deleting '%1$s' (%2$s)", + a.getParentPackage().getShortDescription(), + a.getLocalOsPath()); + + // Delete the actual package + a.deleteLocal(); + + monitor.incProgress(1); + if (monitor.isCancelRequested()) { + break; + } + } + + monitor.incProgress(1); + monitor.setDescription("Done"); + } + }); + } finally { + endOperationPending(); + + // The local package list has changed, make sure to refresh it + mImpl.localReload(); + } + } + } + } + + /** + * Selects the archives that can be deleted and collect their names. + * This can be used with a null {@code outArchives} and a null {@code outMsg} + * just to count the number of archives to be deleted. + * + * @param outMsg A StringBuilder where the names of the packages to be deleted is + * accumulated. This is used to confirm deletion with the user. + * @param outArchives An archive list where to add the archives that can be installed. + * This can be null. + * @return The number of archives that can be deleted. + */ + private int getArchivesToDelete(StringBuilder outMsg, List outArchives) { + if (mTreeViewer == null || + mTreeViewer.getTree() == null || + mTreeViewer.getTree().isDisposed()) { + return 0; + } + Object[] checked = mTreeViewer.getCheckedElements(); + if (checked == null) { + // This should not happen since the button should be disabled + return 0; + } + + int count = 0; + + if (mDisplayArchives) { + // In detail mode, select archives that can be deleted + + for (Object c : checked) { + if (c instanceof Archive) { + Archive a = (Archive) c; + if (a != null && a.isLocal()) { + count++; + if (outMsg != null) { + String osPath = a.getLocalOsPath(); + File dir = new File(osPath); + Package p = a.getParentPackage(); + if (p != null && dir.isDirectory()) { + outMsg.append("\n - ") //$NON-NLS-1$ + .append(p.getShortDescription()); + } + } + if (outArchives != null) { + outArchives.add(a); + } + } + } + } + } else { + // In non-detail mode, select archives of selected packages that can be deleted. + + for (Object c : checked) { + if (c instanceof PkgItem) { + PkgItem pi = (PkgItem) c; + PkgState state = pi.getState(); + if (state == PkgState.INSTALLED) { + Package p = pi.getMainPackage(); + + for (Archive a : p.getArchives()) { + if (a != null && a.isLocal()) { + count++; + if (outMsg != null) { + String osPath = a.getLocalOsPath(); + File dir = new File(osPath); + if (dir.isDirectory()) { + outMsg.append("\n - ") //$NON-NLS-1$ + .append(p.getShortDescription()); + } + } + if (outArchives != null) { + outArchives.add(a); + } + } + } + } + } + } + } + + return count; + } + + // ---------------------- + + + // --- Implementation of ISdkChangeListener --- + + @Override + public void onSdkLoaded() { + onSdkReload(); + } + + @Override + public void onSdkReload() { + // The sdkmanager finished reloading its data. We must not call localReload() from here + // since we don't want to alter the sdkmanager's data that just finished loading. + mImpl.loadPackages(); + } + + @Override + public void preInstallHook() { + // nothing to be done for now. + } + + @Override + public void postInstallHook() { + // nothing to be done for now. + } + + + // --- End of hiding from SWT Designer --- + //$hide<<$ +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPageIcons.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPageIcons.java new file mode 100644 index 00000000..608bded6 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPageIcons.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2012 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.sdkuilib.internal.repository.ui; + + +/** + * Icons used by {@link PackagesPage}. + */ +public class PackagesPageIcons { + + public static final String ICON_CAT_OTHER = "pkgcat_other_16.png"; //$NON-NLS-1$ + public static final String ICON_CAT_PLATFORM = "pkgcat_16.png"; //$NON-NLS-1$ + public static final String ICON_SORT_BY_API = "platform_pkg_16.png"; //$NON-NLS-1$ + public static final String ICON_PKG_NEW = "pkg_new_16.png"; //$NON-NLS-1$ + public static final String ICON_PKG_INCOMPAT = "pkg_incompat_16.png"; //$NON-NLS-1$ + public static final String ICON_PKG_UPDATE = "pkg_update_16.png"; //$NON-NLS-1$ + public static final String ICON_PKG_INSTALLED = "pkg_installed_16.png"; //$NON-NLS-1$ +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPageImpl.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPageImpl.java new file mode 100644 index 00000000..71f990e6 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPageImpl.java @@ -0,0 +1,564 @@ +/* + * Copyright (C) 2012 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.sdkuilib.internal.repository.ui; + +import com.android.SdkConstants; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.internal.repository.DownloadCache; +import com.android.sdklib.internal.repository.DownloadCache.Strategy; +import com.android.sdklib.internal.repository.archives.Archive; +import com.android.sdklib.internal.repository.packages.Package; +import com.android.sdklib.internal.repository.sources.SdkSource; +import com.android.sdklib.internal.repository.updater.PackageLoader; +import com.android.sdklib.internal.repository.updater.PackageLoader.ISourceLoadedCallback; +import com.android.sdklib.internal.repository.updater.PkgItem; +import com.android.sdklib.internal.repository.updater.PkgItem.PkgState; +import com.android.sdklib.repository.IDescription; +import com.android.sdkuilib.internal.repository.SwtUpdaterData; +import com.android.sdkuilib.internal.repository.core.PackagesDiffLogic; +import com.android.sdkuilib.internal.repository.core.PkgCategory; +import com.android.sdkuilib.internal.repository.core.PkgCategoryApi; +import com.android.sdkuilib.internal.repository.core.PkgContentProvider; +import com.android.sdkuilib.internal.repository.icons.ImageFactory; + +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.jface.viewers.IInputProvider; +import org.eclipse.jface.viewers.ITableFontProvider; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.List; + +/** + * Base class for {@link PackagesPage} that holds most of the logic to display + * the tree/list of packages. This class holds most of the logic and {@link PackagesPage} + * holds most of the UI (creating the UI, dealing with menus and buttons and tree + * selection.) This makes it easier to test the functionality by mocking only a + * subset of the UI. + */ +abstract class PackagesPageImpl { + + final SwtUpdaterData mSwtUpdaterData; + final PackagesDiffLogic mDiffLogic; + + private ICheckboxTreeViewer mITreeViewer; + private ITreeViewerColumn mIColumnName; + private ITreeViewerColumn mIColumnApi; + private ITreeViewerColumn mIColumnRevision; + private ITreeViewerColumn mIColumnStatus; + + PackagesPageImpl(SwtUpdaterData swtUpdaterData) { + mSwtUpdaterData = swtUpdaterData; + mDiffLogic = new PackagesDiffLogic(swtUpdaterData); + } + + /** + * Utility method that derived classes can override to check whether the UI is disposed. + * When the UI is disposed, most operations that affect the UI will be bypassed. + * @return True if UI is not available and should not be touched. + */ + abstract protected boolean isUiDisposed(); + + /** + * Utility method to execute a runnable on the main UI thread. + * Will do nothing if {@link #isUiDisposed()} returns false. + * @param runnable The runnable to execute on the main UI thread. + */ + abstract protected void syncExec(Runnable runnable); + + /** + * Synchronizes the 'checked' state of PkgItems in the tree with their internal isChecked state. + */ + abstract protected void syncViewerSelection(); + + void performFirstLoad() { + // First a package loader is created that only checks + // the local cache xml files. It populates the package + // list based on what the client got last, essentially. + loadPackages(true /*useLocalCache*/, false /*overrideExisting*/); + + // Next a regular package loader is created that will + // respect the expiration and refresh parameters of the + // download cache. + loadPackages(false /*useLocalCache*/, true /*overrideExisting*/); + } + + public void setITreeViewer(ICheckboxTreeViewer iTreeViewer) { + mITreeViewer = iTreeViewer; + } + + public void setIColumns( + ITreeViewerColumn columnName, + ITreeViewerColumn columnApi, + ITreeViewerColumn columnRevision, + ITreeViewerColumn columnStatus) { + mIColumnName = columnName; + mIColumnApi = columnApi; + mIColumnRevision = columnRevision; + mIColumnStatus = columnStatus; + } + + void postCreate() { + // Caller needs to call setITreeViewer before this. + assert mITreeViewer != null; + // Caller needs to call setIColumns before this. + assert mIColumnApi != null; + assert mIColumnName != null; + assert mIColumnStatus != null; + assert mIColumnRevision != null; + + mITreeViewer.setContentProvider(new PkgContentProvider(mITreeViewer)); + + mIColumnApi.setLabelProvider( + new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(mIColumnApi))); + mIColumnName.setLabelProvider( + new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(mIColumnName))); + mIColumnStatus.setLabelProvider( + new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(mIColumnStatus))); + mIColumnRevision.setLabelProvider( + new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(mIColumnRevision))); + } + + /** + * Performs a full reload by removing all cached packages data, including the platforms + * and addons from the sdkmanager instance. This will perform a full local parsing + * as well as a full reload of the remote data (by fetching all sources again.) + */ + void fullReload() { + // Clear all source information, forcing them to be refreshed. + mSwtUpdaterData.getSources().clearAllPackages(); + // Clear and reload all local data too. + localReload(); + } + + /** + * Performs a full reload of all the local package information, including the platforms + * and addons from the sdkmanager instance. This will perform a full local parsing. + *

+ * This method does NOT force a new fetch of the remote sources. + * + * @see #fullReload() + */ + void localReload() { + // Clear all source caches, otherwise loading will use the cached data + mSwtUpdaterData.getLocalSdkParser().clearPackages(); + mSwtUpdaterData.getSdkManager().reloadSdk(mSwtUpdaterData.getSdkLog()); + loadPackages(); + } + + /** + * Performs a "normal" reload of the package information, use the default download + * cache and refreshing strategy as needed. + */ + void loadPackages() { + loadPackages(false /*useLocalCache*/, false /*overrideExisting*/); + } + + /** + * Performs a reload of the package information. + * + * @param useLocalCache When true, the {@link PackageLoader} is switched to use + * a specific {@link DownloadCache} using the {@link Strategy#ONLY_CACHE}, meaning + * it will only use data from the local cache. It will not try to fetch or refresh + * manifests. This is used once the very first time the sdk manager window opens + * and is typically followed by a regular load with refresh. + */ + abstract protected void loadPackages(boolean useLocalCache, boolean overrideExisting); + + /** + * Actual implementation of {@link #loadPackages(boolean, boolean)}. + * Derived implementations must call this to do the actual work after setting up the UI. + */ + void loadPackagesImpl(final boolean useLocalCache, final boolean overrideExisting) { + if (mSwtUpdaterData == null) { + return; + } + + PackageLoader packageLoader = getPackageLoader(useLocalCache); + assert packageLoader != null; + + mDiffLogic.updateStart(); + packageLoader.loadPackages(overrideExisting, new ISourceLoadedCallback() { + @Override + public boolean onUpdateSource(SdkSource source, Package[] newPackages) { + // This runs in a thread and must not access UI directly. + final boolean changed = mDiffLogic.updateSourcePackages(source, newPackages); + + syncExec(new Runnable() { + @Override + public void run() { + if (changed || + mITreeViewer.getInput() != mDiffLogic.getCategories()) { + refreshViewerInput(); + } + } + }); + + // Return true to tell the loader to continue with the next source. + // Return false to stop the loader if any UI has been disposed, which can + // happen if the user is trying to close the window during the load operation. + return !isUiDisposed(); + } + + @Override + public void onLoadCompleted() { + // This runs in a thread and must not access UI directly. + final boolean changed = mDiffLogic.updateEnd(); + + syncExec(new Runnable() { + @Override + public void run() { + if (changed || + mITreeViewer.getInput() != mDiffLogic.getCategories()) { + try { + refreshViewerInput(); + } catch (Exception ignore) {} + } + + if (!useLocalCache && + mDiffLogic.isFirstLoadComplete() && + !isUiDisposed()) { + // At the end of the first load, if nothing is selected then + // automatically select all new and update packages. + Object[] checked = mITreeViewer.getCheckedElements(); + if (checked == null || checked.length == 0) { + onSelectNewUpdates( + false, //selectNew + true, //selectUpdates, + true); //selectTop + } + } + } + }); + } + }); + } + + /** + * Used by {@link #loadPackagesImpl(boolean, boolean)} to get the package + * loader for the first or second pass update. When starting the manager + * starts with a first pass that reads only from the local cache, with no + * extra network access. That's {@code useLocalCache} being true. + *

+ * Leter it does a second pass with {@code useLocalCache} set to false + * and actually uses the download cache specified in {@link SwtUpdaterData}. + * + * This is extracted so that we can control this cache via unit tests. + */ + protected PackageLoader getPackageLoader(boolean useLocalCache) { + if (useLocalCache) { + return new PackageLoader(mSwtUpdaterData, new DownloadCache(Strategy.ONLY_CACHE)); + } else { + return mSwtUpdaterData.getPackageLoader(); + } + } + + /** + * Overridden by the UI to respond to a request to refresh the tree viewer + * when the input has changed. + * The implementation must call {@link #setViewerInput()} somehow and will + * also need to adjust the expand state of the tree items and/or update + * some buttons or other state. + */ + abstract protected void refreshViewerInput(); + + /** + * Invoked from {@link #refreshViewerInput()} to actually either set the + * input of the tree viewer or refresh it if it's the same input + * object. + */ + protected void setViewerInput() { + List cats = mDiffLogic.getCategories(); + if (mITreeViewer.getInput() != cats) { + // set initial input + mITreeViewer.setInput(cats); + } else { + // refresh existing, which preserves the expanded state, the selection + // and the checked state. + mITreeViewer.refresh(); + } + } + + /** + * Checks all PkgItems that are either new or have updates or select top platform + * for initial run. + */ + void onSelectNewUpdates(boolean selectNew, boolean selectUpdates, boolean selectTop) { + // This does not update the tree itself, syncViewerSelection does it in the caller. + mDiffLogic.checkNewUpdateItems( + selectNew, + selectUpdates, + selectTop, + SdkConstants.CURRENT_PLATFORM); + syncViewerSelection(); + } + + /** + * Deselect all checked PkgItems. + */ + void onDeselectAll() { + // This does not update the tree itself, syncViewerSelection does it in the caller. + mDiffLogic.uncheckAllItems(); + } + + // ---------------------- + + abstract protected Font getTreeFontItalic(); + + class PkgCellLabelProvider extends ColumnLabelProvider implements ITableFontProvider { + + private final ITreeViewerColumn mColumn; + + public PkgCellLabelProvider(ITreeViewerColumn column) { + super(); + mColumn = column; + } + + @Override + public String getText(Object element) { + + if (mColumn == mIColumnName) { + if (element instanceof PkgCategory) { + return ((PkgCategory) element).getLabel(); + } else if (element instanceof PkgItem) { + return getPkgItemName((PkgItem) element); + } else if (element instanceof IDescription) { + return ((IDescription) element).getShortDescription(); + } + + } else if (mColumn == mIColumnApi) { + AndroidVersion version = null; + if (element instanceof PkgItem) { + version = ((PkgItem) element).getAndroidVersion(); + } + if (version != null) { + return version.getApiString(); + } + + } else if (mColumn == mIColumnRevision) { + if (element instanceof PkgItem) { + PkgItem pkg = (PkgItem) element; + return pkg.getRevision().toShortString(); + } + + } else if (mColumn == mIColumnStatus) { + if (element instanceof PkgItem) { + PkgItem pkg = (PkgItem) element; + + switch(pkg.getState()) { + case INSTALLED: + Package update = pkg.getUpdatePkg(); + if (update != null) { + return String.format( + "Update available: rev. %1$s", + update.getRevision().toShortString()); + } + return "Installed"; + + case NEW: + Package p = pkg.getMainPackage(); + if (p != null && p.hasCompatibleArchive()) { + return "Not installed"; + } else { + return String.format("Not compatible with %1$s", + SdkConstants.currentPlatformName()); + } + } + return pkg.getState().toString(); + + } else if (element instanceof Package) { + // This is an update package. + return "New revision " + ((Package) element).getRevision().toShortString(); + } + } + + return ""; //$NON-NLS-1$ + } + + private String getPkgItemName(PkgItem item) { + String name = item.getName().trim(); + + // When sorting by API, the package name might contains the API number + // or the platform name at the end. If we find it, cut it out since it's + // redundant. + + PkgCategoryApi cat = (PkgCategoryApi) findCategoryForItem(item); + String apiLabel = cat.getApiLabel(); + String platLabel = cat.getPlatformName(); + + if (platLabel != null && name.endsWith(platLabel)) { + return name.substring(0, name.length() - platLabel.length()); + + } else if (apiLabel != null && name.endsWith(apiLabel)) { + return name.substring(0, name.length() - apiLabel.length()); + + } else if (platLabel != null && item.isObsolete() && name.indexOf(platLabel) > 0) { + // For obsolete items, the format is " (Obsolete)" + // so in this case only accept removing a platform name that is not at + // the end. + name = name.replace(platLabel, ""); //$NON-NLS-1$ + } + + // Collapse potential duplicated spacing + name = name.replaceAll(" +", " "); //$NON-NLS-1$ //$NON-NLS-2$ + + return name; + } + + private PkgCategory findCategoryForItem(PkgItem item) { + List cats = mDiffLogic.getCategories(); + for (PkgCategory cat : cats) { + for (PkgItem i : cat.getItems()) { + if (i == item) { + return cat; + } + } + } + + return null; + } + + @Override + public Image getImage(Object element) { + ImageFactory imgFactory = mSwtUpdaterData.getImageFactory(); + + if (imgFactory != null) { + if (mColumn == mIColumnName) { + if (element instanceof PkgCategory) { + return imgFactory.getImageForObject(((PkgCategory) element).getIconRef()); + } else if (element instanceof PkgItem) { + return imgFactory.getImageForObject(((PkgItem) element).getMainPackage()); + } + return imgFactory.getImageForObject(element); + + } else if (mColumn == mIColumnStatus && element instanceof PkgItem) { + PkgItem pi = (PkgItem) element; + switch(pi.getState()) { + case INSTALLED: + if (pi.hasUpdatePkg()) { + return imgFactory.getImageByName(PackagesPageIcons.ICON_PKG_UPDATE); + } else { + return imgFactory.getImageByName(PackagesPageIcons.ICON_PKG_INSTALLED); + } + case NEW: + Package p = pi.getMainPackage(); + if (p != null && p.hasCompatibleArchive()) { + return imgFactory.getImageByName(PackagesPageIcons.ICON_PKG_NEW); + } else { + return imgFactory.getImageByName(PackagesPageIcons.ICON_PKG_INCOMPAT); + } + } + } + } + return super.getImage(element); + } + + // -- ITableFontProvider + + @Override + public Font getFont(Object element, int columnIndex) { + if (element instanceof PkgItem) { + if (((PkgItem) element).getState() == PkgState.NEW) { + return getTreeFontItalic(); + } + } else if (element instanceof Package) { + // update package + return getTreeFontItalic(); + } + return super.getFont(element); + } + + // -- Tooltip support + + @Override + public String getToolTipText(Object element) { + PkgItem pi = element instanceof PkgItem ? (PkgItem) element : null; + if (pi != null) { + element = pi.getMainPackage(); + } + if (element instanceof IDescription) { + String s = getTooltipDescription((IDescription) element); + + if (pi != null && pi.hasUpdatePkg()) { + s += "\n-----------------" + //$NON-NLS-1$ + "\nUpdate Available:\n" + //$NON-NLS-1$ + getTooltipDescription(pi.getUpdatePkg()); + } + + return s; + } + return super.getToolTipText(element); + } + + private String getTooltipDescription(IDescription element) { + String s = element.getLongDescription(); + if (element instanceof Package) { + Package p = (Package) element; + + if (!p.isLocal()) { + // For non-installed item, try to find a download size + for (Archive a : p.getArchives()) { + if (!a.isLocal() && a.isCompatible()) { + s += '\n' + a.getSizeDescription(); + break; + } + } + } + + // Display info about where this package comes/came from + SdkSource src = p.getParentSource(); + if (src != null) { + try { + URL url = new URL(src.getUrl()); + String host = url.getHost(); + if (p.isLocal()) { + s += String.format("\nInstalled from %1$s", host); + } else { + s += String.format("\nProvided by %1$s", host); + } + } catch (MalformedURLException ignore) { + } + } + } + return s; + } + + @Override + public Point getToolTipShift(Object object) { + return new Point(15, 5); + } + + @Override + public int getToolTipDisplayDelayTime(Object object) { + return 500; + } + } + + interface ICheckboxTreeViewer extends IInputProvider { + void setContentProvider(PkgContentProvider pkgContentProvider); + void refresh(); + void setInput(List cats); + Object[] getCheckedElements(); + } + + interface ITreeViewerColumn { + void setLabelProvider(ColumnLabelProvider labelProvider); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PkgTreeColumnViewerLabelProvider.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PkgTreeColumnViewerLabelProvider.java new file mode 100644 index 00000000..575b8a3e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PkgTreeColumnViewerLabelProvider.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2012 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.sdkuilib.internal.repository.ui; + +import org.eclipse.jface.viewers.CellLabelProvider; +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.jface.viewers.TreeColumnViewerLabelProvider; +import org.eclipse.jface.viewers.TreePath; +import org.eclipse.jface.viewers.TreeViewerColumn; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; + +/** + * A custom version of {@link TreeColumnViewerLabelProvider} which + * handles {@link TreePath}s and delegates content to the given + * {@link ColumnLabelProvider} for a given {@link TreeViewerColumn}. + *

+ * The implementation handles a variety of providers (table label, table + * color, table font) but does not implement a tooltip provider, so we + * delegate the calls here to the appropriate {@link ColumnLabelProvider}. + *

+ * Only {@link #getToolTipText(Object)} is really useful for us but we + * delegate all the tooltip calls for completeness and avoid surprises later + * if we ever decide to override more things in the label provider. + */ +class PkgTreeColumnViewerLabelProvider extends TreeColumnViewerLabelProvider { + + private CellLabelProvider mTooltipProvider; + + public PkgTreeColumnViewerLabelProvider(ColumnLabelProvider columnLabelProvider) { + super(columnLabelProvider); + } + + @Override + public void setProviders(Object provider) { + super.setProviders(provider); + if (provider instanceof CellLabelProvider) { + mTooltipProvider = (CellLabelProvider) provider; + } + } + + @Override + public Image getToolTipImage(Object object) { + if (mTooltipProvider != null) { + return mTooltipProvider.getToolTipImage(object); + } + return super.getToolTipImage(object); + } + + @Override + public String getToolTipText(Object element) { + if (mTooltipProvider != null) { + return mTooltipProvider.getToolTipText(element); + } + return super.getToolTipText(element); + } + + @Override + public Color getToolTipBackgroundColor(Object object) { + if (mTooltipProvider != null) { + return mTooltipProvider.getToolTipBackgroundColor(object); + } + return super.getToolTipBackgroundColor(object); + } + + @Override + public Color getToolTipForegroundColor(Object object) { + if (mTooltipProvider != null) { + return mTooltipProvider.getToolTipForegroundColor(object); + } + return super.getToolTipForegroundColor(object); + } + + @Override + public Font getToolTipFont(Object object) { + if (mTooltipProvider != null) { + return mTooltipProvider.getToolTipFont(object); + } + return super.getToolTipFont(object); + } + + @Override + public Point getToolTipShift(Object object) { + if (mTooltipProvider != null) { + return mTooltipProvider.getToolTipShift(object); + } + return super.getToolTipShift(object); + } + + @Override + public boolean useNativeToolTip(Object object) { + if (mTooltipProvider != null) { + return mTooltipProvider.useNativeToolTip(object); + } + return super.useNativeToolTip(object); + } + + @Override + public int getToolTipTimeDisplayed(Object object) { + if (mTooltipProvider != null) { + return mTooltipProvider.getToolTipTimeDisplayed(object); + } + return super.getToolTipTimeDisplayed(object); + } + + @Override + public int getToolTipDisplayDelayTime(Object object) { + if (mTooltipProvider != null) { + return mTooltipProvider.getToolTipDisplayDelayTime(object); + } + return super.getToolTipDisplayDelayTime(object); + } + + @Override + public int getToolTipStyle(Object object) { + if (mTooltipProvider != null) { + return mTooltipProvider.getToolTipStyle(object); + } + return super.getToolTipStyle(object); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/SdkUpdaterWindowImpl2.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/SdkUpdaterWindowImpl2.java new file mode 100644 index 00000000..99c5eb22 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/SdkUpdaterWindowImpl2.java @@ -0,0 +1,574 @@ +/* + * Copyright (C) 2011 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.sdkuilib.internal.repository.ui; + + +import com.android.SdkConstants; +import com.android.sdklib.internal.repository.ITaskFactory; +import com.android.sdklib.internal.repository.sources.SdkSourceProperties; +import com.android.sdklib.internal.repository.updater.SettingsController; +import com.android.sdklib.internal.repository.updater.SettingsController.Settings; +import com.android.sdklib.repository.ISdkChangeListener; +import com.android.sdkuilib.internal.repository.AboutDialog; +import com.android.sdkuilib.internal.repository.ISdkUpdaterWindow; +import com.android.sdkuilib.internal.repository.MenuBarWrapper; +import com.android.sdkuilib.internal.repository.SettingsDialog; +import com.android.sdkuilib.internal.repository.SwtUpdaterData; +import com.android.sdkuilib.internal.repository.icons.ImageFactory; +import com.android.sdkuilib.internal.repository.ui.PackagesPage.MenuAction; +import com.android.sdkuilib.internal.tasks.ILogUiProvider; +import com.android.sdkuilib.internal.tasks.ProgressView; +import com.android.sdkuilib.internal.tasks.ProgressViewFactory; +import com.android.sdkuilib.internal.widgets.ImgDisabledButton; +import com.android.sdkuilib.internal.widgets.ToggleButton; +import com.android.sdkuilib.repository.AvdManagerWindow.AvdInvocationContext; +import com.android.sdkuilib.repository.SdkUpdaterWindow.SdkInvocationContext; +import com.android.utils.ILogger; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Image; +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.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.ProgressBar; +import org.eclipse.swt.widgets.Shell; + +/** + * This is the private implementation of the UpdateWindow + * for the second version of the SDK Manager. + *

+ * This window features only one embedded page, the combined installed+available package list. + */ +public class SdkUpdaterWindowImpl2 implements ISdkUpdaterWindow { + + public static final String APP_NAME = "Android SDK Manager"; + private static final String SIZE_POS_PREFIX = "sdkman2"; //$NON-NLS-1$ + + private final Shell mParentShell; + private final SdkInvocationContext mContext; + /** Internal data shared between the window and its pages. */ + private final SwtUpdaterData mSwtUpdaterData; + + // --- UI members --- + + protected Shell mShell; + private PackagesPage mPkgPage; + private ProgressBar mProgressBar; + private Label mStatusText; + private ImgDisabledButton mButtonStop; + private ToggleButton mButtonShowLog; + private SettingsController mSettingsController; + private LogWindow mLogWindow; + + /** + * Creates a new window. Caller must call open(), which will block. + * + * @param parentShell Parent shell. + * @param sdkLog Logger. Cannot be null. + * @param osSdkRoot The OS path to the SDK root. + * @param context The {@link SdkInvocationContext} to change the behavior depending on who's + * opening the SDK Manager. + */ + public SdkUpdaterWindowImpl2( + Shell parentShell, + ILogger sdkLog, + String osSdkRoot, + SdkInvocationContext context) { + mParentShell = parentShell; + mContext = context; + mSwtUpdaterData = new SwtUpdaterData(osSdkRoot, sdkLog); + } + + /** + * Creates a new window. Caller must call open(), which will block. + *

+ * This is to be used when the window is opened from {@link AvdManagerWindowImpl1} + * to share the same {@link SwtUpdaterData} structure. + * + * @param parentShell Parent shell. + * @param swtUpdaterData The parent's updater data. + * @param context The {@link SdkInvocationContext} to change the behavior depending on who's + * opening the SDK Manager. + */ + public SdkUpdaterWindowImpl2( + Shell parentShell, + SwtUpdaterData swtUpdaterData, + SdkInvocationContext context) { + mParentShell = parentShell; + mContext = context; + mSwtUpdaterData = swtUpdaterData; + } + + /** + * Opens the window. + * @wbp.parser.entryPoint + */ + @Override + public void open() { + if (mParentShell == null) { + Display.setAppName(APP_NAME); //$hide$ (hide from SWT designer) + } + + createShell(); + preCreateContent(); + createContents(); + createMenuBar(); + createLogWindow(); + mShell.open(); + mShell.layout(); + + if (postCreateContent()) { //$hide$ (hide from SWT designer) + Display display = Display.getDefault(); + while (!mShell.isDisposed()) { + if (!display.readAndDispatch()) { + display.sleep(); + } + } + } + + SdkSourceProperties p = new SdkSourceProperties(); + p.save(); + + dispose(); //$hide$ + } + + private void createShell() { + // The SDK Manager must use a shell trim when standalone + // or a dialog trim when invoked from somewhere else. + int style = SWT.SHELL_TRIM; + if (mContext != SdkInvocationContext.STANDALONE) { + style |= SWT.APPLICATION_MODAL; + } + + mShell = new Shell(mParentShell, style); + mShell.addDisposeListener(new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + ShellSizeAndPos.saveSizeAndPos(mShell, SIZE_POS_PREFIX); + onAndroidSdkUpdaterDispose(); //$hide$ (hide from SWT designer) + } + }); + + GridLayout glShell = new GridLayout(2, false); + glShell.verticalSpacing = 0; + glShell.horizontalSpacing = 0; + glShell.marginWidth = 0; + glShell.marginHeight = 0; + mShell.setLayout(glShell); + + mShell.setMinimumSize(new Point(600, 300)); + mShell.setSize(700, 500); + mShell.setText(APP_NAME); + + ShellSizeAndPos.loadSizeAndPos(mShell, SIZE_POS_PREFIX); + } + + private void createContents() { + mPkgPage = new PackagesPage(mShell, SWT.NONE, mSwtUpdaterData, mContext); + mPkgPage.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1)); + + Composite composite1 = new Composite(mShell, SWT.NONE); + composite1.setLayout(new GridLayout(1, false)); + composite1.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + + mProgressBar = new ProgressBar(composite1, SWT.NONE); + mProgressBar.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + + mStatusText = new Label(composite1, SWT.NONE); + mStatusText.setText("Status Placeholder"); //$NON-NLS-1$ placeholder + mStatusText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + + Composite composite2 = new Composite(mShell, SWT.NONE); + composite2.setLayout(new GridLayout(2, false)); + + mButtonStop = new ImgDisabledButton(composite2, SWT.NONE, + getImage("stop_enabled_16.png"), //$NON-NLS-1$ + getImage("stop_disabled_16.png"), //$NON-NLS-1$ + "Click to abort the current task", + ""); //$NON-NLS-1$ nothing to abort + mButtonStop.addListener(SWT.Selection, new Listener() { + @Override + public void handleEvent(Event event) { + onStopSelected(); + } + }); + + mButtonShowLog = new ToggleButton(composite2, SWT.NONE, + getImage("log_off_16.png"), //$NON-NLS-1$ + getImage("log_on_16.png"), //$NON-NLS-1$ + "Click to show the log window", // tooltip for state hidden=>shown + "Click to hide the log window"); // tooltip for state shown=>hidden + mButtonShowLog.addListener(SWT.Selection, new Listener() { + @Override + public void handleEvent(Event event) { + onToggleLogWindow(); + } + }); + } + + @SuppressWarnings("unused") // MenuItem works using side effects + private void createMenuBar() { + + Menu menuBar = new Menu(mShell, SWT.BAR); + mShell.setMenuBar(menuBar); + + MenuItem menuBarPackages = new MenuItem(menuBar, SWT.CASCADE); + menuBarPackages.setText("Packages"); + + Menu menuPkgs = new Menu(menuBarPackages); + menuBarPackages.setMenu(menuPkgs); + + MenuItem showUpdatesNew = new MenuItem(menuPkgs, + MenuAction.TOGGLE_SHOW_UPDATE_NEW_PKG.getMenuStyle()); + showUpdatesNew.setText( + MenuAction.TOGGLE_SHOW_UPDATE_NEW_PKG.getMenuTitle()); + mPkgPage.registerMenuAction( + MenuAction.TOGGLE_SHOW_UPDATE_NEW_PKG, showUpdatesNew); + + MenuItem showInstalled = new MenuItem(menuPkgs, + MenuAction.TOGGLE_SHOW_INSTALLED_PKG.getMenuStyle()); + showInstalled.setText( + MenuAction.TOGGLE_SHOW_INSTALLED_PKG.getMenuTitle()); + mPkgPage.registerMenuAction( + MenuAction.TOGGLE_SHOW_INSTALLED_PKG, showInstalled); + + MenuItem showObsoletePackages = new MenuItem(menuPkgs, + MenuAction.TOGGLE_SHOW_OBSOLETE_PKG.getMenuStyle()); + showObsoletePackages.setText( + MenuAction.TOGGLE_SHOW_OBSOLETE_PKG.getMenuTitle()); + mPkgPage.registerMenuAction( + MenuAction.TOGGLE_SHOW_OBSOLETE_PKG, showObsoletePackages); + + MenuItem showArchives = new MenuItem(menuPkgs, + MenuAction.TOGGLE_SHOW_ARCHIVES.getMenuStyle()); + showArchives.setText( + MenuAction.TOGGLE_SHOW_ARCHIVES.getMenuTitle()); + mPkgPage.registerMenuAction( + MenuAction.TOGGLE_SHOW_ARCHIVES, showArchives); + + new MenuItem(menuPkgs, SWT.SEPARATOR); + + MenuItem reload = new MenuItem(menuPkgs, + MenuAction.RELOAD.getMenuStyle()); + reload.setText( + MenuAction.RELOAD.getMenuTitle()); + mPkgPage.registerMenuAction( + MenuAction.RELOAD, reload); + + MenuItem menuBarTools = new MenuItem(menuBar, SWT.CASCADE); + menuBarTools.setText("Tools"); + + Menu menuTools = new Menu(menuBarTools); + menuBarTools.setMenu(menuTools); + + if (mContext == SdkInvocationContext.STANDALONE) { + MenuItem manageAvds = new MenuItem(menuTools, SWT.NONE); + manageAvds.setText("Manage AVDs..."); + manageAvds.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent event) { + onAvdManager(); + } + }); + } + + MenuItem manageSources = new MenuItem(menuTools, + MenuAction.SHOW_ADDON_SITES.getMenuStyle()); + manageSources.setText( + MenuAction.SHOW_ADDON_SITES.getMenuTitle()); + mPkgPage.registerMenuAction( + MenuAction.SHOW_ADDON_SITES, manageSources); + + if (mContext == SdkInvocationContext.STANDALONE || mContext == SdkInvocationContext.IDE) { + try { + new MenuBarWrapper(APP_NAME, menuTools) { + @Override + public void onPreferencesMenuSelected() { + + // capture a copy of the initial settings + Settings settings1 = new Settings(mSettingsController.getSettings()); + + // open the dialog and wait for it to close + SettingsDialog sd = new SettingsDialog(mShell, mSwtUpdaterData); + sd.open(); + + // get the new settings + Settings settings2 = mSettingsController.getSettings(); + + // We need to reload the package list if the http mode or the preview + // modes have changed. + if (settings1.getForceHttp() != settings2.getForceHttp() || + settings1.getEnablePreviews() != settings2.getEnablePreviews()) { + mPkgPage.onSdkReload(); + } + } + + @Override + public void onAboutMenuSelected() { + AboutDialog ad = new AboutDialog(mShell, mSwtUpdaterData); + ad.open(); + } + + @Override + public void printError(String format, Object... args) { + if (mSwtUpdaterData != null) { + mSwtUpdaterData.getSdkLog().error(null, format, args); + } + } + }; + } catch (Throwable e) { + mSwtUpdaterData.getSdkLog().error(e, "Failed to setup menu bar"); + e.printStackTrace(); + } + } + } + + private Image getImage(String filename) { + if (mSwtUpdaterData != null) { + ImageFactory imgFactory = mSwtUpdaterData.getImageFactory(); + if (imgFactory != null) { + return imgFactory.getImageByName(filename); + } + } + return null; + } + + /** + * Creates the log window. + *

+ * If this is invoked from an IDE, we also define a secondary logger so that all + * messages flow to the IDE log. This may or may not be what we want in the end + * (e.g. a middle ground would be to repeat error, and ignore normal/verbose) + */ + private void createLogWindow() { + mLogWindow = new LogWindow(mShell, + mContext == SdkInvocationContext.IDE ? mSwtUpdaterData.getSdkLog() : null); + mLogWindow.open(); + } + + + // -- Start of internal part ---------- + // Hide everything down-below from SWT designer + //$hide>>$ + + // --- Public API ----------- + + /** + * Adds a new listener to be notified when a change is made to the content of the SDK. + */ + @Override + public void addListener(ISdkChangeListener listener) { + mSwtUpdaterData.addListeners(listener); + } + + /** + * Removes a new listener to be notified anymore when a change is made to the content of + * the SDK. + */ + @Override + public void removeListener(ISdkChangeListener listener) { + mSwtUpdaterData.removeListener(listener); + } + + // --- Internals & UI Callbacks ----------- + + /** + * Called before the UI is created. + */ + private void preCreateContent() { + mSwtUpdaterData.setWindowShell(mShell); + // We need the UI factory to create the UI + mSwtUpdaterData.setImageFactory(new ImageFactory(mShell.getDisplay())); + // Note: we can't create the TaskFactory yet because we need the UI + // to be created first, so this is done in postCreateContent(). + } + + /** + * Once the UI has been created, initializes the content. + * This creates the pages, selects the first one, setups sources and scans for local folders. + * + * Returns true if we should show the window. + */ + private boolean postCreateContent() { + ProgressViewFactory factory = new ProgressViewFactory(); + + // This class delegates all logging to the mLogWindow window + // and filters errors to make sure the window is visible when + // an error is logged. + ILogUiProvider logAdapter = new ILogUiProvider() { + @Override + public void setDescription(String description) { + mLogWindow.setDescription(description); + } + + @Override + public void log(String log) { + mLogWindow.log(log); + } + + @Override + public void logVerbose(String log) { + mLogWindow.logVerbose(log); + } + + @Override + public void logError(String log) { + mLogWindow.logError(log); + + // Run the window visibility check/toggle on the UI thread. + // Note: at least on Windows, it seems ok to check for the window visibility + // on a sub-thread but that doesn't seem cross-platform safe. We shouldn't + // have a lot of error logging, so this should be acceptable. If not, we could + // cache the visibility state. + if (mShell != null && !mShell.isDisposed()) { + mShell.getDisplay().syncExec(new Runnable() { + @Override + public void run() { + if (!mLogWindow.isVisible()) { + // Don't toggle the window visibility directly. + // Instead use the same action as the log-toggle button + // so that the button's state be kept in sync. + onToggleLogWindow(); + } + } + }); + } + } + }; + + factory.setProgressView( + new ProgressView(mStatusText, mProgressBar, mButtonStop, logAdapter)); + mSwtUpdaterData.setTaskFactory(factory); + + setWindowImage(mShell); + + setupSources(); + initializeSettings(); + + if (mSwtUpdaterData.checkIfInitFailed()) { + return false; + } + + mSwtUpdaterData.broadcastOnSdkLoaded(); + + // Tell the one page its the selected one + mPkgPage.performFirstLoad(); + + return true; + } + + /** + * Creates the icon of the window shell. + * + * @param shell The shell on which to put the icon + */ + private void setWindowImage(Shell shell) { + String imageName = "android_icon_16.png"; //$NON-NLS-1$ + if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_DARWIN) { + imageName = "android_icon_128.png"; //$NON-NLS-1$ + } + + if (mSwtUpdaterData != null) { + ImageFactory imgFactory = mSwtUpdaterData.getImageFactory(); + if (imgFactory != null) { + shell.setImage(imgFactory.getImageByName(imageName)); + } + } + } + + /** + * Called by the main loop when the window has been disposed. + */ + private void dispose() { + mLogWindow.close(); + mSwtUpdaterData.getSources().saveUserAddons(mSwtUpdaterData.getSdkLog()); + } + + /** + * Callback called when the window shell is disposed. + */ + private void onAndroidSdkUpdaterDispose() { + if (mSwtUpdaterData != null) { + ImageFactory imgFactory = mSwtUpdaterData.getImageFactory(); + if (imgFactory != null) { + imgFactory.dispose(); + } + } + } + + /** + * Used to initialize the sources. + */ + private void setupSources() { + mSwtUpdaterData.setupDefaultSources(); + } + + /** + * Initializes settings. + * This must be called after addExtraPages(), which created a settings page. + * Iterate through all the pages to find the first (and supposedly unique) setting page, + * and use it to load and apply these settings. + */ + private void initializeSettings() { + mSettingsController = mSwtUpdaterData.getSettingsController(); + mSettingsController.loadSettings(); + mSettingsController.applySettings(); + } + + private void onToggleLogWindow() { + // toggle visibility + if (!mButtonShowLog.isDisposed()) { + mLogWindow.setVisible(!mLogWindow.isVisible()); + mButtonShowLog.setState(mLogWindow.isVisible() ? 1 : 0); + } + } + + private void onStopSelected() { + // TODO + } + + private void onAvdManager() { + ITaskFactory oldFactory = mSwtUpdaterData.getTaskFactory(); + + try { + AvdManagerWindowImpl1 win = new AvdManagerWindowImpl1( + mShell, + mSwtUpdaterData, + AvdInvocationContext.DIALOG); + + win.open(); + } catch (Exception e) { + mSwtUpdaterData.getSdkLog().error(e, "AVD Manager window error"); + } finally { + mSwtUpdaterData.setTaskFactory(oldFactory); + } + } + + // End of hiding from SWT Designer + //$hide<<$ +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/ShellSizeAndPos.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/ShellSizeAndPos.java new file mode 100644 index 00000000..bdb11e5f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/ShellSizeAndPos.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2011 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.sdkuilib.internal.repository.ui; + + +import com.android.prefs.AndroidLocation; + +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.Monitor; +import org.eclipse.swt.widgets.Shell; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Properties; + +/** + * Utility to save & restore the size and position on a window + * using a common config file. + */ +public class ShellSizeAndPos { + + private static final String SETTINGS_FILENAME = "androidwin.cfg"; //$NON-NLS-1$ + private static final String PX = "_px"; //$NON-NLS-1$ + private static final String PY = "_py"; //$NON-NLS-1$ + private static final String SX = "_sx"; //$NON-NLS-1$ + private static final String SY = "_sy"; //$NON-NLS-1$ + + public static void loadSizeAndPos(Shell shell, String prefix) { + Properties props = loadProperties(); + + try { + int px = Integer.parseInt(props.getProperty(prefix + PX)); + int py = Integer.parseInt(props.getProperty(prefix + PY)); + int sx = Integer.parseInt(props.getProperty(prefix + SX)); + int sy = Integer.parseInt(props.getProperty(prefix + SY)); + + Point p1 = new Point(px, py); + Point p2 = new Point(px + sx, py + sy); + Rectangle r = new Rectangle(px, py, sy, sy); + + Monitor bestMatch = null; + int bestSurface = -1; + for (Monitor monitor : shell.getDisplay().getMonitors()) { + Rectangle area = monitor.getClientArea(); + if (area.contains(p1) && area.contains(p2)) { + // The shell is fully visible on this monitor. Just use that. + bestMatch = monitor; + bestSurface = Integer.MAX_VALUE; + break; + } else { + // Find which monitor displays the largest surface of the window. + // We'll use this one to center the window there, to make sure we're not + // starting split between several monitors. + Rectangle i = area.intersection(r); + int surface = i.width * i.height; + if (surface > bestSurface) { + bestSurface = surface; + bestMatch = monitor; + } + } + } + + if (bestMatch != null && bestSurface != Integer.MAX_VALUE) { + // Recenter the window on this monitor and make sure it fits + Rectangle area = bestMatch.getClientArea(); + + sx = Math.min(sx, area.width); + sy = Math.min(sy, area.height); + px = area.x + (area.width - sx) / 2; + py = area.y + (area.height - sy) / 2; + } + + shell.setLocation(px, py); + shell.setSize(sx, sy); + + } catch ( Exception e) { + // Ignore exception. We could typically get NPE from the getProperty + // or NumberFormatException from parseInt calls. Either way, do + // nothing if anything goes wrong. + } + } + + public static void saveSizeAndPos(Shell shell, String prefix) { + Properties props = loadProperties(); + + Point loc = shell.getLocation(); + Point size = shell.getSize(); + + props.setProperty(prefix + PX, Integer.toString(loc.x)); + props.setProperty(prefix + PY, Integer.toString(loc.y)); + props.setProperty(prefix + SX, Integer.toString(size.x)); + props.setProperty(prefix + SY, Integer.toString(size.y)); + + saveProperties(props); + } + + /** + * Load properties saved in {@link #SETTINGS_FILENAME}. + * If the file does not exists or doesn't load properly, just return an + * empty set of properties. + */ + private static Properties loadProperties() { + Properties props = new Properties(); + FileInputStream fis = null; + + try { + String folder = AndroidLocation.getFolder(); + File f = new File(folder, SETTINGS_FILENAME); + if (f.exists()) { + fis = new FileInputStream(f); + + props.load(fis); + } + } catch (Exception e) { + // Ignore + } finally { + if (fis != null) { + try { + fis.close(); + } catch (IOException e) { + } + } + } + + return props; + } + + private static void saveProperties(Properties props) { + FileOutputStream fos = null; + + try { + String folder = AndroidLocation.getFolder(); + File f = new File(folder, SETTINGS_FILENAME); + fos = new FileOutputStream(f); + + props.store(fos, "## Size and Pos for SDK Manager Windows"); //$NON-NLS-1$ + + } catch (Exception e) { + // ignore + } finally { + if (fos != null) { + try { + fos.close(); + } catch (IOException e) { + } + } + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ILogUiProvider.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ILogUiProvider.java new file mode 100644 index 00000000..fbc73092 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ILogUiProvider.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2011 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.sdkuilib.internal.tasks; + + +/** + * Interface for a user interface that displays the log from a task monitor. + */ +public interface ILogUiProvider { + + /** + * Sets the description in the current task dialog. + * This method can be invoked from a non-UI thread. + */ + public abstract void setDescription(String description); + + /** + * Logs a "normal" information line. + * This method can be invoked from a non-UI thread. + */ + public abstract void log(String log); + + /** + * Logs an "error" information line. + * This method can be invoked from a non-UI thread. + */ + public abstract void logError(String log); + + /** + * Logs a "verbose" information line, that is extra details which are typically + * not that useful for the end-user and might be hidden until explicitly shown. + * This method can be invoked from a non-UI thread. + */ + public abstract void logVerbose(String log); + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/IProgressUiProvider.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/IProgressUiProvider.java new file mode 100644 index 00000000..e10a0c24 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/IProgressUiProvider.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2011 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.sdkuilib.internal.tasks; + +import com.android.sdklib.internal.repository.ITaskMonitor; +import com.android.sdklib.internal.repository.UserCredentials; + +import org.eclipse.swt.widgets.ProgressBar; + +/** + * Interface for a user interface that displays both a task status + * (e.g. via an {@link ITaskMonitor}) and the progress state of the + * task (e.g. via a progress bar.) + *

+ * See {@link ITaskMonitor} for details on how a monitor expects to + * be displayed. + */ +interface IProgressUiProvider extends ILogUiProvider { + + public abstract boolean isCancelRequested(); + + /** + * Sets the description in the current task dialog. + * This method can be invoked from a non-UI thread. + */ + @Override + public abstract void setDescription(String description); + + /** + * Sets the max value of the progress bar. + * This method can be invoked from a non-UI thread. + * + * @see ProgressBar#setMaximum(int) + */ + public abstract void setProgressMax(int max); + + /** + * Sets the current value of the progress bar. + * This method can be invoked from a non-UI thread. + */ + public abstract void setProgress(int value); + + /** + * Returns the current value of the progress bar, + * between 0 and up to {@link #setProgressMax(int)} - 1. + * This method can be invoked from a non-UI thread. + */ + public abstract int getProgress(); + + /** + * Display a yes/no question dialog box. + * + * This implementation allow this to be called from any thread, it + * makes sure the dialog is opened synchronously in the ui thread. + * + * @param title The title of the dialog box + * @param message The error message + * @return true if YES was clicked. + */ + public abstract boolean displayPrompt(String title, String message); + + /** + * Launch an interface which asks for login credentials. Implementations + * MUST allow this to be called from any thread, e.g. by making sure the + * dialog is opened synchronously in the UI thread. + * + * @param title The title of the dialog box. + * @param message The message to be displayed as an instruction. + * @return Returns user provided credentials + */ + public UserCredentials displayLoginCredentialsPrompt(String title, String message); + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTask.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTask.java new file mode 100644 index 00000000..2563a5ff --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTask.java @@ -0,0 +1,108 @@ +/* + * 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.sdkuilib.internal.tasks; + +import com.android.sdklib.internal.repository.ITask; +import com.android.sdklib.internal.repository.ITaskMonitor; + +import org.eclipse.swt.widgets.Shell; + + +/** + * An {@link ITaskMonitor} that displays a {@link ProgressTaskDialog}. + */ +public final class ProgressTask extends TaskMonitorImpl { + + private final String mTitle; + private final ProgressTaskDialog mDialog; + private volatile boolean mAutoClose = true; + + + /** + * Creates a new {@link ProgressTask} with the given title. + * This does NOT start the task. The caller must invoke {@link #start(ITask)}. + */ + public ProgressTask(Shell parent, String title) { + super(new ProgressTaskDialog(parent)); + mTitle = title; + mDialog = (ProgressTaskDialog) getUiProvider(); + mDialog.setText(mTitle); + } + + /** + * Execute the given task in a separate thread (not the UI thread). + * This blocks till the thread ends. + *

+ * The {@link ProgressTask} must not be reused after this call. + */ + public void start(ITask task) { + assert mDialog != null; + mDialog.open(createTaskThread(mTitle, task)); + } + + /** + * Changes the auto-close behavior of the dialog on task completion. + * + * @param autoClose True if the dialog should be closed automatically when the task + * has completed. + */ + public void setAutoClose(boolean autoClose) { + if (autoClose != mAutoClose) { + if (autoClose) { + mDialog.setAutoCloseRequested(); + } else { + mDialog.setManualCloseRequested(); + } + mAutoClose = autoClose; + } + } + + /** + * Creates a thread to run the task. The thread has not been started yet. + * When the task completes, requests to close the dialog. + * + * @return A new thread that will run the task. The thread has not been started yet. + */ + private Thread createTaskThread(String title, final ITask task) { + if (task != null) { + return new Thread(title) { + @Override + public void run() { + task.run(ProgressTask.this); + if (mAutoClose) { + mDialog.setAutoCloseRequested(); + } else { + mDialog.setManualCloseRequested(); + } + } + }; + } + return null; + } + + /** + * {@inheritDoc} + *

+ * Sets the dialog to not auto-close since we want the user to see the error + * (this is equivalent to calling {@code setAutoClose(false)}). + */ + @Override + public void logError(String format, Object...args) { + setAutoClose(false); + super.logError(format, args); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTaskDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTaskDialog.java new file mode 100644 index 00000000..1a16799f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTaskDialog.java @@ -0,0 +1,520 @@ +/* + * 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.sdkuilib.internal.tasks; + +import com.android.SdkConstants; +import com.android.sdklib.internal.repository.ITaskMonitor; +import com.android.sdklib.internal.repository.UserCredentials; +import com.android.sdkuilib.ui.AuthenticationDialog; +import com.android.sdkuilib.ui.GridDialog; +import com.android.utils.Pair; + +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.ShellAdapter; +import org.eclipse.swt.events.ShellEvent; +import org.eclipse.swt.graphics.Point; +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.Label; +import org.eclipse.swt.widgets.ProgressBar; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + + +/** + * Implements a {@link ProgressTaskDialog}, used by the {@link ProgressTask} class. + * This separates the dialog UI from the task logic. + * + * Note: this does not implement the {@link ITaskMonitor} interface to avoid confusing + * SWT Designer. + */ +final class ProgressTaskDialog extends Dialog implements IProgressUiProvider { + + /** + * Min Y location for dialog. Need to deal with the menu bar on mac os. + */ + private final static int MIN_Y = SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN ? + 20 : 0; + + private static enum CancelMode { + /** Cancel button says "Cancel" and is enabled. Waiting for user to cancel. */ + ACTIVE, + /** Cancel button has been clicked. Waiting for thread to finish. */ + CANCEL_PENDING, + /** Close pending. Close button clicked or thread finished but there were some + * messages so the user needs to manually close. */ + CLOSE_MANUAL, + /** Close button clicked or thread finished. The window will automatically close. */ + CLOSE_AUTO + } + + /** The current mode of operation of the dialog. */ + private CancelMode mCancelMode = CancelMode.ACTIVE; + + /** Last dialog size for this session. */ + private static Point sLastSize; + + + // UI fields + private Shell mDialogShell; + private Composite mRootComposite; + private Label mLabel; + private ProgressBar mProgressBar; + private Button mCancelButton; + private Text mResultText; + + + /** + * Create the dialog. + * @param parent Parent container + */ + public ProgressTaskDialog(Shell parent) { + super(parent, SWT.APPLICATION_MODAL); + } + + /** + * Open the dialog and blocks till it gets closed + * @param taskThread The thread to run the task. Cannot be null. + */ + public void open(Thread taskThread) { + createContents(); + positionShell(); //$hide$ (hide from SWT designer) + mDialogShell.open(); + mDialogShell.layout(); + + startThread(taskThread); //$hide$ (hide from SWT designer) + + Display display = getParent().getDisplay(); + while (!mDialogShell.isDisposed() && mCancelMode != CancelMode.CLOSE_AUTO) { + if (!display.readAndDispatch()) { + display.sleep(); + } + } + + setCancelRequested(); //$hide$ (hide from SWT designer) + + if (!mDialogShell.isDisposed()) { + sLastSize = mDialogShell.getSize(); + mDialogShell.close(); + } + } + + /** + * Create contents of the dialog. + */ + private void createContents() { + mDialogShell = new Shell(getParent(), SWT.DIALOG_TRIM | SWT.RESIZE); + mDialogShell.addShellListener(new ShellAdapter() { + @Override + public void shellClosed(ShellEvent e) { + onShellClosed(e); + } + }); + mDialogShell.setLayout(new GridLayout(1, false)); + mDialogShell.setSize(450, 300); + mDialogShell.setText(getText()); + + mRootComposite = new Composite(mDialogShell, SWT.NONE); + mRootComposite.setLayout(new GridLayout(2, false)); + mRootComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1)); + + mLabel = new Label(mRootComposite, SWT.NONE); + mLabel.setText("Task"); + mLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 2, 1)); + + mProgressBar = new ProgressBar(mRootComposite, SWT.NONE); + mProgressBar.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + mCancelButton = new Button(mRootComposite, SWT.NONE); + mCancelButton.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1)); + mCancelButton.setText("Cancel"); + + mCancelButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + onCancelSelected(); //$hide$ + } + }); + + mResultText = new Text(mRootComposite, + SWT.BORDER | SWT.READ_ONLY | SWT.WRAP | + SWT.H_SCROLL | SWT.V_SCROLL | SWT.CANCEL | SWT.MULTI); + mResultText.setEditable(true); + mResultText.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1)); + } + + // -- End of UI, Start of internal logic ---------- + // Hide everything down-below from SWT designer + //$hide>>$ + + @Override + public boolean isCancelRequested() { + return mCancelMode != CancelMode.ACTIVE; + } + + /** + * Sets the mode to cancel pending. + * The first time this grays the cancel button, to let the user know that the + * cancel operation is pending. + */ + public void setCancelRequested() { + if (!mDialogShell.isDisposed()) { + // The dialog is not disposed, make sure to run all this in the UI thread + // and lock on the cancel button mode. + mDialogShell.getDisplay().syncExec(new Runnable() { + + @Override + public void run() { + synchronized (mCancelMode) { + if (mCancelMode == CancelMode.ACTIVE) { + mCancelMode = CancelMode.CANCEL_PENDING; + + if (!mCancelButton.isDisposed()) { + mCancelButton.setEnabled(false); + } + } + } + } + }); + } else { + // The dialog is disposed. Just set the boolean. We shouldn't be here. + if (mCancelMode == CancelMode.ACTIVE) { + mCancelMode = CancelMode.CANCEL_PENDING; + } + } + } + + /** + * Sets the mode to close manual. + * The first time, this also ungrays the pause button and converts it to a close button. + */ + public void setManualCloseRequested() { + if (!mDialogShell.isDisposed()) { + // The dialog is not disposed, make sure to run all this in the UI thread + // and lock on the cancel button mode. + mDialogShell.getDisplay().syncExec(new Runnable() { + + @Override + public void run() { + synchronized (mCancelMode) { + if (mCancelMode != CancelMode.CLOSE_MANUAL && + mCancelMode != CancelMode.CLOSE_AUTO) { + mCancelMode = CancelMode.CLOSE_MANUAL; + + if (!mCancelButton.isDisposed()) { + mCancelButton.setEnabled(true); + mCancelButton.setText("Close"); + } + } + } + } + }); + } else { + // The dialog is disposed. Just set the booleans. We shouldn't be here. + if (mCancelMode != CancelMode.CLOSE_MANUAL && + mCancelMode != CancelMode.CLOSE_AUTO) { + mCancelMode = CancelMode.CLOSE_MANUAL; + } + } + } + + /** + * Sets the mode to close auto. + * The main loop will just exit and close the shell at the first opportunity. + */ + public void setAutoCloseRequested() { + synchronized (mCancelMode) { + if (mCancelMode != CancelMode.CLOSE_AUTO) { + mCancelMode = CancelMode.CLOSE_AUTO; + } + } + } + + /** + * Callback invoked when the cancel button is selected. + * When in closing mode, this simply closes the shell. Otherwise triggers a cancel. + */ + private void onCancelSelected() { + if (mCancelMode == CancelMode.CLOSE_MANUAL) { + setAutoCloseRequested(); + } else { + setCancelRequested(); + } + } + + /** + * Callback invoked when the shell is closed either by clicking the close button + * on by calling shell.close(). + * This does the same thing as clicking the cancel/close button unless the mode is + * to auto close in which case we should do nothing to let the shell close normally. + */ + private void onShellClosed(ShellEvent e) { + if (mCancelMode != CancelMode.CLOSE_AUTO) { + e.doit = false; // don't close directly + onCancelSelected(); + } + } + + /** + * Sets the description in the current task dialog. + * This method can be invoked from a non-UI thread. + */ + @Override + public void setDescription(final String description) { + mDialogShell.getDisplay().syncExec(new Runnable() { + @Override + public void run() { + if (!mLabel.isDisposed()) { + mLabel.setText(description); + } + } + }); + } + + /** + * Adds to the log in the current task dialog. + * This method can be invoked from a non-UI thread. + */ + @Override + public void log(final String info) { + if (!mDialogShell.isDisposed()) { + mDialogShell.getDisplay().syncExec(new Runnable() { + @Override + public void run() { + if (!mResultText.isDisposed()) { + mResultText.setVisible(true); + String lastText = mResultText.getText(); + if (lastText != null && + lastText.length() > 0 && + !lastText.endsWith("\n") && //$NON-NLS-1$ + !info.startsWith("\n")) { //$NON-NLS-1$ + mResultText.append("\n"); //$NON-NLS-1$ + } + mResultText.append(info); + } + } + }); + } + } + + @Override + public void logError(String info) { + log(info); + } + + @Override + public void logVerbose(String info) { + log(info); + } + + /** + * Sets the max value of the progress bar. + * This method can be invoked from a non-UI thread. + * + * @see ProgressBar#setMaximum(int) + */ + @Override + public void setProgressMax(final int max) { + if (!mDialogShell.isDisposed()) { + mDialogShell.getDisplay().syncExec(new Runnable() { + @Override + public void run() { + if (!mProgressBar.isDisposed()) { + mProgressBar.setMaximum(max); + } + } + }); + } + } + + /** + * Sets the current value of the progress bar. + * This method can be invoked from a non-UI thread. + */ + @Override + public void setProgress(final int value) { + if (!mDialogShell.isDisposed()) { + mDialogShell.getDisplay().syncExec(new Runnable() { + @Override + public void run() { + if (!mProgressBar.isDisposed()) { + mProgressBar.setSelection(value); + } + } + }); + } + } + + /** + * Returns the current value of the progress bar, + * between 0 and up to {@link #setProgressMax(int)} - 1. + * This method can be invoked from a non-UI thread. + */ + @Override + public int getProgress() { + final int[] result = new int[] { 0 }; + + if (!mDialogShell.isDisposed()) { + mDialogShell.getDisplay().syncExec(new Runnable() { + @Override + public void run() { + if (!mProgressBar.isDisposed()) { + result[0] = mProgressBar.getSelection(); + } + } + }); + } + + return result[0]; + } + + /** + * Display a yes/no question dialog box. + * + * This implementation allow this to be called from any thread, it + * makes sure the dialog is opened synchronously in the ui thread. + * + * @param title The title of the dialog box + * @param message The error message + * @return true if YES was clicked. + */ + @Override + public boolean displayPrompt(final String title, final String message) { + Display display = mDialogShell.getDisplay(); + + // we need to ask the user what he wants to do. + final boolean[] result = new boolean[] { false }; + display.syncExec(new Runnable() { + @Override + public void run() { + result[0] = MessageDialog.openQuestion(mDialogShell, title, message); + } + }); + return result[0]; + } + + /** + * This method opens a pop-up window which requests for User Login and + * password. + * + * @param title The title of the window. + * @param message The message to displayed in the login/password window. + * @return Returns a {@link Pair} holding the entered login and password. + * The information must always be in the following order: + * Login,Password. So in order to retrieve the login callers + * should retrieve the first element, and the second value for the + * password. + * If operation is canceled by user the return value must be null. + * @see ITaskMonitor#displayLoginCredentialsPrompt(String, String) + */ + @Override + public UserCredentials displayLoginCredentialsPrompt( + final String title, final String message) { + Display display = mDialogShell.getDisplay(); + + // open dialog and request login and password + GetUserCredentialsTask task = new GetUserCredentialsTask(mDialogShell, title, message); + display.syncExec(task); + + return task.getUserCredentials(); + } + + private static class GetUserCredentialsTask implements Runnable { + private UserCredentials mResult = null; + + private Shell mShell; + private String mTitle; + private String mMessage; + + public GetUserCredentialsTask(Shell shell, String title, String message) { + mShell = shell; + mTitle = title; + mMessage = message; + } + + @Override + public void run() { + AuthenticationDialog authenticationDialog = new AuthenticationDialog(mShell, + mTitle, mMessage); + int dlgResult= authenticationDialog.open(); + if(dlgResult == GridDialog.OK) { + mResult = new UserCredentials( + authenticationDialog.getLogin(), + authenticationDialog.getPassword(), + authenticationDialog.getWorkstation(), + authenticationDialog.getDomain()); + } + } + + public UserCredentials getUserCredentials() { + return mResult; + } + } + + /** + * Starts the thread that runs the task. + * This is deferred till the UI is created. + */ + private void startThread(Thread taskThread) { + if (taskThread != null) { + taskThread.start(); + } + } + + /** + * Centers the dialog in its parent shell. + */ + private void positionShell() { + // Centers the dialog in its parent shell + Shell child = mDialogShell; + Shell parent = getParent(); + if (child != null && parent != null) { + + // get the parent client area with a location relative to the display + Rectangle parentArea = parent.getClientArea(); + Point parentLoc = parent.getLocation(); + int px = parentLoc.x; + int py = parentLoc.y; + int pw = parentArea.width; + int ph = parentArea.height; + + // Reuse the last size if there's one, otherwise use the default + Point childSize = sLastSize != null ? sLastSize : child.getSize(); + int cw = childSize.x; + int ch = childSize.y; + + int x = px + (pw - cw) / 2; + if (x < 0) x = 0; + + int y = py + (ph - ch) / 2; + if (y < MIN_Y) y = MIN_Y; + + child.setLocation(x, y); + child.setSize(cw, ch); + } + } + + // End of hiding from SWT Designer + //$hide<<$ +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTaskFactory.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTaskFactory.java new file mode 100644 index 00000000..bd2cc14f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTaskFactory.java @@ -0,0 +1,67 @@ +/* + * 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.sdkuilib.internal.tasks; + +import com.android.sdklib.internal.repository.ITask; +import com.android.sdklib.internal.repository.ITaskFactory; +import com.android.sdklib.internal.repository.ITaskMonitor; + +import org.eclipse.swt.widgets.Shell; + +/** + * An {@link ITaskFactory} that creates a new {@link ProgressTask} dialog + * for each new task. + */ +public final class ProgressTaskFactory implements ITaskFactory { + + private final Shell mShell; + + public ProgressTaskFactory(Shell shell) { + mShell = shell; + } + + @Override + public void start(String title, ITask task) { + start(title, null /*parentMonitor*/, task); + } + + @Override + public void start(String title, ITaskMonitor parentMonitor, ITask task) { + + if (parentMonitor == null) { + ProgressTask p = new ProgressTask(mShell, title); + p.start(task); + } else { + // Use all the reminder of the parent monitor. + if (parentMonitor.getProgressMax() == 0) { + parentMonitor.setProgressMax(1); + } + + ITaskMonitor sub = parentMonitor.createSubMonitor( + parentMonitor.getProgressMax() - parentMonitor.getProgress()); + try { + task.run(sub); + } finally { + int delta = + sub.getProgressMax() - sub.getProgress(); + if (delta > 0) { + sub.incProgress(delta); + } + } + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressView.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressView.java new file mode 100644 index 00000000..fe7d6c51 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressView.java @@ -0,0 +1,376 @@ +/* + * Copyright (C) 2011 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.sdkuilib.internal.tasks; + +import com.android.sdklib.internal.repository.ITask; +import com.android.sdklib.internal.repository.ITaskMonitor; +import com.android.sdklib.internal.repository.UserCredentials; +import com.android.sdkuilib.ui.AuthenticationDialog; +import com.android.sdkuilib.ui.GridDialog; + +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.swt.SWT; +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.ProgressBar; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Widget; + +import java.util.concurrent.atomic.AtomicReference; + + +/** + * Implements a "view" that uses an existing progress bar, status button and + * status text to display a {@link ITaskMonitor}. + */ +public final class ProgressView implements IProgressUiProvider { + + private static enum State { + /** View created but there's no task running. Next state can only be ACTIVE. */ + IDLE, + /** A task is currently running. Next state is either STOP_PENDING or IDLE. */ + ACTIVE, + /** Stop button has been clicked. Waiting for thread to finish. Next state is IDLE. */ + STOP_PENDING, + } + + /** The current mode of operation of the dialog. */ + private State mState = State.IDLE; + + + + // UI fields + private final Label mLabel; + private final Control mStopButton; + private final ProgressBar mProgressBar; + + /** Logger object. Cannot not be null. */ + private final ILogUiProvider mLog; + + /** + * Creates a new {@link ProgressView} object, a simple "holder" for the various + * widgets used to display and update a progress + status bar. + * + * @param label The label to display titles of status updates (e.g. task titles and + * calls to {@link #setDescription(String)}.) Must not be null. + * @param progressBar The progress bar to update during a task. Must not be null. + * @param stopButton The stop button. It will be disabled when there's no task that can + * be interrupted. A selection listener will be attached to it. Optional. Can be null. + * @param log A mandatory logger object that will be used to report all the log. + * Must not be null. + */ + public ProgressView( + Label label, + ProgressBar progressBar, + Control stopButton, + ILogUiProvider log) { + mLabel = label; + mProgressBar = progressBar; + mLog = log; + mProgressBar.setEnabled(false); + + mStopButton = stopButton; + if (mStopButton != null) { + mStopButton.addListener(SWT.Selection, new Listener() { + @Override + public void handleEvent(Event event) { + if (mState == State.ACTIVE) { + changeState(State.STOP_PENDING); + } + } + }); + } + } + + /** + * Starts the task and block till it's either finished or canceled. + * This can be called from a non-UI thread safely. + *

+ * When a task is started from within a monitor, it reuses the thread + * from the parent. Otherwise it starts a new thread and runs it own + * UI loop. This means the task can perform UI operations using + * {@link Display#asyncExec(Runnable)}. + *

+ * In either case, the method only returns when the task has finished. + */ + public void startTask( + final String title, + final ITaskMonitor parentMonitor, + final ITask task) { + if (task != null) { + try { + if (parentMonitor == null && !mProgressBar.isDisposed()) { + mLabel.setText(title); + mProgressBar.setSelection(0); + mProgressBar.setEnabled(true); + changeState(ProgressView.State.ACTIVE); + } + + Runnable r = new Runnable() { + @Override + public void run() { + if (parentMonitor == null) { + task.run(new TaskMonitorImpl(ProgressView.this)); + + } else { + // Use all the reminder of the parent monitor. + if (parentMonitor.getProgressMax() == 0) { + parentMonitor.setProgressMax(1); + } + ITaskMonitor sub = parentMonitor.createSubMonitor( + parentMonitor.getProgressMax() - parentMonitor.getProgress()); + try { + task.run(sub); + } finally { + int delta = + sub.getProgressMax() - sub.getProgress(); + if (delta > 0) { + sub.incProgress(delta); + } + } + } + } + }; + + // If for some reason the UI has been disposed, just abort the thread. + if (mProgressBar.isDisposed()) { + return; + } + + if (TaskMonitorImpl.isTaskMonitorImpl(parentMonitor)) { + // If there's a parent monitor and it's our own class, we know this parent + // is already running a thread and the base one is running an event loop. + // We should thus not run a second event loop and we can process the + // runnable right here instead of spawning a thread inside the thread. + r.run(); + + } else { + // No parent monitor. This is the first one so we need a thread and + // we need to process UI events. + + final Thread t = new Thread(r, title); + t.start(); + + // Process the app's event loop whilst we wait for the thread to finish + while (!mProgressBar.isDisposed() && t.isAlive()) { + Display display = mProgressBar.getDisplay(); + if (!mProgressBar.isDisposed() && !display.readAndDispatch()) { + display.sleep(); + } + } + } + } catch (Exception e) { + // TODO log + + } finally { + if (parentMonitor == null && !mProgressBar.isDisposed()) { + changeState(ProgressView.State.IDLE); + mProgressBar.setSelection(0); + mProgressBar.setEnabled(false); + } + } + } + } + + private void syncExec(final Widget widget, final Runnable runnable) { + if (widget != null && !widget.isDisposed()) { + widget.getDisplay().syncExec(new Runnable() { + @Override + public void run() { + // Check again whether the widget got disposed between the time where + // we requested the syncExec and the time it actually happened. + if (!widget.isDisposed()) { + runnable.run(); + } + } + }); + } + } + + private void changeState(State state) { + if (mState != null ) { + mState = state; + } + + syncExec(mStopButton, new Runnable() { + @Override + public void run() { + mStopButton.setEnabled(mState == State.ACTIVE); + } + }); + + } + + // --- Implementation of ITaskUiProvider --- + + @Override + public boolean isCancelRequested() { + return mState != State.ACTIVE; + } + + /** + * Sets the description in the current task dialog. + * This method can be invoked from a non-UI thread. + */ + @Override + public void setDescription(final String description) { + syncExec(mLabel, new Runnable() { + @Override + public void run() { + mLabel.setText(description); + } + }); + + mLog.setDescription(description); + } + + /** + * Logs a "normal" information line. + * This method can be invoked from a non-UI thread. + */ + @Override + public void log(String log) { + mLog.log(log); + } + + /** + * Logs an "error" information line. + * This method can be invoked from a non-UI thread. + */ + @Override + public void logError(String log) { + mLog.logError(log); + } + + /** + * Logs a "verbose" information line, that is extra details which are typically + * not that useful for the end-user and might be hidden until explicitly shown. + * This method can be invoked from a non-UI thread. + */ + @Override + public void logVerbose(String log) { + mLog.logVerbose(log); + } + + /** + * Sets the max value of the progress bar. + * This method can be invoked from a non-UI thread. + * + * @see ProgressBar#setMaximum(int) + */ + @Override + public void setProgressMax(final int max) { + syncExec(mProgressBar, new Runnable() { + @Override + public void run() { + mProgressBar.setMaximum(max); + } + }); + } + + /** + * Sets the current value of the progress bar. + * This method can be invoked from a non-UI thread. + */ + @Override + public void setProgress(final int value) { + syncExec(mProgressBar, new Runnable() { + @Override + public void run() { + mProgressBar.setSelection(value); + } + }); + } + + /** + * Returns the current value of the progress bar, + * between 0 and up to {@link #setProgressMax(int)} - 1. + * This method can be invoked from a non-UI thread. + */ + @Override + public int getProgress() { + final int[] result = new int[] { 0 }; + + if (!mProgressBar.isDisposed()) { + mProgressBar.getDisplay().syncExec(new Runnable() { + @Override + public void run() { + if (!mProgressBar.isDisposed()) { + result[0] = mProgressBar.getSelection(); + } + } + }); + } + + return result[0]; + } + + @Override + public boolean displayPrompt(final String title, final String message) { + final boolean[] result = new boolean[] { false }; + + syncExec(mProgressBar, new Runnable() { + @Override + public void run() { + Shell shell = mProgressBar.getShell(); + result[0] = MessageDialog.openQuestion(shell, title, message); + } + }); + + return result[0]; + } + + /** + * This method opens a pop-up window which requests for User Credentials. + * + * @param title The title of the window. + * @param message The message to displayed in the login/password window. + * @return Returns user provided credentials. + * If operation is canceled by user the return value must be null. + * @see ITaskMonitor#displayLoginCredentialsPrompt(String, String) + */ + @Override + public UserCredentials + displayLoginCredentialsPrompt(final String title, final String message) { + final AtomicReference result = new AtomicReference(null); + + // open dialog and request login and password + syncExec(mProgressBar, new Runnable() { + @Override + public void run() { + Shell shell = mProgressBar.getShell(); + AuthenticationDialog authenticationDialog = new AuthenticationDialog(shell, + title, + message); + int dlgResult = authenticationDialog.open(); + if (dlgResult == GridDialog.OK) { + result.set(new UserCredentials( + authenticationDialog.getLogin(), + authenticationDialog.getPassword(), + authenticationDialog.getWorkstation(), + authenticationDialog.getDomain())); + } + } + }); + + return result.get(); + } +} + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressViewFactory.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressViewFactory.java new file mode 100644 index 00000000..1d39c597 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressViewFactory.java @@ -0,0 +1,48 @@ +/* + * 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.sdkuilib.internal.tasks; + +import com.android.sdklib.internal.repository.ITask; +import com.android.sdklib.internal.repository.ITaskFactory; +import com.android.sdklib.internal.repository.ITaskMonitor; + +/** + * An {@link ITaskFactory} that creates a new {@link ProgressTask} dialog + * for each new task. + */ +public final class ProgressViewFactory implements ITaskFactory { + + private ProgressView mProgressView; + + public ProgressViewFactory() { + } + + public void setProgressView(ProgressView progressView) { + mProgressView = progressView; + } + + @Override + public void start(String title, ITask task) { + start(title, null /*monitor*/, task); + } + + @Override + public void start(String title, ITaskMonitor parentMonitor, ITask task) { + assert mProgressView != null; + mProgressView.startTask(title, parentMonitor, task); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/TaskMonitorImpl.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/TaskMonitorImpl.java new file mode 100644 index 00000000..eaeb7522 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/TaskMonitorImpl.java @@ -0,0 +1,369 @@ +/* + * Copyright (C) 2011 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.sdkuilib.internal.tasks; + +import com.android.annotations.NonNull; +import com.android.sdklib.internal.repository.ITaskMonitor; +import com.android.sdklib.internal.repository.UserCredentials; + +/** + * Internal class that implements the logic of an {@link ITaskMonitor}. + * It doesn't deal with any UI directly. Instead it delegates the UI to + * the provided {@link IProgressUiProvider}. + */ +class TaskMonitorImpl implements ITaskMonitor { + + private static final double MAX_COUNT = 10000.0; + + private interface ISubTaskMonitor extends ITaskMonitor { + public void subIncProgress(double realDelta); + } + + private double mIncCoef = 0; + private double mValue = 0; + private final IProgressUiProvider mUi; + + /** + * Returns true if the given {@code monitor} is an instance of {@link TaskMonitorImpl} + * or its private SubTaskMonitor. + */ + public static boolean isTaskMonitorImpl(ITaskMonitor monitor) { + return monitor instanceof TaskMonitorImpl || monitor instanceof SubTaskMonitor; + } + + /** + * Constructs a new {@link TaskMonitorImpl} that relies on the given + * {@link IProgressUiProvider} to change the user interface. + * @param ui The {@link IProgressUiProvider}. Cannot be null. + */ + public TaskMonitorImpl(IProgressUiProvider ui) { + mUi = ui; + } + + /** Returns the {@link IProgressUiProvider} passed to the constructor. */ + public IProgressUiProvider getUiProvider() { + return mUi; + } + + /** + * Sets the description in the current task dialog. + * This method can be invoked from a non-UI thread. + */ + @Override + public void setDescription(String format, Object... args) { + final String text = String.format(format, args); + mUi.setDescription(text); + } + + /** + * Logs a "normal" information line. + * This method can be invoked from a non-UI thread. + */ + @Override + public void log(String format, Object... args) { + String text = String.format(format, args); + mUi.log(text); + } + + /** + * Logs an "error" information line. + * This method can be invoked from a non-UI thread. + */ + @Override + public void logError(String format, Object... args) { + String text = String.format(format, args); + mUi.logError(text); + } + + /** + * Logs a "verbose" information line, that is extra details which are typically + * not that useful for the end-user and might be hidden until explicitly shown. + * This method can be invoked from a non-UI thread. + */ + @Override + public void logVerbose(String format, Object... args) { + String text = String.format(format, args); + mUi.logVerbose(text); + } + + /** + * Sets the max value of the progress bar. + * This method can be invoked from a non-UI thread. + * + * Weird things will happen if setProgressMax is called multiple times + * *after* {@link #incProgress(int)}: we don't try to adjust it on the + * fly. + */ + @Override + public void setProgressMax(int max) { + assert max > 0; + // Always set the dialog's progress max to 10k since it only handles + // integers and we want to have a better inner granularity. Instead + // we use the max to compute a coefficient for inc deltas. + mUi.setProgressMax((int) MAX_COUNT); + mIncCoef = max > 0 ? MAX_COUNT / max : 0; + assert mIncCoef > 0; + } + + @Override + public int getProgressMax() { + return mIncCoef > 0 ? (int) (MAX_COUNT / mIncCoef) : 0; + } + + /** + * Increments the current value of the progress bar. + * + * This method can be invoked from a non-UI thread. + */ + @Override + public void incProgress(int delta) { + if (delta > 0 && mIncCoef > 0) { + internalIncProgress(delta * mIncCoef); + } + } + + private void internalIncProgress(double realDelta) { + mValue += realDelta; + mUi.setProgress((int)mValue); + } + + /** + * Returns the current value of the progress bar, + * between 0 and up to {@link #setProgressMax(int)} - 1. + * + * This method can be invoked from a non-UI thread. + */ + @Override + public int getProgress() { + // mIncCoef is 0 if setProgressMax hasn't been used yet. + return mIncCoef > 0 ? (int)(mUi.getProgress() / mIncCoef) : 0; + } + + /** + * Returns true if the "Cancel" button was selected. + * It is up to the task thread to pool this and exit. + */ + @Override + public boolean isCancelRequested() { + return mUi.isCancelRequested(); + } + + /** + * Displays a yes/no question dialog box. + * + * This implementation allow this to be called from any thread, it + * makes sure the dialog is opened synchronously in the ui thread. + * + * @param title The title of the dialog box + * @param message The error message + * @return true if YES was clicked. + */ + @Override + public boolean displayPrompt(final String title, final String message) { + return mUi.displayPrompt(title, message); + } + + /** + * Displays a Login/Password dialog. This implementation allows this method to be + * called from any thread, it makes sure the dialog is opened synchronously + * in the ui thread. + * + * @param title The title of the dialog box + * @param message Message to be displayed + * @return Pair with entered login/password. Login is always the first + * element and Password is always the second. If any error occurs a + * pair with empty strings is returned. + */ + @Override + public UserCredentials displayLoginCredentialsPrompt(String title, String message) { + return mUi.displayLoginCredentialsPrompt(title, message); + } + + /** + * Creates a sub-monitor that will use up to tickCount on the progress bar. + * tickCount must be 1 or more. + */ + @Override + public ITaskMonitor createSubMonitor(int tickCount) { + assert mIncCoef > 0; + assert tickCount > 0; + return new SubTaskMonitor(this, null, mValue, tickCount * mIncCoef); + } + + // ----- ILogger interface ---- + + @Override + public void error(Throwable throwable, String errorFormat, Object... arg) { + if (errorFormat != null) { + logError("Error: " + errorFormat, arg); + } + + if (throwable != null) { + logError("%s", throwable.getMessage()); //$NON-NLS-1$ + } + } + + @Override + public void warning(@NonNull String warningFormat, Object... arg) { + log("Warning: " + warningFormat, arg); + } + + @Override + public void info(@NonNull String msgFormat, Object... arg) { + log(msgFormat, arg); + } + + @Override + public void verbose(@NonNull String msgFormat, Object... arg) { + log(msgFormat, arg); + } + + // ----- Sub Monitor ----- + + private static class SubTaskMonitor implements ISubTaskMonitor { + + private final TaskMonitorImpl mRoot; + private final ISubTaskMonitor mParent; + private final double mStart; + private final double mSpan; + private double mSubValue; + private double mSubCoef; + + /** + * Creates a new sub task monitor which will work for the given range [start, start+span] + * in its parent. + * + * @param taskMonitor The ProgressTask root + * @param parent The immediate parent. Can be the null or another sub task monitor. + * @param start The start value in the root's coordinates + * @param span The span value in the root's coordinates + */ + public SubTaskMonitor(TaskMonitorImpl taskMonitor, + ISubTaskMonitor parent, + double start, + double span) { + mRoot = taskMonitor; + mParent = parent; + mStart = start; + mSpan = span; + mSubValue = start; + } + + @Override + public boolean isCancelRequested() { + return mRoot.isCancelRequested(); + } + + @Override + public void setDescription(String format, Object... args) { + mRoot.setDescription(format, args); + } + + @Override + public void log(String format, Object... args) { + mRoot.log(format, args); + } + + @Override + public void logError(String format, Object... args) { + mRoot.logError(format, args); + } + + @Override + public void logVerbose(String format, Object... args) { + mRoot.logVerbose(format, args); + } + + @Override + public void setProgressMax(int max) { + assert max > 0; + mSubCoef = max > 0 ? mSpan / max : 0; + assert mSubCoef > 0; + } + + @Override + public int getProgressMax() { + return mSubCoef > 0 ? (int) (mSpan / mSubCoef) : 0; + } + + @Override + public int getProgress() { + // subCoef can be 0 if setProgressMax() and incProgress() haven't been called yet + assert mSubValue == mStart || mSubCoef > 0; + return mSubCoef > 0 ? (int)((mSubValue - mStart) / mSubCoef) : 0; + } + + @Override + public void incProgress(int delta) { + if (delta > 0 && mSubCoef > 0) { + subIncProgress(delta * mSubCoef); + } + } + + @Override + public void subIncProgress(double realDelta) { + mSubValue += realDelta; + if (mParent != null) { + mParent.subIncProgress(realDelta); + } else { + mRoot.internalIncProgress(realDelta); + } + } + + @Override + public boolean displayPrompt(String title, String message) { + return mRoot.displayPrompt(title, message); + } + + @Override + public UserCredentials displayLoginCredentialsPrompt(String title, String message) { + return mRoot.displayLoginCredentialsPrompt(title, message); + } + + @Override + public ITaskMonitor createSubMonitor(int tickCount) { + assert mSubCoef > 0; + assert tickCount > 0; + return new SubTaskMonitor(mRoot, + this, + mSubValue, + tickCount * mSubCoef); + } + + // ----- ILogger interface ---- + + @Override + public void error(Throwable throwable, String errorFormat, Object... arg) { + mRoot.error(throwable, errorFormat, arg); + } + + @Override + public void warning(@NonNull String warningFormat, Object... arg) { + mRoot.warning(warningFormat, arg); + } + + @Override + public void info(@NonNull String msgFormat, Object... arg) { + mRoot.info(msgFormat, arg); + } + + @Override + public void verbose(@NonNull String msgFormat, Object... arg) { + mRoot.verbose(msgFormat, arg); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationDialog.java new file mode 100644 index 00000000..643732fb --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationDialog.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2012 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.sdkuilib.internal.widgets; + +import com.android.annotations.NonNull; +import com.android.sdklib.devices.Device; +import com.android.sdklib.internal.avd.AvdInfo; +import com.android.sdklib.internal.avd.AvdManager; +import com.android.sdkuilib.internal.repository.icons.ImageFactory; +import com.android.sdkuilib.internal.widgets.AvdCreationPresenter.IWidgetAdapter; +import com.android.utils.ILogger; + +import org.eclipse.swt.widgets.Shell; + +/** + * Displays a dialog to create an AVD. + *

+ * Implementation: the code is split using a simplified MVP pattern.
+ * {@link AvdCreationPresenter} contains all the logic of the dialog and performs all the + * actions and event handling.
+ * {@link AvdCreationSwtView} creates the SWT shell and maps the controls and forwards all + * events to the presenter. The presenter uses the {@link IWidgetAdapter} exposed by the + * SwtView class to update the display. + *

+ * To transform this dialog to Swing or a text-based unit-test, simply keep the presenter + * as-is and re-implement the {@link IWidgetAdapter}. + */ +public class AvdCreationDialog extends AvdCreationSwtView { + + public AvdCreationDialog(Shell shell, + AvdManager avdManager, + ImageFactory imageFactory, + ILogger log, + AvdInfo editAvdInfo) { + super(shell, imageFactory, new AvdCreationPresenter(avdManager, log, editAvdInfo)); + + } + + /** Returns the AVD Created, if successful. */ + public AvdInfo getCreatedAvd() { + return getPresenter().getCreatedAvd(); + } + + /** + * Can be called after the constructor to set the default device for this AVD. + * Useful especially for new AVDs. + * @param device + */ + public void selectInitialDevice(@NonNull Device device) { + getPresenter().selectInitialDevice(device); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationPresenter.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationPresenter.java new file mode 100644 index 00000000..338690f6 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationPresenter.java @@ -0,0 +1,1488 @@ +/* + * Copyright (C) 2014 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.sdkuilib.internal.widgets; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.prefs.AndroidLocation.AndroidLocationException; +import com.android.resources.Density; +import com.android.resources.ScreenSize; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.ISystemImage; +import com.android.sdklib.SystemImage; +import com.android.sdklib.devices.Camera; +import com.android.sdklib.devices.CameraLocation; +import com.android.sdklib.devices.Device; +import com.android.sdklib.devices.DeviceManager; +import com.android.sdklib.devices.Hardware; +import com.android.sdklib.devices.Screen; +import com.android.sdklib.devices.Software; +import com.android.sdklib.devices.Storage; +import com.android.sdklib.internal.avd.AvdInfo; +import com.android.sdklib.internal.avd.AvdManager; +import com.android.sdklib.internal.avd.AvdManager.AvdConflict; +import com.android.sdklib.internal.avd.HardwareProperties; +import com.android.sdklib.repository.descriptors.IdDisplay; +import com.android.sdklib.repository.local.LocalSdk; +import com.android.utils.ILogger; +import com.android.utils.Pair; +import com.google.common.base.Joiner; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Implements all the logic of the {@link AvdCreationDialog}. + *

+ * Implementation detail: the dialog is split using a simple MVP pattern.
+ * This code, the presenter, handles all the logic including event handling and controls + * what is displayed in the dialog. It does not directly manipulate the UI and in fact the + * code has no swt import. Instead the constructor is given a {@link IWidgetAdapter} + * implementation. This makes it possible to test the logic of the dialog as-is + * in a unit test by simply passing a different {@link IWidgetAdapter} implementation to the + * constructor. + */ +class AvdCreationPresenter { + + @NonNull + private IWidgetAdapter mWidgets; + private AvdManager mAvdManager; + private ILogger mSdkLog; + private AvdInfo mAvdInfo; + + private final TreeMap mCurrentTargets = + new TreeMap(); + + private static final AvdSkinChoice SKIN_DYNAMIC = + new AvdSkinChoice(SkinType.DYNAMIC, "Skin with dynamic hardware controls"); + private static final AvdSkinChoice SKIN_NONE = + new AvdSkinChoice(SkinType.NONE, "No skin"); + + private final List mComboDevices = new ArrayList(); + private final List mComboSkins = new ArrayList(); + private final List mComboSystemImages = new ArrayList(); + private final List mComboTargets = new ArrayList(); + + private Device mInitWithDevice; + private AvdInfo mCreatedAvd; + + public enum Ctrl { + BUTTON_OK, + BUTTON_BROWSE_SDCARD, + + COMBO_DEVICE, + COMBO_TARGET, + COMBO_TAG_ABI, + COMBO_SKIN, + COMBO_FRONT_CAM, + COMBO_BACK_CAM, + COMBO_DATA_PART_SIZE, + COMBO_SDCARD_SIZE, + + CHECK_FORCE_CREATION, + CHECK_KEYBOARD, + CHECK_SNAPSHOT, + CHECK_GPU_EMUL, + RADIO_SDCARD_SIZE, + RADIO_SDCARD_FILE, + + TEXT_AVD_NAME, + TEXT_RAM, + TEXT_VM_HEAP, + TEXT_DATA_PART, + TEXT_SDCARD_SIZE, + TEXT_SDCARD_FILE, + + ICON_STATUS, + TEXT_STATUS, + } + + /** Interface exposed by the view. Presenter calls this to update UI. */ + public interface IWidgetAdapter { + void setTitle(@NonNull String title); + int getComboIndex(@NonNull Ctrl ctrl); + int getComboSize (@NonNull Ctrl ctrl); + void selectComboIndex(@NonNull Ctrl ctrl, int index); + String getComboItem (@NonNull Ctrl ctrl, int index); + void addComboItem (@NonNull Ctrl ctrl, String label); + /** Set combo labels or clear combo if labels is null. */ + void setComboItems(@NonNull Ctrl ctrl, @Nullable String[] labels); + boolean isEnabled(@NonNull Ctrl ctrl); + void setEnabled(@NonNull Ctrl ctrl, boolean enabled); + boolean isChecked(@NonNull Ctrl ctrl); + void setChecked(@NonNull Ctrl ctrl, boolean checked); + String getText(@NonNull Ctrl ctrl); + void setText(@NonNull Ctrl ctrl, String text); + void setImage(@NonNull Ctrl ctrl, @Nullable String imageName); + String openFileDialog(@NonNull String string); + void repack(); + + /** + * Creates a MessageBoxLog, a special MessageBox that returns a logger instance + * to capture logs. The caller then performs actions and outputs to the logger + * and at the end calls MessageBoxLog.displayResult() to create a message box + * with the result of the logged output. + */ + IMessageBoxLogger newDelayedMessageBoxLog(String title, boolean logErrorsOnly); + + } + + public AvdCreationPresenter(@NonNull AvdManager avdManager, + @NonNull ILogger log, + @Nullable AvdInfo editAvdInfo) { + mAvdManager = avdManager; + mSdkLog = log; + mAvdInfo = editAvdInfo; + } + + /** Returns the AVD Created, if successful. */ + public AvdInfo getCreatedAvd() { + return mCreatedAvd; + } + + /** Called by the view constructor to set the view updater. */ + public void setWidgetAdapter(@NonNull IWidgetAdapter widgetAdapter) { + mWidgets = widgetAdapter; + } + + /** + * Can be called after the constructor to set the default device for this AVD. + * Useful especially for new AVDs. + */ + public void selectInitialDevice(@NonNull Device device) { + mInitWithDevice = device; + } + + public void onViewInit() { + mWidgets.setTitle( + mAvdInfo == null ? "Create new Android Virtual Device (AVD)" + : "Edit Android Virtual Device (AVD)"); + initializeDevices(); + + // setup the 2 default choices (no skin, dynamic skin); do not select any right now. + mComboSkins.add(SKIN_DYNAMIC); + mComboSkins.add(SKIN_NONE); + Collections.sort(mComboSkins); + mWidgets.addComboItem(Ctrl.COMBO_SKIN, mComboSkins.get(0).getLabel()); + mWidgets.addComboItem(Ctrl.COMBO_SKIN, mComboSkins.get(1).getLabel()); + + // Preload target combo *after* ABI/Tag and Skin combos have been setup as + // they will be setup depending on the selected target. + preloadTargetCombo(); + + toggleCameras(); + + enableSdCardWidgets(true); + + + if (mAvdInfo != null) { + fillExistingAvdInfo(mAvdInfo); + } else if (mInitWithDevice != null) { + fillInitialDeviceInfo(mInitWithDevice); + } + + validatePage(); + } + + //------- + + + + private void initializeDevices() { + LocalSdk localSdk = mAvdManager.getLocalSdk(); + File location = localSdk.getLocation(); + if (location != null) { + DeviceManager deviceManager = DeviceManager.createInstance(location, mSdkLog); + Collection deviceList = deviceManager.getDevices(DeviceManager.ALL_DEVICES); + + // Sort + List nexus = new ArrayList(deviceList.size()); + List other = new ArrayList(deviceList.size()); + for (Device device : deviceList) { + if (isNexus(device) && !isGeneric(device)) { + nexus.add(device); + } else { + other.add(device); + } + } + Collections.reverse(other); + Collections.sort(nexus, new Comparator() { + @Override + public int compare(Device device1, Device device2) { + // Descending order of age + return nexusRank(device2) - nexusRank(device1); + } + }); + + mComboDevices.clear(); + mComboDevices.addAll(nexus); + mComboDevices.addAll(other); + + String[] labels = new String[mComboDevices.size()]; + for (int i = 0, n = mComboDevices.size(); i < n; i++) { + Device device = mComboDevices.get(i); + if (isNexus(device) && !isGeneric(device)) { + labels[i] = getNexusLabel(device); + } else { + labels[i] = getGenericLabel(device); + } + } + + mWidgets.setComboItems(Ctrl.COMBO_DEVICE, labels); + } + } + + @Nullable + private Device getSelectedDevice() { + int index = mWidgets.getComboIndex(Ctrl.COMBO_DEVICE); + if (index != -1 && index < mComboDevices.size()) { + return mComboDevices.get(index); + } + + return null; + } + + /** Called by fillExisting/InitialDeviceInfo to select the device in the combo list. */ + @SuppressWarnings("deprecation") + private void selectDevice(String manufacturer, String name) { + for (int i = 0, n = mComboDevices.size(); i < n; i++) { + Device device = mComboDevices.get(i); + if (device.getManufacturer().equals(manufacturer) + && device.getName().equals(name)) { + mWidgets.selectComboIndex(Ctrl.COMBO_DEVICE, i); + break; + } + } + } + + /** Called by fillExisting/InitialDeviceInfo to select the device in the combo list. */ + private void selectDevice(Device device) { + for (int i = 0, n = mComboDevices.size(); i < n; i++) { + if (mComboDevices.get(i).equals(device)) { + mWidgets.selectComboIndex(Ctrl.COMBO_DEVICE, i); + break; + } + } + } + + void onDeviceComboChanged() { + Device currentDevice = getSelectedDevice(); + if (currentDevice != null) { + fillDeviceProperties(currentDevice); + } + + toggleCameras(); + validatePage(); + } + + void onAvdNameModified() { + String name = mWidgets.getText(Ctrl.TEXT_AVD_NAME).trim(); + if (mAvdInfo == null || !name.equals(mAvdInfo.getName())) { + // Case where we're creating a new AVD or editing an existing one + // and the AVD name has been changed... check for name uniqueness. + + Pair conflict = mAvdManager.isAvdNameConflicting(name); + if (conflict.getFirst() != AvdManager.AvdConflict.NO_CONFLICT) { + // If we're changing the state from disabled to enabled, make sure + // to uncheck the button, to force the user to voluntarily re-enforce it. + // This happens when editing an existing AVD and changing the name from + // the existing AVD to another different existing AVD. + if (!mWidgets.isEnabled(Ctrl.CHECK_FORCE_CREATION)) { + mWidgets.setEnabled(Ctrl.CHECK_FORCE_CREATION, true); + mWidgets.setChecked(Ctrl.CHECK_FORCE_CREATION, false); + } + } else { + mWidgets.setEnabled(Ctrl.CHECK_FORCE_CREATION, false); + mWidgets.setChecked(Ctrl.CHECK_FORCE_CREATION, false); + } + } else { + // Case where we're editing an existing AVD with the name unchanged. + mWidgets.setEnabled(Ctrl.CHECK_FORCE_CREATION, false); + mWidgets.setChecked(Ctrl.CHECK_FORCE_CREATION, false); + } + validatePage(); + + } + + + + private void fillDeviceProperties(Device device) { + Hardware hw = device.getDefaultHardware(); + Long ram = hw.getRam().getSizeAsUnit(Storage.Unit.MiB); + mWidgets.setText(Ctrl.TEXT_RAM, Long.toString(ram)); + + // Set the default VM heap size. This is based on the Android CDD minimums for each + // screen size and density. + Screen s = hw.getScreen(); + ScreenSize size = s.getSize(); + Density density = s.getPixelDensity(); + int vmHeapSize = 32; + if (size.equals(ScreenSize.XLARGE)) { + switch (density) { + case LOW: + case MEDIUM: + vmHeapSize = 32; + break; + case TV: + case HIGH: + vmHeapSize = 64; + break; + case XHIGH: + case XXHIGH: + case XXXHIGH: + vmHeapSize = 128; + break; + case NODPI: + break; + } + } else { + switch (density) { + case LOW: + case MEDIUM: + vmHeapSize = 16; + break; + case TV: + case HIGH: + vmHeapSize = 32; + break; + case XHIGH: + case XXHIGH: + case XXXHIGH: + vmHeapSize = 64; + break; + case NODPI: + break; + } + } + mWidgets.setText(Ctrl.TEXT_VM_HEAP, Integer.toString(vmHeapSize)); + + boolean reloadTabAbiCombo = false; + + List allSoftware = device.getAllSoftware(); + if (allSoftware != null && !allSoftware.isEmpty()) { + Software first = allSoftware.get(0); + int min = first.getMinSdkLevel();; + int max = first.getMaxSdkLevel();; + for (int i = 1; i < allSoftware.size(); i++) { + min = Math.min(min, first.getMinSdkLevel()); + max = Math.max(max, first.getMaxSdkLevel()); + } + if (mCurrentTargets != null) { + int bestApiLevel = Integer.MAX_VALUE; + IAndroidTarget bestTarget = null; + for (IAndroidTarget target : mCurrentTargets.values()) { + if (!target.isPlatform()) { + continue; + } + int apiLevel = target.getVersion().getApiLevel(); + if (apiLevel >= min && apiLevel <= max) { + if (bestTarget == null || apiLevel < bestApiLevel) { + bestTarget = target; + bestApiLevel = apiLevel; + } + } + } + + if (bestTarget != null) { + selectTarget(bestTarget); + reloadTabAbiCombo = true; + } + } + } + + if (!reloadTabAbiCombo) { + String deviceTagId = device.getTagId(); + Pair currTagAbi = getSelectedTagAbi(); + if (deviceTagId != null && + (currTagAbi == null || !deviceTagId.equals(currTagAbi.getFirst().getId()))) { + reloadTabAbiCombo = true; + } + } + + if (reloadTabAbiCombo) { + reloadTagAbiCombo(); + } + } + + private void toggleCameras() { + mWidgets.setEnabled(Ctrl.COMBO_FRONT_CAM, false); + mWidgets.setEnabled(Ctrl.COMBO_BACK_CAM, false); + Device d = getSelectedDevice(); + if (d != null) { + for (Camera c : d.getDefaultHardware().getCameras()) { + if (CameraLocation.FRONT.equals(c.getLocation())) { + mWidgets.setEnabled(Ctrl.COMBO_FRONT_CAM, true); + } + if (CameraLocation.BACK.equals(c.getLocation())) { + mWidgets.setEnabled(Ctrl.COMBO_BACK_CAM, true); + } + } + } + } + + private void preloadTargetCombo() { + String selected = null; + int index = mWidgets.getComboIndex(Ctrl.COMBO_TARGET); + if (index >= 0) { + selected = mWidgets.getComboItem(Ctrl.COMBO_TARGET, index); + } + + mCurrentTargets.clear(); + mWidgets.setComboItems(Ctrl.COMBO_TARGET, null); + + boolean found = false; + index = -1; + + mComboTargets.clear(); + LocalSdk localSdk = mAvdManager.getLocalSdk(); + if (localSdk != null) { + for (IAndroidTarget target : localSdk.getTargets()) { + String name; + if (target.isPlatform()) { + name = String.format("%s - API Level %s", + target.getName(), + target.getVersion().getApiString()); + } else { + name = String.format("%s (%s) - API Level %s", + target.getName(), + target.getVendor(), + target.getVersion().getApiString()); + } + mCurrentTargets.put(name, target); + mWidgets.addComboItem(Ctrl.COMBO_TARGET, name); + mComboTargets.add(target); + if (!found) { + index++; + found = name.equals(selected); + } + } + } + + mWidgets.setEnabled(Ctrl.COMBO_TARGET, mCurrentTargets.size() > 0); + + if (found) { + mWidgets.selectComboIndex(Ctrl.COMBO_TARGET, index); + } + + reloadTagAbiCombo(); + } + + private void selectTarget(IAndroidTarget target) { + for (int i = 0, n = mComboTargets.size(); i < n; i++) { + if (target == mComboTargets.get(i)) { + mWidgets.selectComboIndex(Ctrl.COMBO_TARGET, i); + break; + } + } + } + + private IAndroidTarget getSelectedTarget() { + int index = mWidgets.getComboIndex(Ctrl.COMBO_TARGET); + if (index != -1 && index < mComboTargets.size()) { + return mComboTargets.get(index); + } + + return null; + } + + /** + * Reload all the abi types in the selection list. + * Also adds/remove the skin choices embedded in a tag/abi, if any. + */ + void reloadTagAbiCombo() { + + int index = mWidgets.getComboIndex(Ctrl.COMBO_TARGET); + if (index >= 0) { + String targetName = mWidgets.getComboItem(Ctrl.COMBO_TARGET, index); + IAndroidTarget target = mCurrentTargets.get(targetName); + + ISystemImage[] systemImages = getSystemImages(target); + + // If user explicitly selected an ABI before, preserve that option + // If user did not explicitly select before (only one option before) + // force them to select + String selected = null; + index = mWidgets.getComboIndex(Ctrl.COMBO_TAG_ABI); + if (index >= 0 && mWidgets.getComboSize(Ctrl.COMBO_TAG_ABI) > 1) { + selected = mWidgets.getComboItem(Ctrl.COMBO_TAG_ABI, index); + } + + // if there's a selected device that requires a specific non-default tag-id, + // filter the list to only match this tag. + Device currDevice = getSelectedDevice(); + String deviceTagId = currDevice == null ? null : currDevice.getTagId(); + if (deviceTagId != null && + (deviceTagId.isEmpty() || SystemImage.DEFAULT_TAG.equals(deviceTagId))) { + deviceTagId = null; + } + + // filter and create the list + mWidgets.setComboItems(Ctrl.COMBO_TAG_ABI, null); + mComboSystemImages.clear(); + + int i; + boolean found = false; + for (i = 0; i < systemImages.length; i++) { + ISystemImage systemImage = systemImages[i]; + if (deviceTagId != null && !deviceTagId.equals(systemImage.getTag().getId())) { + continue; + } + mComboSystemImages.add(systemImage); + String prettyAbiType = AvdInfo.getPrettyAbiType(systemImage); + mWidgets.addComboItem(Ctrl.COMBO_TAG_ABI, prettyAbiType); + if (!found) { + found = prettyAbiType.equals(selected); + if (found) { + mWidgets.selectComboIndex(Ctrl.COMBO_TAG_ABI, i); + } + } + } + + mWidgets.setEnabled(Ctrl.COMBO_TAG_ABI, !mComboSystemImages.isEmpty()); + + if (mComboSystemImages.isEmpty()) { + mWidgets.addComboItem(Ctrl.COMBO_TAG_ABI, "No system images installed for this target."); + mWidgets.selectComboIndex(Ctrl.COMBO_TAG_ABI, 0); + } else if (mComboSystemImages.size() == 1) { + mWidgets.selectComboIndex(Ctrl.COMBO_TAG_ABI, 0); + } + } + + reloadSkinCombo(); + } + + void reloadSkinCombo() { + AvdSkinChoice selected = getSelectedSkinChoice(); + + // Remove existing target & tag skins + for (Iterator it = mComboSkins.iterator(); it.hasNext(); ) { + AvdSkinChoice choice = it.next(); + if (choice.hasPath()) { + it.remove(); + } + } + + IAndroidTarget target = getSelectedTarget(); + if (target != null) { + ISystemImage sysImg = getSelectedSysImg(); + Set sysImgSkins = new HashSet(); + if (sysImg != null) { + sysImgSkins.addAll(Arrays.asList(sysImg.getSkins())); + } + + // path of sdk/system-images + String sdkSysImgPath = new File(mAvdManager.getLocalSdk().getLocation(), + SdkConstants.FD_SYSTEM_IMAGES).getAbsolutePath(); + + for (File skin : target.getSkins()) { + String label = skin.getName(); + String skinPath = skin.getAbsolutePath(); + if (skinPath.startsWith(sdkSysImgPath)) { + if (sysImg == null) { + // Reject a sys-img based skin if no sys img is selected + continue; + } + if (!sysImgSkins.contains(skin)) { + // If a skin comes from a tagged system-image, only display + // those matching the current system image. + continue; + } + if (!SystemImage.DEFAULT_TAG.equals(sysImg.getTag().getId())) { + // Append the tag name if it's not the similar to the label. + String display = sysImg.getTag().getDisplay(); + String azDisplay = display.toLowerCase(Locale.US).replaceAll("[^a-z]", ""); + String azLabel = label .toLowerCase(Locale.US).replaceAll("[^a-z]", ""); + if (!azLabel.contains(azDisplay)) { + label = String.format("%s (%s)", label, display); + } + } + } + AvdSkinChoice sc = new AvdSkinChoice(SkinType.FROM_TARGET, label, skin); + mComboSkins.add(sc); + } + } + + Collections.sort(mComboSkins); + + mWidgets.setComboItems(Ctrl.COMBO_SKIN, null); + for (int i = 0; i < mComboSkins.size(); i++) { + AvdSkinChoice choice = mComboSkins.get(i); + mWidgets.addComboItem(Ctrl.COMBO_SKIN, choice.getLabel()); + if (choice == selected) { + mWidgets.selectComboIndex(Ctrl.COMBO_SKIN, i); + } + } + + } + + /** + * Enable or disable the sd card widgets. + * + * @param sizeMode if true the size-based widgets are to be enabled, and the + * file-based ones disabled. + */ + void enableSdCardWidgets(boolean sizeMode) { + mWidgets.setEnabled(Ctrl.TEXT_SDCARD_SIZE, sizeMode); + mWidgets.setEnabled(Ctrl.COMBO_SDCARD_SIZE, sizeMode); + + mWidgets.setEnabled(Ctrl.TEXT_SDCARD_FILE, !sizeMode); + mWidgets.setEnabled(Ctrl.BUTTON_BROWSE_SDCARD, !sizeMode); + } + + void onBrowseSdCard() { + String fileName = mWidgets. openFileDialog("Choose SD Card image file."); + if (fileName != null) { + mWidgets.setText(Ctrl.TEXT_SDCARD_FILE, fileName); + } + validatePage(); + } + + void validatePage() { + String error = null; + ArrayList warnings = new ArrayList(); + boolean valid = true; + + String avdName = mWidgets.getText(Ctrl.TEXT_AVD_NAME); + + if (avdName.isEmpty()) { + error = "AVD Name cannot be empty"; + setPageValid(false, error, null); + return; + } + + if (!AvdManager.RE_AVD_NAME.matcher(avdName).matches()) { + error = String.format( + "AVD name '%1$s' contains invalid characters.\nAllowed characters are: %2$s", + avdName, AvdManager.CHARS_AVD_NAME); + setPageValid(false, error, null); + return; + } + + if (mWidgets.getComboIndex(Ctrl.COMBO_DEVICE) < 0) { + error = "No device selected"; + setPageValid(false, error, null); + return; + } + + if (mWidgets.getComboIndex(Ctrl.COMBO_TARGET) < 0) { + error = "No target selected"; + setPageValid(false, error, null); + return; + } + + if (mComboSystemImages.isEmpty()) { + error = "No CPU/ABI system image available for this target"; + setPageValid(false, error, null); + return; + } else if (getSelectedTagAbi() == null) { + error = "No CPU/ABI system image selected"; + setPageValid(false, error, null); + return; + } + + // If the target is an addon, check its base platform requirement is satisfied. + String targetName = mWidgets.getComboItem(Ctrl.COMBO_TARGET, mWidgets.getComboIndex(Ctrl.COMBO_TARGET)); + IAndroidTarget target = mCurrentTargets.get(targetName); + if (target != null && !target.isPlatform()) { + + ISystemImage[] sis = target.getSystemImages(); + if (sis != null && sis.length > 0) { + // Note: if an addon has no system-images of its own, it depends on its parent + // platform and it wouldn't have been loaded properly if the platform were + // missing so we don't need to double-check that part here. + + Pair tagAbi = getSelectedTagAbi(); + IdDisplay tag = tagAbi.getFirst(); + String abiType = tagAbi.getSecond(); + if (abiType != null && + !abiType.isEmpty() && + target.getParent().getSystemImage(tag, abiType) == null) { + // We have a system-image requirement but there is no such system image + // loaded in the parent platform. This AVD won't run properly. + warnings.add( + String.format( + "This AVD may not work unless you install the %1$s system image " + + "for %2$s (%3$s) first.", + AvdInfo.getPrettyAbiType(tag, abiType), + target.getParent().getName(), + target.getParent().getVersion().toString())); + } + } + } + + AvdSkinChoice skinChoice = getSelectedSkinChoice(); + if (skinChoice == null) { + error = "No skin selected"; + setPageValid(false, error, null); + return; + } + + if (mWidgets.getText(Ctrl.TEXT_RAM).isEmpty()) { + error = "Mising RAM value"; + setPageValid(false, error, null); + return; + } + + if (mWidgets.getText(Ctrl.TEXT_VM_HEAP).isEmpty()) { + error = "Mising VM Heap value"; + setPageValid(false, error, null); + return; + } + + if (mWidgets.getText(Ctrl.TEXT_DATA_PART).isEmpty() || mWidgets.getComboIndex(Ctrl.COMBO_DATA_PART_SIZE) < 0) { + error = "Invalid Data partition size."; + setPageValid(false, error, null); + return; + } + + // validate sdcard size or file + if (mWidgets.isChecked(Ctrl.RADIO_SDCARD_SIZE)) { + if (!mWidgets.getText(Ctrl.TEXT_SDCARD_SIZE).isEmpty() && mWidgets.getComboIndex(Ctrl.COMBO_SDCARD_SIZE) >= 0) { + try { + long sdSize = Long.parseLong(mWidgets.getText(Ctrl.TEXT_SDCARD_SIZE)); + + int sizeIndex = mWidgets.getComboIndex(Ctrl.COMBO_SDCARD_SIZE); + if (sizeIndex >= 0) { + // index 0 shifts by 10 (1024=K), index 1 by 20, etc. + sdSize <<= 10 * (1 + sizeIndex); + } + + if (sdSize < AvdManager.SDCARD_MIN_BYTE_SIZE || + sdSize > AvdManager.SDCARD_MAX_BYTE_SIZE) { + valid = false; + error = "SD Card size is invalid. Range is 9 MiB..1023 GiB."; + } + } catch (NumberFormatException e) { + valid = false; + error = " SD Card size must be a valid integer between 9 MiB and 1023 GiB"; + } + } + } else { + if (mWidgets.getText(Ctrl.TEXT_SDCARD_FILE).isEmpty() || + !new File(mWidgets.getText(Ctrl.TEXT_SDCARD_FILE)).isFile()) { + valid = false; + error = "SD Card path isn't valid."; + } + } + if (!valid) { + setPageValid(valid, error, null); + return; + } + + if (mWidgets.isEnabled(Ctrl.CHECK_FORCE_CREATION) && !mWidgets.isChecked(Ctrl.CHECK_FORCE_CREATION)) { + valid = false; + error = String.format( + "The AVD name '%s' is already used.\n" + + "Check \"Override the existing AVD\" to delete the existing one.", + mWidgets.getText(Ctrl.TEXT_AVD_NAME)); + } + + if (mAvdInfo != null && !mAvdInfo.getName().equals(mWidgets.getText(Ctrl.TEXT_AVD_NAME))) { + warnings.add( + String.format("The AVD '%1$s' will be duplicated into '%2$s'.", + mAvdInfo.getName(), + mWidgets.getText(Ctrl.TEXT_AVD_NAME))); + } + + // On Windows, display a warning if attempting to create AVD's with RAM > 512 MB. + // This restriction should go away when we switch to using a 64 bit emulator. + if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) { + long ramSize = 0; + try { + ramSize = Long.parseLong(mWidgets.getText(Ctrl.TEXT_RAM)); + } catch (NumberFormatException e) { + // ignore + } + + if (ramSize > 768) { + warnings.add( + "On Windows, emulating RAM greater than 768M may fail depending on the" + + " system load. Try progressively smaller values of RAM if the emulator" + + " fails to launch."); + } + } + + if (mWidgets.isChecked(Ctrl.CHECK_GPU_EMUL) && mWidgets.isChecked(Ctrl.CHECK_SNAPSHOT)) { + valid = false; + error = "GPU Emulation and Snapshot cannot be used simultaneously"; + } + + String warning = Joiner.on('\n').join(warnings); + setPageValid(valid, error, warning); + return; + } + + private void setPageValid(boolean valid, String error, String warning) { + mWidgets.setEnabled(Ctrl.BUTTON_OK, valid); + if (error != null && !error.isEmpty()) { + mWidgets.setImage(Ctrl.ICON_STATUS, "reject_icon16.png"); //$NON-NLS-1$ + mWidgets.setText(Ctrl.TEXT_STATUS, error); + } else if (warning != null && !warning.isEmpty()) { + mWidgets.setImage(Ctrl.ICON_STATUS, "warning_icon16.png"); //$NON-NLS-1$ + mWidgets.setText(Ctrl.TEXT_STATUS, warning); + } else { + mWidgets.setImage(Ctrl.ICON_STATUS, null); + mWidgets.setText(Ctrl.TEXT_STATUS, " \n "); //$NON-NLS-1$ + } + + mWidgets.repack(); + } + + boolean createAvd() { + String avdName = mWidgets.getText(Ctrl.TEXT_AVD_NAME); + if (avdName == null || avdName.isEmpty()) { + return false; + } + + String targetName = mWidgets.getComboItem(Ctrl.COMBO_TARGET, mWidgets.getComboIndex(Ctrl.COMBO_TARGET)); + IAndroidTarget target = mCurrentTargets.get(targetName); + if (target == null) { + return false; + } + + // get the tag & abi type + Pair tagAbi = getSelectedTagAbi(); + if (tagAbi == null) { + return false; + } + IdDisplay tag = tagAbi.getFirst(); + String abiType = tagAbi.getSecond(); + + // get the SD card data from the UI. + String sdName = null; + if (mWidgets.isChecked(Ctrl.RADIO_SDCARD_SIZE)) { + // size mode + String value = mWidgets.getText(Ctrl.TEXT_SDCARD_SIZE).trim(); + if (value.length() > 0) { + sdName = value; + // add the unit + switch (mWidgets.getComboIndex(Ctrl.COMBO_SDCARD_SIZE)) { + case 0: + sdName += "K"; //$NON-NLS-1$ + break; + case 1: + sdName += "M"; //$NON-NLS-1$ + break; + case 2: + sdName += "G"; //$NON-NLS-1$ + break; + default: + // shouldn't be here + assert false; + } + } + } else { + // file mode. + sdName = mWidgets.getText(Ctrl.TEXT_SDCARD_FILE).trim(); + } + + // Get the device + Device device = getSelectedDevice(); + if (device == null) { + return false; + } + + File skinFolder = null; + String skinName = null; + AvdSkinChoice skinChoice = getSelectedSkinChoice(); + if (skinChoice == null) { + return false; + } + if (skinChoice.hasPath()) { + skinFolder = skinChoice.getPath(); + } else { + Screen s = device.getDefaultHardware().getScreen(); + skinName = s.getXDimension() + "x" + s.getYDimension(); + } + + ILogger log = mSdkLog; + if (log == null || log instanceof MessageBoxLog) { + // If the current logger is a message box, we use our own (to make sure + // to display errors right away and customize the title). + log = mWidgets.newDelayedMessageBoxLog( + String.format("Result of creating AVD '%s':", avdName), + false /* logErrorsOnly */); + } + + Map hwProps = DeviceManager.getHardwareProperties(device); + if (mWidgets.isChecked(Ctrl.CHECK_GPU_EMUL)) { + hwProps.put(AvdManager.AVD_INI_GPU_EMULATION, HardwareProperties.BOOLEAN_YES); + } + + File avdFolder = null; + try { + avdFolder = AvdInfo.getDefaultAvdFolder(mAvdManager, avdName); + } catch (AndroidLocationException e) { + return false; + } + + // Although the device has this information, some devices have more RAM than we'd want to + // allocate to an emulator. + hwProps.put(AvdManager.AVD_INI_RAM_SIZE, mWidgets.getText(Ctrl.TEXT_RAM)); + hwProps.put(AvdManager.AVD_INI_VM_HEAP_SIZE, mWidgets.getText(Ctrl.TEXT_VM_HEAP)); + + String suffix; + switch (mWidgets.getComboIndex(Ctrl.COMBO_DATA_PART_SIZE)) { + case 0: + suffix = "M"; + break; + case 1: + suffix = "G"; + break; + default: + suffix = "K"; + } + hwProps.put(AvdManager.AVD_INI_DATA_PARTITION_SIZE, + mWidgets.getText(Ctrl.TEXT_DATA_PART) + suffix); + + hwProps.put(HardwareProperties.HW_KEYBOARD, + mWidgets.isChecked(Ctrl.CHECK_KEYBOARD) ? + HardwareProperties.BOOLEAN_YES : HardwareProperties.BOOLEAN_NO); + + hwProps.put(AvdManager.AVD_INI_SKIN_DYNAMIC, + skinChoice.getType() == SkinType.DYNAMIC ? + HardwareProperties.BOOLEAN_YES : HardwareProperties.BOOLEAN_NO); + + if (mWidgets.isEnabled(Ctrl.COMBO_FRONT_CAM)) { + hwProps.put(AvdManager.AVD_INI_CAMERA_FRONT, + mWidgets.getText(Ctrl.COMBO_FRONT_CAM).toLowerCase()); + } + + if (mWidgets.isEnabled(Ctrl.COMBO_BACK_CAM)) { + hwProps.put(AvdManager.AVD_INI_CAMERA_BACK, + mWidgets.getText(Ctrl.COMBO_BACK_CAM).toLowerCase()); + } + + if (sdName != null) { + hwProps.put(HardwareProperties.HW_SDCARD, HardwareProperties.BOOLEAN_YES); + } + + AvdInfo avdInfo = mAvdManager.createAvd(avdFolder, + avdName, + target, + tag, + abiType, + skinFolder, + skinName, + sdName, + hwProps, + device.getBootProps(), + mWidgets.isChecked(Ctrl.CHECK_SNAPSHOT), + mWidgets.isChecked(Ctrl.CHECK_FORCE_CREATION), + mAvdInfo != null, // edit existing + log); + + mCreatedAvd = avdInfo; + boolean success = avdInfo != null; + + if (log instanceof IMessageBoxLogger) { + ((IMessageBoxLogger) log).displayResult(success); + } + return success; + } + + @Nullable + private AvdSkinChoice getSelectedSkinChoice() { + int choiceIndex = mWidgets.getComboIndex(Ctrl.COMBO_SKIN); + if (choiceIndex >= 0 && choiceIndex < mComboSkins.size()) { + return mComboSkins.get(choiceIndex); + } + return null; + } + + @Nullable + private Pair getSelectedTagAbi() { + ISystemImage selected = getSelectedSysImg(); + if (selected != null) { + return Pair.of(selected.getTag(), selected.getAbiType()); + } + return null; + } + + @Nullable + private ISystemImage getSelectedSysImg() { + if (!mComboSystemImages.isEmpty()) { + int abiIndex = mWidgets.getComboIndex(Ctrl.COMBO_TAG_ABI); + if (abiIndex >= 0 && abiIndex < mComboSystemImages.size()) { + return mComboSystemImages.get(abiIndex); + } + } + return null; + } + + private void fillExistingAvdInfo(AvdInfo avd) { + mWidgets.setText(Ctrl.TEXT_AVD_NAME, avd.getName()); + selectDevice(avd.getDeviceManufacturer(), avd.getDeviceName()); + toggleCameras(); + + IAndroidTarget target = avd.getTarget(); + + if (target != null && !mCurrentTargets.isEmpty()) { + // Try to select the target in the target combo. + // This will fail if the AVD needs to be repaired. + // + // This is a linear search but the list is always + // small enough and we only do this once. + int n = mWidgets.getComboSize(Ctrl.COMBO_TARGET); + for (int i = 0; i < n; i++) { + if (target.equals(mCurrentTargets.get(mWidgets.getComboItem(Ctrl.COMBO_TARGET, i)))) { + // Note: combo.select does not trigger the combo's widgetSelected callback. + mWidgets.selectComboIndex(Ctrl.COMBO_TARGET, i); + reloadTagAbiCombo(); + break; + } + } + } + + ISystemImage[] systemImages = getSystemImages(target); + if (target != null && systemImages.length > 0) { + mWidgets.setEnabled(Ctrl.COMBO_TAG_ABI, systemImages.length > 1); + String abiType = AvdInfo.getPrettyAbiType(avd.getTag(), avd.getAbiType()); + int n = mWidgets.getComboSize(Ctrl.COMBO_TAG_ABI); + for (int i = 0; i < n; i++) { + if (abiType.equals(mWidgets.getComboItem(Ctrl.COMBO_TAG_ABI, i))) { + mWidgets.selectComboIndex(Ctrl.COMBO_TAG_ABI, i); + reloadSkinCombo(); + break; + } + } + } + + Map props = avd.getProperties(); + + if (props != null) { + String snapshots = props.get(AvdManager.AVD_INI_SNAPSHOT_PRESENT); + if (snapshots != null && snapshots.length() > 0) { + mWidgets.setChecked(Ctrl.CHECK_SNAPSHOT, snapshots.equals("true")); + } + + String gpuEmulation = props.get(AvdManager.AVD_INI_GPU_EMULATION); + mWidgets.setChecked(Ctrl.CHECK_GPU_EMUL, gpuEmulation != null && + gpuEmulation.equals(HardwareProperties.BOOLEAN_YES)); + + String sdcard = props.get(AvdManager.AVD_INI_SDCARD_PATH); + if (sdcard != null && sdcard.length() > 0) { + enableSdCardWidgets(false); + mWidgets.setChecked(Ctrl.RADIO_SDCARD_SIZE, false); + mWidgets.setChecked(Ctrl.RADIO_SDCARD_FILE, true); + mWidgets.setText(Ctrl.TEXT_SDCARD_FILE, sdcard); + } + + String ramSize = props.get(AvdManager.AVD_INI_RAM_SIZE); + if (ramSize != null) { + mWidgets.setText(Ctrl.TEXT_RAM, ramSize); + } + + String vmHeapSize = props.get(AvdManager.AVD_INI_VM_HEAP_SIZE); + if (vmHeapSize != null) { + mWidgets.setText(Ctrl.TEXT_VM_HEAP, vmHeapSize); + } + + String dataPartitionSize = props.get(AvdManager.AVD_INI_DATA_PARTITION_SIZE); + if (dataPartitionSize != null) { + mWidgets.setText(Ctrl.TEXT_DATA_PART, + dataPartitionSize.substring(0, dataPartitionSize.length() - 1)); + switch (dataPartitionSize.charAt(dataPartitionSize.length() - 1)) { + case 'M': + mWidgets.selectComboIndex(Ctrl.COMBO_DATA_PART_SIZE, 0); + break; + case 'G': + mWidgets.selectComboIndex(Ctrl.COMBO_DATA_PART_SIZE, 1); + break; + default: + mWidgets.selectComboIndex(Ctrl.COMBO_DATA_PART_SIZE, -1); + } + } + + mWidgets.setChecked(Ctrl.CHECK_KEYBOARD, + HardwareProperties.BOOLEAN_YES.equalsIgnoreCase( + props.get(HardwareProperties.HW_KEYBOARD))); + + SkinType defaultSkinType = SkinType.NONE; + // the AVD .ini skin path is relative to the SDK folder *or* is a numeric size. + String skinIniPath = props.get(AvdManager.AVD_INI_SKIN_PATH); + if (skinIniPath != null) { + File skinFolder = new File(mAvdManager.getLocalSdk().getLocation(), skinIniPath); + + for (int i = 0; i < mComboSkins.size(); i++) { + if (mComboSkins.get(i).hasPath() && + skinFolder.equals(mComboSkins.get(i).getPath())) { + mWidgets.selectComboIndex(Ctrl.COMBO_SKIN, i); + defaultSkinType = null; + break; + } + } + } + + if (defaultSkinType != null) { + if (HardwareProperties.BOOLEAN_YES.equalsIgnoreCase( + props.get(AvdManager.AVD_INI_SKIN_DYNAMIC))) { + defaultSkinType = SkinType.DYNAMIC; + } + + for (int i = 0; i < mComboSkins.size(); i++) { + if (mComboSkins.get(i).getType() == defaultSkinType) { + mWidgets.selectComboIndex(Ctrl.COMBO_SKIN, i); + break; + } + } + + } + + String cameraFront = props.get(AvdManager.AVD_INI_CAMERA_FRONT); + if (cameraFront != null) { + for (int i = 0, n = mWidgets.getComboSize(Ctrl.COMBO_FRONT_CAM); i < n; i++) { + String item = mWidgets.getComboItem(Ctrl.COMBO_FRONT_CAM, i); + if (item.toLowerCase().equals(cameraFront)) { + mWidgets.selectComboIndex(Ctrl.COMBO_FRONT_CAM, i); + break; + } + } + } + + String cameraBack = props.get(AvdManager.AVD_INI_CAMERA_BACK); + if (cameraBack != null) { + for (int i = 0, n = mWidgets.getComboSize(Ctrl.COMBO_BACK_CAM); i < n; i++) { + String item = mWidgets.getComboItem(Ctrl.COMBO_BACK_CAM, i); + if (item.toLowerCase().equals(cameraBack)) { + mWidgets.selectComboIndex(Ctrl.COMBO_BACK_CAM, i); + break; + } + } + } + + sdcard = props.get(AvdManager.AVD_INI_SDCARD_SIZE); + if (sdcard != null && sdcard.length() > 0) { + String[] values = new String[2]; + long sdcardSize = AvdManager.parseSdcardSize(sdcard, values); + + if (sdcardSize != AvdManager.SDCARD_NOT_SIZE_PATTERN) { + enableSdCardWidgets(true); + mWidgets.setChecked(Ctrl.RADIO_SDCARD_FILE, false); + mWidgets.setChecked(Ctrl.RADIO_SDCARD_SIZE, true); + + mWidgets.setText(Ctrl.TEXT_SDCARD_SIZE, values[0]); + + String suffix = values[1]; + int n = mWidgets.getComboSize(Ctrl.COMBO_SDCARD_SIZE); + for (int i = 0; i < n; i++) { + if (mWidgets.getComboItem(Ctrl.COMBO_SDCARD_SIZE, i).startsWith(suffix)) { + mWidgets.selectComboIndex(Ctrl.COMBO_SDCARD_SIZE, i); + } + } + } + } + } + } + + @SuppressWarnings("deprecation") + private void fillInitialDeviceInfo(Device device) { + String name = device.getManufacturer(); + if (!name.equals("Generic") && // TODO define & use constants + !name.equals("User") && + device.getName().indexOf(name) == -1) { + name = " by " + name; + } else { + name = ""; + } + name = "AVD for " + device.getName() + name; + // sanitize the name + name = name.replaceAll("[^0-9a-zA-Z_-]+", " ").trim().replaceAll("[ _]+", "_"); + mWidgets.setText(Ctrl.TEXT_AVD_NAME, name); + + // Select the device + selectDevice(device); + toggleCameras(); + + // If there's only one target, select it by default. + // TODO: if there are more than 1 target, select the higher platform target as + // a likely default. + if (mWidgets.getComboSize(Ctrl.COMBO_TARGET) == 1) { + mWidgets.selectComboIndex(Ctrl.COMBO_TARGET, 0); + reloadTagAbiCombo(); + } + + fillDeviceProperties(device); + } + + /** + * Returns the list of system images of a target. + *

+ * If target is null, returns an empty list. If target is an add-on with no + * system images, return the list from its parent platform. + * + * @param target An IAndroidTarget. Can be null. + * @return A non-null ISystemImage array. Can be empty. + */ + @NonNull + private ISystemImage[] getSystemImages(IAndroidTarget target) { + if (target != null) { + ISystemImage[] images = target.getSystemImages(); + + if ((images == null || images.length == 0) && !target.isPlatform()) { + // This is an add-on and it does not provide any system image. + + // Before LMP / Tools 23.0.4, the behavior was to provide the + // parent (platform) system-image using this code: + // + // images = target.getParent().getSystemImages(); + // + // After tools 23.0.4, the behavior is to NOT provide the + // platform system-image for the add-on. + } + + if (images != null) { + return images; + } + } + + return new ISystemImage[0]; + } + + // Code copied from DeviceMenuListener in ADT; unify post release + + private static final String NEXUS = "Nexus"; //$NON-NLS-1$ + private static final String GENERIC = "Generic"; //$NON-NLS-1$ + private static Pattern PATTERN = Pattern.compile( + "(\\d+\\.?\\d*)in (.+?)( \\(.*Nexus.*\\))?"); //$NON-NLS-1$ + + private static int nexusRank(Device device) { + @SuppressWarnings("deprecation") + String name = device.getName(); + if (name.endsWith(" One")) { //$NON-NLS-1$ + return 1; + } + if (name.endsWith(" S")) { //$NON-NLS-1$ + return 2; + } + if (name.startsWith("Galaxy")) { //$NON-NLS-1$ + return 3; + } + if (name.endsWith(" 7")) { //$NON-NLS-1$ + return 4; + } + if (name.endsWith(" 10")) { //$NON-NLS-1$ + return 5; + } + if (name.endsWith(" 4")) { //$NON-NLS-1$ + return 6; + } + + return 7; + } + + private static boolean isGeneric(Device device) { + return device.getManufacturer().equals(GENERIC); + } + + @SuppressWarnings("deprecation") + private static boolean isNexus(Device device) { + return device.getName().contains(NEXUS); + } + + private static String getGenericLabel(Device d) { + // * Replace "'in'" with '"' (e.g. 2.7" QVGA instead of 2.7in QVGA) + // * Use the same precision for all devices (all but one specify decimals) + // * Add some leading space such that the dot ends up roughly in the + // same space + // * Add in screen resolution and density + @SuppressWarnings("deprecation") + String name = d.getName(); + if (name.equals("3.7 FWVGA slider")) { //$NON-NLS-1$ + // Fix metadata: this one entry doesn't have "in" like the rest of them + name = "3.7in FWVGA slider"; //$NON-NLS-1$ + } + + Matcher matcher = PATTERN.matcher(name); + if (matcher.matches()) { + String size = matcher.group(1); + String n = matcher.group(2); + int dot = size.indexOf('.'); + if (dot == -1) { + size = size + ".0"; + dot = size.length() - 2; + } + for (int i = 0; i < 2 - dot; i++) { + size = ' ' + size; + } + name = size + "\" " + n; + } + + return String.format(java.util.Locale.US, "%1$s (%2$s)", name, + getResolutionString(d)); + } + + private static String getNexusLabel(Device d) { + @SuppressWarnings("deprecation") + String name = d.getName(); + Screen screen = d.getDefaultHardware().getScreen(); + float length = (float) screen.getDiagonalLength(); + return String.format(java.util.Locale.US, "%1$s (%3$s\", %2$s)", + name, getResolutionString(d), Float.toString(length)); + } + + @Nullable + private static String getResolutionString(Device device) { + Screen screen = device.getDefaultHardware().getScreen(); + return String.format(java.util.Locale.US, + "%1$d \u00D7 %2$d: %3$s", // U+00D7: Unicode multiplication sign + screen.getXDimension(), + screen.getYDimension(), + screen.getPixelDensity().getResourceValue()); + } + + //------- + + + /** + * AVD skin type. Order defines the order of the skin combo list. + */ + private enum SkinType { + DYNAMIC, + NONE, + FROM_TARGET, + } + + /* + * Choice of AVD skin: dynamic, no skin, or one from the target. + * The 2 "internals" skins (dynamic and no skin) have no path. + * The target-based skins have a path. + */ + private static class AvdSkinChoice implements Comparable { + + private final SkinType mType; + private final String mLabel; + private final File mPath; + + AvdSkinChoice(@NonNull SkinType type, @NonNull String label) { + this(type, label, null); + } + + AvdSkinChoice(@NonNull SkinType type, @NonNull String label, @NonNull File path) { + mType = type; + mLabel = label; + mPath = path; + } + + @NonNull + public SkinType getType() { + return mType; + } + + @NonNull + public String getLabel() { + return mLabel; + } + + @Nullable + public File getPath() { + return mPath; + } + + public boolean hasPath() { + return mType == SkinType.FROM_TARGET; + } + + @Override + public int compareTo(AvdSkinChoice o) { + int t = mType.compareTo(o.mType); + if (t == 0) { + t = mLabel.compareTo(o.mLabel); + } + if (t == 0 && mPath != null && o.mPath != null) { + t = mPath.compareTo(o.mPath); + } + return t; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((mType == null) ? 0 : mType.hashCode()); + result = prime * result + ((mLabel == null) ? 0 : mLabel.hashCode()); + result = prime * result + ((mPath == null) ? 0 : mPath.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof AvdSkinChoice)) { + return false; + } + AvdSkinChoice other = (AvdSkinChoice) obj; + if (mType != other.mType) { + return false; + } + if (mLabel == null) { + if (other.mLabel != null) { + return false; + } + } else if (!mLabel.equals(other.mLabel)) { + return false; + } + if (mPath == null) { + if (other.mPath != null) { + return false; + } + } else if (!mPath.equals(other.mPath)) { + return false; + } + return true; + } + + + } + + public void onTargetComboChanged() { + reloadTagAbiCombo(); + validatePage(); + } + + public void onTagComboChanged() { + reloadSkinCombo(); + validatePage(); + } + + public void onRadioSdCardSizeChanged() { + boolean sizeMode = mWidgets.isChecked(Ctrl.RADIO_SDCARD_SIZE); + enableSdCardWidgets(sizeMode); + validatePage(); + } + + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationSwtView.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationSwtView.java new file mode 100644 index 00000000..dd2cef89 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationSwtView.java @@ -0,0 +1,609 @@ +/* + * Copyright (C) 2014 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.sdkuilib.internal.widgets; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdkuilib.internal.repository.icons.ImageFactory; +import com.android.sdkuilib.internal.widgets.AvdCreationPresenter.Ctrl; +import com.android.sdkuilib.internal.widgets.AvdCreationPresenter.IWidgetAdapter; +import com.android.sdkuilib.ui.GridDialog; + +import org.eclipse.jface.dialogs.IDialogConstants; +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.events.VerifyEvent; +import org.eclipse.swt.events.VerifyListener; +import org.eclipse.swt.graphics.Point; +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.FileDialog; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +import java.util.HashMap; +import java.util.Map; + +/** + * Creates the SWT shell and controls for the {@link AvdCreationDialog}. + * All the logic is handled by the {@link AvdCreationPresenter}. + * + * @see AvdCreationPresenter + */ +class AvdCreationSwtView extends GridDialog { + + private final ImageFactory mImageFactory; + private final AvdCreationPresenter mPresenter; + + private Button mBtnOK; + + private Text mTextAvdName; + + private Combo mComboDevice; + + private Combo mComboTarget; + private Combo mComboTagAbi; + + private Button mCheckKeyboard; + private Combo mComboSkinCombo; + + private Combo mComboFrontCamera; + private Combo mComboBackCamera; + + private Button mCheckSnapshot; + private Button mCheckGpuEmulation; + + private Text mTextRam; + private Text mTextVmHeap; + + private Text mTextDataPartition; + private Combo mComboDataPartitionSize; + + private Button mRadioSdCardSize; + private Text mTextSdCardSize; + private Combo mComboSdCardSize; + private Button mRadioSdCardFile; + private Text mTextSdCardFile; + private Button mBtnBrowseSdCard; + + private Button mCheckForceCreation; + private Composite mCompositeStatus; + + private Label mIconStatus; + private Label mTextStatus; + + private final Map mControlMap = new HashMap(); + + /** + * {@link VerifyListener} for {@link Text} widgets that should only contains + * numbers. + */ + private final VerifyListener mDigitVerifier = new VerifyListener() { + @Override + public void verifyText(VerifyEvent event) { + int count = event.text.length(); + for (int i = 0; i < count; i++) { + char c = event.text.charAt(i); + if (c < '0' || c > '9') { + event.doit = false; + return; + } + } + } + }; + + public AvdCreationSwtView(Shell shell, + @NonNull ImageFactory imageFactory, + @NonNull AvdCreationPresenter presenter) { + super(shell, 2, false); + mImageFactory = imageFactory; + mPresenter = presenter; + setShellStyle(getShellStyle() | SWT.RESIZE); + + mPresenter.setWidgetAdapter(new IWidgetAdapter() { + @Override + public void setTitle(@NonNull String title) { + getShell().setText(title); + } + + @Override + public int getComboIndex(@NonNull Ctrl ctrl) { + Control c = mControlMap.get(ctrl); + if (c instanceof Combo) { + return ((Combo) c).getSelectionIndex(); + } + return -1; + } + + @Override + public int getComboSize(@NonNull Ctrl ctrl) { + Control c = mControlMap.get(ctrl); + if (c instanceof Combo) { + return ((Combo) c).getItemCount(); + } + return 0; + } + + @Nullable + @Override + public String getComboItem(@NonNull Ctrl ctrl, int index) { + Control c = mControlMap.get(ctrl); + if (c instanceof Combo) { + return ((Combo) c).getItem(index); + } + return null; + } + + @Override + public void selectComboIndex(@NonNull Ctrl ctrl, int index) { + Control c = mControlMap.get(ctrl); + if (c instanceof Combo) { + ((Combo) c).select(index); + } + } + + @Override + public void addComboItem(@NonNull Ctrl ctrl, String label) { + Control c = mControlMap.get(ctrl); + if (c instanceof Combo) { + ((Combo) c).add(label); + } + } + + @Override + public void setComboItems(@NonNull Ctrl ctrl, String[] labels) { + Control c = mControlMap.get(ctrl); + if (c instanceof Combo) { + Combo combo = ((Combo) c); + combo.removeAll(); + if (labels != null) { + combo.setItems(labels); + } + } + } + + @Override + public boolean isEnabled(@NonNull Ctrl ctrl) { + Control c = mControlMap.get(ctrl); + if (c != null) { + return c.isEnabled(); + } + return false; + } + + @Override + public void setEnabled(@NonNull Ctrl ctrl, boolean enabled) { + Control c = mControlMap.get(ctrl); + if (c != null) { + c.setEnabled(enabled); + } + } + + @Override + public boolean isChecked(@NonNull Ctrl ctrl) { + Control c = mControlMap.get(ctrl); + if (c instanceof Button) { + return ((Button) c).getSelection(); + } + return false; + } + + @Override + public void setChecked(@NonNull Ctrl ctrl, boolean checked) { + Control c = mControlMap.get(ctrl); + if (c instanceof Button) { + ((Button) c).setSelection(checked); + } + } + + @Override + public String getText(@NonNull Ctrl ctrl) { + Control c = mControlMap.get(ctrl); + if (c instanceof Text) { + return ((Text) c).getText(); + } else if (c instanceof Combo) { + return ((Combo) c).getText(); + } else if (c instanceof Label) { + return ((Label) c).getText(); + } else if (c instanceof Button) { + return ((Button) c).getText(); + } + return null; + } + + @Override + public void setText(@NonNull Ctrl ctrl, @NonNull String text) { + Control c = mControlMap.get(ctrl); + if (c instanceof Text) { + ((Text) c).setText(text); + } else if (c instanceof Combo) { + ((Combo) c).setText(text); + } else if (c instanceof Label) { + ((Label) c).setText(text); + } else if (c instanceof Button) { + ((Button) c).setText(text); + } + } + + @Override + public void setImage(@NonNull Ctrl ctrl, @Nullable String imageName) { + Control c = mControlMap.get(ctrl); + if (c instanceof Label) { + ((Label) c).setImage( + imageName == null ? null : mImageFactory.getImageByName(imageName)); + } + } + + @Nullable + @Override + public String openFileDialog(@NonNull String title) { + FileDialog dlg = new FileDialog(getContents().getShell(), SWT.OPEN); + dlg.setText(title); + return dlg.open(); + } + + @Override + public void repack() { + mCompositeStatus.pack(true); + getShell().layout(true, true); + } + + @Override + public IMessageBoxLogger newDelayedMessageBoxLog(String title, boolean logErrorsOnly) { + return new MessageBoxLog(title, getContents().getDisplay(), logErrorsOnly); + } + }); + } + + @NonNull + public AvdCreationPresenter getPresenter() { + return mPresenter; + } + + @Override + protected Control createContents(Composite parent) { + // super.createContents will call createDialogContent() + // below and then continue here. + Control control = super.createContents(parent); + getShell().setMinimumSize(new Point(350, 600)); + + mBtnOK = getButton(IDialogConstants.OK_ID); + + registerControlMap(); + mPresenter.onViewInit(); + + return control; + } + + @Override + public void createDialogContent(Composite parent) { + Label label; + String tooltip; + ValidateListener validateListener = new ValidateListener(); + + // --- avd name + label = new Label(parent, SWT.NONE); + label.setText("AVD Name:"); + tooltip = "The name of the Android Virtual Device"; + label.setToolTipText(tooltip); + mTextAvdName = new Text(parent, SWT.BORDER); + mTextAvdName.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mTextAvdName.addModifyListener(new CreateNameModifyListener()); + + // --- device selection + label = new Label(parent, SWT.NONE); + label.setText("Device:"); + tooltip = "The device this AVD will be based on"; + mComboDevice = new Combo(parent, SWT.READ_ONLY | SWT.DROP_DOWN); + mComboDevice.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mComboDevice.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + mPresenter.onDeviceComboChanged(); + } + }); + + // --- api target + label = new Label(parent, SWT.NONE); + label.setText("Target:"); + tooltip = "The target API of the AVD"; + label.setToolTipText(tooltip); + mComboTarget = new Combo(parent, SWT.READ_ONLY | SWT.DROP_DOWN); + mComboTarget.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mComboTarget.setToolTipText(tooltip); + mComboTarget.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mPresenter.onTargetComboChanged(); + } + }); + + // --- avd ABIs + label = new Label(parent, SWT.NONE); + label.setText("CPU/ABI:"); + tooltip = "The CPU/ABI of the virtual device"; + label.setToolTipText(tooltip); + mComboTagAbi = new Combo(parent, SWT.READ_ONLY | SWT.DROP_DOWN); + mComboTagAbi.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mComboTagAbi.setToolTipText(tooltip); + mComboTagAbi.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mPresenter.onTagComboChanged(); + } + }); + + label = new Label(parent, SWT.NONE); + label.setText("Keyboard:"); + mCheckKeyboard = new Button(parent, SWT.CHECK); + mCheckKeyboard.setSelection(true); // default to having a keyboard irrespective of device + mCheckKeyboard.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mCheckKeyboard.setText("Hardware keyboard present"); + + // --- skins + label = new Label(parent, SWT.NONE); + label.setText("Skin:"); + mComboSkinCombo = new Combo(parent, SWT.READ_ONLY | SWT.DROP_DOWN); + mComboSkinCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mComboSkinCombo.addSelectionListener(validateListener); + + // --- camera + label = new Label(parent, SWT.NONE); + label.setText("Front Camera:"); + tooltip = ""; + label.setToolTipText(tooltip); + mComboFrontCamera = new Combo(parent, SWT.READ_ONLY | SWT.DROP_DOWN); + mComboFrontCamera.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mComboFrontCamera.add("None"); + mComboFrontCamera.add("Emulated"); + mComboFrontCamera.add("Webcam0"); + mComboFrontCamera.select(0); + + label = new Label(parent, SWT.NONE); + label.setText("Back Camera:"); + tooltip = ""; + label.setToolTipText(tooltip); + mComboBackCamera = new Combo(parent, SWT.READ_ONLY | SWT.DROP_DOWN); + mComboBackCamera.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mComboBackCamera.add("None"); + mComboBackCamera.add("Emulated"); + mComboBackCamera.add("Webcam0"); + mComboBackCamera.select(0); + + // --- memory options group + label = new Label(parent, SWT.NONE); + label.setText("Memory Options:"); + + Group memoryGroup = new Group(parent, SWT.NONE); + memoryGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + memoryGroup.setLayout(new GridLayout(4, false)); + + label = new Label(memoryGroup, SWT.NONE); + label.setText("RAM:"); + tooltip = "The amount of RAM the emulated device should have in MiB"; + label.setToolTipText(tooltip); + mTextRam = new Text(memoryGroup, SWT.BORDER); + mTextRam.addVerifyListener(mDigitVerifier); + mTextRam.addModifyListener(validateListener); + mTextRam.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + label = new Label(memoryGroup, SWT.NONE); + label.setText("VM Heap:"); + tooltip = "The amount of memory, in MiB, available to typical Android applications"; + label.setToolTipText(tooltip); + mTextVmHeap = new Text(memoryGroup, SWT.BORDER); + mTextVmHeap.addVerifyListener(mDigitVerifier); + mTextVmHeap.addModifyListener(validateListener); + mTextVmHeap.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mTextVmHeap.setToolTipText(tooltip); + + // --- Data partition group + label = new Label(parent, SWT.NONE); + label.setText("Internal Storage:"); + tooltip = "The size of the data partition on the device."; + Group storageGroup = new Group(parent, SWT.NONE); + storageGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + storageGroup.setLayout(new GridLayout(2, false)); + mTextDataPartition = new Text(storageGroup, SWT.BORDER); + mTextDataPartition.setText("200"); + mTextDataPartition.addVerifyListener(mDigitVerifier); + mTextDataPartition.addModifyListener(validateListener); + mTextDataPartition.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mComboDataPartitionSize = new Combo(storageGroup, SWT.READ_ONLY | SWT.DROP_DOWN); + mComboDataPartitionSize.add("MiB"); + mComboDataPartitionSize.add("GiB"); + mComboDataPartitionSize.select(0); + mComboDataPartitionSize.addModifyListener(validateListener); + + // --- sd card group + label = new Label(parent, SWT.NONE); + label.setText("SD Card:"); + label.setLayoutData(new GridData(GridData.BEGINNING, GridData.BEGINNING, + false, false)); + + final Group sdCardGroup = new Group(parent, SWT.NONE); + sdCardGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + sdCardGroup.setLayout(new GridLayout(3, false)); + + mRadioSdCardSize = new Button(sdCardGroup, SWT.RADIO); + mRadioSdCardSize.setText("Size:"); + mRadioSdCardSize.setToolTipText("Create a new SD Card file"); + mRadioSdCardSize.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + mPresenter.onRadioSdCardSizeChanged(); + } + }); + + mTextSdCardSize = new Text(sdCardGroup, SWT.BORDER); + mTextSdCardSize.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mTextSdCardSize.addVerifyListener(mDigitVerifier); + mTextSdCardSize.addModifyListener(validateListener); + mTextSdCardSize.setToolTipText("Size of the new SD Card file (must be at least 9 MiB)"); + + mComboSdCardSize = new Combo(sdCardGroup, SWT.DROP_DOWN | SWT.READ_ONLY); + mComboSdCardSize.add("KiB"); + mComboSdCardSize.add("MiB"); + mComboSdCardSize.add("GiB"); + mComboSdCardSize.select(1); + mComboSdCardSize.addSelectionListener(validateListener); + + mRadioSdCardFile = new Button(sdCardGroup, SWT.RADIO); + mRadioSdCardFile.setText("File:"); + mRadioSdCardFile.setToolTipText("Use an existing file for the SD Card"); + + mTextSdCardFile = new Text(sdCardGroup, SWT.BORDER); + mTextSdCardFile.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mTextSdCardFile.addModifyListener(validateListener); + mTextSdCardFile.setToolTipText("File to use for the SD Card"); + + mBtnBrowseSdCard = new Button(sdCardGroup, SWT.PUSH); + mBtnBrowseSdCard.setText("Browse..."); + mBtnBrowseSdCard.setToolTipText("Select the file to use for the SD Card"); + mBtnBrowseSdCard.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + mPresenter.onBrowseSdCard(); + } + }); + + mRadioSdCardSize.setSelection(true); + + // --- avd options group + label = new Label(parent, SWT.NONE); + label.setText("Emulation Options:"); + Group optionsGroup = new Group(parent, SWT.NONE); + optionsGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + optionsGroup.setLayout(new GridLayout(2, true)); + mCheckSnapshot = new Button(optionsGroup, SWT.CHECK); + mCheckSnapshot.setText("Snapshot"); + mCheckSnapshot.setToolTipText("Emulator's state will be persisted between emulator executions"); + mCheckSnapshot.addSelectionListener(validateListener); + mCheckGpuEmulation = new Button(optionsGroup, SWT.CHECK); + mCheckGpuEmulation.setText("Use Host GPU"); + mCheckGpuEmulation.setToolTipText("Enable hardware OpenGLES emulation"); + mCheckGpuEmulation.addSelectionListener(validateListener); + + // --- force creation group + mCheckForceCreation = new Button(parent, SWT.CHECK); + mCheckForceCreation.setText("Override the existing AVD with the same name"); + mCheckForceCreation + .setToolTipText("There's already an AVD with the same name. Check this to delete it and replace it by the new AVD."); + mCheckForceCreation.setLayoutData(new GridData(GridData.BEGINNING, GridData.CENTER, + true, false, 2, 1)); + mCheckForceCreation.setEnabled(false); + mCheckForceCreation.addSelectionListener(validateListener); + + // add a separator to separate from the ok/cancel button + label = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL); + label.setLayoutData(new GridData(GridData.FILL, GridData.CENTER, true, false, 3, 1)); + + // add stuff for the error display + mCompositeStatus = new Composite(parent, SWT.NONE); + mCompositeStatus.setLayoutData(new GridData(GridData.FILL, GridData.CENTER, + true, false, 3, 1)); + GridLayout gl; + mCompositeStatus.setLayout(gl = new GridLayout(2, false)); + gl.marginHeight = gl.marginWidth = 0; + + mIconStatus = new Label(mCompositeStatus, SWT.NONE); + mIconStatus.setLayoutData(new GridData(GridData.BEGINNING, GridData.BEGINNING, + false, false)); + mTextStatus = new Label(mCompositeStatus, SWT.WRAP); + GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, false, 1, 1); + // allow for approx 3 lines of text corresponding to the number of lines in the longest + // error or warning + gridData.heightHint = 50; + mTextStatus.setLayoutData(gridData); + mTextStatus.setText(""); //$NON-NLS-1$ + } + + private void registerControlMap() { + mControlMap.put(Ctrl.BUTTON_OK, mBtnOK); + mControlMap.put(Ctrl.BUTTON_BROWSE_SDCARD, mBtnBrowseSdCard); + + mControlMap.put(Ctrl.COMBO_DEVICE, mComboDevice); + mControlMap.put(Ctrl.COMBO_TARGET, mComboTarget); + mControlMap.put(Ctrl.COMBO_TAG_ABI, mComboTagAbi); + mControlMap.put(Ctrl.COMBO_SKIN, mComboSkinCombo); + mControlMap.put(Ctrl.COMBO_FRONT_CAM, mComboFrontCamera); + mControlMap.put(Ctrl.COMBO_BACK_CAM, mComboBackCamera); + mControlMap.put(Ctrl.COMBO_DATA_PART_SIZE, mComboDataPartitionSize); + mControlMap.put(Ctrl.COMBO_SDCARD_SIZE, mComboSdCardSize); + + mControlMap.put(Ctrl.CHECK_FORCE_CREATION, mCheckForceCreation); + mControlMap.put(Ctrl.CHECK_KEYBOARD, mCheckKeyboard); + mControlMap.put(Ctrl.CHECK_SNAPSHOT, mCheckSnapshot); + mControlMap.put(Ctrl.CHECK_GPU_EMUL, mCheckGpuEmulation); + mControlMap.put(Ctrl.RADIO_SDCARD_SIZE, mRadioSdCardSize); + mControlMap.put(Ctrl.RADIO_SDCARD_FILE, mRadioSdCardFile); + + mControlMap.put(Ctrl.TEXT_AVD_NAME, mTextAvdName); + mControlMap.put(Ctrl.TEXT_RAM, mTextRam); + mControlMap.put(Ctrl.TEXT_VM_HEAP, mTextVmHeap); + mControlMap.put(Ctrl.TEXT_DATA_PART, mTextDataPartition); + mControlMap.put(Ctrl.TEXT_SDCARD_SIZE, mTextSdCardSize); + mControlMap.put(Ctrl.TEXT_SDCARD_FILE, mTextSdCardFile); + + mControlMap.put(Ctrl.ICON_STATUS, mIconStatus); + mControlMap.put(Ctrl.TEXT_STATUS, mTextStatus); + } + + /** + * {@link ModifyListener} used for live-validation of the fields content. + */ + private class ValidateListener extends SelectionAdapter implements ModifyListener { + @Override + public void modifyText(ModifyEvent e) { + mPresenter.validatePage(); + } + + @Override + public void widgetSelected(SelectionEvent e) { + super.widgetSelected(e); + mPresenter.validatePage(); + } + } + + /** + * Callback when the AVD name is changed. When creating a new AVD, enables + * the force checkbox if the name is a duplicate. When editing an existing + * AVD, it's OK for the name to match the existing AVD. + */ + private class CreateNameModifyListener implements ModifyListener { + @Override + public void modifyText(ModifyEvent e) { + mPresenter.onAvdNameModified(); + } + } + + @Override + public void okPressed() { + if (mPresenter.createAvd()) { + super.okPressed(); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdDetailsDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdDetailsDialog.java new file mode 100644 index 00000000..2f6b8800 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdDetailsDialog.java @@ -0,0 +1,162 @@ +/* + * 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.sdkuilib.internal.widgets; + +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.internal.avd.AvdInfo; +import com.android.sdklib.internal.avd.AvdInfo.AvdStatus; +import com.android.sdklib.internal.avd.AvdManager; +import com.android.sdkuilib.ui.GridDataBuilder; +import com.android.sdkuilib.ui.GridLayoutBuilder; +import com.android.sdkuilib.ui.SwtBaseDialog; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; + +import java.util.HashMap; +import java.util.Map; + +/** + * Dialog displaying the details of an AVD. + */ +final class AvdDetailsDialog extends SwtBaseDialog { + + private final AvdInfo mAvdInfo; + + public AvdDetailsDialog(Shell shell, AvdInfo avdInfo) { + super(shell, SWT.APPLICATION_MODAL, "AVD details"); + mAvdInfo = avdInfo; + } + + /** + * Create contents of the dialog. + */ + @Override + protected void createContents() { + Shell shell = getShell(); + GridLayoutBuilder.create(shell).columns(2); + GridDataBuilder.create(shell).fill(); + + GridLayout gl; + + Composite c = new Composite(shell, SWT.NONE); + c.setLayout(gl = new GridLayout(2, false)); + gl.marginHeight = gl.marginWidth = 0; + c.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + if (mAvdInfo != null) { + displayValue(c, "Name:", mAvdInfo.getName()); + displayValue(c, "CPU/ABI:", AvdInfo.getPrettyAbiType(mAvdInfo)); + + displayValue(c, "Path:", mAvdInfo.getDataFolderPath()); + + if (mAvdInfo.getStatus() != AvdStatus.OK) { + displayValue(c, "Error:", mAvdInfo.getErrorMessage()); + } else { + IAndroidTarget target = mAvdInfo.getTarget(); + AndroidVersion version = target.getVersion(); + displayValue(c, "Target:", String.format("%s (API level %s)", + target.getName(), version.getApiString())); + + // display some extra values. + Map properties = mAvdInfo.getProperties(); + if (properties != null) { + String skin = properties.get(AvdManager.AVD_INI_SKIN_NAME); + if (skin != null) { + displayValue(c, "Skin:", skin); + } + + String sdcard = properties.get(AvdManager.AVD_INI_SDCARD_SIZE); + if (sdcard == null) { + sdcard = properties.get(AvdManager.AVD_INI_SDCARD_PATH); + } + if (sdcard != null) { + displayValue(c, "SD Card:", sdcard); + } + + String snapshot = properties.get(AvdManager.AVD_INI_SNAPSHOT_PRESENT); + if (snapshot != null) { + displayValue(c, "Snapshot:", snapshot); + } + + // display other hardware + HashMap copy = new HashMap(properties); + // remove stuff we already displayed (or that we don't want to display) + copy.remove(AvdManager.AVD_INI_ABI_TYPE); + copy.remove(AvdManager.AVD_INI_CPU_ARCH); + copy.remove(AvdManager.AVD_INI_SKIN_NAME); + copy.remove(AvdManager.AVD_INI_SKIN_PATH); + copy.remove(AvdManager.AVD_INI_SDCARD_SIZE); + copy.remove(AvdManager.AVD_INI_SDCARD_PATH); + copy.remove(AvdManager.AVD_INI_IMAGES_1); + copy.remove(AvdManager.AVD_INI_IMAGES_2); + + if (copy.size() > 0) { + Label l = new Label(shell, SWT.SEPARATOR | SWT.HORIZONTAL); + l.setLayoutData(new GridData( + GridData.FILL, GridData.CENTER, false, false, 2, 1)); + + c = new Composite(shell, SWT.NONE); + c.setLayout(gl = new GridLayout(2, false)); + gl.marginHeight = gl.marginWidth = 0; + c.setLayoutData(new GridData(GridData.FILL_BOTH)); + + for (Map.Entry entry : copy.entrySet()) { + displayValue(c, entry.getKey() + ":", entry.getValue()); + } + } + } + } + } + } + + // -- Start of internal part ---------- + // Hide everything down-below from SWT designer + //$hide>>$ + + + @Override + protected void postCreate() { + // pass + } + + /** + * Displays a value with a label. + * + * @param parent the parent Composite in which to display the value. This Composite must use a + * {@link GridLayout} with 2 columns. + * @param label the label of the value to display. + * @param value the string value to display. + */ + private void displayValue(Composite parent, String label, String value) { + Label l = new Label(parent, SWT.NONE); + l.setText(label); + l.setLayoutData(new GridData(GridData.END, GridData.CENTER, false, false)); + + l = new Label(parent, SWT.NONE); + l.setText(value); + l.setLayoutData(new GridData(GridData.FILL, GridData.CENTER, true, false)); + } + + // End of hiding from SWT Designer + //$hide<<$ +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdSelector.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdSelector.java new file mode 100644 index 00000000..7d05df97 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdSelector.java @@ -0,0 +1,1263 @@ +/* + * 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.sdkuilib.internal.widgets; + +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.prefs.AndroidLocation.AndroidLocationException; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.SystemImage; +import com.android.sdklib.internal.avd.AvdInfo; +import com.android.sdklib.internal.avd.AvdInfo.AvdStatus; +import com.android.sdklib.internal.avd.AvdManager; +import com.android.sdklib.internal.repository.ITask; +import com.android.sdklib.internal.repository.ITaskMonitor; +import com.android.sdklib.internal.repository.updater.SettingsController; +import com.android.sdklib.repository.descriptors.IdDisplay; +import com.android.sdkuilib.internal.repository.icons.ImageFactory; +import com.android.sdkuilib.internal.repository.icons.ImageFactory.Filter; +import com.android.sdkuilib.internal.repository.ui.AvdManagerWindowImpl1; +import com.android.sdkuilib.internal.tasks.ProgressTask; +import com.android.sdkuilib.repository.AvdManagerWindow.AvdInvocationContext; +import com.android.sdkuilib.ui.GridDialog; +import com.android.utils.GrabProcessOutput; +import com.android.utils.GrabProcessOutput.IProcessOutput; +import com.android.utils.GrabProcessOutput.Wait; +import com.android.utils.ILogger; +import com.android.utils.NullLogger; + +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.viewers.DecorationOverlayIcon; +import org.eclipse.jface.viewers.IDecoration; +import org.eclipse.jface.window.Window; +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.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Image; +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.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.TableItem; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Formatter; +import java.util.Locale; + + +/** + * The AVD selector is a table that is added to the given parent composite. + *

+ * After using one of the constructors, call {@link #setSelection(AvdInfo)}, + * {@link #setSelectionListener(SelectionListener)} and finally use + * {@link #getSelected()} to retrieve the selection. + */ +public final class AvdSelector { + private static int NUM_COL = 2; + + private final DisplayMode mDisplayMode; + + private AvdManager mAvdManager; + private final String mOsSdkPath; // TODO consider converting to File later + + private Table mTable; + private Button mDeleteButton; + private Button mDetailsButton; + private Button mNewButton; + private Button mEditButton; + private Button mRefreshButton; + private Button mManagerButton; + private Button mRepairButton; + private Button mStartButton; + + private SelectionListener mSelectionListener; + private IAvdFilter mTargetFilter; + + /** Defaults to true. Changed by the {@link #setEnabled(boolean)} method to represent the + * "global" enabled state on this composite. */ + private boolean mIsEnabled = true; + + private ImageFactory mImageFactory; + private Image mBrokenImage; + private Image mInvalidImage; + + private SettingsController mController; + + private final ILogger mSdkLog; + + private boolean mInternalRefresh; + + + /** + * The display mode of the AVD Selector. + */ + public static enum DisplayMode { + /** + * Manager mode. Invalid AVDs are displayed. Buttons to create/delete AVDs + */ + MANAGER, + + /** + * Non manager mode. Only valid AVDs are displayed. Cannot create/delete AVDs, but + * there is a button to open the AVD Manager. + * In the "check" selection mode, checkboxes are displayed on each line + * and {@link AvdSelector#getSelected()} returns the line that is checked + * even if it is not the currently selected line. Only one line can + * be checked at once. + */ + SIMPLE_CHECK, + + /** + * Non manager mode. Only valid AVDs are displayed. Cannot create/delete AVDs, but + * there is a button to open the AVD Manager. + * In the "select" selection mode, there are no checkboxes and + * {@link AvdSelector#getSelected()} returns the line currently selected. + * Only one line can be selected at once. + */ + SIMPLE_SELECTION, + } + + /** + * A filter to control the whether or not an AVD should be displayed by the AVD Selector. + */ + public interface IAvdFilter { + /** + * Called before {@link #accept(AvdInfo)} is called for any AVD. + */ + void prepare(); + + /** + * Called to decided whether an AVD should be displayed. + * @param avd the AVD to test. + * @return true if the AVD should be displayed. + */ + boolean accept(AvdInfo avd); + + /** + * Called after {@link #accept(AvdInfo)} has been called on all the AVDs. + */ + void cleanup(); + } + + /** + * Internal implementation of {@link IAvdFilter} to filter out the AVDs that are not + * running an image compatible with a specific target. + */ + private final static class TargetBasedFilter implements IAvdFilter { + private final IAndroidTarget mTarget; + + TargetBasedFilter(IAndroidTarget target) { + mTarget = target; + } + + @Override + public void prepare() { + // nothing to prepare + } + + @Override + public boolean accept(AvdInfo avd) { + if (avd != null) { + return mTarget.canRunOn(avd.getTarget()); + } + + return false; + } + + @Override + public void cleanup() { + // nothing to clean up + } + } + + /** + * Creates a new SDK Target Selector, and fills it with a list of {@link AvdInfo}, filtered + * by a {@link IAndroidTarget}. + *

Only the {@link AvdInfo} able to run application developed for the given + * {@link IAndroidTarget} will be displayed. + * + * @param parent The parent composite where the selector will be added. + * @param osSdkPath The SDK root path. When not null, enables the start button to start + * an emulator on a given AVD. + * @param manager the AVD manager. + * @param filter When non-null, will allow filtering the AVDs to display. + * @param displayMode The display mode ({@link DisplayMode}). + * @param sdkLog The logger. Cannot be null. + */ + public AvdSelector(Composite parent, + String osSdkPath, + AvdManager manager, + IAvdFilter filter, + DisplayMode displayMode, + ILogger sdkLog) { + mOsSdkPath = osSdkPath; + mAvdManager = manager; + mTargetFilter = filter; + mDisplayMode = displayMode; + mSdkLog = sdkLog; + + // get some bitmaps. + mImageFactory = new ImageFactory(parent.getDisplay()); + mBrokenImage = mImageFactory.getImageByName("warning_icon16.png"); + mInvalidImage = mImageFactory.getImageByName("reject_icon16.png"); + + // Layout has 2 columns + Composite group = new Composite(parent, SWT.NONE); + GridLayout gl; + group.setLayout(gl = new GridLayout(NUM_COL, false /*makeColumnsEqualWidth*/)); + gl.marginHeight = gl.marginWidth = 0; + group.setLayoutData(new GridData(GridData.FILL_BOTH)); + group.setFont(parent.getFont()); + group.addDisposeListener(new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent arg0) { + mImageFactory.dispose(); + } + }); + + int style = SWT.FULL_SELECTION | SWT.SINGLE | SWT.BORDER; + if (displayMode == DisplayMode.SIMPLE_CHECK) { + style |= SWT.CHECK; + } + mTable = new Table(group, style); + mTable.setHeaderVisible(true); + mTable.setLinesVisible(false); + setTableHeightHint(0); + + Composite buttons = new Composite(group, SWT.NONE); + buttons.setLayout(gl = new GridLayout(1, false /*makeColumnsEqualWidth*/)); + gl.marginHeight = gl.marginWidth = 0; + buttons.setLayoutData(new GridData(GridData.FILL_VERTICAL)); + buttons.setFont(group.getFont()); + + if (displayMode == DisplayMode.MANAGER) { + mNewButton = new Button(buttons, SWT.PUSH | SWT.FLAT); + mNewButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mNewButton.setText("Create..."); + mNewButton.setToolTipText("Creates a new AVD."); + mNewButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + onNew(); + } + }); + } + + + mStartButton = new Button(buttons, SWT.PUSH | SWT.FLAT); + mStartButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mStartButton.setText("Start..."); + mStartButton.setToolTipText("Starts the selected AVD."); + mStartButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + onStart(); + } + }); + + @SuppressWarnings("unused") + Label spacing = new Label(buttons, SWT.NONE); + + if (displayMode == DisplayMode.MANAGER) { + mEditButton = new Button(buttons, SWT.PUSH | SWT.FLAT); + mEditButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mEditButton.setText("Edit..."); + mEditButton.setToolTipText("Edit an existing AVD."); + mEditButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + onEdit(); + } + }); + + mRepairButton = new Button(buttons, SWT.PUSH | SWT.FLAT); + mRepairButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mRepairButton.setText("Repair..."); + mRepairButton.setToolTipText("Repairs the selected AVD."); + mRepairButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + onRepair(); + } + }); + + mDeleteButton = new Button(buttons, SWT.PUSH | SWT.FLAT); + mDeleteButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mDeleteButton.setText("Delete..."); + mDeleteButton.setToolTipText("Deletes the selected AVD."); + mDeleteButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + onDelete(); + } + }); + } + + mDetailsButton = new Button(buttons, SWT.PUSH | SWT.FLAT); + mDetailsButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mDetailsButton.setText("Details..."); + mDetailsButton.setToolTipText("Displays details of the selected AVD."); + mDetailsButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + onDetails(); + } + }); + + Composite padding = new Composite(buttons, SWT.NONE); + padding.setLayoutData(new GridData(GridData.FILL_VERTICAL)); + + mRefreshButton = new Button(buttons, SWT.PUSH | SWT.FLAT); + mRefreshButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mRefreshButton.setText("Refresh"); + mRefreshButton.setToolTipText("Reloads the list of AVD.\nUse this if you create AVDs from the command line."); + mRefreshButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + refresh(true); + } + }); + + if (displayMode != DisplayMode.MANAGER) { + mManagerButton = new Button(buttons, SWT.PUSH | SWT.FLAT); + mManagerButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mManagerButton.setText("Manager..."); + mManagerButton.setToolTipText("Launches the AVD manager."); + mManagerButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + onAvdManager(); + } + }); + } else { + Composite legend = new Composite(group, SWT.NONE); + legend.setLayout(gl = new GridLayout(4, false /*makeColumnsEqualWidth*/)); + gl.marginHeight = gl.marginWidth = 0; + legend.setLayoutData(new GridData(GridData.FILL, GridData.BEGINNING, true, false, + NUM_COL, 1)); + legend.setFont(group.getFont()); + + new Label(legend, SWT.NONE).setImage(mBrokenImage); + new Label(legend, SWT.NONE).setText("A repairable Android Virtual Device."); + new Label(legend, SWT.NONE).setImage(mInvalidImage); + new Label(legend, SWT.NONE).setText("An Android Virtual Device that failed to load. Click 'Details' to see the error."); + } + + // create the table columns + final TableColumn column0 = new TableColumn(mTable, SWT.NONE); + column0.setText("AVD Name"); + final TableColumn column1 = new TableColumn(mTable, SWT.NONE); + column1.setText("Target Name"); + final TableColumn column2 = new TableColumn(mTable, SWT.NONE); + column2.setText("Platform"); + final TableColumn column3 = new TableColumn(mTable, SWT.NONE); + column3.setText("API Level"); + final TableColumn column4 = new TableColumn(mTable, SWT.NONE); + column4.setText("CPU/ABI"); + + adjustColumnsWidth(mTable, column0, column1, column2, column3, column4); + setupSelectionListener(mTable); + fillTable(mTable); + setEnabled(true); + } + + /** + * Creates a new SDK Target Selector, and fills it with a list of {@link AvdInfo}. + * + * @param parent The parent composite where the selector will be added. + * @param manager the AVD manager. + * @param displayMode The display mode ({@link DisplayMode}). + * @param sdkLog The logger. Cannot be null. + */ + public AvdSelector(Composite parent, + String osSdkPath, + AvdManager manager, + DisplayMode displayMode, + ILogger sdkLog) { + this(parent, osSdkPath, manager, (IAvdFilter)null /* filter */, displayMode, sdkLog); + } + + /** + * Creates a new SDK Target Selector, and fills it with a list of {@link AvdInfo}, filtered + * by an {@link IAndroidTarget}. + *

Only the {@link AvdInfo} able to run applications developed for the given + * {@link IAndroidTarget} will be displayed. + * + * @param parent The parent composite where the selector will be added. + * @param manager the AVD manager. + * @param filter Only shows the AVDs matching this target (must not be null). + * @param displayMode The display mode ({@link DisplayMode}). + * @param sdkLog The logger. Cannot be null. + */ + public AvdSelector(Composite parent, + String osSdkPath, + AvdManager manager, + IAndroidTarget filter, + DisplayMode displayMode, + ILogger sdkLog) { + this(parent, osSdkPath, manager, new TargetBasedFilter(filter), displayMode, sdkLog); + } + + /** + * Sets an optional SettingsController. + * @param controller the controller. + */ + public void setSettingsController(SettingsController controller) { + mController = controller; + } + + /** + * Sets the table grid layout data. + * + * @param heightHint If > 0, the height hint is set to the requested value. + */ + public void setTableHeightHint(int heightHint) { + GridData data = new GridData(); + if (heightHint > 0) { + data.heightHint = heightHint; + } + data.grabExcessVerticalSpace = true; + data.grabExcessHorizontalSpace = true; + data.horizontalAlignment = GridData.FILL; + data.verticalAlignment = GridData.FILL; + mTable.setLayoutData(data); + } + + /** + * Refresh the display of Android Virtual Devices. + * Tries to keep the selection. + *

+ * This must be called from the UI thread. + * + * @param reload if true, the AVD manager will reload the AVD from the disk. + * @return false if the reloading failed. This is always true if reload is + * false. + */ + public boolean refresh(boolean reload) { + if (!mInternalRefresh) { + try { + // Note that AvdManagerPage.onDevicesChange() will trigger a + // refresh while the AVDs are being reloaded so prevent from + // having a recursive call to here. + mInternalRefresh = true; + if (reload) { + try { + mAvdManager.reloadAvds(NullLogger.getLogger()); + } catch (AndroidLocationException e) { + return false; + } + } + + AvdInfo selected = getSelected(); + fillTable(mTable); + setSelection(selected); + return true; + } finally { + mInternalRefresh = false; + } + } + return false; + } + + /** + * Sets a new AVD manager + * This does not refresh the display. Call {@link #refresh(boolean)} to do so. + * @param manager the AVD manager. + */ + public void setManager(AvdManager manager) { + mAvdManager = manager; + } + + /** + * Sets a new AVD filter. + * This does not refresh the display. Call {@link #refresh(boolean)} to do so. + * @param filter An IAvdFilter. If non-null, this will filter out the AVD to not display. + */ + public void setFilter(IAvdFilter filter) { + mTargetFilter = filter; + } + + /** + * Sets a new Android Target-based AVD filter. + * This does not refresh the display. Call {@link #refresh(boolean)} to do so. + * @param target An IAndroidTarget. If non-null, only AVD whose target are compatible with the + * filter target will displayed an available for selection. + */ + public void setFilter(IAndroidTarget target) { + if (target != null) { + mTargetFilter = new TargetBasedFilter(target); + } else { + mTargetFilter = null; + } + } + + /** + * Sets a selection listener. Set it to null to remove it. + * The listener will be called after this table processed its selection + * events so that the caller can see the updated state. + *

+ * The event's item contains a {@link TableItem}. + * The {@link TableItem#getData()} contains an {@link IAndroidTarget}. + *

+ * It is recommended that the caller uses the {@link #getSelected()} method instead. + *

+ * The default behavior for double click (when not in {@link DisplayMode#SIMPLE_CHECK}) is to + * display the details of the selected AVD.
+ * To disable it (when you provide your own double click action), set + * {@link SelectionEvent#doit} to false in + * {@link SelectionListener#widgetDefaultSelected(SelectionEvent)} + * + * @param selectionListener The new listener or null to remove it. + */ + public void setSelectionListener(SelectionListener selectionListener) { + mSelectionListener = selectionListener; + } + + /** + * Sets the current target selection. + *

+ * If the selection is actually changed, this will invoke the selection listener + * (if any) with a null event. + * + * @param target the target to be selected. Use null to deselect everything. + * @return true if the target could be selected, false otherwise. + */ + public boolean setSelection(AvdInfo target) { + boolean found = false; + boolean modified = false; + + int selIndex = mTable.getSelectionIndex(); + int index = 0; + for (TableItem i : mTable.getItems()) { + if (mDisplayMode == DisplayMode.SIMPLE_CHECK) { + if ((AvdInfo) i.getData() == target) { + found = true; + if (!i.getChecked()) { + modified = true; + i.setChecked(true); + } + } else if (i.getChecked()) { + modified = true; + i.setChecked(false); + } + } else { + if ((AvdInfo) i.getData() == target) { + found = true; + if (index != selIndex) { + mTable.setSelection(index); + modified = true; + } + break; + } + + index++; + } + } + + if (modified && mSelectionListener != null) { + mSelectionListener.widgetSelected(null); + } + + enableActionButtons(); + + return found; + } + + /** + * Returns the currently selected item. In {@link DisplayMode#SIMPLE_CHECK} mode this will + * return the {@link AvdInfo} that is checked instead of the list selection. + * + * @return The currently selected item or null. + */ + public AvdInfo getSelected() { + if (mDisplayMode == DisplayMode.SIMPLE_CHECK) { + for (TableItem i : mTable.getItems()) { + if (i.getChecked()) { + return (AvdInfo) i.getData(); + } + } + } else { + int selIndex = mTable.getSelectionIndex(); + if (selIndex >= 0) { + return (AvdInfo) mTable.getItem(selIndex).getData(); + } + } + + return null; + } + + /** + * Enables the receiver if the argument is true, and disables it otherwise. + * A disabled control is typically not selectable from the user interface + * and draws with an inactive or "grayed" look. + * + * @param enabled the new enabled state. + */ + public void setEnabled(boolean enabled) { + // We can only enable widgets if the AVD Manager is defined. + mIsEnabled = enabled && mAvdManager != null; + + mTable.setEnabled(mIsEnabled); + mRefreshButton.setEnabled(mIsEnabled); + + if (mNewButton != null) { + mNewButton.setEnabled(mIsEnabled); + } + if (mManagerButton != null) { + mManagerButton.setEnabled(mIsEnabled); + } + + enableActionButtons(); + } + + public boolean isEnabled() { + return mIsEnabled; + } + + /** + * Adds a listener to adjust the columns width when the parent is resized. + *

+ * If we need something more fancy, we might want to use this: + * http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet77.java?view=co + */ + private void adjustColumnsWidth(final Table table, + final TableColumn column0, + final TableColumn column1, + final TableColumn column2, + final TableColumn column3, + final TableColumn column4) { + // Add a listener to resize the column to the full width of the table + table.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Rectangle r = table.getClientArea(); + column0.setWidth(r.width * 20 / 100); // 20% + column1.setWidth(r.width * 30 / 100); // 30% + column2.setWidth(r.width * 10 / 100); // 10% + column3.setWidth(r.width * 10 / 100); // 10% + column4.setWidth(r.width * 30 / 100); // 30% + } + }); + } + + /** + * Creates a selection listener that will check or uncheck the whole line when + * double-clicked (aka "the default selection"). + */ + private void setupSelectionListener(final Table table) { + // Add a selection listener that will check/uncheck items when they are double-clicked + table.addSelectionListener(new SelectionListener() { + + /** + * Handles single-click selection on the table. + * {@inheritDoc} + */ + @Override + public void widgetSelected(SelectionEvent e) { + if (e.item instanceof TableItem) { + TableItem i = (TableItem) e.item; + enforceSingleSelection(i); + } + + if (mSelectionListener != null) { + mSelectionListener.widgetSelected(e); + } + + enableActionButtons(); + } + + /** + * Handles double-click selection on the table. + * Note that the single-click handler will probably already have been called. + * + * On double-click, always check the table item. + * + * {@inheritDoc} + */ + @Override + public void widgetDefaultSelected(SelectionEvent e) { + if (e.item instanceof TableItem) { + TableItem i = (TableItem) e.item; + if (mDisplayMode == DisplayMode.SIMPLE_CHECK) { + i.setChecked(true); + } + enforceSingleSelection(i); + + } + + // whether or not we display details. default: true when not in SIMPLE_CHECK mode. + boolean showDetails = mDisplayMode != DisplayMode.SIMPLE_CHECK; + + if (mSelectionListener != null) { + mSelectionListener.widgetDefaultSelected(e); + showDetails &= e.doit; // enforce false in SIMPLE_CHECK + } + + if (showDetails) { + onDetails(); + } + + enableActionButtons(); + } + + /** + * To ensure single selection, uncheck all other items when this one is selected. + * This makes the chekboxes act as radio buttons. + */ + private void enforceSingleSelection(TableItem item) { + if (mDisplayMode == DisplayMode.SIMPLE_CHECK) { + if (item.getChecked()) { + Table parentTable = item.getParent(); + for (TableItem i2 : parentTable.getItems()) { + if (i2 != item && i2.getChecked()) { + i2.setChecked(false); + } + } + } + } else { + // pass + } + } + }); + } + + /** + * Fills the table with all AVD. + * The table columns are: + *

    + *
  • column 0: sdk name + *
  • column 1: sdk vendor + *
  • column 2: sdk api name + *
  • column 3: sdk version + *
+ */ + private void fillTable(final Table table) { + table.removeAll(); + + // get the AVDs + AvdInfo avds[] = null; + if (mAvdManager != null) { + if (mDisplayMode == DisplayMode.MANAGER) { + avds = mAvdManager.getAllAvds(); + } else { + avds = mAvdManager.getValidAvds(); + } + } + + if (avds != null && avds.length > 0) { + Arrays.sort(avds, new Comparator() { + @Override + public int compare(AvdInfo o1, AvdInfo o2) { + return o1.compareTo(o2); + } + }); + + table.setEnabled(true); + + if (mTargetFilter != null) { + mTargetFilter.prepare(); + } + + for (AvdInfo avd : avds) { + if (mTargetFilter == null || mTargetFilter.accept(avd)) { + TableItem item = new TableItem(table, SWT.NONE); + item.setData(avd); + item.setText(0, avd.getName()); + if (mDisplayMode == DisplayMode.MANAGER) { + AvdStatus status = avd.getStatus(); + + boolean isOk = status == AvdStatus.OK; + boolean isRepair = isAvdRepairable(status); + boolean isInvalid = !isOk && !isRepair; + + Image img = getTagImage(avd.getTag(), isOk, isRepair, isInvalid); + item.setImage(0, img); + } + IAndroidTarget target = avd.getTarget(); + if (target != null) { + item.setText(1, target.getFullName()); + item.setText(2, target.getVersionName()); + item.setText(3, target.getVersion().getApiString()); + item.setText(4, AvdInfo.getPrettyAbiType(avd)); + } else { + item.setText(1, "?"); + item.setText(2, "?"); + item.setText(3, "?"); + item.setText(4, "?"); + } + } + } + + if (mTargetFilter != null) { + mTargetFilter.cleanup(); + } + } + + if (table.getItemCount() == 0) { + table.setEnabled(false); + TableItem item = new TableItem(table, SWT.NONE); + item.setData(null); + item.setText(0, "--"); + item.setText(1, "No AVD available"); + item.setText(2, "--"); + item.setText(3, "--"); + } + } + + @NonNull + private Image getTagImage(IdDisplay tag, + final boolean isOk, + final boolean isRepair, + final boolean isInvalid) { + if (tag == null) { + tag = SystemImage.DEFAULT_TAG; + } + + String fname = String.format("tag_%s_32.png", tag.getId()); + String kname = String.format("%d%d%d_%s", (isOk ? 1 : 0), + (isRepair ? 1 : 0), + (isInvalid ? 1 : 0), + fname); + return mImageFactory.getImageByName(fname, kname, new Filter() { + @Override + public Image filter(Image source) { + // We don't need an overlay for good AVDs. + if (isOk) { + return source; + } + + Image overlayImg = isRepair ? mBrokenImage : mInvalidImage; + ImageDescriptor overlayDesc = ImageDescriptor.createFromImage(overlayImg); + + DecorationOverlayIcon overlaid = + new DecorationOverlayIcon(source, overlayDesc, IDecoration.BOTTOM_RIGHT); + return overlaid.createImage(); + } + }); + } + + /** + * Returns the currently selected AVD in the table. + *

+ * Unlike {@link #getSelected()} this will always return the item being selected + * in the list, ignoring the check boxes state in {@link DisplayMode#SIMPLE_CHECK} mode. + */ + private AvdInfo getTableSelection() { + int selIndex = mTable.getSelectionIndex(); + if (selIndex >= 0) { + return (AvdInfo) mTable.getItem(selIndex).getData(); + } + + return null; + } + + /** + * Updates the enable state of the Details, Start, Delete and Update buttons. + */ + @SuppressWarnings("null") + private void enableActionButtons() { + if (mIsEnabled == false) { + mDetailsButton.setEnabled(false); + mStartButton.setEnabled(false); + + if (mEditButton != null) { + mEditButton.setEnabled(false); + } + if (mDeleteButton != null) { + mDeleteButton.setEnabled(false); + } + if (mRepairButton != null) { + mRepairButton.setEnabled(false); + } + } else { + AvdInfo selection = getTableSelection(); + boolean hasSelection = selection != null; + + mDetailsButton.setEnabled(hasSelection); + mStartButton.setEnabled(mOsSdkPath != null && + hasSelection && + selection.getStatus() == AvdStatus.OK); + + if (mEditButton != null) { + mEditButton.setEnabled(hasSelection); + } + if (mDeleteButton != null) { + mDeleteButton.setEnabled(hasSelection); + } + if (mRepairButton != null) { + mRepairButton.setEnabled(hasSelection && isAvdRepairable(selection.getStatus())); + } + } + } + + private void onNew() { + AvdCreationDialog dlg = new AvdCreationDialog(mTable.getShell(), + mAvdManager, + mImageFactory, + mSdkLog, + null); + + if (dlg.open() == Window.OK) { + refresh(false /*reload*/); + } + } + + @SuppressWarnings("deprecation") + private void onEdit() { + AvdInfo avdInfo = getTableSelection(); + GridDialog dlg; + if (!avdInfo.getDeviceName().isEmpty()) { + dlg = new AvdCreationDialog(mTable.getShell(), + mAvdManager, + mImageFactory, + mSdkLog, + avdInfo); + } else { + dlg = new LegacyAvdEditDialog(mTable.getShell(), + mAvdManager, + mImageFactory, + mSdkLog, + avdInfo); + } + + + if (dlg.open() == Window.OK) { + refresh(false /*reload*/); + } + } + + private void onDetails() { + AvdInfo avdInfo = getTableSelection(); + + AvdDetailsDialog dlg = new AvdDetailsDialog(mTable.getShell(), avdInfo); + dlg.open(); + } + + private void onDelete() { + final AvdInfo avdInfo = getTableSelection(); + + // get the current Display + final Display display = mTable.getDisplay(); + + // check if the AVD is running + if (mAvdManager.isAvdRunning(avdInfo)) { + display.asyncExec(new Runnable() { + @Override + public void run() { + Shell shell = display.getActiveShell(); + MessageDialog.openError(shell, + "Delete Android Virtual Device", + String.format( + "The Android Virtual Device '%1$s' is currently running in an emulator and cannot be deleted.", + avdInfo.getName())); + } + }); + return; + } + + // Confirm you want to delete this AVD + final boolean[] result = new boolean[1]; + display.syncExec(new Runnable() { + @Override + public void run() { + Shell shell = display.getActiveShell(); + result[0] = MessageDialog.openQuestion(shell, + "Delete Android Virtual Device", + String.format( + "Please confirm that you want to delete the Android Virtual Device named '%s'. This operation cannot be reverted.", + avdInfo.getName())); + } + }); + + if (result[0] == false) { + return; + } + + // log for this action. + ILogger log = mSdkLog; + if (log == null || log instanceof MessageBoxLog) { + // If the current logger is a message box, we use our own (to make sure + // to display errors right away and customize the title). + log = new MessageBoxLog( + String.format("Result of deleting AVD '%s':", avdInfo.getName()), + display, + false /*logErrorsOnly*/); + } + + // delete the AVD + boolean success = mAvdManager.deleteAvd(avdInfo, log); + + // display the result + if (log instanceof MessageBoxLog) { + ((MessageBoxLog) log).displayResult(success); + } + + if (success) { + refresh(false /*reload*/); + } + } + + /** + * Repairs the selected AVD. + *

+ * For now this only supports fixing the wrong value in image.sysdir.* + */ + private void onRepair() { + final AvdInfo avdInfo = getTableSelection(); + + // get the current Display + final Display display = mTable.getDisplay(); + + // log for this action. + ILogger log = mSdkLog; + if (log == null || log instanceof MessageBoxLog) { + // If the current logger is a message box, we use our own (to make sure + // to display errors right away and customize the title). + log = new MessageBoxLog( + String.format("Result of updating AVD '%s':", avdInfo.getName()), + display, + false /*logErrorsOnly*/); + } + + if (avdInfo.getStatus() == AvdStatus.ERROR_IMAGE_DIR) { + // delete the AVD + try { + mAvdManager.updateAvd(avdInfo, log); + } catch (IOException e) { + log.error(e, null); + } + refresh(false /*reload*/); + } else if (avdInfo.getStatus() == AvdStatus.ERROR_DEVICE_CHANGED) { + try { + mAvdManager.updateDeviceChanged(avdInfo, log); + } catch (IOException e) { + log.error(e, null); + } + refresh(false /*reload*/); + } else if (avdInfo.getStatus() == AvdStatus.ERROR_DEVICE_MISSING) { + onEdit(); + } + } + + private void onAvdManager() { + + // get the current Display + Display display = mTable.getDisplay(); + + // log for this action. + ILogger log = mSdkLog; + if (log == null || log instanceof MessageBoxLog) { + // If the current logger is a message box, we use our own (to make sure + // to display errors right away and customize the title). + log = new MessageBoxLog("Result of SDK Manager", display, true /*logErrorsOnly*/); + } + + try { + AvdManagerWindowImpl1 win = new AvdManagerWindowImpl1( + mTable.getShell(), + log, + mOsSdkPath, + AvdInvocationContext.DIALOG); + + win.open(); + } catch (Exception ignore) {} + + refresh(true /*reload*/); // UpdaterWindow uses its own AVD manager so this one must reload. + + if (log instanceof MessageBoxLog) { + ((MessageBoxLog) log).displayResult(true); + } + } + + private void onStart() { + AvdInfo avdInfo = getTableSelection(); + + if (avdInfo == null || mOsSdkPath == null) { + return; + } + + AvdStartDialog dialog = new AvdStartDialog( + mTable.getShell(), + avdInfo, + new File(mOsSdkPath), + mController, + mSdkLog); + if (dialog.open() == Window.OK) { + String path = mOsSdkPath + File.separator + + SdkConstants.OS_SDK_TOOLS_FOLDER + + SdkConstants.FN_EMULATOR; + + final String avdName = avdInfo.getName(); + + // build the command line based on the available parameters. + ArrayList list = new ArrayList(); + list.add(path); + list.add("-avd"); //$NON-NLS-1$ + list.add(avdName); + if (dialog.hasWipeData()) { + list.add("-wipe-data"); //$NON-NLS-1$ + } + if (dialog.hasSnapshot()) { + if (!dialog.hasSnapshotLaunch()) { + list.add("-no-snapshot-load"); + } + if (!dialog.hasSnapshotSave()) { + list.add("-no-snapshot-save"); + } + } + float scale = dialog.getScale(); + if (scale != 0.f) { + // do the rounding ourselves. This is because %.1f will write .4899 as .4 + scale = Math.round(scale * 100); + scale /= 100.f; + list.add("-scale"); //$NON-NLS-1$ + // because the emulator expects English decimal values, don't use String.format + // but a Formatter. + Formatter formatter = new Formatter(Locale.US); + formatter.format("%.2f", scale); //$NON-NLS-1$ + list.add(formatter.toString()); + formatter.close(); + } + + // convert the list into an array for the call to exec. + final String[] command = list.toArray(new String[list.size()]); + + // launch the emulator + final ProgressTask progress = new ProgressTask(mTable.getShell(), + "Starting Android Emulator"); + progress.start(new ITask() { + volatile ITaskMonitor mMonitor = null; + + @Override + public void run(final ITaskMonitor monitor) { + mMonitor = monitor; + try { + monitor.setDescription( + "Starting emulator for AVD '%1$s'", + avdName); + monitor.log("Starting emulator for AVD '%1$s'", avdName); + + // we'll wait 100ms*100 = 10s. The emulator sometimes seem to + // start mostly OK just to crash a few seconds later. 10 seconds + // seems a good wait for that case. + int n = 100; + monitor.setProgressMax(n); + + Process process = Runtime.getRuntime().exec(command); + GrabProcessOutput.grabProcessOutput( + process, + Wait.ASYNC, + new IProcessOutput() { + @Override + public void out(@Nullable String line) { + filterStdOut(line); + } + + @Override + public void err(@Nullable String line) { + filterStdErr(line); + } + }); + + // This small wait prevents the dialog from closing too fast: + // When it works, the emulator returns immediately, even if + // no UI is shown yet. And when it fails (because the AVD is + // locked/running) this allows us to have time to capture the + // error and display it. + for (int i = 0; i < n; i++) { + try { + Thread.sleep(100); + monitor.incProgress(1); + } catch (InterruptedException e) { + // ignore + } + } + } catch (Exception e) { + monitor.logError("Failed to start emulator: %1$s", + e.getMessage()); + } finally { + mMonitor = null; + } + } + + private void filterStdOut(String line) { + ITaskMonitor m = mMonitor; + if (line == null || m == null) { + return; + } + + // Skip some non-useful messages. + if (line.indexOf("NSQuickDrawView") != -1) { //$NON-NLS-1$ + // Discard the MacOS warning: + // "This application, or a library it uses, is using NSQuickDrawView, + // which has been deprecated. Apps should cease use of QuickDraw and move + // to Quartz." + return; + } + + if (line.toLowerCase().indexOf("error") != -1 || //$NON-NLS-1$ + line.indexOf("qemu: fatal") != -1) { //$NON-NLS-1$ + // Sometimes the emulator seems to output errors on stdout. Catch these. + m.logError("%1$s", line); //$NON-NLS-1$ + return; + } + + m.log("%1$s", line); //$NON-NLS-1$ + } + + private void filterStdErr(String line) { + ITaskMonitor m = mMonitor; + if (line == null || m == null) { + return; + } + + if (line.indexOf("emulator: device") != -1 || //$NON-NLS-1$ + line.indexOf("HAX is working") != -1) { //$NON-NLS-1$ + // These are not errors. Output them as regular stdout messages. + m.log("%1$s", line); //$NON-NLS-1$ + return; + } + + m.logError("%1$s", line); //$NON-NLS-1$ + } + }); + } + } + + private boolean isAvdRepairable(AvdStatus avdStatus) { + return avdStatus == AvdStatus.ERROR_IMAGE_DIR + || avdStatus == AvdStatus.ERROR_DEVICE_CHANGED + || avdStatus == AvdStatus.ERROR_DEVICE_MISSING; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdStartDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdStartDialog.java new file mode 100644 index 00000000..00eb1aa6 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdStartDialog.java @@ -0,0 +1,643 @@ +/* + * 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.sdkuilib.internal.widgets; + +import com.android.sdklib.devices.Device; +import com.android.sdklib.devices.DeviceManager; +import com.android.sdklib.internal.avd.AvdInfo; +import com.android.sdklib.internal.avd.AvdManager; +import com.android.sdklib.internal.repository.updater.SettingsController; +import com.android.sdkuilib.ui.GridDialog; +import com.android.utils.ILogger; +import com.android.utils.SdkUtils; + +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.window.Window; +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.events.VerifyEvent; +import org.eclipse.swt.events.VerifyListener; +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.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +import java.awt.Toolkit; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.text.NumberFormat; +import java.text.ParseException; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Dialog dealing with emulator launch options. The following options are supported: + *

    + *
  • -wipe-data
  • + *
  • -scale
  • + *
+ * Values are stored (in the class as static field) to be reused while the app is still running. + * The Monitor dpi is stored in the settings if available. + */ +final class AvdStartDialog extends GridDialog { + // static field to reuse values during the same session. + private static boolean sWipeData = false; + private static boolean sSnapshotSave = true; + private static boolean sSnapshotLaunch = true; + private static int sMonitorDpi = 72; // used if there's no setting controller. + private static final Map sSkinScaling = new HashMap(); + + private static final Pattern sScreenSizePattern = Pattern.compile("\\d*(\\.\\d?)?"); + + private final AvdInfo mAvd; + private final File mSdkLocation; + private final SettingsController mSettingsController; + private final DeviceManager mDeviceManager; + + private Text mScreenSize; + private Text mMonitorDpi; + private Button mScaleButton; + + private float mScale = 0.f; + private boolean mWipeData = false; + private int mDensity = 160; // medium density + private int mSize1 = -1; + private int mSize2 = -1; + private String mSkinDisplay; + private boolean mEnableScaling = true; + private Label mScaleField; + private boolean mHasSnapshot = true; + private boolean mSnapshotSave = true; + private boolean mSnapshotLaunch = true; + private Button mSnapshotLaunchCheckbox; + + AvdStartDialog(Shell parentShell, AvdInfo avd, File sdkLocation, + SettingsController settingsController, ILogger sdkLog) { + super(parentShell, 2, false); + mAvd = avd; + mSdkLocation = sdkLocation; + mSettingsController = settingsController; + mDeviceManager = DeviceManager.createInstance(mSdkLocation, sdkLog); + if (mAvd == null) { + throw new IllegalArgumentException("avd cannot be null"); + } + if (mSdkLocation == null) { + throw new IllegalArgumentException("sdkLocation cannot be null"); + } + + computeSkinData(); + } + + public boolean hasWipeData() { + return mWipeData; + } + + /** + * Returns the scaling factor, or 0.f if none are set. + */ + public float getScale() { + return mScale; + } + + @Override + public void createDialogContent(final Composite parent) { + GridData gd; + + Label l = new Label(parent, SWT.NONE); + l.setText("Skin:"); + + l = new Label(parent, SWT.NONE); + l.setText(mSkinDisplay == null ? "None" : mSkinDisplay); + l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + l = new Label(parent, SWT.NONE); + l.setText("Density:"); + + l = new Label(parent, SWT.NONE); + l.setText(getDensityText()); + l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mScaleButton = new Button(parent, SWT.CHECK); + mScaleButton.setText("Scale display to real size"); + mScaleButton.setEnabled(mEnableScaling); + boolean defaultState = mEnableScaling && sSkinScaling.get(mAvd.getName()) != null; + mScaleButton.setSelection(defaultState); + mScaleButton.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.horizontalSpan = 2; + final Group scaleGroup = new Group(parent, SWT.NONE); + scaleGroup.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.horizontalIndent = 30; + gd.horizontalSpan = 2; + scaleGroup.setLayout(new GridLayout(3, false)); + + l = new Label(scaleGroup, SWT.NONE); + l.setText("Screen Size (in):"); + mScreenSize = new Text(scaleGroup, SWT.BORDER); + mScreenSize.setText(getScreenSize()); + mScreenSize.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mScreenSize.addVerifyListener(new VerifyListener() { + @Override + public void verifyText(VerifyEvent event) { + // combine the current content and the new text + String text = mScreenSize.getText(); + text = text.substring(0, event.start) + event.text + text.substring(event.end); + + // now make sure it's a match for the regex + event.doit = sScreenSizePattern.matcher(text).matches(); + } + }); + mScreenSize.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent event) { + onScaleChange(); + } + }); + + // empty composite, only 2 widgets on this line. + new Composite(scaleGroup, SWT.NONE).setLayoutData(gd = new GridData()); + gd.widthHint = gd.heightHint = 0; + + l = new Label(scaleGroup, SWT.NONE); + l.setText("Monitor dpi:"); + mMonitorDpi = new Text(scaleGroup, SWT.BORDER); + mMonitorDpi.setText(Integer.toString(getMonitorDpi())); + mMonitorDpi.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.widthHint = 50; + mMonitorDpi.addVerifyListener(new VerifyListener() { + @Override + public void verifyText(VerifyEvent event) { + // check for digit only. + for (int i = 0 ; i < event.text.length(); i++) { + char letter = event.text.charAt(i); + if (letter < '0' || letter > '9') { + event.doit = false; + return; + } + } + } + }); + mMonitorDpi.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent event) { + onScaleChange(); + } + }); + + Button button = new Button(scaleGroup, SWT.PUSH | SWT.FLAT); + button.setText("?"); + button.setToolTipText("Click to figure out your monitor's pixel density"); + button.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + ResolutionChooserDialog dialog = new ResolutionChooserDialog(parent.getShell()); + if (dialog.open() == Window.OK) { + mMonitorDpi.setText(Integer.toString(dialog.getDensity())); + } + } + }); + + l = new Label(scaleGroup, SWT.NONE); + l.setText("Scale:"); + mScaleField = new Label(scaleGroup, SWT.NONE); + mScaleField.setLayoutData(new GridData(GridData.FILL, GridData.CENTER, + true /*grabExcessHorizontalSpace*/, + true /*grabExcessVerticalSpace*/, + 2 /*horizontalSpan*/, + 1 /*verticalSpan*/)); + setScale(mScale); // set initial text value + + enableGroup(scaleGroup, defaultState); + + mScaleButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent event) { + boolean enabled = mScaleButton.getSelection(); + enableGroup(scaleGroup, enabled); + if (enabled) { + onScaleChange(); + } else { + setScale(0); + } + } + }); + + final Button wipeButton = new Button(parent, SWT.CHECK); + wipeButton.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.horizontalSpan = 2; + wipeButton.setText("Wipe user data"); + wipeButton.setSelection(mWipeData = sWipeData); + wipeButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + mWipeData = wipeButton.getSelection(); + updateSnapshotLaunchAvailability(); + } + }); + + Map prop = mAvd.getProperties(); + String snapshotPresent = prop.get(AvdManager.AVD_INI_SNAPSHOT_PRESENT); + mHasSnapshot = (snapshotPresent != null) && snapshotPresent.equals("true"); + + mSnapshotLaunchCheckbox = new Button(parent, SWT.CHECK); + mSnapshotLaunchCheckbox.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.horizontalSpan = 2; + mSnapshotLaunchCheckbox.setText("Launch from snapshot"); + updateSnapshotLaunchAvailability(); + mSnapshotLaunchCheckbox.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + mSnapshotLaunch = mSnapshotLaunchCheckbox.getSelection(); + } + }); + + final Button snapshotSaveCheckbox = new Button(parent, SWT.CHECK); + snapshotSaveCheckbox.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.horizontalSpan = 2; + snapshotSaveCheckbox.setText("Save to snapshot"); + snapshotSaveCheckbox.setSelection((mSnapshotSave = sSnapshotSave) && mHasSnapshot); + snapshotSaveCheckbox.setEnabled(mHasSnapshot); + snapshotSaveCheckbox.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + mSnapshotSave = snapshotSaveCheckbox.getSelection(); + } + }); + + l = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL); + l.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.horizontalSpan = 2; + + // if the scaling is enabled by default, we must initialize the value of mScale + if (defaultState) { + onScaleChange(); + } + } + + /** On Windows we need to manually enable/disable the children of a group */ + private void enableGroup(final Group group, boolean enabled) { + group.setEnabled(enabled); + for (Control c : group.getChildren()) { + c.setEnabled(enabled); + } + } + + @Override + protected void configureShell(Shell newShell) { + super.configureShell(newShell); + newShell.setText("Launch Options"); + } + + @Override + protected Button createButton(Composite parent, int id, String label, boolean defaultButton) { + if (id == IDialogConstants.OK_ID) { + label = "Launch"; + } + + return super.createButton(parent, id, label, defaultButton); + } + + @Override + protected void okPressed() { + // override ok to store some info + // first the monitor dpi + String dpi = mMonitorDpi.getText(); + if (dpi.length() > 0) { + sMonitorDpi = Integer.parseInt(dpi); + + // if there is a setting controller, save it + if (mSettingsController != null) { + mSettingsController.setMonitorDensity(sMonitorDpi); + mSettingsController.saveSettings(); + } + } + + // now the scale factor + String key = mAvd.getName(); + sSkinScaling.remove(key); + if (mScaleButton.getSelection()) { + String size = mScreenSize.getText(); + if (size.length() > 0) { + sSkinScaling.put(key, size); + } + } + + // and then the wipe-data checkbox + sWipeData = mWipeData; + + // and the snapshot handling if those checkboxes are enabled. + if (mHasSnapshot) { + sSnapshotSave = mSnapshotSave; + if (!mWipeData) { + sSnapshotLaunch = mSnapshotLaunch; + } + } + + // finally continue with the ok action + super.okPressed(); + } + + private void computeSkinData() { + Map prop = mAvd.getProperties(); + String dpi = prop.get("hw.lcd.density"); + if (dpi != null && dpi.length() > 0) { + mDensity = Integer.parseInt(dpi); + } + + findSkinResolution(); + } + + private void onScaleChange() { + String sizeStr = mScreenSize.getText(); + if (sizeStr.length() == 0) { + setScale(0); + return; + } + + String dpiStr = mMonitorDpi.getText(); + if (dpiStr.length() == 0) { + setScale(0); + return; + } + + int dpi = Integer.parseInt(dpiStr); + + // The size number is formatted using String.format (locale formatting) + float size; + try { + size = sizeStr.isEmpty() ? 0.0f : NumberFormat.getNumberInstance().parse(sizeStr).floatValue(); + } catch (ParseException e) { + setScale(0); + return; + } + + /* + * We are trying to emulate the following device: + * resolution: 'mSize1'x'mSize2' + * density: 'mDensity' + * screen diagonal: 'size' + * ontop a monitor running at 'dpi' + */ + // We start by computing the screen diagonal in pixels, if the density was really mDensity + float diagonalPx = (float)Math.sqrt(mSize1*mSize1+mSize2*mSize2); + // Now we would convert this in actual inches: + // diagonalIn = diagonal / mDensity + // the scale factor is a mix of adapting to the new density and to the new size. + // (size/diagonalIn) * (dpi/mDensity) + // this can be simplified to: + setScale((size * dpi) / diagonalPx); + } + + private void setScale(float scale) { + mScale = scale; + + // Do the rounding exactly like AvdSelector will do. + scale = Math.round(scale * 100); + scale /= 100.f; + + if (scale == 0.f) { + mScaleField.setText("default"); //$NON-NLS-1$ + } else { + mScaleField.setText(String.format(Locale.getDefault(), "%.2f", scale)); //$NON-NLS-1$ + } + } + + /** + * Returns the monitor dpi to start with. + * This can be coming from the settings, the session-based storage, or the from whatever Java + * can tell us. + */ + private int getMonitorDpi() { + if (mSettingsController != null) { + sMonitorDpi = mSettingsController.getSettings().getMonitorDensity(); + } + + if (sMonitorDpi == -1) { // first time? try to get a value + sMonitorDpi = Toolkit.getDefaultToolkit().getScreenResolution(); + } + + return sMonitorDpi; + } + + /** + * Returns the screen size to start with. + *

If an emulator with the same skin was already launched, scaled, the size used is reused. + *

If one hasn't been launched and the AVD is based on a device, use the device's screen + * size. Otherwise, use the default (3). + */ + private String getScreenSize() { + String size = sSkinScaling.get(mAvd.getName()); + if (size != null) { + return size; + } + + Map properties = mAvd.getProperties(); + if (properties != null) { + String name = properties.get(AvdManager.AVD_INI_DEVICE_NAME); + String mfctr = properties.get(AvdManager.AVD_INI_DEVICE_MANUFACTURER); + if (name != null && mfctr != null) { + Device d = mDeviceManager.getDevice(name, mfctr); + if (d != null) { + double screenSize = + d.getDefaultHardware().getScreen().getDiagonalLength(); + return String.format(Locale.getDefault(), "%.1f", screenSize); + } + } + } + + return "3"; + } + + /** + * Returns a display string for the density. + */ + private String getDensityText() { + switch (mDensity) { + case 120: + return "Low (120)"; + case 160: + return "Medium (160)"; + case 240: + return "High (240)"; + } + + return Integer.toString(mDensity); + } + + /** + * Finds the skin resolution and sets it in {@link #mSize1} and {@link #mSize2}. + */ + private void findSkinResolution() { + Map prop = mAvd.getProperties(); + String skinName = prop.get(AvdManager.AVD_INI_SKIN_NAME); + + if (skinName != null) { + Matcher m = AvdManager.NUMERIC_SKIN_SIZE.matcher(skinName); + if (m != null && m.matches()) { + mSize1 = Integer.parseInt(m.group(1)); + mSize2 = Integer.parseInt(m.group(2)); + mSkinDisplay = skinName; + mEnableScaling = true; + return; + } + } + + // The resolution is inside the layout file of the skin. + mEnableScaling = false; // default to false for now. + + // path to the skin layout file. + String skinPath = prop.get(AvdManager.AVD_INI_SKIN_PATH); + if (skinPath != null) { + File skinFolder = new File(mSdkLocation, skinPath); + if (skinFolder.isDirectory()) { + File layoutFile = new File(skinFolder, "layout"); + if (layoutFile.isFile()) { + if (parseLayoutFile(layoutFile)) { + mSkinDisplay = String.format("%1$s (%2$dx%3$d)", skinName, mSize1, mSize2); + mEnableScaling = true; + } else { + mSkinDisplay = skinName; + } + } + } + } + } + + /** + * Parses a layout file. + *

+ * the format is relatively easy. It's a collection of items defined as + * ≶name> { + * ≶content> + * } + * + * content is either 1+ items or 1+ properties + * properties are defined as + * ≶name>≶whitespace>≶value> + * + * We're going to look for an item called display, with 2 properties height and width. + * This is very basic parser. + * + * @param layoutFile the file to parse + * @return true if both sizes where found. + */ + private boolean parseLayoutFile(File layoutFile) { + BufferedReader input = null; + try { + input = new BufferedReader(new FileReader(layoutFile)); + String line; + + while ((line = input.readLine()) != null) { + // trim to remove whitespace + line = line.trim(); + int len = line.length(); + if (len == 0) continue; + + // check if this is a new item + if (line.charAt(len-1) == '{') { + // this is the start of a node + String[] tokens = line.split(" "); + if ("display".equals(tokens[0])) { + // this is the one we're looking for! + while ((mSize1 == -1 || mSize2 == -1) && + (line = input.readLine()) != null) { + // trim to remove whitespace + line = line.trim(); + len = line.length(); + if (len == 0) continue; + + if ("}".equals(line)) { // looks like we're done with the item. + break; + } + + tokens = line.split(" "); + if (tokens.length >= 2) { + // there can be multiple space between the name and value + // in which case we'll get an extra empty token in the middle. + if ("width".equals(tokens[0])) { + mSize1 = Integer.parseInt(tokens[tokens.length-1]); + } else if ("height".equals(tokens[0])) { + mSize2 = Integer.parseInt(tokens[tokens.length-1]); + } + } + } + + return mSize1 != -1 && mSize2 != -1; + } + } + + } + // if it reaches here, display was not found. + // false is returned below. + } catch (IOException e) { + // ignore. + } finally { + if (input != null) { + try { + input.close(); + } catch (IOException e) { + // ignore + } + } + } + + return false; + } + + /** + * @return Whether there's a snapshot file available. + */ + public boolean hasSnapshot() { + return mHasSnapshot; + } + + /** + * @return Whether to launch and load snapshot. + */ + public boolean hasSnapshotLaunch() { + return mSnapshotLaunch && !hasWipeData(); + } + + /** + * @return Whether to preserve emulator state to snapshot. + */ + public boolean hasSnapshotSave() { + return mSnapshotSave; + } + + /** + * Updates snapshot launch availability, for when mWipeData value changes. + */ + private void updateSnapshotLaunchAvailability() { + boolean enabled = !mWipeData && mHasSnapshot; + mSnapshotLaunchCheckbox.setEnabled(enabled); + mSnapshotLaunch = enabled && sSnapshotLaunch; + mSnapshotLaunchCheckbox.setSelection(mSnapshotLaunch); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/DeviceCreationDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/DeviceCreationDialog.java new file mode 100644 index 00000000..263ca210 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/DeviceCreationDialog.java @@ -0,0 +1,1087 @@ +/* + * Copyright (C) 2012 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.sdkuilib.internal.widgets; + +import com.android.annotations.Nullable; +import com.android.resources.Density; +import com.android.resources.Keyboard; +import com.android.resources.KeyboardState; +import com.android.resources.Navigation; +import com.android.resources.NavigationState; +import com.android.resources.ResourceEnum; +import com.android.resources.ScreenOrientation; +import com.android.resources.ScreenRatio; +import com.android.resources.ScreenSize; +import com.android.resources.TouchScreen; +import com.android.sdklib.devices.Abi; +import com.android.sdklib.devices.ButtonType; +import com.android.sdklib.devices.Camera; +import com.android.sdklib.devices.CameraLocation; +import com.android.sdklib.devices.Device; +import com.android.sdklib.devices.DeviceManager; +import com.android.sdklib.devices.DeviceManager.DeviceFilter; +import com.android.sdklib.devices.Hardware; +import com.android.sdklib.devices.Multitouch; +import com.android.sdklib.devices.Network; +import com.android.sdklib.devices.PowerType; +import com.android.sdklib.devices.Screen; +import com.android.sdklib.devices.ScreenType; +import com.android.sdklib.devices.Sensor; +import com.android.sdklib.devices.Software; +import com.android.sdklib.devices.State; +import com.android.sdklib.devices.Storage; +import com.android.sdkuilib.internal.repository.icons.ImageFactory; +import com.android.sdkuilib.ui.GridDataBuilder; +import com.android.sdkuilib.ui.GridDialog; +import com.android.sdkuilib.ui.GridLayoutBuilder; + +import org.eclipse.jface.dialogs.IDialogConstants; +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.events.SelectionListener; +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.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +public class DeviceCreationDialog extends GridDialog { + + private static final String MANUFACTURER = "User"; + + private final ImageFactory mImageFactory; + private final DeviceManager mManager; + private Collection mUserDevices; + + private Device mDevice; + + private Text mDeviceName; + private Text mDiagonalLength; + private Text mXDimension; + private Text mYDimension; + private Button mKeyboard; + private Button mDpad; + private Button mTrackball; + private Button mNoNav; + private Text mRam; + private Combo mRamCombo; + private Combo mButtons; + private Combo mSize; + private Combo mDensity; + private Combo mRatio; + private Button mAccelerometer; // hw.accelerometer + private Button mGyro; // hw.sensors.orientation + private Button mGps; // hw.sensors.gps + private Button mProximitySensor; // hw.sensors.proximity + private Button mCameraFront; + private Button mCameraRear; + private Group mStateGroup; + private Button mPortrait; + private Label mPortraitLabel; + private Button mPortraitNav; + private Button mLandscape; + private Label mLandscapeLabel; + private Button mLandscapeNav; + private Button mPortraitKeys; + private Label mPortraitKeysLabel; + private Button mPortraitKeysNav; + private Button mLandscapeKeys; + private Label mLandscapeKeysLabel; + private Button mLandscapeKeysNav; + + private Button mForceCreation; + private Label mStatusIcon; + private Label mStatusLabel; + + private Button mOkButton; + + /** The hardware instance attached to each of the states of the created device. */ + private Hardware mHardware; + /** The instance of the Device created by the dialog, if the user pressed {@code mOkButton}. */ + private Device mCreatedDevice; + + /** + * This contains the Software for the device. Since it has no effect on the + * emulator whatsoever, we just use a single instance with reasonable + * defaults. */ + private static final Software mSoftware; + + static { + mSoftware = new Software(); + mSoftware.setLiveWallpaperSupport(true); + mSoftware.setGlVersion("2.0"); + } + + public DeviceCreationDialog(Shell parentShell, + DeviceManager manager, + ImageFactory imageFactory, + @Nullable Device device) { + super(parentShell, 3, false); + mImageFactory = imageFactory; + mDevice = device; + mManager = manager; + mUserDevices = mManager.getDevices(DeviceFilter.USER); + } + + /** + * Returns the instance of the Device created by the dialog, + * if the user pressed the OK|create|edit|clone button. + * Typically only non-null if the dialog returns OK. + */ + public Device getCreatedDevice() { + return mCreatedDevice; + } + + @Override + protected Control createContents(Composite parent) { + Control control = super.createContents(parent); + + mOkButton = getButton(IDialogConstants.OK_ID); + + if (mDevice == null) { + getShell().setText("Create New Device"); + } else { + if (mUserDevices.contains(mDevice)) { + getShell().setText("Edit Device"); + } else { + getShell().setText("Clone Device"); + } + } + + Object ld = mOkButton.getLayoutData(); + if (ld instanceof GridData) { + ((GridData) ld).widthHint = 100; + } + + validatePage(); + + return control; + } + + @Override + public void createDialogContent(Composite parent) { + + ValidationListener validator = new ValidationListener(); + SizeListener sizeListener = new SizeListener(); + NavStateListener navListener = new NavStateListener(); + + Composite column1 = new Composite(parent, SWT.NONE); + GridDataBuilder.create(column1).hFill().vTop(); + GridLayoutBuilder.create(column1).columns(2); + + // vertical separator between column 1 and 2 + Label label = new Label(parent, SWT.SEPARATOR | SWT.VERTICAL); + GridDataBuilder.create(label).vFill().vGrab(); + + Composite column2 = new Composite(parent, SWT.NONE); + GridDataBuilder.create(column2).hFill().vTop(); + GridLayoutBuilder.create(column2).columns(2); + + // Column 1 + + String tooltip = "Name of the new device"; + generateLabel("Name:", tooltip, column1); + mDeviceName = generateText(column1, tooltip, new CreateNameModifyListener()); + + tooltip = "Diagonal length of the screen in inches"; + generateLabel("Screen Size (in):", tooltip, column1); + mDiagonalLength = generateText(column1, tooltip, sizeListener); + + tooltip = "The resolution of the device in pixels"; + generateLabel("Resolution (px):", tooltip, column1); + Composite dimensionGroup = new Composite(column1, SWT.NONE); // Like a Group with no border + GridDataBuilder.create(dimensionGroup).hFill(); + GridLayoutBuilder.create(dimensionGroup).columns(3).noMargins(); + mXDimension = generateText(dimensionGroup, tooltip, sizeListener); + new Label(dimensionGroup, SWT.NONE).setText("x"); + mYDimension = generateText(dimensionGroup, tooltip, sizeListener); + + label = new Label(column1, SWT.None); // empty space holder + GridDataBuilder.create(label).hFill().hGrab().hSpan(2); + + // Column 2 + + tooltip = "The screen size bucket that the device falls into"; + generateLabel("Size:", tooltip, column2); + mSize = generateCombo(column2, tooltip, ScreenSize.values(), 1, validator); + + tooltip = "The aspect ratio bucket the screen falls into. A \"long\" screen is wider."; + generateLabel("Screen Ratio:", tooltip, column2); + mRatio = generateCombo(column2, tooltip, ScreenRatio.values(), 1, validator); + + tooltip = "The pixel density bucket the device falls in"; + generateLabel("Density:", tooltip, column2); + mDensity = generateCombo(column2, tooltip, Density.values(), 3, validator); + + label = new Label(column2, SWT.None); // empty space holder + GridDataBuilder.create(label).hFill().hGrab().hSpan(2); + + + // Column 1, second row + + generateLabel("Sensors:", "The sensors available on the device", column1); + Group sensorGroup = new Group(column1, SWT.NONE); + sensorGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + sensorGroup.setLayout(new GridLayout(2, false)); + mAccelerometer = generateButton(sensorGroup, "Accelerometer", + "Presence of an accelerometer", SWT.CHECK, true, validator); + mGyro = generateButton(sensorGroup, "Gyroscope", + "Presence of a gyroscope", SWT.CHECK, true, validator); + mGps = generateButton(sensorGroup, "GPS", "Presence of a GPS", SWT.CHECK, true, validator); + mProximitySensor = generateButton(sensorGroup, "Proximity Sensor", + "Presence of a proximity sensor", SWT.CHECK, true, validator); + + generateLabel("Cameras", "The cameras available on the device", column1); + Group cameraGroup = new Group(column1, SWT.NONE); + cameraGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + cameraGroup.setLayout(new GridLayout(2, false)); + mCameraFront = generateButton(cameraGroup, "Front", "Presence of a front camera", + SWT.CHECK, false, validator); + mCameraRear = generateButton(cameraGroup, "Rear", "Presence of a rear camera", + SWT.CHECK, true, validator); + + generateLabel("Input:", "The input hardware on the given device", column1); + Group inputGroup = new Group(column1, SWT.NONE); + inputGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + inputGroup.setLayout(new GridLayout(3, false)); + mKeyboard = generateButton(inputGroup, "Keyboard", "Presence of a hardware keyboard", + SWT.CHECK, false, + new KeyboardListener()); + GridData gridData = new GridData(GridData.FILL_HORIZONTAL); + gridData.horizontalSpan = 3; + mKeyboard.setLayoutData(gridData); + mNoNav = generateButton(inputGroup, "No Nav", "No hardware navigation", + SWT.RADIO, true, navListener); + mDpad = generateButton(inputGroup, "DPad", "The device has a DPad navigation element", + SWT.RADIO, false, navListener); + mTrackball = generateButton(inputGroup, "Trackball", + "The device has a trackball navigation element", SWT.RADIO, false, navListener); + + tooltip = "The amount of RAM on the device"; + generateLabel("RAM:", tooltip, column1); + Group ramGroup = new Group(column1, SWT.NONE); + ramGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + ramGroup.setLayout(new GridLayout(2, false)); + mRam = generateText(ramGroup, tooltip, validator); + mRamCombo = new Combo(ramGroup, SWT.DROP_DOWN | SWT.READ_ONLY); + mRamCombo.setToolTipText(tooltip); + mRamCombo.add("MiB"); + mRamCombo.add("GiB"); + mRamCombo.select(0); + mRamCombo.addModifyListener(validator); + + // Column 2, second row + + tooltip = "Type of buttons (Home, Menu, etc.) on the device. " + + "This can be software buttons like on the Galaxy Nexus, or hardware buttons like " + + "the capacitive buttons on the Nexus S."; + generateLabel("Buttons:", tooltip, column2); + mButtons = new Combo(column2, SWT.DROP_DOWN | SWT.READ_ONLY); + mButtons.setToolTipText(tooltip); + mButtons.add(ButtonType.SOFT.getDescription()); + mButtons.add(ButtonType.HARD.getDescription()); + mButtons.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mButtons.select(0); + mButtons.addModifyListener(validator); + + generateLabel("Device States:", "The available states for the given device", column2); + + mStateGroup = new Group(column2, SWT.NONE); + mStateGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mStateGroup.setLayout(new GridLayout(2, true)); + + tooltip = "The device has a portait position with no keyboard available"; + mPortraitLabel = generateLabel("Portrait:", tooltip, mStateGroup); + gridData = new GridData(GridData.FILL_HORIZONTAL); + gridData.horizontalSpan = 2; + mPortraitLabel.setLayoutData(gridData); + mPortrait = generateButton(mStateGroup, "Enabled", tooltip, SWT.CHECK, true, + navListener); + mPortraitNav = generateButton(mStateGroup, "Navigation", + "Hardware navigation is available in this state", SWT.CHECK, true, validator); + mPortraitNav.setEnabled(false); + + tooltip = "The device has a landscape position with no keyboard available"; + mLandscapeLabel = generateLabel("Landscape:", tooltip, mStateGroup); + gridData = new GridData(GridData.FILL_HORIZONTAL); + gridData.horizontalSpan = 2; + mLandscapeLabel.setLayoutData(gridData); + mLandscape = generateButton(mStateGroup, "Enabled", tooltip, SWT.CHECK, true, + navListener); + mLandscapeNav = generateButton(mStateGroup, "Navigation", + "Hardware navigation is available in this state", SWT.CHECK, true, validator); + mLandscapeNav.setEnabled(false); + + tooltip = "The device has a portait position with a keyboard available"; + mPortraitKeysLabel = generateLabel("Portrait with keyboard:", tooltip, mStateGroup); + gridData = new GridData(GridData.FILL_HORIZONTAL); + gridData.horizontalSpan = 2; + mPortraitKeysLabel.setLayoutData(gridData); + mPortraitKeysLabel.setEnabled(false); + mPortraitKeys = generateButton(mStateGroup, "Enabled", tooltip, SWT.CHECK, true, + navListener); + mPortraitKeys.setEnabled(false); + mPortraitKeysNav = generateButton(mStateGroup, "Navigation", + "Hardware navigation is available in this state", SWT.CHECK, true, validator); + mPortraitKeysNav.setEnabled(false); + + tooltip = "The device has a landscape position with the keyboard open"; + mLandscapeKeysLabel = generateLabel("Landscape with keyboard:", tooltip, mStateGroup); + gridData = new GridData(GridData.FILL_HORIZONTAL); + gridData.horizontalSpan = 2; + mLandscapeKeysLabel.setLayoutData(gridData); + mLandscapeKeysLabel.setEnabled(false); + mLandscapeKeys = generateButton(mStateGroup, "Enabled", tooltip, SWT.CHECK, true, + navListener); + mLandscapeKeys.setEnabled(false); + mLandscapeKeysNav = generateButton(mStateGroup, "Navigation", + "Hardware navigation is available in this state", SWT.CHECK, true, validator); + mLandscapeKeysNav.setEnabled(false); + + + mForceCreation = new Button(column2, SWT.CHECK); + mForceCreation.setText("Override the existing device with the same name"); + mForceCreation.setToolTipText("There's already an AVD with the same name. Check this to delete it and replace it by the new AVD."); + mForceCreation.setLayoutData(new GridData(GridData.BEGINNING, GridData.CENTER, + true, false, 2, 1)); + mForceCreation.setEnabled(false); + mForceCreation.addSelectionListener(validator); + + + // -- third row + + // add a separator to separate from the ok/cancel button + label = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL); + label.setLayoutData(new GridData(GridData.FILL, GridData.CENTER, true, false, 3, 1)); + + // add stuff for the error display + Composite statusComposite = new Composite(parent, SWT.NONE); + GridLayout gl; + statusComposite.setLayoutData( + new GridData(GridData.FILL, GridData.CENTER, true, false, 3, 1)); + statusComposite.setLayout(gl = new GridLayout(2, false)); + gl.marginHeight = gl.marginWidth = 0; + + mStatusIcon = new Label(statusComposite, SWT.NONE); + mStatusIcon.setLayoutData(new GridData(GridData.BEGINNING, GridData.BEGINNING, + false, false)); + mStatusLabel = new Label(statusComposite, SWT.NONE); + mStatusLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mStatusLabel.setText(""); //$NON-NLS-1$ + + prefillWithDevice(mDevice); + + validatePage(); + } + + private Button generateButton(Composite parent, String text, String tooltip, int type, + boolean selected, SelectionListener listener) { + Button b = new Button(parent, type); + b.setText(text); + b.setToolTipText(tooltip); + b.setSelection(selected); + b.addSelectionListener(listener); + b.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + return b; + } + + /** + * Generates a combo widget attached to the given parent, then sets the + * tooltip, adds all of the {@link String}s returned by + * {@link ResourceEnum#getResourceValue()} for each {@link ResourceEnum}, + * sets the combo to the given index and adds the given + * {@link ModifyListener}. + */ + private Combo generateCombo(Composite parent, String tooltip, ResourceEnum[] values, + int selection, + ModifyListener validator) { + Combo c = new Combo(parent, SWT.DROP_DOWN | SWT.READ_ONLY); + c.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + c.setToolTipText(tooltip); + for (ResourceEnum r : values) { + c.add(r.getResourceValue()); + } + c.select(selection); + c.addModifyListener(validator); + return c; + } + + /** Generates a text widget with the given tooltip, parent and listener */ + private Text generateText(Composite parent, String tooltip, ModifyListener listener) { + Text t = new Text(parent, SWT.BORDER); + t.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + t.setToolTipText(tooltip); + t.addModifyListener(listener); + return t; + } + + /** Generates a label and attaches it to the given parent */ + private Label generateLabel(String text, String tooltip, Composite parent) { + Label label = new Label(parent, SWT.NONE); + label.setText(text); + label.setToolTipText(tooltip); + label.setLayoutData(new GridData(GridData.VERTICAL_ALIGN_CENTER)); + return label; + } + + /** + * Callback when the device name is changed. Enforces that device names + * don't conflict with already existing devices unless we're editing that + * device. + */ + private class CreateNameModifyListener implements ModifyListener { + @Override + public void modifyText(ModifyEvent e) { + String name = mDeviceName.getText(); + boolean nameCollision = false; + for (Device d : mUserDevices) { + if (MANUFACTURER.equals(d.getManufacturer()) && name.equals(d.getName())) { + nameCollision = true; + break; + } + } + mForceCreation.setEnabled(nameCollision); + mForceCreation.setSelection(!nameCollision); + + validatePage(); + } + } + + /** + * Callback attached to the diagonal length and resolution text boxes. Sets + * the screen size and display density based on their values, then validates + * the page. + */ + private class SizeListener implements ModifyListener { + @Override + public void modifyText(ModifyEvent e) { + + if (!mDiagonalLength.getText().isEmpty()) { + try { + double diagonal = Double.parseDouble(mDiagonalLength.getText()); + double diagonalDp = 160.0 * diagonal; + + // Set the Screen Size + if (diagonalDp >= 1200) { + mSize.select(ScreenSize.getIndex(ScreenSize.getEnum("xlarge"))); + } else if (diagonalDp >= 800) { + mSize.select(ScreenSize.getIndex(ScreenSize.getEnum("large"))); + } else if (diagonalDp >= 568) { + mSize.select(ScreenSize.getIndex(ScreenSize.getEnum("normal"))); + } else { + mSize.select(ScreenSize.getIndex(ScreenSize.getEnum("small"))); + } + if (!mXDimension.getText().isEmpty() && !mYDimension.getText().isEmpty()) { + + // Set the density based on which bucket it's closest to + double x = Double.parseDouble(mXDimension.getText()); + double y = Double.parseDouble(mYDimension.getText()); + double dpi = Math.sqrt(x * x + y * y) / diagonal; + double difference = Double.MAX_VALUE; + Density bucket = Density.MEDIUM; + for (Density d : Density.values()) { + if (Math.abs(d.getDpiValue() - dpi) < difference) { + difference = Math.abs(d.getDpiValue() - dpi); + bucket = d; + } + } + mDensity.select(Density.getIndex(bucket)); + } + } catch (NumberFormatException ignore) {} + } + } + } + + + /** + * Callback attached to the keyboard checkbox.Enables / disables device + * states based on the keyboard presence and then validates the page. + */ + private class KeyboardListener extends SelectionAdapter { + @Override + public void widgetSelected(SelectionEvent event) { + super.widgetSelected(event); + if (mKeyboard.getSelection()) { + mPortraitKeys.setEnabled(true); + mPortraitKeysLabel.setEnabled(true); + mLandscapeKeys.setEnabled(true); + mLandscapeKeysLabel.setEnabled(true); + } else { + mPortraitKeys.setEnabled(false); + mPortraitKeysLabel.setEnabled(false); + mLandscapeKeys.setEnabled(false); + mLandscapeKeysLabel.setEnabled(false); + } + toggleNav(); + validatePage(); + } + + } + + /** + * Listens for changes on widgets that affect nav availability and toggles + * the nav checkboxes for device states based on them. + */ + private class NavStateListener extends SelectionAdapter { + @Override + public void widgetSelected(SelectionEvent event) { + super.widgetSelected(event); + toggleNav(); + validatePage(); + } + } + + /** + * Method that inspects all of the relevant dialog state and enables or disables the nav + * elements accordingly. + */ + private void toggleNav() { + mPortraitNav.setEnabled(mPortrait.getSelection() && !mNoNav.getSelection()); + mLandscapeNav.setEnabled(mLandscape.getSelection() && !mNoNav.getSelection()); + mPortraitKeysNav.setEnabled(mPortraitKeys.getSelection() && mPortraitKeys.getEnabled() + && !mNoNav.getSelection()); + mLandscapeKeysNav.setEnabled(mLandscapeKeys.getSelection() + && mLandscapeKeys.getEnabled() && !mNoNav.getSelection()); + validatePage(); + } + + /** + * Callback that validates the page on modification events or widget + * selections + */ + private class ValidationListener extends SelectionAdapter implements ModifyListener { + @Override + public void modifyText(ModifyEvent e) { + validatePage(); + } + + @Override + public void widgetSelected(SelectionEvent e) { + super.widgetSelected(e); + validatePage(); + } + } + + /** + * Validates all of the config options to ensure a valid device can be + * created from them. + * + * @return Whether the config options will result in a valid device. + */ + private boolean validatePage() { + boolean valid = true; + String error = null; + String warning = null; + setError(null); + + String name = mDeviceName.getText(); + + /* If we're editing / cloning a device, this will get called when the name gets pre-filled + * but the ok button won't be populated yet, so we need to skip the initial setting. + */ + if (mOkButton != null) { + if (mDevice == null) { + getShell().setText("Create New Device"); + mOkButton.setText("Create Device"); + } else { + if (mDevice.getName().equals(name)){ + if (mUserDevices.contains(mDevice)) { + getShell().setText("Edit Device"); + mOkButton.setText("Edit Device"); + } else { + warning = "Only user created devices are editable.\nA clone of it will be created under " + + "the \"User\" category."; + getShell().setText("Clone Device"); + mOkButton.setText("Clone Device"); + } + } else { + warning = "The device \"" + mDevice.getName() +"\" will be duplicated into\n" + + "\"" + name + "\" under the \"User\" category"; + getShell().setText("Clone Device"); + mOkButton.setText("Clone Device"); + } + } + } + + if (valid && name.isEmpty()) { + warning = "Please enter a name for the device."; + valid = false; + } + if (valid && !validateFloat("Diagonal Length", mDiagonalLength.getText())) { + warning = "Please enter a screen size."; + valid = false; + } + if (valid && !validateInt("Resolution", mXDimension.getText())) { + warning = "Please enter the screen resolution."; + valid = false; + } + if (valid && !validateInt("Resolution", mYDimension.getText())) { + warning = "Please enter the screen resolution."; + valid = false; + } + if (valid && mSize.getSelectionIndex() < 0) { + error = "A size bucket must be selected."; + valid = false; + } + if (valid && mDensity.getSelectionIndex() < 0) { + error = "A screen density bucket must be selected"; + valid = false; + } + if (valid && mRatio.getSelectionIndex() < 0) { + error = "A screen ratio must be selected."; + valid = false; + } + if (valid && !mNoNav.getSelection() && !mTrackball.getSelection() && !mDpad.getSelection()) { + error = "A mode of hardware navigation, or no navigation, has to be selected."; + valid = false; + } + if (valid && !validateInt("RAM", mRam.getText())) { + warning = "Please enter a RAM amount."; + valid = false; + } + if (valid && mRamCombo.getSelectionIndex() < 0) { + error = "RAM must have a selected unit."; + valid = false; + } + if (valid && mButtons.getSelectionIndex() < 0) { + error = "A button type must be selected."; + valid = false; + } + if (valid) { + if (mKeyboard.getSelection()) { + if (!mPortraitKeys.getSelection() + && !mPortrait.getSelection() + && !mLandscapeKeys.getSelection() + && !mLandscape.getSelection()) { + error = "At least one device state must be enabled."; + valid = false; + } + } else { + if (!mPortrait.getSelection() && !mLandscape.getSelection()) { + error = "At least one device state must be enabled"; + valid = false; + } + } + } + if (mForceCreation.isEnabled() && !mForceCreation.getSelection()) { + error = "Name conflicts with an existing device."; + valid = false; + } + + if (mOkButton != null) { + mOkButton.setEnabled(valid); + } + + if (error != null) { + setError(error); + } else if (warning != null) { + setWarning(warning); + } + + return valid; + } + + /** + * Validates the string is a valid, positive float. If not, it sets the + * error at the bottom of the dialog and returns false. Note this does + * not unset the error message, it's up to the caller to unset it iff + * it knows there are no errors on the page. + */ + private boolean validateFloat(String box, String value) { + if (value == null || value.isEmpty()) { + return false; + } + boolean ret = true; + try { + double val = Double.parseDouble(value); + if (val <= 0) { + ret = false; + } + } catch (NumberFormatException e) { + ret = false; + } + if (!ret) { + setError(box + " must be a valid, positive number."); + } + return ret; + } + + /** + * Validates the string is a valid, positive integer. If not, it sets the + * error at the bottom of the dialog and returns false. Note this does + * not unset the error message, it's up to the caller to unset it iff + * it knows there are no errors on the page. + */ + private boolean validateInt(String box, String value) { + if (value == null || value.isEmpty()) { + return false; + } + boolean ret = true; + try { + int val = Integer.parseInt(value); + if (val <= 0) { + ret = false; + } + } catch (NumberFormatException e) { + ret = false; + } + + if (!ret) { + setError(box + " must be a valid, positive integer."); + } + + return ret; + } + + /** + * Sets the error to the given string. If null, removes the error message. + */ + private void setError(@Nullable String error) { + if (error == null) { + mStatusIcon.setImage(null); + mStatusLabel.setText(""); + } else { + mStatusIcon.setImage(mImageFactory.getImageByName("reject_icon16.png")); + mStatusLabel.setText(error); + } + } + + /** + * Sets the warning message to the given string. If null, removes the + * warning message. + */ + private void setWarning(@Nullable String warning) { + if (warning == null) { + mStatusIcon.setImage(null); + mStatusLabel.setText(""); + } else { + mStatusIcon.setImage(mImageFactory.getImageByName("warning_icon16.png")); + mStatusLabel.setText(warning); + } + } + + /** Sets the hardware for the new device */ + private void prefillWithDevice(@Nullable Device device) { + if (device == null) { + + // Setup the default hardware instance with reasonable values for + // the things which are configurable via this dialog. + mHardware = new Hardware(); + + Screen s = new Screen(); + s.setXdpi(316); + s.setYdpi(316); + s.setMultitouch(Multitouch.JAZZ_HANDS); + s.setMechanism(TouchScreen.FINGER); + s.setScreenType(ScreenType.CAPACITIVE); + mHardware.setScreen(s); + + mHardware.addNetwork(Network.BLUETOOTH); + mHardware.addNetwork(Network.WIFI); + mHardware.addNetwork(Network.NFC); + + mHardware.addSensor(Sensor.BAROMETER); + mHardware.addSensor(Sensor.COMPASS); + mHardware.addSensor(Sensor.LIGHT_SENSOR); + + mHardware.setHasMic(true); + mHardware.addInternalStorage(new Storage(4, Storage.Unit.GiB)); + mHardware.setCpu("Generic CPU"); + mHardware.setGpu("Generic GPU"); + + mHardware.addSupportedAbi(Abi.ARMEABI); + mHardware.addSupportedAbi(Abi.ARMEABI_V7A); + mHardware.addSupportedAbi(Abi.MIPS); + mHardware.addSupportedAbi(Abi.X86); + + mHardware.setChargeType(PowerType.BATTERY); + return; + } + mHardware = device.getDefaultHardware().deepCopy(); + mDeviceName.setText(device.getName()); + mForceCreation.setSelection(true); + Screen s = mHardware.getScreen(); + mDiagonalLength.setText(Double.toString(s.getDiagonalLength())); + mXDimension.setText(Integer.toString(s.getXDimension())); + mYDimension.setText(Integer.toString(s.getYDimension())); + String size = s.getSize().getResourceValue(); + for (int i = 0; i < mSize.getItemCount(); i++) { + if (size.equals(mSize.getItem(i))) { + mSize.select(i); + break; + } + } + String ratio = s.getRatio().getResourceValue(); + for (int i = 0; i < mRatio.getItemCount(); i++) { + if (ratio.equals(mRatio.getItem(i))) { + mRatio.select(i); + break; + } + } + String density = s.getPixelDensity().getResourceValue(); + for (int i = 0; i < mDensity.getItemCount(); i++) { + if (density.equals(mDensity.getItem(i))) { + mDensity.select(i); + break; + } + } + mKeyboard.setSelection(!Keyboard.NOKEY.equals(mHardware.getKeyboard())); + mDpad.setSelection(Navigation.DPAD.equals(mHardware.getNav())); + mTrackball.setSelection(Navigation.TRACKBALL.equals(mHardware.getNav())); + mNoNav.setSelection(Navigation.NONAV.equals(mHardware.getNav())); + mAccelerometer.setSelection(mHardware.getSensors().contains(Sensor.ACCELEROMETER)); + mGyro.setSelection(mHardware.getSensors().contains(Sensor.GYROSCOPE)); + mGps.setSelection(mHardware.getSensors().contains(Sensor.GPS)); + mProximitySensor.setSelection(mHardware.getSensors().contains(Sensor.PROXIMITY_SENSOR)); + mCameraFront.setSelection(false); + mCameraRear.setSelection(false); + for (Camera c : mHardware.getCameras()) { + if (CameraLocation.FRONT.equals(c.getLocation())) { + mCameraFront.setSelection(true); + } else if (CameraLocation.BACK.equals(c.getLocation())) { + mCameraRear.setSelection(true); + } + } + mRam.setText(Long.toString(mHardware.getRam().getSizeAsUnit(Storage.Unit.MiB))); + mRamCombo.select(0); + + for (int i = 0; i < mButtons.getItemCount(); i++) { + if (mButtons.getItem(i).equals(mHardware.getButtonType().getDescription())) { + mButtons.select(i); + break; + } + } + + for (State state : device.getAllStates()) { + Button nav = null; + if (state.getOrientation().equals(ScreenOrientation.PORTRAIT)) { + if (state.getKeyState().equals(KeyboardState.EXPOSED)) { + mPortraitKeys.setSelection(true); + nav = mPortraitKeysNav; + } else { + mPortrait.setSelection(true); + nav = mPortraitNav; + } + } else { + if (state.getKeyState().equals(KeyboardState.EXPOSED)) { + mLandscapeKeys.setSelection(true); + nav = mLandscapeKeysNav; + } else { + mLandscape.setSelection(true); + nav = mLandscapeNav; + } + } + nav.setSelection(state.getNavState().equals(NavigationState.EXPOSED) + && !mHardware.getNav().equals(Navigation.NONAV)); + } + } + + /** + * If given a valid page, generates the corresponding device. The device is + * then added to the user device list, replacing any previous device with + * its given name and manufacturer, and the list is saved out to disk. + */ + @Override + protected void okPressed() { + if (validatePage()) { + Device.Builder builder = new Device.Builder(); + builder.setManufacturer("User"); + builder.setName(mDeviceName.getText()); + + if (mDevice != null) { + builder.setTagId(mDevice.getTagId()); + for (Map.Entry entry : mDevice.getBootProps().entrySet()) { + builder.addBootProp(entry.getKey(), entry.getValue()); + } + } + + builder.addSoftware(mSoftware); + Screen s = mHardware.getScreen(); + double diagonal = Double.parseDouble(mDiagonalLength.getText()); + int x = Integer.parseInt(mXDimension.getText()); + int y = Integer.parseInt(mYDimension.getText()); + s.setDiagonalLength(diagonal); + s.setXDimension(x); + s.setYDimension(y); + // The diagonal DPI will be somewhere in between the X and Y dpi if + // they differ + double dpi = Math.sqrt(x * x + y * y) / diagonal; + // The diagonal DPI should keep only two digits precision. + dpi = Math.round(dpi * 100) / 100.0; + s.setXdpi(dpi); + s.setYdpi(dpi); + s.setPixelDensity(Density.getEnum(mDensity.getText())); + s.setSize(ScreenSize.getEnum(mSize.getText())); + s.setRatio(ScreenRatio.getEnum(mRatio.getText())); + if (mAccelerometer.getSelection()) { + mHardware.addSensor(Sensor.ACCELEROMETER); + } + if (mGyro.getSelection()) { + mHardware.addSensor(Sensor.GYROSCOPE); + } + if (mGps.getSelection()) { + mHardware.addSensor(Sensor.GPS); + } + if (mProximitySensor.getSelection()) { + mHardware.addSensor(Sensor.PROXIMITY_SENSOR); + } + if (mCameraFront.getSelection()) { + Camera c = new Camera(); + c.setAutofocus(true); + c.setFlash(true); + c.setLocation(CameraLocation.FRONT); + mHardware.addCamera(c); + } + if (mCameraRear.getSelection()) { + Camera c = new Camera(); + c.setAutofocus(true); + c.setFlash(true); + c.setLocation(CameraLocation.BACK); + mHardware.addCamera(c); + } + if (mKeyboard.getSelection()) { + mHardware.setKeyboard(Keyboard.QWERTY); + } else { + mHardware.setKeyboard(Keyboard.NOKEY); + } + if (mDpad.getSelection()) { + mHardware.setNav(Navigation.DPAD); + } else if (mTrackball.getSelection()) { + mHardware.setNav(Navigation.TRACKBALL); + } else { + mHardware.setNav(Navigation.NONAV); + } + long ram = Long.parseLong(mRam.getText()); + Storage.Unit unit = Storage.Unit.getEnum(mRamCombo.getText()); + mHardware.setRam(new Storage(ram, unit)); + if (mButtons.getText().equals(ButtonType.HARD.getDescription())) { + mHardware.setButtonType(ButtonType.HARD); + } else { + mHardware.setButtonType(ButtonType.SOFT); + } + + // Set the first enabled state to the default state + boolean defaultSelected = false; + if (mPortrait.getSelection()) { + State state = new State(); + state.setName("Portrait"); + state.setDescription("The device in portrait orientation"); + state.setOrientation(ScreenOrientation.PORTRAIT); + if (mHardware.getNav().equals(Navigation.NONAV) || !mPortraitNav.getSelection()) { + state.setNavState(NavigationState.HIDDEN); + } else { + state.setNavState(NavigationState.EXPOSED); + } + if (mHardware.getKeyboard().equals(Keyboard.NOKEY)) { + state.setKeyState(KeyboardState.SOFT); + } else { + state.setKeyState(KeyboardState.HIDDEN); + } + state.setHardware(mHardware); + if (!defaultSelected) { + state.setDefaultState(true); + defaultSelected = true; + } + builder.addState(state); + } + if (mLandscape.getSelection()) { + State state = new State(); + state.setName("Landscape"); + state.setDescription("The device in landscape orientation"); + state.setOrientation(ScreenOrientation.LANDSCAPE); + if (mHardware.getNav().equals(Navigation.NONAV) || !mLandscapeNav.getSelection()) { + state.setNavState(NavigationState.HIDDEN); + } else { + state.setNavState(NavigationState.EXPOSED); + } + if (mHardware.getKeyboard().equals(Keyboard.NOKEY)) { + state.setKeyState(KeyboardState.SOFT); + } else { + state.setKeyState(KeyboardState.HIDDEN); + } + state.setHardware(mHardware); + if (!defaultSelected) { + state.setDefaultState(true); + defaultSelected = true; + } + builder.addState(state); + } + if (mKeyboard.getSelection()) { + if (mPortraitKeys.getSelection()) { + State state = new State(); + state.setName("Portrait with keyboard"); + state.setDescription("The device in portrait orientation with a keyboard open"); + state.setOrientation(ScreenOrientation.LANDSCAPE); + if (mHardware.getNav().equals(Navigation.NONAV) + || !mPortraitKeysNav.getSelection()) { + state.setNavState(NavigationState.HIDDEN); + } else { + state.setNavState(NavigationState.EXPOSED); + } + state.setKeyState(KeyboardState.EXPOSED); + state.setHardware(mHardware); + if (!defaultSelected) { + state.setDefaultState(true); + defaultSelected = true; + } + builder.addState(state); + } + if (mLandscapeKeys.getSelection()) { + State state = new State(); + state.setName("Landscape with keyboard"); + state.setDescription("The device in landscape orientation with a keyboard open"); + state.setOrientation(ScreenOrientation.LANDSCAPE); + if (mHardware.getNav().equals(Navigation.NONAV) + || !mLandscapeKeysNav.getSelection()) { + state.setNavState(NavigationState.HIDDEN); + } else { + state.setNavState(NavigationState.EXPOSED); + } + state.setKeyState(KeyboardState.EXPOSED); + state.setHardware(mHardware); + if (!defaultSelected) { + state.setDefaultState(true); + defaultSelected = true; + } + builder.addState(state); + } + } + Device d = builder.build(); + if (mForceCreation.isEnabled() && mForceCreation.getSelection()) { + mManager.replaceUserDevice(d); + } else { + mManager.addUserDevice(d); + } + mManager.saveUserDevices(); + mCreatedDevice = d; + super.okPressed(); + } + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/HardwarePropertyChooser.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/HardwarePropertyChooser.java new file mode 100644 index 00000000..b471cc79 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/HardwarePropertyChooser.java @@ -0,0 +1,150 @@ +/* + * 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.sdkuilib.internal.widgets; + +import com.android.sdklib.internal.avd.HardwareProperties.HardwareProperty; +import com.android.sdklib.internal.avd.HardwareProperties.HardwarePropertyType; +import com.android.sdkuilib.ui.GridDialog; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeSet; + +/** + * Dialog to choose a hardware property + */ +class HardwarePropertyChooser extends GridDialog { + + private final Map mProperties; + private final Collection mExceptProperties; + private HardwareProperty mChosenProperty; + private Label mTypeLabel; + private Label mDescriptionLabel; + + HardwarePropertyChooser(Shell parentShell, + Map properties, + Collection exceptProperties) { + super(parentShell, 2, false); + mProperties = properties; + mExceptProperties = exceptProperties; + } + + public HardwareProperty getProperty() { + return mChosenProperty; + } + + @Override + public void createDialogContent(Composite parent) { + Label l = new Label(parent, SWT.NONE); + l.setText("Property:"); + + final Combo c = new Combo(parent, SWT.DROP_DOWN | SWT.READ_ONLY); + // simple list for index->name resolution. + final ArrayList indexToName = new ArrayList(); + + // Sort the combo entries by display name if available, otherwise by hardware name. + Set> entries = + new TreeSet>( + new Comparator>() { + @Override + public int compare(Entry entry0, + Entry entry1) { + String s0 = entry0.getValue().getAbstract(); + String s1 = entry1.getValue().getAbstract(); + if (s0 != null && s1 != null) { + return s0.compareTo(s1); + } + return entry0.getKey().compareTo(entry1.getKey()); + } + }); + entries.addAll(mProperties.entrySet()); + + for (Entry entry : entries) { + if (entry.getValue().isValidForUi() && + mExceptProperties.contains(entry.getKey()) == false) { + c.add(entry.getValue().getAbstract()); + indexToName.add(entry.getKey()); + } + } + boolean hasValues = true; + if (indexToName.size() == 0) { + hasValues = false; + c.add("No properties"); + c.select(0); + c.setEnabled(false); + } + + c.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent event) { + int index = c.getSelectionIndex(); + String name = indexToName.get(index); + processSelection(name, true /* pack */); + } + }); + + l = new Label(parent, SWT.NONE); + l.setText("Type:"); + + mTypeLabel = new Label(parent, SWT.NONE); + + l = new Label(parent, SWT.NONE); + l.setText("Description:"); + + mDescriptionLabel = new Label(parent, SWT.NONE); + + if (hasValues) { + c.select(0); + processSelection(indexToName.get(0), false /* pack */); + } + } + + private void processSelection(String name, boolean pack) { + mChosenProperty = name == null ? null : mProperties.get(name); + + String type = "Unknown"; + String desc = "Unknown"; + + if (mChosenProperty != null) { + desc = mChosenProperty.getDescription(); + HardwarePropertyType vt = mChosenProperty.getType(); + if (vt != null) { + type = vt.getName(); + } + } + + mTypeLabel.setText(type); + mDescriptionLabel.setText(desc); + + if (pack) { + getShell().pack(); + } + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/IMessageBoxLogger.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/IMessageBoxLogger.java new file mode 100644 index 00000000..7ce4c743 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/IMessageBoxLogger.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2014 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.sdkuilib.internal.widgets; + +import com.android.utils.ILogger; + + +/** + * Collects all log and displays it in a message box dialog. + *

+ * This is good if only a few lines of log are expected. + * If you pass logErrorsOnly to the constructor, the message box + * will be shown only if errors were generated, which is the typical use-case. + *

+ * To use this:
+ * - Construct a new {@link IMessageBoxLogger}.
+ * - Pass the logger to the action.
+ * - Once the action is completed, call {@link #displayResult(boolean)} + * indicating whether the operation was successful or not. + * + * When logErrorsOnly is true, if the operation was not successful or errors + * were generated, this will display the message box. + */ +public interface IMessageBoxLogger extends ILogger { + + /** + * Displays the log if anything was captured. + *

+ * @param success Used only when the logger was constructed with logErrorsOnly==true. + * In this case the dialog will only be shown either if success if false or some errors + * where captured. + */ + public void displayResult(final boolean success); +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/ImgDisabledButton.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/ImgDisabledButton.java new file mode 100644 index 00000000..7f1361ec --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/ImgDisabledButton.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2011 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.sdkuilib.internal.widgets; + + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Composite; + +/** + * A label that can display 2 images depending on its enabled/disabled state. + * This acts as a button by firing the {@link SWT#Selection} listener. + */ +public class ImgDisabledButton extends ToggleButton { + public ImgDisabledButton( + Composite parent, + int style, + Image imageEnabled, + Image imageDisabled, + String tooltipEnabled, + String tooltipDisabled) { + super(parent, + style, + imageEnabled, + imageDisabled, + tooltipEnabled, + tooltipDisabled); + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + updateImageAndTooltip(); + redraw(); + } + + @Override + public void setState(int state) { + throw new UnsupportedOperationException(); // not available for this type of button + } + + @Override + public int getState() { + return (isDisposed() || !isEnabled()) ? 1 : 0; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/LegacyAvdEditDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/LegacyAvdEditDialog.java new file mode 100644 index 00000000..ee892637 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/LegacyAvdEditDialog.java @@ -0,0 +1,1439 @@ +/* + * 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.sdkuilib.internal.widgets; + +import com.android.SdkConstants; +import com.android.io.FileWrapper; +import com.android.prefs.AndroidLocation.AndroidLocationException; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.ISystemImage; +import com.android.sdklib.SystemImage; +import com.android.sdklib.internal.avd.AvdInfo; +import com.android.sdklib.internal.avd.AvdManager; +import com.android.sdklib.internal.avd.AvdManager.AvdConflict; +import com.android.sdklib.internal.avd.HardwareProperties; +import com.android.sdklib.internal.avd.HardwareProperties.HardwareProperty; +import com.android.sdklib.internal.project.ProjectProperties; +import com.android.sdklib.repository.descriptors.IdDisplay; +import com.android.sdklib.repository.local.LocalSdk; +import com.android.sdkuilib.internal.repository.icons.ImageFactory; +import com.android.sdkuilib.ui.GridDialog; +import com.android.utils.ILogger; +import com.android.utils.Pair; + +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.viewers.CellEditor; +import org.eclipse.jface.viewers.CellLabelProvider; +import org.eclipse.jface.viewers.ComboBoxCellEditor; +import org.eclipse.jface.viewers.EditingSupport; +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.SelectionChangedEvent; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.viewers.TableViewerColumn; +import org.eclipse.jface.viewers.TextCellEditor; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.jface.viewers.ViewerCell; +import org.eclipse.jface.window.Window; +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.events.VerifyEvent; +import org.eclipse.swt.events.VerifyListener; +import org.eclipse.swt.graphics.Point; +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.FileDialog; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.Text; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TreeMap; +import java.util.regex.Matcher; + +/** + * AVD creation or edit dialog. + * + * @deprecated Replaced by {@link AvdCreationDialog} + */ +final class LegacyAvdEditDialog extends GridDialog { + + private static final String ABI_SYS_IMG_DATA_KEY = "systemImagesData"; //$NON-NLS-1$ + + private final AvdManager mAvdManager; + private final TreeMap mCurrentTargets = + new TreeMap(); + + private final Map mHardwareMap; + private final Map mProperties = new HashMap(); + // a list of user-edited properties. + private final ArrayList mEditedProperties = new ArrayList(); + private final ImageFactory mImageFactory; + private final ILogger mSdkLog; + /** + * The original AvdInfo if we're editing an existing AVD. + * Null when we're creating a new AVD. + */ + private final AvdInfo mEditAvdInfo; + + private Text mAvdName; + private Combo mTargetCombo; + + private Combo mTagAbiCombo; + private String mAbiType; + + private Button mSdCardSizeRadio; + private Text mSdCardSize; + private Combo mSdCardSizeCombo; + + private Text mSdCardFile; + private Button mBrowseSdCard; + private Button mSdCardFileRadio; + + private Button mSnapshotCheck; + + private Button mSkinListRadio; + private Combo mSkinCombo; + + private Button mSkinSizeRadio; + private Text mSkinSizeWidth; + private Text mSkinSizeHeight; + + private TableViewer mHardwareViewer; + private Button mDeleteHardwareProp; + + private Button mForceCreation; + private Button mOkButton; + private Label mStatusIcon; + private Label mStatusLabel; + private Composite mStatusComposite; + + /** + * {@link VerifyListener} for {@link Text} widgets that should only contains numbers. + */ + private final VerifyListener mDigitVerifier = new VerifyListener() { + @Override + public void verifyText(VerifyEvent event) { + int count = event.text.length(); + for (int i = 0 ; i < count ; i++) { + char c = event.text.charAt(i); + if (c < '0' || c > '9') { + event.doit = false; + return; + } + } + } + }; + + /** + * Callback when the AVD name is changed. + * When creating a new AVD, enables the force checkbox if the name is a duplicate. + * When editing an existing AVD, it's OK for the name to match the existing AVD. + */ + private class CreateNameModifyListener implements ModifyListener { + @Override + public void modifyText(ModifyEvent e) { + String name = mAvdName.getText().trim(); + if (mEditAvdInfo == null || !name.equals(mEditAvdInfo.getName())) { + // Case where we're creating a new AVD or editing an existing one + // and the AVD name has been changed... check for name uniqueness. + + Pair conflict = mAvdManager.isAvdNameConflicting(name); + if (conflict.getFirst() != AvdManager.AvdConflict.NO_CONFLICT) { + // If we're changing the state from disabled to enabled, make sure + // to uncheck the button, to force the user to voluntarily re-enforce it. + // This happens when editing an existing AVD and changing the name from + // the existing AVD to another different existing AVD. + if (!mForceCreation.isEnabled()) { + mForceCreation.setEnabled(true); + mForceCreation.setSelection(false); + } + } else { + mForceCreation.setEnabled(false); + mForceCreation.setSelection(false); + } + } else { + // Case where we're editing an existing AVD with the name unchanged. + + mForceCreation.setEnabled(false); + mForceCreation.setSelection(false); + } + validatePage(); + } + } + + /** + * {@link ModifyListener} used for live-validation of the fields content. + */ + private class ValidateListener extends SelectionAdapter implements ModifyListener { + @Override + public void modifyText(ModifyEvent e) { + validatePage(); + } + + @Override + public void widgetSelected(SelectionEvent e) { + super.widgetSelected(e); + validatePage(); + } + } + + /** + * Creates the dialog. Caller should then use {@link Window#open()} and + * refresh if the status is {@link Window#OK}. + * + * @param parentShell The parent shell. + * @param avdManager The existing {@link AvdManager} to use. Must not be null. + * @param imageFactory An existing {@link ImageFactory} to use. Must not be null. + * @param log An existing {@link ILogger} where output will go. Must not be null. + * @param editAvdInfo An optional {@link AvdInfo}. When null, the dialog is used + * to create a new AVD. When non-null, the dialog is used to edit this AVD. + */ + protected LegacyAvdEditDialog(Shell parentShell, + AvdManager avdManager, + ImageFactory imageFactory, + ILogger log, + AvdInfo editAvdInfo) { + super(parentShell, 2, false); + mAvdManager = avdManager; + mImageFactory = imageFactory; + mSdkLog = log; + mEditAvdInfo = editAvdInfo; + + File hardwareDefs = null; + + LocalSdk localSdk = avdManager.getLocalSdk(); + if (localSdk != null) { + File sdkPath = localSdk.getLocation(); + if (sdkPath != null) { + hardwareDefs = new File(new File(sdkPath, SdkConstants.OS_SDK_TOOLS_LIB_FOLDER), + SdkConstants.FN_HARDWARE_INI); + } + } + + if (hardwareDefs == null) { + log.error(null, "Failed to load file %s from SDK", SdkConstants.FN_HARDWARE_INI); + mHardwareMap = new HashMap(); + } else { + mHardwareMap = HardwareProperties.parseHardwareDefinitions( + hardwareDefs, null /*sdkLog*/); + } + } + + @Override + public void create() { + super.create(); + + Point p = getShell().getSize(); + if (p.x < 400) { + p.x = 400; + } + getShell().setSize(p); + } + + @Override + protected Control createContents(Composite parent) { + Control control = super.createContents(parent); + getShell().setText(mEditAvdInfo == null ? "Create new Android Virtual Device (AVD)" + : "Edit Android Virtual Device (AVD)"); + + mOkButton = getButton(IDialogConstants.OK_ID); + + fillExistingAvdInfo(); + validatePage(); + + return control; + } + + @Override + public void createDialogContent(final Composite parent) { + GridData gd; + GridLayout gl; + + Label label = new Label(parent, SWT.NONE); + label.setText("Name:"); + String tooltip = "Name of the new Android Virtual Device"; + label.setToolTipText(tooltip); + + mAvdName = new Text(parent, SWT.BORDER); + mAvdName.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mAvdName.addModifyListener(new CreateNameModifyListener()); + mAvdName.setToolTipText(tooltip); + + label = new Label(parent, SWT.NONE); + label.setText("Target:"); + tooltip = "The version of Android to use in the virtual device"; + label.setToolTipText(tooltip); + + mTargetCombo = new Combo(parent, SWT.READ_ONLY | SWT.DROP_DOWN); + mTargetCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mTargetCombo.setToolTipText(tooltip); + mTargetCombo.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + super.widgetSelected(e); + reloadSkinCombo(); + reloadTagAbiCombo(); + validatePage(); + } + }); + + //ABI group + label = new Label(parent, SWT.NONE); + label.setText("CPU/ABI:"); + tooltip = "The CPU/ABI to use in the virtual device"; + label.setToolTipText(tooltip); + + mTagAbiCombo = new Combo(parent, SWT.READ_ONLY | SWT.DROP_DOWN); + mTagAbiCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mTagAbiCombo.setToolTipText(tooltip); + mTagAbiCombo.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + super.widgetSelected(e); + validatePage(); + } + }); + mTagAbiCombo.setEnabled(false); + + // --- sd card group + label = new Label(parent, SWT.NONE); + label.setText("SD Card:"); + label.setLayoutData(new GridData(GridData.BEGINNING, GridData.BEGINNING, + false, false)); + + final Group sdCardGroup = new Group(parent, SWT.NONE); + sdCardGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + sdCardGroup.setLayout(new GridLayout(3, false)); + + mSdCardSizeRadio = new Button(sdCardGroup, SWT.RADIO); + mSdCardSizeRadio.setText("Size:"); + mSdCardSizeRadio.setToolTipText("Create a new SD Card file"); + mSdCardSizeRadio.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + boolean sizeMode = mSdCardSizeRadio.getSelection(); + enableSdCardWidgets(sizeMode); + validatePage(); + } + }); + + ValidateListener validateListener = new ValidateListener(); + + mSdCardSize = new Text(sdCardGroup, SWT.BORDER); + mSdCardSize.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mSdCardSize.addVerifyListener(mDigitVerifier); + mSdCardSize.addModifyListener(validateListener); + mSdCardSize.setToolTipText("Size of the new SD Card file (must be at least 9 MiB)"); + + mSdCardSizeCombo = new Combo(sdCardGroup, SWT.DROP_DOWN | SWT.READ_ONLY); + mSdCardSizeCombo.add("KiB"); + mSdCardSizeCombo.add("MiB"); + mSdCardSizeCombo.add("GiB"); + mSdCardSizeCombo.select(1); + mSdCardSizeCombo.addSelectionListener(validateListener); + + mSdCardFileRadio = new Button(sdCardGroup, SWT.RADIO); + mSdCardFileRadio.setText("File:"); + mSdCardFileRadio.setToolTipText("Use an existing file for the SD Card"); + + mSdCardFile = new Text(sdCardGroup, SWT.BORDER); + mSdCardFile.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mSdCardFile.addModifyListener(validateListener); + mSdCardFile.setToolTipText("File to use for the SD Card"); + + mBrowseSdCard = new Button(sdCardGroup, SWT.PUSH); + mBrowseSdCard.setText("Browse..."); + mBrowseSdCard.setToolTipText("Select the file to use for the SD Card"); + mBrowseSdCard.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + onBrowseSdCard(); + validatePage(); + } + }); + + mSdCardSizeRadio.setSelection(true); + enableSdCardWidgets(true); + + // --- snapshot group + + label = new Label(parent, SWT.NONE); + label.setText("Snapshot:"); + label.setLayoutData(new GridData(GridData.BEGINNING, GridData.BEGINNING, + false, false)); + + final Group snapshotGroup = new Group(parent, SWT.NONE); + snapshotGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + snapshotGroup.setLayout(new GridLayout(3, false)); + + mSnapshotCheck = new Button(snapshotGroup, SWT.CHECK); + mSnapshotCheck.setText("Enabled"); + mSnapshotCheck.setToolTipText( + "Emulator's state will be persisted between emulator executions"); + + // --- skin group + label = new Label(parent, SWT.NONE); + label.setText("Skin:"); + label.setLayoutData(new GridData(GridData.BEGINNING, GridData.BEGINNING, + false, false)); + + final Group skinGroup = new Group(parent, SWT.NONE); + skinGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + skinGroup.setLayout(new GridLayout(4, false)); + + mSkinListRadio = new Button(skinGroup, SWT.RADIO); + mSkinListRadio.setText("Built-in:"); + mSkinListRadio.setToolTipText("Select an emulated screen size provided by the current Android target"); + mSkinListRadio.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + boolean listMode = mSkinListRadio.getSelection(); + enableSkinWidgets(listMode); + validatePage(); + } + }); + + mSkinCombo = new Combo(skinGroup, SWT.READ_ONLY | SWT.DROP_DOWN); + mSkinCombo.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); + gd.horizontalSpan = 3; + mSkinCombo.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + // get the skin info + loadSkin(); + } + }); + + mSkinSizeRadio = new Button(skinGroup, SWT.RADIO); + mSkinSizeRadio.setText("Resolution:"); + mSkinSizeRadio.setToolTipText("Select a custom emulated screen size"); + + mSkinSizeWidth = new Text(skinGroup, SWT.BORDER); + mSkinSizeWidth.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mSkinSizeWidth.addVerifyListener(mDigitVerifier); + mSkinSizeWidth.addModifyListener(validateListener); + mSkinSizeWidth.setToolTipText("Width in pixels of the emulated screen size"); + + new Label(skinGroup, SWT.NONE).setText("x"); + + mSkinSizeHeight = new Text(skinGroup, SWT.BORDER); + mSkinSizeHeight.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mSkinSizeHeight.addVerifyListener(mDigitVerifier); + mSkinSizeHeight.addModifyListener(validateListener); + mSkinSizeHeight.setToolTipText("Height in pixels of the emulated screen size"); + + mSkinListRadio.setSelection(true); + enableSkinWidgets(true); + + // --- hardware group + label = new Label(parent, SWT.NONE); + label.setText("Hardware:"); + label.setLayoutData(new GridData(GridData.BEGINNING, GridData.BEGINNING, + false, false)); + + final Group hwGroup = new Group(parent, SWT.NONE); + hwGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + hwGroup.setLayout(new GridLayout(2, false)); + + createHardwareTable(hwGroup); + + // composite for the side buttons + Composite hwButtons = new Composite(hwGroup, SWT.NONE); + hwButtons.setLayoutData(new GridData(GridData.FILL_VERTICAL)); + hwButtons.setLayout(gl = new GridLayout(1, false)); + gl.marginHeight = gl.marginWidth = 0; + + Button b = new Button(hwButtons, SWT.PUSH | SWT.FLAT); + b.setText("New..."); + b.setToolTipText("Add a new hardware property"); + b.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + b.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent event) { + HardwarePropertyChooser dialog = new HardwarePropertyChooser(parent.getShell(), + mHardwareMap, mProperties.keySet()); + if (dialog.open() == Window.OK) { + HardwareProperty choice = dialog.getProperty(); + if (choice != null) { + mProperties.put(choice.getName(), choice.getDefault()); + mHardwareViewer.refresh(); + } + } + } + }); + mDeleteHardwareProp = new Button(hwButtons, SWT.PUSH | SWT.FLAT); + mDeleteHardwareProp.setText("Delete"); + mDeleteHardwareProp.setToolTipText("Delete the selected hardware property"); + mDeleteHardwareProp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mDeleteHardwareProp.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + ISelection selection = mHardwareViewer.getSelection(); + if (selection instanceof IStructuredSelection) { + String hwName = (String)((IStructuredSelection)selection).getFirstElement(); + mProperties.remove(hwName); + mHardwareViewer.refresh(); + } + } + }); + mDeleteHardwareProp.setEnabled(false); + + // --- end hardware group + + mForceCreation = new Button(parent, SWT.CHECK); + mForceCreation.setText("Override the existing AVD with the same name"); + mForceCreation.setToolTipText("There's already an AVD with the same name. Check this to delete it and replace it by the new AVD."); + mForceCreation.setLayoutData(new GridData(GridData.BEGINNING, GridData.CENTER, + true, false, 2, 1)); + mForceCreation.setEnabled(false); + mForceCreation.addSelectionListener(validateListener); + + // add a separator to separate from the ok/cancel button + label = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL); + label.setLayoutData(new GridData(GridData.FILL, GridData.CENTER, true, false, 3, 1)); + + // add stuff for the error display + mStatusComposite = new Composite(parent, SWT.NONE); + mStatusComposite.setLayoutData(new GridData(GridData.FILL, GridData.CENTER, + true, false, 3, 1)); + mStatusComposite.setLayout(gl = new GridLayout(2, false)); + gl.marginHeight = gl.marginWidth = 0; + + mStatusIcon = new Label(mStatusComposite, SWT.NONE); + mStatusIcon.setLayoutData(new GridData(GridData.BEGINNING, GridData.BEGINNING, + false, false)); + mStatusLabel = new Label(mStatusComposite, SWT.NONE); + mStatusLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + mStatusLabel.setText(" \n "); //$NON-NLS-1$ + + reloadTargetCombo(); + } + + /** + * Creates the UI for the hardware properties table. + * This creates the {@link Table}, and several viewers ({@link TableViewer}, + * {@link TableViewerColumn}) and adds edit support for the 2nd column + */ + private void createHardwareTable(Composite parent) { + final Table hardwareTable = new Table(parent, SWT.SINGLE | SWT.FULL_SELECTION); + GridData gd = new GridData(GridData.FILL_HORIZONTAL | GridData.FILL_VERTICAL); + gd.widthHint = 200; + gd.heightHint = 100; + hardwareTable.setLayoutData(gd); + hardwareTable.setHeaderVisible(true); + hardwareTable.setLinesVisible(true); + + // -- Table viewer + mHardwareViewer = new TableViewer(hardwareTable); + mHardwareViewer.addSelectionChangedListener(new ISelectionChangedListener() { + @Override + public void selectionChanged(SelectionChangedEvent event) { + // it's a single selection mode, we can just access the selection index + // from the table directly. + mDeleteHardwareProp.setEnabled(hardwareTable.getSelectionIndex() != -1); + } + }); + + // only a content provider. Use viewers per column below (for editing support) + mHardwareViewer.setContentProvider(new IStructuredContentProvider() { + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // we can just ignore this. we just use mProperties directly. + } + + @Override + public Object[] getElements(Object arg0) { + return mProperties.keySet().toArray(); + } + + @Override + public void dispose() { + // pass + } + }); + + // -- column 1: prop abstract name + TableColumn col1 = new TableColumn(hardwareTable, SWT.LEFT); + col1.setText("Property"); + col1.setWidth(150); + TableViewerColumn tvc1 = new TableViewerColumn(mHardwareViewer, col1); + tvc1.setLabelProvider(new CellLabelProvider() { + @Override + public void update(ViewerCell cell) { + String name = cell.getElement().toString(); + HardwareProperty prop = mHardwareMap.get(name); + if (prop != null) { + cell.setText(prop.getAbstract()); + } else { + cell.setText(String.format("%1$s (Unknown)", name)); + } + } + }); + + // -- column 2: prop value + TableColumn col2 = new TableColumn(hardwareTable, SWT.LEFT); + col2.setText("Value"); + col2.setWidth(50); + TableViewerColumn tvc2 = new TableViewerColumn(mHardwareViewer, col2); + tvc2.setLabelProvider(new CellLabelProvider() { + @Override + public void update(ViewerCell cell) { + String value = mProperties.get(cell.getElement()); + cell.setText(value != null ? value : ""); + } + }); + + // add editing support to the 2nd column + tvc2.setEditingSupport(new EditingSupport(mHardwareViewer) { + @Override + protected void setValue(Object element, Object value) { + String hardwareName = (String)element; + HardwareProperty property = mHardwareMap.get(hardwareName); + int index; + switch (property.getType()) { + case INTEGER: + mProperties.put((String)element, (String)value); + break; + case DISKSIZE: + if (HardwareProperties.DISKSIZE_PATTERN.matcher((String)value).matches()) { + mProperties.put((String)element, (String)value); + } + break; + case BOOLEAN: + index = (Integer)value; + mProperties.put((String)element, HardwareProperties.getBooleanValue(index)); + break; + case STRING_ENUM: + case INTEGER_ENUM: + // For a combo, value is the index of the enum to use. + index = (Integer)value; + String[] values = property.getEnum(); + if (values != null && values.length > index) { + mProperties.put((String)element, values[index]); + } + break; + } + mHardwareViewer.refresh(element); + } + + @Override + protected Object getValue(Object element) { + String hardwareName = (String)element; + HardwareProperty property = mHardwareMap.get(hardwareName); + String value = mProperties.get(hardwareName); + switch (property.getType()) { + case INTEGER: + // intended fall-through. + case DISKSIZE: + return value; + case BOOLEAN: + return HardwareProperties.getBooleanValueIndex(value); + case STRING_ENUM: + case INTEGER_ENUM: + // For a combo, we need to return the index of the value in the enum + String[] values = property.getEnum(); + if (values != null) { + for (int i = 0; i < values.length; i++) { + if (values[i].equals(value)) { + return i; + } + } + } + } + + return null; + } + + @Override + protected CellEditor getCellEditor(Object element) { + String hardwareName = (String)element; + HardwareProperty property = mHardwareMap.get(hardwareName); + switch (property.getType()) { + // TODO: custom TextCellEditor that restrict input. + case INTEGER: + // intended fall-through. + case DISKSIZE: + return new TextCellEditor(hardwareTable); + case BOOLEAN: + return new ComboBoxCellEditor(hardwareTable, + new String[] { + HardwareProperties.getBooleanValue(0), + HardwareProperties.getBooleanValue(1), + }, + SWT.READ_ONLY | SWT.DROP_DOWN); + case STRING_ENUM: + case INTEGER_ENUM: + String[] values = property.getEnum(); + if (values != null && values.length > 0) { + return new ComboBoxCellEditor(hardwareTable, + values, + SWT.READ_ONLY | SWT.DROP_DOWN); + } + } + return null; + } + + @Override + protected boolean canEdit(Object element) { + String hardwareName = (String)element; + HardwareProperty property = mHardwareMap.get(hardwareName); + return property != null; + } + }); + + + mHardwareViewer.setInput(mProperties); + } + + // -- Start of internal part ---------- + // Hide everything down-below from SWT designer + //$hide>>$ + + /** + * When editing an existing AVD info, fill the UI that has just been created with + * the values from the AVD. + */ + public void fillExistingAvdInfo() { + if (mEditAvdInfo == null) { + return; + } + + mAvdName.setText(mEditAvdInfo.getName()); + + Map props = mEditAvdInfo.getProperties(); + + IAndroidTarget target = mEditAvdInfo.getTarget(); + if (target != null && !mCurrentTargets.isEmpty()) { + // Try to select the target in the target combo. + // This will fail if the AVD needs to be repaired. + // + // This is a linear search but the list is always + // small enough and we only do this once. + int n = mTargetCombo.getItemCount(); + for (int i = 0;i < n; i++) { + if (target.equals(mCurrentTargets.get(mTargetCombo.getItem(i)))) { + mTargetCombo.select(i); + reloadTagAbiCombo(); + reloadSkinCombo(); + break; + } + } + } + + // select the abi type + ISystemImage[] systemImages = getSystemImages(target); + if (target != null && systemImages.length > 0) { + mTagAbiCombo.setEnabled(systemImages.length > 1); + String abiType = AvdInfo.getPrettyAbiType(mEditAvdInfo); + int n = mTagAbiCombo.getItemCount(); + for (int i = 0; i < n; i++) { + if (abiType.equals(mTagAbiCombo.getItem(i))) { + mTagAbiCombo.select(i); + reloadSkinCombo(); + break; + } + } + } + + if (props != null) { + + // First try the skin name and if it doesn't work fallback on the skin path + nextSkin: for (int s = 0; s < 2; s++) { + String skin = props.get(s == 0 ? AvdManager.AVD_INI_SKIN_NAME + : AvdManager.AVD_INI_SKIN_PATH); + if (skin != null && skin.length() > 0) { + Matcher m = AvdManager.NUMERIC_SKIN_SIZE.matcher(skin); + if (m.matches() && m.groupCount() == 2) { + enableSkinWidgets(false); + mSkinListRadio.setSelection(false); + mSkinSizeRadio.setSelection(true); + mSkinSizeWidth.setText(m.group(1)); + mSkinSizeHeight.setText(m.group(2)); + break nextSkin; + } else { + enableSkinWidgets(true); + mSkinSizeRadio.setSelection(false); + mSkinListRadio.setSelection(true); + + int n = mSkinCombo.getItemCount(); + for (int i = 0; i < n; i++) { + if (skin.equals(mSkinCombo.getItem(i))) { + mSkinCombo.select(i); + break nextSkin; + } + } + } + } + } + + String sdcard = props.get(AvdManager.AVD_INI_SDCARD_PATH); + if (sdcard != null && sdcard.length() > 0) { + enableSdCardWidgets(false); + mSdCardSizeRadio.setSelection(false); + mSdCardFileRadio.setSelection(true); + mSdCardFile.setText(sdcard); + } + + sdcard = props.get(AvdManager.AVD_INI_SDCARD_SIZE); + if (sdcard != null && sdcard.length() > 0) { + String[] values = new String[2]; + long sdcardSize = AvdManager.parseSdcardSize(sdcard, values); + + if (sdcardSize != AvdManager.SDCARD_NOT_SIZE_PATTERN) { + enableSdCardWidgets(true); + mSdCardFileRadio.setSelection(false); + mSdCardSizeRadio.setSelection(true); + + mSdCardSize.setText(values[0]); + + String suffix = values[1]; + int n = mSdCardSizeCombo.getItemCount(); + for (int i = 0; i < n; i++) { + if (mSdCardSizeCombo.getItem(i).startsWith(suffix)) { + mSdCardSizeCombo.select(i); + } + } + } + } + + String snapshots = props.get(AvdManager.AVD_INI_SNAPSHOT_PRESENT); + if (snapshots != null && snapshots.length() > 0) { + mSnapshotCheck.setSelection(snapshots.equals("true")); + } + } + + mProperties.clear(); + + if (props != null) { + for (Entry entry : props.entrySet()) { + HardwareProperty prop = mHardwareMap.get(entry.getKey()); + if (prop != null && prop.isValidForUi()) { + mProperties.put(entry.getKey(), entry.getValue()); + } + } + } + + // Cleanup known non-hardware properties + mProperties.remove(AvdManager.AVD_INI_ABI_TYPE); + mProperties.remove(AvdManager.AVD_INI_CPU_ARCH); + mProperties.remove(AvdManager.AVD_INI_SKIN_PATH); + mProperties.remove(AvdManager.AVD_INI_SKIN_NAME); + mProperties.remove(AvdManager.AVD_INI_SDCARD_SIZE); + mProperties.remove(AvdManager.AVD_INI_SDCARD_PATH); + mProperties.remove(AvdManager.AVD_INI_SNAPSHOT_PRESENT); + mProperties.remove(AvdManager.AVD_INI_IMAGES_1); + mProperties.remove(AvdManager.AVD_INI_IMAGES_2); + + mHardwareViewer.refresh(); + } + + @Override + protected void okPressed() { + if (createAvd()) { + super.okPressed(); + } + } + + /** + * Enable or disable the sd card widgets. + * @param sizeMode if true the size-based widgets are to be enabled, and the file-based ones + * disabled. + */ + private void enableSdCardWidgets(boolean sizeMode) { + mSdCardSize.setEnabled(sizeMode); + mSdCardSizeCombo.setEnabled(sizeMode); + + mSdCardFile.setEnabled(!sizeMode); + mBrowseSdCard.setEnabled(!sizeMode); + } + + /** + * Enable or disable the skin widgets. + * @param listMode if true the list-based widgets are to be enabled, and the size-based ones + * disabled. + */ + private void enableSkinWidgets(boolean listMode) { + mSkinCombo.setEnabled(listMode); + + mSkinSizeWidth.setEnabled(!listMode); + mSkinSizeHeight.setEnabled(!listMode); + } + + + private void onBrowseSdCard() { + FileDialog dlg = new FileDialog(getContents().getShell(), SWT.OPEN); + dlg.setText("Choose SD Card image file."); + + String fileName = dlg.open(); + if (fileName != null) { + mSdCardFile.setText(fileName); + } + } + + + + private void reloadTargetCombo() { + String selected = null; + int index = mTargetCombo.getSelectionIndex(); + if (index >= 0) { + selected = mTargetCombo.getItem(index); + } + + mCurrentTargets.clear(); + mTargetCombo.removeAll(); + + boolean found = false; + index = -1; + + LocalSdk localSdk = mAvdManager.getLocalSdk(); + if (localSdk != null) { + for (IAndroidTarget target : localSdk.getTargets()) { + String name; + if (target.isPlatform()) { + name = String.format("%s - API Level %s", + target.getName(), + target.getVersion().getApiString()); + } else { + name = String.format("%s (%s) - API Level %s", + target.getName(), + target.getVendor(), + target.getVersion().getApiString()); + } + mCurrentTargets.put(name, target); + mTargetCombo.add(name); + if (!found) { + index++; + found = name.equals(selected); + } + } + } + + mTargetCombo.setEnabled(mCurrentTargets.size() > 0); + + if (found) { + mTargetCombo.select(index); + } + + reloadSkinCombo(); + } + + private void reloadSkinCombo() { + String selected = null; + int index = mSkinCombo.getSelectionIndex(); + if (index >= 0) { + selected = mSkinCombo.getItem(index); + } + + mSkinCombo.removeAll(); + mSkinCombo.setEnabled(false); + + index = mTargetCombo.getSelectionIndex(); + if (index >= 0) { + + String targetName = mTargetCombo.getItem(index); + + boolean found = false; + IAndroidTarget target = mCurrentTargets.get(targetName); + if (target != null) { + mSkinCombo.add(String.format("Default (%s)", target.getDefaultSkin())); + + index = -1; + for (File skin : target.getSkins()) { + mSkinCombo.add(skin.getName()); + if (!found) { + index++; + found = skin.equals(selected); + } + } + + mSkinCombo.setEnabled(true); + + if (found) { + mSkinCombo.select(index); + } else { + mSkinCombo.select(0); // default + loadSkin(); + } + } + } + } + + /** + * Reload all the abi types in the selection list + */ + private void reloadTagAbiCombo() { + String selected = null; + boolean found = false; + + int index = mTargetCombo.getSelectionIndex(); + if (index >= 0) { + String targetName = mTargetCombo.getItem(index); + IAndroidTarget target = mCurrentTargets.get(targetName); + + ISystemImage[] systemImages = getSystemImages(target); + + // keep a reference to the array into the combo app data field + // so that we can lookup the tag/abi later in getSelectedAbiType() + mTagAbiCombo.setData(ABI_SYS_IMG_DATA_KEY, systemImages); + + mTagAbiCombo.setEnabled(systemImages.length > 1); + + // If user explicitly selected an ABI before, preserve that option + // If user did not explicitly select before (only one option before) + // force them to select + index = mTagAbiCombo.getSelectionIndex(); + if (index >= 0 && mTagAbiCombo.getItemCount() > 1) { + selected = mTagAbiCombo.getItem(index); + } + + mTagAbiCombo.removeAll(); + + int i; + for ( i = 0; i < systemImages.length ; i++ ) { + String prettyAbiType = AvdInfo.getPrettyAbiType(systemImages[i]); + mTagAbiCombo.add(prettyAbiType); + if (!found) { + found = prettyAbiType.equals(selected); + if (found) { + mTagAbiCombo.select(i); + } + } + } + + if (systemImages.length == 1) { + mTagAbiCombo.select(0); + } + } + } + + /** + * Validates the fields, displays errors and warnings. + * Enables the finish button if there are no errors. + */ + private void validatePage() { + String error = null; + String warning = null; + + // Validate AVD name + String avdName = mAvdName.getText().trim(); + boolean hasAvdName = avdName.length() > 0; + boolean isCreate = mEditAvdInfo == null || !avdName.equals(mEditAvdInfo.getName()); + + if (hasAvdName && !AvdManager.RE_AVD_NAME.matcher(avdName).matches()) { + error = String.format( + "AVD name '%1$s' contains invalid characters.\nAllowed characters are: %2$s", + avdName, AvdManager.CHARS_AVD_NAME); + } + + // Validate target + if (hasAvdName && error == null && mTargetCombo.getSelectionIndex() < 0) { + error = "A target must be selected in order to create an AVD."; + } + + // validate abi type if the selected target supports multi archs. + if (hasAvdName && error == null && mTargetCombo.getSelectionIndex() > 0) { + int index = mTargetCombo.getSelectionIndex(); + String targetName = mTargetCombo.getItem(index); + IAndroidTarget target = mCurrentTargets.get(targetName); + ISystemImage[] systemImages = getSystemImages(target); + if (systemImages.length > 1 && mTagAbiCombo.getSelectionIndex() < 0) { + error = "An ABI type must be selected in order to create an AVD."; + } + } + + // Validate SDCard path or value + if (error == null) { + // get the mode. We only need to check the file since the + // verifier on the size Text will prevent invalid input + boolean sdcardFileMode = mSdCardFileRadio.getSelection(); + if (sdcardFileMode) { + String sdName = mSdCardFile.getText().trim(); + if (sdName.length() > 0 && !new File(sdName).isFile()) { + error = "SD Card path isn't valid."; + } + } else { + String valueString = mSdCardSize.getText(); + if (valueString.length() > 0) { + long value = 0; + try { + value = Long.parseLong(valueString); + + int sizeIndex = mSdCardSizeCombo.getSelectionIndex(); + if (sizeIndex >= 0) { + // index 0 shifts by 10 (1024=K), index 1 by 20, etc. + value <<= 10*(1 + sizeIndex); + } + + if (value < AvdManager.SDCARD_MIN_BYTE_SIZE || + value > AvdManager.SDCARD_MAX_BYTE_SIZE) { + value = 0; + } + } catch (Exception e) { + // ignore, we'll test value below. + } + if (value <= 0) { + error = "SD Card size is invalid. Range is 9 MiB..1023 GiB."; + } else if (mEditAvdInfo != null) { + // When editing an existing AVD, compare with the existing + // sdcard size, if any. It only matters if there was an sdcard setting + // before. + Map props = mEditAvdInfo.getProperties(); + if (props != null) { + String original = + mEditAvdInfo.getProperties().get(AvdManager.AVD_INI_SDCARD_SIZE); + if (original != null && original.length() > 0) { + long originalSize = + AvdManager.parseSdcardSize(original, null/*parsedStrings*/); + if (originalSize > 0 && value != originalSize) { + warning = "A new SD Card file will be created.\nThe current SD Card file will be lost."; + } + } + } + } + } + } + } + + // validate the skin + if (error == null) { + // get the mode, we should only check if it's in size mode since + // the built-in list mode is always valid. + if (mSkinSizeRadio.getSelection()) { + // need both with and heigh to be non null + String width = mSkinSizeWidth.getText(); // no need for trim, since the verifier + String height = mSkinSizeHeight.getText(); // rejects non digit. + + if (width.length() == 0 || height.length() == 0) { + error = "Skin size is incorrect.\nBoth dimensions must be > 0."; + } + } + } + + // Check for duplicate AVD name + if (isCreate && hasAvdName && error == null && !mForceCreation.getSelection()) { + Pair conflict = mAvdManager.isAvdNameConflicting(avdName); + assert conflict != null; + switch(conflict.getFirst()) { + case NO_CONFLICT: + break; + case CONFLICT_EXISTING_AVD: + case CONFLICT_INVALID_AVD: + error = String.format( + "The AVD name '%s' is already used.\n" + + "Check \"Override the existing AVD\" to delete the existing one.", + avdName); + break; + case CONFLICT_EXISTING_PATH: + error = String.format( + "Conflict with %s\n" + + "Check \"Override the existing AVD\" to delete the existing one.", + conflict.getSecond()); + break; + default: + // Hmm not supposed to happen... probably someone expanded the + // enum without adding something here. In this case just do an + // assert and use a generic error message. + error = String.format( + "Conflict %s with %s.\n" + + "Check \"Override the existing AVD\" to delete the existing one.", + conflict.getFirst().toString(), + conflict.getSecond()); + assert false; + break; + } + } + + if (error == null && mEditAvdInfo != null && isCreate) { + warning = String.format("The AVD '%1$s' will be duplicated into '%2$s'.", + mEditAvdInfo.getName(), + avdName); + } + + // Validate the create button + boolean can_create = hasAvdName && error == null; + if (can_create) { + can_create &= mTargetCombo.getSelectionIndex() >= 0; + } + mOkButton.setEnabled(can_create); + + // Adjust the create button label as needed + if (isCreate) { + mOkButton.setText("Create AVD"); + } else { + mOkButton.setText("Edit AVD"); + } + + // -- update UI + if (error != null) { + mStatusIcon.setImage(mImageFactory.getImageByName("reject_icon16.png")); //$NON-NLS-1$ + mStatusLabel.setText(error); + } else if (warning != null) { + mStatusIcon.setImage(mImageFactory.getImageByName("warning_icon16.png")); //$NON-NLS-1$ + mStatusLabel.setText(warning); + } else { + mStatusIcon.setImage(null); + mStatusLabel.setText(" \n "); //$NON-NLS-1$ + } + + mStatusComposite.pack(true); + } + + private void loadSkin() { + int targetIndex = mTargetCombo.getSelectionIndex(); + if (targetIndex < 0) { + return; + } + + // resolve the target. + String targetName = mTargetCombo.getItem(targetIndex); + IAndroidTarget target = mCurrentTargets.get(targetName); + if (target == null) { + return; + } + + // get the skin name + String skinName = null; + int skinIndex = mSkinCombo.getSelectionIndex(); + if (skinIndex < 0) { + return; + } else if (skinIndex == 0) { // default skin for the target + File skin = target.getDefaultSkin(); + if (skin != null) { + skinName = skin.getName(); + } + } else { + skinName = mSkinCombo.getItem(skinIndex); + } + + // load the skin properties + String path = target.getPath(IAndroidTarget.SKINS); + File skin = new File(path, skinName); + if (skin.isDirectory() == false && target.isPlatform() == false) { + // it's possible the skin is in the parent target + path = target.getParent().getPath(IAndroidTarget.SKINS); + skin = new File(path, skinName); + } + + if (skin.isDirectory() == false) { + return; + } + + // now get the hardware.ini from the add-on (if applicable) and from the skin + // (if applicable) + HashMap hardwareValues = new HashMap(); + if (target.isPlatform() == false) { + FileWrapper targetHardwareFile = new FileWrapper(target.getLocation(), + AvdManager.HARDWARE_INI); + if (targetHardwareFile.isFile()) { + Map targetHardwareConfig = ProjectProperties.parsePropertyFile( + targetHardwareFile, null /*log*/); + + if (targetHardwareConfig != null) { + hardwareValues.putAll(targetHardwareConfig); + } + } + } + + // from the skin + FileWrapper skinHardwareFile = new FileWrapper(skin, AvdManager.HARDWARE_INI); + if (skinHardwareFile.isFile()) { + Map skinHardwareConfig = ProjectProperties.parsePropertyFile( + skinHardwareFile, null /*log*/); + + if (skinHardwareConfig != null) { + hardwareValues.putAll(skinHardwareConfig); + } + } + + // now set those values in the list of properties for the AVD. + // We just check that none of those properties have been edited by the user yet. + for (Entry entry : hardwareValues.entrySet()) { + if (mEditedProperties.contains(entry.getKey()) == false) { + mProperties.put(entry.getKey(), entry.getValue()); + } + } + + mHardwareViewer.refresh(); + } + + /** + * Creates an AVD from the values in the UI. Called when the user presses the OK button. + */ + private boolean createAvd() { + String avdName = mAvdName.getText().trim(); + int index = mTargetCombo.getSelectionIndex(); + + // quick check on the name and the target selection + if (avdName.length() == 0 || index < 0) { + return false; + } + + // resolve the target. + String targetName = mTargetCombo.getItem(index); + IAndroidTarget target = mCurrentTargets.get(targetName); + if (target == null) { + return false; + } + + // get the tag & abi type + IdDisplay tag = SystemImage.DEFAULT_TAG; + mAbiType = SdkConstants.ABI_ARMEABI; + Object appData = mTagAbiCombo.getData(ABI_SYS_IMG_DATA_KEY); + if (appData instanceof ISystemImage[]) { + int abiIndex = mTagAbiCombo.getSelectionIndex(); + ISystemImage[] systemImages = (ISystemImage[]) appData; + if (abiIndex >= 0 && abiIndex < systemImages.length) { + ISystemImage systemImage = systemImages[abiIndex]; + tag = systemImage.getTag(); + mAbiType = systemImage.getAbiType(); + } + } + + // get the SD card data from the UI. + String sdName = null; + if (mSdCardSizeRadio.getSelection()) { + // size mode + String value = mSdCardSize.getText().trim(); + if (value.length() > 0) { + sdName = value; + // add the unit + switch (mSdCardSizeCombo.getSelectionIndex()) { + case 0: + sdName += "K"; //$NON-NLS-1$ + break; + case 1: + sdName += "M"; //$NON-NLS-1$ + break; + case 2: + sdName += "G"; //$NON-NLS-1$ + break; + default: + // shouldn't be here + assert false; + } + } + } else { + // file mode. + sdName = mSdCardFile.getText().trim(); + } + + // get the Skin data from the UI + String skinName = null; + if (mSkinListRadio.getSelection()) { + // built-in list provides the skin + int skinIndex = mSkinCombo.getSelectionIndex(); + if (skinIndex > 0) { + // index 0 is the default, we don't use it + skinName = mSkinCombo.getItem(skinIndex); + } + } else { + // size mode. get both size and writes it as a skin name + // thanks to validatePage() we know the content of the fields is correct + skinName = mSkinSizeWidth.getText() + "x" + mSkinSizeHeight.getText(); //$NON-NLS-1$ + } + + ILogger log = mSdkLog; + if (log == null || log instanceof MessageBoxLog) { + // If the current logger is a message box, we use our own (to make sure + // to display errors right away and customize the title). + log = new MessageBoxLog( + String.format("Result of creating AVD '%s':", avdName), + getContents().getDisplay(), + false /*logErrorsOnly*/); + } + + File avdFolder = null; + try { + avdFolder = AvdInfo.getDefaultAvdFolder(mAvdManager, avdName); + } catch (AndroidLocationException e) { + return false; + } + + boolean force = mForceCreation.getSelection(); + boolean snapshot = mSnapshotCheck.getSelection(); + + boolean success = false; + AvdInfo avdInfo = mAvdManager.createAvd( + avdFolder, + avdName, + target, + tag, + mAbiType, + null, //skinFolder + skinName, + sdName, + mProperties, + null, // bootProps + snapshot, + force, + mEditAvdInfo != null, //edit existing + log); + + success = avdInfo != null; + + if (log instanceof MessageBoxLog) { + ((MessageBoxLog) log).displayResult(success); + } + return success; + } + + /** + * Returns the list of system images of a target. + *

+ * If target is null, returns an empty list. + * If target is an add-on with no system images, return the list from its parent platform. + * + * @param target An IAndroidTarget. Can be null. + * @return A non-null ISystemImage array. Can be empty. + */ + private ISystemImage[] getSystemImages(IAndroidTarget target) { + if (target != null) { + ISystemImage[] images = target.getSystemImages(); + + if ((images == null || images.length == 0) && !target.isPlatform()) { + // If an add-on does not provide any system images, use the ones from the parent. + images = target.getParent().getSystemImages(); + } + + if (images != null) { + return images; + } + } + + return new ISystemImage[0]; + } + + // End of hiding from SWT Designer + //$hide<<$ +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/MessageBoxLog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/MessageBoxLog.java new file mode 100644 index 00000000..68463ee0 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/MessageBoxLog.java @@ -0,0 +1,150 @@ +/* + * 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.sdkuilib.internal.widgets; + +import com.android.annotations.NonNull; + +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +import java.util.ArrayList; + + +/** + * Collects all log and displays it in a message box dialog. + *

+ * This is good if only a few lines of log are expected. + * If you pass logErrorsOnly to the constructor, the message box + * will be shown only if errors were generated, which is the typical use-case. + *

+ * To use this:
+ * - Construct a new {@link MessageBoxLog}.
+ * - Pass the logger to the action.
+ * - Once the action is completed, call {@link #displayResult(boolean)} + * indicating whether the operation was successful or not. + * + * When logErrorsOnly is true, if the operation was not successful or errors + * were generated, this will display the message box. + */ +public final class MessageBoxLog implements IMessageBoxLogger { + + final ArrayList logMessages = new ArrayList(); + private final String mMessage; + private final Display mDisplay; + private final boolean mLogErrorsOnly; + + /** + * Creates a logger that will capture all logs and eventually display them + * in a simple message box. + * + * @param message + * @param display + * @param logErrorsOnly + */ + public MessageBoxLog(String message, Display display, boolean logErrorsOnly) { + mMessage = message; + mDisplay = display; + mLogErrorsOnly = logErrorsOnly; + } + + @Override + public void error(Throwable throwable, String errorFormat, Object... arg) { + if (errorFormat != null) { + logMessages.add(String.format("Error: " + errorFormat, arg)); + } + + if (throwable != null) { + logMessages.add(throwable.getMessage()); + } + } + + @Override + public void warning(@NonNull String warningFormat, Object... arg) { + if (!mLogErrorsOnly) { + logMessages.add(String.format("Warning: " + warningFormat, arg)); + } + } + + @Override + public void info(@NonNull String msgFormat, Object... arg) { + if (!mLogErrorsOnly) { + logMessages.add(String.format(msgFormat, arg)); + } + } + + @Override + public void verbose(@NonNull String msgFormat, Object... arg) { + if (!mLogErrorsOnly) { + logMessages.add(String.format(msgFormat, arg)); + } + } + + /** + * Displays the log if anything was captured. + *

+ * @param success Used only when the logger was constructed with logErrorsOnly==true. + * In this case the dialog will only be shown either if success if false or some errors + * where captured. + */ + @Override + public void displayResult(final boolean success) { + if (logMessages.size() > 0) { + final StringBuilder sb = new StringBuilder(mMessage + "\n\n"); + for (String msg : logMessages) { + if (msg.length() > 0) { + if (msg.charAt(0) != '\n') { + int n = sb.length(); + if (n > 0 && sb.charAt(n-1) != '\n') { + sb.append('\n'); + } + } + sb.append(msg); + } + } + + // display the message + // dialog box only run in ui thread.. + if (mDisplay != null && !mDisplay.isDisposed()) { + mDisplay.asyncExec(new Runnable() { + @Override + public void run() { + // This is typically displayed at the end, so make sure the UI + // instances are not disposed. + Shell shell = null; + if (mDisplay != null && !mDisplay.isDisposed()) { + shell = mDisplay.getActiveShell(); + } + if (shell == null || shell.isDisposed()) { + return; + } + // Use the success icon if the call indicates success. + // However just use the error icon if the logger was only recording errors. + if (success && !mLogErrorsOnly) { + MessageDialog.openInformation(shell, "Android Virtual Devices Manager", + sb.toString()); + } else { + MessageDialog.openError(shell, "Android Virtual Devices Manager", + sb.toString()); + + } + } + }); + } + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/ResolutionChooserDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/ResolutionChooserDialog.java new file mode 100644 index 00000000..e5e1d6cf --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/ResolutionChooserDialog.java @@ -0,0 +1,123 @@ +/* + * 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.sdkuilib.internal.widgets; + +import com.android.sdkuilib.ui.GridDialog; + +import org.eclipse.jface.dialogs.IDialogConstants; +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.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.Label; +import org.eclipse.swt.widgets.Monitor; +import org.eclipse.swt.widgets.Shell; + +/** + * Small dialog to let a user choose a screen size (from a fixed list) and a resolution + * (as returned by {@link Display#getMonitors()}). + + * After the dialog as returned, one can query {@link #getDensity()} to get the chosen monitor + * pixel density. + */ +public class ResolutionChooserDialog extends GridDialog { + public final static float[] MONITOR_SIZES = new float[] { + 13.3f, 14, 15.4f, 15.6f, 17, 19, 20, 21, 24, 30, + }; + + private Button mButton; + private Combo mScreenSizeCombo; + private Combo mMonitorCombo; + + private Monitor[] mMonitors; + private int mScreenSizeIndex = -1; + private int mMonitorIndex = 0; + + public ResolutionChooserDialog(Shell parentShell) { + super(parentShell, 2, false); + } + + /** + * Returns the pixel density of the user-chosen monitor. + */ + public int getDensity() { + float size = MONITOR_SIZES[mScreenSizeIndex]; + Rectangle rect = mMonitors[mMonitorIndex].getBounds(); + + // compute the density + double d = Math.sqrt(rect.width * rect.width + rect.height * rect.height) / size; + return (int)Math.round(d); + } + + @Override + protected void configureShell(Shell newShell) { + newShell.setText("Monitor Density"); + super.configureShell(newShell); + } + + @Override + protected Control createContents(Composite parent) { + Control control = super.createContents(parent); + mButton = getButton(IDialogConstants.OK_ID); + mButton.setEnabled(false); + return control; + } + + @Override + public void createDialogContent(Composite parent) { + Label l = new Label(parent, SWT.NONE); + l.setText("Screen Size:"); + + mScreenSizeCombo = new Combo(parent, SWT.DROP_DOWN | SWT.READ_ONLY); + for (float size : MONITOR_SIZES) { + if (Math.round(size) == size) { + mScreenSizeCombo.add(String.format("%.0f\"", size)); + } else { + mScreenSizeCombo.add(String.format("%.1f\"", size)); + } + } + mScreenSizeCombo.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + mScreenSizeIndex = mScreenSizeCombo.getSelectionIndex(); + mButton.setEnabled(mScreenSizeIndex != -1); + } + }); + + l = new Label(parent, SWT.NONE); + l.setText("Resolution:"); + + mMonitorCombo = new Combo(parent, SWT.DROP_DOWN | SWT.READ_ONLY); + mMonitors = parent.getDisplay().getMonitors(); + for (Monitor m : mMonitors) { + Rectangle r = m.getBounds(); + mMonitorCombo.add(String.format("%d x %d", r.width, r.height)); + } + mMonitorCombo.select(mMonitorIndex); + mMonitorCombo.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetDefaultSelected(SelectionEvent arg0) { + mMonitorIndex = mMonitorCombo.getSelectionIndex(); + } + }); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/SdkTargetSelector.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/SdkTargetSelector.java new file mode 100644 index 00000000..bee160f6 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/SdkTargetSelector.java @@ -0,0 +1,460 @@ +/* + * 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.sdkuilib.internal.widgets; + +import com.android.SdkConstants; +import com.android.sdklib.IAndroidTarget; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ControlAdapter; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Point; +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.Control; +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.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.TableItem; + +import java.util.Arrays; +import java.util.Comparator; + + +/** + * The SDK target selector is a table that is added to the given parent composite. + *

+ * To use, create it using {@link #SdkTargetSelector(Composite, IAndroidTarget[], boolean)} then + * call {@link #setSelection(IAndroidTarget)}, {@link #setSelectionListener(SelectionListener)} + * and finally use {@link #getSelected()} to retrieve the + * selection. + */ +public class SdkTargetSelector { + + private IAndroidTarget[] mTargets; + private final boolean mAllowSelection; + private SelectionListener mSelectionListener; + private Table mTable; + private Label mDescription; + private Composite mInnerGroup; + + /** Cache for {@link #getCheckboxWidth()} */ + private static int sCheckboxWidth = -1; + + /** + * Creates a new SDK Target Selector. + * + * @param parent The parent composite where the selector will be added. + * @param targets The list of targets. This is not copied, the caller must not modify. + * Targets can be null or an empty array, in which case the table is disabled. + */ + public SdkTargetSelector(Composite parent, IAndroidTarget[] targets) { + this(parent, targets, true /*allowSelection*/); + } + + /** + * Creates a new SDK Target Selector. + * + * @param parent The parent composite where the selector will be added. + * @param targets The list of targets. This is not copied, the caller must not modify. + * Targets can be null or an empty array, in which case the table is disabled. + * @param allowSelection True if selection is enabled. + */ + public SdkTargetSelector(Composite parent, IAndroidTarget[] targets, boolean allowSelection) { + // Layout has 1 column + mInnerGroup = new Composite(parent, SWT.NONE); + mInnerGroup.setLayout(new GridLayout()); + mInnerGroup.setLayoutData(new GridData(GridData.FILL_BOTH)); + mInnerGroup.setFont(parent.getFont()); + + mAllowSelection = allowSelection; + int style = SWT.BORDER | SWT.SINGLE | SWT.FULL_SELECTION; + if (allowSelection) { + style |= SWT.CHECK; + } + mTable = new Table(mInnerGroup, style); + mTable.setHeaderVisible(true); + mTable.setLinesVisible(false); + + GridData data = new GridData(); + data.grabExcessVerticalSpace = true; + data.grabExcessHorizontalSpace = true; + data.horizontalAlignment = GridData.FILL; + data.verticalAlignment = GridData.FILL; + mTable.setLayoutData(data); + + mDescription = new Label(mInnerGroup, SWT.WRAP); + mDescription.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + // create the table columns + final TableColumn column0 = new TableColumn(mTable, SWT.NONE); + column0.setText("Target Name"); + final TableColumn column1 = new TableColumn(mTable, SWT.NONE); + column1.setText("Vendor"); + final TableColumn column2 = new TableColumn(mTable, SWT.NONE); + column2.setText("Platform"); + final TableColumn column3 = new TableColumn(mTable, SWT.NONE); + column3.setText("API Level"); + + adjustColumnsWidth(mTable, column0, column1, column2, column3); + setupSelectionListener(mTable); + setTargets(targets); + setupTooltip(mTable); + } + + /** + * Returns the layout data of the inner composite widget that contains the target selector. + * By default the layout data is set to a {@link GridData} with a {@link GridData#FILL_BOTH} + * mode. + *

+ * This can be useful if you want to change the {@link GridData#horizontalSpan} for example. + */ + public Object getLayoutData() { + return mInnerGroup.getLayoutData(); + } + + /** + * Returns the list of known targets. + *

+ * This is not a copy. Callers must not modify this array. + */ + public IAndroidTarget[] getTargets() { + return mTargets; + } + + /** + * Changes the targets of the SDK Target Selector. + * + * @param targets The list of targets. This is not copied, the caller must not modify. + */ + public void setTargets(IAndroidTarget[] targets) { + mTargets = targets; + if (mTargets != null) { + Arrays.sort(mTargets, new Comparator() { + @Override + public int compare(IAndroidTarget o1, IAndroidTarget o2) { + return o1.compareTo(o2); + } + }); + } + + fillTable(mTable); + } + + /** + * Sets a selection listener. Set it to null to remove it. + * The listener will be called after this table processed its selection + * events so that the caller can see the updated state. + *

+ * The event's item contains a {@link TableItem}. + * The {@link TableItem#getData()} contains an {@link IAndroidTarget}. + *

+ * It is recommended that the caller uses the {@link #getSelected()} method instead. + * + * @param selectionListener The new listener or null to remove it. + */ + public void setSelectionListener(SelectionListener selectionListener) { + mSelectionListener = selectionListener; + } + + /** + * Sets the current target selection. + *

+ * If the selection is actually changed, this will invoke the selection listener + * (if any) with a null event. + * + * @param target the target to be selection + * @return true if the target could be selected, false otherwise. + */ + public boolean setSelection(IAndroidTarget target) { + if (!mAllowSelection) { + return false; + } + + boolean found = false; + boolean modified = false; + + if (mTable != null && !mTable.isDisposed()) { + for (TableItem i : mTable.getItems()) { + if ((IAndroidTarget) i.getData() == target) { + found = true; + if (!i.getChecked()) { + modified = true; + i.setChecked(true); + } + } else if (i.getChecked()) { + modified = true; + i.setChecked(false); + } + } + } + + if (modified && mSelectionListener != null) { + mSelectionListener.widgetSelected(null); + } + + return found; + } + + /** + * Returns the selected item. + * + * @return The selected item or null. + */ + public IAndroidTarget getSelected() { + if (mTable == null || mTable.isDisposed()) { + return null; + } + + for (TableItem i : mTable.getItems()) { + if (i.getChecked()) { + return (IAndroidTarget) i.getData(); + } + } + return null; + } + + /** + * Adds a listener to adjust the columns width when the parent is resized. + *

+ * If we need something more fancy, we might want to use this: + * http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet77.java?view=co + */ + private void adjustColumnsWidth(final Table table, + final TableColumn column0, + final TableColumn column1, + final TableColumn column2, + final TableColumn column3) { + // Add a listener to resize the column to the full width of the table + table.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Rectangle r = table.getClientArea(); + int width = r.width; + + // On the Mac, the width of the checkbox column is not included (and checkboxes + // are shown if mAllowSelection=true). Subtract this size from the available + // width to be distributed among the columns. + if (mAllowSelection + && SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN) { + width -= getCheckboxWidth(); + } + + column0.setWidth(width * 30 / 100); // 30% + column1.setWidth(width * 45 / 100); // 45% + column2.setWidth(width * 15 / 100); // 15% + column3.setWidth(width * 10 / 100); // 10% + } + }); + } + + + /** + * Creates a selection listener that will check or uncheck the whole line when + * double-clicked (aka "the default selection"). + */ + private void setupSelectionListener(final Table table) { + if (!mAllowSelection) { + return; + } + + // Add a selection listener that will check/uncheck items when they are double-clicked + table.addSelectionListener(new SelectionListener() { + /** Default selection means double-click on "most" platforms */ + @Override + public void widgetDefaultSelected(SelectionEvent e) { + if (e.item instanceof TableItem) { + TableItem i = (TableItem) e.item; + i.setChecked(!i.getChecked()); + enforceSingleSelection(i); + updateDescription(i); + } + + if (mSelectionListener != null) { + mSelectionListener.widgetDefaultSelected(e); + } + } + + @Override + public void widgetSelected(SelectionEvent e) { + if (e.item instanceof TableItem) { + TableItem i = (TableItem) e.item; + enforceSingleSelection(i); + updateDescription(i); + } + + if (mSelectionListener != null) { + mSelectionListener.widgetSelected(e); + } + } + + /** + * If we're not in multiple selection mode, uncheck all other + * items when this one is selected. + */ + private void enforceSingleSelection(TableItem item) { + if (item.getChecked()) { + Table parentTable = item.getParent(); + for (TableItem i2 : parentTable.getItems()) { + if (i2 != item && i2.getChecked()) { + i2.setChecked(false); + } + } + } + } + }); + } + + + /** + * Fills the table with all SDK targets. + * The table columns are: + *

    + *
  • column 0: sdk name + *
  • column 1: sdk vendor + *
  • column 2: sdk api name + *
  • column 3: sdk version + *
+ */ + private void fillTable(final Table table) { + + if (table == null || table.isDisposed()) { + return; + } + + table.removeAll(); + + if (mTargets != null && mTargets.length > 0) { + table.setEnabled(true); + for (IAndroidTarget target : mTargets) { + TableItem item = new TableItem(table, SWT.NONE); + item.setData(target); + item.setText(0, target.getName()); + item.setText(1, target.getVendor()); + item.setText(2, target.getVersionName()); + item.setText(3, target.getVersion().getApiString()); + } + } else { + table.setEnabled(false); + TableItem item = new TableItem(table, SWT.NONE); + item.setData(null); + item.setText(0, "--"); + item.setText(1, "No target available"); + item.setText(2, "--"); + item.setText(3, "--"); + } + } + + /** + * Sets up a tooltip that displays the current item description. + *

+ * Displaying a tooltip over the table looks kind of odd here. Instead we actually + * display the description in a label under the table. + */ + private void setupTooltip(final Table table) { + + if (table == null || table.isDisposed()) { + return; + } + + /* + * Reference: + * http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet125.java?view=markup + */ + + final Listener listener = new Listener() { + @Override + public void handleEvent(Event event) { + + switch(event.type) { + case SWT.KeyDown: + case SWT.MouseExit: + case SWT.MouseDown: + return; + + case SWT.MouseHover: + updateDescription(table.getItem(new Point(event.x, event.y))); + break; + + case SWT.Selection: + if (event.item instanceof TableItem) { + updateDescription((TableItem) event.item); + } + break; + + default: + return; + } + + } + }; + + table.addListener(SWT.Dispose, listener); + table.addListener(SWT.KeyDown, listener); + table.addListener(SWT.MouseMove, listener); + table.addListener(SWT.MouseHover, listener); + } + + /** + * Updates the description label with the description of the item's android target, if any. + */ + private void updateDescription(TableItem item) { + if (item != null) { + Object data = item.getData(); + if (data instanceof IAndroidTarget) { + String newTooltip = ((IAndroidTarget) data).getDescription(); + mDescription.setText(newTooltip == null ? "" : newTooltip); //$NON-NLS-1$ + } + } + } + + /** Enables or disables the controls. */ + public void setEnabled(boolean enabled) { + if (mInnerGroup != null && mTable != null && !mTable.isDisposed()) { + enableControl(mInnerGroup, enabled); + } + } + + /** Enables or disables controls; recursive for composite controls. */ + private void enableControl(Control c, boolean enabled) { + c.setEnabled(enabled); + if (c instanceof Composite) + for (Control c2 : ((Composite) c).getChildren()) { + enableControl(c2, enabled); + } + } + + /** Computes the width of a checkbox */ + private int getCheckboxWidth() { + if (sCheckboxWidth == -1) { + Shell shell = new Shell(mTable.getShell(), SWT.NO_TRIM); + Button checkBox = new Button(shell, SWT.CHECK); + sCheckboxWidth = checkBox.computeSize(SWT.DEFAULT, SWT.DEFAULT).x; + shell.dispose(); + } + + return sCheckboxWidth; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/ToggleButton.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/ToggleButton.java new file mode 100644 index 00000000..24138bc5 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/ToggleButton.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2011 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.sdkuilib.internal.widgets; + + +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.CLabel; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.MouseListener; +import org.eclipse.swt.events.MouseTrackListener; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Event; + +/** + * A label that can display 2 images depending on its internal state. + * This acts as a button by firing the {@link SWT#Selection} listener. + */ +public class ToggleButton extends CLabel { + private Image[] mImage = new Image[2]; + private String[] mTooltip = new String[2]; + private boolean mMouseIn; + private int mState = 0; + + + public ToggleButton( + Composite parent, + int style, + Image image1, + Image image2, + String tooltip1, + String tooltip2) { + super(parent, style); + mImage[0] = image1; + mImage[1] = image2; + mTooltip[0] = tooltip1; + mTooltip[1] = tooltip2; + updateImageAndTooltip(); + + addMouseListener(new MouseListener() { + @Override + public void mouseDown(MouseEvent e) { + // pass + } + + @Override + public void mouseUp(MouseEvent e) { + // We select on mouse-up, as it should be properly done since this is the + // only way a user can cancel a button click by moving out of the button. + if (mMouseIn && e.button == 1) { + notifyListeners(SWT.Selection, new Event()); + } + } + + @Override + public void mouseDoubleClick(MouseEvent e) { + if (mMouseIn && e.button == 1) { + notifyListeners(SWT.DefaultSelection, new Event()); + } + } + }); + + addMouseTrackListener(new MouseTrackListener() { + @Override + public void mouseExit(MouseEvent e) { + if (mMouseIn) { + mMouseIn = false; + redraw(); + } + } + + @Override + public void mouseEnter(MouseEvent e) { + if (!mMouseIn) { + mMouseIn = true; + redraw(); + } + } + + @Override + public void mouseHover(MouseEvent e) { + // pass + } + }); + } + + @Override + public int getStyle() { + int style = super.getStyle(); + if (mMouseIn) { + style |= SWT.SHADOW_IN; + } + return style; + } + + /** + * Sets current state. + * @param state A value 0 or 1. + */ + public void setState(int state) { + assert state == 0 || state == 1; + mState = state; + updateImageAndTooltip(); + redraw(); + } + + /** + * Returns the current state + * @return Returns the current state, either 0 or 1. + */ + public int getState() { + return mState; + } + + protected void updateImageAndTooltip() { + setImage(mImage[getState()]); + setToolTipText(mTooltip[getState()]); + } +} + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/repository/AvdManagerWindow.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/repository/AvdManagerWindow.java new file mode 100644 index 00000000..01972988 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/repository/AvdManagerWindow.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2011 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.sdkuilib.repository; + +import com.android.sdkuilib.internal.repository.ui.AvdManagerWindowImpl1; +import com.android.sdkuilib.internal.widgets.AvdSelector; +import com.android.utils.ILogger; + +import org.eclipse.swt.widgets.Shell; + +/** + * Opens an AVD Manager Window. + * + * This is the public entry point for using the window. + */ +public class AvdManagerWindow { + + /** The actual window implementation to which this class delegates. */ + private AvdManagerWindowImpl1 mWindow; + + /** + * Enum giving some indication of what is invoking this window. + * The behavior and UI will change slightly depending on the context. + *

+ * Note: if you add Android support to your specific IDE, you might want + * to specialize this context enum. + */ + public enum AvdInvocationContext { + /** + * The AVD Manager is invoked from the stand-alone 'android' tool. + * In this mode, we present an about box, a settings page. + * For SdkMan2, we also have a menu bar and link to the SDK Manager 2. + */ + STANDALONE, + + /** + * The AVD Manager is embedded as a dialog in the SDK Manager + * or in the {@link AvdSelector}. + * This is similar to the {@link #STANDALONE} mode except we don't need + * to display a menu bar at all since we don't want a menu item linking + * back to the SDK Manager and we don't need to redisplay the options + * and about which are already on the root window. + */ + DIALOG, + + /** + * The AVD Manager is invoked from an IDE. + * In this mode, we do not modify the menu bar. + * There is no about box and no settings. + */ + IDE, + } + + + /** + * Creates a new window. Caller must call open(), which will block. + * + * @param parentShell Parent shell. + * @param sdkLog Logger. Cannot be null. + * @param osSdkRoot The OS path to the SDK root. + * @param context The {@link AvdInvocationContext} to change the behavior depending on who's + * opening the SDK Manager. + */ + public AvdManagerWindow( + Shell parentShell, + ILogger sdkLog, + String osSdkRoot, + AvdInvocationContext context) { + mWindow = new AvdManagerWindowImpl1( + parentShell, + sdkLog, + osSdkRoot, + context); + } + + /** + * Opens the window. + */ + public void open() { + mWindow.open(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/repository/SdkUpdaterWindow.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/repository/SdkUpdaterWindow.java new file mode 100644 index 00000000..b91f03c6 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/repository/SdkUpdaterWindow.java @@ -0,0 +1,113 @@ +/* + * 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.sdkuilib.repository; + +import com.android.sdklib.repository.ISdkChangeListener; +import com.android.sdkuilib.internal.repository.ISdkUpdaterWindow; +import com.android.sdkuilib.internal.repository.ui.SdkUpdaterWindowImpl2; +import com.android.utils.ILogger; + +import org.eclipse.swt.widgets.Shell; + +/** + * Opens an SDK Manager Window. + * + * This is the public entry point for using the window. + */ +public class SdkUpdaterWindow { + + /** The actual window implementation to which this class delegates. */ + private ISdkUpdaterWindow mWindow; + + /** + * Enum giving some indication of what is invoking this window. + * The behavior and UI will change slightly depending on the context. + *

+ * Note: if you add Android support to your specific IDE, you might want + * to specialize this context enum. + */ + public enum SdkInvocationContext { + /** + * The SDK Manager is invoked from the stand-alone 'android' tool. + * In this mode, we present an about box, a settings page. + * For SdkMan2, we also have a menu bar and link to the AVD manager. + */ + STANDALONE, + + /** + * The SDK Manager is invoked from the standalone AVD Manager. + * This is similar to the standalone mode except that in this case we + * don't display a menu item linking to the AVD Manager. + */ + AVD_MANAGER, + + /** + * The SDK Manager is invoked from an IDE. + * In this mode, we do not modify the menu bar. There is no about box + * and no settings (e.g. HTTP proxy settings are inherited from Eclipse.) + */ + IDE, + + /** + * The SDK Manager is invoked from the AVD Selector. + * For SdkMan1, this means the AVD page will be displayed first. + * For SdkMan2, we won't be using this. + */ + AVD_SELECTOR + } + + /** + * Creates a new window. Caller must call open(), which will block. + * + * @param parentShell Parent shell. + * @param sdkLog Logger. Cannot be null. + * @param osSdkRoot The OS path to the SDK root. + * @param context The {@link SdkInvocationContext} to change the behavior depending on who's + * opening the SDK Manager. + */ + public SdkUpdaterWindow( + Shell parentShell, + ILogger sdkLog, + String osSdkRoot, + SdkInvocationContext context) { + + mWindow = new SdkUpdaterWindowImpl2(parentShell, sdkLog, osSdkRoot, context); + } + + /** + * Adds a new listener to be notified when a change is made to the content of the SDK. + * This should be called before {@link #open()}. + */ + public void addListener(ISdkChangeListener listener) { + mWindow.addListener(listener); + } + + /** + * Removes a new listener to be notified anymore when a change is made to the content of + * the SDK. + */ + public void removeListener(ISdkChangeListener listener) { + mWindow.removeListener(listener); + } + + /** + * Opens the window. + */ + public void open() { + mWindow.open(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/AuthenticationDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/AuthenticationDialog.java new file mode 100644 index 00000000..c077369f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/AuthenticationDialog.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2011 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.sdkuilib.ui; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +/** + * Dialog which collects from the user his/her login and password. + */ +public class AuthenticationDialog extends GridDialog { + private Text mTxtLogin; + private Text mTxtPassword; + private Text mTxtWorkstation; + private Text mTxtDomain; + + private String mTitle; + private String mMessage; + + private static String sLogin = ""; + private static String sPassword = ""; + private static String sWorkstation = ""; + private static String sDomain = ""; + + /** + * Constructor which retrieves the parent {@link Shell} and the message to + * be displayed in this dialog. + * + * @param parentShell Parent Shell + * @param title Title of the window. + * @param message Message the be displayed in this dialog. + */ + public AuthenticationDialog(Shell parentShell, String title, String message) { + super(parentShell, 1, false); + // assign fields + mTitle = title; + mMessage = message; + } + + @Override + public void createDialogContent(Composite parent) { + // Configure Dialog + getShell().setText(mTitle); + GridData data = new GridData(SWT.FILL, SWT.FILL, true, true); + parent.setLayoutData(data); + + // Upper Composite + Composite upperComposite = new Composite(parent, SWT.NONE); + GridLayout layout = new GridLayout(2, false); + layout.verticalSpacing = 10; + upperComposite.setLayout(layout); + data = new GridData(SWT.FILL, SWT.CENTER, true, true); + upperComposite.setLayoutData(data); + + // add message label + Label lblMessage = new Label(upperComposite, SWT.WRAP); + lblMessage.setText(mMessage); + data = new GridData(SWT.FILL, SWT.CENTER, true, true, 2, 1); + data.widthHint = 500; + lblMessage.setLayoutData(data); + + // add user name label and text field + Label lblUserName = new Label(upperComposite, SWT.NONE); + lblUserName.setText("Login:"); + data = new GridData(SWT.LEFT, SWT.CENTER, false, false); + lblUserName.setLayoutData(data); + + mTxtLogin = new Text(upperComposite, SWT.SINGLE | SWT.BORDER); + data = new GridData(SWT.FILL, SWT.CENTER, true, false); + mTxtLogin.setLayoutData(data); + mTxtLogin.setFocus(); + mTxtLogin.setText(sLogin); + mTxtLogin.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent arg0) { + sLogin = mTxtLogin.getText().trim(); + } + }); + + // add password label and text field + Label lblPassword = new Label(upperComposite, SWT.NONE); + lblPassword.setText("Password:"); + data = new GridData(SWT.LEFT, SWT.CENTER, false, false); + lblPassword.setLayoutData(data); + + mTxtPassword = new Text(upperComposite, SWT.SINGLE | SWT.PASSWORD | SWT.BORDER); + data = new GridData(SWT.FILL, SWT.CENTER, true, false); + mTxtPassword.setLayoutData(data); + mTxtPassword.setText(sPassword); + mTxtPassword.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent arg0) { + sPassword = mTxtPassword.getText(); + } + }); + + // add a label indicating that the following two fields are optional + Label lblInfo = new Label(upperComposite, SWT.NONE); + lblInfo.setText("Provide the following info if your proxy uses NTLM authentication. Leave blank otherwise."); + data = new GridData(); + data.horizontalSpan = 2; + lblInfo.setLayoutData(data); + + // add workstation label and text field + Label lblWorkstation = new Label(upperComposite, SWT.NONE); + lblWorkstation.setText("Workstation:"); + data = new GridData(SWT.LEFT, SWT.CENTER, false, false); + lblWorkstation.setLayoutData(data); + + mTxtWorkstation = new Text(upperComposite, SWT.SINGLE | SWT.BORDER); + data = new GridData(SWT.FILL, SWT.CENTER, true, false); + mTxtWorkstation.setLayoutData(data); + mTxtWorkstation.setText(sWorkstation); + mTxtWorkstation.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent arg0) { + sWorkstation = mTxtWorkstation.getText().trim(); + } + }); + + // add domain label and text field + Label lblDomain = new Label(upperComposite, SWT.NONE); + lblDomain.setText("Domain:"); + data = new GridData(SWT.LEFT, SWT.CENTER, false, false); + lblDomain.setLayoutData(data); + + mTxtDomain = new Text(upperComposite, SWT.SINGLE | SWT.BORDER); + data = new GridData(SWT.FILL, SWT.CENTER, true, false); + mTxtDomain.setLayoutData(data); + mTxtDomain.setText(sDomain); + mTxtDomain.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent arg0) { + sDomain = mTxtDomain.getText().trim(); + } + }); + } + + /** + * Retrieves the Login field information + * + * @return Login field value or empty String. Return value is never null + */ + public String getLogin() { + return sLogin; + } + + /** + * Retrieves the Password field information + * + * @return Password field value or empty String. Return value is never null + */ + public String getPassword() { + return sPassword; + } + + /** + * Retrieves the workstation field information + * + * @return Workstation field value or empty String. Return value is never null + */ + public String getWorkstation() { + return sWorkstation; + } + + /** + * Retrieves the domain field information + * + * @return Domain field value or empty String. Return value is never null + */ + public String getDomain() { + return sDomain; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/GridDataBuilder.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/GridDataBuilder.java new file mode 100644 index 00000000..c1e6a510 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/GridDataBuilder.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2010 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.sdkuilib.ui; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Control; + +/** + * A little helper to create a new {@link GridData} and set its properties. + *

+ * Example of usage:
+ * + * GridDataHelper.create(myControl).hSpan(2).hAlignCenter().fill(); + * + */ +public final class GridDataBuilder { + + private GridData mGD; + + private GridDataBuilder() { + mGD = new GridData(); + } + + /** + * Creates new {@link GridData} and associates it on the control composite. + */ + static public GridDataBuilder create(Control control) { + GridDataBuilder gdh = new GridDataBuilder(); + control.setLayoutData(gdh.mGD); + return gdh; + } + + /** Sets widthHint to w. */ + public GridDataBuilder wHint(int w) { + mGD.widthHint = w; + return this; + } + + /** Sets heightHint to h. */ + public GridDataBuilder hHint(int h) { + mGD.heightHint = h; + return this; + } + + /** Sets horizontalIndent to h. */ + public GridDataBuilder hIndent(int h) { + mGD.horizontalIndent = h; + return this; + } + + /** Sets horizontalSpan to h. */ + public GridDataBuilder hSpan(int h) { + mGD.horizontalSpan = h; + return this; + } + + /** Sets verticalSpan to v. */ + public GridDataBuilder vSpan(int v) { + mGD.verticalSpan = v; + return this; + } + + /** Sets horizontalAlignment to {@link SWT#CENTER}. */ + public GridDataBuilder hCenter() { + mGD.horizontalAlignment = SWT.CENTER; + return this; + } + + /** Sets verticalAlignment to {@link SWT#CENTER}. */ + public GridDataBuilder vCenter() { + mGD.verticalAlignment = SWT.CENTER; + return this; + } + + /** Sets verticalAlignment to {@link SWT#TOP}. */ + public GridDataBuilder vTop() { + mGD.verticalAlignment = SWT.TOP; + return this; + } + + /** Sets verticalAlignment to {@link SWT#BOTTOM}. */ + public GridDataBuilder vBottom() { + mGD.verticalAlignment = SWT.BOTTOM; + return this; + } + + /** Sets horizontalAlignment to {@link SWT#LEFT}. */ + public GridDataBuilder hLeft() { + mGD.horizontalAlignment = SWT.LEFT; + return this; + } + + /** Sets horizontalAlignment to {@link SWT#RIGHT}. */ + public GridDataBuilder hRight() { + mGD.horizontalAlignment = SWT.RIGHT; + return this; + } + + /** Sets horizontalAlignment to {@link GridData#FILL}. */ + public GridDataBuilder hFill() { + mGD.horizontalAlignment = GridData.FILL; + return this; + } + + /** Sets verticalAlignment to {@link GridData#FILL}. */ + public GridDataBuilder vFill() { + mGD.verticalAlignment = GridData.FILL; + return this; + } + + /** + * Sets both horizontalAlignment and verticalAlignment + * to {@link GridData#FILL}. + */ + public GridDataBuilder fill() { + mGD.horizontalAlignment = GridData.FILL; + mGD.verticalAlignment = GridData.FILL; + return this; + } + + /** Sets grabExcessHorizontalSpace to true. */ + public GridDataBuilder hGrab() { + mGD.grabExcessHorizontalSpace = true; + return this; + } + + /** Sets grabExcessVerticalSpace to true. */ + public GridDataBuilder vGrab() { + mGD.grabExcessVerticalSpace = true; + return this; + } + + /** + * Sets both grabExcessHorizontalSpace and + * grabExcessVerticalSpace to true. + */ + public GridDataBuilder grab() { + mGD.grabExcessHorizontalSpace = true; + mGD.grabExcessVerticalSpace = true; + return this; + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/GridDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/GridDialog.java new file mode 100644 index 00000000..bf654204 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/GridDialog.java @@ -0,0 +1,84 @@ +/* + * 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.sdkuilib.ui; + +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.swt.SWT; +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.Shell; + +/** + * JFace-based dialog that properly sets up a {@link GridLayout} top composite with the proper + * margin. + *

+ * Implementing dialog must create the content of the dialog in + * {@link #createDialogContent(Composite)}. + *

+ * A JFace dialog is perfect if you want a typical "OK | cancel" workflow, with the OK and + * cancel things all handled for you using a predefined layout. If you want a different set + * of buttons or a different layout, consider {@link SwtBaseDialog} instead. + */ +public abstract class GridDialog extends Dialog { + + private final int mNumColumns; + private final boolean mMakeColumnsEqualWidth; + + /** + * Creates the dialog + * @param parentShell the parent {@link Shell}. + * @param numColumns the number of columns in the grid + * @param makeColumnsEqualWidth whether or not the columns will have equal width + */ + public GridDialog(Shell parentShell, int numColumns, boolean makeColumnsEqualWidth) { + super(parentShell); + mNumColumns = numColumns; + mMakeColumnsEqualWidth = makeColumnsEqualWidth; + } + + /** + * Creates the content of the dialog. The parent composite is a {@link GridLayout} + * created with the numColumn and makeColumnsEqualWidth parameters + * passed to {@link #GridDialog(Shell, int, boolean)}. + *

+ * This is called by the parent's {@link #createContents(Composite)}. + * + * @param parent the parent composite. + */ + public abstract void createDialogContent(Composite parent); + + @Override + protected Control createDialogArea(Composite parent) { + Composite top = new Composite(parent, SWT.NONE); + GridLayout layout = new GridLayout(mNumColumns, mMakeColumnsEqualWidth); + layout.marginHeight = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_MARGIN); + layout.marginWidth = convertHorizontalDLUsToPixels(IDialogConstants.HORIZONTAL_MARGIN); + layout.verticalSpacing = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_SPACING); + layout.horizontalSpacing = convertHorizontalDLUsToPixels( + IDialogConstants.HORIZONTAL_SPACING); + top.setLayout(layout); + top.setLayoutData(new GridData(GridData.FILL_BOTH)); + + createDialogContent(top); + + applyDialogFont(top); + return top; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/GridLayoutBuilder.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/GridLayoutBuilder.java new file mode 100644 index 00000000..6b9398f2 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/GridLayoutBuilder.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2010 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.sdkuilib.ui; + +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; + +/** + * A little helper to create a new {@link GridLayout}, associate to a {@link Composite} + * and set its common attributes. + *

+ * Example of usage:
+ * + * GridLayoutHelper.create(myComposite).noMargins().vSpacing(0).columns(2); + * + */ +public final class GridLayoutBuilder { + + private GridLayout mGL; + + private GridLayoutBuilder() { + mGL = new GridLayout(); + } + + /** + * Creates new {@link GridLayout} and associates it on the parent composite. + */ + static public GridLayoutBuilder create(Composite parent) { + GridLayoutBuilder glh = new GridLayoutBuilder(); + parent.setLayout(glh.mGL); + return glh; + } + + /** Sets all margins to 0. */ + public GridLayoutBuilder noMargins() { + mGL.marginHeight = 0; + mGL.marginWidth = 0; + mGL.marginLeft = 0; + mGL.marginTop = 0; + mGL.marginRight = 0; + mGL.marginBottom = 0; + return this; + } + + /** Sets all margins to n. */ + public GridLayoutBuilder margins(int n) { + mGL.marginHeight = n; + mGL.marginWidth = n; + mGL.marginLeft = n; + mGL.marginTop = n; + mGL.marginRight = n; + mGL.marginBottom = n; + return this; + } + + /** Sets numColumns to n. */ + public GridLayoutBuilder columns(int n) { + mGL.numColumns = n; + return this; + } + + /** Sets makeColumnsEqualWidth to true. */ + public GridLayoutBuilder columnsEqual() { + mGL.makeColumnsEqualWidth = true; + return this; + } + + /** Sets verticalSpacing to v. */ + public GridLayoutBuilder vSpacing(int v) { + mGL.verticalSpacing = v; + return this; + } + + /** Sets horizontalSpacing to h. */ + public GridLayoutBuilder hSpacing(int h) { + mGL.horizontalSpacing = h; + return this; + } + + /** + * Sets horizontalSpacing and verticalSpacing + * to s. + */ + public GridLayoutBuilder spacing(int s) { + mGL.verticalSpacing = s; + mGL.horizontalSpacing = s; + return this; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/SwtBaseDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/SwtBaseDialog.java new file mode 100644 index 00000000..fc7026fb --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/SwtBaseDialog.java @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2011 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.sdkuilib.ui; + +import com.android.SdkConstants; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.Dialog; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +import java.util.HashMap; +import java.util.Map; + +/** + * A base class for an SWT Dialog. + *

+ * The base class offers the following goodies:
+ * - Dialog is automatically centered on its parent.
+ * - Dialog size is reused during the session.
+ * - A simple API with an {@link #open()} method that returns a boolean.
+ *

+ * A typical usage is: + *

+ *   MyDialog extends SwtBaseDialog { ... }
+ *   MyDialog d = new MyDialog(parentShell, "My Dialog Title");
+ *   if (d.open()) {
+ *      ...do something like refresh parent list view
+ *   }
+ * 
+ * We also have a JFace-base {@link GridDialog}. + * The JFace dialog is good when you just want a typical OK/Cancel layout with the + * buttons all managed for you. + * This SWT base dialog has little decoration. + * It's up to you to manage whatever buttons you want, if any. + */ +public abstract class SwtBaseDialog extends Dialog { + + /** + * Min Y location for dialog. Need to deal with the menu bar on mac os. + */ + private final static int MIN_Y = + SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN ? 20 : 0; + + /** Last dialog size for this session, different for each dialog class. */ + private static Map, Point> sLastSizeMap = new HashMap, Point>(); + + private volatile boolean mQuitRequested = false; + private boolean mReturnValue; + private Shell mShell; + + /** + * Create the dialog. + * + * @param parent The parent's shell + * @param title The dialog title. Can be null. + */ + public SwtBaseDialog(Shell parent, int swtStyle, String title) { + super(parent, swtStyle); + if (title != null) { + setText(title); + } + } + + /** + * Open the dialog. + * + * @return The last value set using {@link #setReturnValue(boolean)} or false by default. + */ + public boolean open() { + if (!mQuitRequested) { + createShell(); + } + if (!mQuitRequested) { + createContents(); + } + if (!mQuitRequested) { + positionShell(); + } + if (!mQuitRequested) { + postCreate(); + } + if (!mQuitRequested) { + mShell.open(); + mShell.layout(); + eventLoop(); + } + + return mReturnValue; + } + + /** + * Creates the shell for this dialog. + * The default shell has a size of 450x300, which is also its minimum size. + * You might want to override these values. + *

+ * Called before {@link #createContents()}. + */ + protected void createShell() { + mShell = new Shell(getParent(), SWT.DIALOG_TRIM | SWT.RESIZE | SWT.APPLICATION_MODAL); + mShell.setMinimumSize(new Point(450, 300)); + mShell.setSize(450, 300); + if (getText() != null) { + mShell.setText(getText()); + } + mShell.addDisposeListener(new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + saveSize(); + } + }); + } + + /** + * Creates the content and attaches it to the current shell (cf. {@link #getShell()}). + *

+ * Derived classes should consider creating the UI here and initializing their + * state in {@link #postCreate()}. + */ + protected abstract void createContents(); + + /** + * Called after {@link #createContents()} and after {@link #positionShell()} + * just before the dialog is actually shown on screen. + *

+ * Derived classes should consider creating the UI in {@link #createContents()} and + * initialize it here. + */ + protected abstract void postCreate(); + + /** + * Run the event loop. + * This is called from {@link #open()} after {@link #postCreate()} and + * after the window has been shown on screen. + * Derived classes might want to use this as a place to start automated + * tasks that will update the UI. + */ + protected void eventLoop() { + Display display = getParent().getDisplay(); + while (!mQuitRequested && !mShell.isDisposed()) { + if (!display.readAndDispatch()) { + display.sleep(); + } + } + } + + /** + * Returns the current value that {@link #open()} will return to the caller. + * Default is false. + */ + protected boolean getReturnValue() { + return mReturnValue; + } + + /** + * Sets the value that {@link #open()} will return to the caller. + * @param returnValue The new value to be returned by {@link #open()}. + */ + protected void setReturnValue(boolean returnValue) { + mReturnValue = returnValue; + } + + /** + * Returns the shell created by {@link #createShell()}. + * @return The current {@link Shell}. + */ + protected Shell getShell() { + return mShell; + } + + /** + * Saves the dialog size and close the dialog. + * The {@link #open()} method will given return value (see {@link #setReturnValue(boolean)}. + *

+ * It's safe to call this method before the shell is initialized, + * in which case the dialog will close as soon as possible. + */ + protected void close() { + if (mShell != null && !mShell.isDisposed()) { + saveSize(); + getShell().close(); + } + mQuitRequested = true; + } + + //------- + + /** + * Centers the dialog in its parent shell. + */ + private void positionShell() { + // Centers the dialog in its parent shell + Shell child = mShell; + Shell parent = getParent(); + if (child != null && parent != null) { + // get the parent client area with a location relative to the display + Rectangle parentArea = parent.getClientArea(); + Point parentLoc = parent.getLocation(); + int px = parentLoc.x; + int py = parentLoc.y; + int pw = parentArea.width; + int ph = parentArea.height; + + // Reuse the last size if there's one, otherwise use the default + Point childSize = sLastSizeMap.get(this.getClass()); + if (childSize == null) { + childSize = child.getSize(); + } + int cw = childSize.x; + int ch = childSize.y; + + int x = px + (pw - cw) / 2; + if (x < 0) x = 0; + + int y = py + (ph - ch) / 2; + if (y < MIN_Y) y = MIN_Y; + + child.setLocation(x, y); + child.setSize(cw, ch); + } + } + + private void saveSize() { + if (mShell != null && !mShell.isDisposed()) { + sLastSizeMap.put(this.getClass(), mShell.getSize()); + } + } + +} diff --git a/andmore-swt/org.eclipse.andmore.swt/.classpath b/andmore-swt/org.eclipse.andmore.swt/.classpath new file mode 100644 index 00000000..54f49f95 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/.classpath @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.swt/.gitignore b/andmore-swt/org.eclipse.andmore.swt/.gitignore new file mode 100644 index 00000000..f4f27d8a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/.gitignore @@ -0,0 +1,2 @@ +/target/ +/libs/ \ No newline at end of file diff --git a/andmore-swt/org.eclipse.andmore.swt/.project b/andmore-swt/org.eclipse.andmore.swt/.project new file mode 100644 index 00000000..872dc77f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/.project @@ -0,0 +1,34 @@ + + + org.eclipse.andmore.swt + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/andmore-swt/org.eclipse.andmore.swt/.settings/org.eclipse.core.resources.prefs b/andmore-swt/org.eclipse.andmore.swt/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..4824b802 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/andmore-swt/org.eclipse.andmore.swt/.settings/org.eclipse.jdt.core.prefs b/andmore-swt/org.eclipse.andmore.swt/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..c137e176 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,98 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore +org.eclipse.jdt.core.compiler.annotation.nonnull=com.android.annotations.NonNull +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=com.android.annotations.NonNullByDefault +org.eclipse.jdt.core.compiler.annotation.nonnullisdefault=disabled +org.eclipse.jdt.core.compiler.annotation.nullable=com.android.annotations.Nullable +org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=warning +org.eclipse.jdt.core.compiler.problem.deprecation=warning +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore +org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=warning +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning +org.eclipse.jdt.core.compiler.problem.forbiddenReference=error +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning +org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning +org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning +org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning +org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning +org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning +org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning +org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedImport=warning +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning +org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-swt/org.eclipse.andmore.swt/.settings/org.eclipse.m2e.core.prefs b/andmore-swt/org.eclipse.andmore.swt/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore-swt/org.eclipse.andmore.swt/META-INF/MANIFEST.MF b/andmore-swt/org.eclipse.andmore.swt/META-INF/MANIFEST.MF new file mode 100644 index 00000000..65bf4bfc --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/META-INF/MANIFEST.MF @@ -0,0 +1,92 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Localization: plugin +Bundle-Name: %Bundle-Name +Bundle-SymbolicName: org.eclipse.andmore.swt;singleton:=true +Bundle-Version: 0.5.2.qualifier +Require-Bundle: org.eclipse.ui, + org.eclipse.core.runtime, + org.apache.httpcomponents.httpclient;bundle-version="4.1.3", + org.apache.httpcomponents.httpcore;bundle-version="4.1.4", + org.apache.commons.logging;bundle-version="1.1.1", + com.google.gson;bundle-version="2.2.4" +Bundle-ActivationPolicy: lazy +Bundle-Vendor: %Bundle-Vendor +Bundle-ClassPath: ., + libs/annotations-25.3.3.jar, + libs/common-25.3.3.jar, + libs/guava-18.0.jar, + libs/httpmime-4.1.jar, + libs/kxml2-2.3.0.jar, + libs/layoutlib-api-25.3.3.jar, + libs/sdklib-25.3.3.jar, + libs/dvlib-25.3.3.jar, + libs/sdk-common-25.3.3.jar, + libs/repository-25.3.3.jar, + libs/ddmlib-25.3.3.jar, + libs/jimfs-1.1.jar +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Export-Package: com.android, + com.android.annotations, + com.android.annotations.concurrency, + com.android.dvlib, + com.android.ide.common.blame, + com.android.ide.common.internal, + com.android.ide.common.rendering, + com.android.ide.common.rendering.api, + com.android.ide.common.res2, + com.android.ide.common.resources, + com.android.ide.common.resources.configuration, + com.android.ide.common.sdk, + com.android.ide.common.util, + com.android.ide.common.xml, + com.android.io, + com.android.prefs, + com.android.repository, + com.android.repository.api, + com.android.repository.io, + com.android.repository.testframework, + com.android.repository.util, + com.android.resources, + com.android.ddmlib, + com.android.ddmlib.log, + com.android.ddmlib.logcat, + com.android.ddmlib.testrunner, + com.android.ddmlib.utils, + com.android.sdklib, + com.android.sdklib.build, + com.android.sdklib.devices, + com.android.sdklib.internal.avd, + com.android.sdklib.internal.build, + com.android.sdklib.internal.project, + com.android.sdklib.repository, + com.android.sdklib.repository.legacy.remote.internal, + com.android.sdklib.repository.meta, + com.android.sdklib.repository.targets, + com.android.sdklib.util, + com.android.util, + com.android.utils, + com.android.xml, + com.google.common.annotations, + com.google.common.base, + com.google.common.base.internal, + com.google.common.cache, + com.google.common.collect, + com.google.common.eventbus, + com.google.common.hash, + com.google.common.io, + com.google.common.math, + com.google.common.net, + com.google.common.primitives, + com.google.common.reflect, + com.google.common.util.concurrent, + org.apache.http.entity.mime, + org.apache.http.entity.mime.content, + org.eclipse.andmore.base, + org.kxml2.io, + org.kxml2.kdom, + org.kxml2.wap, + org.kxml2.wap.syncml, + org.kxml2.wap.wml, + org.kxml2.wap.wv, + org.xmlpull.v1 diff --git a/andmore-swt/org.eclipse.andmore.swt/about.html b/andmore-swt/org.eclipse.andmore.swt/about.html new file mode 100644 index 00000000..52791768 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/about.html @@ -0,0 +1,29 @@ + + + + + +About + + +

About This Content

+ +

March 31, 2015

+

License

+ +

The Eclipse Foundation makes available all content in this plug-in ("Content"). Unless otherwise +indicated below, the Content is provided to you under the terms and conditions of the +Eclipse Public License Version 1.0 ("EPL"). A copy of the EPL is available +at http://www.eclipse.org/legal/epl-v10.html. +For purposes of the EPL, "Program" will mean the Content.

+ +

If you did not receive this Content directly from the Eclipse Foundation, the Content is +being redistributed by another party ("Redistributor") and different terms and conditions may +apply to your use of any object code in the Content. Check the Redistributor's license that was +provided with the Content. If no such license exists, contact the Redistributor. Unless otherwise +indicated below, the terms and conditions of the EPL still apply to any source code in the Content +and such source code may be obtained at http://www.eclipse.org.

+ + + \ No newline at end of file diff --git a/andmore-swt/org.eclipse.andmore.swt/build.properties b/andmore-swt/org.eclipse.andmore.swt/build.properties new file mode 100644 index 00000000..b47d897d --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/build.properties @@ -0,0 +1,9 @@ +output.. = bin/ +bin.includes = .,\ + libs/,\ + META-INF/,\ + plugin.xml,\ + plugin.properties,\ + about.html +jars.compile.order = . +source.. = src/ diff --git a/andmore-swt/org.eclipse.andmore.swt/findbugsExclusion.xml b/andmore-swt/org.eclipse.andmore.swt/findbugsExclusion.xml new file mode 100644 index 00000000..5869a9b2 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/findbugsExclusion.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.swt/plugin.properties b/andmore-swt/org.eclipse.andmore.swt/plugin.properties new file mode 100644 index 00000000..c93b97ad --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/plugin.properties @@ -0,0 +1,9 @@ +######################################################### +# +# Properties file +# +######################################################### + + +Bundle-Name=Common Android Utilities +Bundle-Vendor=Eclipse Andmore Project diff --git a/andmore-swt/org.eclipse.andmore.swt/plugin.xml b/andmore-swt/org.eclipse.andmore.swt/plugin.xml new file mode 100644 index 00000000..b60f62ae --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/plugin.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/andmore-swt/org.eclipse.andmore.swt/pom.xml b/andmore-swt/org.eclipse.andmore.swt/pom.xml new file mode 100644 index 00000000..52c173a0 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/pom.xml @@ -0,0 +1,246 @@ + + + 4.0.0 + + org.eclipse.andmore.swt + eclipse-plugin + swt base + + + ../pom.xml + org.eclipse.andmore + swt-droid-parent + 0.5.2-SNAPSHOT + + + + com.android.tools.build + builder + ${android.builder.version} + + + com.android.tools.build + builder-model + + + com.android.tools.build + builder-test-api + + + com.android.tools.jack + jack-api + + + com.android.tools.jill + jill-api + + + com.android.tools.analytics-library + protos + + + com.android.tools.analytics-library + shared + + + com.android.tools.analytics-library + tracker + + + com.squareup + javawriter + + + org.bouncycastle + bcpkix-jdk15on + + + org.bouncycastle + bcprov-jdk15on + + + org.ow2.asm + asm + + + org.ow2.asm + asm-tree + + + + + com.android.tools + sdklib + ${android.tools.version} + + + com.android.tools.ddms + ddmlib + ${android.tools.version} + + + com.android.tools + dvlib + ${android.tools.version} + + + com.android.tools + repository + ${android.tools.version} + + + com.android.tools + sdk-common + ${android.tools.version} + + + com.android.tools.build + builder-model + + + com.android.tools.build + builder-test-api + + + + + com.android.tools.build + builder-model + 2.3.3 + + + org.bouncycastle + bcpkix-jdk15on + 1.48 + + + org.bouncycastle + bcprov-jdk15on + 1.48 + + + com.intellij + annotations + 12.0 + + + org.apache.commons + commons-compress + 1.8.1 + + + com.google.jimfs + jimfs + 1.1 + + + org.apache.httpcomponents + httpmime + 4.1 + + + + + + org.eclipse.tycho + tycho-source-plugin + ${tycho-version} + + + plugin-source + + plugin-source + + + + + + maven-dependency-plugin + + + copy + initialize + + copy + + + + + + com.android.tools + common + ${android.tools.version} + + + net.sf.kxml + kxml2 + 2.3.0 + + + com.android.tools + annotations + ${android.tools.version} + + + com.google.guava + guava + 18.0 + + + com.android.tools + dvlib + ${android.tools.version} + + + com.android.tools + sdk-common + ${android.tools.version} + + + com.android.tools + sdklib + ${android.tools.version} + + + com.android.tools.ddms + ddmlib + ${android.tools.version} + + + com.google.jimfs + jimfs + 1.1 + + + org.apache.httpcomponents + httpmime + 4.1 + + + com.android.tools + sdk-common + ${android.tools.version} + + + com.android.tools + repository + ${android.tools.version} + + + ${project.basedir}/libs + false + false + true + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.swt/src/org/eclipse/andmore/base/InstallDetails.java b/andmore-swt/org.eclipse.andmore.swt/src/org/eclipse/andmore/base/InstallDetails.java new file mode 100644 index 00000000..7ef9cc02 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/src/org/eclipse/andmore/base/InstallDetails.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2012 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 org.eclipse.andmore.base; + +import org.eclipse.core.runtime.Platform; +import org.osgi.framework.Bundle; +import org.osgi.framework.Version; + +public class InstallDetails { + private static final String ANDMORE_PLUGIN_ID = "org.eclipse.andmore"; //$NON-NLS-1$ + private static final String ECLIPSE_PLATFORM_PLUGIN_ID = "org.eclipse.platform"; //$NON-NLS-1$ + + /** + * Returns true if the ADT plugin is available in the current platform. This + * is useful for distinguishing between specific RCP applications vs. ADT + + * Eclipse. + */ + public static boolean isAdtInstalled() { + Bundle b = Platform.getBundle(ANDMORE_PLUGIN_ID); + return b != null; + } + + /** Returns the version of current eclipse platform. */ + public static Version getPlatformVersion() { + Bundle b = Platform.getBundle(ECLIPSE_PLATFORM_PLUGIN_ID); + return b == null ? Version.emptyVersion : b.getVersion(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/.classpath b/andmore-swt/org.eclipse.andmore.swtmenubar/.classpath new file mode 100644 index 00000000..00782ce2 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/.gitignore b/andmore-swt/org.eclipse.andmore.swtmenubar/.gitignore new file mode 100644 index 00000000..0f630157 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/.gitignore @@ -0,0 +1,2 @@ +/target/ +/bin/ diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/.project b/andmore-swt/org.eclipse.andmore.swtmenubar/.project new file mode 100644 index 00000000..4b23edae --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/.project @@ -0,0 +1,34 @@ + + + org.eclipse.andmore.swtmenbar + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/.settings/README.txt b/andmore-swt/org.eclipse.andmore.swtmenubar/.settings/README.txt new file mode 100644 index 00000000..2945bee0 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/.settings/README.txt @@ -0,0 +1,2 @@ +Copy this in eclipse project as a .settings folder at the root. +This ensure proper compilation compliance and warning/error levels. \ No newline at end of file diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.core.resources.prefs b/andmore-swt/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..4824b802 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.jdt.core.prefs b/andmore-swt/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..295926d9 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,7 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.m2e.core.prefs b/andmore-swt/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/META-INF/MANIFEST.MF b/andmore-swt/org.eclipse.andmore.swtmenubar/META-INF/MANIFEST.MF new file mode 100644 index 00000000..e1994f0e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/META-INF/MANIFEST.MF @@ -0,0 +1,13 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Localization: plugin +Bundle-Name: %Bundle-Name +Bundle-SymbolicName: org.eclipse.andmore.swtmenubar;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Vendor: %Bundle-Vendor +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Require-Bundle: org.eclipse.core.commands;bundle-version="3.8.1", + org.eclipse.equinox.common;bundle-version="3.8.0", + org.eclipse.jface;bundle-version="3.12.2" +Export-Package: com.android.menubar +Bundle-ClassPath: . diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/MODULE_LICENSE_EPL b/andmore-swt/org.eclipse.andmore.swtmenubar/MODULE_LICENSE_EPL new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/NOTICE b/andmore-swt/org.eclipse.andmore.swtmenubar/NOTICE new file mode 100644 index 00000000..e06fed78 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/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/andmore-swt/org.eclipse.andmore.swtmenubar/README b/andmore-swt/org.eclipse.andmore.swtmenubar/README new file mode 100644 index 00000000..56f3a619 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/README @@ -0,0 +1,80 @@ +Using the Eclipse project SwtMenuBar +------------------------------------ + +This project provides a platform-specific way to hook into +the default OS menu bar. + +On MacOS, it allows an SWT app to have an About menu item +and to hook into the default Preferences menu item. + +On Windows and Linux, an SWT Menu should be provided (typically +named "Tools") into which the About and Options menu items +will be added. + + +Consequently the implementation contains platform-specific source +folders for the Java files that rely on a platform-specific version +of SWT.jar. + +Right now we have the following source folders: +- src/ - Generic implementation for all platforms. +- src-darwin/ - Implementation for MacOS Carbon. + +*Only* the default "src/" folder is declared in the project .classpath +so that the project can be opened in Eclipse on any platform and still +work. However that means that on MacOS the custom src-darwin folder is +not used by default. + + + +1- To build the library: + +Do not use Eclipse to build the library. Instead use the makefile: + +$ cd $TOP_OF_ANDROID_TREE +$ . build/envsetup.sh && lunch sdk-eng +$ make swtmenubar + +This will create a Jar in /out/host//framework/ +that can then be included in the target application. + + +2- To use the library in a target application: + +Build the swtmenubar library as explained in step 1. + +In the target application, define a classpath variable in Eclipse: +- Open Preferences > Java > Build Path > Classpath Variables +- Create a new classpath variable named ANDROID_OUT_FRAMEWORK +- Set its folder value to /out/host//framework + +Then add a variable to the Build Path of the target project: +- Open Project > Properties > Java Build Path +- Select the "Libraries" tab +- Use "Add Variable" +- Select ANDROID_OUT_FRAMEWORK +- Select "Extend..." +- Select swtmenubar.jar (which you previously built at step 1) + + +3- Tip for developing this library: + +Keep in mind that src-darwin folder must not be added to the +source folder list, otherwise the library would not compile +on Windows or Linux. + +If you change anything to IMenuBarCallback, make sure to test +on a Mac to be sure you're not breaking the API. + +To work on this on a Mac, you can either: +a- simply temporarily add src-darwin as a source folder to the + build path and remove it before submitting. +b- or directly edit the java files and rebuild the library using + 'make swtmenubar' from a shell. + +To test the library, use 'make swtmenubar'. This will build the +library in out/... and the sdkmanager project is already setup +to find it there. + +-- +EOF diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/build.gradle b/andmore-swt/org.eclipse.andmore.swtmenubar/build.gradle new file mode 100644 index 00000000..1870b695 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/build.gradle @@ -0,0 +1,16 @@ +group = 'com.android.tools' +archivesBaseName = 'swtmenubar' + +dependencies { + compile project(':base:sdklib') + + testCompile 'junit:junit:3.8.1' +} + +sourceSets { + main.resources.srcDir 'src/main/java' + + main.java.srcDir 'src/main/java' + // Also add the MacOS specific sources for SWT Cocoa + main.java.srcDir 'src/main-darwin/java' +} diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/build.properties b/andmore-swt/org.eclipse.andmore.swtmenubar/build.properties new file mode 100644 index 00000000..ecc09051 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/build.properties @@ -0,0 +1,4 @@ +source.. = src/main/java/ +output.. = bin/ +bin.includes = META-INF/,\ + . diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/plugin.properties b/andmore-swt/org.eclipse.andmore.swtmenubar/plugin.properties new file mode 100644 index 00000000..7b27c3e4 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/plugin.properties @@ -0,0 +1,4 @@ +#Properties file for com.android.ide.eclipse.swtmenubar +Bundle-Vendor = Eclipse Andmore +Bundle-Name = UI Menubar +category.name = Android diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/pom.xml b/andmore-swt/org.eclipse.andmore.swtmenubar/pom.xml new file mode 100644 index 00000000..112d2cd7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + ../pom.xml + org.eclipse.andmore + swt-droid-parent + 0.5.2-SNAPSHOT + + org.eclipse.andmore.swtmenubar + eclipse-plugin + swtmenubar + + + + org.eclipse.tycho + tycho-maven-plugin + true + + + org.eclipse.tycho + tycho-source-plugin + ${tycho-version} + + + plugin-source + + plugin-source + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/src/main-darwin/java/com/android/menubar/internal/MenuBarEnhancerCocoa.java b/andmore-swt/org.eclipse.andmore.swtmenubar/src/main-darwin/java/com/android/menubar/internal/MenuBarEnhancerCocoa.java new file mode 100644 index 00000000..ee449e6a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/src/main-darwin/java/com/android/menubar/internal/MenuBarEnhancerCocoa.java @@ -0,0 +1,341 @@ +/* + * Copyright (C) 2011 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. + * + * History: + * Original code by the CarbonUIEnhancer from Agynami + * with the implementation being modified from the org.eclipse.ui.internal.cocoa.CocoaUIEnhancer, + * then modified by http://www.transparentech.com/opensource/cocoauienhancer to use reflection + * rather than 'link' to SWT cocoa, and finally modified to be usable by the SwtMenuBar project. + */ + +package com.android.menubar.internal; + +import com.android.menubar.IMenuBarCallback; +import com.android.menubar.IMenuBarEnhancer; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.internal.C; +import org.eclipse.swt.internal.Callback; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Menu; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class MenuBarEnhancerCocoa implements IMenuBarEnhancer { + + private static final long kAboutMenuItem = 0; + private static final long kPreferencesMenuItem = 2; + // private static final long kServicesMenuItem = 4; + // private static final long kHideApplicationMenuItem = 6; + private static final long kQuitMenuItem = 10; + + static long mSelPreferencesMenuItemSelected; + static long mSelAboutMenuItemSelected; + static Callback mProc3Args; + + private String mAppName; + + /** + * Class invoked via the Callback object to run the about and preferences + * actions. + *

+ * If you don't use JFace in your application (SWT only), change the + * {@link org.eclipse.jface.action.IAction}s to + * {@link org.eclipse.swt.widgets.Listener}s. + *

+ */ + private static class ActionProctarget { + private final IMenuBarCallback mCallbacks; + + public ActionProctarget(IMenuBarCallback callbacks) { + mCallbacks = callbacks; + } + + /** + * Will be called on 32bit SWT. + */ + @SuppressWarnings("unused") + public int actionProc(int id, int sel, int arg0) { + return (int) actionProc((long) id, (long) sel, (long) arg0); + } + + /** + * Will be called on 64bit SWT. + */ + public long actionProc(long id, long sel, long arg0) { + if (sel == mSelAboutMenuItemSelected) { + mCallbacks.onAboutMenuSelected(); + } else if (sel == mSelPreferencesMenuItemSelected) { + mCallbacks.onPreferencesMenuSelected(); + } else { + // Unknown selection! + } + // Return value is not used. + return 0; + } + } + + /** + * Construct a new CocoaUIEnhancer. + * + * @param mAppName The name of the application. It will be used to customize + * the About and Quit menu items. If you do not wish to customize + * the About and Quit menu items, just pass null here. + */ + public MenuBarEnhancerCocoa() { + } + + public MenuBarMode getMenuBarMode() { + return MenuBarMode.MAC_OS; + } + + /** + * Setup the About and Preferences native menut items with the + * given application name and links them to the callback. + * + * @param appName The application name. + * @param display The SWT display. Must not be null. + * @param callbacks The callbacks invoked by the menus. + */ + public void setupMenu( + String appName, + Display display, + IMenuBarCallback callbacks) { + + mAppName = appName; + + // This is our callback object whose 'actionProc' method will be called + // when the About or Preferences menuItem is invoked. + ActionProctarget target = new ActionProctarget(callbacks); + + try { + // Initialize the menuItems. + initialize(target); + } catch (Exception e) { + throw new IllegalStateException(e); + } + + // Schedule disposal of callback object + display.disposeExec(new Runnable() { + public void run() { + invoke(mProc3Args, "dispose"); + } + }); + } + + private void initialize(Object callbackObject) + throws Exception { + + Class osCls = classForName("org.eclipse.swt.internal.cocoa.OS"); + + // Register names in objective-c. + if (mSelAboutMenuItemSelected == 0) { + mSelPreferencesMenuItemSelected = registerName(osCls, "preferencesMenuItemSelected:"); //$NON-NLS-1$ + mSelAboutMenuItemSelected = registerName(osCls, "aboutMenuItemSelected:"); //$NON-NLS-1$ + } + + // Create an SWT Callback object that will invoke the actionProc method + // of our internal callback Object. + mProc3Args = new Callback(callbackObject, "actionProc", 3); //$NON-NLS-1$ + Method getAddress = Callback.class.getMethod("getAddress", new Class[0]); + Object object = getAddress.invoke(mProc3Args, (Object[]) null); + long proc3 = convertToLong(object); + if (proc3 == 0) { + SWT.error(SWT.ERROR_NO_MORE_CALLBACKS); + } + + Class nsMenuCls = classForName("org.eclipse.swt.internal.cocoa.NSMenu"); + Class nsMenuitemCls = classForName("org.eclipse.swt.internal.cocoa.NSMenuItem"); + Class nsStringCls = classForName("org.eclipse.swt.internal.cocoa.NSString"); + Class nsApplicationCls = classForName("org.eclipse.swt.internal.cocoa.NSApplication"); + + // Instead of creating a new delegate class in objective-c, + // just use the current SWTApplicationDelegate. An instance of this + // is a field of the Cocoa Display object and is already the target + // for the menuItems. So just get this class and add the new methods + // to it. + object = invoke(osCls, "objc_lookUpClass", new Object[] { + "SWTApplicationDelegate" + }); + long cls = convertToLong(object); + + // Add the action callbacks for Preferences and About menu items. + invoke(osCls, "class_addMethod", + new Object[] { + wrapPointer(cls), + wrapPointer(mSelPreferencesMenuItemSelected), + wrapPointer(proc3), "@:@"}); //$NON-NLS-1$ + invoke(osCls, "class_addMethod", + new Object[] { + wrapPointer(cls), + wrapPointer(mSelAboutMenuItemSelected), + wrapPointer(proc3), "@:@"}); //$NON-NLS-1$ + + // Get the Mac OS X Application menu. + Object sharedApplication = invoke(nsApplicationCls, "sharedApplication"); + Object mainMenu = invoke(sharedApplication, "mainMenu"); + Object mainMenuItem = invoke(nsMenuCls, mainMenu, "itemAtIndex", new Object[] { + wrapPointer(0) + }); + Object appMenu = invoke(mainMenuItem, "submenu"); + + // Create the About menu command + Object aboutMenuItem = + invoke(nsMenuCls, appMenu, "itemAtIndex", new Object[] { + wrapPointer(kAboutMenuItem) + }); + if (mAppName != null) { + Object nsStr = invoke(nsStringCls, "stringWith", new Object[] { + "About " + mAppName + }); + invoke(nsMenuitemCls, aboutMenuItem, "setTitle", new Object[] { + nsStr + }); + } + // Rename the quit action. + if (mAppName != null) { + Object quitMenuItem = + invoke(nsMenuCls, appMenu, "itemAtIndex", new Object[] { + wrapPointer(kQuitMenuItem) + }); + Object nsStr = invoke(nsStringCls, "stringWith", new Object[] { + "Quit " + mAppName + }); + invoke(nsMenuitemCls, quitMenuItem, "setTitle", new Object[] { + nsStr + }); + } + + // Enable the Preferences menuItem. + Object prefMenuItem = + invoke(nsMenuCls, appMenu, "itemAtIndex", new Object[] { + wrapPointer(kPreferencesMenuItem) + }); + invoke(nsMenuitemCls, prefMenuItem, "setEnabled", new Object[] { + true + }); + + // Set the action to execute when the About or Preferences menuItem is + // invoked. + // + // We don't need to set the target here as the current target is the + // SWTApplicationDelegate and we have registered the new selectors on + // it. So just set the new action to invoke the selector. + invoke(nsMenuitemCls, prefMenuItem, "setAction", + new Object[] { + wrapPointer(mSelPreferencesMenuItemSelected) + }); + invoke(nsMenuitemCls, aboutMenuItem, "setAction", + new Object[] { + wrapPointer(mSelAboutMenuItemSelected) + }); + } + + private long registerName(Class osCls, String name) + throws IllegalArgumentException, SecurityException, IllegalAccessException, + InvocationTargetException, NoSuchMethodException { + Object object = invoke(osCls, "sel_registerName", new Object[] { + name + }); + return convertToLong(object); + } + + private long convertToLong(Object object) { + if (object instanceof Integer) { + Integer i = (Integer) object; + return i.longValue(); + } + if (object instanceof Long) { + Long l = (Long) object; + return l.longValue(); + } + return 0; + } + + private static Object wrapPointer(long value) { + Class PTR_CLASS = C.PTR_SIZEOF == 8 ? long.class : int.class; + if (PTR_CLASS == long.class) { + return new Long(value); + } else { + return new Integer((int) value); + } + } + + private static Object invoke(Class clazz, String methodName, Object[] args) { + return invoke(clazz, null, methodName, args); + } + + private static Object invoke(Class clazz, Object target, String methodName, Object[] args) { + try { + Class[] signature = new Class[args.length]; + for (int i = 0; i < args.length; i++) { + Class thisClass = args[i].getClass(); + if (thisClass == Integer.class) + signature[i] = int.class; + else if (thisClass == Long.class) + signature[i] = long.class; + else if (thisClass == Byte.class) + signature[i] = byte.class; + else if (thisClass == Boolean.class) + signature[i] = boolean.class; + else + signature[i] = thisClass; + } + Method method = clazz.getMethod(methodName, signature); + return method.invoke(target, args); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + private Class classForName(String classname) { + try { + Class cls = Class.forName(classname); + return cls; + } catch (ClassNotFoundException e) { + throw new IllegalStateException(e); + } + } + + private Object invoke(Class cls, String methodName) { + return invoke(cls, methodName, (Class[]) null, (Object[]) null); + } + + private Object invoke(Class cls, String methodName, Class[] paramTypes, + Object... arguments) { + try { + Method m = cls.getDeclaredMethod(methodName, paramTypes); + return m.invoke(null, arguments); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + private Object invoke(Object obj, String methodName) { + return invoke(obj, methodName, (Class[]) null, (Object[]) null); + } + + private Object invoke(Object obj, String methodName, Class[] paramTypes, Object... arguments) { + try { + Method m = obj.getClass().getDeclaredMethod(methodName, paramTypes); + return m.invoke(obj, arguments); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/src/main/java/com/android/menubar/IMenuBarCallback.java b/andmore-swt/org.eclipse.andmore.swtmenubar/src/main/java/com/android/menubar/IMenuBarCallback.java new file mode 100644 index 00000000..490d60fc --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/src/main/java/com/android/menubar/IMenuBarCallback.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2011 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.menubar; + + + +/** + * Callbacks used by {@link IMenuBarEnhancer}. + */ +public interface IMenuBarCallback { + /** + * Invoked when the About menu item is selected by the user. + */ + abstract public void onAboutMenuSelected(); + + /** + * Invoked when the Preferences or Options menu item is selected by the user. + */ + abstract public void onPreferencesMenuSelected(); + + /** + * Used by the enhancer implementations to report errors. + * + * @param format A printf-like format string. + * @param args The parameters for the printf-like format string. + */ + abstract public void printError(String format, Object...args); +} diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/src/main/java/com/android/menubar/IMenuBarEnhancer.java b/andmore-swt/org.eclipse.andmore.swtmenubar/src/main/java/com/android/menubar/IMenuBarEnhancer.java new file mode 100644 index 00000000..77b7051c --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/src/main/java/com/android/menubar/IMenuBarEnhancer.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2011 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.menubar; + +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Menu; + + +/** + * Interface to the platform-specific MenuBarEnhancer implementation returned by + * {@link MenuBarEnhancer#setupMenu}. + */ +public interface IMenuBarEnhancer { + + /** Values that indicate how the menu bar is being handlded. */ + public enum MenuBarMode { + /** + * The Mac-specific About and Preferences are being used. + * No File > Exit menu should be provided by the application. + */ + MAC_OS, + /** + * The provided SWT {@link Menu} is being used for About and Options. + * The application should provide a File > Exit menu. + */ + GENERIC + } + + /** + * Returns a {@link MenuBarMode} enum that indicates how the menu bar is going to + * or has been modified. This is implementation specific and can be called before or + * after {@link #setupMenu}. + *

+ * Callers would typically call that to know if they need to hide or display + * menu items. For example when {@link MenuBarMode#MAC_OS} is used, an app + * would typically not need to provide any "File > Exit" menu item. + * + * @return One of the {@link MenuBarMode} values. + */ + public MenuBarMode getMenuBarMode(); + + /** + * Updates the menu bar to provide an About menu item and a Preferences menu item. + * Depending on the platform, the menu items might be decorated with the + * given {@code appName}. + *

+ * Users should not call this directly. + * {@link MenuBarEnhancer#setupMenu} should be used instead. + * + * @param appName Name used for the About menu item and similar. Must not be null. + * @param display The SWT display. Must not be null. + * @param callbacks Callbacks called when "About" and "Preferences" menu items are invoked. + * Must not be null. + */ + public void setupMenu( + String appName, + Display display, + IMenuBarCallback callbacks); +} diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/src/main/java/com/android/menubar/MenuBarEnhancer.java b/andmore-swt/org.eclipse.andmore.swtmenubar/src/main/java/com/android/menubar/MenuBarEnhancer.java new file mode 100644 index 00000000..179419c0 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/src/main/java/com/android/menubar/MenuBarEnhancer.java @@ -0,0 +1,248 @@ +/* + * Copyright (C) 2011 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.menubar; + +import com.android.menubar.IMenuBarEnhancer.MenuBarMode; + +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.action.IMenuManager; +import org.eclipse.jface.action.Separator; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Menu; +import org.eclipse.swt.widgets.MenuItem; + + +/** + * On Mac, {@link MenuBarEnhancer#setupMenu} plugs a listener on the About and the + * Preferences menu items of the standard "application" menu in the menu bar. + * On Windows or Linux, it adds relevant items to a given {@link Menu} linked to + * the same listeners. + */ +public final class MenuBarEnhancer { + + private MenuBarEnhancer() { + } + + /** + * Creates an instance of {@link IMenuBarEnhancer} specific to the current platform + * and invoke its {@link IMenuBarEnhancer#setupMenu} to updates the menu bar. + *

+ * Depending on the platform, this will either hook into the existing About menu item + * and a Preferences or Options menu item or add new ones to the given {@code swtMenu}. + * Depending on the platform, the menu items might be decorated with the + * given {@code appName}. + *

+ * Potential errors are reported through {@link IMenuBarCallback}. + * + * @param appName Name used for the About menu item and similar. Must not be null. + * @param swtMenu For non-mac platform this is the menu where the "About" and + * the "Options" menu items are created. Typically the menu might be + * called "Tools". Must not be null. + * @param callbacks Callbacks called when "About" and "Preferences" menu items are invoked. + * Must not be null. + * @return An actual {@link IMenuBarEnhancer} implementation. Can be null on failure. + * This is currently not of any use for the caller but is left in case + * we want to expand the functionality later. + */ + public static IMenuBarEnhancer setupMenu( + String appName, + final Menu swtMenu, + IMenuBarCallback callbacks) { + + IMenuBarEnhancer enhancer = getEnhancer(callbacks, swtMenu.getDisplay()); + + // Default implementation for generic platforms + if (enhancer == null) { + enhancer = getGenericEnhancer(swtMenu); + } + + try { + enhancer.setupMenu(appName, swtMenu.getDisplay(), callbacks); + } catch (Exception e) { + // If the enhancer failed, try to fall back on the generic one + if (enhancer.getMenuBarMode() != MenuBarMode.GENERIC) { + enhancer = getGenericEnhancer(swtMenu); + try { + enhancer.setupMenu(appName, swtMenu.getDisplay(), callbacks); + } catch (Exception e2) { + callbacks.printError("SWTMenuBar failed: %s", e2.toString()); + return null; + } + } + } + return enhancer; + } + + private static IMenuBarEnhancer getGenericEnhancer(final Menu swtMenu) { + IMenuBarEnhancer enhancer; + enhancer = new IMenuBarEnhancer() { + + @Override + public MenuBarMode getMenuBarMode() { + return MenuBarMode.GENERIC; + } + + @Override + public void setupMenu( + String appName, + Display display, + final IMenuBarCallback callbacks) { + if (swtMenu.getItemCount() > 0) { + new MenuItem(swtMenu, SWT.SEPARATOR); + } + + // Note: we use "Preferences" on Mac and "Options" on Windows/Linux. + final MenuItem pref = new MenuItem(swtMenu, SWT.NONE); + pref.setText("&Options..."); + + final MenuItem about = new MenuItem(swtMenu, SWT.NONE); + about.setText("&About..."); + + pref.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + try { + pref.setEnabled(false); + callbacks.onPreferencesMenuSelected(); + super.widgetSelected(e); + } finally { + pref.setEnabled(true); + } + } + }); + + about.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + try { + about.setEnabled(false); + callbacks.onAboutMenuSelected(); + super.widgetSelected(e); + } finally { + about.setEnabled(true); + } + } + }); + } + }; + return enhancer; + } + + + public static IMenuBarEnhancer setupMenuManager( + String appName, + Display display, + final IMenuManager menuManager, + final IAction aboutAction, + final IAction preferencesAction, + final IAction quitAction) { + + IMenuBarCallback callbacks = new IMenuBarCallback() { + @Override + public void printError(String format, Object... args) { + System.err.println(String.format(format, args)); + } + + @Override + public void onPreferencesMenuSelected() { + if (preferencesAction != null) { + preferencesAction.run(); + } + } + + @Override + public void onAboutMenuSelected() { + if (aboutAction != null) { + aboutAction.run(); + } + } + }; + + IMenuBarEnhancer enhancer = getEnhancer(callbacks, display); + + // Default implementation for generic platforms + if (enhancer == null) { + enhancer = new IMenuBarEnhancer() { + + @Override + public MenuBarMode getMenuBarMode() { + return MenuBarMode.GENERIC; + } + + @Override + public void setupMenu( + String appName, + Display display, + final IMenuBarCallback callbacks) { + if (!menuManager.isEmpty()) { + menuManager.add(new Separator()); + } + + if (aboutAction != null) { + menuManager.add(aboutAction); + } + if (preferencesAction != null) { + menuManager.add(preferencesAction); + } + if (quitAction != null) { + if (aboutAction != null || preferencesAction != null) { + menuManager.add(new Separator()); + } + menuManager.add(quitAction); + } + } + }; + } + + enhancer.setupMenu(appName, display, callbacks); + return enhancer; + } + + private static IMenuBarEnhancer getEnhancer(IMenuBarCallback callbacks, Display display) { + IMenuBarEnhancer enhancer = null; + String p = SWT.getPlatform(); + String className = null; + if ("cocoa".equals(p)) { //$NON-NLS-1$ + className = "com.android.menubar.internal.MenuBarEnhancerCocoa"; //$NON-NLS-1$ + + if (SWT.getVersion() >= 3700 && MenuBarEnhancer37.isSupported(display)) { + className = MenuBarEnhancer37.class.getName(); + } + } + + if (System.getenv("DEBUG_SWTMENUBAR") != null) { + callbacks.printError("DEBUG SwtMenuBar: SWT=%1$s, class=%2$s", p, className); + } + + if (className != null) { + try { + Class clazz = Class.forName(className); + enhancer = (IMenuBarEnhancer) clazz.newInstance(); + } catch (Exception e) { + // Log an error and fallback on the default implementation. + callbacks.printError( + "Failed to instantiate %1$s: %2$s", //$NON-NLS-1$ + className, + e.toString()); + } + } + return enhancer; + } +} diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/src/main/java/com/android/menubar/MenuBarEnhancer37.java b/andmore-swt/org.eclipse.andmore.swtmenubar/src/main/java/com/android/menubar/MenuBarEnhancer37.java new file mode 100644 index 00000000..3e03d6cc --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/src/main/java/com/android/menubar/MenuBarEnhancer37.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2012 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. + * + * References: + * Based on the SWT snippet example at + * http://dev.eclipse.org/viewcvs/viewvc.cgi/org.eclipse.swt.snippets/src/org/eclipse/swt/snippets/Snippet354.java?view=co + */ + +package com.android.menubar; + + +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Menu; +import org.eclipse.swt.widgets.MenuItem; + +import java.lang.reflect.Method; + +public class MenuBarEnhancer37 implements IMenuBarEnhancer { + + private static final int kAboutMenuItem = -1; // SWT.ID_ABOUT in SWT 3.7 + private static final int kPreferencesMenuItem = -2; // SWT.ID_PREFERENCES in SWT 3.7 + private static final int kQuitMenuItem = -6; // SWT.ID_QUIT in SWT 3.7 + + public MenuBarEnhancer37() { + } + + @Override + public MenuBarMode getMenuBarMode() { + return MenuBarMode.MAC_OS; + } + + /** + * Setup the About and Preferences native menut items with the + * given application name and links them to the callback. + * + * @param appName The application name. + * @param display The SWT display. Must not be null. + * @param callbacks The callbacks invoked by the menus. + */ + @Override + public void setupMenu( + String appName, + Display display, + IMenuBarCallback callbacks) { + + try { + // Initialize the menuItems. + initialize(display, appName, callbacks); + } catch (Exception e) { + throw new IllegalStateException(e); + } + + // Schedule disposal of callback object + display.disposeExec(new Runnable() { + @Override + public void run() { + } + }); + } + + /** + * Checks whether the required SWT 3.7 APIs are available. + *
+ * Calling this will load the class, which is OK since this class doesn't + * directly use any SWT 3.7 API -- instead it uses reflection so that the + * code can be loaded under SWT 3.6. + * + * @param display The current SWT display. + * @return True if the SWT 3.7 API are available and this enhancer can be used. + */ + public static boolean isSupported(Display display) { + try { + Object sysMenu = call0(display, "getSystemMenu"); + if (sysMenu instanceof Menu) { + return findMenuById((Menu)sysMenu, kPreferencesMenuItem) != null && + findMenuById((Menu)sysMenu, kAboutMenuItem) != null; + } + } catch (Exception ignore) {} + return false; + } + + private void initialize( + Display display, + String appName, + final IMenuBarCallback callbacks) + throws Exception { + Object sysMenu = call0(display, "getSystemMenu"); + if (sysMenu instanceof Menu) { + MenuItem menu = findMenuById((Menu)sysMenu, kPreferencesMenuItem); + if (menu != null) { + menu.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent event) { + callbacks.onPreferencesMenuSelected(); + } + }); + } + + menu = findMenuById((Menu)sysMenu, kAboutMenuItem); + if (menu != null) { + menu.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent event) { + callbacks.onAboutMenuSelected(); + } + }); + menu.setText("About " + appName); + } + + menu = findMenuById((Menu)sysMenu, kQuitMenuItem); + if (menu != null) { + // We already support the "quit" operation, no need for an extra handler here. + menu.setText("Quit " + appName); + } + + } + } + + private static Object call0(Object obj, String method) { + try { + Method m = obj.getClass().getMethod(method, (Class[])null); + if (m != null) { + return m.invoke(obj, (Object[])null); + } + } catch (Exception ignore) {} + return null; + } + + private static MenuItem findMenuById(Menu menu, int id) { + MenuItem[] items = menu.getItems(); + for (int i = items.length - 1; i >= 0; i--) { + MenuItem item = items[i]; + Object menuId = call0(item, "getID"); + if (menuId instanceof Integer) { + if (((Integer) menuId).intValue() == id) { + return item; + } + } + } + return null; + } +} diff --git a/andmore-swt/org.eclipse.andmore.swtmenubar/swtmenubar.iml b/andmore-swt/org.eclipse.andmore.swtmenubar/swtmenubar.iml new file mode 100644 index 00000000..22023bf7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swtmenubar/swtmenubar.iml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/.classpath b/andmore-swt/org.eclipse.andmore.traceviewuilib/.classpath new file mode 100644 index 00000000..00782ce2 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/.gitignore b/andmore-swt/org.eclipse.andmore.traceviewuilib/.gitignore new file mode 100644 index 00000000..0f630157 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/.gitignore @@ -0,0 +1,2 @@ +/target/ +/bin/ diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/.project b/andmore-swt/org.eclipse.andmore.traceviewuilib/.project new file mode 100644 index 00000000..dcc06edb --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/.project @@ -0,0 +1,34 @@ + + + org.eclipse.andmore.traceviewuilib + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/.settings/README.txt b/andmore-swt/org.eclipse.andmore.traceviewuilib/.settings/README.txt new file mode 100644 index 00000000..2945bee0 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/.settings/README.txt @@ -0,0 +1,2 @@ +Copy this in eclipse project as a .settings folder at the root. +This ensure proper compilation compliance and warning/error levels. \ No newline at end of file diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.core.resources.prefs b/andmore-swt/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..4824b802 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.jdt.core.prefs b/andmore-swt/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..295926d9 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,7 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.m2e.core.prefs b/andmore-swt/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/META-INF/MANIFEST.MF b/andmore-swt/org.eclipse.andmore.traceviewuilib/META-INF/MANIFEST.MF new file mode 100644 index 00000000..09e73fec --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/META-INF/MANIFEST.MF @@ -0,0 +1,15 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Localization: plugin +Bundle-Name: %Bundle-Name +Bundle-SymbolicName: org.eclipse.andmore.traceviewuilib;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Vendor: %Bundle-Vendor +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Require-Bundle: org.eclipse.core.commands;bundle-version="3.8.1", + org.eclipse.equinox.common;bundle-version="3.8.0", + org.eclipse.jface;bundle-version="3.12.2", + org.eclipse.andmore.swt, + org.eclipse.andmore.sdkstats +Export-Package: com.android.traceview +Bundle-ClassPath: . diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/MODULE_LICENSE_EPL b/andmore-swt/org.eclipse.andmore.traceviewuilib/MODULE_LICENSE_EPL new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/NOTICE b/andmore-swt/org.eclipse.andmore.traceviewuilib/NOTICE new file mode 100644 index 00000000..70c54220 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/NOTICE @@ -0,0 +1,190 @@ + + Copyright (c) 2005-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. + + 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. + + + 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 + diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/README b/andmore-swt/org.eclipse.andmore.traceviewuilib/README new file mode 100644 index 00000000..7f9f87dd --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/README @@ -0,0 +1,11 @@ +Using the Eclipse projects for traceview. + +traceview 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. diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/build.gradle b/andmore-swt/org.eclipse.andmore.traceviewuilib/build.gradle new file mode 100644 index 00000000..1d923d7f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/build.gradle @@ -0,0 +1,23 @@ +group = 'com.android.tools' +archivesBaseName = 'traceview' + +dependencies { + compile project(':base:common') + compile project(':swt:sdkstats') +} + +sdk { + linux { + item('etc/traceview') { executable true } + } + mac { + item('etc/traceview') { executable true } + } + windows { + item 'etc/traceview.bat' + } +} + +// configure the manifest of the buildDistributionJar task. +sdkJar.manifest.attributes("Main-Class": "com.android.traceview.MainWindow") + diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/build.properties b/andmore-swt/org.eclipse.andmore.traceviewuilib/build.properties new file mode 100644 index 00000000..ecc09051 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/build.properties @@ -0,0 +1,4 @@ +source.. = src/main/java/ +output.. = bin/ +bin.includes = META-INF/,\ + . diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/etc/traceview b/andmore-swt/org.eclipse.andmore.traceviewuilib/etc/traceview new file mode 100644 index 00000000..4bb5ff54 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/etc/traceview @@ -0,0 +1,108 @@ +#!/bin/bash +# +# Copyright 2005-2006, 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}"` +progname=`basename "${prog}"` +cd "${progdir}" +progdir=`pwd` +prog="${progdir}"/"${progname}" +cd "${oldwd}" + +jarfile=traceview.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 "${progname}: can't find $jarfile" + exit 1 +fi + +javaCmd="java" + +os=`uname` +if [ $os == 'Darwin' ]; then + javaOpts="-Xmx1600M -XstartOnFirstThread" +else + javaOpts="-Xmx1600M" +fi + +if [ `uname` = "Linux" ]; then + export GDK_NATIVE_WINDOWS=true +fi + +while expr "x$1" : 'x-J' >/dev/null; do + opt=`expr "x$1" : 'x-J\(.*\)'` + javaOpts="${javaOpts} -${opt}" + shift +done + +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}/prebuilts/tools/${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 + +if [ -x $progdir/monitor ]; then + echo "The standalone version of traceview is deprecated." + echo "Please use Android Device Monitor (tools/monitor) instead." +fi +exec "${javaCmd}" $javaOpts -Djava.ext.dirs="$frameworkdir" -Dcom.android.traceview.toolsdir="$progdir" -jar "$jarpath" "$@" diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/etc/traceview.bat b/andmore-swt/org.eclipse.andmore.traceviewuilib/etc/traceview.bat new file mode 100644 index 00000000..92fd8bef --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/etc/traceview.bat @@ -0,0 +1,65 @@ +@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=traceview.jar +set frameworkdir=. + +if exist %frameworkdir%\%jarfile% goto JarFileOk + set frameworkdir=lib + +if exist %frameworkdir%\%jarfile% goto JarFileOk + set frameworkdir=..\framework + +:JarFileOk + +set jarpath=%frameworkdir%\%jarfile% + +if not defined ANDROID_SWT goto QueryArch + set swt_path=%ANDROID_SWT% + goto SwtDone + +:QueryArch + + for /f "delims=" %%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% + +echo The standalone version of traceview is deprecated. +echo Please use Android Device Monitor (tools/monitor) instead. +call "%java_exe%" "-Djava.ext.dirs=%javaextdirs%" -Dcom.android.traceview.toolsdir= -jar %jarpath% %* diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/plugin.properties b/andmore-swt/org.eclipse.andmore.traceviewuilib/plugin.properties new file mode 100644 index 00000000..53bfd0c9 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/plugin.properties @@ -0,0 +1,4 @@ +#Properties file for com.android.ide.eclipse.traceview +Bundle-Vendor = Eclipse Andmore +Bundle-Name = UI Traceview +category.name = Android diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/pom.xml b/andmore-swt/org.eclipse.andmore.traceviewuilib/pom.xml new file mode 100644 index 00000000..ece003fe --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + ../pom.xml + org.eclipse.andmore + swt-droid-parent + 0.5.2-SNAPSHOT + + org.eclipse.andmore.traceviewuilib + eclipse-plugin + traceviewuilib + + + + org.eclipse.tycho + tycho-maven-plugin + true + + + org.eclipse.tycho + tycho-source-plugin + ${tycho-version} + + + plugin-source + + plugin-source + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/Call.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/Call.java new file mode 100644 index 00000000..3bc434ba --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/Call.java @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2006 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.traceview; + +import org.eclipse.swt.graphics.Color; + +class Call implements TimeLineView.Block { + final private ThreadData mThreadData; + final private MethodData mMethodData; + final Call mCaller; // the caller, or null if this is the root + + private String mName; + private boolean mIsRecursive; + + long mGlobalStartTime; + long mGlobalEndTime; + + long mThreadStartTime; + long mThreadEndTime; + + long mInclusiveRealTime; // real time spent in this call including its children + long mExclusiveRealTime; // real time spent in this call including its children + + long mInclusiveCpuTime; // cpu time spent in this call including its children + long mExclusiveCpuTime; // cpu time spent in this call excluding its children + + Call(ThreadData threadData, MethodData methodData, Call caller) { + mThreadData = threadData; + mMethodData = methodData; + mName = methodData.getProfileName(); + mCaller = caller; + } + + public void updateName() { + mName = mMethodData.getProfileName(); + } + + @Override + public double addWeight(int x, int y, double weight) { + return mMethodData.addWeight(x, y, weight); + } + + @Override + public void clearWeight() { + mMethodData.clearWeight(); + } + + @Override + public long getStartTime() { + return mGlobalStartTime; + } + + @Override + public long getEndTime() { + return mGlobalEndTime; + } + + @Override + public long getExclusiveCpuTime() { + return mExclusiveCpuTime; + } + + @Override + public long getInclusiveCpuTime() { + return mInclusiveCpuTime; + } + + @Override + public long getExclusiveRealTime() { + return mExclusiveRealTime; + } + + @Override + public long getInclusiveRealTime() { + return mInclusiveRealTime; + } + + @Override + public Color getColor() { + return mMethodData.getColor(); + } + + @Override + public String getName() { + return mName; + } + + public void setName(String name) { + mName = name; + } + + public ThreadData getThreadData() { + return mThreadData; + } + + public int getThreadId() { + return mThreadData.getId(); + } + + @Override + public MethodData getMethodData() { + return mMethodData; + } + + @Override + public boolean isContextSwitch() { + return mMethodData.getId() == -1; + } + + @Override + public boolean isIgnoredBlock() { + // Ignore the top-level call or context switches within the top-level call. + return mCaller == null || isContextSwitch() && mCaller.mCaller == null; + } + + @Override + public TimeLineView.Block getParentBlock() { + return mCaller; + } + + public boolean isRecursive() { + return mIsRecursive; + } + + void setRecursive(boolean isRecursive) { + mIsRecursive = isRecursive; + } + + void addCpuTime(long elapsedCpuTime) { + mExclusiveCpuTime += elapsedCpuTime; + mInclusiveCpuTime += elapsedCpuTime; + } + + /** + * Record time spent in the method call. + */ + void finish() { + if (mCaller != null) { + mCaller.mInclusiveCpuTime += mInclusiveCpuTime; + mCaller.mInclusiveRealTime += mInclusiveRealTime; + } + + mMethodData.addElapsedExclusive(mExclusiveCpuTime, mExclusiveRealTime); + if (!mIsRecursive) { + mMethodData.addTopExclusive(mExclusiveCpuTime, mExclusiveRealTime); + } + mMethodData.addElapsedInclusive(mInclusiveCpuTime, mInclusiveRealTime, + mIsRecursive, mCaller); + } + + public static final class TraceAction { + public static final int ACTION_ENTER = 0; + public static final int ACTION_EXIT = 1; + + public final int mAction; + public final Call mCall; + + public TraceAction(int action, Call call) { + mAction = action; + mCall = call; + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ColorController.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ColorController.java new file mode 100644 index 00000000..cdbc32fe --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ColorController.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2006 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.traceview; + +import java.util.HashMap; + +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.graphics.RGB; +import org.eclipse.swt.widgets.Display; + +public class ColorController { + private static final int[] systemColors = { SWT.COLOR_BLUE, SWT.COLOR_RED, + SWT.COLOR_GREEN, SWT.COLOR_CYAN, SWT.COLOR_MAGENTA, SWT.COLOR_DARK_BLUE, + SWT.COLOR_DARK_RED, SWT.COLOR_DARK_GREEN, SWT.COLOR_DARK_YELLOW, + SWT.COLOR_DARK_CYAN, SWT.COLOR_DARK_MAGENTA, SWT.COLOR_BLACK }; + + private static RGB[] rgbColors = { new RGB(90, 90, 255), // blue + new RGB(0, 240, 0), // green + new RGB(255, 0, 0), // red + new RGB(0, 255, 255), // cyan + new RGB(255, 80, 255), // magenta + new RGB(200, 200, 0), // yellow + new RGB(40, 0, 200), // dark blue + new RGB(150, 255, 150), // light green + new RGB(150, 0, 0), // dark red + new RGB(30, 150, 150), // dark cyan + new RGB(200, 200, 255), // light blue + new RGB(0, 120, 0), // dark green + new RGB(255, 150, 150), // light red + new RGB(140, 80, 140), // dark magenta + new RGB(150, 100, 50), // brown + new RGB(70, 70, 70), // dark grey + }; + + private static HashMap colorCache = new HashMap(); + private static HashMap imageCache = new HashMap(); + + public ColorController() { + } + + public static Color requestColor(Display display, RGB rgb) { + return requestColor(display, rgb.red, rgb.green, rgb.blue); + } + + public static Image requestColorSquare(Display display, RGB rgb) { + return requestColorSquare(display, rgb.red, rgb.green, rgb.blue); + } + + public static Color requestColor(Display display, int red, int green, int blue) { + int key = (red << 16) | (green << 8) | blue; + Color color = colorCache.get(key); + if (color == null) { + color = new Color(display, red, green, blue); + colorCache.put(key, color); + } + return color; + } + + public static Image requestColorSquare(Display display, int red, int green, int blue) { + int key = (red << 16) | (green << 8) | blue; + Image image = imageCache.get(key); + if (image == null) { + image = new Image(display, 8, 14); + GC gc = new GC(image); + Color color = requestColor(display, red, green, blue); + gc.setBackground(color); + gc.fillRectangle(image.getBounds()); + gc.dispose(); + imageCache.put(key, image); + } + return image; + } + + public static void assignMethodColors(Display display, MethodData[] methods) { + int nextColorIndex = 0; + for (MethodData md : methods) { + RGB rgb = rgbColors[nextColorIndex]; + if (++nextColorIndex == rgbColors.length) + nextColorIndex = 0; + Color color = requestColor(display, rgb); + Image image = requestColorSquare(display, rgb); + md.setColor(color); + md.setImage(image); + + // Compute and set a faded color + int fadedRed = 150 + rgb.red / 4; + int fadedGreen = 150 + rgb.green / 4; + int fadedBlue = 150 + rgb.blue / 4; + RGB faded = new RGB(fadedRed, fadedGreen, fadedBlue); + color = requestColor(display, faded); + image = requestColorSquare(display, faded); + md.setFadedColor(color); + md.setFadedImage(image); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/DmTraceReader.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/DmTraceReader.java new file mode 100644 index 00000000..9fd586b1 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/DmTraceReader.java @@ -0,0 +1,754 @@ +/* + * Copyright (C) 2006 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.traceview; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.BufferUnderflowException; +import java.nio.ByteOrder; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class DmTraceReader extends TraceReader { + private static final int TRACE_MAGIC = 0x574f4c53; + + private static final int METHOD_TRACE_ENTER = 0x00; // method entry + private static final int METHOD_TRACE_EXIT = 0x01; // method exit + private static final int METHOD_TRACE_UNROLL = 0x02; // method exited by exception unrolling + + // When in dual clock mode, we report that a context switch has occurred + // when skew between the real time and thread cpu clocks is more than this + // many microseconds. + private static final long MIN_CONTEXT_SWITCH_TIME_USEC = 100; + + private enum ClockSource { + THREAD_CPU, WALL, DUAL, + }; + + private int mVersionNumber; + private boolean mRegression; + private ProfileProvider mProfileProvider; + private String mTraceFileName; + private MethodData mTopLevel; + private ArrayList mCallList; + private HashMap mPropertiesMap; + private HashMap mMethodMap; + private HashMap mThreadMap; + private ThreadData[] mSortedThreads; + private MethodData[] mSortedMethods; + private long mTotalCpuTime; + private long mTotalRealTime; + private MethodData mContextSwitch; + private int mRecordSize; + private ClockSource mClockSource; + + // A regex for matching the thread "id name" lines in the .key file + private static final Pattern mIdNamePattern = Pattern.compile("(\\d+)\t(.*)"); //$NON-NLS-1$ + + public DmTraceReader(String traceFileName, boolean regression) throws IOException { + mTraceFileName = traceFileName; + mRegression = regression; + mPropertiesMap = new HashMap(); + mMethodMap = new HashMap(); + mThreadMap = new HashMap(); + mCallList = new ArrayList(); + + // Create a single top-level MethodData object to hold the profile data + // for time spent in the unknown caller. + mTopLevel = new MethodData(0, "(toplevel)"); + mContextSwitch = new MethodData(-1, "(context switch)"); + mMethodMap.put(0, mTopLevel); + mMethodMap.put(-1, mContextSwitch); + generateTrees(); + } + + void generateTrees() throws IOException { + long offset = parseKeys(); + parseData(offset); + analyzeData(); + } + + @Override + public ProfileProvider getProfileProvider() { + if (mProfileProvider == null) + mProfileProvider = new ProfileProvider(this); + return mProfileProvider; + } + + private MappedByteBuffer mapFile(String filename, long offset) throws IOException { + MappedByteBuffer buffer = null; + FileInputStream dataFile = new FileInputStream(filename); + try { + File file = new File(filename); + FileChannel fc = dataFile.getChannel(); + buffer = fc.map(FileChannel.MapMode.READ_ONLY, offset, file.length() - offset); + buffer.order(ByteOrder.LITTLE_ENDIAN); + + return buffer; + } finally { + dataFile.close(); // this *also* closes the associated channel, fc + } + } + + private void readDataFileHeader(MappedByteBuffer buffer) { + int magic = buffer.getInt(); + if (magic != TRACE_MAGIC) { + System.err.printf( + "Error: magic number mismatch; got 0x%x, expected 0x%x\n", + magic, TRACE_MAGIC); + throw new RuntimeException(); + } + + // read version + int version = buffer.getShort(); + if (version != mVersionNumber) { + System.err.printf( + "Error: version number mismatch; got %d in data header but %d in options\n", + version, mVersionNumber); + throw new RuntimeException(); + } + if (version < 1 || version > 3) { + System.err.printf( + "Error: unsupported trace version number %d. " + + "Please use a newer version of TraceView to read this file.", version); + throw new RuntimeException(); + } + + // read offset + int offsetToData = buffer.getShort() - 16; + + // read startWhen + buffer.getLong(); + + // read record size + if (version == 1) { + mRecordSize = 9; + } else if (version == 2) { + mRecordSize = 10; + } else { + mRecordSize = buffer.getShort(); + offsetToData -= 2; + } + + // Skip over offsetToData bytes + while (offsetToData-- > 0) { + buffer.get(); + } + } + + private void parseData(long offset) throws IOException { + MappedByteBuffer buffer = mapFile(mTraceFileName, offset); + readDataFileHeader(buffer); + + ArrayList trace = null; + if (mClockSource == ClockSource.THREAD_CPU) { + trace = new ArrayList(); + } + + final boolean haveThreadClock = mClockSource != ClockSource.WALL; + final boolean haveGlobalClock = mClockSource != ClockSource.THREAD_CPU; + + // Parse all call records to obtain elapsed time information. + ThreadData prevThreadData = null; + for (;;) { + int threadId; + int methodId; + long threadTime, globalTime; + try { + int recordSize = mRecordSize; + + if (mVersionNumber == 1) { + threadId = buffer.get(); + recordSize -= 1; + } else { + threadId = buffer.getShort(); + recordSize -= 2; + } + + methodId = buffer.getInt(); + recordSize -= 4; + + switch (mClockSource) { + case WALL: + threadTime = 0; + globalTime = buffer.getInt(); + recordSize -= 4; + break; + case DUAL: + threadTime = buffer.getInt(); + globalTime = buffer.getInt(); + recordSize -= 8; + break; + default: + case THREAD_CPU: + threadTime = buffer.getInt(); + globalTime = 0; + recordSize -= 4; + break; + } + + while (recordSize-- > 0) { + buffer.get(); + } + } catch (BufferUnderflowException ex) { + break; + } + + int methodAction = methodId & 0x03; + methodId = methodId & ~0x03; + MethodData methodData = mMethodMap.get(methodId); + if (methodData == null) { + String name = String.format("(0x%1$x)", methodId); //$NON-NLS-1$ + methodData = new MethodData(methodId, name); + mMethodMap.put(methodId, methodData); + } + + ThreadData threadData = mThreadMap.get(threadId); + if (threadData == null) { + String name = String.format("[%1$d]", threadId); //$NON-NLS-1$ + threadData = new ThreadData(threadId, name, mTopLevel); + mThreadMap.put(threadId, threadData); + } + + long elapsedGlobalTime = 0; + if (haveGlobalClock) { + if (!threadData.mHaveGlobalTime) { + threadData.mGlobalStartTime = globalTime; + threadData.mHaveGlobalTime = true; + } else { + elapsedGlobalTime = globalTime - threadData.mGlobalEndTime; + } + threadData.mGlobalEndTime = globalTime; + } + + if (haveThreadClock) { + long elapsedThreadTime = 0; + if (!threadData.mHaveThreadTime) { + threadData.mThreadStartTime = threadTime; + threadData.mThreadCurrentTime = threadTime; + threadData.mHaveThreadTime = true; + } else { + elapsedThreadTime = threadTime - threadData.mThreadEndTime; + } + threadData.mThreadEndTime = threadTime; + + if (!haveGlobalClock) { + // Detect context switches whenever execution appears to switch from one + // thread to another. This assumption is only valid on uniprocessor + // systems (which is why we now have a dual clock mode). + // We represent context switches in the trace by pushing a call record + // with MethodData mContextSwitch onto the stack of the previous + // thread. We arbitrarily set the start and end time of the context + // switch such that the context switch occurs in the middle of the thread + // time and itself accounts for zero thread time. + if (prevThreadData != null && prevThreadData != threadData) { + // Begin context switch from previous thread. + Call switchCall = prevThreadData.enter(mContextSwitch, trace); + switchCall.mThreadStartTime = prevThreadData.mThreadEndTime; + mCallList.add(switchCall); + + // Return from context switch to current thread. + Call top = threadData.top(); + if (top.getMethodData() == mContextSwitch) { + threadData.exit(mContextSwitch, trace); + long beforeSwitch = elapsedThreadTime / 2; + top.mThreadStartTime += beforeSwitch; + top.mThreadEndTime = top.mThreadStartTime; + } + } + prevThreadData = threadData; + } else { + // If we have a global clock, then we can detect context switches (or blocking + // calls or cpu suspensions or clock anomalies) by comparing global time to + // thread time for successive calls that occur on the same thread. + // As above, we represent the context switch using a special method call. + long sleepTime = elapsedGlobalTime - elapsedThreadTime; + if (sleepTime > MIN_CONTEXT_SWITCH_TIME_USEC) { + Call switchCall = threadData.enter(mContextSwitch, trace); + long beforeSwitch = elapsedThreadTime / 2; + long afterSwitch = elapsedThreadTime - beforeSwitch; + switchCall.mGlobalStartTime = globalTime - elapsedGlobalTime + beforeSwitch; + switchCall.mGlobalEndTime = globalTime - afterSwitch; + switchCall.mThreadStartTime = threadTime - afterSwitch; + switchCall.mThreadEndTime = switchCall.mThreadStartTime; + threadData.exit(mContextSwitch, trace); + mCallList.add(switchCall); + } + } + + // Add thread CPU time. + Call top = threadData.top(); + top.addCpuTime(elapsedThreadTime); + } + + switch (methodAction) { + case METHOD_TRACE_ENTER: { + Call call = threadData.enter(methodData, trace); + if (haveGlobalClock) { + call.mGlobalStartTime = globalTime; + } + if (haveThreadClock) { + call.mThreadStartTime = threadTime; + } + mCallList.add(call); + break; + } + case METHOD_TRACE_EXIT: + case METHOD_TRACE_UNROLL: { + Call call = threadData.exit(methodData, trace); + if (call != null) { + if (haveGlobalClock) { + call.mGlobalEndTime = globalTime; + } + if (haveThreadClock) { + call.mThreadEndTime = threadTime; + } + } + break; + } + default: + throw new RuntimeException("Unrecognized method action: " + methodAction); + } + } + + // Exit any pending open-ended calls. + for (ThreadData threadData : mThreadMap.values()) { + threadData.endTrace(trace); + } + + // Recreate the global timeline from thread times, if needed. + if (!haveGlobalClock) { + long globalTime = 0; + prevThreadData = null; + for (TraceAction traceAction : trace) { + Call call = traceAction.mCall; + ThreadData threadData = call.getThreadData(); + + if (traceAction.mAction == TraceAction.ACTION_ENTER) { + long threadTime = call.mThreadStartTime; + globalTime += call.mThreadStartTime - threadData.mThreadCurrentTime; + call.mGlobalStartTime = globalTime; + if (!threadData.mHaveGlobalTime) { + threadData.mHaveGlobalTime = true; + threadData.mGlobalStartTime = globalTime; + } + threadData.mThreadCurrentTime = threadTime; + } else if (traceAction.mAction == TraceAction.ACTION_EXIT) { + long threadTime = call.mThreadEndTime; + globalTime += call.mThreadEndTime - threadData.mThreadCurrentTime; + call.mGlobalEndTime = globalTime; + threadData.mGlobalEndTime = globalTime; + threadData.mThreadCurrentTime = threadTime; + } // else, ignore ACTION_INCOMPLETE calls, nothing to do + prevThreadData = threadData; + } + } + + // Finish updating all calls and calculate the total time spent. + for (int i = mCallList.size() - 1; i >= 0; i--) { + Call call = mCallList.get(i); + + // Calculate exclusive real-time by subtracting inclusive real time + // accumulated by children from the total span. + long realTime = call.mGlobalEndTime - call.mGlobalStartTime; + call.mExclusiveRealTime = Math.max(realTime - call.mInclusiveRealTime, 0); + call.mInclusiveRealTime = realTime; + + call.finish(); + } + mTotalCpuTime = 0; + mTotalRealTime = 0; + for (ThreadData threadData : mThreadMap.values()) { + Call rootCall = threadData.getRootCall(); + threadData.updateRootCallTimeBounds(); + rootCall.finish(); + mTotalCpuTime += rootCall.mInclusiveCpuTime; + mTotalRealTime += rootCall.mInclusiveRealTime; + } + + if (mRegression) { + System.out.format("totalCpuTime %dus\n", mTotalCpuTime); + System.out.format("totalRealTime %dus\n", mTotalRealTime); + + dumpThreadTimes(); + dumpCallTimes(); + } + } + + static final int PARSE_VERSION = 0; + static final int PARSE_THREADS = 1; + static final int PARSE_METHODS = 2; + static final int PARSE_OPTIONS = 4; + + long parseKeys() throws IOException { + long offset = 0; + BufferedReader in = null; + try { + in = new BufferedReader(new InputStreamReader( + new FileInputStream(mTraceFileName), "US-ASCII")); + + int mode = PARSE_VERSION; + String line = null; + while (true) { + line = in.readLine(); + if (line == null) { + throw new IOException("Key section does not have an *end marker"); + } + + // Calculate how much we have read from the file so far. The + // extra byte is for the line ending not included by readLine(). + offset += line.length() + 1; + if (line.startsWith("*")) { + if (line.equals("*version")) { + mode = PARSE_VERSION; + continue; + } + if (line.equals("*threads")) { + mode = PARSE_THREADS; + continue; + } + if (line.equals("*methods")) { + mode = PARSE_METHODS; + continue; + } + if (line.equals("*end")) { + break; + } + } + switch (mode) { + case PARSE_VERSION: + mVersionNumber = Integer.decode(line); + mode = PARSE_OPTIONS; + break; + case PARSE_THREADS: + parseThread(line); + break; + case PARSE_METHODS: + parseMethod(line); + break; + case PARSE_OPTIONS: + parseOption(line); + break; + } + } + } catch (FileNotFoundException ex) { + System.err.println(ex.getMessage()); + } finally { + if (in != null) { + in.close(); + } + } + + if (mClockSource == null) { + mClockSource = ClockSource.THREAD_CPU; + } + + return offset; + } + + void parseOption(String line) { + String[] tokens = line.split("="); + if (tokens.length == 2) { + String key = tokens[0]; + String value = tokens[1]; + mPropertiesMap.put(key, value); + + if (key.equals("clock")) { + if (value.equals("thread-cpu")) { + mClockSource = ClockSource.THREAD_CPU; + } else if (value.equals("wall")) { + mClockSource = ClockSource.WALL; + } else if (value.equals("dual")) { + mClockSource = ClockSource.DUAL; + } + } + } + } + + void parseThread(String line) { + String idStr = null; + String name = null; + Matcher matcher = mIdNamePattern.matcher(line); + if (matcher.find()) { + idStr = matcher.group(1); + name = matcher.group(2); + } + if (idStr == null) return; + if (name == null) name = "(unknown)"; + + int id = Integer.decode(idStr); + mThreadMap.put(id, new ThreadData(id, name, mTopLevel)); + } + + void parseMethod(String line) { + String[] tokens = line.split("\t"); + int id = Long.decode(tokens[0]).intValue(); + String className = tokens[1]; + String methodName = null; + String signature = null; + String pathname = null; + int lineNumber = -1; + if (tokens.length == 6) { + methodName = tokens[2]; + signature = tokens[3]; + pathname = tokens[4]; + lineNumber = Integer.decode(tokens[5]); + pathname = constructPathname(className, pathname); + } else if (tokens.length > 2) { + if (tokens[3].startsWith("(")) { + methodName = tokens[2]; + signature = tokens[3]; + } else { + pathname = tokens[2]; + lineNumber = Integer.decode(tokens[3]); + } + } + + mMethodMap.put(id, new MethodData(id, className, methodName, signature, + pathname, lineNumber)); + } + + private String constructPathname(String className, String pathname) { + int index = className.lastIndexOf('/'); + if (index > 0 && index < className.length() - 1 + && pathname.endsWith(".java")) + pathname = className.substring(0, index + 1) + pathname; + return pathname; + } + + private void analyzeData() { + final TimeBase timeBase = getPreferredTimeBase(); + + // Sort the threads into decreasing cpu time + Collection tv = mThreadMap.values(); + mSortedThreads = tv.toArray(new ThreadData[tv.size()]); + Arrays.sort(mSortedThreads, new Comparator() { + @Override + public int compare(ThreadData td1, ThreadData td2) { + if (timeBase.getTime(td2) > timeBase.getTime(td1)) + return 1; + if (timeBase.getTime(td2) < timeBase.getTime(td1)) + return -1; + return td2.getName().compareTo(td1.getName()); + } + }); + + // Sort the methods into decreasing inclusive time + Collection mv = mMethodMap.values(); + MethodData[] methods; + methods = mv.toArray(new MethodData[mv.size()]); + Arrays.sort(methods, new Comparator() { + @Override + public int compare(MethodData md1, MethodData md2) { + if (timeBase.getElapsedInclusiveTime(md2) > timeBase.getElapsedInclusiveTime(md1)) + return 1; + if (timeBase.getElapsedInclusiveTime(md2) < timeBase.getElapsedInclusiveTime(md1)) + return -1; + return md1.getName().compareTo(md2.getName()); + } + }); + + // Count the number of methods with non-zero inclusive time + int nonZero = 0; + for (MethodData md : methods) { + if (timeBase.getElapsedInclusiveTime(md) == 0) + break; + nonZero += 1; + } + + // Copy the methods with non-zero time + mSortedMethods = new MethodData[nonZero]; + int ii = 0; + for (MethodData md : methods) { + if (timeBase.getElapsedInclusiveTime(md) == 0) + break; + md.setRank(ii); + mSortedMethods[ii++] = md; + } + + // Let each method analyze its profile data + for (MethodData md : mSortedMethods) { + md.analyzeData(timeBase); + } + + // Update all the calls to include the method rank in + // their name. + for (Call call : mCallList) { + call.updateName(); + } + + if (mRegression) { + dumpMethodStats(); + } + } + + /* + * This method computes a list of records that describe the the execution + * timeline for each thread. Each record is a pair: (row, block) where: row: + * is the ThreadData object block: is the call (containing the start and end + * times) + */ + @Override + public ArrayList getThreadTimeRecords() { + TimeLineView.Record record; + ArrayList timeRecs; + timeRecs = new ArrayList(); + + // For each thread, push a "toplevel" call that encompasses the + // entire execution of the thread. + for (ThreadData threadData : mSortedThreads) { + if (!threadData.isEmpty() && threadData.getId() != 0) { + record = new TimeLineView.Record(threadData, threadData.getRootCall()); + timeRecs.add(record); + } + } + + for (Call call : mCallList) { + record = new TimeLineView.Record(call.getThreadData(), call); + timeRecs.add(record); + } + + if (mRegression) { + dumpTimeRecs(timeRecs); + System.exit(0); + } + return timeRecs; + } + + private void dumpThreadTimes() { + System.out.print("\nThread Times\n"); + System.out.print("id t-start t-end g-start g-end name\n"); + for (ThreadData threadData : mThreadMap.values()) { + System.out.format("%2d %8d %8d %8d %8d %s\n", + threadData.getId(), + threadData.mThreadStartTime, threadData.mThreadEndTime, + threadData.mGlobalStartTime, threadData.mGlobalEndTime, + threadData.getName()); + } + } + + private void dumpCallTimes() { + System.out.print("\nCall Times\n"); + System.out.print("id t-start t-end g-start g-end excl. incl. method\n"); + for (Call call : mCallList) { + System.out.format("%2d %8d %8d %8d %8d %8d %8d %s\n", + call.getThreadId(), call.mThreadStartTime, call.mThreadEndTime, + call.mGlobalStartTime, call.mGlobalEndTime, + call.mExclusiveCpuTime, call.mInclusiveCpuTime, + call.getMethodData().getName()); + } + } + + private void dumpMethodStats() { + System.out.print("\nMethod Stats\n"); + System.out.print("Excl Cpu Incl Cpu Excl Real Incl Real Calls Method\n"); + for (MethodData md : mSortedMethods) { + System.out.format("%9d %9d %9d %9d %9s %s\n", + md.getElapsedExclusiveCpuTime(), md.getElapsedInclusiveCpuTime(), + md.getElapsedExclusiveRealTime(), md.getElapsedInclusiveRealTime(), + md.getCalls(), md.getProfileName()); + } + } + + private void dumpTimeRecs(ArrayList timeRecs) { + System.out.print("\nTime Records\n"); + System.out.print("id t-start t-end g-start g-end method\n"); + for (TimeLineView.Record record : timeRecs) { + Call call = (Call) record.block; + System.out.format("%2d %8d %8d %8d %8d %s\n", + call.getThreadId(), call.mThreadStartTime, call.mThreadEndTime, + call.mGlobalStartTime, call.mGlobalEndTime, + call.getMethodData().getName()); + } + } + + @Override + public HashMap getThreadLabels() { + HashMap labels = new HashMap(); + for (ThreadData t : mThreadMap.values()) { + labels.put(t.getId(), t.getName()); + } + return labels; + } + + @Override + public MethodData[] getMethods() { + return mSortedMethods; + } + + @Override + public ThreadData[] getThreads() { + return mSortedThreads; + } + + @Override + public long getTotalCpuTime() { + return mTotalCpuTime; + } + + @Override + public long getTotalRealTime() { + return mTotalRealTime; + } + + @Override + public boolean haveCpuTime() { + return mClockSource != ClockSource.WALL; + } + + @Override + public boolean haveRealTime() { + return mClockSource != ClockSource.THREAD_CPU; + } + + @Override + public HashMap getProperties() { + return mPropertiesMap; + } + + @Override + public TimeBase getPreferredTimeBase() { + if (mClockSource == ClockSource.WALL) { + return TimeBase.REAL_TIME; + } + return TimeBase.CPU_TIME; + } + + @Override + public String getClockSource() { + switch (mClockSource) { + case THREAD_CPU: + return "cpu time"; + case WALL: + return "real time"; + case DUAL: + return "real time, dual clock"; + } + return null; + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/MainWindow.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/MainWindow.java new file mode 100644 index 00000000..6a8721fd --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/MainWindow.java @@ -0,0 +1,300 @@ +/* + * Copyright (C) 2006 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.traceview; + +import com.android.sdkstats.SdkStatsService; + +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.MenuManager; +import org.eclipse.jface.window.ApplicationWindow; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.SashForm; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Image; +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.Shell; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.channels.FileChannel; +import java.util.HashMap; +import java.util.Properties; + +public class MainWindow extends ApplicationWindow { + + private final static String PING_NAME = "Traceview"; + + private TraceReader mReader; + private String mTraceName; + + // A global cache of string names. + public static HashMap sStringCache = new HashMap(); + + public MainWindow(String traceName, TraceReader reader) { + super(null); + mReader = reader; + mTraceName = traceName; + + addMenuBar(); + } + + public void run() { + setBlockOnOpen(true); + open(); + } + + @Override + protected void configureShell(Shell shell) { + super.configureShell(shell); + shell.setText("Traceview: " + mTraceName); + + InputStream in = getClass().getClassLoader().getResourceAsStream( + "icons/traceview-128.png"); + if (in != null) { + shell.setImage(new Image(shell.getDisplay(), in)); + } + + shell.setBounds(100, 10, 1282, 900); + } + + @Override + protected Control createContents(Composite parent) { + ColorController.assignMethodColors(parent.getDisplay(), mReader.getMethods()); + SelectionController selectionController = new SelectionController(); + + GridLayout gridLayout = new GridLayout(1, false); + gridLayout.marginWidth = 0; + gridLayout.marginHeight = 0; + gridLayout.horizontalSpacing = 0; + gridLayout.verticalSpacing = 0; + parent.setLayout(gridLayout); + + Display display = parent.getDisplay(); + Color darkGray = display.getSystemColor(SWT.COLOR_DARK_GRAY); + + // Create a sash form to separate the timeline view (on top) + // and the profile view (on bottom) + SashForm sashForm1 = new SashForm(parent, SWT.VERTICAL); + sashForm1.setBackground(darkGray); + sashForm1.SASH_WIDTH = 3; + GridData data = new GridData(GridData.FILL_BOTH); + sashForm1.setLayoutData(data); + + // Create the timeline view + new TimeLineView(sashForm1, mReader, selectionController); + + // Create the profile view + new ProfileView(sashForm1, mReader, selectionController); + return sashForm1; + } + + @Override + protected MenuManager createMenuManager() { + MenuManager manager = super.createMenuManager(); + + MenuManager viewMenu = new MenuManager("View"); + manager.add(viewMenu); + + Action showPropertiesAction = new Action("Show Properties...") { + @Override + public void run() { + showProperties(); + } + }; + viewMenu.add(showPropertiesAction); + + return manager; + } + + private void showProperties() { + PropertiesDialog dialog = new PropertiesDialog(getShell()); + dialog.setProperties(mReader.getProperties()); + dialog.open(); + } + + /** + * Convert the old two-file format into the current concatenated one. + * + * @param base Base path of the two files, i.e. base.key and base.data + * @return Path to a temporary file that will be deleted on exit. + * @throws IOException + */ + private static String makeTempTraceFile(String base) throws IOException { + // Make a temporary file that will go away on exit and prepare to + // write into it. + File temp = File.createTempFile(base, ".trace"); + temp.deleteOnExit(); + + FileOutputStream dstStream = null; + FileInputStream keyStream = null; + FileInputStream dataStream = null; + + try { + dstStream = new FileOutputStream(temp); + FileChannel dstChannel = dstStream.getChannel(); + + // First copy the contents of the key file into our temp file. + keyStream = new FileInputStream(base + ".key"); + FileChannel srcChannel = keyStream.getChannel(); + long size = dstChannel.transferFrom(srcChannel, 0, srcChannel.size()); + srcChannel.close(); + + // Then concatenate the data file. + dataStream = new FileInputStream(base + ".data"); + srcChannel = dataStream.getChannel(); + dstChannel.transferFrom(srcChannel, size, srcChannel.size()); + } finally { + if (dstStream != null) { + dstStream.close(); // also closes dstChannel + } + if (keyStream != null) { + keyStream.close(); // also closes srcChannel + } + if (dataStream != null) { + dataStream.close(); + } + } + + // Return the path of the temp file. + return temp.getPath(); + } + + /** + * Returns the tools revision number. + */ + private static String getRevision() { + Properties p = new Properties(); + try{ + String toolsdir = System.getProperty("com.android.traceview.toolsdir"); //$NON-NLS-1$ + File sourceProp; + if (toolsdir == null || toolsdir.length() == 0) { + sourceProp = new File("source.properties"); //$NON-NLS-1$ + } else { + sourceProp = new File(toolsdir, "source.properties"); //$NON-NLS-1$ + } + + FileInputStream fis = null; + try { + fis = new FileInputStream(sourceProp); + p.load(fis); + } finally { + if (fis != null) { + try { + fis.close(); + } catch (IOException ignore) { + } + } + } + + String revision = p.getProperty("Pkg.Revision"); //$NON-NLS-1$ + if (revision != null && revision.length() > 0) { + return revision; + } + } catch (FileNotFoundException e) { + // couldn't find the file? don't ping. + } catch (IOException e) { + // couldn't find the file? don't ping. + } + + return null; + } + + + public static void main(String[] args) { + TraceReader reader = null; + boolean regression = false; + + // ping the usage server + + String revision = getRevision(); + if (revision != null) { + new SdkStatsService().ping(PING_NAME, revision); + } + + // Process command line arguments + int argc = 0; + int len = args.length; + while (argc < len) { + String arg = args[argc]; + if (arg.charAt(0) != '-') { + break; + } + if (arg.equals("-r")) { + regression = true; + } else { + break; + } + argc++; + } + if (argc != len - 1) { + System.out.printf("Usage: java %s [-r] trace%n", MainWindow.class.getName()); + System.out.printf(" -r regression only%n"); + return; + } + + String traceName = args[len - 1]; + File file = new File(traceName); + if (file.exists() && file.isDirectory()) { + System.out.printf("Qemu trace files not supported yet.\n"); + System.exit(1); + // reader = new QtraceReader(traceName); + } else { + // If the filename as given doesn't exist... + if (!file.exists()) { + // Try appending .trace. + if (new File(traceName + ".trace").exists()) { + traceName = traceName + ".trace"; + // Next, see if it is the old two-file trace. + } else if (new File(traceName + ".data").exists() + && new File(traceName + ".key").exists()) { + try { + traceName = makeTempTraceFile(traceName); + } catch (IOException e) { + System.err.printf("cannot convert old trace file '%s'\n", traceName); + System.exit(1); + } + // Otherwise, give up. + } else { + System.err.printf("trace file '%s' not found\n", traceName); + System.exit(1); + } + } + + try { + reader = new DmTraceReader(traceName, regression); + } catch (IOException e) { + System.err.printf("Failed to read the trace file"); + e.printStackTrace(); + System.exit(1); + return; + } + } + + reader.getTraceUnits().setTimeScale(TraceUnits.TimeScale.MilliSeconds); + + Display.setAppName("Traceview"); + new MainWindow(traceName, reader).run(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/MethodData.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/MethodData.java new file mode 100644 index 00000000..200aa185 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/MethodData.java @@ -0,0 +1,513 @@ +/* + * Copyright (C) 2006 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.traceview; + +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Image; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; + +public class MethodData { + + private int mId; + private int mRank = -1; + private String mClassName; + private String mMethodName; + private String mSignature; + private String mName; + private String mProfileName; + private String mPathname; + private int mLineNumber; + private long mElapsedExclusiveCpuTime; + private long mElapsedInclusiveCpuTime; + private long mTopExclusiveCpuTime; + private long mElapsedExclusiveRealTime; + private long mElapsedInclusiveRealTime; + private long mTopExclusiveRealTime; + private int[] mNumCalls = new int[2]; // index 0=normal, 1=recursive + private Color mColor; + private Color mFadedColor; + private Image mImage; + private Image mFadedImage; + private HashMap mParents; + private HashMap mChildren; + + // The parents of this method when this method was in a recursive call + private HashMap mRecursiveParents; + + // The children of this method when this method was in a recursive call + private HashMap mRecursiveChildren; + + private ProfileNode[] mProfileNodes; + private int mX; + private int mY; + private double mWeight; + + public MethodData(int id, String className) { + mId = id; + mClassName = className; + mMethodName = null; + mSignature = null; + mPathname = null; + mLineNumber = -1; + computeName(); + computeProfileName(); + } + + public MethodData(int id, String className, String methodName, + String signature, String pathname, int lineNumber) { + mId = id; + mClassName = className; + mMethodName = methodName; + mSignature = signature; + mPathname = pathname; + mLineNumber = lineNumber; + computeName(); + computeProfileName(); + } + + public double addWeight(int x, int y, double weight) { + if (mX == x && mY == y) + mWeight += weight; + else { + mX = x; + mY = y; + mWeight = weight; + } + return mWeight; + } + + public void clearWeight() { + mWeight = 0; + } + + public int getRank() { + return mRank; + } + + public void setRank(int rank) { + mRank = rank; + computeProfileName(); + } + + public void addElapsedExclusive(long cpuTime, long realTime) { + mElapsedExclusiveCpuTime += cpuTime; + mElapsedExclusiveRealTime += realTime; + } + + public void addElapsedInclusive(long cpuTime, long realTime, + boolean isRecursive, Call parent) { + if (isRecursive == false) { + mElapsedInclusiveCpuTime += cpuTime; + mElapsedInclusiveRealTime += realTime; + mNumCalls[0] += 1; + } else { + mNumCalls[1] += 1; + } + + if (parent == null) + return; + + // Find the child method in the parent + MethodData parentMethod = parent.getMethodData(); + if (parent.isRecursive()) { + parentMethod.mRecursiveChildren = updateInclusive(cpuTime, realTime, + parentMethod, this, false, + parentMethod.mRecursiveChildren); + } else { + parentMethod.mChildren = updateInclusive(cpuTime, realTime, + parentMethod, this, false, parentMethod.mChildren); + } + + // Find the parent method in the child + if (isRecursive) { + mRecursiveParents = updateInclusive(cpuTime, realTime, this, parentMethod, true, + mRecursiveParents); + } else { + mParents = updateInclusive(cpuTime, realTime, this, parentMethod, true, + mParents); + } + } + + private HashMap updateInclusive(long cpuTime, long realTime, + MethodData contextMethod, MethodData elementMethod, + boolean elementIsParent, HashMap map) { + if (map == null) { + map = new HashMap(4); + } else { + ProfileData profileData = map.get(elementMethod.mId); + if (profileData != null) { + profileData.addElapsedInclusive(cpuTime, realTime); + return map; + } + } + + ProfileData elementData = new ProfileData(contextMethod, + elementMethod, elementIsParent); + elementData.setElapsedInclusive(cpuTime, realTime); + elementData.setNumCalls(1); + map.put(elementMethod.mId, elementData); + return map; + } + + public void analyzeData(TimeBase timeBase) { + // Sort the parents and children into decreasing inclusive time + ProfileData[] sortedParents; + ProfileData[] sortedChildren; + ProfileData[] sortedRecursiveParents; + ProfileData[] sortedRecursiveChildren; + + sortedParents = sortProfileData(mParents, timeBase); + sortedChildren = sortProfileData(mChildren, timeBase); + sortedRecursiveParents = sortProfileData(mRecursiveParents, timeBase); + sortedRecursiveChildren = sortProfileData(mRecursiveChildren, timeBase); + + // Add "self" time to the top of the sorted children + sortedChildren = addSelf(sortedChildren); + + // Create the ProfileNode objects that we need + ArrayList nodes = new ArrayList(); + ProfileNode profileNode; + if (mParents != null) { + profileNode = new ProfileNode("Parents", this, sortedParents, + true, false); + nodes.add(profileNode); + } + if (mChildren != null) { + profileNode = new ProfileNode("Children", this, sortedChildren, + false, false); + nodes.add(profileNode); + } + if (mRecursiveParents!= null) { + profileNode = new ProfileNode("Parents while recursive", this, + sortedRecursiveParents, true, true); + nodes.add(profileNode); + } + if (mRecursiveChildren != null) { + profileNode = new ProfileNode("Children while recursive", this, + sortedRecursiveChildren, false, true); + nodes.add(profileNode); + } + mProfileNodes = nodes.toArray(new ProfileNode[nodes.size()]); + } + + // Create and return a ProfileData[] array that is a sorted copy + // of the given HashMap values. + private ProfileData[] sortProfileData(HashMap map, + final TimeBase timeBase) { + if (map == null) + return null; + + // Convert the hash values to an array of ProfileData + Collection values = map.values(); + ProfileData[] sorted = values.toArray(new ProfileData[values.size()]); + + // Sort the array by elapsed inclusive time + Arrays.sort(sorted, new Comparator() { + @Override + public int compare(ProfileData pd1, ProfileData pd2) { + if (timeBase.getElapsedInclusiveTime(pd2) > timeBase.getElapsedInclusiveTime(pd1)) + return 1; + if (timeBase.getElapsedInclusiveTime(pd2) < timeBase.getElapsedInclusiveTime(pd1)) + return -1; + return 0; + } + }); + return sorted; + } + + private ProfileData[] addSelf(ProfileData[] children) { + ProfileData[] pdata; + if (children == null) { + pdata = new ProfileData[1]; + } else { + pdata = new ProfileData[children.length + 1]; + System.arraycopy(children, 0, pdata, 1, children.length); + } + pdata[0] = new ProfileSelf(this); + return pdata; + } + + public void addTopExclusive(long cpuTime, long realTime) { + mTopExclusiveCpuTime += cpuTime; + mTopExclusiveRealTime += realTime; + } + + public long getTopExclusiveCpuTime() { + return mTopExclusiveCpuTime; + } + + public long getTopExclusiveRealTime() { + return mTopExclusiveRealTime; + } + + public int getId() { + return mId; + } + + private void computeName() { + if (mMethodName == null) { + mName = mClassName; + return; + } + + StringBuilder sb = new StringBuilder(); + sb.append(mClassName); + sb.append("."); //$NON-NLS-1$ + sb.append(mMethodName); + sb.append(" "); //$NON-NLS-1$ + sb.append(mSignature); + mName = sb.toString(); + } + + public String getName() { + return mName; + } + + public String getClassName() { + return mClassName; + } + + public String getMethodName() { + return mMethodName; + } + + public String getProfileName() { + return mProfileName; + } + + public String getSignature() { + return mSignature; + } + + public void computeProfileName() { + if (mRank == -1) { + mProfileName = mName; + return; + } + + StringBuilder sb = new StringBuilder(); + sb.append(mRank); + sb.append(" "); //$NON-NLS-1$ + sb.append(getName()); + mProfileName = sb.toString(); + } + + public String getCalls() { + return String.format("%d+%d", mNumCalls[0], mNumCalls[1]); + } + + public int getTotalCalls() { + return mNumCalls[0] + mNumCalls[1]; + } + + public Color getColor() { + return mColor; + } + + public void setColor(Color color) { + mColor = color; + } + + public void setImage(Image image) { + mImage = image; + } + + public Image getImage() { + return mImage; + } + + @Override + public String toString() { + return getName(); + } + + public long getElapsedExclusiveCpuTime() { + return mElapsedExclusiveCpuTime; + } + + public long getElapsedExclusiveRealTime() { + return mElapsedExclusiveRealTime; + } + + public long getElapsedInclusiveCpuTime() { + return mElapsedInclusiveCpuTime; + } + + public long getElapsedInclusiveRealTime() { + return mElapsedInclusiveRealTime; + } + + public void setFadedColor(Color fadedColor) { + mFadedColor = fadedColor; + } + + public Color getFadedColor() { + return mFadedColor; + } + + public void setFadedImage(Image fadedImage) { + mFadedImage = fadedImage; + } + + public Image getFadedImage() { + return mFadedImage; + } + + public void setPathname(String pathname) { + mPathname = pathname; + } + + public String getPathname() { + return mPathname; + } + + public void setLineNumber(int lineNumber) { + mLineNumber = lineNumber; + } + + public int getLineNumber() { + return mLineNumber; + } + + public ProfileNode[] getProfileNodes() { + return mProfileNodes; + } + + public static class Sorter implements Comparator { + @Override + public int compare(MethodData md1, MethodData md2) { + if (mColumn == Column.BY_NAME) { + int result = md1.getName().compareTo(md2.getName()); + return (mDirection == Direction.INCREASING) ? result : -result; + } + if (mColumn == Column.BY_INCLUSIVE_CPU_TIME) { + if (md2.getElapsedInclusiveCpuTime() > md1.getElapsedInclusiveCpuTime()) + return (mDirection == Direction.INCREASING) ? -1 : 1; + if (md2.getElapsedInclusiveCpuTime() < md1.getElapsedInclusiveCpuTime()) + return (mDirection == Direction.INCREASING) ? 1 : -1; + return md1.getName().compareTo(md2.getName()); + } + if (mColumn == Column.BY_EXCLUSIVE_CPU_TIME) { + if (md2.getElapsedExclusiveCpuTime() > md1.getElapsedExclusiveCpuTime()) + return (mDirection == Direction.INCREASING) ? -1 : 1; + if (md2.getElapsedExclusiveCpuTime() < md1.getElapsedExclusiveCpuTime()) + return (mDirection == Direction.INCREASING) ? 1 : -1; + return md1.getName().compareTo(md2.getName()); + } + if (mColumn == Column.BY_INCLUSIVE_REAL_TIME) { + if (md2.getElapsedInclusiveRealTime() > md1.getElapsedInclusiveRealTime()) + return (mDirection == Direction.INCREASING) ? -1 : 1; + if (md2.getElapsedInclusiveRealTime() < md1.getElapsedInclusiveRealTime()) + return (mDirection == Direction.INCREASING) ? 1 : -1; + return md1.getName().compareTo(md2.getName()); + } + if (mColumn == Column.BY_EXCLUSIVE_REAL_TIME) { + if (md2.getElapsedExclusiveRealTime() > md1.getElapsedExclusiveRealTime()) + return (mDirection == Direction.INCREASING) ? -1 : 1; + if (md2.getElapsedExclusiveRealTime() < md1.getElapsedExclusiveRealTime()) + return (mDirection == Direction.INCREASING) ? 1 : -1; + return md1.getName().compareTo(md2.getName()); + } + if (mColumn == Column.BY_CALLS) { + int result = md1.getTotalCalls() - md2.getTotalCalls(); + if (result == 0) + return md1.getName().compareTo(md2.getName()); + return (mDirection == Direction.INCREASING) ? result : -result; + } + if (mColumn == Column.BY_CPU_TIME_PER_CALL) { + double time1 = md1.getElapsedInclusiveCpuTime(); + time1 = time1 / md1.getTotalCalls(); + double time2 = md2.getElapsedInclusiveCpuTime(); + time2 = time2 / md2.getTotalCalls(); + double diff = time1 - time2; + int result = 0; + if (diff < 0) + result = -1; + else if (diff > 0) + result = 1; + if (result == 0) + return md1.getName().compareTo(md2.getName()); + return (mDirection == Direction.INCREASING) ? result : -result; + } + if (mColumn == Column.BY_REAL_TIME_PER_CALL) { + double time1 = md1.getElapsedInclusiveRealTime(); + time1 = time1 / md1.getTotalCalls(); + double time2 = md2.getElapsedInclusiveRealTime(); + time2 = time2 / md2.getTotalCalls(); + double diff = time1 - time2; + int result = 0; + if (diff < 0) + result = -1; + else if (diff > 0) + result = 1; + if (result == 0) + return md1.getName().compareTo(md2.getName()); + return (mDirection == Direction.INCREASING) ? result : -result; + } + return 0; + } + + public void setColumn(Column column) { + // If the sort column specified is the same as last time, + // then reverse the sort order. + if (mColumn == column) { + // Reverse the sort order + if (mDirection == Direction.INCREASING) + mDirection = Direction.DECREASING; + else + mDirection = Direction.INCREASING; + } else { + // Sort names into increasing order, data into decreasing order. + if (column == Column.BY_NAME) + mDirection = Direction.INCREASING; + else + mDirection = Direction.DECREASING; + } + mColumn = column; + } + + public Column getColumn() { + return mColumn; + } + + public void setDirection(Direction direction) { + mDirection = direction; + } + + public Direction getDirection() { + return mDirection; + } + + public static enum Column { + BY_NAME, BY_EXCLUSIVE_CPU_TIME, BY_EXCLUSIVE_REAL_TIME, + BY_INCLUSIVE_CPU_TIME, BY_INCLUSIVE_REAL_TIME, BY_CALLS, + BY_REAL_TIME_PER_CALL, BY_CPU_TIME_PER_CALL, + }; + + public static enum Direction { + INCREASING, DECREASING + }; + + private Column mColumn; + private Direction mDirection; + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileData.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileData.java new file mode 100644 index 00000000..dc7e7876 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileData.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2006 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.traceview; + + +public class ProfileData { + + protected MethodData mElement; + + /** mContext is either the parent or child of mElement */ + protected MethodData mContext; + protected boolean mElementIsParent; + protected long mElapsedInclusiveCpuTime; + protected long mElapsedInclusiveRealTime; + protected int mNumCalls; + + public ProfileData() { + } + + public ProfileData(MethodData context, MethodData element, + boolean elementIsParent) { + mContext = context; + mElement = element; + mElementIsParent = elementIsParent; + } + + public String getProfileName() { + return mElement.getProfileName(); + } + + public MethodData getMethodData() { + return mElement; + } + + public void addElapsedInclusive(long cpuTime, long realTime) { + mElapsedInclusiveCpuTime += cpuTime; + mElapsedInclusiveRealTime += realTime; + mNumCalls += 1; + } + + public void setElapsedInclusive(long cpuTime, long realTime) { + mElapsedInclusiveCpuTime = cpuTime; + mElapsedInclusiveRealTime = realTime; + } + + public long getElapsedInclusiveCpuTime() { + return mElapsedInclusiveCpuTime; + } + + public long getElapsedInclusiveRealTime() { + return mElapsedInclusiveRealTime; + } + + public void setNumCalls(int numCalls) { + mNumCalls = numCalls; + } + + public String getNumCalls() { + int totalCalls; + if (mElementIsParent) + totalCalls = mContext.getTotalCalls(); + else + totalCalls = mElement.getTotalCalls(); + return String.format("%d/%d", mNumCalls, totalCalls); + } + + public boolean isParent() { + return mElementIsParent; + } + + public MethodData getContext() { + return mContext; + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileNode.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileNode.java new file mode 100644 index 00000000..de61702a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileNode.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2006 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.traceview; + +public class ProfileNode { + + private String mLabel; + private MethodData mMethodData; + private ProfileData[] mChildren; + private boolean mIsParent; + private boolean mIsRecursive; + + public ProfileNode(String label, MethodData methodData, + ProfileData[] children, boolean isParent, boolean isRecursive) { + mLabel = label; + mMethodData = methodData; + mChildren = children; + mIsParent = isParent; + mIsRecursive = isRecursive; + } + + public String getLabel() { + return mLabel; + } + + public ProfileData[] getChildren() { + return mChildren; + } + + public boolean isParent() { + return mIsParent; + } + + public boolean isRecursive() { + return mIsRecursive; + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileProvider.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileProvider.java new file mode 100644 index 00000000..8b9ebc8d --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileProvider.java @@ -0,0 +1,475 @@ +/* + * Copyright (C) 2006 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.traceview; + +import org.eclipse.jface.viewers.IColorProvider; +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.LabelProvider; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.swt.SWT; +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.widgets.Display; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeColumn; +import org.eclipse.swt.widgets.TreeItem; + +import java.io.InputStream; +import java.util.Arrays; + +class ProfileProvider implements ITreeContentProvider { + + private MethodData[] mRoots; + private SelectionAdapter mListener; + private TreeViewer mTreeViewer; + private TraceReader mReader; + private Image mSortUp; + private Image mSortDown; + private String mColumnNames[] = { "Name", + "Incl Cpu Time %", "Incl Cpu Time", "Excl Cpu Time %", "Excl Cpu Time", + "Incl Real Time %", "Incl Real Time", "Excl Real Time %", "Excl Real Time", + "Calls+Recur\nCalls/Total", "Cpu Time/Call", "Real Time/Call" }; + private int mColumnWidths[] = { 370, + 100, 100, 100, 100, + 100, 100, 100, 100, + 100, 100, 100 }; + private int mColumnAlignments[] = { SWT.LEFT, + SWT.RIGHT, SWT.RIGHT, SWT.RIGHT, SWT.RIGHT, + SWT.RIGHT, SWT.RIGHT, SWT.RIGHT, SWT.RIGHT, + SWT.CENTER, SWT.RIGHT, SWT.RIGHT }; + private static final int COL_NAME = 0; + private static final int COL_INCLUSIVE_CPU_TIME_PER = 1; + private static final int COL_INCLUSIVE_CPU_TIME = 2; + private static final int COL_EXCLUSIVE_CPU_TIME_PER = 3; + private static final int COL_EXCLUSIVE_CPU_TIME = 4; + private static final int COL_INCLUSIVE_REAL_TIME_PER = 5; + private static final int COL_INCLUSIVE_REAL_TIME = 6; + private static final int COL_EXCLUSIVE_REAL_TIME_PER = 7; + private static final int COL_EXCLUSIVE_REAL_TIME = 8; + private static final int COL_CALLS = 9; + private static final int COL_CPU_TIME_PER_CALL = 10; + private static final int COL_REAL_TIME_PER_CALL = 11; + private long mTotalCpuTime; + private long mTotalRealTime; + private int mPrevMatchIndex = -1; + + public ProfileProvider(TraceReader reader) { + mRoots = reader.getMethods(); + mReader = reader; + mTotalCpuTime = reader.getTotalCpuTime(); + mTotalRealTime = reader.getTotalRealTime(); + Display display = Display.getCurrent(); + InputStream in = getClass().getClassLoader().getResourceAsStream( + "icons/sort_up.png"); + mSortUp = new Image(display, in); + in = getClass().getClassLoader().getResourceAsStream( + "icons/sort_down.png"); + mSortDown = new Image(display, in); + } + + private MethodData doMatchName(String name, int startIndex) { + // Check if the given "name" has any uppercase letters + boolean hasUpper = hasUpperCaseCharacter(name); + for (int ii = startIndex; ii < mRoots.length; ++ii) { + MethodData md = mRoots[ii]; + String fullName = md.getName(); + // If there were no upper case letters in the given name, + // then ignore case when matching. + if (!hasUpper) + fullName = fullName.toLowerCase(); + if (fullName.indexOf(name) != -1) { + mPrevMatchIndex = ii; + return md; + } + } + mPrevMatchIndex = -1; + return null; + } + + public MethodData findMatchingName(String name) { + return doMatchName(name, 0); + } + + public MethodData findNextMatchingName(String name) { + return doMatchName(name, mPrevMatchIndex + 1); + } + + public MethodData findMatchingTreeItem(TreeItem item) { + if (item == null) + return null; + String text = item.getText(); + if (Character.isDigit(text.charAt(0)) == false) + return null; + int spaceIndex = text.indexOf(' '); + String numstr = text.substring(0, spaceIndex); + int rank = Integer.valueOf(numstr); + for (MethodData md : mRoots) { + if (md.getRank() == rank) + return md; + } + return null; + } + + public void setTreeViewer(TreeViewer treeViewer) { + mTreeViewer = treeViewer; + } + + public String[] getColumnNames() { + return mColumnNames; + } + + public int[] getColumnWidths() { + int[] widths = Arrays.copyOf(mColumnWidths, mColumnWidths.length); + if (!mReader.haveCpuTime()) { + widths[COL_EXCLUSIVE_CPU_TIME] = 0; + widths[COL_EXCLUSIVE_CPU_TIME_PER] = 0; + widths[COL_INCLUSIVE_CPU_TIME] = 0; + widths[COL_INCLUSIVE_CPU_TIME_PER] = 0; + widths[COL_CPU_TIME_PER_CALL] = 0; + } + if (!mReader.haveRealTime()) { + widths[COL_EXCLUSIVE_REAL_TIME] = 0; + widths[COL_EXCLUSIVE_REAL_TIME_PER] = 0; + widths[COL_INCLUSIVE_REAL_TIME] = 0; + widths[COL_INCLUSIVE_REAL_TIME_PER] = 0; + widths[COL_REAL_TIME_PER_CALL] = 0; + } + return widths; + } + + public int[] getColumnAlignments() { + return mColumnAlignments; + } + + @Override + public Object[] getChildren(Object element) { + if (element instanceof MethodData) { + MethodData md = (MethodData) element; + return md.getProfileNodes(); + } + if (element instanceof ProfileNode) { + ProfileNode pn = (ProfileNode) element; + return pn.getChildren(); + } + return new Object[0]; + } + + @Override + public Object getParent(Object element) { + return null; + } + + @Override + public boolean hasChildren(Object element) { + if (element instanceof MethodData) + return true; + if (element instanceof ProfileNode) + return true; + return false; + } + + @Override + public Object[] getElements(Object element) { + return mRoots; + } + + @Override + public void dispose() { + } + + @Override + public void inputChanged(Viewer arg0, Object arg1, Object arg2) { + } + + public Object getRoot() { + return "root"; + } + + public SelectionAdapter getColumnListener() { + if (mListener == null) + mListener = new ColumnListener(); + return mListener; + } + + public LabelProvider getLabelProvider() { + return new ProfileLabelProvider(); + } + + class ProfileLabelProvider extends LabelProvider implements + ITableLabelProvider, IColorProvider { + Color colorRed; + Color colorParentsBack; + Color colorChildrenBack; + TraceUnits traceUnits; + + public ProfileLabelProvider() { + Display display = Display.getCurrent(); + colorRed = display.getSystemColor(SWT.COLOR_RED); + colorParentsBack = new Color(display, 230, 230, 255); // blue + colorChildrenBack = new Color(display, 255, 255, 210); // yellow + traceUnits = mReader.getTraceUnits(); + } + + @Override + public String getColumnText(Object element, int col) { + if (element instanceof MethodData) { + MethodData md = (MethodData) element; + if (col == COL_NAME) + return md.getProfileName(); + if (col == COL_EXCLUSIVE_CPU_TIME) { + double val = md.getElapsedExclusiveCpuTime(); + val = traceUnits.getScaledValue(val); + return String.format("%.3f", val); + } + if (col == COL_EXCLUSIVE_CPU_TIME_PER) { + double val = md.getElapsedExclusiveCpuTime(); + double per = val * 100.0 / mTotalCpuTime; + return String.format("%.1f%%", per); + } + if (col == COL_INCLUSIVE_CPU_TIME) { + double val = md.getElapsedInclusiveCpuTime(); + val = traceUnits.getScaledValue(val); + return String.format("%.3f", val); + } + if (col == COL_INCLUSIVE_CPU_TIME_PER) { + double val = md.getElapsedInclusiveCpuTime(); + double per = val * 100.0 / mTotalCpuTime; + return String.format("%.1f%%", per); + } + if (col == COL_EXCLUSIVE_REAL_TIME) { + double val = md.getElapsedExclusiveRealTime(); + val = traceUnits.getScaledValue(val); + return String.format("%.3f", val); + } + if (col == COL_EXCLUSIVE_REAL_TIME_PER) { + double val = md.getElapsedExclusiveRealTime(); + double per = val * 100.0 / mTotalRealTime; + return String.format("%.1f%%", per); + } + if (col == COL_INCLUSIVE_REAL_TIME) { + double val = md.getElapsedInclusiveRealTime(); + val = traceUnits.getScaledValue(val); + return String.format("%.3f", val); + } + if (col == COL_INCLUSIVE_REAL_TIME_PER) { + double val = md.getElapsedInclusiveRealTime(); + double per = val * 100.0 / mTotalRealTime; + return String.format("%.1f%%", per); + } + if (col == COL_CALLS) + return md.getCalls(); + if (col == COL_CPU_TIME_PER_CALL) { + int numCalls = md.getTotalCalls(); + double val = md.getElapsedInclusiveCpuTime(); + val = val / numCalls; + val = traceUnits.getScaledValue(val); + return String.format("%.3f", val); + } + if (col == COL_REAL_TIME_PER_CALL) { + int numCalls = md.getTotalCalls(); + double val = md.getElapsedInclusiveRealTime(); + val = val / numCalls; + val = traceUnits.getScaledValue(val); + return String.format("%.3f", val); + } + } else if (element instanceof ProfileSelf) { + ProfileSelf ps = (ProfileSelf) element; + if (col == COL_NAME) + return ps.getProfileName(); + if (col == COL_INCLUSIVE_CPU_TIME) { + double val = ps.getElapsedInclusiveCpuTime(); + val = traceUnits.getScaledValue(val); + return String.format("%.3f", val); + } + if (col == COL_INCLUSIVE_CPU_TIME_PER) { + double total; + double val = ps.getElapsedInclusiveCpuTime(); + MethodData context = ps.getContext(); + total = context.getElapsedInclusiveCpuTime(); + double per = val * 100.0 / total; + return String.format("%.1f%%", per); + } + if (col == COL_INCLUSIVE_REAL_TIME) { + double val = ps.getElapsedInclusiveRealTime(); + val = traceUnits.getScaledValue(val); + return String.format("%.3f", val); + } + if (col == COL_INCLUSIVE_REAL_TIME_PER) { + double total; + double val = ps.getElapsedInclusiveRealTime(); + MethodData context = ps.getContext(); + total = context.getElapsedInclusiveRealTime(); + double per = val * 100.0 / total; + return String.format("%.1f%%", per); + } + return ""; + } else if (element instanceof ProfileData) { + ProfileData pd = (ProfileData) element; + if (col == COL_NAME) + return pd.getProfileName(); + if (col == COL_INCLUSIVE_CPU_TIME) { + double val = pd.getElapsedInclusiveCpuTime(); + val = traceUnits.getScaledValue(val); + return String.format("%.3f", val); + } + if (col == COL_INCLUSIVE_CPU_TIME_PER) { + double total; + double val = pd.getElapsedInclusiveCpuTime(); + MethodData context = pd.getContext(); + total = context.getElapsedInclusiveCpuTime(); + double per = val * 100.0 / total; + return String.format("%.1f%%", per); + } + if (col == COL_INCLUSIVE_REAL_TIME) { + double val = pd.getElapsedInclusiveRealTime(); + val = traceUnits.getScaledValue(val); + return String.format("%.3f", val); + } + if (col == COL_INCLUSIVE_REAL_TIME_PER) { + double total; + double val = pd.getElapsedInclusiveRealTime(); + MethodData context = pd.getContext(); + total = context.getElapsedInclusiveRealTime(); + double per = val * 100.0 / total; + return String.format("%.1f%%", per); + } + if (col == COL_CALLS) + return pd.getNumCalls(); + return ""; + } else if (element instanceof ProfileNode) { + ProfileNode pn = (ProfileNode) element; + if (col == COL_NAME) + return pn.getLabel(); + return ""; + } + return "col" + col; + } + + @Override + public Image getColumnImage(Object element, int col) { + if (col != COL_NAME) + return null; + if (element instanceof MethodData) { + MethodData md = (MethodData) element; + return md.getImage(); + } + if (element instanceof ProfileData) { + ProfileData pd = (ProfileData) element; + MethodData md = pd.getMethodData(); + return md.getImage(); + } + return null; + } + + @Override + public Color getForeground(Object element) { + return null; + } + + @Override + public Color getBackground(Object element) { + if (element instanceof ProfileData) { + ProfileData pd = (ProfileData) element; + if (pd.isParent()) + return colorParentsBack; + return colorChildrenBack; + } + if (element instanceof ProfileNode) { + ProfileNode pn = (ProfileNode) element; + if (pn.isParent()) + return colorParentsBack; + return colorChildrenBack; + } + return null; + } + } + + class ColumnListener extends SelectionAdapter { + MethodData.Sorter sorter = new MethodData.Sorter(); + + @Override + public void widgetSelected(SelectionEvent event) { + TreeColumn column = (TreeColumn) event.widget; + String name = column.getText(); + Tree tree = column.getParent(); + tree.setRedraw(false); + TreeColumn[] columns = tree.getColumns(); + for (TreeColumn col : columns) { + col.setImage(null); + } + if (name == mColumnNames[COL_NAME]) { + // Sort names alphabetically + sorter.setColumn(MethodData.Sorter.Column.BY_NAME); + Arrays.sort(mRoots, sorter); + } else if (name == mColumnNames[COL_EXCLUSIVE_CPU_TIME]) { + sorter.setColumn(MethodData.Sorter.Column.BY_EXCLUSIVE_CPU_TIME); + Arrays.sort(mRoots, sorter); + } else if (name == mColumnNames[COL_EXCLUSIVE_CPU_TIME_PER]) { + sorter.setColumn(MethodData.Sorter.Column.BY_EXCLUSIVE_CPU_TIME); + Arrays.sort(mRoots, sorter); + } else if (name == mColumnNames[COL_INCLUSIVE_CPU_TIME]) { + sorter.setColumn(MethodData.Sorter.Column.BY_INCLUSIVE_CPU_TIME); + Arrays.sort(mRoots, sorter); + } else if (name == mColumnNames[COL_INCLUSIVE_CPU_TIME_PER]) { + sorter.setColumn(MethodData.Sorter.Column.BY_INCLUSIVE_CPU_TIME); + Arrays.sort(mRoots, sorter); + } else if (name == mColumnNames[COL_EXCLUSIVE_REAL_TIME]) { + sorter.setColumn(MethodData.Sorter.Column.BY_EXCLUSIVE_REAL_TIME); + Arrays.sort(mRoots, sorter); + } else if (name == mColumnNames[COL_EXCLUSIVE_REAL_TIME_PER]) { + sorter.setColumn(MethodData.Sorter.Column.BY_EXCLUSIVE_REAL_TIME); + Arrays.sort(mRoots, sorter); + } else if (name == mColumnNames[COL_INCLUSIVE_REAL_TIME]) { + sorter.setColumn(MethodData.Sorter.Column.BY_INCLUSIVE_REAL_TIME); + Arrays.sort(mRoots, sorter); + } else if (name == mColumnNames[COL_INCLUSIVE_REAL_TIME_PER]) { + sorter.setColumn(MethodData.Sorter.Column.BY_INCLUSIVE_REAL_TIME); + Arrays.sort(mRoots, sorter); + } else if (name == mColumnNames[COL_CALLS]) { + sorter.setColumn(MethodData.Sorter.Column.BY_CALLS); + Arrays.sort(mRoots, sorter); + } else if (name == mColumnNames[COL_CPU_TIME_PER_CALL]) { + sorter.setColumn(MethodData.Sorter.Column.BY_CPU_TIME_PER_CALL); + Arrays.sort(mRoots, sorter); + } else if (name == mColumnNames[COL_REAL_TIME_PER_CALL]) { + sorter.setColumn(MethodData.Sorter.Column.BY_REAL_TIME_PER_CALL); + Arrays.sort(mRoots, sorter); + } + MethodData.Sorter.Direction direction = sorter.getDirection(); + if (direction == MethodData.Sorter.Direction.INCREASING) + column.setImage(mSortDown); + else + column.setImage(mSortUp); + tree.setRedraw(true); + mTreeViewer.refresh(); + } + } + + public static boolean hasUpperCaseCharacter(String s) + { + for (int i = 0; i < s.length(); i++) { + if (Character.isUpperCase(s.charAt(i))) { + return true; + } + } + return false; + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileSelf.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileSelf.java new file mode 100644 index 00000000..f98b6b2f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileSelf.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2006 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.traceview; + +public class ProfileSelf extends ProfileData { + public ProfileSelf(MethodData methodData) { + mElement = methodData; + mContext = methodData; + } + + @Override + public String getProfileName() { + return "self"; + } + + @Override + public long getElapsedInclusiveCpuTime() { + return mElement.getTopExclusiveCpuTime(); + } + + @Override + public long getElapsedInclusiveRealTime() { + return mElement.getTopExclusiveRealTime(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileView.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileView.java new file mode 100644 index 00000000..60eb5cbd --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ProfileView.java @@ -0,0 +1,332 @@ +/* + * Copyright (C) 2006 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.traceview; + +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.ITreeViewerListener; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.jface.viewers.TreeExpansionEvent; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.KeyAdapter; +import org.eclipse.swt.events.KeyEvent; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.graphics.Color; +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.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Text; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeColumn; +import org.eclipse.swt.widgets.TreeItem; + +import java.util.ArrayList; +import java.util.Observable; +import java.util.Observer; + +public class ProfileView extends Composite implements Observer { + + private TreeViewer mTreeViewer; + private Text mSearchBox; + private SelectionController mSelectionController; + private ProfileProvider mProfileProvider; + private Color mColorNoMatch; + private Color mColorMatch; + private MethodData mCurrentHighlightedMethod; + private MethodHandler mMethodHandler; + + public interface MethodHandler { + void handleMethod(MethodData method); + } + + public ProfileView(Composite parent, TraceReader reader, + SelectionController selectController) { + super(parent, SWT.NONE); + setLayout(new GridLayout(1, false)); + this.mSelectionController = selectController; + mSelectionController.addObserver(this); + + // Add a tree viewer at the top + mTreeViewer = new TreeViewer(this, SWT.MULTI | SWT.NONE); + mTreeViewer.setUseHashlookup(true); + mProfileProvider = reader.getProfileProvider(); + mProfileProvider.setTreeViewer(mTreeViewer); + SelectionAdapter listener = mProfileProvider.getColumnListener(); + final Tree tree = mTreeViewer.getTree(); + tree.setHeaderVisible(true); + tree.setLayoutData(new GridData(GridData.FILL_BOTH)); + + // Get the column names from the ProfileProvider + String[] columnNames = mProfileProvider.getColumnNames(); + int[] columnWidths = mProfileProvider.getColumnWidths(); + int[] columnAlignments = mProfileProvider.getColumnAlignments(); + for (int ii = 0; ii < columnWidths.length; ++ii) { + TreeColumn column = new TreeColumn(tree, SWT.LEFT); + column.setText(columnNames[ii]); + column.setWidth(columnWidths[ii]); + column.setMoveable(true); + column.addSelectionListener(listener); + column.setAlignment(columnAlignments[ii]); + } + + // Add a listener to the tree so that we can make the row + // height smaller. + tree.addListener(SWT.MeasureItem, new Listener() { + @Override + public void handleEvent(Event event) { + int fontHeight = event.gc.getFontMetrics().getHeight(); + event.height = fontHeight; + } + }); + + mTreeViewer.setContentProvider(mProfileProvider); + mTreeViewer.setLabelProvider(mProfileProvider.getLabelProvider()); + mTreeViewer.setInput(mProfileProvider.getRoot()); + + // Create another composite to hold the label and text box + Composite composite = new Composite(this, SWT.NONE); + composite.setLayout(new GridLayout(2, false)); + composite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + // Add a label for the search box + Label label = new Label(composite, SWT.NONE); + label.setText("Find:"); + + // Add a text box for searching for method names + mSearchBox = new Text(composite, SWT.BORDER); + mSearchBox.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + Display display = getDisplay(); + mColorNoMatch = new Color(display, 255, 200, 200); + mColorMatch = mSearchBox.getBackground(); + + mSearchBox.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent ev) { + String query = mSearchBox.getText(); + if (query.length() == 0) + return; + findName(query); + } + }); + + // Add a key listener to the text box so that we can clear + // the text box if the user presses . + mSearchBox.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent event) { + if (event.keyCode == SWT.ESC) { + mSearchBox.setText(""); + } else if (event.keyCode == SWT.CR) { + String query = mSearchBox.getText(); + if (query.length() == 0) + return; + findNextName(query); + } + } + }); + + // Also add a key listener to the tree viewer so that the + // user can just start typing anywhere in the tree view. + tree.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent event) { + if (event.keyCode == SWT.ESC) { + mSearchBox.setText(""); + } else if (event.keyCode == SWT.BS) { + // Erase the last character from the search box + String text = mSearchBox.getText(); + int len = text.length(); + String chopped; + if (len <= 1) + chopped = ""; + else + chopped = text.substring(0, len - 1); + mSearchBox.setText(chopped); + } else if (event.keyCode == SWT.CR) { + String query = mSearchBox.getText(); + if (query.length() == 0) + return; + findNextName(query); + } else { + // Append the given character to the search box + String str = String.valueOf(event.character); + mSearchBox.append(str); + } + event.doit = false; + } + }); + + // Add a selection listener to the tree so that the user can click + // on a method that is a child or parent and jump to that method. + mTreeViewer + .addSelectionChangedListener(new ISelectionChangedListener() { + @Override + public void selectionChanged(SelectionChangedEvent ev) { + ISelection sel = ev.getSelection(); + if (sel.isEmpty()) + return; + if (sel instanceof IStructuredSelection) { + IStructuredSelection selection = (IStructuredSelection) sel; + Object element = selection.getFirstElement(); + if (element == null) + return; + if (element instanceof MethodData) { + MethodData md = (MethodData) element; + highlightMethod(md, true); + } + if (element instanceof ProfileData) { + MethodData md = ((ProfileData) element) + .getMethodData(); + highlightMethod(md, true); + } + } + } + }); + + // Add a tree listener so that we can expand the parents and children + // of a method when a method is expanded. + mTreeViewer.addTreeListener(new ITreeViewerListener() { + @Override + public void treeExpanded(TreeExpansionEvent event) { + Object element = event.getElement(); + if (element instanceof MethodData) { + MethodData md = (MethodData) element; + expandNode(md); + } + } + @Override + public void treeCollapsed(TreeExpansionEvent event) { + } + }); + + tree.addListener(SWT.MouseDown, new Listener() { + @Override + public void handleEvent(Event event) { + Point point = new Point(event.x, event.y); + TreeItem treeItem = tree.getItem(point); + MethodData md = mProfileProvider.findMatchingTreeItem(treeItem); + if (md == null) + return; + ArrayList selections = new ArrayList(); + selections.add(Selection.highlight("MethodData", md)); + mSelectionController.change(selections, "ProfileView"); + + if (mMethodHandler != null && (event.stateMask & SWT.MOD1) != 0) { + mMethodHandler.handleMethod(md); + } + } + }); + } + + public void setMethodHandler(MethodHandler handler) { + mMethodHandler = handler; + } + + private void findName(String query) { + MethodData md = mProfileProvider.findMatchingName(query); + selectMethod(md); + } + + private void findNextName(String query) { + MethodData md = mProfileProvider.findNextMatchingName(query); + selectMethod(md); + } + + private void selectMethod(MethodData md) { + if (md == null) { + mSearchBox.setBackground(mColorNoMatch); + return; + } + mSearchBox.setBackground(mColorMatch); + highlightMethod(md, false); + } + + @Override + public void update(Observable objservable, Object arg) { + // Ignore updates from myself + if (arg == "ProfileView") + return; + // System.out.printf("profileview update from %s\n", arg); + ArrayList selections; + selections = mSelectionController.getSelections(); + for (Selection selection : selections) { + Selection.Action action = selection.getAction(); + if (action != Selection.Action.Highlight) + continue; + String name = selection.getName(); + if (name == "MethodData") { + MethodData md = (MethodData) selection.getValue(); + highlightMethod(md, true); + return; + } + if (name == "Call") { + Call call = (Call) selection.getValue(); + MethodData md = call.getMethodData(); + highlightMethod(md, true); + return; + } + } + } + + private void highlightMethod(MethodData md, boolean clearSearch) { + if (md == null) + return; + // Avoid an infinite recursion + if (md == mCurrentHighlightedMethod) + return; + if (clearSearch) { + mSearchBox.setText(""); + mSearchBox.setBackground(mColorMatch); + } + mCurrentHighlightedMethod = md; + mTreeViewer.collapseAll(); + // Expand this node and its children + expandNode(md); + StructuredSelection sel = new StructuredSelection(md); + mTreeViewer.setSelection(sel, true); + Tree tree = mTreeViewer.getTree(); + TreeItem[] items = tree.getSelection(); + if (items.length != 0) { + tree.setTopItem(items[0]); + // workaround a Mac bug by adding showItem(). + tree.showItem(items[0]); + } + } + + private void expandNode(MethodData md) { + ProfileNode[] nodes = md.getProfileNodes(); + mTreeViewer.setExpandedState(md, true); + // Also expand the "Parents" and "Children" nodes. + if (nodes != null) { + for (ProfileNode node : nodes) { + if (node.isRecursive() == false) + mTreeViewer.setExpandedState(node, true); + } + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/PropertiesDialog.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/PropertiesDialog.java new file mode 100644 index 00000000..cc0cce06 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/PropertiesDialog.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2011 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.traceview; + +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.viewers.ArrayContentProvider; +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.viewers.TableViewerColumn; +import org.eclipse.swt.SWT; +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.Shell; + +import java.util.HashMap; +import java.util.Map.Entry; + +public class PropertiesDialog extends Dialog { + private HashMap mProperties; + + public PropertiesDialog(Shell parent) { + super(parent); + + setShellStyle(SWT.DIALOG_TRIM | SWT.RESIZE); + } + + public void setProperties(HashMap properties) { + mProperties = properties; + } + + @Override + protected void createButtonsForButtonBar(Composite parent) { + createButton(parent, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL, true); + } + + @Override + protected Control createDialogArea(Composite parent) { + Composite container = (Composite) super.createDialogArea(parent); + GridLayout gridLayout = new GridLayout(1, false); + gridLayout.marginWidth = 0; + gridLayout.marginHeight = 0; + gridLayout.horizontalSpacing = 0; + gridLayout.verticalSpacing = 0; + container.setLayout(gridLayout); + + TableViewer tableViewer = new TableViewer(container, SWT.HIDE_SELECTION + | SWT.V_SCROLL | SWT.BORDER); + tableViewer.getTable().setLinesVisible(true); + tableViewer.getTable().setHeaderVisible(true); + + TableViewerColumn propertyColumn = new TableViewerColumn(tableViewer, SWT.NONE); + propertyColumn.getColumn().setText("Property"); + propertyColumn.setLabelProvider(new ColumnLabelProvider() { + @Override + @SuppressWarnings("unchecked") + public String getText(Object element) { + Entry entry = (Entry) element; + return entry.getKey(); + } + }); + propertyColumn.getColumn().setWidth(400); + + TableViewerColumn valueColumn = new TableViewerColumn(tableViewer, SWT.NONE); + valueColumn.getColumn().setText("Value"); + valueColumn.setLabelProvider(new ColumnLabelProvider() { + @Override + @SuppressWarnings("unchecked") + public String getText(Object element) { + Entry entry = (Entry) element; + return entry.getValue(); + } + }); + valueColumn.getColumn().setWidth(200); + + tableViewer.setContentProvider(new ArrayContentProvider()); + tableViewer.setInput(mProperties.entrySet().toArray()); + + GridData gridData = new GridData(); + gridData.verticalAlignment = GridData.FILL; + gridData.horizontalAlignment = GridData.FILL; + gridData.grabExcessHorizontalSpace = true; + gridData.grabExcessVerticalSpace = true; + tableViewer.getControl().setLayoutData(gridData); + + return container; + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/Selection.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/Selection.java new file mode 100644 index 00000000..7d2e47f3 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/Selection.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2006 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.traceview; + +public class Selection { + + private Action mAction; + private String mName; + private Object mValue; + + public Selection(Action action, String name, Object value) { + mAction = action; + mName = name; + mValue = value; + } + + public static Selection highlight(String name, Object value) { + return new Selection(Action.Highlight, name, value); + } + + public static Selection include(String name, Object value) { + return new Selection(Action.Include, name, value); + } + + public static Selection exclude(String name, Object value) { + return new Selection(Action.Exclude, name, value); + } + + public void setName(String name) { + mName = name; + } + + public String getName() { + return mName; + } + + public void setValue(Object value) { + mValue = value; + } + + public Object getValue() { + return mValue; + } + + public void setAction(Action action) { + mAction = action; + } + + public Action getAction() { + return mAction; + } + + public static enum Action { + Highlight, Include, Exclude, Aggregate + }; +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/SelectionController.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/SelectionController.java new file mode 100644 index 00000000..daf22149 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/SelectionController.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2006 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.traceview; + +import java.util.ArrayList; +import java.util.Observable; + +public class SelectionController extends Observable { + + private ArrayList mSelections; + + public void change(ArrayList selections, Object arg) { + this.mSelections = selections; + setChanged(); + notifyObservers(arg); + } + + public ArrayList getSelections() { + return mSelections; + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ThreadData.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ThreadData.java new file mode 100644 index 00000000..023ae2bf --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/ThreadData.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2006 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.traceview; + +import java.util.ArrayList; +import java.util.HashMap; + +class ThreadData implements TimeLineView.Row { + + private int mId; + private String mName; + private boolean mIsEmpty; + + private Call mRootCall; + private ArrayList mStack = new ArrayList(); + + // This is a hash of all the methods that are currently on the stack. + private HashMap mStackMethods = new HashMap(); + + boolean mHaveGlobalTime; + long mGlobalStartTime; + long mGlobalEndTime; + + boolean mHaveThreadTime; + long mThreadStartTime; + long mThreadEndTime; + + long mThreadCurrentTime; // only used while parsing thread-cpu clock + + ThreadData(int id, String name, MethodData topLevel) { + mId = id; + mName = String.format("[%d] %s", id, name); + mIsEmpty = true; + mRootCall = new Call(this, topLevel, null); + mRootCall.setName(mName); + mStack.add(mRootCall); + } + + @Override + public String getName() { + return mName; + } + + public Call getRootCall() { + return mRootCall; + } + + /** + * Returns true if no calls have ever been recorded for this thread. + */ + public boolean isEmpty() { + return mIsEmpty; + } + + Call enter(MethodData method, ArrayList trace) { + if (mIsEmpty) { + mIsEmpty = false; + if (trace != null) { + trace.add(new TraceAction(TraceAction.ACTION_ENTER, mRootCall)); + } + } + + Call caller = top(); + Call call = new Call(this, method, caller); + mStack.add(call); + + if (trace != null) { + trace.add(new TraceAction(TraceAction.ACTION_ENTER, call)); + } + + Integer num = mStackMethods.get(method); + if (num == null) { + num = 0; + } else if (num > 0) { + call.setRecursive(true); + } + mStackMethods.put(method, num + 1); + + return call; + } + + Call exit(MethodData method, ArrayList trace) { + Call call = top(); + if (call.mCaller == null) { + return null; + } + + if (call.getMethodData() != method) { + String error = "Method exit (" + method.getName() + + ") does not match current method (" + call.getMethodData().getName() + + ")"; + throw new RuntimeException(error); + } + + mStack.remove(mStack.size() - 1); + + if (trace != null) { + trace.add(new TraceAction(TraceAction.ACTION_EXIT, call)); + } + + Integer num = mStackMethods.get(method); + if (num != null) { + if (num == 1) { + mStackMethods.remove(method); + } else { + mStackMethods.put(method, num - 1); + } + } + + return call; + } + + Call top() { + return mStack.get(mStack.size() - 1); + } + + void endTrace(ArrayList trace) { + for (int i = mStack.size() - 1; i >= 1; i--) { + Call call = mStack.get(i); + call.mGlobalEndTime = mGlobalEndTime; + call.mThreadEndTime = mThreadEndTime; + if (trace != null) { + trace.add(new TraceAction(TraceAction.ACTION_INCOMPLETE, call)); + } + } + mStack.clear(); + mStackMethods.clear(); + } + + void updateRootCallTimeBounds() { + if (!mIsEmpty) { + mRootCall.mGlobalStartTime = mGlobalStartTime; + mRootCall.mGlobalEndTime = mGlobalEndTime; + mRootCall.mThreadStartTime = mThreadStartTime; + mRootCall.mThreadEndTime = mThreadEndTime; + } + } + + @Override + public String toString() { + return mName; + } + + @Override + public int getId() { + return mId; + } + + public long getCpuTime() { + return mRootCall.mInclusiveCpuTime; + } + + public long getRealTime() { + return mRootCall.mInclusiveRealTime; + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TickScaler.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TickScaler.java new file mode 100644 index 00000000..1f3fa0f7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TickScaler.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2006 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.traceview; + +class TickScaler { + + private double mMinVal; // required input + private double mMaxVal; // required input + private double mRangeVal; + private int mNumPixels; // required input + private int mPixelsPerTick; // required input + private double mPixelsPerRange; + private double mTickIncrement; + private double mMinMajorTick; + + TickScaler(double minVal, double maxVal, int numPixels, int pixelsPerTick) { + mMinVal = minVal; + mMaxVal = maxVal; + mNumPixels = numPixels; + mPixelsPerTick = pixelsPerTick; + } + + public void setMinVal(double minVal) { + mMinVal = minVal; + } + + public double getMinVal() { + return mMinVal; + } + + public void setMaxVal(double maxVal) { + mMaxVal = maxVal; + } + + public double getMaxVal() { + return mMaxVal; + } + + public void setNumPixels(int numPixels) { + mNumPixels = numPixels; + } + + public int getNumPixels() { + return mNumPixels; + } + + public void setPixelsPerTick(int pixelsPerTick) { + mPixelsPerTick = pixelsPerTick; + } + + public int getPixelsPerTick() { + return mPixelsPerTick; + } + + public void setPixelsPerRange(double pixelsPerRange) { + mPixelsPerRange = pixelsPerRange; + } + + public double getPixelsPerRange() { + return mPixelsPerRange; + } + + public void setTickIncrement(double tickIncrement) { + mTickIncrement = tickIncrement; + } + + public double getTickIncrement() { + return mTickIncrement; + } + + public void setMinMajorTick(double minMajorTick) { + mMinMajorTick = minMajorTick; + } + + public double getMinMajorTick() { + return mMinMajorTick; + } + + // Convert a time value to a 0-based pixel value + public int valueToPixel(double value) { + return (int) Math.ceil(mPixelsPerRange * (value - mMinVal) - 0.5); + } + + // Convert a time value to a 0-based fractional pixel + public double valueToPixelFraction(double value) { + return mPixelsPerRange * (value - mMinVal); + } + + // Convert a 0-based pixel value to a time value + public double pixelToValue(int pixel) { + return mMinVal + (pixel / mPixelsPerRange); + } + + public void computeTicks(boolean useGivenEndPoints) { + int numTicks = mNumPixels / mPixelsPerTick; + mRangeVal = mMaxVal - mMinVal; + mTickIncrement = mRangeVal / numTicks; + double dlogTickIncrement = Math.log10(mTickIncrement); + int logTickIncrement = (int) Math.floor(dlogTickIncrement); + double scale = Math.pow(10, logTickIncrement); + double scaledTickIncr = mTickIncrement / scale; + if (scaledTickIncr > 5.0) + scaledTickIncr = 10; + else if (scaledTickIncr > 2) + scaledTickIncr = 5; + else if (scaledTickIncr > 1) + scaledTickIncr = 2; + else + scaledTickIncr = 1; + mTickIncrement = scaledTickIncr * scale; + + if (!useGivenEndPoints) { + // Round up the max val to the next minor tick + double minorTickIncrement = mTickIncrement / 5; + double dval = mMaxVal / minorTickIncrement; + int ival = (int) dval; + if (ival != dval) + mMaxVal = (ival + 1) * minorTickIncrement; + + // Round down the min val to a multiple of tickIncrement + ival = (int) (mMinVal / mTickIncrement); + mMinVal = ival * mTickIncrement; + mMinMajorTick = mMinVal; + } else { + int ival = (int) (mMinVal / mTickIncrement); + mMinMajorTick = ival * mTickIncrement; + if (mMinMajorTick < mMinVal) + mMinMajorTick = mMinMajorTick + mTickIncrement; + } + + mRangeVal = mMaxVal - mMinVal; + mPixelsPerRange = (double) mNumPixels / mRangeVal; + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TimeBase.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TimeBase.java new file mode 100644 index 00000000..b9a53753 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TimeBase.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2006 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.traceview; + +interface TimeBase { + public static final TimeBase CPU_TIME = new CpuTimeBase(); + public static final TimeBase REAL_TIME = new RealTimeBase(); + + public long getTime(ThreadData threadData); + public long getElapsedInclusiveTime(MethodData methodData); + public long getElapsedExclusiveTime(MethodData methodData); + public long getElapsedInclusiveTime(ProfileData profileData); + + public static final class CpuTimeBase implements TimeBase { + @Override + public long getTime(ThreadData threadData) { + return threadData.getCpuTime(); + } + + @Override + public long getElapsedInclusiveTime(MethodData methodData) { + return methodData.getElapsedInclusiveCpuTime(); + } + + @Override + public long getElapsedExclusiveTime(MethodData methodData) { + return methodData.getElapsedExclusiveCpuTime(); + } + + @Override + public long getElapsedInclusiveTime(ProfileData profileData) { + return profileData.getElapsedInclusiveCpuTime(); + } + } + + public static final class RealTimeBase implements TimeBase { + @Override + public long getTime(ThreadData threadData) { + return threadData.getRealTime(); + } + + @Override + public long getElapsedInclusiveTime(MethodData methodData) { + return methodData.getElapsedInclusiveRealTime(); + } + + @Override + public long getElapsedExclusiveTime(MethodData methodData) { + return methodData.getElapsedExclusiveRealTime(); + } + + @Override + public long getElapsedInclusiveTime(ProfileData profileData) { + return profileData.getElapsedInclusiveRealTime(); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TimeLineView.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TimeLineView.java new file mode 100644 index 00000000..518f676d --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TimeLineView.java @@ -0,0 +1,2154 @@ +/* + * Copyright (C) 2006 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.traceview; + +import org.eclipse.jface.resource.FontRegistry; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.SashForm; +import org.eclipse.swt.events.MouseAdapter; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.MouseMoveListener; +import org.eclipse.swt.events.MouseWheelListener; +import org.eclipse.swt.events.PaintEvent; +import org.eclipse.swt.events.PaintListener; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Cursor; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +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.Canvas; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.ScrollBar; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Observable; +import java.util.Observer; + +public class TimeLineView extends Composite implements Observer { + + private HashMap mRowByName; + private RowData[] mRows; + private Segment[] mSegments; + private HashMap mThreadLabels; + private Timescale mTimescale; + private Surface mSurface; + private RowLabels mLabels; + private SashForm mSashForm; + private int mScrollOffsetY; + + public static final int PixelsPerTick = 50; + private TickScaler mScaleInfo = new TickScaler(0, 0, 0, PixelsPerTick); + private static final int LeftMargin = 10; // blank space on left + private static final int RightMargin = 60; // blank space on right + + private Color mColorBlack; + private Color mColorGray; + private Color mColorDarkGray; + private Color mColorForeground; + private Color mColorRowBack; + private Color mColorZoomSelection; + private FontRegistry mFontRegistry; + + /** vertical height of drawn blocks in each row */ + private static final int rowHeight = 20; + + /** the blank space between rows */ + private static final int rowYMargin = 12; + private static final int rowYMarginHalf = rowYMargin / 2; + + /** total vertical space for row */ + private static final int rowYSpace = rowHeight + rowYMargin; + private static final int majorTickLength = 8; + private static final int minorTickLength = 4; + private static final int timeLineOffsetY = 58; + private static final int tickToFontSpacing = 2; + + /** start of first row */ + private static final int topMargin = 90; + private int mMouseRow = -1; + private int mNumRows; + private int mStartRow; + private int mEndRow; + private TraceUnits mUnits; + private String mClockSource; + private boolean mHaveCpuTime; + private boolean mHaveRealTime; + private int mSmallFontWidth; + private int mSmallFontHeight; + private SelectionController mSelectionController; + private MethodData mHighlightMethodData; + private Call mHighlightCall; + private static final int MinInclusiveRange = 3; + + /** Setting the fonts looks good on Linux but bad on Macs */ + private boolean mSetFonts = false; + + public static interface Block { + public String getName(); + public MethodData getMethodData(); + public long getStartTime(); + public long getEndTime(); + public Color getColor(); + public double addWeight(int x, int y, double weight); + public void clearWeight(); + public long getExclusiveCpuTime(); + public long getInclusiveCpuTime(); + public long getExclusiveRealTime(); + public long getInclusiveRealTime(); + public boolean isContextSwitch(); + public boolean isIgnoredBlock(); + public Block getParentBlock(); + } + + public static interface Row { + public int getId(); + public String getName(); + } + + public static class Record { + Row row; + Block block; + + public Record(Row row, Block block) { + this.row = row; + this.block = block; + } + } + + public TimeLineView(Composite parent, TraceReader reader, + SelectionController selectionController) { + super(parent, SWT.NONE); + mRowByName = new HashMap(); + this.mSelectionController = selectionController; + selectionController.addObserver(this); + mUnits = reader.getTraceUnits(); + mClockSource = reader.getClockSource(); + mHaveCpuTime = reader.haveCpuTime(); + mHaveRealTime = reader.haveRealTime(); + mThreadLabels = reader.getThreadLabels(); + + Display display = getDisplay(); + mColorGray = display.getSystemColor(SWT.COLOR_GRAY); + mColorDarkGray = display.getSystemColor(SWT.COLOR_DARK_GRAY); + mColorBlack = display.getSystemColor(SWT.COLOR_BLACK); + // mColorBackground = display.getSystemColor(SWT.COLOR_WHITE); + mColorForeground = display.getSystemColor(SWT.COLOR_BLACK); + mColorRowBack = new Color(display, 240, 240, 255); + mColorZoomSelection = new Color(display, 230, 230, 230); + + mFontRegistry = new FontRegistry(display); + mFontRegistry.put("small", //$NON-NLS-1$ + new FontData[] { new FontData("Arial", 8, SWT.NORMAL) }); //$NON-NLS-1$ + mFontRegistry.put("courier8", //$NON-NLS-1$ + new FontData[] { new FontData("Courier New", 8, SWT.BOLD) }); //$NON-NLS-1$ + mFontRegistry.put("medium", //$NON-NLS-1$ + new FontData[] { new FontData("Courier New", 10, SWT.NORMAL) }); //$NON-NLS-1$ + + Image image = new Image(display, new Rectangle(100, 100, 100, 100)); + GC gc = new GC(image); + if (mSetFonts) { + gc.setFont(mFontRegistry.get("small")); //$NON-NLS-1$ + } + mSmallFontWidth = gc.getFontMetrics().getAverageCharWidth(); + mSmallFontHeight = gc.getFontMetrics().getHeight(); + + image.dispose(); + gc.dispose(); + + setLayout(new FillLayout()); + + // Create a sash form for holding two canvas views, one for the + // thread labels and one for the thread timeline. + mSashForm = new SashForm(this, SWT.HORIZONTAL); + mSashForm.setBackground(mColorGray); + mSashForm.SASH_WIDTH = 3; + + // Create a composite for the left side of the sash + Composite composite = new Composite(mSashForm, SWT.NONE); + GridLayout layout = new GridLayout(1, true /* make columns equal width */); + layout.marginHeight = 0; + layout.marginWidth = 0; + layout.verticalSpacing = 1; + composite.setLayout(layout); + + // Create a blank corner space in the upper left corner + BlankCorner corner = new BlankCorner(composite); + GridData gridData = new GridData(GridData.FILL_HORIZONTAL); + gridData.heightHint = topMargin; + corner.setLayoutData(gridData); + + // Add the thread labels below the blank corner. + mLabels = new RowLabels(composite); + gridData = new GridData(GridData.FILL_BOTH); + mLabels.setLayoutData(gridData); + + // Create another composite for the right side of the sash + composite = new Composite(mSashForm, SWT.NONE); + layout = new GridLayout(1, true /* make columns equal width */); + layout.marginHeight = 0; + layout.marginWidth = 0; + layout.verticalSpacing = 1; + composite.setLayout(layout); + + mTimescale = new Timescale(composite); + gridData = new GridData(GridData.FILL_HORIZONTAL); + gridData.heightHint = topMargin; + mTimescale.setLayoutData(gridData); + + mSurface = new Surface(composite); + gridData = new GridData(GridData.FILL_BOTH); + mSurface.setLayoutData(gridData); + mSashForm.setWeights(new int[] { 1, 5 }); + + final ScrollBar vBar = mSurface.getVerticalBar(); + vBar.addListener(SWT.Selection, new Listener() { + @Override + public void handleEvent(Event e) { + mScrollOffsetY = vBar.getSelection(); + Point dim = mSurface.getSize(); + int newScrollOffsetY = computeVisibleRows(dim.y); + if (newScrollOffsetY != mScrollOffsetY) { + mScrollOffsetY = newScrollOffsetY; + vBar.setSelection(newScrollOffsetY); + } + mLabels.redraw(); + mSurface.redraw(); + } + }); + + final ScrollBar hBar = mSurface.getHorizontalBar(); + hBar.addListener(SWT.Selection, new Listener() { + @Override + public void handleEvent(Event e) { + mSurface.setScaleFromHorizontalScrollBar(hBar.getSelection()); + mSurface.redraw(); + } + }); + + mSurface.addListener(SWT.Resize, new Listener() { + @Override + public void handleEvent(Event e) { + Point dim = mSurface.getSize(); + + // If we don't need the scroll bar then don't display it. + if (dim.y >= mNumRows * rowYSpace) { + vBar.setVisible(false); + } else { + vBar.setVisible(true); + } + int newScrollOffsetY = computeVisibleRows(dim.y); + if (newScrollOffsetY != mScrollOffsetY) { + mScrollOffsetY = newScrollOffsetY; + vBar.setSelection(newScrollOffsetY); + } + + int spaceNeeded = mNumRows * rowYSpace; + vBar.setMaximum(spaceNeeded); + vBar.setThumb(dim.y); + + mLabels.redraw(); + mSurface.redraw(); + } + }); + + mSurface.addMouseListener(new MouseAdapter() { + @Override + public void mouseUp(MouseEvent me) { + mSurface.mouseUp(me); + } + + @Override + public void mouseDown(MouseEvent me) { + mSurface.mouseDown(me); + } + + @Override + public void mouseDoubleClick(MouseEvent me) { + mSurface.mouseDoubleClick(me); + } + }); + + mSurface.addMouseMoveListener(new MouseMoveListener() { + @Override + public void mouseMove(MouseEvent me) { + mSurface.mouseMove(me); + } + }); + + mSurface.addMouseWheelListener(new MouseWheelListener() { + @Override + public void mouseScrolled(MouseEvent me) { + mSurface.mouseScrolled(me); + } + }); + + mTimescale.addMouseListener(new MouseAdapter() { + @Override + public void mouseUp(MouseEvent me) { + mTimescale.mouseUp(me); + } + + @Override + public void mouseDown(MouseEvent me) { + mTimescale.mouseDown(me); + } + + @Override + public void mouseDoubleClick(MouseEvent me) { + mTimescale.mouseDoubleClick(me); + } + }); + + mTimescale.addMouseMoveListener(new MouseMoveListener() { + @Override + public void mouseMove(MouseEvent me) { + mTimescale.mouseMove(me); + } + }); + + mLabels.addMouseMoveListener(new MouseMoveListener() { + @Override + public void mouseMove(MouseEvent me) { + mLabels.mouseMove(me); + } + }); + + setData(reader.getThreadTimeRecords()); + } + + @Override + public void update(Observable objservable, Object arg) { + // Ignore updates from myself + if (arg == "TimeLineView") //$NON-NLS-1$ + return; + // System.out.printf("timeline update from %s\n", arg); + boolean foundHighlight = false; + ArrayList selections; + selections = mSelectionController.getSelections(); + for (Selection selection : selections) { + Selection.Action action = selection.getAction(); + if (action != Selection.Action.Highlight) + continue; + String name = selection.getName(); + // System.out.printf(" timeline highlight %s from %s\n", name, arg); + if (name == "MethodData") { //$NON-NLS-1$ + foundHighlight = true; + mHighlightMethodData = (MethodData) selection.getValue(); + // System.out.printf(" method %s\n", + // highlightMethodData.getName()); + mHighlightCall = null; + startHighlighting(); + } else if (name == "Call") { //$NON-NLS-1$ + foundHighlight = true; + mHighlightCall = (Call) selection.getValue(); + // System.out.printf(" call %s\n", highlightCall.getName()); + mHighlightMethodData = null; + startHighlighting(); + } + } + if (foundHighlight == false) + mSurface.clearHighlights(); + } + + public void setData(ArrayList records) { + if (records == null) + records = new ArrayList(); + + if (false) { + System.out.println("TimelineView() list of records:"); //$NON-NLS-1$ + for (Record r : records) { + System.out.printf("row '%s' block '%s' [%d, %d]\n", r.row //$NON-NLS-1$ + .getName(), r.block.getName(), r.block.getStartTime(), + r.block.getEndTime()); + if (r.block.getStartTime() > r.block.getEndTime()) { + System.err.printf("Error: block startTime > endTime\n"); //$NON-NLS-1$ + System.exit(1); + } + } + } + + // Sort the records into increasing start time, and decreasing end time + Collections.sort(records, new Comparator() { + @Override + public int compare(Record r1, Record r2) { + long start1 = r1.block.getStartTime(); + long start2 = r2.block.getStartTime(); + if (start1 > start2) + return 1; + if (start1 < start2) + return -1; + + // The start times are the same, so compare the end times + long end1 = r1.block.getEndTime(); + long end2 = r2.block.getEndTime(); + if (end1 > end2) + return -1; + if (end1 < end2) + return 1; + + return 0; + } + }); + + ArrayList segmentList = new ArrayList(); + + // The records are sorted into increasing start time, + // so the minimum start time is the start time of the first record. + double minVal = 0; + if (records.size() > 0) + minVal = records.get(0).block.getStartTime(); + + // Sum the time spent in each row and block, and + // keep track of the maximum end time. + double maxVal = 0; + for (Record rec : records) { + Row row = rec.row; + Block block = rec.block; + if (block.isIgnoredBlock()) { + continue; + } + + String rowName = row.getName(); + RowData rd = mRowByName.get(rowName); + if (rd == null) { + rd = new RowData(row); + mRowByName.put(rowName, rd); + } + long blockStartTime = block.getStartTime(); + long blockEndTime = block.getEndTime(); + if (blockEndTime > rd.mEndTime) { + long start = Math.max(blockStartTime, rd.mEndTime); + rd.mElapsed += blockEndTime - start; + rd.mEndTime = blockEndTime; + } + if (blockEndTime > maxVal) + maxVal = blockEndTime; + + // Keep track of nested blocks by using a stack (for each row). + // Create a Segment object for each visible part of a block. + Block top = rd.top(); + if (top == null) { + rd.push(block); + continue; + } + + long topStartTime = top.getStartTime(); + long topEndTime = top.getEndTime(); + if (topEndTime >= blockStartTime) { + // Add this segment if it has a non-zero elapsed time. + if (topStartTime < blockStartTime) { + Segment segment = new Segment(rd, top, topStartTime, + blockStartTime); + segmentList.add(segment); + } + + // If this block starts where the previous (top) block ends, + // then pop off the top block. + if (topEndTime == blockStartTime) + rd.pop(); + rd.push(block); + } else { + // We may have to pop several frames here. + popFrames(rd, top, blockStartTime, segmentList); + rd.push(block); + } + } + + // Clean up the stack of each row + for (RowData rd : mRowByName.values()) { + Block top = rd.top(); + popFrames(rd, top, Integer.MAX_VALUE, segmentList); + } + + mSurface.setRange(minVal, maxVal); + mSurface.setLimitRange(minVal, maxVal); + + // Sort the rows into decreasing elapsed time + Collection rv = mRowByName.values(); + mRows = rv.toArray(new RowData[rv.size()]); + Arrays.sort(mRows, new Comparator() { + @Override + public int compare(RowData rd1, RowData rd2) { + return (int) (rd2.mElapsed - rd1.mElapsed); + } + }); + + // Assign ranks to the sorted rows + for (int ii = 0; ii < mRows.length; ++ii) { + mRows[ii].mRank = ii; + } + + // Compute the number of rows with data + mNumRows = 0; + for (int ii = 0; ii < mRows.length; ++ii) { + if (mRows[ii].mElapsed == 0) + break; + mNumRows += 1; + } + + // Sort the blocks into increasing rows, and within rows into + // increasing start values. + mSegments = segmentList.toArray(new Segment[segmentList.size()]); + Arrays.sort(mSegments, new Comparator() { + @Override + public int compare(Segment bd1, Segment bd2) { + RowData rd1 = bd1.mRowData; + RowData rd2 = bd2.mRowData; + int diff = rd1.mRank - rd2.mRank; + if (diff == 0) { + long timeDiff = bd1.mStartTime - bd2.mStartTime; + if (timeDiff == 0) + timeDiff = bd1.mEndTime - bd2.mEndTime; + return (int) timeDiff; + } + return diff; + } + }); + + if (false) { + for (Segment segment : mSegments) { + System.out.printf("seg '%s' [%6d, %6d] %s\n", + segment.mRowData.mName, segment.mStartTime, + segment.mEndTime, segment.mBlock.getName()); + if (segment.mStartTime > segment.mEndTime) { + System.err.printf("Error: segment startTime > endTime\n"); + System.exit(1); + } + } + } + } + + private static void popFrames(RowData rd, Block top, long startTime, + ArrayList segmentList) { + long topEndTime = top.getEndTime(); + long lastEndTime = top.getStartTime(); + while (topEndTime <= startTime) { + if (topEndTime > lastEndTime) { + Segment segment = new Segment(rd, top, lastEndTime, topEndTime); + segmentList.add(segment); + lastEndTime = topEndTime; + } + rd.pop(); + top = rd.top(); + if (top == null) + return; + topEndTime = top.getEndTime(); + } + + // If we get here, then topEndTime > startTime + if (lastEndTime < startTime) { + Segment bd = new Segment(rd, top, lastEndTime, startTime); + segmentList.add(bd); + } + } + + private class RowLabels extends Canvas { + + /** The space between the row label and the sash line */ + private static final int labelMarginX = 2; + + public RowLabels(Composite parent) { + super(parent, SWT.NO_BACKGROUND); + addPaintListener(new PaintListener() { + @Override + public void paintControl(PaintEvent pe) { + draw(pe.display, pe.gc); + } + }); + } + + private void mouseMove(MouseEvent me) { + int rownum = (me.y + mScrollOffsetY) / rowYSpace; + if (mMouseRow != rownum) { + mMouseRow = rownum; + redraw(); + mSurface.redraw(); + } + } + + private void draw(Display display, GC gc) { + if (mSegments.length == 0) { + // gc.setBackground(colorBackground); + // gc.fillRectangle(getBounds()); + return; + } + Point dim = getSize(); + + // Create an image for double-buffering + Image image = new Image(display, getBounds()); + + // Set up the off-screen gc + GC gcImage = new GC(image); + if (mSetFonts) + gcImage.setFont(mFontRegistry.get("medium")); //$NON-NLS-1$ + + if (mNumRows > 2) { + // Draw the row background stripes + gcImage.setBackground(mColorRowBack); + for (int ii = 1; ii < mNumRows; ii += 2) { + RowData rd = mRows[ii]; + int y1 = rd.mRank * rowYSpace - mScrollOffsetY; + gcImage.fillRectangle(0, y1, dim.x, rowYSpace); + } + } + + // Draw the row labels + int offsetY = rowYMarginHalf - mScrollOffsetY; + for (int ii = mStartRow; ii <= mEndRow; ++ii) { + RowData rd = mRows[ii]; + int y1 = rd.mRank * rowYSpace + offsetY; + Point extent = gcImage.stringExtent(rd.mName); + int x1 = dim.x - extent.x - labelMarginX; + gcImage.drawString(rd.mName, x1, y1, true); + } + + // Draw a highlight box on the row where the mouse is. + if (mMouseRow >= mStartRow && mMouseRow <= mEndRow) { + gcImage.setForeground(mColorGray); + int y1 = mMouseRow * rowYSpace - mScrollOffsetY; + gcImage.drawRectangle(0, y1, dim.x, rowYSpace); + } + + // Draw the off-screen buffer to the screen + gc.drawImage(image, 0, 0); + + // Clean up + image.dispose(); + gcImage.dispose(); + } + } + + private class BlankCorner extends Canvas { + public BlankCorner(Composite parent) { + //super(parent, SWT.NO_BACKGROUND); + super(parent, SWT.NONE); + addPaintListener(new PaintListener() { + @Override + public void paintControl(PaintEvent pe) { + draw(pe.display, pe.gc); + } + }); + } + + private void draw(Display display, GC gc) { + // Create a blank image and draw it to the canvas + Image image = new Image(display, getBounds()); + gc.drawImage(image, 0, 0); + + // Clean up + image.dispose(); + } + } + + private class Timescale extends Canvas { + private Point mMouse = new Point(LeftMargin, 0); + private Cursor mZoomCursor; + private String mMethodName = null; + private Color mMethodColor = null; + private String mDetails; + private int mMethodStartY; + private int mDetailsStartY; + private int mMarkStartX; + private int mMarkEndX; + + /** The space between the colored block and the method name */ + private static final int METHOD_BLOCK_MARGIN = 10; + + public Timescale(Composite parent) { + //super(parent, SWT.NO_BACKGROUND); + super(parent, SWT.NONE); + Display display = getDisplay(); + mZoomCursor = new Cursor(display, SWT.CURSOR_SIZEWE); + setCursor(mZoomCursor); + mMethodStartY = mSmallFontHeight + 1; + mDetailsStartY = mMethodStartY + mSmallFontHeight + 1; + addPaintListener(new PaintListener() { + @Override + public void paintControl(PaintEvent pe) { + draw(pe.display, pe.gc); + } + }); + } + + public void setVbarPosition(int x) { + mMouse.x = x; + } + + public void setMarkStart(int x) { + mMarkStartX = x; + } + + public void setMarkEnd(int x) { + mMarkEndX = x; + } + + public void setMethodName(String name) { + mMethodName = name; + } + + public void setMethodColor(Color color) { + mMethodColor = color; + } + + public void setDetails(String details) { + mDetails = details; + } + + private void mouseMove(MouseEvent me) { + me.y = -1; + mSurface.mouseMove(me); + } + + private void mouseDown(MouseEvent me) { + mSurface.startScaling(me.x); + mSurface.redraw(); + } + + private void mouseUp(MouseEvent me) { + mSurface.stopScaling(me.x); + } + + private void mouseDoubleClick(MouseEvent me) { + mSurface.resetScale(); + mSurface.redraw(); + } + + private void draw(Display display, GC gc) { + Point dim = getSize(); + + // Create an image for double-buffering + Image image = new Image(display, getBounds()); + + // Set up the off-screen gc + GC gcImage = new GC(image); + if (mSetFonts) + gcImage.setFont(mFontRegistry.get("medium")); //$NON-NLS-1$ + + if (mSurface.drawingSelection()) { + drawSelection(display, gcImage); + } + + drawTicks(display, gcImage); + + // Draw the vertical bar where the mouse is + gcImage.setForeground(mColorDarkGray); + gcImage.drawLine(mMouse.x, timeLineOffsetY, mMouse.x, dim.y); + + // Draw the current millseconds + drawTickLegend(display, gcImage); + + // Draw the method name and color, if needed + drawMethod(display, gcImage); + + // Draw the details, if needed + drawDetails(display, gcImage); + + // Draw the off-screen buffer to the screen + gc.drawImage(image, 0, 0); + + // Clean up + image.dispose(); + gcImage.dispose(); + } + + private void drawSelection(Display display, GC gc) { + Point dim = getSize(); + gc.setForeground(mColorGray); + gc.drawLine(mMarkStartX, timeLineOffsetY, mMarkStartX, dim.y); + gc.setBackground(mColorZoomSelection); + int x, width; + if (mMarkStartX < mMarkEndX) { + x = mMarkStartX; + width = mMarkEndX - mMarkStartX; + } else { + x = mMarkEndX; + width = mMarkStartX - mMarkEndX; + } + if (width > 1) { + gc.fillRectangle(x, timeLineOffsetY, width, dim.y); + } + } + + private void drawTickLegend(Display display, GC gc) { + int mouseX = mMouse.x - LeftMargin; + double mouseXval = mScaleInfo.pixelToValue(mouseX); + String info = mUnits.labelledString(mouseXval); + gc.setForeground(mColorForeground); + gc.drawString(info, LeftMargin + 2, 1, true); + + // Display the maximum data value + double maxVal = mScaleInfo.getMaxVal(); + info = mUnits.labelledString(maxVal); + if (mClockSource != null) { + info = String.format(" max %s (%s)", info, mClockSource); //$NON-NLS-1$ + } else { + info = String.format(" max %s ", info); //$NON-NLS-1$ + } + Point extent = gc.stringExtent(info); + Point dim = getSize(); + int x1 = dim.x - RightMargin - extent.x; + gc.drawString(info, x1, 1, true); + } + + private void drawMethod(Display display, GC gc) { + if (mMethodName == null) { + return; + } + + int x1 = LeftMargin; + int y1 = mMethodStartY; + gc.setBackground(mMethodColor); + int width = 2 * mSmallFontWidth; + gc.fillRectangle(x1, y1, width, mSmallFontHeight); + x1 += width + METHOD_BLOCK_MARGIN; + gc.drawString(mMethodName, x1, y1, true); + } + + private void drawDetails(Display display, GC gc) { + if (mDetails == null) { + return; + } + + int x1 = LeftMargin + 2 * mSmallFontWidth + METHOD_BLOCK_MARGIN; + int y1 = mDetailsStartY; + gc.drawString(mDetails, x1, y1, true); + } + + private void drawTicks(Display display, GC gc) { + Point dim = getSize(); + int y2 = majorTickLength + timeLineOffsetY; + int y3 = minorTickLength + timeLineOffsetY; + int y4 = y2 + tickToFontSpacing; + gc.setForeground(mColorForeground); + gc.drawLine(LeftMargin, timeLineOffsetY, dim.x - RightMargin, + timeLineOffsetY); + double minVal = mScaleInfo.getMinVal(); + double maxVal = mScaleInfo.getMaxVal(); + double minMajorTick = mScaleInfo.getMinMajorTick(); + double tickIncrement = mScaleInfo.getTickIncrement(); + double minorTickIncrement = tickIncrement / 5; + double pixelsPerRange = mScaleInfo.getPixelsPerRange(); + + // Draw the initial minor ticks, if any + if (minVal < minMajorTick) { + gc.setForeground(mColorGray); + double xMinor = minMajorTick; + for (int ii = 1; ii <= 4; ++ii) { + xMinor -= minorTickIncrement; + if (xMinor < minVal) + break; + int x1 = LeftMargin + + (int) (0.5 + (xMinor - minVal) * pixelsPerRange); + gc.drawLine(x1, timeLineOffsetY, x1, y3); + } + } + + if (tickIncrement <= 10) { + // TODO avoid rendering the loop when tickIncrement is invalid. It can be zero + // or too small. + // System.out.println(String.format("Timescale.drawTicks error: tickIncrement=%1f", tickIncrement)); + return; + } + for (double x = minMajorTick; x <= maxVal; x += tickIncrement) { + int x1 = LeftMargin + + (int) (0.5 + (x - minVal) * pixelsPerRange); + + // Draw a major tick + gc.setForeground(mColorForeground); + gc.drawLine(x1, timeLineOffsetY, x1, y2); + if (x > maxVal) + break; + + // Draw the tick text + String tickString = mUnits.valueOf(x); + gc.drawString(tickString, x1, y4, true); + + // Draw 4 minor ticks between major ticks + gc.setForeground(mColorGray); + double xMinor = x; + for (int ii = 1; ii <= 4; ii++) { + xMinor += minorTickIncrement; + if (xMinor > maxVal) + break; + x1 = LeftMargin + + (int) (0.5 + (xMinor - minVal) * pixelsPerRange); + gc.drawLine(x1, timeLineOffsetY, x1, y3); + } + } + } + } + + private static enum GraphicsState { + Normal, Marking, Scaling, Animating, Scrolling + }; + + private class Surface extends Canvas { + + public Surface(Composite parent) { + super(parent, SWT.NO_BACKGROUND | SWT.V_SCROLL | SWT.H_SCROLL); + Display display = getDisplay(); + mNormalCursor = new Cursor(display, SWT.CURSOR_CROSS); + mIncreasingCursor = new Cursor(display, SWT.CURSOR_SIZEE); + mDecreasingCursor = new Cursor(display, SWT.CURSOR_SIZEW); + + initZoomFractionsWithExp(); + + addPaintListener(new PaintListener() { + @Override + public void paintControl(PaintEvent pe) { + draw(pe.display, pe.gc); + } + }); + + mZoomAnimator = new Runnable() { + @Override + public void run() { + animateZoom(); + } + }; + + mHighlightAnimator = new Runnable() { + @Override + public void run() { + animateHighlight(); + } + }; + } + + private void initZoomFractionsWithExp() { + mZoomFractions = new double[ZOOM_STEPS]; + int next = 0; + for (int ii = 0; ii < ZOOM_STEPS / 2; ++ii, ++next) { + mZoomFractions[next] = (double) (1 << ii) + / (double) (1 << (ZOOM_STEPS / 2)); + // System.out.printf("%d %f\n", next, zoomFractions[next]); + } + for (int ii = 2; ii < 2 + ZOOM_STEPS / 2; ++ii, ++next) { + mZoomFractions[next] = (double) ((1 << ii) - 1) + / (double) (1 << ii); + // System.out.printf("%d %f\n", next, zoomFractions[next]); + } + } + + @SuppressWarnings("unused") + private void initZoomFractionsWithSinWave() { + mZoomFractions = new double[ZOOM_STEPS]; + for (int ii = 0; ii < ZOOM_STEPS; ++ii) { + double offset = Math.PI * ii / ZOOM_STEPS; + mZoomFractions[ii] = (Math.sin((1.5 * Math.PI + offset)) + 1.0) / 2.0; + // System.out.printf("%d %f\n", ii, zoomFractions[ii]); + } + } + + public void setRange(double minVal, double maxVal) { + mMinDataVal = minVal; + mMaxDataVal = maxVal; + mScaleInfo.setMinVal(minVal); + mScaleInfo.setMaxVal(maxVal); + } + + public void setLimitRange(double minVal, double maxVal) { + mLimitMinVal = minVal; + mLimitMaxVal = maxVal; + } + + public void resetScale() { + mScaleInfo.setMinVal(mLimitMinVal); + mScaleInfo.setMaxVal(mLimitMaxVal); + } + + public void setScaleFromHorizontalScrollBar(int selection) { + double minVal = mScaleInfo.getMinVal(); + double maxVal = mScaleInfo.getMaxVal(); + double visibleRange = maxVal - minVal; + + minVal = mLimitMinVal + selection; + maxVal = minVal + visibleRange; + if (maxVal > mLimitMaxVal) { + maxVal = mLimitMaxVal; + minVal = maxVal - visibleRange; + } + mScaleInfo.setMinVal(minVal); + mScaleInfo.setMaxVal(maxVal); + + mGraphicsState = GraphicsState.Scrolling; + } + + private void updateHorizontalScrollBar() { + double minVal = mScaleInfo.getMinVal(); + double maxVal = mScaleInfo.getMaxVal(); + double visibleRange = maxVal - minVal; + double fullRange = mLimitMaxVal - mLimitMinVal; + + ScrollBar hBar = getHorizontalBar(); + if (fullRange > visibleRange) { + hBar.setVisible(true); + hBar.setMinimum(0); + hBar.setMaximum((int)Math.ceil(fullRange)); + hBar.setThumb((int)Math.ceil(visibleRange)); + hBar.setSelection((int)Math.floor(minVal - mLimitMinVal)); + } else { + hBar.setVisible(false); + } + } + + private void draw(Display display, GC gc) { + if (mSegments.length == 0) { + // gc.setBackground(colorBackground); + // gc.fillRectangle(getBounds()); + return; + } + + // Create an image for double-buffering + Image image = new Image(display, getBounds()); + + // Set up the off-screen gc + GC gcImage = new GC(image); + if (mSetFonts) + gcImage.setFont(mFontRegistry.get("small")); //$NON-NLS-1$ + + // Draw the background + // gcImage.setBackground(colorBackground); + // gcImage.fillRectangle(image.getBounds()); + + if (mGraphicsState == GraphicsState.Scaling) { + double diff = mMouse.x - mMouseMarkStartX; + if (diff > 0) { + double newMinVal = mScaleMinVal - diff / mScalePixelsPerRange; + if (newMinVal < mLimitMinVal) + newMinVal = mLimitMinVal; + mScaleInfo.setMinVal(newMinVal); + // System.out.printf("diff %f scaleMin %f newMin %f\n", + // diff, scaleMinVal, newMinVal); + } else if (diff < 0) { + double newMaxVal = mScaleMaxVal - diff / mScalePixelsPerRange; + if (newMaxVal > mLimitMaxVal) + newMaxVal = mLimitMaxVal; + mScaleInfo.setMaxVal(newMaxVal); + // System.out.printf("diff %f scaleMax %f newMax %f\n", + // diff, scaleMaxVal, newMaxVal); + } + } + + // Recompute the ticks and strips only if the size has changed, + // or we scrolled so that a new row is visible. + Point dim = getSize(); + if (mStartRow != mCachedStartRow || mEndRow != mCachedEndRow + || mScaleInfo.getMinVal() != mCachedMinVal + || mScaleInfo.getMaxVal() != mCachedMaxVal) { + mCachedStartRow = mStartRow; + mCachedEndRow = mEndRow; + int xdim = dim.x - TotalXMargin; + mScaleInfo.setNumPixels(xdim); + boolean forceEndPoints = (mGraphicsState == GraphicsState.Scaling + || mGraphicsState == GraphicsState.Animating + || mGraphicsState == GraphicsState.Scrolling); + mScaleInfo.computeTicks(forceEndPoints); + mCachedMinVal = mScaleInfo.getMinVal(); + mCachedMaxVal = mScaleInfo.getMaxVal(); + if (mLimitMinVal > mScaleInfo.getMinVal()) + mLimitMinVal = mScaleInfo.getMinVal(); + if (mLimitMaxVal < mScaleInfo.getMaxVal()) + mLimitMaxVal = mScaleInfo.getMaxVal(); + + // Compute the strips + computeStrips(); + + // Update the horizontal scrollbar. + updateHorizontalScrollBar(); + } + + if (mNumRows > 2) { + // Draw the row background stripes + gcImage.setBackground(mColorRowBack); + for (int ii = 1; ii < mNumRows; ii += 2) { + RowData rd = mRows[ii]; + int y1 = rd.mRank * rowYSpace - mScrollOffsetY; + gcImage.fillRectangle(0, y1, dim.x, rowYSpace); + } + } + + if (drawingSelection()) { + drawSelection(display, gcImage); + } + + String blockName = null; + Color blockColor = null; + String blockDetails = null; + + if (mDebug) { + double pixelsPerRange = mScaleInfo.getPixelsPerRange(); + System.out + .printf( + "dim.x %d pixels %d minVal %f, maxVal %f ppr %f rpp %f\n", + dim.x, dim.x - TotalXMargin, mScaleInfo + .getMinVal(), mScaleInfo.getMaxVal(), + pixelsPerRange, 1.0 / pixelsPerRange); + } + + // Draw the strips + Block selectBlock = null; + for (Strip strip : mStripList) { + if (strip.mColor == null) { + // System.out.printf("strip.color is null\n"); + continue; + } + gcImage.setBackground(strip.mColor); + gcImage.fillRectangle(strip.mX, strip.mY - mScrollOffsetY, strip.mWidth, + strip.mHeight); + if (mMouseRow == strip.mRowData.mRank) { + if (mMouse.x >= strip.mX + && mMouse.x < strip.mX + strip.mWidth) { + Block block = strip.mSegment.mBlock; + blockName = block.getName(); + blockColor = strip.mColor; + if (mHaveCpuTime) { + if (mHaveRealTime) { + blockDetails = String.format( + "excl cpu %s, incl cpu %s, " + + "excl real %s, incl real %s", + mUnits.labelledString(block.getExclusiveCpuTime()), + mUnits.labelledString(block.getInclusiveCpuTime()), + mUnits.labelledString(block.getExclusiveRealTime()), + mUnits.labelledString(block.getInclusiveRealTime())); + } else { + blockDetails = String.format( + "excl cpu %s, incl cpu %s", + mUnits.labelledString(block.getExclusiveCpuTime()), + mUnits.labelledString(block.getInclusiveCpuTime())); + } + } else { + blockDetails = String.format( + "excl real %s, incl real %s", + mUnits.labelledString(block.getExclusiveRealTime()), + mUnits.labelledString(block.getInclusiveRealTime())); + } + } + if (mMouseSelect.x >= strip.mX + && mMouseSelect.x < strip.mX + strip.mWidth) { + selectBlock = strip.mSegment.mBlock; + } + } + } + mMouseSelect.x = 0; + mMouseSelect.y = 0; + + if (selectBlock != null) { + ArrayList selections = new ArrayList(); + // Get the row label + RowData rd = mRows[mMouseRow]; + selections.add(Selection.highlight("Thread", rd.mName)); //$NON-NLS-1$ + selections.add(Selection.highlight("Call", selectBlock)); //$NON-NLS-1$ + + int mouseX = mMouse.x - LeftMargin; + double mouseXval = mScaleInfo.pixelToValue(mouseX); + selections.add(Selection.highlight("Time", mouseXval)); //$NON-NLS-1$ + + mSelectionController.change(selections, "TimeLineView"); //$NON-NLS-1$ + mHighlightMethodData = null; + mHighlightCall = (Call) selectBlock; + startHighlighting(); + } + + // Draw a highlight box on the row where the mouse is. + // Except don't draw the box if we are animating the + // highlighing of a call or method because the inclusive + // highlight bar passes through the highlight box and + // causes an annoying flashing artifact. + if (mMouseRow >= 0 && mMouseRow < mNumRows && mHighlightStep == 0) { + gcImage.setForeground(mColorGray); + int y1 = mMouseRow * rowYSpace - mScrollOffsetY; + gcImage.drawLine(0, y1, dim.x, y1); + gcImage.drawLine(0, y1 + rowYSpace, dim.x, y1 + rowYSpace); + } + + // Highlight a selected method, if any + drawHighlights(gcImage, dim); + + // Draw a vertical line where the mouse is. + gcImage.setForeground(mColorDarkGray); + int lineEnd = Math.min(dim.y, mNumRows * rowYSpace); + gcImage.drawLine(mMouse.x, 0, mMouse.x, lineEnd); + + if (blockName != null) { + mTimescale.setMethodName(blockName); + mTimescale.setMethodColor(blockColor); + mTimescale.setDetails(blockDetails); + mShowHighlightName = false; + } else if (mShowHighlightName) { + // Draw the highlighted method name + MethodData md = mHighlightMethodData; + if (md == null && mHighlightCall != null) + md = mHighlightCall.getMethodData(); + if (md == null) + System.out.printf("null highlight?\n"); //$NON-NLS-1$ + if (md != null) { + mTimescale.setMethodName(md.getProfileName()); + mTimescale.setMethodColor(md.getColor()); + mTimescale.setDetails(null); + } + } else { + mTimescale.setMethodName(null); + mTimescale.setMethodColor(null); + mTimescale.setDetails(null); + } + mTimescale.redraw(); + + // Draw the off-screen buffer to the screen + gc.drawImage(image, 0, 0); + + // Clean up + image.dispose(); + gcImage.dispose(); + } + + private void drawHighlights(GC gc, Point dim) { + int height = mHighlightHeight; + if (height <= 0) + return; + for (Range range : mHighlightExclusive) { + gc.setBackground(range.mColor); + int xStart = range.mXdim.x; + int width = range.mXdim.y; + gc.fillRectangle(xStart, range.mY - height - mScrollOffsetY, width, height); + } + + // Draw the inclusive lines a bit shorter + height -= 1; + if (height <= 0) + height = 1; + + // Highlight the inclusive ranges + gc.setForeground(mColorDarkGray); + gc.setBackground(mColorDarkGray); + for (Range range : mHighlightInclusive) { + int x1 = range.mXdim.x; + int x2 = range.mXdim.y; + boolean drawLeftEnd = false; + boolean drawRightEnd = false; + if (x1 >= LeftMargin) + drawLeftEnd = true; + else + x1 = LeftMargin; + if (x2 >= LeftMargin) + drawRightEnd = true; + else + x2 = dim.x - RightMargin; + int y1 = range.mY + rowHeight + 2 - mScrollOffsetY; + + // If the range is very narrow, then just draw a small + // rectangle. + if (x2 - x1 < MinInclusiveRange) { + int width = x2 - x1; + if (width < 2) + width = 2; + gc.fillRectangle(x1, y1, width, height); + continue; + } + if (drawLeftEnd) { + if (drawRightEnd) { + // Draw both ends + int[] points = { x1, y1, x1, y1 + height, x2, + y1 + height, x2, y1 }; + gc.drawPolyline(points); + } else { + // Draw the left end + int[] points = { x1, y1, x1, y1 + height, x2, + y1 + height }; + gc.drawPolyline(points); + } + } else { + if (drawRightEnd) { + // Draw the right end + int[] points = { x1, y1 + height, x2, y1 + height, x2, + y1 }; + gc.drawPolyline(points); + } else { + // Draw neither end, just the line + int[] points = { x1, y1 + height, x2, y1 + height }; + gc.drawPolyline(points); + } + } + + // Draw the arrowheads, if necessary + if (drawLeftEnd == false) { + int[] points = { x1 + 7, y1 + height - 4, x1, y1 + height, + x1 + 7, y1 + height + 4 }; + gc.fillPolygon(points); + } + if (drawRightEnd == false) { + int[] points = { x2 - 7, y1 + height - 4, x2, y1 + height, + x2 - 7, y1 + height + 4 }; + gc.fillPolygon(points); + } + } + } + + private boolean drawingSelection() { + return mGraphicsState == GraphicsState.Marking + || mGraphicsState == GraphicsState.Animating; + } + + private void drawSelection(Display display, GC gc) { + Point dim = getSize(); + gc.setForeground(mColorGray); + gc.drawLine(mMouseMarkStartX, 0, mMouseMarkStartX, dim.y); + gc.setBackground(mColorZoomSelection); + int width; + int mouseX = (mGraphicsState == GraphicsState.Animating) ? mMouseMarkEndX : mMouse.x; + int x; + if (mMouseMarkStartX < mouseX) { + x = mMouseMarkStartX; + width = mouseX - mMouseMarkStartX; + } else { + x = mouseX; + width = mMouseMarkStartX - mouseX; + } + gc.fillRectangle(x, 0, width, dim.y); + } + + private void computeStrips() { + double minVal = mScaleInfo.getMinVal(); + double maxVal = mScaleInfo.getMaxVal(); + + // Allocate space for the pixel data + Pixel[] pixels = new Pixel[mNumRows]; + for (int ii = 0; ii < mNumRows; ++ii) + pixels[ii] = new Pixel(); + + // Clear the per-block pixel data + for (int ii = 0; ii < mSegments.length; ++ii) { + mSegments[ii].mBlock.clearWeight(); + } + + mStripList.clear(); + mHighlightExclusive.clear(); + mHighlightInclusive.clear(); + MethodData callMethod = null; + long callStart = 0; + long callEnd = -1; + RowData callRowData = null; + int prevMethodStart = -1; + int prevMethodEnd = -1; + int prevCallStart = -1; + int prevCallEnd = -1; + if (mHighlightCall != null) { + int callPixelStart = -1; + int callPixelEnd = -1; + callStart = mHighlightCall.getStartTime(); + callEnd = mHighlightCall.getEndTime(); + callMethod = mHighlightCall.getMethodData(); + if (callStart >= minVal) + callPixelStart = mScaleInfo.valueToPixel(callStart); + if (callEnd <= maxVal) + callPixelEnd = mScaleInfo.valueToPixel(callEnd); + // System.out.printf("callStart,End %d,%d minVal,maxVal %f,%f + // callPixelStart,End %d,%d\n", + // callStart, callEnd, minVal, maxVal, callPixelStart, + // callPixelEnd); + int threadId = mHighlightCall.getThreadId(); + String threadName = mThreadLabels.get(threadId); + callRowData = mRowByName.get(threadName); + int y1 = callRowData.mRank * rowYSpace + rowYMarginHalf; + Color color = callMethod.getColor(); + mHighlightInclusive.add(new Range(callPixelStart + LeftMargin, + callPixelEnd + LeftMargin, y1, color)); + } + for (Segment segment : mSegments) { + if (segment.mEndTime <= minVal) + continue; + if (segment.mStartTime >= maxVal) + continue; + + Block block = segment.mBlock; + + // Skip over blocks that were not assigned a color, including the + // top level block and others that have zero inclusive time. + Color color = block.getColor(); + if (color == null) + continue; + + double recordStart = Math.max(segment.mStartTime, minVal); + double recordEnd = Math.min(segment.mEndTime, maxVal); + if (recordStart == recordEnd) + continue; + int pixelStart = mScaleInfo.valueToPixel(recordStart); + int pixelEnd = mScaleInfo.valueToPixel(recordEnd); + int width = pixelEnd - pixelStart; + boolean isContextSwitch = segment.mIsContextSwitch; + + RowData rd = segment.mRowData; + MethodData md = block.getMethodData(); + + // We will add the scroll offset later when we draw the strips + int y1 = rd.mRank * rowYSpace + rowYMarginHalf; + + // If we can't display any more rows, then quit + if (rd.mRank > mEndRow) + break; + + // System.out.printf("segment %s val: [%.1f, %.1f] frac [%f, %f] + // pixel: [%d, %d] pix.start %d weight %.2f %s\n", + // block.getName(), recordStart, recordEnd, + // scaleInfo.valueToPixelFraction(recordStart), + // scaleInfo.valueToPixelFraction(recordEnd), + // pixelStart, pixelEnd, pixels[rd.rank].start, + // pixels[rd.rank].maxWeight, + // pixels[rd.rank].segment != null + // ? pixels[rd.rank].segment.block.getName() + // : "null"); + + if (mHighlightMethodData != null) { + if (mHighlightMethodData == md) { + if (prevMethodStart != pixelStart || prevMethodEnd != pixelEnd) { + prevMethodStart = pixelStart; + prevMethodEnd = pixelEnd; + int rangeWidth = width; + if (rangeWidth == 0) + rangeWidth = 1; + mHighlightExclusive.add(new Range(pixelStart + + LeftMargin, rangeWidth, y1, color)); + callStart = block.getStartTime(); + int callPixelStart = -1; + if (callStart >= minVal) + callPixelStart = mScaleInfo.valueToPixel(callStart); + int callPixelEnd = -1; + callEnd = block.getEndTime(); + if (callEnd <= maxVal) + callPixelEnd = mScaleInfo.valueToPixel(callEnd); + if (prevCallStart != callPixelStart || prevCallEnd != callPixelEnd) { + prevCallStart = callPixelStart; + prevCallEnd = callPixelEnd; + mHighlightInclusive.add(new Range( + callPixelStart + LeftMargin, + callPixelEnd + LeftMargin, y1, color)); + } + } + } else if (mFadeColors) { + color = md.getFadedColor(); + } + } else if (mHighlightCall != null) { + if (segment.mStartTime >= callStart + && segment.mEndTime <= callEnd && callMethod == md + && callRowData == rd) { + if (prevMethodStart != pixelStart || prevMethodEnd != pixelEnd) { + prevMethodStart = pixelStart; + prevMethodEnd = pixelEnd; + int rangeWidth = width; + if (rangeWidth == 0) + rangeWidth = 1; + mHighlightExclusive.add(new Range(pixelStart + + LeftMargin, rangeWidth, y1, color)); + } + } else if (mFadeColors) { + color = md.getFadedColor(); + } + } + + // Cases: + // 1. This segment starts on a different pixel than the + // previous segment started on. In this case, emit + // the pixel strip, if any, and: + // A. If the width is 0, then add this segment's + // weight to the Pixel. + // B. If the width > 0, then emit a strip for this + // segment (no partial Pixel data). + // + // 2. Otherwise (the new segment starts on the same + // pixel as the previous segment): add its "weight" + // to the current pixel, and: + // A. If the new segment has width 1, + // then emit the pixel strip and then + // add the segment's weight to the pixel. + // B. If the new segment has width > 1, + // then emit the pixel strip, and emit the rest + // of the strip for this segment (no partial Pixel + // data). + + Pixel pix = pixels[rd.mRank]; + if (pix.mStart != pixelStart) { + if (pix.mSegment != null) { + // Emit the pixel strip. This also clears the pixel. + emitPixelStrip(rd, y1, pix); + } + + if (width == 0) { + // Compute the "weight" of this segment for the first + // pixel. For a pixel N, the "weight" of a segment is + // how much of the region [N - 0.5, N + 0.5] is covered + // by the segment. + double weight = computeWeight(recordStart, recordEnd, + isContextSwitch, pixelStart); + weight = block.addWeight(pixelStart, rd.mRank, weight); + if (weight > pix.mMaxWeight) { + pix.setFields(pixelStart, weight, segment, color, + rd); + } + } else { + int x1 = pixelStart + LeftMargin; + Strip strip = new Strip( + x1, isContextSwitch ? y1 + rowHeight - 1 : y1, + width, isContextSwitch ? 1 : rowHeight, + rd, segment, color); + mStripList.add(strip); + } + } else { + double weight = computeWeight(recordStart, recordEnd, + isContextSwitch, pixelStart); + weight = block.addWeight(pixelStart, rd.mRank, weight); + if (weight > pix.mMaxWeight) { + pix.setFields(pixelStart, weight, segment, color, rd); + } + if (width == 1) { + // Emit the pixel strip. This also clears the pixel. + emitPixelStrip(rd, y1, pix); + + // Compute the weight for the next pixel + pixelStart += 1; + weight = computeWeight(recordStart, recordEnd, + isContextSwitch, pixelStart); + weight = block.addWeight(pixelStart, rd.mRank, weight); + pix.setFields(pixelStart, weight, segment, color, rd); + } else if (width > 1) { + // Emit the pixel strip. This also clears the pixel. + emitPixelStrip(rd, y1, pix); + + // Emit a strip for the rest of the segment. + pixelStart += 1; + width -= 1; + int x1 = pixelStart + LeftMargin; + Strip strip = new Strip( + x1, isContextSwitch ? y1 + rowHeight - 1 : y1, + width, isContextSwitch ? 1 : rowHeight, + rd,segment, color); + mStripList.add(strip); + } + } + } + + // Emit the last pixels of each row, if any + for (int ii = 0; ii < mNumRows; ++ii) { + Pixel pix = pixels[ii]; + if (pix.mSegment != null) { + RowData rd = pix.mRowData; + int y1 = rd.mRank * rowYSpace + rowYMarginHalf; + // Emit the pixel strip. This also clears the pixel. + emitPixelStrip(rd, y1, pix); + } + } + + if (false) { + System.out.printf("computeStrips()\n"); + for (Strip strip : mStripList) { + System.out.printf("%3d, %3d width %3d height %d %s\n", + strip.mX, strip.mY, strip.mWidth, strip.mHeight, + strip.mSegment.mBlock.getName()); + } + } + } + + private double computeWeight(double start, double end, + boolean isContextSwitch, int pixel) { + if (isContextSwitch) { + return 0; + } + double pixelStartFraction = mScaleInfo.valueToPixelFraction(start); + double pixelEndFraction = mScaleInfo.valueToPixelFraction(end); + double leftEndPoint = Math.max(pixelStartFraction, pixel - 0.5); + double rightEndPoint = Math.min(pixelEndFraction, pixel + 0.5); + double weight = rightEndPoint - leftEndPoint; + return weight; + } + + private void emitPixelStrip(RowData rd, int y, Pixel pixel) { + Strip strip; + + if (pixel.mSegment == null) + return; + + int x = pixel.mStart + LeftMargin; + // Compute the percentage of the row height proportional to + // the weight of this pixel. But don't let the proportion + // exceed 3/4 of the row height so that we can easily see + // if a given time range includes more than one method. + int height = (int) (pixel.mMaxWeight * rowHeight * 0.75); + if (height < mMinStripHeight) + height = mMinStripHeight; + int remainder = rowHeight - height; + if (remainder > 0) { + strip = new Strip(x, y, 1, remainder, rd, pixel.mSegment, + mFadeColors ? mColorGray : mColorBlack); + mStripList.add(strip); + // System.out.printf("emitPixel (%d, %d) height %d black\n", + // x, y, remainder); + } + strip = new Strip(x, y + remainder, 1, height, rd, pixel.mSegment, + pixel.mColor); + mStripList.add(strip); + // System.out.printf("emitPixel (%d, %d) height %d %s\n", + // x, y + remainder, height, pixel.segment.block.getName()); + pixel.mSegment = null; + pixel.mMaxWeight = 0.0; + } + + private void mouseMove(MouseEvent me) { + if (false) { + if (mHighlightMethodData != null) { + mHighlightMethodData = null; + // Force a recomputation of the strip colors + mCachedEndRow = -1; + } + } + Point dim = mSurface.getSize(); + int x = me.x; + if (x < LeftMargin) + x = LeftMargin; + if (x > dim.x - RightMargin) + x = dim.x - RightMargin; + mMouse.x = x; + mMouse.y = me.y; + mTimescale.setVbarPosition(x); + if (mGraphicsState == GraphicsState.Marking) { + mTimescale.setMarkEnd(x); + } + + if (mGraphicsState == GraphicsState.Normal) { + // Set the cursor to the normal state. + mSurface.setCursor(mNormalCursor); + } else if (mGraphicsState == GraphicsState.Marking) { + // Make the cursor point in the direction of the sweep + if (mMouse.x >= mMouseMarkStartX) + mSurface.setCursor(mIncreasingCursor); + else + mSurface.setCursor(mDecreasingCursor); + } + int rownum = (mMouse.y + mScrollOffsetY) / rowYSpace; + if (me.y < 0 || me.y >= dim.y) { + rownum = -1; + } + if (mMouseRow != rownum) { + mMouseRow = rownum; + mLabels.redraw(); + } + redraw(); + } + + private void mouseDown(MouseEvent me) { + Point dim = mSurface.getSize(); + int x = me.x; + if (x < LeftMargin) + x = LeftMargin; + if (x > dim.x - RightMargin) + x = dim.x - RightMargin; + mMouseMarkStartX = x; + mGraphicsState = GraphicsState.Marking; + mSurface.setCursor(mIncreasingCursor); + mTimescale.setMarkStart(mMouseMarkStartX); + mTimescale.setMarkEnd(mMouseMarkStartX); + redraw(); + } + + private void mouseUp(MouseEvent me) { + mSurface.setCursor(mNormalCursor); + if (mGraphicsState != GraphicsState.Marking) { + mGraphicsState = GraphicsState.Normal; + return; + } + mGraphicsState = GraphicsState.Animating; + Point dim = mSurface.getSize(); + + // If the user released the mouse outside the drawing area then + // cancel the zoom. + if (me.y <= 0 || me.y >= dim.y) { + mGraphicsState = GraphicsState.Normal; + redraw(); + return; + } + + int x = me.x; + if (x < LeftMargin) + x = LeftMargin; + if (x > dim.x - RightMargin) + x = dim.x - RightMargin; + mMouseMarkEndX = x; + + // If the user clicked and released the mouse at the same point + // (+/- a pixel or two) then cancel the zoom (but select the + // method). + int dist = mMouseMarkEndX - mMouseMarkStartX; + if (dist < 0) + dist = -dist; + if (dist <= 2) { + mGraphicsState = GraphicsState.Normal; + + // Select the method underneath the mouse + mMouseSelect.x = mMouseMarkStartX; + mMouseSelect.y = me.y; + redraw(); + return; + } + + // Make mouseEndX be the higher end point + if (mMouseMarkEndX < mMouseMarkStartX) { + int temp = mMouseMarkEndX; + mMouseMarkEndX = mMouseMarkStartX; + mMouseMarkStartX = temp; + } + + // If the zoom area is the whole window (or nearly the whole + // window) then cancel the zoom. + if (mMouseMarkStartX <= LeftMargin + MinZoomPixelMargin + && mMouseMarkEndX >= dim.x - RightMargin - MinZoomPixelMargin) { + mGraphicsState = GraphicsState.Normal; + redraw(); + return; + } + + // Compute some variables needed for zooming. + // It's probably easiest to explain by an example. There + // are two scales (or dimensions) involved: one for the pixels + // and one for the values (microseconds). To keep the example + // simple, suppose we have pixels in the range [0,16] and + // values in the range [100, 260], and suppose the user + // selects a zoom window from pixel 4 to pixel 8. + // + // usec: 100 140 180 260 + // |-------|ZZZZZZZ|---------------| + // pixel: 0 4 8 16 + // + // I've drawn the pixels starting at zero for simplicity, but + // in fact the drawable area is offset from the left margin + // by the value of "LeftMargin". + // + // The "pixels-per-range" (ppr) in this case is 0.1 (a tenth of + // a pixel per usec). What we want is to redraw the screen in + // several steps, each time increasing the zoom window until the + // zoom window fills the screen. For simplicity, assume that + // we want to zoom in four equal steps. Then the snapshots + // of the screen at each step would look something like this: + // + // usec: 100 140 180 260 + // |-------|ZZZZZZZ|---------------| + // pixel: 0 4 8 16 + // + // usec: ? 140 180 ? + // |-----|ZZZZZZZZZZZZZ|-----------| + // pixel: 0 3 10 16 + // + // usec: ? 140 180 ? + // |---|ZZZZZZZZZZZZZZZZZZZ|-------| + // pixel: 0 2 12 16 + // + // usec: ?140 180 ? + // |-|ZZZZZZZZZZZZZZZZZZZZZZZZZ|---| + // pixel: 0 1 14 16 + // + // usec: 140 180 + // |ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ| + // pixel: 0 16 + // + // The problem is how to compute the endpoints (denoted by ?) + // for each step. This is a little tricky. We first need to + // compute the "fixed point": this is the point in the selection + // that doesn't move left or right. Then we can recompute the + // "ppr" (pixels per range) at each step and then find the + // endpoints. The computation of the end points is done + // in animateZoom(). This method computes the fixed point + // and some other variables needed in animateZoom(). + + double minVal = mScaleInfo.getMinVal(); + double maxVal = mScaleInfo.getMaxVal(); + double ppr = mScaleInfo.getPixelsPerRange(); + mZoomMin = minVal + ((mMouseMarkStartX - LeftMargin) / ppr); + mZoomMax = minVal + ((mMouseMarkEndX - LeftMargin) / ppr); + + // Clamp the min and max values to the actual data min and max + if (mZoomMin < mMinDataVal) + mZoomMin = mMinDataVal; + if (mZoomMax > mMaxDataVal) + mZoomMax = mMaxDataVal; + + // Snap the min and max points to the grid determined by the + // TickScaler + // before we zoom. + int xdim = dim.x - TotalXMargin; + TickScaler scaler = new TickScaler(mZoomMin, mZoomMax, xdim, + PixelsPerTick); + scaler.computeTicks(false); + mZoomMin = scaler.getMinVal(); + mZoomMax = scaler.getMaxVal(); + + // Also snap the mouse points (in pixel space) to be consistent with + // zoomMin and zoomMax (in value space). + mMouseMarkStartX = (int) ((mZoomMin - minVal) * ppr + LeftMargin); + mMouseMarkEndX = (int) ((mZoomMax - minVal) * ppr + LeftMargin); + mTimescale.setMarkStart(mMouseMarkStartX); + mTimescale.setMarkEnd(mMouseMarkEndX); + + // Compute the mouse selection end point distances + mMouseEndDistance = dim.x - RightMargin - mMouseMarkEndX; + mMouseStartDistance = mMouseMarkStartX - LeftMargin; + mZoomMouseStart = mMouseMarkStartX; + mZoomMouseEnd = mMouseMarkEndX; + mZoomStep = 0; + + // Compute the fixed point in both value space and pixel space. + mMin2ZoomMin = mZoomMin - minVal; + mZoomMax2Max = maxVal - mZoomMax; + mZoomFixed = mZoomMin + (mZoomMax - mZoomMin) * mMin2ZoomMin + / (mMin2ZoomMin + mZoomMax2Max); + mZoomFixedPixel = (mZoomFixed - minVal) * ppr + LeftMargin; + mFixedPixelStartDistance = mZoomFixedPixel - LeftMargin; + mFixedPixelEndDistance = dim.x - RightMargin - mZoomFixedPixel; + + mZoomMin2Fixed = mZoomFixed - mZoomMin; + mFixed2ZoomMax = mZoomMax - mZoomFixed; + + getDisplay().timerExec(ZOOM_TIMER_INTERVAL, mZoomAnimator); + redraw(); + update(); + } + + private void mouseScrolled(MouseEvent me) { + mGraphicsState = GraphicsState.Scrolling; + double tMin = mScaleInfo.getMinVal(); + double tMax = mScaleInfo.getMaxVal(); + double zoomFactor = 2; + double tMinRef = mLimitMinVal; + double tMaxRef = mLimitMaxVal; + double t; // the fixed point + double tMinNew; + double tMaxNew; + if (me.count > 0) { + // we zoom in + Point dim = mSurface.getSize(); + int x = me.x; + if (x < LeftMargin) + x = LeftMargin; + if (x > dim.x - RightMargin) + x = dim.x - RightMargin; + double ppr = mScaleInfo.getPixelsPerRange(); + t = tMin + ((x - LeftMargin) / ppr); + tMinNew = Math.max(tMinRef, t - (t - tMin) / zoomFactor); + tMaxNew = Math.min(tMaxRef, t + (tMax - t) / zoomFactor); + } else { + // we zoom out + double factor = (tMax - tMin) / (tMaxRef - tMinRef); + if (factor < 1) { + t = (factor * tMinRef - tMin) / (factor - 1); + tMinNew = Math.max(tMinRef, t - zoomFactor * (t - tMin)); + tMaxNew = Math.min(tMaxRef, t + zoomFactor * (tMax - t)); + } else { + return; + } + } + mScaleInfo.setMinVal(tMinNew); + mScaleInfo.setMaxVal(tMaxNew); + mSurface.redraw(); + } + + // No defined behavior yet for double-click. + private void mouseDoubleClick(MouseEvent me) { + } + + public void startScaling(int mouseX) { + Point dim = mSurface.getSize(); + int x = mouseX; + if (x < LeftMargin) + x = LeftMargin; + if (x > dim.x - RightMargin) + x = dim.x - RightMargin; + mMouseMarkStartX = x; + mGraphicsState = GraphicsState.Scaling; + mScalePixelsPerRange = mScaleInfo.getPixelsPerRange(); + mScaleMinVal = mScaleInfo.getMinVal(); + mScaleMaxVal = mScaleInfo.getMaxVal(); + } + + public void stopScaling(int mouseX) { + mGraphicsState = GraphicsState.Normal; + } + + private void animateHighlight() { + mHighlightStep += 1; + if (mHighlightStep >= HIGHLIGHT_STEPS) { + mFadeColors = false; + mHighlightStep = 0; + // Force a recomputation of the strip colors + mCachedEndRow = -1; + } else { + mFadeColors = true; + mShowHighlightName = true; + mHighlightHeight = highlightHeights[mHighlightStep]; + getDisplay().timerExec(HIGHLIGHT_TIMER_INTERVAL, mHighlightAnimator); + } + redraw(); + } + + private void clearHighlights() { + // System.out.printf("clearHighlights()\n"); + mShowHighlightName = false; + mHighlightHeight = 0; + mHighlightMethodData = null; + mHighlightCall = null; + mFadeColors = false; + mHighlightStep = 0; + // Force a recomputation of the strip colors + mCachedEndRow = -1; + redraw(); + } + + private void animateZoom() { + mZoomStep += 1; + if (mZoomStep > ZOOM_STEPS) { + mGraphicsState = GraphicsState.Normal; + // Force a normal recomputation + mCachedMinVal = mScaleInfo.getMinVal() + 1; + } else if (mZoomStep == ZOOM_STEPS) { + mScaleInfo.setMinVal(mZoomMin); + mScaleInfo.setMaxVal(mZoomMax); + mMouseMarkStartX = LeftMargin; + Point dim = getSize(); + mMouseMarkEndX = dim.x - RightMargin; + mTimescale.setMarkStart(mMouseMarkStartX); + mTimescale.setMarkEnd(mMouseMarkEndX); + getDisplay().timerExec(ZOOM_TIMER_INTERVAL, mZoomAnimator); + } else { + // Zoom in slowly at first, then speed up, then slow down. + // The zoom fractions are precomputed to save time. + double fraction = mZoomFractions[mZoomStep]; + mMouseMarkStartX = (int) (mZoomMouseStart - fraction * mMouseStartDistance); + mMouseMarkEndX = (int) (mZoomMouseEnd + fraction * mMouseEndDistance); + mTimescale.setMarkStart(mMouseMarkStartX); + mTimescale.setMarkEnd(mMouseMarkEndX); + + // Compute the new pixels-per-range. Avoid division by zero. + double ppr; + if (mZoomMin2Fixed >= mFixed2ZoomMax) + ppr = (mZoomFixedPixel - mMouseMarkStartX) / mZoomMin2Fixed; + else + ppr = (mMouseMarkEndX - mZoomFixedPixel) / mFixed2ZoomMax; + double newMin = mZoomFixed - mFixedPixelStartDistance / ppr; + double newMax = mZoomFixed + mFixedPixelEndDistance / ppr; + mScaleInfo.setMinVal(newMin); + mScaleInfo.setMaxVal(newMax); + + getDisplay().timerExec(ZOOM_TIMER_INTERVAL, mZoomAnimator); + } + redraw(); + } + + private static final int TotalXMargin = LeftMargin + RightMargin; + private static final int yMargin = 1; // blank space on top + // The minimum margin on each side of the zoom window, in pixels. + private static final int MinZoomPixelMargin = 10; + private GraphicsState mGraphicsState = GraphicsState.Normal; + private Point mMouse = new Point(LeftMargin, 0); + private int mMouseMarkStartX; + private int mMouseMarkEndX; + private boolean mDebug = false; + private ArrayList mStripList = new ArrayList(); + private ArrayList mHighlightExclusive = new ArrayList(); + private ArrayList mHighlightInclusive = new ArrayList(); + private int mMinStripHeight = 2; + private double mCachedMinVal; + private double mCachedMaxVal; + private int mCachedStartRow; + private int mCachedEndRow; + private double mScalePixelsPerRange; + private double mScaleMinVal; + private double mScaleMaxVal; + private double mLimitMinVal; + private double mLimitMaxVal; + private double mMinDataVal; + private double mMaxDataVal; + private Cursor mNormalCursor; + private Cursor mIncreasingCursor; + private Cursor mDecreasingCursor; + private static final int ZOOM_TIMER_INTERVAL = 10; + private static final int HIGHLIGHT_TIMER_INTERVAL = 50; + private static final int ZOOM_STEPS = 8; // must be even + private int mHighlightHeight = 4; + private final int[] highlightHeights = { 0, 2, 4, 5, 6, 5, 4, 2, 4, 5, + 6 }; + private final int HIGHLIGHT_STEPS = highlightHeights.length; + private boolean mFadeColors; + private boolean mShowHighlightName; + private double[] mZoomFractions; + private int mZoomStep; + private int mZoomMouseStart; + private int mZoomMouseEnd; + private int mMouseStartDistance; + private int mMouseEndDistance; + private Point mMouseSelect = new Point(0, 0); + private double mZoomFixed; + private double mZoomFixedPixel; + private double mFixedPixelStartDistance; + private double mFixedPixelEndDistance; + private double mZoomMin2Fixed; + private double mMin2ZoomMin; + private double mFixed2ZoomMax; + private double mZoomMax2Max; + private double mZoomMin; + private double mZoomMax; + private Runnable mZoomAnimator; + private Runnable mHighlightAnimator; + private int mHighlightStep; + } + + private int computeVisibleRows(int ydim) { + // If we resize, then move the bottom row down. Don't allow the scroll + // to waste space at the bottom. + int offsetY = mScrollOffsetY; + int spaceNeeded = mNumRows * rowYSpace; + if (offsetY + ydim > spaceNeeded) { + offsetY = spaceNeeded - ydim; + if (offsetY < 0) { + offsetY = 0; + } + } + mStartRow = offsetY / rowYSpace; + mEndRow = (offsetY + ydim) / rowYSpace; + if (mEndRow >= mNumRows) { + mEndRow = mNumRows - 1; + } + + return offsetY; + } + + private void startHighlighting() { + // System.out.printf("startHighlighting()\n"); + mSurface.mHighlightStep = 0; + mSurface.mFadeColors = true; + // Force a recomputation of the color strips + mSurface.mCachedEndRow = -1; + getDisplay().timerExec(0, mSurface.mHighlightAnimator); + } + + private static class RowData { + RowData(Row row) { + mName = row.getName(); + mStack = new ArrayList(); + } + + public void push(Block block) { + mStack.add(block); + } + + public Block top() { + if (mStack.size() == 0) + return null; + return mStack.get(mStack.size() - 1); + } + + public void pop() { + if (mStack.size() == 0) + return; + mStack.remove(mStack.size() - 1); + } + + private String mName; + private int mRank; + private long mElapsed; + private long mEndTime; + private ArrayList mStack; + } + + private static class Segment { + Segment(RowData rowData, Block block, long startTime, long endTime) { + mRowData = rowData; + if (block.isContextSwitch()) { + mBlock = block.getParentBlock(); + mIsContextSwitch = true; + } else { + mBlock = block; + } + mStartTime = startTime; + mEndTime = endTime; + } + + private RowData mRowData; + private Block mBlock; + private long mStartTime; + private long mEndTime; + private boolean mIsContextSwitch; + } + + private static class Strip { + Strip(int x, int y, int width, int height, RowData rowData, + Segment segment, Color color) { + mX = x; + mY = y; + mWidth = width; + mHeight = height; + mRowData = rowData; + mSegment = segment; + mColor = color; + } + + int mX; + int mY; + int mWidth; + int mHeight; + RowData mRowData; + Segment mSegment; + Color mColor; + } + + private static class Pixel { + public void setFields(int start, double weight, Segment segment, + Color color, RowData rowData) { + mStart = start; + mMaxWeight = weight; + mSegment = segment; + mColor = color; + mRowData = rowData; + } + + int mStart = -2; // some value that won't match another pixel + double mMaxWeight; + Segment mSegment; + Color mColor; // we need the color here because it may be faded + RowData mRowData; + } + + private static class Range { + Range(int xStart, int width, int y, Color color) { + mXdim.x = xStart; + mXdim.y = width; + mY = y; + mColor = color; + } + + Point mXdim = new Point(0, 0); + int mY; + Color mColor; + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TraceAction.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TraceAction.java new file mode 100644 index 00000000..35270baf --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TraceAction.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2011 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.traceview; + +final class TraceAction { + public static final int ACTION_ENTER = 0; + public static final int ACTION_EXIT = 1; + public static final int ACTION_INCOMPLETE = 2; + + public final int mAction; + public final Call mCall; + + public TraceAction(int action, Call call) { + mAction = action; + mCall = call; + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TraceReader.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TraceReader.java new file mode 100644 index 00000000..759e0d69 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TraceReader.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2006 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.traceview; + +import java.util.ArrayList; +import java.util.HashMap; + +public abstract class TraceReader { + + private TraceUnits mTraceUnits; + + public TraceUnits getTraceUnits() { + if (mTraceUnits == null) + mTraceUnits = new TraceUnits(); + return mTraceUnits; + } + + public ArrayList getThreadTimeRecords() { + return null; + } + + public HashMap getThreadLabels() { + return null; + } + + public MethodData[] getMethods() { + return null; + } + + public ThreadData[] getThreads() { + return null; + } + + public long getTotalCpuTime() { + return 0; + } + + public long getTotalRealTime() { + return 0; + } + + public boolean haveCpuTime() { + return false; + } + + public boolean haveRealTime() { + return false; + } + + public HashMap getProperties() { + return null; + } + + public ProfileProvider getProfileProvider() { + return null; + } + + public TimeBase getPreferredTimeBase() { + return TimeBase.CPU_TIME; + } + + public String getClockSource() { + return null; + } +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TraceUnits.java b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TraceUnits.java new file mode 100644 index 00000000..60c5fca3 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/java/com/android/traceview/TraceUnits.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2006 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.traceview; + +import java.text.DecimalFormat; + +// This should be a singleton. +public class TraceUnits { + + private TimeScale mTimeScale = TimeScale.MicroSeconds; + private double mScale = 1.0; + DecimalFormat mFormatter = new DecimalFormat(); + + public double getScaledValue(long value) { + return value * mScale; + } + + public double getScaledValue(double value) { + return value * mScale; + } + + public String valueOf(long value) { + return valueOf((double) value); + } + + public String valueOf(double value) { + String pattern; + double scaled = value * mScale; + if ((int) scaled == scaled) + pattern = "###,###"; + else + pattern = "###,###.###"; + mFormatter.applyPattern(pattern); + return mFormatter.format(scaled); + } + + public String labelledString(double value) { + String units = label(); + String num = valueOf(value); + return String.format("%s: %s", units, num); + } + + public String labelledString(long value) { + return labelledString((double) value); + } + + public String label() { + if (mScale == 1.0) + return "usec"; + if (mScale == 0.001) + return "msec"; + if (mScale == 0.000001) + return "sec"; + return null; + } + + public void setTimeScale(TimeScale val) { + mTimeScale = val; + switch (val) { + case Seconds: + mScale = 0.000001; + break; + case MilliSeconds: + mScale = 0.001; + break; + case MicroSeconds: + mScale = 1.0; + break; + } + } + + public TimeScale getTimeScale() { + return mTimeScale; + } + + public enum TimeScale { + Seconds, MilliSeconds, MicroSeconds + }; +} diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/resources/icons/sort_down.png b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/resources/icons/sort_down.png new file mode 100644 index 0000000000000000000000000000000000000000..2d4ccc1add771d4d8f9d941013bcc6c5746b3207 GIT binary patch literal 102 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4>3X_2hDb;zCrB6^;Mn@po>AQ3 zkVw;&he14H&1(c^CzVu+bUUg&Hj;H!oNK{w*Ois2_5PXVK#dHZu6{1-oD!Mf48F;!lhDb;zCrE@mu)koc(8DAV zSTTd^!KVVY0OmH$0Otc)30#GmO_3bI?2j)`W@89@%*rG(>wOkbFN3G6pUXO@geCy* CXdEH{ literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/resources/icons/traceview-128.png b/andmore-swt/org.eclipse.andmore.traceviewuilib/src/main/resources/icons/traceview-128.png new file mode 100644 index 0000000000000000000000000000000000000000..5b4eff1be6b4ffd36b0229a947e3f885e41bc529 GIT binary patch literal 17131 zcmbTdWmp}-vp0AS9N^#_EI0>GaCdiicL?ro!Gk+VaF^ij?gWCny99zua9jTOzW3ez z@a}%td7hb`nyIQ^b#-<3^h`~pvZ53!5&;qb06>+I7FT^Q1OKyw;opz_f+nNy1-_ev zwws!xg`1~|i#b5V%+b^wDr0YAX|8H+V&?7q+ngT&fVHqz*LKrZkmvpEXwPW!A09?8 zd#8740Dxb}%gN-kt+^Z2)ZEhAL4f?cy_+0rZ6-jj#i78g;3Q^lWi9RFVy@<+sQ%f< z_A`$exsV`~-;4KMfxWq#3DnEp&cT(}OMv{p^zy!+|K~CjIrP6++-wEN|Jx{S1!bt1 zql-C|gOQoxGczYMl#_*#g^ityi;Eu0%FM#d#LUXX!pgwH#mmOZ%ghY@UmN*5nv0nQ zud2A@|H68w1jw!2+?;rsm^?i_89mt;9bGJ$Sa^7N{-cAHmEoO(!PVQr&BTkr!Ik2F zG>DtKes-~Ta|-KpQ9;a9smkA_U=R~R2I5J;K~q9J0h`30~Wn zBX|^owx$8*M$BxfAvN>Aplk$=$!qHSE}K^x;#k85bt(U&3TALoOOVtUUy9bSa%L*1KOF9E(M!xxq$EO6FYdhTxI zZ7&H%=}Wdr{nnp{2Qhl!|8sc3c^(CsvbZT6X$wN}^FnzdUaXrK(tdfsPtIEcBYw zWM_N(TSH&kTYdjLtB)K}n9b(y&bwZq#FKZYe4vPl{F?YHWXh&-z%y2|Uca3EEGNs_ z_sc3pBk9I{yc&Y9sS~zo;GJ*jrWztt7UQbk>_uhIlOmY#?F zhn}?`8W%#yFjS_R3?0WbMq*Ghd`~H)(g21x+00kF7TuIc49*L0>u;%_9~hUOi=yiv=&I=qn$0NR zZuJ>mO?k7>EZU>)OGH0V1KzmWxE_=o^2P;wk;TZA8!qB+-37ExC?I=-tj@ zgAz7_gq89@s@#K@EgUNrrN1&`x`rmj@vInjE=_`!BSo@)+fOTpyR=>n+`q1b3q9O^ z-5lt*(~$ByICv5rF?}MkS4=1*T)Sxgk27An6c;v=Z{$IeLnn8Mq|n!RA$GTg5v)2= z#^kFM$WG+?Y=le9g(i8{>y(F-F(1rFkI}Cdu(^gJu%t|ELc6_dKTtU`18!((T^JfL z-LOJJilX(P7VQp=(C_n5y{X*l&A=`os*4}sFHsx{X6~oKRO=mqG->sJ2Q7lm-oze4 z6iKZ>NZ*>>{zRwJ{KiY`ox-y|6^I-r$xGe2he((T#{cb{ly?Ml@pK+P^1unf3tPy$ z@Y@;Ja9_yCVEp~h;O1z@hynjo7emTuQQG>D^*@+Pc6pvz0V%N_-YFaIsS2Pv9eyfr zNpCMPYAVTMZg7}*y#-+9T)A{^V2mbP5tKZ-KM(_-V+n3$5O1Wu6`6)@6-AoSL2!}y z051hd&PoUh`pIbK%0Y>Qs%lM1iF`i(a5_7E!Hpb|bhqAsQF%dxnuA3rsw9GHt*9cvL z?D{@3f;nvU%N40NWfhKCFyq3%nROM=2{=Cdq1JP9YDRnVKB>Z{w)Y*l#i97aRL_Ajk#(oA`qUDbDdNVj zCwwh;?F=pV5`PnBrRSB@Eivo^E_Vy$0hU6|-%m&He5~LDt{6D7Z*a-p9w*Vd?dK5( znQ)^+_aA&owEN>HeEO&0U<^_$>Z9q`MG9AI_KVS;_YgSwUzsP&{9s3JVMuabBbS)!tvLxbmKCnjEoesFO$>J$W&YR_m@I|{nfE9BPEn3AGr3X*k~~i1 zovcN%waFj2T?z{1U=&6~=n~VCNpFVMHaPZp%@@g4{_5N;=O4JIB5%2XD+L`F#`^la zJjZ?R!CA9@YKGi*zcxTjA5uCgxx9WON9W+<(RNf2?!aN7emf-?ari)>@!?i*KA46J z_QZGY>tn}SHM^at(I<)xJ+CS^YPvwV12c(4?N3=kURG~+w@$$gfUlQO` zhQvSLym3slUJD&;-0nJR6iHJ;o3Y8aiF3xi#t_}skrxp0Hl+MG61auEZkw0=K@SV= z0V@1AYokvN1p2CNF=osZLPuE43=P;92|hUHp37kd@toUn8MIF4cgjtq(z}A-t%^^# zqc~SJcBceE&)6fkD{DmRMa36f$V+06MH`kBP|=`d&qqPD4uqm3SF@H@t4GBq;4ndY$-e~!nFE?R5tv>YTp z5zg8bZoK*N4WLpE*|-LcH@}a5+y_xLYX{5MrOllESFet;A1a0F|HZxr?I2PfBewAO zry4s^N7y6tIR9J=7W<;#@+5TZbTH15x>QZ3tfqW4xm)|qNtOG`&yTqy3=DvTfu>TK z#0vD$BBHBB^aCvgKLri=-Lqw+jXhdV39_eRO?g=D%YH=pSKwU|$>MpD>nr>`UMJrn zsEgYKi{P<*vDB}Sn&p&eeDOfvz|8&|mEDN3G>-O;RS8UFBZRhMkEfk1zpZE)1v5^|GDfiP(VT zBE{Yn%Yfe@zn#s@of|28`w|ivjEcmM&^x{sU!Fa=mv3@g0+aEiFk%$8xu+%MAV|K< z17%#z9ip5zX{i6sYIXyGTV+4P`X6{7vc=*~vwoh(&**Eqn-k&}-$}WhdO0j2zD{kU zult~21nvPYhu>!7Z@P(+I+@6>Tw|U~BBh2_ytLgn!u(eGTMky?`5&0ad9l&M=(u6) z9YjecRUaAkKK4wNXrK*%~KX z3^eVKIf;cpz|4-+_pK(R^(bfrWhJEauQW(2%MI7g`{Gs-ist_)CIkBCvz1KIpX0x0 zVLZHnv+_!~p~vmGj~F_6aNUYBy6BXEKn{2MxYB{#o@3WH{-z#UiN2CBVMF$mHeTQI zbGb)m=bs`LD?vZ}ovxin~U=7b(RJ-h8bxAoD_BMp_;EVt#f|ZJrFm~5$3=5< zF>0_+IRT>__bKI6eQOagKan(eBy>l)`r^QD22g1?CTy;7#d%l=622Dh1=!uUzCUk6OIoNY~s^rc{FJK}4 zQXQD}Y+9%Vi^)DO|ku9fYzR&Z$&RnE0ucA5b)~Zov z_z;N=QYwaBDpt)>!RL_1ugp|Sm@$3`*=NW4Qb_<&m$$OTJweSQCVoAMuCl*aCpoQ= zk1=~=vgQ%U)=tNZc0jCP)Jhx4K_Ic%JDlOm3jZLm*f*8bc_a9e9V21xUP#lczYgc8 zXX3YCXg_YJ2Gf1bCnTE?obH-2FnlCG3ilz0VJi+{E9Vs;?C%>lZ7%Ed`vYF(o4n(Q zL~Ivjgm(`{#@OeIihtr4Ym*GbJqYMv+9_Ok(!1QR?FoT(i1xsuu56n^7{kwhJHJpK zUr&lXO}QWbG>pd4o&SW>Qm9t4mX5#lb;QIPoa@+wwGec4u4OKm9&c&Sy-4_FOKRLm zQf&6E9TA%FRPFWUHm30y*+@KAXtZ94=%TbB_PmFL-Quh*vcXf;ftBot8e02ZN`gBh z=%iN-QMg9+Sw3R=Pty$@nZJ3M{XF~o;p>SWt(hxffDWB)%`w!O-b?DiPJh**3$KJy4W|Dq{73o_Q?XBqOT39&@j#~89$a$}g=-kyrI zrAjn>$&I~cF!B3Rjn}ot46c6N?Z0ZXpuVkw17&79j&2qngQ(KptaZ$~5C_%oulE5~ z9Qvsl4!v0MF@1a51a;mNg@^8H_xU|a7knT^{J5kUes20b2_6+yU0wZzA<>+IXhD^M z@<&z9@S+nhVD?%a%%7t4sR}-rwX4g+j<7(EUSDHw*O&QfrOLa?;C@x`I7Q-%Em!Jd zv055XOp5%UnLf#;1hS5cc@<)NGiq;;fSihV8U&b@j@#5fUt?7_$iw~hjtrgT%fca7 zT`jCsOaRIQEc>Rs?K11hh)3oY5mDUWS|DJ&mo$dxQT~Z}5xO3<2ni7ijH9tk=nK{L zazrJv^PbARo)WnBmUc9G2_{w|;+>dOSVYQH$8Jxk+n(BhP=@Jf`(0u0MSl_+PBI1Z z18rxefxZ6gr8mdGbjKa(-N7T+KihS$DX)>)WbLO+8?h{b4QZXm5L2ST>~q8rL*$!*S79YdYKw5tZlvRpk2AhQMQI;}uhSfB84l zjfCUkD@On|pzQ-e4EG;Jk-*|}_NyPc&yw|IL?XGV*+<@o8vUEg_yb=f#AB6BQ@Fyv zX9V{EJp@`Y>)?>^?L?`iMkv5E1ik6TiZzostko^IO6gp_*Y%aF(9D)cTM7Wn(^Ghi zXRd61YZJT&7U_U9Tz>BWeoRGx$4J;|d{s}H*zvzEBDZyME|+h%Nt-mE3}JW%fWj&D zv-SuUS1iwhW%81fq#A3yNo^7LzyP68*b;{w2OSyt)ae17*ulI~k%Di*#PPAY5;a(| zBoCw!d?Wgz5Nw-SH9g%o%w6JP*K^gcaskA=!dsW^M>kGClj<0qi48zjoW~B#1l&b5 zwl$V|r1CLI7exlWO=dBIVDafMiLN4-XV@~M-K*Gjr${Pf&8M4Qn-;UbY)7W;8iFT1 zMiUGE-VX^o(Pzbr{<6jZ>~~!~$K4wk(nQ{ST^h_nr&&$Nqxd+}g~`u;cQdXd_~G7} zkABH#h(IwxFG9QR-za``5}j8mz5;c4V?6g32>Hk&u%==#k^9QNN(5>8Fo;rsmTyn2Z-&ul~=X> zsXogMTl0R2qNMH_-sXnMTMe?1NxKUjuHf4pV8ejipJd10nf8gbX9uJE{*}x_p4x#p zw}h+YE&L%)9L0cc0$GBNN+y#Jw)zg9d)sW!?q}jMJ``Z2M`yjzDam#R$>Y$8Z7_Ba zk4l2HdOiFcQzBxeW~nt`V^ys&tcK@|t272*I+W`oAS|^#3%qQBcwQyXsc}GjtH&ay z(IwuPNe$?CBuivCAVCRpEO!Yufrc*(S52`7596PNdBu?*)0~$+!L*~WO&g)>;o9Ak zz<=1ftkzmi=)qdJIfydod1kxUWHEK9sbzCebDt5`rd^td5LbBOL;OkPLR$_`fNH8F#WrczpCw?6_`r9`yW| z3aNbB5qF5qPDqvgV#RWhyR?3B@obLL$}uRKR6Ky_=8AiBWiSVspLo2H>ra^9vr#QS zcxK+wc0-}+J@oI&r_T9Z_=7z&IQdifW40t2f|D@`Bj4M8`J{e~r?OErVJjR!VYNSk*Mmj#ty!b@l;A9^Ki;RDzyd2%3CxO7OKMB|7wDb3XDj z!F|i|^0lG3`xX8dNzp3s<0OfW75pq228~T`aKI}LY5Mk=@t=>!zNP6cJ`BH$I??u` z=}k!q2P}SbdI|_=kEs|{8TT`owAGj{32(1belaBDuY1xU%y~g1(pIfrHC>?DwlKjH zjR4Q}5&gw>4*E4;&V}Z0cDGQfiO|oF0s~%ns`lHBLWW1UK1Q8A9D)B}5w2_Jm+i25 z>3mcXJF!Elh+z)45iQ|8FxZTi0zqLoUqWyMQ_97u5LL*6rGJV^xr1tHly9z_%^<`| z7Uc(drr>D|M+Rd-8GU+L(Da>K=`xS@ml^zp!S?1pPn>7(a5@;6hweXnGxH7LMD-*J zw1@R2=NU&88l`Hs?hhWM7!qqa=j*mwyH~iz2UD}UVnco(Ek0wCd4BzziD!&v{P-_T#v!*Ev_M1e^TIN|ELL(C~drpgqaKdm@~_Odv|JL#1A<#D3m+U?2YEuAfb+) z;S;tu*#oJTw=K&_x9i)UtfBKJ!(O^M9n|3ycZd3EvB;B5cApfa%p_OJhCqCh%96802ajrhiL(sbF6@9s9?e-^6+sLY}*@J?-x&zmz|58Pde&@3Q^+8 zmEYlDl^yy@%azWV7z+^&k$=Bg3R(U}Z0-7ol>N{-@ON;$&Y_~~^L`})po~{oYwt!B zWH3R7Q_pyCNM^_Ix0RPCYLShI2Saur^emtQ%Rwj^t|*;FNrk2eBzqQr1~BjQ~5e4WMxv)MQjp(}3Dy$vCKxa(aEVx6ooDzSs{`d8E7p zga-EV`2=w87}N#67H_M;74l-ncdv&PuYjoaAbKUllR6*mCz%BP=u~1p;n))|bQ9U5 zg8~Hap))Dz4Fa##Yo7@cA5=^a*MH2K?z>m#e3t+WxKL>zMC{VSv_g+xBh{;t|G`OD zb`neCx_}-C1j9W_fn>f!;omNK27=Kr6DeG@oOOXKs;l*s5bp+3Y?zo&K{byY*5A|X)R{`vyt=I(D< zt5~~3ptOQ#yDqa#K1-m($J_xuN)!_j;vnS(JCR=vxpNAj)OB%T+Di1}uEe>}aK>Wv3cWhq^GlZx6(O$(-TwLJbIZu^3iUwNluh+|i zCfg+=nSWT8I~D;!+ZK4BE(k~vU;B$G&smY^iQ^(aFWw=M&q-M)BOnf$FUdAyz7A+F6h@R}EE z?bq}Nl|C63bRcb6slWEHFBH)#eOEI~d zfKN>(e9RhYN+%B#{|aALu;E7FwpIpd2lD!!hdSU9197r3g%3CxynKBLqC&#}UhqLJ zUo3IzJV;d2KLv?J;xf=^H_SkgVjF;b*(C@HB-}<&01Sz4#{5>=L1lu;HnhKeRy}_B zIyi=+njKkV92bt1xF5bV04=6M9(L9bM%vuQ3<*SJ?AD?bQZZ^rlJgL z=tj?KDk_CxbjDv|x+thz!d|vJs;`5W_!piO4Gm<0MAyTVyZNmTcbs|MLg@IkMO!qE z2@f?#b82}3eR3nTkbLpv}<_eH9-L*|RK zs3#-4mCjZXcKJLbZODSiBW6QfU>_tRem3nd7O8`O!5X+q=VRseO~>oDSHT z@2@soeJ4rqR&Rf_VBxY*#ud>VY&9U$d;JRK5mJJ#Rfeq1>>_l$d0ik}Sn&SaL|NB za5$YUKXI}fsBb3!rou3LA{e$IPF_#{fyit54A4NYy%3}VNCa8QYH*q50Di-{x%`W6 zIBL@WNlRs+R}A)n(eQ(bVC4D&qdp&odaaT6YoEpP(6p@44xf9)YA#o#7y1KiQ#Cf)W2EGBNsleT)9W{o^kRuQQpAMtaT`v2+73y6T0C95y%dB~;U*#N&v~#E9g6@S z7Wy7aKot}rjxh>$2FM!HqbdK@?1+phvHU>0l`7Kp!9DRaOM{I}+jF z{96Vq4@-8GNEq4}%CazNLfyTUr}EFV!Ro=fdQC`)hO z{B61%y7LfP!TxdSvmXA{P|~Mb3TiPv7(Q_@^Pd% zM8B4eILjkK`)fCSi%w9RW%@u=YajBXdJUJ|6*I zY+~laj2JQvCVbZ!Tpil`9@5K5CiRW>Tobaume|&#y-{}OvQGO-{_Fatj z!+|hnbs0Z>p$WlTM1@Q9nQ8^hqxK^EooxX~-1zGoHg9tGh`$$pEUWx&W!G2)*e+x@ z14$I)KLq3PPs3D;a_iG0fI;|mKVDiy7y)vBP3IV6PksCI7i`*?#X=a4>VMvjq~Wqh zgAC9gx;3^goMko`=e_fB4Z)b~&^3P6k!%emef@f=`5QWZcL*d`lB{_WC_pY?2m&wG zA3PfLHe#T#5AHxJPo6vE+$%-bU`ga^&Iy@8jQj~#Mjw~eKleaBW`5(AucBFsg>9K< zCA#3hgQhbQicj*_uj7VAQ@z+@&HmNLU;j0f z&dpT_K(T`iWmB%~9CCkK5^{{1qi2d(qN0kNpd3+B}qz=AOEwcuLw$c&sbi3s=Ea;6`iKyFBnMEX*?+cL?`u$UNIb`E#(a zDWj|sbH@bUFIA5BppVsnsFV%vqh^8M7=pd_kWdXvBPJ&4kxY~keK(IsGUaWaEtnHq z>JFo@RuJn6%5|~_u(o5I5Rlc8|CYgmS!%GF?3eb&w#TsxkO&)Psc4C`gQf3EfZr?P z%by5%CjB8MV(tO%0()T9W1tImTq(B`cc0`uuup}z4x-M7sftCd#`zu2fDF)vMqDiU zcgnb#w$ZnsOFYuqyNe9Za@2DXju$adp68OtT2{jL!zdb9bxvFnL529+1*&s5W3^lU z8FLK?s5EP@p)AW7d2>t@C4N;V2N=G#WY{fJY*1cf;?E>>;gl3J|)`s`V_$3cjq(k}^z zVx*?gfSb+IDRs|i(cSVCN4%bZem3h9U_kLb?c-NOvf~dI57rE(^F9hK-*&SV+!#eN zgJ$wyowxz@Xfv>9(YGtxPIP5M&41L}@R`(YC*9YSi3I3ar+-{l;kVZ&ia>Hjd}+0Q zEcw)Os+$dc3XT>z5v><`Bm&Z0Y$IWu`GMqHz*@mK>F-H&OC?(0v|tXXw@E?)YlH8z zA-R*sGZKletBdW*#0sp9kU_75E=f$qg0a(F*rfnNUQ9woF(udsAhp1i)aVfz;mh+& zF1-Vfk~fSN5yys*a{ zx>aP&3N{$7|BGg*XT?6n5=J9xL`F!_N2~yJcQFG#q&Nt->f=U#@+C1O)Y36qg3N{Z zP<$@|Ts4hLe1mK%Afgb7R2JogQ5*dE7n3PUy(j2DL}tBqRi#=9cHDf{N6v0d)IHVC zFBT5Jl2WXGSePVjl^PB>aB>gC04A{6d(1o~5}Yq+=9+z4h@F%bX@bh-wngpicxNeQ za^vEgPe+@SOVOviqy`=usnc-JJRwRTNNESuXsAwWlkH);7G7e-aAjv;GHX#}&vQ%m z;SB2rNr?kQHYl^~hH&ql3pO$>$(2bfmYqz@ej*+#&Puo`3~%o=B+>Kjghblx+5Ek4 z*(?wMe6ECDlsm!4TSTt;KnyJd-R>ZsoUMI(T{*mNaz)F$1*y6~z)hgfrf;C@g#9gw z94GM>Rd5fVJ|x!#_+;7KQ^m$)vym~nswGLP32&F`G_n85>*zyMi9f#j*{u#QoXBtz z1I660tbO)?xJO{iWg#u~N(C+sIZ|zqhi~2)mp(i80a8xJc}j*Pn~HXF&LCku9`-rH zMO#rPmGxa!4wmq=tee8f?!l`R{UcFrhkcm5MB-KKWZFmz`134!y(=&HeI?Jo*pcD{ zC!mRZ`cu&u3Qs+>RTPx>C+R2{WmCA*qCc`&uLiAg)=!DR#W~nv<9j3bU$Rc=4!~%@ zJOtYSt*FkntXP@bADcKZ-&SL@H4h`OZA^Z%^yezxVK6s+K$buq9`*s^f7Wm{K9lG2 zVQ!hf@p?O1=w&R)^KY@cBejv868jyT;9?LLoa=M8WVoYh-m0n%#R1Vl3oo0YtF}ph zJU9_G6ovf(NSHiNF6)zDuN!R_8gMKnRc#j>=mix~2I?-QEtF;+_-sK`KjQgN*qCKO z&P_h*ui@G28f8IGJfZ9p{#A^|@-?w)uVAhaiawlw+RSY!h26p^L~E$~zr`lFmIVkU zq#I{~)h1X>=09aydF^pm_6+PuqOD^WIpLzZX>8yg3+t9lGF7&k_bS7?P_o1rNBlJ3 zv3tGvcbl6t_@zKVKWn62u3aDr*sAb5iO(aZ6sb-Yh4#u;gV5xt?~`Q)%rED*@2M{l zgnbbDeC>u-JQ(Q6$|F{bvzPa7>~K?9YF2YJ#Ztn!p3^k4Ld#QlUE)`}(|oyb3Cb3V zszRc-1%U^a4V{KmQ{=q5b8Ie-`M~qFMz)&D(mSBULd|EHf7p0*eSw`Y@=8aru`WM} z0lVD;CvJW!!MM}6;V31iZf4lkIgNT*-xcckL}x`6sC;3pR7irqGB=9zOWF!%zbx9s z%JjtwJ>_V^DBlDwFrA0J6dM_DrjBxK_LbWlp=umtIPIk2#+*9#bQz38DJ!N+qEFz4 zjFj_tKQalUg2}wAn{;sO4}Pox&)5p#l?oHC41^>yu!Ujp3`U(Q5=a9HRavH5e;3@x z)Vr@HTYKFRdVggTPhc_0Y&o-$Xy`kI_qe9qJ%nDt++G_{+#rCs2;|43j73m3sayBC>G*25wEvWU_Hr7$s z{;76a2};V>{n%G8W2kZ@ty%2RY|j0Zj~^2B!An-7RcTX$wo#@pGUwjpF6!CPJrKV?(pZ&^XzHHq2a` zkf+B)Z|X#=QO*N*KaNmV*yM2bE)*ph8rz0D8wh-tuCTyG8t#&)1G9ajj%NKvQ5_J zq2P}0a*X^^Zl-`3XKWq!iNbGDRZF2$if_Ewrk?|(FxGeP@ORA6k3h-4wJvHb%J5^Nfj`J@Y*sfq8b=_nJ?q>*6228(lFz|0{_*nVaIo*v5AP*|od`GsF5L#w znOQa@vQ|%AA{qPu(8DDXlbN_>S~oyG)mfPpB|t7eUU`RT&D?<`i8($A^+y2Y&s)Fn zU;(JwrdlI2ru$-?BD||M(<;^}Weujy&Wmhzkcv}X?B|aldF$Yv>l{A#P*toVn&Vmv zQpmqC^<9U)-tCFk!KlDP7>Gf`>wv|5yG^5JUK&9xK|W!2T2*iQ4Tsl6BLS_YLLOQD zcM9z|jpu(yPu!f#wUQX9((br4y0|vv%PJm&2OmXsG?1i*(u$`EM5b+}a&nB-%A2;n zec-7%8bBITT;x5=5%34O1`BKpr@fq2X8`U8^>dvko3~Ovw`VEUx8f#U50qIHAHNS? ztWK}O(dP@+kijb)TUFzD57 z3PkNa%>PgbcU7P(q(l(R(VJ*qH_hRy2O9^HTw0g0fBrtC;3 z;_Mgm5X9avWAPgDREbt%3Q`k4zTUN*7Y6RJU(?|Qu;LOTqcr5Z2cEq~Id{)TlDM%Vl4%mGz?u0VoD9k0Dt?}pk8k!vf0LZVWLP=m(t879DMn=)ZE-y)vyM2Rgj zU8-Cbnx$rsGX5A6W%aETb+fde?66FgrV_8Uh3b3==O);zHfnNwyx@UO@9{;QPwd_4+t4>@FjN*4JKo%X`?I{$(}BDE zRss|;C~Ps9V9d^?Cy}8UuJz)L7{r5kx1k!O{_DH*vorvN*Emb91yMm}U~yW8%w0rt zB1(+i6{bsaWUK}@uNn7!sM^W_+w%zpBDVNmo=uFE?u-3~8a#F&4WTDLXy3J7z3t?vS2V0<% zprmQ?AFH#)jQ8A<#}TiDk2l}Oc$zp3$_}T-9S)&#@Xl zH&6Q8hGd2QfcLFyn2D7&LAGxpVwfaxyIN4^qvdv%5#gOL<3Mb|#Jvf|GR(0{&)Mi--ZSd9V&ni59oSom z?R`7Boaht122KZ8-%sl+uq#yiJ2e21cFIc6hZXuvSL+x2=)^29t1-fcx` zFBYyY+DpMGtdfGa3(N=>R-gLy4h8=NoCwb^AgH$Ud!D)R{ESIj{BZYqazIdX%sGyRAa8ICCXbgR}F;_rde*wWeF za|g~UO>Lbc^~*uu7l5M&05g)ds6p}<0BXrtqd4BuwIL^<+IJN|uXo%R^kSd9sqiPB z)XM)rNC?3(hv@6yl3&&2Z>iA0Sw^IQxNVR_0n)_#V+a>dWi$Bwe0& zGiJ88H~gOww)+=(DXnQ`ejyU2mDktxN+#c%≤iIyxf$bZ8IkO>3?(5aDm!xwZR( z_rHgEJz3SP3<3#imn&cNJ2%8dOCn{>J_rWjO?YX_xNQhFGy`4PDxu*M(mKo$ya0r( zZ}{9CoC_+NNor$r>7eH{twk$R&c*w#6@%`K5?J~hL_j2r<-^kLsdDuK#-&>|Dy1$V zr@^;SF8yvy{{tL3jJA@!vm9}@>f{#aV5X&im+IzC$Ww2|D^RdJ)hD(vEkGU%u&#{> z;eUnNjpJ@xh6XKow~@8!Q1Y6Hr?bJtpZwWYk;CZI`H%$F68;iU2RFNG_LTxiZ^XFk zc8TQr5yoO@NUVv6&Z`WpjKhyMs45X?vry}cO9aip+Sa|Xf%On-f+d*erPs0UGiN#q z-~4hMv81a*8Rv&(wK48lG&5-R=vv<>php_W^|q+>K2To4HU+GI5u@A469h_S0XPj) z*M>MnL3RSI?M;0_zhN&r^Kfom*!C;p+3tA3CSBAg`!L_ebUt8jKX2j-8<2ek=gpW) zG?0KJ30wl5Qwc4pYStUYBpir<;*|hS;7_|DS1!BZL-6KX*^t zQ~}&5#R7bAO?>f1>xC_oA9o7y`Q3!NI^NERjl!FscNA47ei9gLnD0p;25vfb-r;Qn zqM7Qgbse0nSU%hzk&x*~moQVdG=Q7Ug1o&Jo=?~b_;)?5{`+qE$x94l4#M{y_& zf3Q>O?&dY<)semNaKrx=ms^P8t}>?kJyB}zLWZ`m?zZ-jaLGm7nsYIa;|;s3S^Pnx z?Wbs@3#nYqrQL^#E@Rw@_KFw_Tck%RyVSI_FfoK;xaOlUFKY49@9M>#0%!ggggIf| zQrhwlSAu3orNdf@RSxf2qzx2Qfhh*0XWIDdS1sF)Vhm6#NYn|>)r$pJHhMXi0Kx_j zcZJdS%l=w#7D)sA5WJ!2k4>3!VtGu+%P6gpvnBL4C3H(pPD*kyVowB4P67BFu2Pg5 zM?SK1kbG&qT5^jKxPl@BoTN$V0hD-LV>b;nNzF8FdhIB?E@g7FoiT|ArLJh3Mo7j3 zQOZp}Qi=YAdw25#+Wq0elS#AbPN?rwCp4#N^ZuSi7_c3Y=0(1BCrHK&Nfm@BC;b}R zU+kr_#48U_L~ z{#|w0z?wE;lMjpZKv~k!X|{-Mvw<-}@}VMvwmMP(@dZZ?g2y(yQg_U9zjw;jWAJLH zh0`hSO)Ak=#zd+HN*I7^bAs)sff=O_`rEaZ!V(hpTL58280X*-+4^P%aX3QeHxTbUYyb_DqFW*$A97ZzU-0t#WSL5cd%iD%g6M%6)W z#H<*F8LVr<7YA+A`LI4Sh`Se)oRg8b$)Iqo$352#Y3BH{b-+kwXU z7T&Zi7HqOaG9Z(x_E(x~LZf88DTj@E2zzazI1lKmJP2H*JH+R zm0Oz}PP$Tbs{K7@Y2tw}(pRwb1E{znV0sd5E#u%^f7wzt#xVDSV`D7LUq!wUMX_Cz zBj_HLpThv(60$=s4F82h)Rnlvxu^bL0T2Q2{>l*Pduukr(C7)Ux_=l>NM~Usj^#)w zBghcs? z?>7mkziF>1F=!ESp+~;cVvw)JAirbr3c}U4-N^8M6OsP3t_2+BmluN=l@;5O(Nab; zMpJJ^-nSykoE_l+aed~F6TmS*3|<03I0-U4?l(Jz#doc|eVhP}U7|>0>q#s;DNb0CFXx_Io0L+W-In07*qoM6N<$f@#_&asU7T literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.traceviewuilib/traceview.iml b/andmore-swt/org.eclipse.andmore.traceviewuilib/traceview.iml new file mode 100644 index 00000000..f58e099e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.traceviewuilib/traceview.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.classpath b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.classpath new file mode 100644 index 00000000..00782ce2 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.gitignore b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.gitignore new file mode 100644 index 00000000..0f630157 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.gitignore @@ -0,0 +1,2 @@ +/target/ +/bin/ diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.project b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.project new file mode 100644 index 00000000..b0c6d667 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.project @@ -0,0 +1,34 @@ + + + org.eclipse.andmore.uiautomatorviewer + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.core.resources.prefs b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..4824b802 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.jdt.core.prefs b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..295926d9 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,7 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.m2e.core.prefs b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/META-INF/MANIFEST.MF b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/META-INF/MANIFEST.MF new file mode 100644 index 00000000..df859f9c --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/META-INF/MANIFEST.MF @@ -0,0 +1,17 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Localization: plugin +Bundle-Name: %Bundle-Name +Bundle-SymbolicName: org.eclipse.andmore.uiautomatorviewer;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Vendor: %Bundle-Vendor +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Require-Bundle: org.eclipse.core.commands;bundle-version="3.8.1", + org.eclipse.equinox.common;bundle-version="3.8.0", + org.eclipse.jface;bundle-version="3.12.2", + org.eclipse.andmore.swt +Export-Package: com.android.uiautomator, + com.android.uiautomator.actions, + com.android.uiautomator.tree, + images +Bundle-ClassPath: . diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/MODULE_LICENSE_APACHE2 b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/MODULE_LICENSE_APACHE2 new file mode 100644 index 00000000..e69de29b diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/NOTICE b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/NOTICE new file mode 100644 index 00000000..70c54220 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/NOTICE @@ -0,0 +1,190 @@ + + Copyright (c) 2005-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. + + 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. + + + 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 + diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/build.gradle b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/build.gradle new file mode 100644 index 00000000..8b8ec3c7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/build.gradle @@ -0,0 +1,26 @@ +group = 'com.android.tools' +archivesBaseName = 'uiautomatorviewer' + +dependencies { + compile project(':base:ddmlib') +} + +sourceSets { + main.resources.srcDir 'src/main/java' +} + +sdk { + linux { + item('etc/uiautomatorviewer') { executable true } + } + mac { + item('etc/uiautomatorviewer') { executable true } + } + windows { + item 'etc/uiautomatorviewer.bat' + } +} + +// configure the manifest of the buildDistributionJar task. +sdkJar.manifest.attributes("Main-Class": "com.android.uiautomator.UiAutomatorViewer") + diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/build.properties b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/build.properties new file mode 100644 index 00000000..ecc09051 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/build.properties @@ -0,0 +1,4 @@ +source.. = src/main/java/ +output.. = bin/ +bin.includes = META-INF/,\ + . diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/etc/uiautomatorviewer b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/etc/uiautomatorviewer new file mode 100644 index 00000000..868efa7a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/etc/uiautomatorviewer @@ -0,0 +1,104 @@ +#!/bin/bash +# +# Copyright 2012, 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}"` +progname=`basename "${prog}"` +cd "${progdir}" +progdir=`pwd` +prog="${progdir}"/"${progname}" +cd "${oldwd}" + +jarfile=uiautomatorviewer.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 "${progname}: can't find $jarfile" + exit 1 +fi + +javaCmd="java" + +os=`uname` +if [ $os == 'Darwin' ]; then + javaOpts="-Xmx1600M -XstartOnFirstThread" +else + javaOpts="-Xmx1600M" +fi + +if [ `uname` = "Linux" ]; then + export GDK_NATIVE_WINDOWS=true +fi + +while expr "x$1" : 'x-J' >/dev/null; do + opt=`expr "x$1" : 'x-J\(.*\)'` + javaOpts="${javaOpts} -${opt}" + shift +done + +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}/prebuilts/tools/${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 + +exec "${javaCmd}" $javaOpts -Djava.ext.dirs="$frameworkdir" -Dcom.android.uiautomator.bindir="$progdir" -jar "$jarpath" "$@" diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/etc/uiautomatorviewer.bat b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/etc/uiautomatorviewer.bat new file mode 100644 index 00000000..4739b018 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/etc/uiautomatorviewer.bat @@ -0,0 +1,66 @@ +@echo off +rem Copyright (C) 2012 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 Get the CWD as a full path with short names only (without spaces) +for %%i in ("%cd%") do set prog_dir=%%~fsi + +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=uiautomatorviewer.jar +set frameworkdir=. + +if exist %frameworkdir%\%jarfile% goto JarFileOk + set frameworkdir=lib + +if exist %frameworkdir%\%jarfile% goto JarFileOk + set frameworkdir=..\framework + +:JarFileOk + +set jarpath=%frameworkdir%\%jarfile% + +if not defined ANDROID_SWT goto QueryArch + set swt_path=%ANDROID_SWT% + goto SwtDone + +:QueryArch + + for /f "delims=" %%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%" "-Djava.ext.dirs=%javaextdirs%" "-Dcom.android.uiautomator.bindir=%prog_dir%" -jar %jarpath% %* diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/plugin.properties b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/plugin.properties new file mode 100644 index 00000000..5cdd8cbd --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/plugin.properties @@ -0,0 +1,4 @@ +#Properties file for com.android.ide.eclipse.uiautomatorviewer +Bundle-Vendor = Eclipse Andmore +Bundle-Name = UI Automator Viewer +category.name = Android diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/pom.xml b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/pom.xml new file mode 100644 index 00000000..5f8fb29e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + ../pom.xml + org.eclipse.andmore + swt-droid-parent + 0.5.2-SNAPSHOT + + org.eclipse.andmore.uiautomatorviewer + eclipse-plugin + uiautomatorviewer + + + + org.eclipse.tycho + tycho-maven-plugin + true + + + org.eclipse.tycho + tycho-source-plugin + ${tycho-version} + + + plugin-source + + plugin-source + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/DebugBridge.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/DebugBridge.java new file mode 100644 index 00000000..4500f99b --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/DebugBridge.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2012 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.uiautomator; + +import com.android.SdkConstants; +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.IDevice; + +import java.io.File; +import java.util.Arrays; +import java.util.List; + +public class DebugBridge { + private static AndroidDebugBridge sDebugBridge; + + private static String getAdbLocation() { + String toolsDir = System.getProperty("com.android.uiautomator.bindir"); //$NON-NLS-1$ + if (toolsDir == null) { + return null; + } + + File sdk = new File(toolsDir).getParentFile(); + + // check if adb is present in platform-tools + File platformTools = new File(sdk, "platform-tools"); + File adb = new File(platformTools, SdkConstants.FN_ADB); + if (adb.exists()) { + return adb.getAbsolutePath(); + } + + // check if adb is present in the tools directory + adb = new File(toolsDir, SdkConstants.FN_ADB); + if (adb.exists()) { + return adb.getAbsolutePath(); + } + + // check if we're in the Android source tree where adb is in $ANDROID_HOST_OUT/bin/adb + String androidOut = System.getenv("ANDROID_HOST_OUT"); + if (androidOut != null) { + String adbLocation = androidOut + File.separator + "bin" + File.separator + + SdkConstants.FN_ADB; + if (new File(adbLocation).exists()) { + return adbLocation; + } + } + + return null; + } + + public static void init() { + String adbLocation = getAdbLocation(); + if (adbLocation != null) { + AndroidDebugBridge.init(false /* debugger support */); + sDebugBridge = AndroidDebugBridge.createBridge(adbLocation, false); + } + } + + public static void terminate() { + if (sDebugBridge != null) { + sDebugBridge = null; + AndroidDebugBridge.terminate(); + } + } + + public static boolean isInitialized() { + return sDebugBridge != null; + } + + public static List getDevices() { + return Arrays.asList(sDebugBridge.getDevices()); + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/OpenDialog.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/OpenDialog.java new file mode 100644 index 00000000..77657aae --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/OpenDialog.java @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2012 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.uiautomator; + +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Point; +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.FileDialog; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +import java.io.File; + +/** + * Implements a file selection dialog for both screen shot and xml dump file + * + * "OK" button won't be enabled unless both files are selected + * It also has a convenience feature such that if one file has been picked, and the other + * file path is empty, then selection for the other file will start from the same base folder + * + */ +public class OpenDialog extends Dialog { + private static final int FIXED_TEXT_FIELD_WIDTH = 300; + private static final int DEFAULT_LAYOUT_SPACING = 10; + private Text mScreenshotText; + private Text mXmlText; + private boolean mFileChanged = false; + private Button mOkButton; + + private static File sScreenshotFile; + private static File sXmlDumpFile; + + /** + * Create the dialog. + * @param parentShell + */ + public OpenDialog(Shell parentShell) { + super(parentShell); + setShellStyle(SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL); + } + + /** + * Create contents of the dialog. + * @param parent + */ + @Override + protected Control createDialogArea(Composite parent) { + Composite container = (Composite) super.createDialogArea(parent); + GridLayout gl_container = new GridLayout(1, false); + gl_container.verticalSpacing = DEFAULT_LAYOUT_SPACING; + gl_container.horizontalSpacing = DEFAULT_LAYOUT_SPACING; + gl_container.marginWidth = DEFAULT_LAYOUT_SPACING; + gl_container.marginHeight = DEFAULT_LAYOUT_SPACING; + container.setLayout(gl_container); + + Group openScreenshotGroup = new Group(container, SWT.NONE); + openScreenshotGroup.setLayout(new GridLayout(2, false)); + openScreenshotGroup.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + openScreenshotGroup.setText("Screenshot"); + + mScreenshotText = new Text(openScreenshotGroup, SWT.BORDER | SWT.READ_ONLY); + if (sScreenshotFile != null) { + mScreenshotText.setText(sScreenshotFile.getAbsolutePath()); + } + GridData gd_screenShotText = new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1); + gd_screenShotText.minimumWidth = FIXED_TEXT_FIELD_WIDTH; + gd_screenShotText.widthHint = FIXED_TEXT_FIELD_WIDTH; + mScreenshotText.setLayoutData(gd_screenShotText); + + Button openScreenshotButton = new Button(openScreenshotGroup, SWT.NONE); + openScreenshotButton.setText("..."); + openScreenshotButton.addListener(SWT.Selection, new Listener() { + @Override + public void handleEvent(Event event) { + handleOpenScreenshotFile(); + } + }); + + Group openXmlGroup = new Group(container, SWT.NONE); + openXmlGroup.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + openXmlGroup.setText("UI XML Dump"); + openXmlGroup.setLayout(new GridLayout(2, false)); + + mXmlText = new Text(openXmlGroup, SWT.BORDER | SWT.READ_ONLY); + mXmlText.setEditable(false); + if (sXmlDumpFile != null) { + mXmlText.setText(sXmlDumpFile.getAbsolutePath()); + } + GridData gd_xmlText = new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1); + gd_xmlText.minimumWidth = FIXED_TEXT_FIELD_WIDTH; + gd_xmlText.widthHint = FIXED_TEXT_FIELD_WIDTH; + mXmlText.setLayoutData(gd_xmlText); + + Button openXmlButton = new Button(openXmlGroup, SWT.NONE); + openXmlButton.setText("..."); + openXmlButton.addListener(SWT.Selection, new Listener() { + @Override + public void handleEvent(Event event) { + handleOpenXmlDumpFile(); + } + }); + + return container; + } + + /** + * Create contents of the button bar. + * @param parent + */ + @Override + protected void createButtonsForButtonBar(Composite parent) { + mOkButton = createButton(parent, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL, true); + createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false); + updateButtonState(); + } + + /** + * Return the initial size of the dialog. + */ + @Override + protected Point getInitialSize() { + return new Point(368, 233); + } + + @Override + protected void configureShell(Shell newShell) { + super.configureShell(newShell); + newShell.setText("Open UI Dump Files"); + } + + private void handleOpenScreenshotFile() { + FileDialog fd = new FileDialog(getShell(), SWT.OPEN); + fd.setText("Open Screenshot File"); + File initialFile = sScreenshotFile; + // if file has never been selected before, try to base initial path on the mXmlDumpFile + if (initialFile == null && sXmlDumpFile != null && sXmlDumpFile.isFile()) { + initialFile = sXmlDumpFile.getParentFile(); + } + if (initialFile != null) { + if (initialFile.isFile()) { + fd.setFileName(initialFile.getAbsolutePath()); + } else if (initialFile.isDirectory()) { + fd.setFilterPath(initialFile.getAbsolutePath()); + } + } + String[] filter = {"*.png"}; + fd.setFilterExtensions(filter); + String selected = fd.open(); + if (selected != null) { + sScreenshotFile = new File(selected); + mScreenshotText.setText(selected); + mFileChanged = true; + } + updateButtonState(); + } + + private void handleOpenXmlDumpFile() { + FileDialog fd = new FileDialog(getShell(), SWT.OPEN); + fd.setText("Open UI Dump XML File"); + File initialFile = sXmlDumpFile; + // if file has never been selected before, try to base initial path on the mScreenshotFile + if (initialFile == null && sScreenshotFile != null && sScreenshotFile.isFile()) { + initialFile = sScreenshotFile.getParentFile(); + } + if (initialFile != null) { + if (initialFile.isFile()) { + fd.setFileName(initialFile.getAbsolutePath()); + } else if (initialFile.isDirectory()) { + fd.setFilterPath(initialFile.getAbsolutePath()); + } + } + String initialPath = mXmlText.getText(); + if (initialPath.isEmpty() && sScreenshotFile != null && sScreenshotFile.isFile()) { + initialPath = sScreenshotFile.getParentFile().getAbsolutePath(); + } + String[] filter = {"*.uix"}; + fd.setFilterExtensions(filter); + String selected = fd.open(); + if (selected != null) { + sXmlDumpFile = new File(selected); + mXmlText.setText(selected); + mFileChanged = true; + } + updateButtonState(); + } + + private void updateButtonState() { + mOkButton.setEnabled(sXmlDumpFile != null && sXmlDumpFile.isFile()); + } + + public boolean hasFileChanged() { + return mFileChanged; + } + + public File getScreenshotFile() { + return sScreenshotFile; + } + + public File getXmlDumpFile() { + return sXmlDumpFile; + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorHelper.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorHelper.java new file mode 100644 index 00000000..c4daa1c3 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorHelper.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2012 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.uiautomator; + +import com.android.ddmlib.CollectingOutputReceiver; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.RawImage; +import com.android.ddmlib.SyncService; +import com.android.uiautomator.tree.BasicTreeNode; +import com.android.uiautomator.tree.RootWindowNode; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.swt.SWT; +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.widgets.Display; + +import java.io.File; +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class UiAutomatorHelper { + public static final int UIAUTOMATOR_MIN_API_LEVEL = 16; + + private static final String UIAUTOMATOR = "/system/bin/uiautomator"; //$NON-NLS-1$ + private static final String UIAUTOMATOR_DUMP_COMMAND = "dump"; //$NON-NLS-1$ + private static final String UIDUMP_DEVICE_PATH = "/data/local/tmp/uidump.xml"; //$NON-NLS-1$ + private static final int XML_CAPTURE_TIMEOUT_SEC = 40; + + private static boolean supportsUiAutomator(IDevice device) { + String apiLevelString = device.getProperty(IDevice.PROP_BUILD_API_LEVEL); + int apiLevel; + try { + apiLevel = Integer.parseInt(apiLevelString); + } catch (NumberFormatException e) { + apiLevel = UIAUTOMATOR_MIN_API_LEVEL; + } + + return apiLevel >= UIAUTOMATOR_MIN_API_LEVEL; + } + + private static void getUiHierarchyFile(IDevice device, File dst, + IProgressMonitor monitor, boolean compressed) { + if (monitor == null) { + monitor = new NullProgressMonitor(); + } + + monitor.subTask("Deleting old UI XML snapshot ..."); + String command = "rm " + UIDUMP_DEVICE_PATH; + + try { + CountDownLatch commandCompleteLatch = new CountDownLatch(1); + device.executeShellCommand(command, + new CollectingOutputReceiver(commandCompleteLatch)); + commandCompleteLatch.await(5, TimeUnit.SECONDS); + } catch (Exception e1) { + // ignore exceptions while deleting stale files + } + + monitor.subTask("Taking UI XML snapshot..."); + if (compressed){ + command = String.format("%s %s --compressed %s", UIAUTOMATOR, + UIAUTOMATOR_DUMP_COMMAND, + UIDUMP_DEVICE_PATH); + } else { + command = String.format("%s %s %s", UIAUTOMATOR, + UIAUTOMATOR_DUMP_COMMAND, + UIDUMP_DEVICE_PATH); + } + CountDownLatch commandCompleteLatch = new CountDownLatch(1); + + try { + device.executeShellCommand( + command, + new CollectingOutputReceiver(commandCompleteLatch), + XML_CAPTURE_TIMEOUT_SEC * 1000); + commandCompleteLatch.await(XML_CAPTURE_TIMEOUT_SEC, TimeUnit.SECONDS); + + monitor.subTask("Pull UI XML snapshot from device..."); + device.getSyncService().pullFile(UIDUMP_DEVICE_PATH, + dst.getAbsolutePath(), SyncService.getNullProgressMonitor()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + //to maintain a backward compatible api, use non-compressed as default snapshot type + public static UiAutomatorResult takeSnapshot(IDevice device, IProgressMonitor monitor) + throws UiAutomatorException { + return takeSnapshot(device, monitor,false); + } + + public static UiAutomatorResult takeSnapshot(IDevice device, IProgressMonitor monitor, + boolean compressed) throws UiAutomatorException { + if (monitor == null) { + monitor = new NullProgressMonitor(); + } + + monitor.subTask("Checking if device support UI Automator"); + if (!supportsUiAutomator(device)) { + String msg = "UI Automator requires a device with API Level " + + UIAUTOMATOR_MIN_API_LEVEL; + throw new UiAutomatorException(msg, null); + } + + monitor.subTask("Creating temporary files for uiautomator results."); + File tmpDir = null; + File xmlDumpFile = null; + File screenshotFile = null; + try { + tmpDir = File.createTempFile("uiautomatorviewer_", ""); + tmpDir.delete(); + if (!tmpDir.mkdirs()) + throw new IOException("Failed to mkdir"); + xmlDumpFile = File.createTempFile("dump_", ".uix", tmpDir); + screenshotFile = File.createTempFile("screenshot_", ".png", tmpDir); + } catch (Exception e) { + String msg = "Error while creating temporary file to save snapshot: " + + e.getMessage(); + throw new UiAutomatorException(msg, e); + } + + tmpDir.deleteOnExit(); + xmlDumpFile.deleteOnExit(); + screenshotFile.deleteOnExit(); + + monitor.subTask("Obtaining UI hierarchy"); + try { + UiAutomatorHelper.getUiHierarchyFile(device, xmlDumpFile, monitor, compressed); + } catch (Exception e) { + String msg = "Error while obtaining UI hierarchy XML file: " + e.getMessage(); + throw new UiAutomatorException(msg, e); + } + + UiAutomatorModel model; + try { + model = new UiAutomatorModel(xmlDumpFile); + } catch (Exception e) { + String msg = "Error while parsing UI hierarchy XML file: " + e.getMessage(); + throw new UiAutomatorException(msg, e); + } + + monitor.subTask("Obtaining device screenshot"); + RawImage rawImage; + try { + rawImage = device.getScreenshot(); + } catch (Exception e) { + String msg = "Error taking device screenshot: " + e.getMessage(); + throw new UiAutomatorException(msg, e); + } + + // rotate the screen shot per device rotation + BasicTreeNode root = model.getXmlRootNode(); + if (root instanceof RootWindowNode) { + for (int i = 0; i < ((RootWindowNode)root).getRotation(); i++) { + rawImage = rawImage.getRotated(); + } + } + PaletteData palette = new PaletteData( + rawImage.getRedMask(), + rawImage.getGreenMask(), + rawImage.getBlueMask()); + ImageData imageData = new ImageData(rawImage.width, rawImage.height, + rawImage.bpp, palette, 1, rawImage.data); + ImageLoader loader = new ImageLoader(); + loader.data = new ImageData[] { imageData }; + loader.save(screenshotFile.getAbsolutePath(), SWT.IMAGE_PNG); + Image screenshot = new Image(Display.getDefault(), imageData); + + return new UiAutomatorResult(xmlDumpFile, model, screenshot); + } + + @SuppressWarnings("serial") + public static class UiAutomatorException extends Exception { + public UiAutomatorException(String msg, Throwable t) { + super(msg, t); + } + } + + public static class UiAutomatorResult { + public final File uiHierarchy; + public final UiAutomatorModel model; + public final Image screenshot; + + public UiAutomatorResult(File uiXml, UiAutomatorModel m, Image s) { + uiHierarchy = uiXml; + model = m; + screenshot = s; + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorModel.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorModel.java new file mode 100644 index 00000000..c3ba9e68 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorModel.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2012 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.uiautomator; + +import com.android.uiautomator.tree.AttributePair; +import com.android.uiautomator.tree.BasicTreeNode; +import com.android.uiautomator.tree.BasicTreeNode.IFindNodeListener; +import com.android.uiautomator.tree.UiHierarchyXmlLoader; +import com.android.uiautomator.tree.UiNode; + +import org.eclipse.swt.graphics.Rectangle; + +import java.io.File; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +public class UiAutomatorModel { + private BasicTreeNode mRootNode; + private BasicTreeNode mSelectedNode; + private Rectangle mCurrentDrawingRect; + private List mNafNodes; + + // determines whether we lookup the leaf UI node on mouse move of screenshot image + private boolean mExploreMode = true; + + private boolean mShowNafNodes = false; + private List mNodelist; + private Set mSearchKeySet = new HashSet(); + + public UiAutomatorModel(File xmlDumpFile) { + mSearchKeySet.add("text"); + mSearchKeySet.add("content-desc"); + + UiHierarchyXmlLoader loader = new UiHierarchyXmlLoader(); + BasicTreeNode rootNode = loader.parseXml(xmlDumpFile.getAbsolutePath()); + if (rootNode == null) { + System.err.println("null rootnode after parsing."); + throw new IllegalArgumentException("Invalid ui automator hierarchy file."); + } + + mNafNodes = loader.getNafNodes(); + if (mRootNode != null) { + mRootNode.clearAllChildren(); + } + + mRootNode = rootNode; + mExploreMode = true; + mNodelist = loader.getAllNodes(); + } + + public BasicTreeNode getXmlRootNode() { + return mRootNode; + } + + public BasicTreeNode getSelectedNode() { + return mSelectedNode; + } + + /** + * change node selection in the Model recalculate the rect to highlight, + * also notifies the View to refresh accordingly + * + * @param node + */ + public void setSelectedNode(BasicTreeNode node) { + mSelectedNode = node; + if (mSelectedNode instanceof UiNode) { + UiNode uiNode = (UiNode) mSelectedNode; + mCurrentDrawingRect = new Rectangle(uiNode.x, uiNode.y, uiNode.width, uiNode.height); + } else { + mCurrentDrawingRect = null; + } + } + + public Rectangle getCurrentDrawingRect() { + return mCurrentDrawingRect; + } + + /** + * Do a search in tree to find a leaf node or deepest parent node containing the coordinate + * + * @param x + * @param y + * @return + */ + public BasicTreeNode updateSelectionForCoordinates(int x, int y) { + BasicTreeNode node = null; + + if (mRootNode != null) { + MinAreaFindNodeListener listener = new MinAreaFindNodeListener(); + boolean found = mRootNode.findLeafMostNodesAtPoint(x, y, listener); + if (found && listener.mNode != null && !listener.mNode.equals(mSelectedNode)) { + node = listener.mNode; + } + } + + return node; + } + + public boolean isExploreMode() { + return mExploreMode; + } + + public void toggleExploreMode() { + mExploreMode = !mExploreMode; + } + + public void setExploreMode(boolean exploreMode) { + mExploreMode = exploreMode; + } + + private static class MinAreaFindNodeListener implements IFindNodeListener { + BasicTreeNode mNode = null; + + @Override + public void onFoundNode(BasicTreeNode node) { + if (mNode == null) { + mNode = node; + } else { + if ((node.height * node.width) < (mNode.height * mNode.width)) { + mNode = node; + } + } + } + } + + public List getNafNodes() { + return mNafNodes; + } + + public void toggleShowNaf() { + mShowNafNodes = !mShowNafNodes; + } + + public boolean shouldShowNafNodes() { + return mShowNafNodes; + } + + public List searchNode(String tofind) { + List result = new LinkedList(); + for (BasicTreeNode node : mNodelist) { + Object[] attrs = node.getAttributesArray(); + for (Object attr : attrs) { + if (!mSearchKeySet.contains(((AttributePair) attr).key)) + continue; + if (((AttributePair) attr).value.toLowerCase().contains(tofind.toLowerCase())) { + result.add(node); + break; + } + } + } + return result; + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorView.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorView.java new file mode 100644 index 00000000..c6e95113 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorView.java @@ -0,0 +1,608 @@ +/* + * Copyright (C) 2012 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.uiautomator; + +import com.android.uiautomator.actions.ExpandAllAction; +import com.android.uiautomator.actions.ImageHelper; +import com.android.uiautomator.actions.ToggleNafAction; +import com.android.uiautomator.tree.AttributePair; +import com.android.uiautomator.tree.BasicTreeNode; +import com.android.uiautomator.tree.BasicTreeNodeContentProvider; +import com.android.uiautomator.tree.UiNode; + +import org.eclipse.jface.action.ToolBarManager; +import org.eclipse.jface.layout.TableColumnLayout; +import org.eclipse.jface.viewers.ArrayContentProvider; +import org.eclipse.jface.viewers.CellEditor; +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.jface.viewers.ColumnWeightData; +import org.eclipse.jface.viewers.EditingSupport; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.LabelProvider; +import org.eclipse.jface.viewers.SelectionChangedEvent; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.viewers.TableViewerColumn; +import org.eclipse.jface.viewers.TextCellEditor; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.SashForm; +import org.eclipse.swt.custom.StackLayout; +import org.eclipse.swt.events.KeyEvent; +import org.eclipse.swt.events.KeyListener; +import org.eclipse.swt.events.MouseAdapter; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.events.MouseMoveListener; +import org.eclipse.swt.events.PaintEvent; +import org.eclipse.swt.events.PaintListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Cursor; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.ImageLoader; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.graphics.Transform; +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.Canvas; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.FileDialog; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.Text; +import org.eclipse.swt.widgets.ToolBar; +import org.eclipse.swt.widgets.ToolItem; +import org.eclipse.swt.widgets.Tree; + +import java.io.File; +import java.util.List; + +public class UiAutomatorView extends Composite { + private static final int IMG_BORDER = 2; + + // The screenshot area is made of a stack layout of two components: screenshot canvas and + // a "specify screenshot" button. If a screenshot is already available, then that is displayed + // on the canvas. If it is not availble, then the "specify screenshot" button is displayed. + private Composite mScreenshotComposite; + private StackLayout mStackLayout; + private Composite mSetScreenshotComposite; + private Canvas mScreenshotCanvas; + + private TreeViewer mTreeViewer; + private TableViewer mTableViewer; + + private float mScale = 1.0f; + private int mDx, mDy; + + private UiAutomatorModel mModel; + private File mModelFile; + private Image mScreenshot; + + private List mSearchResult; + private int mSearchResultIndex; + private ToolItem itemDeleteAndInfo; + private Text searchTextarea; + private Cursor mOrginialCursor; + private ToolItem itemPrev, itemNext; + private ToolItem coordinateLabel; + + private String mLastSearchedTerm; + + private Cursor mCrossCursor; + + public UiAutomatorView(Composite parent, int style) { + super(parent, SWT.NONE); + setLayout(new FillLayout()); + + SashForm baseSash = new SashForm(this, SWT.HORIZONTAL); + mOrginialCursor = getShell().getCursor(); + mCrossCursor = new Cursor(getDisplay(), SWT.CURSOR_CROSS); + mScreenshotComposite = new Composite(baseSash, SWT.BORDER); + mStackLayout = new StackLayout(); + mScreenshotComposite.setLayout(mStackLayout); + // draw the canvas with border, so the divider area for sash form can be highlighted + mScreenshotCanvas = new Canvas(mScreenshotComposite, SWT.BORDER); + mStackLayout.topControl = mScreenshotCanvas; + mScreenshotComposite.layout(); + + // set cursor when enter canvas + mScreenshotCanvas.addListener(SWT.MouseEnter, new Listener() { + @Override + public void handleEvent(Event arg0) { + getShell().setCursor(mCrossCursor); + } + }); + mScreenshotCanvas.addListener(SWT.MouseExit, new Listener() { + @Override + public void handleEvent(Event arg0) { + getShell().setCursor(mOrginialCursor); + } + }); + + mScreenshotCanvas.addMouseListener(new MouseAdapter() { + @Override + public void mouseUp(MouseEvent e) { + if (mModel != null) { + mModel.toggleExploreMode(); + redrawScreenshot(); + } + } + }); + mScreenshotCanvas.setBackground( + getShell().getDisplay().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND)); + mScreenshotCanvas.addPaintListener(new PaintListener() { + @Override + public void paintControl(PaintEvent e) { + if (mScreenshot != null) { + updateScreenshotTransformation(); + // shifting the image here, so that there's a border around screen shot + // this makes highlighting red rectangles on the screen shot edges more visible + Transform t = new Transform(e.gc.getDevice()); + t.translate(mDx, mDy); + t.scale(mScale, mScale); + e.gc.setTransform(t); + e.gc.drawImage(mScreenshot, 0, 0); + // this resets the transformation to identity transform, i.e. no change + // we don't use transformation here because it will cause the line pattern + // and line width of highlight rect to be scaled, causing to appear to be blurry + e.gc.setTransform(null); + if (mModel.shouldShowNafNodes()) { + // highlight the "Not Accessibility Friendly" nodes + e.gc.setForeground(e.gc.getDevice().getSystemColor(SWT.COLOR_YELLOW)); + e.gc.setBackground(e.gc.getDevice().getSystemColor(SWT.COLOR_YELLOW)); + for (Rectangle r : mModel.getNafNodes()) { + e.gc.setAlpha(50); + e.gc.fillRectangle(mDx + getScaledSize(r.x), mDy + getScaledSize(r.y), + getScaledSize(r.width), getScaledSize(r.height)); + e.gc.setAlpha(255); + e.gc.setLineStyle(SWT.LINE_SOLID); + e.gc.setLineWidth(2); + e.gc.drawRectangle(mDx + getScaledSize(r.x), mDy + getScaledSize(r.y), + getScaledSize(r.width), getScaledSize(r.height)); + } + } + + // draw the search result rects + if (mSearchResult != null){ + for (BasicTreeNode result : mSearchResult){ + if (result instanceof UiNode) { + UiNode uiNode = (UiNode) result; + Rectangle rect = new Rectangle( + uiNode.x, uiNode.y, uiNode.width, uiNode.height); + e.gc.setForeground( + e.gc.getDevice().getSystemColor(SWT.COLOR_YELLOW)); + e.gc.setLineStyle(SWT.LINE_DASH); + e.gc.setLineWidth(1); + e.gc.drawRectangle(mDx + getScaledSize(rect.x), + mDy + getScaledSize(rect.y), + getScaledSize(rect.width), getScaledSize(rect.height)); + } + } + } + + // draw the mouseover rects + Rectangle rect = mModel.getCurrentDrawingRect(); + if (rect != null) { + e.gc.setForeground(e.gc.getDevice().getSystemColor(SWT.COLOR_RED)); + if (mModel.isExploreMode()) { + // when we highlight nodes dynamically on mouse move, + // use dashed borders + e.gc.setLineStyle(SWT.LINE_DASH); + e.gc.setLineWidth(1); + } else { + // when highlighting nodes on tree node selection, + // use solid borders + e.gc.setLineStyle(SWT.LINE_SOLID); + e.gc.setLineWidth(2); + } + e.gc.drawRectangle(mDx + getScaledSize(rect.x), mDy + getScaledSize(rect.y), + getScaledSize(rect.width), getScaledSize(rect.height)); + } + } + } + }); + mScreenshotCanvas.addMouseMoveListener(new MouseMoveListener() { + @Override + public void mouseMove(MouseEvent e) { + if (mModel != null) { + int x = getInverseScaledSize(e.x - mDx); + int y = getInverseScaledSize(e.y - mDy); + // show coordinate + coordinateLabel.setText(String.format("(%d,%d)", x,y)); + if (mModel.isExploreMode()) { + BasicTreeNode node = mModel.updateSelectionForCoordinates(x, y); + if (node != null) { + updateTreeSelection(node); + } + } + } + } + }); + + mSetScreenshotComposite = new Composite(mScreenshotComposite, SWT.NONE); + mSetScreenshotComposite.setLayout(new GridLayout()); + + final Button setScreenshotButton = new Button(mSetScreenshotComposite, SWT.PUSH); + setScreenshotButton.setText("Specify Screenshot..."); + setScreenshotButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + FileDialog fd = new FileDialog(setScreenshotButton.getShell()); + fd.setFilterExtensions(new String[] {"*.png" }); + if (mModelFile != null) { + fd.setFilterPath(mModelFile.getParent()); + } + String screenshotPath = fd.open(); + if (screenshotPath == null) { + return; + } + + ImageData[] data; + try { + data = new ImageLoader().load(screenshotPath); + } catch (Exception e) { + return; + } + + // "data" is an array, probably used to handle images that has multiple frames + // i.e. gifs or icons, we just care if it has at least one here + if (data.length < 1) { + return; + } + + mScreenshot = new Image(Display.getDefault(), data[0]); + redrawScreenshot(); + } + }); + + // right sash is split into 2 parts: upper-right and lower-right + // both are composites with borders, so that the horizontal divider can be highlighted by + // the borders + SashForm rightSash = new SashForm(baseSash, SWT.VERTICAL); + + // upper-right base contains the toolbar and the tree + Composite upperRightBase = new Composite(rightSash, SWT.BORDER); + upperRightBase.setLayout(new GridLayout(1, false)); + + ToolBarManager toolBarManager = new ToolBarManager(SWT.FLAT); + toolBarManager.add(new ExpandAllAction(this)); + toolBarManager.add(new ToggleNafAction(this)); + ToolBar searchtoolbar = toolBarManager.createControl(upperRightBase); + + // add search box and navigation buttons for search results + ToolItem itemSeparator = new ToolItem(searchtoolbar, SWT.SEPARATOR | SWT.RIGHT); + searchTextarea = new Text(searchtoolbar, SWT.BORDER | SWT.SINGLE | SWT.SEARCH); + searchTextarea.pack(); + itemSeparator.setWidth(searchTextarea.getBounds().width); + itemSeparator.setControl(searchTextarea); + itemPrev = new ToolItem(searchtoolbar, SWT.SIMPLE); + itemPrev.setImage(ImageHelper.loadImageDescriptorFromResource("images/prev.png") + .createImage()); + itemNext = new ToolItem(searchtoolbar, SWT.SIMPLE); + itemNext.setImage(ImageHelper.loadImageDescriptorFromResource("images/next.png") + .createImage()); + itemDeleteAndInfo = new ToolItem(searchtoolbar, SWT.SIMPLE); + itemDeleteAndInfo.setImage(ImageHelper.loadImageDescriptorFromResource("images/delete.png") + .createImage()); + itemDeleteAndInfo.setToolTipText("Clear search results"); + coordinateLabel = new ToolItem(searchtoolbar, SWT.SIMPLE); + coordinateLabel.setText(""); + coordinateLabel.setEnabled(false); + + // add search function + searchTextarea.addKeyListener(new KeyListener() { + @Override + public void keyReleased(KeyEvent event) { + if (event.keyCode == SWT.CR) { + String term = searchTextarea.getText(); + if (!term.isEmpty()) { + if (term.equals(mLastSearchedTerm)) { + nextSearchResult(); + return; + } + clearSearchResult(); + mSearchResult = mModel.searchNode(term); + if (!mSearchResult.isEmpty()) { + mSearchResultIndex = 0; + updateSearchResultSelection(); + mLastSearchedTerm = term; + } + } + } + } + + @Override + public void keyPressed(KeyEvent event) { + } + }); + SelectionListener l = new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent se) { + if (se.getSource() == itemPrev) { + prevSearchResult(); + } else if (se.getSource() == itemNext) { + nextSearchResult(); + } else if (se.getSource() == itemDeleteAndInfo) { + searchTextarea.setText(""); + clearSearchResult(); + } + } + }; + itemPrev.addSelectionListener(l); + itemNext.addSelectionListener(l); + itemDeleteAndInfo.addSelectionListener(l); + + searchtoolbar.pack(); + searchtoolbar.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mTreeViewer = new TreeViewer(upperRightBase, SWT.NONE); + mTreeViewer.setContentProvider(new BasicTreeNodeContentProvider()); + // default LabelProvider uses toString() to generate text to display + mTreeViewer.setLabelProvider(new LabelProvider()); + mTreeViewer.addSelectionChangedListener(new ISelectionChangedListener() { + @Override + public void selectionChanged(SelectionChangedEvent event) { + BasicTreeNode selectedNode = null; + if (event.getSelection() instanceof IStructuredSelection) { + IStructuredSelection selection = (IStructuredSelection) event.getSelection(); + Object o = selection.getFirstElement(); + if (o instanceof BasicTreeNode) { + selectedNode = (BasicTreeNode) o; + } + } + + mModel.setSelectedNode(selectedNode); + redrawScreenshot(); + if (selectedNode != null) { + loadAttributeTable(); + } + } + }); + Tree tree = mTreeViewer.getTree(); + tree.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1)); + // move focus so that it's not on tool bar (looks weird) + tree.setFocus(); + + // lower-right base contains the detail group + Composite lowerRightBase = new Composite(rightSash, SWT.BORDER); + lowerRightBase.setLayout(new FillLayout()); + Group grpNodeDetail = new Group(lowerRightBase, SWT.NONE); + grpNodeDetail.setLayout(new FillLayout(SWT.HORIZONTAL)); + grpNodeDetail.setText("Node Detail"); + + Composite tableContainer = new Composite(grpNodeDetail, SWT.NONE); + + TableColumnLayout columnLayout = new TableColumnLayout(); + tableContainer.setLayout(columnLayout); + + mTableViewer = new TableViewer(tableContainer, SWT.NONE | SWT.FULL_SELECTION); + Table table = mTableViewer.getTable(); + table.setLinesVisible(true); + // use ArrayContentProvider here, it assumes the input to the TableViewer + // is an array, where each element represents a row in the table + mTableViewer.setContentProvider(new ArrayContentProvider()); + + TableViewerColumn tableViewerColumnKey = new TableViewerColumn(mTableViewer, SWT.NONE); + TableColumn tblclmnKey = tableViewerColumnKey.getColumn(); + tableViewerColumnKey.setLabelProvider(new ColumnLabelProvider() { + @Override + public String getText(Object element) { + if (element instanceof AttributePair) { + // first column, shows the attribute name + return ((AttributePair) element).key; + } + return super.getText(element); + } + }); + columnLayout.setColumnData(tblclmnKey, + new ColumnWeightData(1, ColumnWeightData.MINIMUM_WIDTH, true)); + + TableViewerColumn tableViewerColumnValue = new TableViewerColumn(mTableViewer, SWT.NONE); + tableViewerColumnValue.setEditingSupport(new AttributeTableEditingSupport(mTableViewer)); + TableColumn tblclmnValue = tableViewerColumnValue.getColumn(); + columnLayout.setColumnData(tblclmnValue, + new ColumnWeightData(2, ColumnWeightData.MINIMUM_WIDTH, true)); + tableViewerColumnValue.setLabelProvider(new ColumnLabelProvider() { + @Override + public String getText(Object element) { + if (element instanceof AttributePair) { + // second column, shows the attribute value + return ((AttributePair) element).value; + } + return super.getText(element); + } + }); + // sets the ratio of the vertical split: left 5 vs right 3 + baseSash.setWeights(new int[] {5, 3 }); + } + + protected void prevSearchResult() { + if (mSearchResult == null) + return; + if(mSearchResult.isEmpty()){ + mSearchResult = null; + return; + } + mSearchResultIndex = mSearchResultIndex - 1; + if (mSearchResultIndex < 0){ + mSearchResultIndex += mSearchResult.size(); + } + updateSearchResultSelection(); + } + protected void clearSearchResult() { + itemDeleteAndInfo.setText(""); + mSearchResult = null; + mSearchResultIndex = 0; + mLastSearchedTerm = ""; + mScreenshotCanvas.redraw(); + } + protected void nextSearchResult() { + if (mSearchResult == null) + return; + if(mSearchResult.isEmpty()){ + mSearchResult = null; + return; + } + mSearchResultIndex = (mSearchResultIndex + 1) % mSearchResult.size(); + updateSearchResultSelection(); + } + + private void updateSearchResultSelection() { + updateTreeSelection(mSearchResult.get(mSearchResultIndex)); + itemDeleteAndInfo.setText("" + (mSearchResultIndex + 1) + "/" + + mSearchResult.size()); + } + + private int getScaledSize(int size) { + if (mScale == 1.0f) { + return size; + } else { + return new Double(Math.floor((size * mScale))).intValue(); + } + } + + private int getInverseScaledSize(int size) { + if (mScale == 1.0f) { + return size; + } else { + return new Double(Math.floor((size / mScale))).intValue(); + } + } + + private void updateScreenshotTransformation() { + Rectangle canvas = mScreenshotCanvas.getBounds(); + Rectangle image = mScreenshot.getBounds(); + float scaleX = (canvas.width - 2 * IMG_BORDER - 1) / (float) image.width; + float scaleY = (canvas.height - 2 * IMG_BORDER - 1) / (float) image.height; + + // use the smaller scale here so that we can fit the entire screenshot + mScale = Math.min(scaleX, scaleY); + // calculate translation values to center the image on the canvas + mDx = (canvas.width - getScaledSize(image.width) - IMG_BORDER * 2) / 2 + IMG_BORDER; + mDy = (canvas.height - getScaledSize(image.height) - IMG_BORDER * 2) / 2 + IMG_BORDER; + } + + private class AttributeTableEditingSupport extends EditingSupport { + + private TableViewer mViewer; + + public AttributeTableEditingSupport(TableViewer viewer) { + super(viewer); + mViewer = viewer; + } + + @Override + protected boolean canEdit(Object arg0) { + return true; + } + + @Override + protected CellEditor getCellEditor(Object arg0) { + return new TextCellEditor(mViewer.getTable()); + } + + @Override + protected Object getValue(Object o) { + return ((AttributePair) o).value; + } + + @Override + protected void setValue(Object arg0, Object arg1) { + } + } + + /** + * Causes a redraw of the canvas. + * + * The drawing code of canvas will handle highlighted nodes and etc based on data + * retrieved from Model + */ + public void redrawScreenshot() { + if (mScreenshot == null) { + mStackLayout.topControl = mSetScreenshotComposite; + } else { + mStackLayout.topControl = mScreenshotCanvas; + } + mScreenshotComposite.layout(); + + mScreenshotCanvas.redraw(); + } + + public void setInputHierarchy(Object input) { + mTreeViewer.setInput(input); + } + + public void loadAttributeTable() { + // update the lower right corner table to show the attributes of the node + mTableViewer.setInput(mModel.getSelectedNode().getAttributesArray()); + } + + public void expandAll() { + mTreeViewer.expandAll(); + } + + public void updateTreeSelection(BasicTreeNode node) { + mTreeViewer.setSelection(new StructuredSelection(node), true); + } + + public void setModel(UiAutomatorModel model, File modelBackingFile, Image screenshot) { + mModel = model; + mModelFile = modelBackingFile; + + if (mScreenshot != null) { + mScreenshot.dispose(); + } + mScreenshot = screenshot; + clearSearchResult(); + redrawScreenshot(); + // load xml into tree + BasicTreeNode wrapper = new BasicTreeNode(); + // putting another root node on top of existing root node + // because Tree seems to like to hide the root node + wrapper.addChild(mModel.getXmlRootNode()); + setInputHierarchy(wrapper); + mTreeViewer.getTree().setFocus(); + + } + + public boolean shouldShowNafNodes() { + return mModel != null ? mModel.shouldShowNafNodes() : false; + } + + public void toggleShowNaf() { + if (mModel != null) { + mModel.toggleShowNaf(); + } + } + + public Image getScreenShot() { + return mScreenshot; + } + + public File getModelFile() { + return mModelFile; + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorViewer.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorViewer.java new file mode 100644 index 00000000..1a680012 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/UiAutomatorViewer.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2012 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.uiautomator; + +import com.android.uiautomator.actions.OpenFilesAction; +import com.android.uiautomator.actions.SaveScreenShotAction; +import com.android.uiautomator.actions.ScreenshotAction; + +import org.eclipse.jface.action.ToolBarManager; +import org.eclipse.jface.window.ApplicationWindow; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +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.Display; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.ToolBar; + +import java.io.File; + +public class UiAutomatorViewer extends ApplicationWindow { + private UiAutomatorView mUiAutomatorView; + public UiAutomatorViewer() { + super(null); + } + + @Override + protected Control createContents(Composite parent) { + Composite c = new Composite(parent, SWT.BORDER); + + GridLayout gridLayout = new GridLayout(1, false); + gridLayout.marginWidth = 0; + gridLayout.marginHeight = 0; + gridLayout.horizontalSpacing = 0; + gridLayout.verticalSpacing = 0; + c.setLayout(gridLayout); + + GridData gd = new GridData(GridData.FILL_HORIZONTAL); + c.setLayoutData(gd); + + ToolBarManager toolBarManager = new ToolBarManager(SWT.FLAT); + toolBarManager.add(new OpenFilesAction(this)); + toolBarManager.add(new ScreenshotAction(this,false)); + toolBarManager.add(new ScreenshotAction(this,true)); + toolBarManager.add(new SaveScreenShotAction(this)); + ToolBar tb = toolBarManager.createControl(c); + tb.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mUiAutomatorView = new UiAutomatorView(c, SWT.BORDER); + mUiAutomatorView.setLayoutData(new GridData(GridData.FILL_BOTH)); + + return parent; + } + + public static void main(String args[]) { + DebugBridge.init(); + + try { + UiAutomatorViewer window = new UiAutomatorViewer(); + window.setBlockOnOpen(true); + window.open(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + DebugBridge.terminate(); + } + } + + @Override + protected void configureShell(Shell newShell) { + super.configureShell(newShell); + newShell.setText("UI Automator Viewer"); + } + + @Override + protected Point getInitialSize() { + return new Point(800, 600); + } + + public void setModel(final UiAutomatorModel model, final File modelFile, + final Image screenshot) { + if (Display.getDefault().getThread() != Thread.currentThread()) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + mUiAutomatorView.setModel(model, modelFile, screenshot); + } + }); + } else { + mUiAutomatorView.setModel(model, modelFile, screenshot); + } + } + public Image getScreenShot() { + return mUiAutomatorView.getScreenShot(); + } + public File getModelFile(){ + return mUiAutomatorView.getModelFile(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ExpandAllAction.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ExpandAllAction.java new file mode 100644 index 00000000..266f69f1 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ExpandAllAction.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2012 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.uiautomator.actions; + +import com.android.uiautomator.UiAutomatorView; + +import org.eclipse.jface.action.Action; +import org.eclipse.jface.resource.ImageDescriptor; + +public class ExpandAllAction extends Action { + + UiAutomatorView mView; + + public ExpandAllAction(UiAutomatorView view) { + super("&Expand All"); + mView = view;; + } + + @Override + public ImageDescriptor getImageDescriptor() { + return ImageHelper.loadImageDescriptorFromResource("images/expandall.png"); + } + + @Override + public void run() { + mView.expandAll(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ImageHelper.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ImageHelper.java new file mode 100644 index 00000000..58cfc228 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ImageHelper.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2012 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.uiautomator.actions; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.ImageLoader; + +import java.io.IOException; +import java.io.InputStream; + +public class ImageHelper { + + public static ImageDescriptor loadImageDescriptorFromResource(String path) { + InputStream is = ImageHelper.class.getClassLoader().getResourceAsStream(path); + if (is != null) { + ImageData[] data = null; + try { + data = new ImageLoader().load(is); + } catch (SWTException e) { + } finally { + try { + is.close(); + } catch (IOException e) { + } + } + if (data != null && data.length > 0) { + return ImageDescriptor.createFromImageData(data[0]); + } + } + return null; + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/OpenFilesAction.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/OpenFilesAction.java new file mode 100644 index 00000000..2e108852 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/OpenFilesAction.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2012 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.uiautomator.actions; + +import com.android.uiautomator.OpenDialog; +import com.android.uiautomator.UiAutomatorModel; +import com.android.uiautomator.UiAutomatorViewer; + +import org.eclipse.jface.action.Action; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.ImageLoader; +import org.eclipse.swt.widgets.Display; + +import java.io.File; + +public class OpenFilesAction extends Action { + private UiAutomatorViewer mViewer; + + public OpenFilesAction(UiAutomatorViewer viewer) { + super("&Open"); + + mViewer = viewer; + } + + @Override + public ImageDescriptor getImageDescriptor() { + return ImageHelper.loadImageDescriptorFromResource("images/open-folder.png"); + } + + @Override + public void run() { + OpenDialog d = new OpenDialog(Display.getDefault().getActiveShell()); + if (d.open() != OpenDialog.OK) { + return; + } + + UiAutomatorModel model; + try { + model = new UiAutomatorModel(d.getXmlDumpFile()); + } catch (Exception e) { + // FIXME: show error + return; + } + + Image img = null; + File screenshot = d.getScreenshotFile(); + if (screenshot != null) { + try { + ImageData[] data = new ImageLoader().load(screenshot.getAbsolutePath()); + + // "data" is an array, probably used to handle images that has multiple frames + // i.e. gifs or icons, we just care if it has at least one here + if (data.length < 1) { + throw new RuntimeException("Unable to load image: " + + screenshot.getAbsolutePath()); + } + + img = new Image(Display.getDefault(), data[0]); + } catch (Exception e) { + // FIXME: show error + return; + } + } + + mViewer.setModel(model, d.getXmlDumpFile(), img); + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/SaveScreenShotAction.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/SaveScreenShotAction.java new file mode 100644 index 00000000..97d6621e --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/SaveScreenShotAction.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2013 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.uiautomator.actions; + +import com.android.uiautomator.UiAutomatorViewer; +import com.google.common.io.Files; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.dialogs.ErrorDialog; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.ImageLoader; +import org.eclipse.swt.widgets.DirectoryDialog; +import org.eclipse.swt.widgets.Display; + +import java.io.File; + +public class SaveScreenShotAction extends Action { + private static final String PNG_TYPE = ".png"; + private static final String UIX_TYPE = ".uix"; + private UiAutomatorViewer mViewer; + public SaveScreenShotAction(UiAutomatorViewer viewer) { + super("&Save"); + mViewer = viewer; + } + + @Override + public ImageDescriptor getImageDescriptor() { + return ImageHelper.loadImageDescriptorFromResource("images/save.png"); + } + + @Override + public void run() { + final Image screenshot = mViewer.getScreenShot(); + final File model = mViewer.getModelFile(); + if (model == null || screenshot == null) { + return; + } + DirectoryDialog dd = new DirectoryDialog(Display.getDefault().getActiveShell()); + dd.setText("Save Screenshot and UiX Files"); + final String path = dd.open(); + if (path == null) { + return; + } + + // to prevent blocking the ui thread, we do the saving in the other thread. + new Thread(){ + String filepath; + @Override + public void run() { + filepath = new File(path, model.getName()).toString(); + filepath = filepath.substring(0,filepath.lastIndexOf(".")); + ImageLoader imageLoader = new ImageLoader(); + imageLoader.data = new ImageData[] { + screenshot.getImageData() }; + try { + imageLoader.save(filepath + PNG_TYPE, SWT.IMAGE_PNG); + Files.copy(model, new File(filepath + UIX_TYPE)); + } catch (final Exception e) { + Display.getDefault().syncExec(new Runnable() { + @Override + public void run() { + Status status = new Status(IStatus.ERROR, + "Error writing file", e.getLocalizedMessage()); + ErrorDialog.openError(Display.getDefault().getActiveShell(), + String.format("Error writing %s.uix", filepath), + e.getLocalizedMessage(), status); + } + }); + } + }; + }.start(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ScreenshotAction.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ScreenshotAction.java new file mode 100644 index 00000000..fc021097 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ScreenshotAction.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2012 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.uiautomator.actions; + +import com.android.ddmlib.IDevice; +import com.android.uiautomator.DebugBridge; +import com.android.uiautomator.UiAutomatorHelper; +import com.android.uiautomator.UiAutomatorHelper.UiAutomatorException; +import com.android.uiautomator.UiAutomatorHelper.UiAutomatorResult; +import com.android.uiautomator.UiAutomatorViewer; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.dialogs.ErrorDialog; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.dialogs.ProgressMonitorDialog; +import org.eclipse.jface.operation.IRunnableWithProgress; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; + +import java.lang.reflect.InvocationTargetException; +import java.util.List; + +public class ScreenshotAction extends Action { + UiAutomatorViewer mViewer; + private boolean mCompressed; + + public ScreenshotAction(UiAutomatorViewer viewer, boolean compressed) { + super("&Device Screenshot "+ (compressed ? "with Compressed Hierarchy" : "") + +"(uiautomator dump" + (compressed ? " --compressed)" : ")")); + mViewer = viewer; + mCompressed = compressed; + } + + @Override + public ImageDescriptor getImageDescriptor() { + if(mCompressed) + return ImageHelper.loadImageDescriptorFromResource("images/screenshotcompressed.png"); + else + return ImageHelper.loadImageDescriptorFromResource("images/screenshot.png"); + } + + @Override + public void run() { + if (!DebugBridge.isInitialized()) { + MessageDialog.openError(mViewer.getShell(), + "Error obtaining Device Screenshot", + "Unable to connect to adb. Check if adb is installed correctly."); + return; + } + + final IDevice device = pickDevice(); + if (device == null) { + return; + } + + ProgressMonitorDialog dialog = new ProgressMonitorDialog(mViewer.getShell()); + try { + dialog.run(true, false, new IRunnableWithProgress() { + @Override + public void run(IProgressMonitor monitor) throws InvocationTargetException, + InterruptedException { + UiAutomatorResult result = null; + try { + result = UiAutomatorHelper.takeSnapshot(device, monitor, mCompressed); + } catch (UiAutomatorException e) { + monitor.done(); + showError(e.getMessage(), e); + return; + } + + mViewer.setModel(result.model, result.uiHierarchy, result.screenshot); + monitor.done(); + } + }); + } catch (Exception e) { + showError("Unexpected error while obtaining UI hierarchy", e); + } + } + + private void showError(final String msg, final Throwable t) { + mViewer.getShell().getDisplay().syncExec(new Runnable() { + @Override + public void run() { + Status s = new Status(IStatus.ERROR, "Screenshot", msg, t); + ErrorDialog.openError( + mViewer.getShell(), "Error", "Error obtaining UI hierarchy", s); + } + }); + } + + private IDevice pickDevice() { + List devices = DebugBridge.getDevices(); + if (devices.size() == 0) { + MessageDialog.openError(mViewer.getShell(), + "Error obtaining Device Screenshot", + "No Android devices were detected by adb."); + return null; + } else if (devices.size() == 1) { + return devices.get(0); + } else { + DevicePickerDialog dlg = new DevicePickerDialog(mViewer.getShell(), devices); + if (dlg.open() != Window.OK) { + return null; + } + return dlg.getSelectedDevice(); + } + } + + private static class DevicePickerDialog extends Dialog { + private final List mDevices; + private final String[] mDeviceNames; + private static int sSelectedDeviceIndex; + + public DevicePickerDialog(Shell parentShell, List devices) { + super(parentShell); + + mDevices = devices; + mDeviceNames = new String[mDevices.size()]; + for (int i = 0; i < devices.size(); i++) { + mDeviceNames[i] = devices.get(i).getName(); + } + } + + @Override + protected Control createDialogArea(Composite parentShell) { + Composite parent = (Composite) super.createDialogArea(parentShell); + Composite c = new Composite(parent, SWT.NONE); + + c.setLayout(new GridLayout(2, false)); + + Label l = new Label(c, SWT.NONE); + l.setText("Select device: "); + + final Combo combo = new Combo(c, SWT.BORDER | SWT.READ_ONLY); + combo.setItems(mDeviceNames); + int defaultSelection = + sSelectedDeviceIndex < mDevices.size() ? sSelectedDeviceIndex : 0; + combo.select(defaultSelection); + sSelectedDeviceIndex = defaultSelection; + + combo.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent arg0) { + sSelectedDeviceIndex = combo.getSelectionIndex(); + } + }); + + return parent; + } + + public IDevice getSelectedDevice() { + return mDevices.get(sSelectedDeviceIndex); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ToggleNafAction.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ToggleNafAction.java new file mode 100644 index 00000000..082e219f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/actions/ToggleNafAction.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2012 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.uiautomator.actions; + +import com.android.uiautomator.UiAutomatorView; + +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.resource.ImageDescriptor; + +public class ToggleNafAction extends Action { + private UiAutomatorView mView; + + public ToggleNafAction(UiAutomatorView view) { + super("&Toggle NAF Nodes", IAction.AS_CHECK_BOX); + setChecked(view.shouldShowNafNodes()); + + mView = view; + } + + @Override + public ImageDescriptor getImageDescriptor() { + return ImageHelper.loadImageDescriptorFromResource("images/warning.png"); + } + + @Override + public void run() { + mView.toggleShowNaf(); + mView.redrawScreenshot(); + setChecked(mView.shouldShowNafNodes()); + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/AttributePair.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/AttributePair.java new file mode 100644 index 00000000..f68b6f73 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/AttributePair.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2012 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.uiautomator.tree; + +public class AttributePair { + public String key, value; + + public AttributePair(String key, String value) { + this.key = key; + this.value = value; + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/BasicTreeNode.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/BasicTreeNode.java new file mode 100644 index 00000000..8c5bc1d8 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/BasicTreeNode.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2012 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.uiautomator.tree; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class BasicTreeNode { + + private static final BasicTreeNode[] CHILDREN_TEMPLATE = new BasicTreeNode[] {}; + + protected BasicTreeNode mParent; + + protected final List mChildren = new ArrayList(); + + public int x, y, width, height; + + // whether the boundary fields are applicable for the node or not + // RootWindowNode has no bounds, but UiNodes should + protected boolean mHasBounds = false; + + public void addChild(BasicTreeNode child) { + if (child == null) { + throw new NullPointerException("Cannot add null child"); + } + if (mChildren.contains(child)) { + throw new IllegalArgumentException("node already a child"); + } + mChildren.add(child); + child.mParent = this; + } + + public List getChildrenList() { + return Collections.unmodifiableList(mChildren); + } + + public BasicTreeNode[] getChildren() { + return mChildren.toArray(CHILDREN_TEMPLATE); + } + + public BasicTreeNode getParent() { + return mParent; + } + + public boolean hasChild() { + return mChildren.size() != 0; + } + + public int getChildCount() { + return mChildren.size(); + } + + public void clearAllChildren() { + for (BasicTreeNode child : mChildren) { + child.clearAllChildren(); + } + mChildren.clear(); + } + + /** + * + * Find nodes in the tree containing the coordinate + * + * The found node should have bounds covering the coordinate, and none of its children's + * bounds covers it. Depending on the layout, some app may have multiple nodes matching it, + * the caller must provide a {@link IFindNodeListener} to receive all found nodes + * + * @param px + * @param py + * @return + */ + public boolean findLeafMostNodesAtPoint(int px, int py, IFindNodeListener listener) { + boolean foundInChild = false; + for (BasicTreeNode node : mChildren) { + foundInChild |= node.findLeafMostNodesAtPoint(px, py, listener); + } + // checked all children, if at least one child covers the point, return directly + if (foundInChild) return true; + // check self if the node has no children, or no child nodes covers the point + if (mHasBounds) { + if (x <= px && px <= x + width && y <= py && py <= y + height) { + listener.onFoundNode(this); + return true; + } else { + return false; + } + } else { + return false; + } + } + + public Object[] getAttributesArray () { + return null; + }; + + public static interface IFindNodeListener { + void onFoundNode(BasicTreeNode node); + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/BasicTreeNodeContentProvider.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/BasicTreeNodeContentProvider.java new file mode 100644 index 00000000..3fcd90d8 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/BasicTreeNodeContentProvider.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2012 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.uiautomator.tree; + + +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.Viewer; + +public class BasicTreeNodeContentProvider implements ITreeContentProvider { + + private static final Object[] EMPTY_ARRAY = {}; + + @Override + public void dispose() { + } + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + } + + @Override + public Object[] getElements(Object inputElement) { + return getChildren(inputElement); + } + + @Override + public Object[] getChildren(Object parentElement) { + if (parentElement instanceof BasicTreeNode) { + return ((BasicTreeNode)parentElement).getChildren(); + } + return EMPTY_ARRAY; + } + + @Override + public Object getParent(Object element) { + if (element instanceof BasicTreeNode) { + return ((BasicTreeNode)element).getParent(); + } + return null; + } + + @Override + public boolean hasChildren(Object element) { + if (element instanceof BasicTreeNode) { + return ((BasicTreeNode) element).hasChild(); + } + return false; + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/RootWindowNode.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/RootWindowNode.java new file mode 100644 index 00000000..0d2eae4f --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/RootWindowNode.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2012 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.uiautomator.tree; + + + +public class RootWindowNode extends BasicTreeNode { + + private final String mWindowName; + private Object[] mCachedAttributesArray; + private int mRotation; + + public RootWindowNode(String windowName) { + this(windowName, 0); + } + + public RootWindowNode(String windowName, int rotation) { + mWindowName = windowName; + mRotation = rotation; + } + + @Override + public String toString() { + return mWindowName; + } + + @Override + public Object[] getAttributesArray() { + if (mCachedAttributesArray == null) { + mCachedAttributesArray = new Object[]{new AttributePair("window-name", mWindowName)}; + } + return mCachedAttributesArray; + } + + public int getRotation() { + return mRotation; + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/UiHierarchyXmlLoader.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/UiHierarchyXmlLoader.java new file mode 100644 index 00000000..44124f87 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/UiHierarchyXmlLoader.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2012 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.uiautomator.tree; + +import org.eclipse.swt.graphics.Rectangle; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +public class UiHierarchyXmlLoader { + + private BasicTreeNode mRootNode; + private List mNafNodes; + private List mNodeList; + public UiHierarchyXmlLoader() { + } + + /** + * Uses a SAX parser to process XML dump + * @param xmlPath + * @return + */ + public BasicTreeNode parseXml(String xmlPath) { + mRootNode = null; + mNafNodes = new ArrayList(); + mNodeList = new ArrayList(); + // standard boilerplate to get a SAX parser + SAXParserFactory factory = SAXParserFactory.newInstance(); + SAXParser parser = null; + try { + parser = factory.newSAXParser(); + } catch (ParserConfigurationException e) { + e.printStackTrace(); + return null; + } catch (SAXException e) { + e.printStackTrace(); + return null; + } + // handler class for SAX parser to receiver standard parsing events: + // e.g. on reading "", startElement is called, on reading "", + // endElement is called + DefaultHandler handler = new DefaultHandler(){ + BasicTreeNode mParentNode; + BasicTreeNode mWorkingNode; + @Override + public void startElement(String uri, String localName, String qName, + Attributes attributes) throws SAXException { + boolean nodeCreated = false; + // starting an element implies that the element that has not yet been closed + // will be the parent of the element that is being started here + mParentNode = mWorkingNode; + if ("hierarchy".equals(qName)) { + int rotation = 0; + for (int i = 0; i < attributes.getLength(); i++) { + if ("rotation".equals(attributes.getQName(i))) { + try { + rotation = Integer.parseInt(attributes.getValue(i)); + } catch (NumberFormatException nfe) { + // do nothing + } + } + } + mWorkingNode = new RootWindowNode(attributes.getValue("windowName"), rotation); + nodeCreated = true; + } else if ("node".equals(qName)) { + UiNode tmpNode = new UiNode(); + for (int i = 0; i < attributes.getLength(); i++) { + tmpNode.addAtrribute(attributes.getQName(i), attributes.getValue(i)); + } + mWorkingNode = tmpNode; + nodeCreated = true; + // check if current node is NAF + String naf = tmpNode.getAttribute("NAF"); + if ("true".equals(naf)) { + mNafNodes.add(new Rectangle(tmpNode.x, tmpNode.y, + tmpNode.width, tmpNode.height)); + } + } + // nodeCreated will be false if the element started is neither + // "hierarchy" nor "node" + if (nodeCreated) { + if (mRootNode == null) { + // this will only happen once + mRootNode = mWorkingNode; + } + if (mParentNode != null) { + mParentNode.addChild(mWorkingNode); + mNodeList.add(mWorkingNode); + } + } + } + + @Override + public void endElement(String uri, String localName, String qName) throws SAXException { + //mParentNode should never be null here in a well formed XML + if (mParentNode != null) { + // closing an element implies that we are back to working on + // the parent node of the element just closed, i.e. continue to + // parse more child nodes + mWorkingNode = mParentNode; + mParentNode = mParentNode.getParent(); + } + } + }; + try { + parser.parse(new File(xmlPath), handler); + } catch (SAXException e) { + e.printStackTrace(); + return null; + } catch (IOException e) { + e.printStackTrace(); + return null; + } + return mRootNode; + } + + /** + * Returns the list of "Not Accessibility Friendly" nodes found during parsing. + * + * Call this function after parsing + * + * @return + */ + public List getNafNodes() { + return Collections.unmodifiableList(mNafNodes); + } + + public List getAllNodes(){ + return mNodeList; + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/UiNode.java b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/UiNode.java new file mode 100644 index 00000000..5b5db393 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/com/android/uiautomator/tree/UiNode.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2012 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.uiautomator.tree; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class UiNode extends BasicTreeNode { + private static final Pattern BOUNDS_PATTERN = Pattern + .compile("\\[-?(\\d+),-?(\\d+)\\]\\[-?(\\d+),-?(\\d+)\\]"); + // use LinkedHashMap to preserve the order of the attributes + private final Map mAttributes = new LinkedHashMap(); + private String mDisplayName = "ShouldNotSeeMe"; + private Object[] mCachedAttributesArray; + + public void addAtrribute(String key, String value) { + mAttributes.put(key, value); + updateDisplayName(); + if ("bounds".equals(key)) { + updateBounds(value); + } + } + + public Map getAttributes() { + return Collections.unmodifiableMap(mAttributes); + } + + /** + * Builds the display name based on attributes of the node + */ + private void updateDisplayName() { + String className = mAttributes.get("class"); + if (className == null) + return; + String text = mAttributes.get("text"); + if (text == null) + return; + String contentDescription = mAttributes.get("content-desc"); + if (contentDescription == null) + return; + String index = mAttributes.get("index"); + if (index == null) + return; + String bounds = mAttributes.get("bounds"); + if (bounds == null) { + return; + } + // shorten the standard class names, otherwise it takes up too much space on UI + className = className.replace("android.widget.", ""); + className = className.replace("android.view.", ""); + StringBuilder builder = new StringBuilder(); + builder.append('('); + builder.append(index); + builder.append(") "); + builder.append(className); + if (!text.isEmpty()) { + builder.append(':'); + builder.append(text); + } + if (!contentDescription.isEmpty()) { + builder.append(" {"); + builder.append(contentDescription); + builder.append('}'); + } + builder.append(' '); + builder.append(bounds); + mDisplayName = builder.toString(); + } + + private void updateBounds(String bounds) { + Matcher m = BOUNDS_PATTERN.matcher(bounds); + if (m.matches()) { + x = Integer.parseInt(m.group(1)); + y = Integer.parseInt(m.group(2)); + width = Integer.parseInt(m.group(3)) - x; + height = Integer.parseInt(m.group(4)) - y; + mHasBounds = true; + } else { + throw new RuntimeException("Invalid bounds: " + bounds); + } + } + + @Override + public String toString() { + return mDisplayName; + } + + public String getAttribute(String key) { + return mAttributes.get(key); + } + + @Override + public Object[] getAttributesArray() { + // this approach means we do not handle the situation where an attribute is added + // after this function is first called. This is currently not a concern because the + // tree is supposed to be readonly + if (mCachedAttributesArray == null) { + mCachedAttributesArray = new Object[mAttributes.size()]; + int i = 0; + for (String attr : mAttributes.keySet()) { + mCachedAttributesArray[i++] = new AttributePair(attr, mAttributes.get(attr)); + } + } + return mCachedAttributesArray; + } +} diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/delete.png b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/delete.png new file mode 100644 index 0000000000000000000000000000000000000000..f3e53d7596c135fccdfd8710d01201f29f003d34 GIT binary patch literal 1445 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 AjsO4v literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/open-folder.png b/andmore-swt/org.eclipse.andmore.uiautomatorviewer/src/main/java/images/open-folder.png new file mode 100644 index 0000000000000000000000000000000000000000..8c4a2e1b7e6fe6685db5e2e4c26b57eef74043f7 GIT binary patch literal 383 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 + + 4.0.0 + + + org.eclipse.andmore + andmore-parent + 0.5.2-SNAPSHOT + + + swt-droid-parent + SWT Android Parent + pom + + + + UTF-8 + UTF-8 + + + + org.eclipse.andmore.swt + org.eclipse.andmore.swtmenubar + org.eclipse.andmore.ddmuilib + org.eclipse.andmore.sdkuilib + org.eclipse.andmore.sdkstats + org.eclipse.andmore.traceviewuilib + org.eclipse.andmore.hierarchyviewer2lib + org.eclipse.andmore.uiautomatorviewer + features/org.eclipse.andmore.swt + features/org.eclipse.andmore.swtmenubar + features/org.eclipse.andmore.ddmuilib + features/org.eclipse.andmore.sdkuilib + features/org.eclipse.andmore.sdkstats + features/org.eclipse.andmore.traceviewuilib + features/org.eclipse.andmore.hierarchyviewer2lib + features/org.eclipse.andmore.uiautomatorviewer + + diff --git a/andmore.target/.gitignore b/andmore.target/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/andmore.target/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/andmore.target/.project b/andmore.target/.project new file mode 100644 index 00000000..a894551a --- /dev/null +++ b/andmore.target/.project @@ -0,0 +1,17 @@ + + + andmore.target + + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + + diff --git a/andmore.target/.settings/org.eclipse.m2e.core.prefs b/andmore.target/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore.target/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore.target/andmore.target.target b/andmore.target/andmore.target.target new file mode 100644 index 00000000..c5d38472 --- /dev/null +++ b/andmore.target/andmore.target.target @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/andmore.target/pom.xml b/andmore.target/pom.xml new file mode 100644 index 00000000..d1cfd091 --- /dev/null +++ b/andmore.target/pom.xml @@ -0,0 +1,10 @@ + + 4.0.0 + + org.eclipse.andmore + andmore-parent + 0.5.2-SNAPSHOT + + andmore.target + eclipse-target-definition + \ No newline at end of file diff --git a/android-core/plugins/org.eclipse.andmore/andmore.target b/android-core/plugins/org.eclipse.andmore/andmore.target deleted file mode 100644 index 7f2d567f..00000000 --- a/android-core/plugins/org.eclipse.andmore/andmore.target +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/pom.xml b/pom.xml index f91d9973..a38750e0 100644 --- a/pom.xml +++ b/pom.xml @@ -1,307 +1,348 @@ - - - 4.0.0 - org.eclipse.andmore - andmore-parent - 0.5.2-SNAPSHOT - Andmore Parent - pom - - https://eclipse.org/andmore - - - Eclipse Foundation - http://eclipse.org - - - - - andmore-dev Mailing List - andmore-dev@eclipse.org - https://dev.eclipse.org/mailman/listinfo/andmore-dev - https://dev.eclipse.org/mailman/listinfo/andmore-dev - http://dev.eclipse.org/mhonarc/lists/andmore-dev - - - - - https://bugs.eclipse.org/bugs/buglist.cgi?query_format=advanced;product=Andmore;classification=Tools - Bugzilla - - - - Hudson - https://hudson.eclipse.org/andmore - - - - 0.26.0 - http://download.eclipse.org/releases/neon/ - http://download.eclipse.org/tools/orbit/downloads/drops/R20160520211859/repository - http://download.eclipse.org/sequoyah/updates/2.1/ - http://download.eclipse.org/technology/m2e/releases/ - - - - - sequoyah - p2 - ${sequoyah-site} - - - eclipse - p2 - ${eclipse-site} - - - orbit - p2 - ${orbit-site} - - - m2e-site - ${m2e-update-site} - p2 - - - license-feature - http://download.eclipse.org/cbi/updates/license/ - p2 - - - - - - cbi-releases - https://repo.eclipse.org/content/repositories/cbi-releases/ - - - cbi-snapshots - https://repo.eclipse.org/content/repositories/cbi-snapshots/ - - - - - - repo.eclipse.org - Andmore Maven Repository - Releases - https://repo.eclipse.org/content/repositories/andmore-releases/ - - - repo.eclipse.org - Andmore Maven Repository - Snapshots - https://repo.eclipse.org/content/repositories/andmore-snapshots/ - true - - - - - android-core - andmore-core - - - - src - - - - org.eclipse.tycho - tycho-maven-plugin - ${tycho-version} - true - - - org.eclipse.tycho - target-platform-configuration - ${tycho-version} - - - org.eclipse.tycho - tycho-p2-plugin - ${tycho-version} - - - org.eclipse.tycho.extras - tycho-source-feature-plugin - ${tycho-version} - - - org.eclipse.tycho.extras - tycho-document-bundle-plugin - ${tycho-version} - - - org.eclipse.tycho.extras - tycho-pack200a-plugin - ${tycho-version} - - - org.eclipse.tycho.extras - tycho-pack200b-plugin - ${tycho-version} - - - org.eclipse.cbi.maven.plugins - eclipse-jarsigner-plugin - 1.1.2 - - - org.codehaus.mojo - findbugs-maven-plugin - 3.0.0 - - - - - - org.eclipse.tycho - tycho-maven-plugin - true - - - org.eclipse.tycho.extras - tycho-source-feature-plugin - - - org.eclipse.tycho - target-platform-configuration - - p2 - - - macosx - cocoa - x86_64 - - - linux - gtk - x86_64 - - - linux - gtk - x86 - - - win32 - win32 - x86 - - - win32 - win32 - x86_64 - - - - - - - - - - luna - - http://download.eclipse.org/releases/luna/ - - - - findbugs - - - - org.codehaus.mojo - findbugs-maven-plugin - - true - false - findbugsExclusion.xml - - - - - check - - - - - - - - - sign - - - - org.eclipse.tycho - target-platform-configuration - - true - - - - org.eclipse.tycho.extras - tycho-pack200a-plugin - - - pack200-normalize - - normalize - - verify - - - - - org.eclipse.cbi.maven.plugins - eclipse-jarsigner-plugin - - true - - - - sign - - sign - - verify - - - - - org.eclipse.tycho.extras - tycho-pack200b-plugin - - - pack200-pack - - pack - - verify - - - - - org.eclipse.tycho - tycho-p2-plugin - - - p2-metadata - - p2-metadata - - verify - - - - false - - - - - - - + + + 4.0.0 + org.eclipse.andmore + andmore-parent + 0.5.2-SNAPSHOT + Andmore Parent + pom + + https://eclipse.org/andmore + + + Eclipse Foundation + http://eclipse.org + + + + + andmore-dev Mailing List + andmore-dev@eclipse.org + https://dev.eclipse.org/mailman/listinfo/andmore-dev + https://dev.eclipse.org/mailman/listinfo/andmore-dev + http://dev.eclipse.org/mhonarc/lists/andmore-dev + + + + + https://bugs.eclipse.org/bugs/buglist.cgi?query_format=advanced;product=Andmore;classification=Tools + Bugzilla + + + + Hudson + https://hudson.eclipse.org/andmore + + + + 1.8 + 1.0.0 + http://download.eclipse.org/releases/neon/ + http://download.eclipse.org/tools/orbit/R-builds/R20170307180635/repository + http://download.eclipse.org/technology/m2e/releases/ + 2.3.3 + 25.3.3 + + + + + eclipse + p2 + ${eclipse-site} + + + orbit + p2 + ${orbit-site} + + + m2e-site + ${m2e-update-site} + p2 + + + license-feature + http://download.eclipse.org/cbi/updates/license/ + p2 + + + + homer-core + homer-core + https://nexus.arcsmed.at/content/repositories/homer.core/ + + false + + + + + + + cbi-releases + https://repo.eclipse.org/content/repositories/cbi-releases/ + + + cbi-snapshots + https://repo.eclipse.org/content/repositories/cbi-snapshots/ + + + + + + repo.eclipse.org + Andmore Maven Repository - Releases + https://repo.eclipse.org/content/repositories/andmore-releases/ + + + repo.eclipse.org + Andmore Maven Repository - Snapshots + https://repo.eclipse.org/content/repositories/andmore-snapshots/ + true + + + + + andmore-swt + android-core + andmore-core + andmore.target + + + + src + + + + org.eclipse.tycho + tycho-maven-plugin + ${tycho-version} + true + + + org.eclipse.tycho + target-platform-configuration + ${tycho-version} + + + org.eclipse.tycho + tycho-p2-plugin + ${tycho-version} + + + org.eclipse.tycho.extras + tycho-source-feature-plugin + ${tycho-version} + + + org.eclipse.tycho.extras + tycho-document-bundle-plugin + ${tycho-version} + + + org.eclipse.tycho.extras + tycho-pack200a-plugin + ${tycho-version} + + + org.eclipse.tycho.extras + tycho-pack200b-plugin + ${tycho-version} + + + org.eclipse.cbi.maven.plugins + eclipse-jarsigner-plugin + 1.1.2 + + + org.codehaus.mojo + findbugs-maven-plugin + 3.0.0 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.5.1 + + ${project.java.version} + ${project.java.version} + ${project.java.version} + ${project.java.version} + ${project.build.sourceEncoding} + true + -Xlint:none + -Xlint:none + + + + org.eclipse.tycho + tycho-compiler-plugin + ${tycho-version} + + ${project.java.version} + ${project.java.version} + false + -Xlint:none + + + + org.eclipse.tycho + tycho-maven-plugin + true + + + org.eclipse.tycho.extras + tycho-source-feature-plugin + + + org.eclipse.tycho + target-platform-configuration + + p2 + + + org.eclipse.andmore + andmore.target + ${project.version} + + + + + macosx + cocoa + x86_64 + + + linux + gtk + x86_64 + + + linux + gtk + x86 + + + win32 + win32 + x86 + + + win32 + win32 + x86_64 + + + + + + + + + + luna + + http://download.eclipse.org/releases/luna/ + + + + findbugs + + + + org.codehaus.mojo + findbugs-maven-plugin + + true + false + findbugsExclusion.xml + + + + + check + + + + + + + + + sign + + + + org.eclipse.tycho + target-platform-configuration + + true + + + + org.eclipse.tycho.extras + tycho-pack200a-plugin + + + pack200-normalize + + normalize + + verify + + + + + org.eclipse.cbi.maven.plugins + eclipse-jarsigner-plugin + + true + + + + sign + + sign + + verify + + + + + org.eclipse.tycho.extras + tycho-pack200b-plugin + + + pack200-pack + + pack + + verify + + + + + org.eclipse.tycho + tycho-p2-plugin + + + p2-metadata + + p2-metadata + + verify + + + + false + + + + + + + From ec019cc329efc379097f6ad511b522cd8891ffe3 Mon Sep 17 00:00:00 2001 From: Andrew Bowley Date: Tue, 14 Nov 2017 19:36:38 +1100 Subject: [PATCH 2/5] [Bug 526437] Android SWT Libraries need to be forked 1. Fixed relative paths to parent pom in andmore-swt feature poms 2. Adjusted target file to reduce early workbench load errors 3. Added missing library jar dependency jar download in ddmuilib pom Bug: https://bugs.eclipse.org/bugs/show_bug.cgi?id=526437 Signed-off-by: Andrew Bowley --- .../org.eclipse.andmore.ddmuilib/pom.xml | 2 +- .../pom.xml | 2 +- .../org.eclipse.andmore.sdkstats/pom.xml | 2 +- .../org.eclipse.andmore.sdkuilib/pom.xml | 2 +- .../features/org.eclipse.andmore.swt/pom.xml | 2 +- .../org.eclipse.andmore.swtmenubar/pom.xml | 2 +- .../pom.xml | 2 +- .../pom.xml | 2 +- .../org.eclipse.andmore.ddmuilib/pom.xml | 1 - andmore-swt/org.eclipse.andmore.swt/pom.xml | 20 +++++-------------- andmore.target/andmore.target.target | 20 +++++++++++++------ 11 files changed, 27 insertions(+), 30 deletions(-) diff --git a/andmore-swt/features/org.eclipse.andmore.ddmuilib/pom.xml b/andmore-swt/features/org.eclipse.andmore.ddmuilib/pom.xml index 46037543..f489559b 100644 --- a/andmore-swt/features/org.eclipse.andmore.ddmuilib/pom.xml +++ b/andmore-swt/features/org.eclipse.andmore.ddmuilib/pom.xml @@ -8,7 +8,7 @@ ddmuilib - ../pom.xml + ../../pom.xml org.eclipse.andmore swt-droid-parent 0.5.2-SNAPSHOT diff --git a/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/pom.xml b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/pom.xml index cfb8cc1e..08a4a37e 100644 --- a/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/pom.xml +++ b/andmore-swt/features/org.eclipse.andmore.hierarchyviewer2lib/pom.xml @@ -8,7 +8,7 @@ hierarchyviewer2lib - ../pom.xml + ../../pom.xml org.eclipse.andmore swt-droid-parent 0.5.2-SNAPSHOT diff --git a/andmore-swt/features/org.eclipse.andmore.sdkstats/pom.xml b/andmore-swt/features/org.eclipse.andmore.sdkstats/pom.xml index 6c3e500b..7c24d460 100644 --- a/andmore-swt/features/org.eclipse.andmore.sdkstats/pom.xml +++ b/andmore-swt/features/org.eclipse.andmore.sdkstats/pom.xml @@ -8,7 +8,7 @@ sdkstats - ../pom.xml + ../../pom.xml org.eclipse.andmore swt-droid-parent 0.5.2-SNAPSHOT diff --git a/andmore-swt/features/org.eclipse.andmore.sdkuilib/pom.xml b/andmore-swt/features/org.eclipse.andmore.sdkuilib/pom.xml index a0b56f88..2858670e 100644 --- a/andmore-swt/features/org.eclipse.andmore.sdkuilib/pom.xml +++ b/andmore-swt/features/org.eclipse.andmore.sdkuilib/pom.xml @@ -8,7 +8,7 @@ sdkuilib - ../pom.xml + ../../pom.xml org.eclipse.andmore swt-droid-parent 0.5.2-SNAPSHOT diff --git a/andmore-swt/features/org.eclipse.andmore.swt/pom.xml b/andmore-swt/features/org.eclipse.andmore.swt/pom.xml index 7c9ed59b..f2b437b8 100644 --- a/andmore-swt/features/org.eclipse.andmore.swt/pom.xml +++ b/andmore-swt/features/org.eclipse.andmore.swt/pom.xml @@ -8,7 +8,7 @@ swt base - ../pom.xml + ../../pom.xml org.eclipse.andmore swt-droid-parent 0.5.2-SNAPSHOT diff --git a/andmore-swt/features/org.eclipse.andmore.swtmenubar/pom.xml b/andmore-swt/features/org.eclipse.andmore.swtmenubar/pom.xml index c291652c..314d1759 100644 --- a/andmore-swt/features/org.eclipse.andmore.swtmenubar/pom.xml +++ b/andmore-swt/features/org.eclipse.andmore.swtmenubar/pom.xml @@ -8,7 +8,7 @@ swtmenubar - ../pom.xml + ../../pom.xml org.eclipse.andmore swt-droid-parent 0.5.2-SNAPSHOT diff --git a/andmore-swt/features/org.eclipse.andmore.traceviewuilib/pom.xml b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/pom.xml index 40391cff..fb108a9f 100644 --- a/andmore-swt/features/org.eclipse.andmore.traceviewuilib/pom.xml +++ b/andmore-swt/features/org.eclipse.andmore.traceviewuilib/pom.xml @@ -8,7 +8,7 @@ traceviewuilib - ../pom.xml + ../../pom.xml org.eclipse.andmore swt-droid-parent 0.5.2-SNAPSHOT diff --git a/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/pom.xml b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/pom.xml index 8c566cf8..02b62a9d 100644 --- a/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/pom.xml +++ b/andmore-swt/features/org.eclipse.andmore.uiautomatorviewer/pom.xml @@ -8,7 +8,7 @@ uiautomatorviewer - ../pom.xml + ../../pom.xml org.eclipse.andmore swt-droid-parent 0.5.2-SNAPSHOT diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/pom.xml b/andmore-swt/org.eclipse.andmore.ddmuilib/pom.xml index 3a905784..31b0fb70 100644 --- a/andmore-swt/org.eclipse.andmore.ddmuilib/pom.xml +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/pom.xml @@ -7,7 +7,6 @@ eclipse-plugin ddmuilib - ../pom.xml org.eclipse.andmore swt-droid-parent 0.5.2-SNAPSHOT diff --git a/andmore-swt/org.eclipse.andmore.swt/pom.xml b/andmore-swt/org.eclipse.andmore.swt/pom.xml index 52c173a0..3e48751e 100644 --- a/andmore-swt/org.eclipse.andmore.swt/pom.xml +++ b/andmore-swt/org.eclipse.andmore.swt/pom.xml @@ -104,21 +104,6 @@ - - com.android.tools.build - builder-model - 2.3.3 - - - org.bouncycastle - bcpkix-jdk15on - 1.48 - - - org.bouncycastle - bcprov-jdk15on - 1.48 - com.intellij annotations @@ -208,6 +193,11 @@ com.android.tools.ddms ddmlib + ${android.tools.version} + + + com.android.tools.layoutlib + layoutlib-api ${android.tools.version} diff --git a/andmore.target/andmore.target.target b/andmore.target/andmore.target.target index c5d38472..bcbee37b 100644 --- a/andmore.target/andmore.target.target +++ b/andmore.target/andmore.target.target @@ -2,7 +2,9 @@ - + + + @@ -12,7 +14,6 @@ - @@ -28,9 +29,6 @@ - - - @@ -65,10 +63,13 @@ + + - + + @@ -83,6 +84,13 @@ + + + + + + + From ac4ee26a6e2574c3bcf19986cc5219b810577fc0 Mon Sep 17 00:00:00 2001 From: Andrew Bowley Date: Thu, 23 Nov 2017 13:42:54 +1100 Subject: [PATCH 3/5] [Bug 526437] - Android SWT Libraries need to be forked Replaced sdkuilib module with one rewritten to work with Android API 25 libraries. This required bringing all of Andmore up to API 25 and hence merged changes from separate branch for https://bugs.eclipse.org/bugs/show_bug.cgi?id=525493 Merged Matthew Piggott branch android-n.1 from https://github.com/mpiggott/andmore.git. (next 5 changes) Updated Android SDK version to 25.3.3, lint libraries included. Modified android-core "adt" module for bug 525493 "Projects containing aar libraries currently fail with robolectric 3.2.1" Replaced DexWrapper with JDT Java application launch. Removed legacy SWT jars Updated compile source and Java execution configuration settings universally to JavaSE-1.8. Transferred ADT module IDE windows for SDK packages and devices to sdkuilib. Note AdtUpdateDialog for installing individual packages is incomplete, impacting new project and new installation wizards. Replaced all code for loading images to comply with Eclipse best practice. In this process image files were relocated to dedicated bundle directories. Removed all jar files from distribution which are available online. Maven now downloads them on "initialize" goal. Bug: https://bugs.eclipse.org/bugs/show_bug.cgi?id=526437 Signed-off-by: Andrew Bowley --- andmore-core/features/feature/feature.xml | 1 - .../plugins/android.codeutils/.classpath | 14 +- .../.settings/org.eclipse.jdt.core.prefs | 14 +- .../android.codeutils/META-INF/MANIFEST.MF | 68 +- .../plugins/android.win32.x86_64/.classpath | 14 +- .../.settings/org.eclipse.jdt.core.prefs | 14 +- .../android.win32.x86_64/META-INF/MANIFEST.MF | 20 +- andmore-core/plugins/android/.classpath | 14 +- .../.settings/org.eclipse.jdt.core.prefs | 14 +- .../plugins/android/META-INF/MANIFEST.MF | 94 +- .../org/eclipse/andmore/android/SdkUtils.java | 1901 +++--- .../android/multidex/MultiDexManager.java | 12 +- .../wizards/elements/SdkTargetSelector.java | 632 +- .../wizards/project/TreeContentProvider.java | 259 +- andmore-core/plugins/certmanager/.classpath | 18 +- .../.settings/org.eclipse.jdt.core.prefs | 14 +- .../plugins/certmanager/META-INF/MANIFEST.MF | 64 +- andmore-core/plugins/common/.classpath | 14 +- .../.settings/org.eclipse.jdt.core.prefs | 14 +- .../plugins/common/META-INF/MANIFEST.MF | 76 +- andmore-core/plugins/db.core/.classpath | 14 +- .../.settings/org.eclipse.jdt.core.prefs | 14 +- .../plugins/db.core/META-INF/MANIFEST.MF | 64 +- andmore-core/plugins/db.devices/.classpath | 14 +- .../.settings/org.eclipse.jdt.core.prefs | 14 +- .../plugins/db.devices/META-INF/MANIFEST.MF | 44 +- .../plugins/devices.services/.classpath | 14 +- .../.settings/org.eclipse.jdt.core.prefs | 14 +- .../devices.services/META-INF/MANIFEST.MF | 42 +- andmore-core/plugins/emulator/.classpath | 14 +- .../.settings/org.eclipse.jdt.core.prefs | 14 +- .../plugins/emulator/META-INF/MANIFEST.MF | 95 +- .../instance/AndroidDeviceInstance.java | 9 +- .../device/ui/PropertiesMainComposite.java | 19 +- andmore-core/plugins/handset/.classpath | 14 +- .../.settings/org.eclipse.jdt.core.prefs | 14 +- .../plugins/handset/META-INF/MANIFEST.MF | 40 +- andmore-core/plugins/installer/.classpath | 14 +- .../.settings/org.eclipse.jdt.core.prefs | 14 +- .../plugins/installer/META-INF/MANIFEST.MF | 80 +- andmore-core/plugins/launch/.classpath | 14 +- .../.settings/org.eclipse.jdt.core.prefs | 14 +- .../plugins/launch/META-INF/MANIFEST.MF | 50 +- .../plugins/logger.collector/.classpath | 14 +- .../.settings/org.eclipse.jdt.core.prefs | 14 +- .../logger.collector/META-INF/MANIFEST.MF | 28 +- andmore-core/plugins/logger/.classpath | 16 +- .../.settings/org.eclipse.jdt.core.prefs | 14 +- .../plugins/logger/META-INF/MANIFEST.MF | 26 +- andmore-core/plugins/mat/.classpath | 14 +- .../mat/.settings/org.eclipse.jdt.core.prefs | 14 +- andmore-core/plugins/mat/META-INF/MANIFEST.MF | 42 +- andmore-core/plugins/packaging.ui/.classpath | 14 +- .../.settings/org.eclipse.jdt.core.prefs | 14 +- .../plugins/packaging.ui/META-INF/MANIFEST.MF | 44 +- andmore-core/plugins/remote.device/.classpath | 14 +- .../.settings/org.eclipse.jdt.core.prefs | 14 +- .../remote.device/META-INF/MANIFEST.MF | 40 +- andmore-core/plugins/snippets/.classpath | 14 +- .../.settings/org.eclipse.jdt.core.prefs | 14 +- .../plugins/snippets/META-INF/MANIFEST.MF | 38 +- andmore-core/plugins/translation/.classpath | 14 +- .../.settings/org.eclipse.jdt.core.prefs | 14 +- .../plugins/translation/META-INF/MANIFEST.MF | 52 +- .../org.eclipse.andmore.ddmuilib/.classpath | 1 - .../META-INF/MANIFEST.MF | 63 +- .../org.eclipse.andmore.ddmuilib/plugin.xml | 5 + .../com/android/ddmuilib/AllocationPanel.java | 16 +- .../com/android/ddmuilib/DevicePanel.java | 83 +- .../ddmuilib/EmulatorControlPanel.java | 21 +- .../java/com/android/ddmuilib/HeapPanel.java | 16 +- .../com/android/ddmuilib/ImageLoader.java | 206 - .../ddmuilib/ReplacementImageFactory.java | 66 + .../android/ddmuilib/ScreenShotDialog.java | 13 +- .../ddmuilib/explorer/DeviceExplorer.java | 13 +- .../ddmuilib/heap/NativeHeapPanel.java | 18 +- .../log/event/EventDisplayOptions.java | 19 +- .../ddmuilib/log/event/EventLogPanel.java | 8 +- .../ddmuilib/logcat/EditFilterDialog.java | 46 +- .../android/ddmuilib/logcat/LogCatPanel.java | 24 +- .../com/android/ddmuilib/logcat/LogPanel.java | 10 +- .../META-INF/MANIFEST.MF | 42 +- .../plugin.xml | 5 + .../HierarchyViewerDirector.java | 14 + .../actions/CapturePSDAction.java | 7 +- .../actions/DisplayViewAction.java | 7 +- .../actions/DumpDisplayListAction.java | 7 +- .../actions/EvaluateContrastAction.java | 3 - .../actions/InspectScreenshotAction.java | 6 +- .../actions/InvalidateAction.java | 7 +- .../actions/LoadOverlayAction.java | 7 +- .../actions/LoadViewHierarchyAction.java | 6 +- .../PixelPerfectAutoRefreshAction.java | 7 +- .../actions/ProfileNodesAction.java | 7 +- .../actions/RefreshPixelPerfectAction.java | 7 +- .../RefreshPixelPerfectTreeAction.java | 7 +- .../actions/RefreshViewAction.java | 7 +- .../actions/RefreshWindowsAction.java | 7 +- .../actions/RequestLayoutAction.java | 7 +- .../actions/SavePixelPerfectAction.java | 7 +- .../actions/SaveTreeViewAction.java | 7 +- .../hierarchyviewerlib/ui/CaptureDisplay.java | 6 +- .../hierarchyviewerlib/ui/DeviceSelector.java | 12 +- .../ui/EvaluateContrastDisplay.java | 15 +- .../ui/PixelPerfectTree.java | 9 +- .../hierarchyviewerlib/ui/PropertyViewer.java | 6 +- .../hierarchyviewerlib/ui/TreeView.java | 18 +- .../ui/TreeViewOverview.java | 13 +- .../META-INF/MANIFEST.MF | 57 +- .../build.properties | 3 +- .../repository => }/icons/accept_icon16.png | Bin .../repository => }/icons/addon_pkg_16.png | Bin .../icons/android_icon_128.png | Bin .../repository => }/icons/android_icon_16.png | Bin .../repository => }/icons/archive_icon16.png | Bin .../repository => }/icons/broken_16.png | Bin .../repository => }/icons/broken_pkg_16.png | Bin .../icons/buildtool_pkg_16.png | Bin .../icons}/device.png | Bin .../repository => }/icons/doc_pkg_16.png | Bin .../icons}/emulator.png | Bin .../repository => }/icons/error_icon_16.png | Bin .../repository => }/icons/extra_pkg_16.png | Bin .../repository => }/icons/incompat_icon16.png | Bin .../repository => }/icons/log_off_16.png | Bin .../repository => }/icons/log_on_16.png | Bin .../repository => }/icons/nopkg_icon_16.png | Bin .../repository => }/icons/pkg_incompat_16.png | Bin .../icons/pkg_installed_16.png | Bin .../repository => }/icons/pkg_new_16.png | Bin .../repository => }/icons/pkg_update_16.png | Bin .../repository => }/icons/pkgcat_16.png | Bin .../repository => }/icons/pkgcat_other_16.png | Bin .../repository => }/icons/platform_pkg_16.png | Bin .../icons/platformtool_pkg_16.png | Bin .../repository => }/icons/reject_icon16.png | Bin .../repository => }/icons/sample_pkg_16.png | Bin .../repository => }/icons/sdkman_logo_128.png | Bin .../repository => }/icons/source_pkg_16.png | Bin .../repository => }/icons/status_ok_16.png | Bin .../icons/stop_disabled_16.png | Bin .../repository => }/icons/stop_enabled_16.png | Bin .../repository => }/icons/sysimg_pkg_16.png | Bin .../icons/tag_android-tv_16.png | Bin .../icons/tag_android-tv_32.png | Bin .../icons/tag_android-wear_16.png | Bin .../icons/tag_android-wear_32.png | Bin .../repository => }/icons/tag_default_16.png | Bin .../repository => }/icons/tag_default_32.png | Bin .../repository => }/icons/tool_pkg_16.png | Bin .../repository => }/icons/unknown_icon16.png | Bin .../repository => }/icons/warning_icon16.png | Bin .../libs/.gitignore | 1 + .../org.eclipse.andmore.sdkuilib/plugin.xml | 5 + .../org.eclipse.andmore.sdkuilib/pom.xml | 32 + .../com/android/sdklib/AndroidTargetHash.java | 182 - .../com/android/sdklib/AndroidVersion.java | 392 -- .../com/android/sdklib/BuildToolInfo.java | 391 -- .../com/android/sdklib/IAndroidTarget.java | 334 - .../java/com/android/sdklib/ISystemImage.java | 95 - .../android/sdklib/LegacyAndroidVersion.java | 46 - .../java/com/android/sdklib/SdkManager.java | 410 -- .../com/android/sdklib/SdkVersionInfo.java | 351 - .../java/com/android/sdklib/SystemImage.java | 325 - .../internal/androidTarget/AddOnTarget.java | 463 -- .../internal/androidTarget/MissingTarget.java | 244 - .../androidTarget/OptionalLibraryImpl.java | 70 - .../androidTarget/PlatformTarget.java | 456 -- .../android/sdklib/internal/avd/AvdInfo.java | 410 -- .../sdklib/internal/avd/AvdManager.java | 2196 ------ .../internal/avd/HardwareProperties.java | 327 - .../internal/build/BuildConfig.template | 6 - .../internal/build/BuildConfigGenerator.java | 157 - .../internal/build/DebugKeyProvider.java | 215 - .../sdklib/internal/build/KeystoreHelper.java | 173 - .../internal/build/SignedJarBuilder.java | 399 -- .../sdklib/internal/build/SymbolLoader.java | 102 - .../sdklib/internal/build/SymbolWriter.java | 138 - .../internal/project/ProjectCreator.java | 1571 ----- .../internal/project/ProjectProperties.java | 594 -- .../project/ProjectPropertiesWorkingCopy.java | 280 - .../internal/repository/AdbWrapper.java | 157 - .../repository/AddonsListFetcher.java | 614 -- .../repository/CanceledByUserException.java | 35 - .../internal/repository/DownloadCache.java | 848 --- .../internal/repository/LocalSdkParser.java | 804 --- .../internal/repository/NullTaskMonitor.java | 140 - .../sdklib/internal/repository/SdkStats.java | 627 -- .../sdklib/internal/repository/UrlOpener.java | 530 -- .../repository/archives/ArchFilter.java | 324 - .../internal/repository/archives/Archive.java | 375 -- .../repository/archives/ArchiveInstaller.java | 1220 ---- .../archives/ArchiveReplacement.java | 117 - .../internal/repository/archives/BitSize.java | 66 - .../repository/archives/ChecksumType.java | 54 - .../internal/repository/archives/HostOs.java | 83 - .../repository/archives/LegacyArch.java | 52 - .../repository/archives/LegacyOs.java | 52 - .../repository/packages/AddonPackage.java | 765 --- .../repository/packages/BrokenPackage.java | 241 - .../repository/packages/BuildToolPackage.java | 367 - .../repository/packages/DocPackage.java | 333 - .../repository/packages/ExtraPackage.java | 692 -- .../packages/FullRevisionPackage.java | 164 - .../packages/IAndroidVersionProvider.java | 43 - .../packages/IExactApiLevelDependency.java | 54 - .../packages/IFullRevisionProvider.java | 51 - .../packages/ILayoutlibVersion.java | 47 - .../packages/IMinApiLevelDependency.java | 46 - .../packages/IMinPlatformToolsDependency.java | 55 - .../packages/IMinToolsDependency.java | 48 - .../packages/IPlatformDependency.java | 42 - .../packages/LayoutlibVersionMixin.java | 136 - .../packages/MajorRevisionPackage.java | 164 - .../repository/packages/MinToolsMixin.java | 134 - .../repository/packages/MinToolsPackage.java | 117 - .../packages/NoPreviewRevisionPackage.java | 163 - .../internal/repository/packages/Package.java | 902 --- .../packages/PackageParserUtils.java | 427 -- .../repository/packages/PlatformPackage.java | 388 -- .../packages/PlatformToolPackage.java | 313 - .../repository/packages/SamplePackage.java | 571 -- .../repository/packages/SourcePackage.java | 377 -- .../packages/SystemImagePackage.java | 621 -- .../repository/packages/ToolPackage.java | 384 -- .../repository/sources/SdkAddonSource.java | 113 - .../repository/sources/SdkRepoSource.java | 528 -- .../repository/sources/SdkSource.java | 999 --- .../repository/sources/SdkSourceCategory.java | 94 - .../sources/SdkSourceProperties.java | 254 - .../repository/sources/SdkSources.java | 444 -- .../repository/sources/SdkSysImgSource.java | 114 - .../repository/updater/ArchiveInfo.java | 173 - .../repository/updater/ISettingsPage.java | 113 - .../repository/updater/IUpdaterData.java | 49 - .../repository/updater/PackageLoader.java | 502 -- .../internal/repository/updater/PkgItem.java | 290 - .../repository/updater/SdkUpdaterLogic.java | 1568 ----- .../updater/SdkUpdaterNoWindow.java | 715 -- .../updater/SettingsController.java | 392 -- .../repository/updater/UpdaterData.java | 1336 ---- .../java/com/android/sdklib/io/IFileOp.java | 161 - .../com/android/sdklib/io/LegacyFileOp.java | 488 -- .../repository/AddonManifestIniProps.java | 113 - .../sdklib/repository/FullRevision.java | 426 -- .../sdklib/repository/IDescription.java | 40 - .../sdklib/repository/IListDescription.java | 34 - .../android/sdklib/repository/License.java | 176 - .../sdklib/repository/MajorRevision.java | 59 - .../sdklib/repository/NoPreviewRevision.java | 61 - .../android/sdklib/repository/PkgProps.java | 104 - .../sdklib/repository/PreciseRevision.java | 153 - .../com/android/sdklib/repository/README.txt | 25 - .../sdklib/repository/RepoConstants.java | 213 - .../sdklib/repository/RepoXsdUtil.java | 92 - .../sdklib/repository/SdkAddonConstants.java | 89 - .../repository/SdkAddonsListConstants.java | 109 - .../sdklib/repository/SdkRepoConstants.java | 171 - .../sdklib/repository/SdkStatsConstants.java | 92 - .../sdklib/repository/SdkSysImgConstants.java | 90 - .../descriptors/IPkgCapabilities.java | 82 - .../repository/descriptors/IPkgDesc.java | 189 - .../repository/descriptors/IPkgDescAddon.java | 40 - .../repository/descriptors/IPkgDescExtra.java | 51 - .../repository/descriptors/IdDisplay.java | 84 - .../repository/descriptors/PkgDesc.java | 1206 ---- .../repository/descriptors/PkgDescAddon.java | 76 - .../repository/descriptors/PkgDescExtra.java | 176 - .../repository/descriptors/PkgType.java | 225 - .../repository/local/LocalAddonPkgInfo.java | 503 -- .../local/LocalAddonSysImgPkgInfo.java | 65 - .../local/LocalBuildToolPkgInfo.java | 57 - .../sdklib/repository/local/LocalDirInfo.java | 275 - .../repository/local/LocalDocPkgInfo.java | 47 - .../repository/local/LocalExtraPkgInfo.java | 121 - .../repository/local/LocalNdkPkgInfo.java | 47 - .../sdklib/repository/local/LocalPkgInfo.java | 240 - .../local/LocalPlatformPkgInfo.java | 453 -- .../local/LocalPlatformToolPkgInfo.java | 45 - .../repository/local/LocalSamplePkgInfo.java | 54 - .../sdklib/repository/local/LocalSdk.java | 1239 ---- .../repository/local/LocalSourcePkgInfo.java | 52 - .../repository/local/LocalSysImgPkgInfo.java | 135 - .../repository/local/LocalToolPkgInfo.java | 46 - .../repository/local/PackageParserUtils.java | 166 - .../sdklib/repository/sdk-addon-01.xsd | 295 - .../sdklib/repository/sdk-addon-02.xsd | 361 - .../sdklib/repository/sdk-addon-03.xsd | 381 -- .../sdklib/repository/sdk-addon-04.xsd | 417 -- .../sdklib/repository/sdk-addon-05.xsd | 442 -- .../sdklib/repository/sdk-addon-06.xsd | 472 -- .../sdklib/repository/sdk-addon-07.xsd | 499 -- .../sdklib/repository/sdk-addons-list-1.xsd | 71 - .../sdklib/repository/sdk-addons-list-2.xsd | 106 - .../sdklib/repository/sdk-repository-01.xsd | 381 -- .../sdklib/repository/sdk-repository-02.xsd | 438 -- .../sdklib/repository/sdk-repository-03.xsd | 436 -- .../sdklib/repository/sdk-repository-04.xsd | 500 -- .../sdklib/repository/sdk-repository-05.xsd | 624 -- .../sdklib/repository/sdk-repository-06.xsd | 608 -- .../sdklib/repository/sdk-repository-07.xsd | 612 -- .../sdklib/repository/sdk-repository-08.xsd | 652 -- .../sdklib/repository/sdk-repository-09.xsd | 677 -- .../sdklib/repository/sdk-repository-10.xsd | 653 -- .../sdklib/repository/sdk-repository-11.xsd | 680 -- .../android/sdklib/repository/sdk-stats-1.xsd | 96 - .../sdklib/repository/sdk-sys-img-01.xsd | 229 - .../sdklib/repository/sdk-sys-img-02.xsd | 249 - .../sdklib/repository/sdk-sys-img-03.xsd | 309 - .../sdklib/util/CommandLineParser.java | 976 --- .../com/android/sdklib/util/FormatUtils.java | 53 - .../com/android/sdklib/util/LineUtil.java | 118 - .../internal/repository/AboutDialog.java | 98 +- .../repository/ISdkUpdaterWindow.java | 2 +- .../internal/repository/ISwtUpdaterData.java | 36 - .../internal/repository/ITask.java | 56 +- .../internal/repository/ITaskFactory.java | 123 +- .../internal/repository/ITaskMonitor.java | 310 +- .../repository/LoadPackagesRequest.java | 85 + .../internal/repository/MenuBarWrapper.java | 1 - .../internal/repository/PackageInfo.java | 124 + .../internal/repository/PackageManager.java | 282 + .../internal/repository/Settings.java | 88 + .../internal/repository/SettingsDialog.java | 113 +- .../internal/repository/SwtUpdaterData.java | 240 - .../repository/UpdaterBaseDialog.java | 26 +- .../internal/repository/UserCredentials.java | 103 +- .../internal/repository/avd/AvdAgent.java | 192 + .../internal/repository/avd/SdkTargets.java | 175 + .../repository/content/CategoryKeyType.java | 10 + .../internal/repository/content/INode.java | 126 + .../repository/content/MetaPackage.java | 28 + .../repository/content/PackageAnalyser.java | 304 + .../content/PackageContentProvider.java | 130 + .../repository/content/PackageType.java | 19 + .../repository/content/PkgCategory.java | 136 + .../{core => content}/PkgCategoryApi.java | 43 +- .../repository/content/PkgCellAgent.java | 57 + .../content/PkgCellLabelProvider.java | 79 + .../internal/repository/content/PkgItem.java | 342 + .../PkgTreeColumnViewerLabelProvider.java | 6 +- .../internal/repository/content/UpdateOp.java | 84 + .../repository/content/UpdateOpApi.java | 110 + .../repository/core/PackagesDiffLogic.java | 859 --- .../internal/repository/core/PkgCategory.java | 87 - .../repository/core/PkgCategorySource.java | 70 - .../repository/core/PkgContentProvider.java | 237 - .../repository/core/SdkLogAdapter.java | 112 - .../repository/core/SwtPackageLoader.java | 69 - .../repository/icons/ImageFactory.java | 205 - .../repository/ui/AddonSitesDialog.java | 574 -- .../repository/ui/AvdManagerPage.java | 45 +- .../repository/ui/AvdManagerWindowImpl1.java | 100 +- .../repository/ui/DeviceManagerPage.java | 56 +- .../internal/repository/ui/LogWindow.java | 2 +- .../internal/repository/ui/PackagesPage.java | 764 +-- .../repository/ui/PackagesPageImpl.java | 564 -- .../repository/ui/SdkProgressFactory.java | 288 + .../{ => ui}/SdkUpdaterChooserDialog.java | 660 +- .../repository/ui/SdkUpdaterWindowImpl2.java | 196 +- .../internal/tasks/IProgressUiProvider.java | 15 +- .../sdkuilib/internal/tasks/ProgressTask.java | 4 +- .../internal/tasks/ProgressTaskDialog.java | 26 +- .../internal/tasks/ProgressTaskFactory.java | 6 +- .../sdkuilib/internal/tasks/ProgressView.java | 93 +- .../internal/tasks/ProgressViewFactory.java | 48 - .../internal/tasks/SdkProgressIndicator.java | 181 + .../internal/tasks/TaskMonitorImpl.java | 25 +- .../internal/widgets/AvdCreationDialog.java | 15 +- .../widgets/AvdCreationPresenter.java | 470 +- .../internal/widgets/AvdCreationSwtView.java | 3 +- .../internal/widgets/AvdDetailsDialog.java | 166 +- .../internal/widgets/AvdSelector.java | 393 +- .../internal/widgets/AvdStartDialog.java | 20 +- .../widgets/DeviceCreationDialog.java | 3 +- .../widgets/HardwarePropertyChooser.java | 150 - .../internal/widgets/LegacyAvdEditDialog.java | 1439 ---- .../sdkuilib/repository/AvdManagerWindow.java | 30 +- .../repository/ISdkChangeListener.java | 108 +- .../sdkuilib/repository/SdkUpdaterWindow.java | 31 +- .../repository => }/ui/AdtUpdateDialog.java | 370 +- .../android/sdkuilib/ui/AvdDisplayMode.java | 48 + .../sdkuilib/ui/AvdSelectorWindow.java | 155 + .../sdkuilib/ui}/DeviceChooserDialog.java | 1647 +++-- .../ResolutionChooserDialog.java | 4 +- .../{internal => }/widgets/MessageBoxLog.java | 3 +- .../widgets/SdkTargetSelector.java | 2 +- .../eclipse/andmore/sdktool/SdkCallAgent.java | 96 + .../eclipse/andmore/sdktool/SdkContext.java | 208 + .../eclipse/andmore/sdktool/SdkHelper.java | 108 + .../andmore/sdktool/SdkResourceProvider.java | 28 + .../sdktool/SdkUserInterfacePlugin.java | 137 + .../eclipse/andmore/sdktool/Utilities.java | 114 + .../org.eclipse.andmore.swt/.classpath | 2 +- .../META-INF/MANIFEST.MF | 15 +- andmore-swt/org.eclipse.andmore.swt/pom.xml | 5 - .../base/resources/IEditorIconFactory.java | 38 + .../andmore/base/resources/ImageFactory.java | 120 + .../base/resources/JFaceImageLoader.java | 229 + .../resources/PluginResourceProvider.java} | 48 +- .../org.eclipse.andmore.package/feature.xml | 100 +- .../features/org.eclipse.andmore/feature.xml | 297 +- .../org.eclipse.andmore.base/.classpath | 24 +- .../.settings/org.eclipse.jdt.core.prefs | 196 +- .../META-INF/MANIFEST.MF | 106 +- .../libs/annotations.jar | Bin 4229 -> 0 bytes .../org.eclipse.andmore.base/libs/common.jar | Bin 87588 -> 0 bytes .../org.eclipse.andmore.base/libs/dvlib.jar | Bin 19925 -> 0 bytes .../libs/guava-17.0.jar | Bin 2243036 -> 0 bytes .../libs/httpmime-4.1.jar | Bin 26813 -> 0 bytes .../libs/kxml2-2.3.0.jar | Bin 43858 -> 0 bytes .../libs/layoutlib-api.jar | Bin 84029 -> 0 bytes .../libs/sdk-common.jar | Bin 495908 -> 0 bytes .../org.eclipse.andmore.base/libs/sdklib.jar | Bin 725877 -> 0 bytes .../libs/sdkstats.jar | Bin 16832 -> 0 bytes .../org.eclipse.andmore.ddms/.classpath | 17 +- .../.settings/org.eclipse.jdt.core.prefs | 196 +- .../META-INF/MANIFEST.MF | 64 +- .../org.eclipse.andmore.ddms/build.properties | 28 +- .../ddm/icons}/add.png | Bin .../ddm/icons}/android.png | Bin .../ddm/icons}/backward.png | Bin .../ddm/icons}/capture.png | Bin .../ddm/icons}/clear.png | Bin .../org.eclipse.andmore.ddms/ddm/icons}/d.png | Bin .../ddm/icons}/debug-attach.png | Bin .../ddm/icons}/debug-error.png | Bin .../ddm/icons}/debug-wait.png | Bin .../ddm/icons}/delete.png | Bin .../ddm/icons/device.png | Bin 0 -> 135 bytes .../ddm/icons}/diff.png | Bin .../ddm/icons}/displayfilters.png | Bin .../ddm/icons}/down.png | Bin .../org.eclipse.andmore.ddms/ddm/icons}/e.png | Bin .../ddm/icons}/edit.png | Bin .../ddm/icons}/empty.png | Bin .../ddm/icons/emulator.png | Bin 0 -> 287 bytes .../ddm/icons}/file.png | Bin .../ddm/icons}/folder.png | Bin .../ddm/icons}/forward.png | Bin .../ddm/icons}/gc.png | Bin .../ddm/icons}/groupby.png | Bin .../ddm/icons}/halt.png | Bin .../ddm/icons}/heap.png | Bin .../ddm/icons}/hprof.png | Bin .../org.eclipse.andmore.ddms/ddm/icons}/i.png | Bin .../ddm/icons}/importBug.png | Bin .../ddm/icons}/load.png | Bin .../ddm/icons}/pause.png | Bin .../ddm/icons}/play.png | Bin .../ddm/icons}/pull.png | Bin .../ddm/icons}/push.png | Bin .../ddm/icons}/save.png | Bin .../ddm/icons}/scroll_lock.png | Bin .../ddm/icons}/sort_down.png | Bin .../ddm/icons}/sort_up.png | Bin .../ddm/icons}/thread.png | Bin .../ddm/icons}/tracing_start.png | Bin .../ddm/icons}/tracing_stop.png | Bin .../ddm/icons}/up.png | Bin .../org.eclipse.andmore.ddms/ddm/icons}/v.png | Bin .../org.eclipse.andmore.ddms/ddm/icons}/w.png | Bin .../ddm/icons}/warning.png | Bin .../ddm/icons}/zygote.png | Bin .../org.eclipse.andmore.ddms/libs/ddmlib.jar | Bin 295950 -> 0 bytes .../libs/ddmuilib.jar | Bin 598267 -> 0 bytes .../libs/uiautomatorviewer.jar | Bin 67649 -> 0 bytes .../sdk/icons/accept_icon16.png | Bin 0 -> 3277 bytes .../sdk/icons/addon_pkg_16.png | Bin 0 -> 529 bytes .../sdk/icons/android_icon_128.png | Bin 0 -> 17715 bytes .../sdk/icons/android_icon_16.png | Bin 0 -> 219 bytes .../sdk/icons/archive_icon16.png | Bin 0 -> 493 bytes .../sdk/icons/broken_16.png | Bin 0 -> 257 bytes .../sdk/icons/broken_pkg_16.png | Bin 0 -> 281 bytes .../sdk/icons/buildtool_pkg_16.png | Bin 0 -> 425 bytes .../sdk/icons/device.png | Bin 0 -> 135 bytes .../sdk/icons/doc_pkg_16.png | Bin 0 -> 290 bytes .../sdk/icons/emulator.png | Bin 0 -> 287 bytes .../sdk/icons/error_icon_16.png | Bin 0 -> 626 bytes .../sdk/icons/extra_pkg_16.png | Bin 0 -> 540 bytes .../sdk/icons/incompat_icon16.png | Bin 0 -> 735 bytes .../sdk/icons/log_off_16.png | Bin 0 -> 1218 bytes .../sdk/icons/log_on_16.png | Bin 0 -> 468 bytes .../sdk/icons/nopkg_icon_16.png | Bin 0 -> 397 bytes .../sdk/icons/pkg_incompat_16.png | Bin 0 -> 409 bytes .../sdk/icons/pkg_installed_16.png | Bin 0 -> 563 bytes .../sdk/icons/pkg_new_16.png | Bin 0 -> 262 bytes .../sdk/icons/pkg_update_16.png | Bin 0 -> 553 bytes .../sdk/icons/pkgcat_16.png | Bin 0 -> 1352 bytes .../sdk/icons/pkgcat_other_16.png | Bin 0 -> 335 bytes .../sdk/icons/platform_pkg_16.png | Bin 0 -> 2981 bytes .../sdk/icons/platformtool_pkg_16.png | Bin 0 -> 393 bytes .../sdk/icons/reject_icon16.png | Bin 0 -> 3367 bytes .../sdk/icons/sample_pkg_16.png | Bin 0 -> 3188 bytes .../sdk/icons/sdkman_logo_128.png | Bin 0 -> 2381 bytes .../sdk/icons/source_pkg_16.png | Bin 0 -> 466 bytes .../sdk/icons/status_ok_16.png | Bin 0 -> 3277 bytes .../sdk/icons/stop_disabled_16.png | Bin 0 -> 809 bytes .../sdk/icons/stop_enabled_16.png | Bin 0 -> 642 bytes .../sdk/icons/sysimg_pkg_16.png | Bin 0 -> 1145 bytes .../sdk/icons/tag_android-tv_16.png | Bin 0 -> 2948 bytes .../sdk/icons/tag_android-tv_32.png | Bin 0 -> 2978 bytes .../sdk/icons/tag_android-wear_16.png | Bin 0 -> 3115 bytes .../sdk/icons/tag_android-wear_32.png | Bin 0 -> 3143 bytes .../sdk/icons/tag_default_16.png | Bin 0 -> 3097 bytes .../sdk/icons/tag_default_32.png | Bin 0 -> 3064 bytes .../sdk/icons/tool_pkg_16.png | Bin 0 -> 393 bytes .../sdk/icons/unknown_icon16.png | Bin 0 -> 453 bytes .../sdk/icons/warning_icon16.png | Bin 0 -> 259 bytes .../andmore/ddms/DdmResourceProvider.java | 13 + .../org/eclipse/andmore/ddms/DdmsPlugin.java | 1859 +++--- .../andmore/ddms/views/AllocTrackerView.java | 98 +- .../andmore/ddms/views/DeviceView.java | 1678 ++--- .../ddms/views/EmulatorControlView.java | 83 +- .../andmore/ddms/views/EventLogView.java | 221 +- .../andmore/ddms/views/FileExplorerView.java | 354 +- .../andmore/ddms/views/LogCatView.java | 230 +- .../andmore/ddms/views/OldLogCatView.java | 742 +-- .../.classpath | 22 +- .../org.eclipse.andmore.gldebugger/.classpath | 18 +- .../.settings/org.eclipse.jdt.core.prefs | 196 +- .../META-INF/MANIFEST.MF | 47 +- .../.classpath | 15 +- .../.settings/org.eclipse.jdt.core.prefs | 196 +- .../META-INF/MANIFEST.MF | 37 +- .../build.properties | 23 +- .../hiarch/icons}/auto-refresh.png | Bin .../hiarch/icons}/capture-psd.png | Bin .../hiarch/icons}/device-view-selected.png | Bin .../hiarch/icons}/device-view.png | Bin .../hiarch/icons/device.png | Bin 0 -> 135 bytes .../hiarch/icons}/display.png | Bin .../hiarch/icons/emulator.png | Bin 0 -> 287 bytes .../hiarch/icons/file.png | Bin 0 -> 157 bytes .../hiarch/icons}/filtered.png | Bin .../hiarch/icons/folder.png | Bin 0 -> 123 bytes .../hiarch/icons}/green.png | Bin .../hiarch/icons}/inspect-screenshot.png | Bin .../hiarch/icons}/invalidate.png | Bin .../hiarch/icons}/load-all-views.png | Bin .../hiarch/icons}/load-overlay.png | Bin .../hiarch/icons}/load-view-hierarchy.png | Bin .../hiarch/icons}/not-selected.png | Bin .../hiarch/icons}/on-black.png | Bin .../hiarch/icons}/on-white.png | Bin .../hiarch/icons}/picker.png | Bin .../icons}/pixel-perfect-view-selected.png | Bin .../hiarch/icons}/pixel-perfect-view.png | Bin .../hiarch/icons}/profile.png | Bin .../hiarch/icons}/red.png | Bin .../hiarch/icons}/refresh-windows.png | Bin .../hiarch/icons}/request-layout.png | Bin .../hiarch/icons}/save.png | Bin .../hiarch/icons}/sdk-hierarchyviewer-128.png | Bin .../hiarch/icons}/sdk-hierarchyviewer-16.png | Bin .../hiarch/icons}/selected-filtered-small.png | Bin .../hiarch/icons}/selected-filtered.png | Bin .../hiarch/icons}/selected-small.png | Bin .../hiarch/icons}/selected.png | Bin .../hiarch/icons}/show-extras.png | Bin .../hiarch/icons}/show-overlay.png | Bin .../hiarch/icons}/tree-view-selected.png | Bin .../hiarch/icons}/tree-view.png | Bin .../hiarch/icons}/yellow.png | Bin .../libs/hierarchyviewer2lib.jar | Bin 406917 -> 0 bytes .../HiarchResourceProvider.java | 28 + .../HierarchyViewerPlugin.java | 10 +- .../HierarchyViewerPluginDirector.java | 245 +- .../hierarchyviewer/views/LayoutView.java | 320 +- .../views/PixelPerfectLoupeView.java | 328 +- .../.classpath | 16 +- .../.settings/org.eclipse.jdt.core.prefs | 196 +- .../META-INF/MANIFEST.MF | 34 +- .../tests/IntegrationTestSuite.java | 127 +- .../integration/tests/SdkLoadingTestCase.java | 276 +- .../integration/tests/SdkTestCase.java | 845 +-- .../ApiDemosRenderingTest.java | 665 +- .../sampleProjects/SampleProjectTest.java | 533 +- .../internal/build/AaptParserTest.java | 424 +- .../internal/build/AaptQuickFixTest.java | 599 +- .../internal/build/DexWrapperTest.java | 270 +- .../editors/AndroidContentAssistTest.java | 2248 ++++--- .../AndroidXmlAutoEditStrategyTest.java | 699 +- .../layout/refactoring/AdtProjectTest.java | 1012 +-- .../completion1-expected-completion11.txt | 135 +- .../completion1-expected-completion12.txt | 135 +- .../completion1-expected-completion39.txt | 314 +- .../completion1-expected-completion6.txt | 46 +- .../completion12-expected-completion75.txt | 200 +- .../completion5-expected-completion40.txt | 314 +- .../completion8-expected-completion44.txt | 26 +- .../completion9-expected-completion64.txt | 137 +- ...ompletionvalues1-expected-completion29.txt | 6 +- ...ompletionvalues1-expected-completion32.txt | 645 +- .../drawable1-expected-completion48.txt | 7 +- .../manifest-expected-completion16.txt | 32 +- .../manifest-expected-completion18.txt | 61 +- .../navigation1-expected-complation76.txt | 158 +- .../editors/manifest/ManifestInfoTest.java | 751 +-- .../templates/TemplateHandlerTest.java | 1841 +++--- .../common/layout/BaseLayoutRuleTest.java | 534 +- .../configuration/ConfigurationTest.java | 298 +- .../.settings/org.eclipse.jdt.core.prefs | 196 +- .../META-INF/MANIFEST.MF | 34 +- .../META-INF/MANIFEST.MF | 59 +- .../internal/launch/NdkGdbLaunchDelegate.java | 983 +-- .../org.eclipse.andmore.overlay/.classpath | 12 +- .../.settings/org.eclipse.jdt.core.prefs | 196 +- .../META-INF/MANIFEST.MF | 38 +- .../org.eclipse.andmore.package/.classpath | 14 +- .../.settings/org.eclipse.jdt.core.prefs | 14 +- .../META-INF/MANIFEST.MF | 20 +- .../.settings/org.eclipse.jdt.core.prefs | 196 +- .../META-INF/MANIFEST.MF | 42 +- .../org.eclipse.andmore.traceview/.classpath | 15 +- .../.settings/org.eclipse.jdt.core.prefs | 196 +- .../META-INF/MANIFEST.MF | 35 +- .../libs/traceview.jar | Bin 137241 -> 0 bytes .../plugins/org.eclipse.andmore/.classpath | 40 +- .../.settings/org.eclipse.jdt.core.prefs | 196 +- .../org.eclipse.andmore/META-INF/MANIFEST.MF | 281 +- .../org.eclipse.andmore/libs/.gitignore | 9 + .../libs/freemarker-2.3.20.jar | Bin 1018549 -> 0 bytes .../org.eclipse.andmore/libs/lint-api.jar | Bin 190126 -> 0 bytes .../org.eclipse.andmore/libs/lint-checks.jar | Bin 675633 -> 0 bytes .../libs/manifest-merger.jar | Bin 167459 -> 0 bytes .../org.eclipse.andmore/libs/sdkuilib.jar | Bin 481453 -> 0 bytes .../org.eclipse.andmore/libs/swtmenubar.jar | Bin 18580 -> 0 bytes .../plugins/org.eclipse.andmore/pom.xml | 275 +- .../sdklib/repository/FullRevision.java | 51 + .../sdklib/repository/PreciseRevision.java | 51 + .../src/org/eclipse/andmore/AdtUtils.java | 3122 ++++----- .../andmore/common/layout/BaseLayoutRule.java | 1757 ++--- .../andmore/internal/VersionCheck.java | 371 +- .../internal/actions/AddSupportJarAction.java | 1287 ++-- .../internal/actions/AvdManagerAction.java | 159 +- .../internal/actions/FixProjectAction.java | 374 +- .../internal/actions/SdkManagerAction.java | 719 +- .../andmore/internal/build/AaptQuickFix.java | 862 +-- .../andmore/internal/build/AidlProcessor.java | 970 +-- .../andmore/internal/build/BuildHelper.java | 2849 ++++---- .../internal/build/builders/BaseBuilder.java | 971 ++- .../build/builders/PreCompilerBuilder.java | 2824 ++++---- .../internal/editors/AndroidXmlEditor.java | 3431 +++++----- .../andmore/internal/editors/IconFactory.java | 938 +-- .../editors/layout/BasePullParser.java | 488 +- .../editors/layout/ProjectCallback.java | 1461 ++-- .../editors/layout/RenderSecurityManager.java | 626 ++ .../editors/layout/UiElementPullParser.java | 1314 ++-- .../editors/layout/WidgetPullParser.java | 339 +- .../layout/configuration/Configuration.java | 2161 +++--- .../configuration/ConfigurationChooser.java | 4184 ++++++------ .../configuration/ConfigurationClient.java | 261 +- .../configuration/ConfigurationMatcher.java | 1685 +++-- .../configuration/DeviceMenuListener.java | 401 +- .../layout/gle2/GraphicalEditorPart.java | 5877 +++++++++-------- .../editors/layout/gle2/LayoutActionBar.java | 1464 ++-- .../editors/layout/gle2/RenderLogger.java | 654 +- .../editors/layout/gle2/RenderPreview.java | 2667 ++++---- .../editors/layout/gle2/RenderService.java | 1328 ++-- .../manifest/ManifestContentAssist.java | 187 +- .../editors/manifest/ManifestInfo.java | 1912 +++--- .../launch/AndroidLaunchController.java | 3762 +++++------ .../internal/launch/AvdCompatibility.java | 119 +- .../internal/launch/EmulatorConfigTab.java | 1183 ++-- .../internal/lint/EclipseLintClient.java | 2704 ++++---- .../lint/GlobalLintConfiguration.java | 369 +- .../andmore/internal/lint/TypoFix.java | 244 +- .../preferences/AndroidPreferencePage.java | 518 +- .../internal/preferences/EditorsPage.java | 364 +- .../properties/AndroidPropertyPage.java | 399 +- .../resources/manager/ProjectResources.java | 543 +- .../sdk/AdtManifestMergeCallback.java | 46 - .../andmore/internal/sdk/ProjectState.java | 1511 +++-- .../org/eclipse/andmore/internal/sdk/Sdk.java | 3257 ++++----- .../internal/welcome/WelcomeWizard.java | 381 +- .../exportgradle/BuildFileCreator.java | 1280 ++-- .../newproject/ApplicationInfoPage.java | 1619 ++--- .../wizards/newproject/ImportedProject.java | 519 +- .../newproject/NewProjectWizardState.java | 819 ++- .../wizards/newproject/ProjectNamePage.java | 1219 ++-- .../wizards/newproject/SdkSelectionPage.java | 967 ++- .../wizards/templates/CreateFileChange.java | 214 +- .../wizards/templates/NewProjectPage.java | 1863 +++--- .../wizards/templates/TemplateHandler.java | 2507 +++---- .../org/eclipse/andmore/io/IFileWrapper.java | 323 +- .../eclipse/andmore/io/IFolderWrapper.java | 405 +- android-core/pom.xml | 87 +- pom.xml | 5 + 689 files changed, 60165 insertions(+), 110868 deletions(-) create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/plugin.xml delete mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ImageLoader.java create mode 100644 andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ReplacementImageFactory.java create mode 100644 andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/plugin.xml rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/accept_icon16.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/addon_pkg_16.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/android_icon_128.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/android_icon_16.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/archive_icon16.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/broken_16.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/broken_pkg_16.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/buildtool_pkg_16.png (100%) rename andmore-swt/{org.eclipse.andmore.ddmuilib/src/main/java/images => org.eclipse.andmore.sdkuilib/icons}/device.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/doc_pkg_16.png (100%) rename andmore-swt/{org.eclipse.andmore.ddmuilib/src/main/java/images => org.eclipse.andmore.sdkuilib/icons}/emulator.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/error_icon_16.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/extra_pkg_16.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/incompat_icon16.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/log_off_16.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/log_on_16.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/nopkg_icon_16.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/pkg_incompat_16.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/pkg_installed_16.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/pkg_new_16.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/pkg_update_16.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/pkgcat_16.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/pkgcat_other_16.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/platform_pkg_16.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/platformtool_pkg_16.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/reject_icon16.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/sample_pkg_16.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/sdkman_logo_128.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/source_pkg_16.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/status_ok_16.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/stop_disabled_16.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/stop_enabled_16.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/sysimg_pkg_16.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/tag_android-tv_16.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/tag_android-tv_32.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/tag_android-wear_16.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/tag_android-wear_32.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/tag_default_16.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/tag_default_32.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/tool_pkg_16.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/unknown_icon16.png (100%) rename andmore-swt/org.eclipse.andmore.sdkuilib/{src/main/java/com/android/sdkuilib/internal/repository => }/icons/warning_icon16.png (100%) create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/libs/.gitignore create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/plugin.xml delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/AndroidTargetHash.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/AndroidVersion.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/BuildToolInfo.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/IAndroidTarget.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/ISystemImage.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/LegacyAndroidVersion.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/SdkManager.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/SdkVersionInfo.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/SystemImage.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/androidTarget/AddOnTarget.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/androidTarget/MissingTarget.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/androidTarget/OptionalLibraryImpl.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/androidTarget/PlatformTarget.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/avd/AvdInfo.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/avd/AvdManager.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/avd/HardwareProperties.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/BuildConfig.template delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/BuildConfigGenerator.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/DebugKeyProvider.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/KeystoreHelper.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/SignedJarBuilder.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/SymbolLoader.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/SymbolWriter.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/project/ProjectCreator.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/project/ProjectProperties.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/project/ProjectPropertiesWorkingCopy.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/AdbWrapper.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/AddonsListFetcher.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/CanceledByUserException.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/DownloadCache.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/LocalSdkParser.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/NullTaskMonitor.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/SdkStats.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/UrlOpener.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/ArchFilter.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/Archive.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/ArchiveInstaller.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/ArchiveReplacement.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/BitSize.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/ChecksumType.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/HostOs.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/LegacyArch.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/LegacyOs.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/AddonPackage.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/BrokenPackage.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/BuildToolPackage.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/DocPackage.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/ExtraPackage.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/FullRevisionPackage.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IAndroidVersionProvider.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IExactApiLevelDependency.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IFullRevisionProvider.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/ILayoutlibVersion.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IMinApiLevelDependency.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IMinPlatformToolsDependency.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IMinToolsDependency.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IPlatformDependency.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/LayoutlibVersionMixin.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/MajorRevisionPackage.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/MinToolsMixin.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/MinToolsPackage.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/NoPreviewRevisionPackage.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/Package.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/PackageParserUtils.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/PlatformPackage.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/PlatformToolPackage.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/SamplePackage.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/SourcePackage.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/SystemImagePackage.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/ToolPackage.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkAddonSource.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkRepoSource.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSource.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSourceCategory.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSourceProperties.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSources.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSysImgSource.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/ArchiveInfo.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/ISettingsPage.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/IUpdaterData.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/PackageLoader.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/PkgItem.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/SdkUpdaterLogic.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/SdkUpdaterNoWindow.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/SettingsController.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/UpdaterData.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/io/IFileOp.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/io/LegacyFileOp.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/AddonManifestIniProps.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/FullRevision.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/IDescription.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/IListDescription.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/License.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/MajorRevision.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/NoPreviewRevision.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/PkgProps.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/PreciseRevision.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/README.txt delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/RepoConstants.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/RepoXsdUtil.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkAddonConstants.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkAddonsListConstants.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkRepoConstants.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkStatsConstants.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkSysImgConstants.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IPkgCapabilities.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IPkgDesc.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IPkgDescAddon.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IPkgDescExtra.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IdDisplay.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/PkgDesc.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/PkgDescAddon.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/PkgDescExtra.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/PkgType.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalAddonPkgInfo.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalAddonSysImgPkgInfo.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalBuildToolPkgInfo.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalDirInfo.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalDocPkgInfo.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalExtraPkgInfo.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalNdkPkgInfo.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalPkgInfo.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalPlatformPkgInfo.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalPlatformToolPkgInfo.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalSamplePkgInfo.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalSdk.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalSourcePkgInfo.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalSysImgPkgInfo.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalToolPkgInfo.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/PackageParserUtils.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-01.xsd delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-02.xsd delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-03.xsd delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-04.xsd delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-05.xsd delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-06.xsd delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-07.xsd delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addons-list-1.xsd delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addons-list-2.xsd delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-01.xsd delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-02.xsd delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-03.xsd delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-04.xsd delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-05.xsd delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-06.xsd delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-07.xsd delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-08.xsd delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-09.xsd delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-10.xsd delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-11.xsd delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-stats-1.xsd delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-sys-img-01.xsd delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-sys-img-02.xsd delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-sys-img-03.xsd delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/util/CommandLineParser.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/util/FormatUtils.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/util/LineUtil.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ISwtUpdaterData.java rename andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/{sdklib => sdkuilib}/internal/repository/ITask.java (80%) rename andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/{sdklib => sdkuilib}/internal/repository/ITaskFactory.java (92%) rename andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/{sdklib => sdkuilib}/internal/repository/ITaskMonitor.java (90%) create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/LoadPackagesRequest.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/PackageInfo.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/PackageManager.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/Settings.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SwtUpdaterData.java rename andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/{sdklib => sdkuilib}/internal/repository/UserCredentials.java (87%) create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/avd/AvdAgent.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/avd/SdkTargets.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/CategoryKeyType.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/INode.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/MetaPackage.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PackageAnalyser.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PackageContentProvider.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PackageType.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgCategory.java rename andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/{core => content}/PkgCategoryApi.java (65%) create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgCellAgent.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgCellLabelProvider.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgItem.java rename andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/{ui => content}/PkgTreeColumnViewerLabelProvider.java (94%) create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/UpdateOp.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/UpdateOpApi.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PackagesDiffLogic.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgCategory.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgCategorySource.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgContentProvider.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/SdkLogAdapter.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/SwtPackageLoader.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/ImageFactory.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AddonSitesDialog.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPageImpl.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/SdkProgressFactory.java rename andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/{ => ui}/SdkUpdaterChooserDialog.java (67%) delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressViewFactory.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/SdkProgressIndicator.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/HardwarePropertyChooser.java delete mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/LegacyAvdEditDialog.java rename andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/{sdklib => sdkuilib}/repository/ISdkChangeListener.java (95%) rename andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/{internal/repository => }/ui/AdtUpdateDialog.java (52%) create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/AvdDisplayMode.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/AvdSelectorWindow.java rename {android-core/plugins/org.eclipse.andmore/src/org/eclipse/andmore/internal/launch => andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui}/DeviceChooserDialog.java (87%) rename andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/{internal/widgets => ui}/ResolutionChooserDialog.java (95%) rename andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/{internal => }/widgets/MessageBoxLog.java (95%) rename andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/{internal => }/widgets/SdkTargetSelector.java (96%) create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkCallAgent.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkContext.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkHelper.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkResourceProvider.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkUserInterfacePlugin.java create mode 100644 andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/Utilities.java create mode 100644 andmore-swt/org.eclipse.andmore.swt/src/org/eclipse/andmore/base/resources/IEditorIconFactory.java create mode 100644 andmore-swt/org.eclipse.andmore.swt/src/org/eclipse/andmore/base/resources/ImageFactory.java create mode 100644 andmore-swt/org.eclipse.andmore.swt/src/org/eclipse/andmore/base/resources/JFaceImageLoader.java rename andmore-swt/{org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/project/IPropertySource.java => org.eclipse.andmore.swt/src/org/eclipse/andmore/base/resources/PluginResourceProvider.java} (68%) delete mode 100644 android-core/plugins/org.eclipse.andmore.base/libs/annotations.jar delete mode 100644 android-core/plugins/org.eclipse.andmore.base/libs/common.jar delete mode 100644 android-core/plugins/org.eclipse.andmore.base/libs/dvlib.jar delete mode 100644 android-core/plugins/org.eclipse.andmore.base/libs/guava-17.0.jar delete mode 100644 android-core/plugins/org.eclipse.andmore.base/libs/httpmime-4.1.jar delete mode 100644 android-core/plugins/org.eclipse.andmore.base/libs/kxml2-2.3.0.jar delete mode 100644 android-core/plugins/org.eclipse.andmore.base/libs/layoutlib-api.jar delete mode 100644 android-core/plugins/org.eclipse.andmore.base/libs/sdk-common.jar delete mode 100644 android-core/plugins/org.eclipse.andmore.base/libs/sdklib.jar delete mode 100644 android-core/plugins/org.eclipse.andmore.base/libs/sdkstats.jar rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/add.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/android.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/backward.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/capture.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/clear.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/d.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/debug-attach.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/debug-error.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/debug-wait.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/delete.png (100%) create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/device.png rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/diff.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/displayfilters.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/down.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/e.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/edit.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/empty.png (100%) create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/ddm/icons/emulator.png rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/file.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/folder.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/forward.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/gc.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/groupby.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/halt.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/heap.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/hprof.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/i.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/importBug.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/load.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/pause.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/play.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/pull.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/push.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/save.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/scroll_lock.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/sort_down.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/sort_up.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/thread.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/tracing_start.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/tracing_stop.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/up.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/v.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/w.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/warning.png (100%) rename {andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images => android-core/plugins/org.eclipse.andmore.ddms/ddm/icons}/zygote.png (100%) delete mode 100644 android-core/plugins/org.eclipse.andmore.ddms/libs/ddmlib.jar delete mode 100644 android-core/plugins/org.eclipse.andmore.ddms/libs/ddmuilib.jar delete mode 100644 android-core/plugins/org.eclipse.andmore.ddms/libs/uiautomatorviewer.jar create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/accept_icon16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/addon_pkg_16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/android_icon_128.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/android_icon_16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/archive_icon16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/broken_16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/broken_pkg_16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/buildtool_pkg_16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/device.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/doc_pkg_16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/emulator.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/error_icon_16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/extra_pkg_16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/incompat_icon16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/log_off_16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/log_on_16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/nopkg_icon_16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/pkg_incompat_16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/pkg_installed_16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/pkg_new_16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/pkg_update_16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/pkgcat_16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/pkgcat_other_16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/platform_pkg_16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/platformtool_pkg_16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/reject_icon16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/sample_pkg_16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/sdkman_logo_128.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/source_pkg_16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/status_ok_16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/stop_disabled_16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/stop_enabled_16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/sysimg_pkg_16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tag_android-tv_16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tag_android-tv_32.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tag_android-wear_16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tag_android-wear_32.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tag_default_16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tag_default_32.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tool_pkg_16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/unknown_icon16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/warning_icon16.png create mode 100644 android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/DdmResourceProvider.java rename {andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images => android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons}/auto-refresh.png (100%) rename {andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images => android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons}/capture-psd.png (100%) rename {andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images => android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons}/device-view-selected.png (100%) rename {andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images => android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons}/device-view.png (100%) create mode 100644 android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/device.png rename {andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images => android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons}/display.png (100%) create mode 100644 android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/emulator.png create mode 100644 android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/file.png rename {andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images => android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons}/filtered.png (100%) create mode 100644 android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/folder.png rename {andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images => android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons}/green.png (100%) rename {andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images => android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons}/inspect-screenshot.png (100%) rename {andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images => android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons}/invalidate.png (100%) rename {andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images => android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons}/load-all-views.png (100%) rename {andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images => android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons}/load-overlay.png (100%) rename {andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images => android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons}/load-view-hierarchy.png (100%) rename {andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images => android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons}/not-selected.png (100%) rename {andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images => android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons}/on-black.png (100%) rename {andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images => android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons}/on-white.png (100%) rename {andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images => android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons}/picker.png (100%) rename {andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images => android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons}/pixel-perfect-view-selected.png (100%) rename {andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images => android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons}/pixel-perfect-view.png (100%) rename {andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images => android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons}/profile.png (100%) rename {andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images => android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons}/red.png (100%) rename {andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images => android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons}/refresh-windows.png (100%) rename {andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images => android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons}/request-layout.png (100%) rename {andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images => android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons}/save.png (100%) rename {andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images => android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons}/sdk-hierarchyviewer-128.png (100%) rename {andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images => android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons}/sdk-hierarchyviewer-16.png (100%) rename {andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images => android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons}/selected-filtered-small.png (100%) rename {andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images => android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons}/selected-filtered.png (100%) rename {andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images => android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons}/selected-small.png (100%) rename {andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images => android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons}/selected.png (100%) rename {andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images => android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons}/show-extras.png (100%) rename {andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images => android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons}/show-overlay.png (100%) rename {andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images => android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons}/tree-view-selected.png (100%) rename {andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images => android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons}/tree-view.png (100%) rename {andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images => android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons}/yellow.png (100%) delete mode 100644 android-core/plugins/org.eclipse.andmore.hierarchyviewer/libs/hierarchyviewer2lib.jar create mode 100644 android-core/plugins/org.eclipse.andmore.hierarchyviewer/src/org/eclipse/andmore/hierarchyviewer/HiarchResourceProvider.java delete mode 100644 android-core/plugins/org.eclipse.andmore.traceview/libs/traceview.jar create mode 100644 android-core/plugins/org.eclipse.andmore/libs/.gitignore delete mode 100644 android-core/plugins/org.eclipse.andmore/libs/freemarker-2.3.20.jar delete mode 100644 android-core/plugins/org.eclipse.andmore/libs/lint-api.jar delete mode 100644 android-core/plugins/org.eclipse.andmore/libs/lint-checks.jar delete mode 100644 android-core/plugins/org.eclipse.andmore/libs/manifest-merger.jar delete mode 100644 android-core/plugins/org.eclipse.andmore/libs/sdkuilib.jar delete mode 100644 android-core/plugins/org.eclipse.andmore/libs/swtmenubar.jar create mode 100644 android-core/plugins/org.eclipse.andmore/src/com/android/sdklib/repository/FullRevision.java create mode 100644 android-core/plugins/org.eclipse.andmore/src/com/android/sdklib/repository/PreciseRevision.java create mode 100644 android-core/plugins/org.eclipse.andmore/src/org/eclipse/andmore/internal/editors/layout/RenderSecurityManager.java delete mode 100755 android-core/plugins/org.eclipse.andmore/src/org/eclipse/andmore/internal/sdk/AdtManifestMergeCallback.java diff --git a/andmore-core/features/feature/feature.xml b/andmore-core/features/feature/feature.xml index f8ca1f0c..1e756cdb 100644 --- a/andmore-core/features/feature/feature.xml +++ b/andmore-core/features/feature/feature.xml @@ -28,7 +28,6 @@ version="0.0.0"/> - diff --git a/andmore-core/plugins/android.codeutils/.classpath b/andmore-core/plugins/android.codeutils/.classpath index c72d35a0..54f561c7 100644 --- a/andmore-core/plugins/android.codeutils/.classpath +++ b/andmore-core/plugins/android.codeutils/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/andmore-core/plugins/android.codeutils/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/android.codeutils/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/android.codeutils/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/android.codeutils/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/android.codeutils/META-INF/MANIFEST.MF b/andmore-core/plugins/android.codeutils/META-INF/MANIFEST.MF index 842c005b..677a0a19 100644 --- a/andmore-core/plugins/android.codeutils/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/android.codeutils/META-INF/MANIFEST.MF @@ -1,34 +1,34 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.android.codeutils;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.android.codeutils.CodeUtilsActivator -Bundle-Vendor: %providerName -Bundle-ActivationPolicy: lazy -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Bundle-Localization: plugin -Export-Package: org.eclipse.andmore.android.codeutils.codegeneration, - org.eclipse.andmore.android.codeutils.db.actions, - org.eclipse.andmore.android.codeutils.db.utils, - org.eclipse.andmore.android.codeutils.wizards, - org.eclipse.andmore.android.db.deployment, - org.eclipse.andmore.android.db.wizards.model, - org.eclipse.andmore.wizards.buildingblocks -Bundle-ClassPath: . -Require-Bundle: org.eclipse.core.runtime, - org.eclipse.core.resources, - org.eclipse.datatools.connectivity, - org.eclipse.datatools.modelbase.sql, - org.eclipse.jdt.ui, - org.eclipse.ui, - org.eclipse.ui.ide, - org.eclipse.andmore.android, - org.eclipse.andmore.android.common, - org.eclipse.jface.text, - org.eclipse.datatools.connectivity.sqm.core, - org.eclipse.jdt.core, - org.apache.xerces, - org.eclipse.datatools.sqltools.data.ui, - org.eclipse.sequoyah.localization.tools -Import-Package: org.eclipse.ui.texteditor +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.android.codeutils;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.android.codeutils.CodeUtilsActivator +Bundle-Vendor: %providerName +Bundle-ActivationPolicy: lazy +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-Localization: plugin +Export-Package: org.eclipse.andmore.android.codeutils.codegeneration, + org.eclipse.andmore.android.codeutils.db.actions, + org.eclipse.andmore.android.codeutils.db.utils, + org.eclipse.andmore.android.codeutils.wizards, + org.eclipse.andmore.android.db.deployment, + org.eclipse.andmore.android.db.wizards.model, + org.eclipse.andmore.wizards.buildingblocks +Bundle-ClassPath: . +Require-Bundle: org.eclipse.core.runtime, + org.eclipse.core.resources, + org.eclipse.datatools.connectivity, + org.eclipse.datatools.modelbase.sql, + org.eclipse.jdt.ui, + org.eclipse.ui, + org.eclipse.ui.ide, + org.eclipse.andmore.android, + org.eclipse.andmore.android.common, + org.eclipse.jface.text, + org.eclipse.datatools.connectivity.sqm.core, + org.eclipse.jdt.core, + org.apache.xerces, + org.eclipse.datatools.sqltools.data.ui, + org.eclipse.sequoyah.localization.tools +Import-Package: org.eclipse.ui.texteditor diff --git a/andmore-core/plugins/android.win32.x86_64/.classpath b/andmore-core/plugins/android.win32.x86_64/.classpath index 0b1bcf94..7498423d 100644 --- a/andmore-core/plugins/android.win32.x86_64/.classpath +++ b/andmore-core/plugins/android.win32.x86_64/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/andmore-core/plugins/android.win32.x86_64/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/android.win32.x86_64/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/android.win32.x86_64/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/android.win32.x86_64/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/android.win32.x86_64/META-INF/MANIFEST.MF b/andmore-core/plugins/android.win32.x86_64/META-INF/MANIFEST.MF index 7acf665a..b3ee5082 100644 --- a/andmore-core/plugins/android.win32.x86_64/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/android.win32.x86_64/META-INF/MANIFEST.MF @@ -1,10 +1,10 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %Bundle-Name -Bundle-SymbolicName: org.eclipse.andmore.android.win32.x86_64;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Vendor: %Bundle-Vendor -Fragment-Host: org.eclipse.andmore.android -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Bundle-Localization: fragment -Eclipse-PlatformFilter: (& (osgi.os=win32) (osgi.arch=x86_64)) +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %Bundle-Name +Bundle-SymbolicName: org.eclipse.andmore.android.win32.x86_64;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Vendor: %Bundle-Vendor +Fragment-Host: org.eclipse.andmore.android +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-Localization: fragment +Eclipse-PlatformFilter: (& (osgi.os=win32) (osgi.arch=x86_64)) diff --git a/andmore-core/plugins/android/.classpath b/andmore-core/plugins/android/.classpath index c72d35a0..54f561c7 100644 --- a/andmore-core/plugins/android/.classpath +++ b/andmore-core/plugins/android/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/andmore-core/plugins/android/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/android/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/android/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/android/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/android/META-INF/MANIFEST.MF b/andmore-core/plugins/android/META-INF/MANIFEST.MF index 7f7ada1e..51e0e04d 100644 --- a/andmore-core/plugins/android/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/android/META-INF/MANIFEST.MF @@ -1,46 +1,48 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.android;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.android.AndroidPlugin -Bundle-Vendor: %providerName -Require-Bundle: org.eclipse.ui, - org.eclipse.ui.ide, - org.eclipse.andmore.android.common, - org.eclipse.andmore.android.logger.collector, - org.eclipse.core.runtime, - org.eclipse.core.filesystem, - org.eclipse.andmore, - org.eclipse.andmore.ddms, - org.eclipse.jdt.core, - org.eclipse.jface.text, - org.apache.xerces, - org.eclipse.ui.console, - org.eclipse.sequoyah.localization.tools, - org.eclipse.sequoyah.device.framework, - org.eclipse.sequoyah.device.common.utilities, - org.eclipse.debug.ui, - org.eclipse.core.expressions, - org.eclipse.andmore.base, - org.eclipse.jdt.ui, - org.apache.commons.net;bundle-version="1.4.1", - org.apache.oro;bundle-version="2.0.8" -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Bundle-Localization: plugin -Export-Package: - org.eclipse.andmore.android, - org.eclipse.andmore.android.devices, - org.eclipse.andmore.android.i18n, - org.eclipse.andmore.android.model, - org.eclipse.andmore.android.multidex, - org.eclipse.andmore.android.nativeos, - org.eclipse.andmore.android.sdkmanager, - org.eclipse.andmore.android.utilities, - org.eclipse.andmore.android.wizards.elements, - org.eclipse.andmore.android.wizards.installapp, - org.eclipse.andmore.android.wizards.monkey -Bundle-ActivationPolicy: lazy -Import-Package: org.eclipse.equinox.security.storage, - org.eclipse.ui.console -Bundle-ClassPath: . +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.android;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.android.AndroidPlugin +Bundle-Vendor: %providerName +Require-Bundle: org.eclipse.ui, + org.eclipse.ui.ide, + org.eclipse.andmore.android.common, + org.eclipse.andmore.android.logger.collector, + org.eclipse.core.runtime, + org.eclipse.core.filesystem, + org.eclipse.andmore, + org.eclipse.andmore.ddms, + org.eclipse.jdt.core, + org.eclipse.jface.text, + org.apache.xerces, + org.eclipse.ui.console, + org.eclipse.sequoyah.localization.tools, + org.eclipse.sequoyah.device.framework, + org.eclipse.sequoyah.device.common.utilities, + org.eclipse.debug.ui, + org.eclipse.core.expressions, + org.eclipse.jdt.ui, + org.apache.commons.net;bundle-version="1.4.1", + org.apache.oro;bundle-version="2.0.8", + org.eclipse.andmore.swt;bundle-version="0.5.2", + org.eclipse.andmore.ddmuilib;bundle-version="0.5.2", + org.eclipse.andmore.sdkuilib;bundle-version="0.5.2" +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-Localization: plugin +Export-Package: + org.eclipse.andmore.android, + org.eclipse.andmore.android.devices, + org.eclipse.andmore.android.i18n, + org.eclipse.andmore.android.model, + org.eclipse.andmore.android.multidex, + org.eclipse.andmore.android.nativeos, + org.eclipse.andmore.android.sdkmanager, + org.eclipse.andmore.android.utilities, + org.eclipse.andmore.android.wizards.elements, + org.eclipse.andmore.android.wizards.installapp, + org.eclipse.andmore.android.wizards.monkey +Bundle-ActivationPolicy: lazy +Import-Package: org.eclipse.equinox.security.storage, + org.eclipse.ui.console +Bundle-ClassPath: . diff --git a/andmore-core/plugins/android/src/org/eclipse/andmore/android/SdkUtils.java b/andmore-core/plugins/android/src/org/eclipse/andmore/android/SdkUtils.java index 393fc889..0fe79f7c 100644 --- a/andmore-core/plugins/android/src/org/eclipse/andmore/android/SdkUtils.java +++ b/andmore-core/plugins/android/src/org/eclipse/andmore/android/SdkUtils.java @@ -1,950 +1,951 @@ -/* - * Copyright (C) 2012 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 org.eclipse.andmore.android; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FilenameFilter; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Properties; - -import org.eclipse.andmore.AndmoreAndroidPlugin; -import org.eclipse.andmore.android.common.IAndroidConstants; -import org.eclipse.andmore.android.common.exception.AndroidException; -import org.eclipse.andmore.android.common.log.AndmoreLogger; -import org.eclipse.andmore.android.common.utilities.FileUtil; -import org.eclipse.andmore.android.i18n.AndroidNLS; -import org.eclipse.andmore.android.manifest.AndroidProjectManifestFile; -import org.eclipse.andmore.android.model.manifest.AndroidManifestFile; -import org.eclipse.andmore.android.model.manifest.dom.AndroidManifestNode; -import org.eclipse.andmore.android.model.manifest.dom.UsesSDKNode; -import org.eclipse.andmore.internal.sdk.AndroidTargetData; -import org.eclipse.andmore.internal.sdk.Sdk; -import org.eclipse.core.resources.IProject; -import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.Status; -import org.eclipse.osgi.util.NLS; -import org.eclipse.swt.widgets.Display; -import org.eclipse.ui.PlatformUI; - -import com.android.SdkConstants; -import com.android.sdklib.AndroidVersion; -import com.android.sdklib.IAndroidTarget; -import com.android.sdklib.internal.avd.AvdInfo; -import com.android.sdklib.internal.avd.AvdManager; -import com.android.sdkuilib.internal.widgets.MessageBoxLog; -import com.android.utils.ILogger; - -/** - * DESCRIPTION: This class provides utility methods related to the Android SDK.
- * USAGE: See public methods - */ - -public class SdkUtils { - public static final int API_LEVEL_FOR_PLATFORM_VERSION_3_0_0 = 11; - - public static final String VM_CONFIG_FILENAME = "config.ini"; //$NON-NLS-1$ - - public static final String USERIMAGE_FILENAME = "userdata-qemu.img"; //$NON-NLS-1$ - - public static final String[] STATEDATA_FILENAMES = { "cache.img", "userdata.img", "emulator-user.ini" //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - }; - - public static final String EMU_CONFIG_SKIN_NAME_PROPERTY = "skin.name"; //$NON-NLS-1$ - - /** - * Gets the current SDK object - */ - public static Sdk getCurrentSdk() { - return Sdk.getCurrent(); - } - - /** - * Gets the directory where the configured SDK is located - */ - public static String getSdkPath() { - String sdkDir = null; - Sdk sdk = getCurrentSdk(); - if (sdk != null) { - sdkDir = sdk.getSdkOsLocation(); - } - return sdkDir; - } - - /** - * Gets the path to the "tools" folder of the SDK - * - * @return - */ - public static String getSdkToolsPath() { - return AndmoreAndroidPlugin.getOsSdkToolsFolder(); - } - - public static IAndroidTarget getTargetByAPILevel(Integer apiLevel) { - IAndroidTarget returnTarget = null; - - for (IAndroidTarget target : getAllTargets()) { - if (target.getVersion().getApiLevel() == apiLevel) { - returnTarget = target; - } - } - return returnTarget; - } - - /** - * Get the AAPT application path from an android target if null is passed, - * try to get some aapt - * - * @param target - * @return - */ - public static String getTargetAAPTPath(IAndroidTarget target) { - IAndroidTarget realTarget = null; - if (target == null) { - AndmoreLogger.warn(SdkUtils.class, "Trying to find a suitable aapt application to use"); //$NON-NLS-1$ - IAndroidTarget[] allTargets = Sdk.getCurrent().getTargets(); - if (allTargets.length > 0) { - realTarget = allTargets[0]; - } - } else { - realTarget = target; - } - - while ((realTarget != null) && !realTarget.isPlatform()) { - realTarget = realTarget.getParent(); - } - - if (realTarget == null) { - AndmoreLogger.warn(SdkUtils.class, "No aapt executable found: do you have an Android platform installed?"); //$NON-NLS-1$ - } - - return realTarget != null ? realTarget.getPath(IAndroidTarget.ANDROID_JAR) : null; - } - - /** - * Gets the path to the "adb" executable of the SDK - * - * @return - */ - public static String getAdbPath() { - return AndmoreAndroidPlugin.getOsAbsoluteAdb(); - } - - /** - * Reloads the recognized AVD list - */ - public static void reloadAvds() { - - try { - getVmManager().reloadAvds(NullSdkLogger.getLogger()); - } catch (Exception e) { - AndmoreLogger.error(SdkUtils.class, "Error while reloading AVDs"); //$NON-NLS-1$ - } - - } - - protected static class NullSdkLogger implements ILogger { - - private static NullSdkLogger logger; - - private NullSdkLogger() { - - } - - public static ILogger getLogger() { - if (logger == null) { - logger = new NullSdkLogger(); - } - return logger; - } - - @Override - public void error(Throwable arg0, String arg1, Object... arg2) { - - } - - @Override - public void info(String arg0, Object... arg1) { - - } - - @Override - public void verbose(String arg0, Object... arg1) { - - } - - @Override - public void warning(String arg0, Object... arg1) { - - } - - } - - /** - * Gets the VmManager object - */ - public static AvdManager getVmManager() { - AvdManager vmManager = null; - Sdk sdk = getCurrentSdk(); - if (sdk != null) { - vmManager = sdk.getAvdManager(); - } - - return vmManager; - } - - /** - * Gets all available Targets - */ - public static IAndroidTarget[] getAllTargets() { - IAndroidTarget[] allTargets = null; - Sdk sdk = getCurrentSdk(); - if (sdk != null) { - allTargets = sdk.getTargets(); - } - return allTargets; - } - - /** - * Gets a Target by name - * - * @param name - * the target name - */ - public static IAndroidTarget getTargetByName(String name) { - IAndroidTarget ret = null; - - if ((name != null) && (name.length() > 0)) { - IAndroidTarget[] allTargets = getAllTargets(); - - for (int i = 0; i < allTargets.length; i++) { - if (name.equals(allTargets[i].getName())) { - ret = allTargets[i]; - break; - } - } - } - - return ret; - } - - /** - * Gets all VMs - */ - public static AvdInfo[] getAllVms() { - AvdInfo[] allVmInfo = null; - AvdManager vmManager = getVmManager(); - if (vmManager != null) { - allVmInfo = vmManager.getAllAvds(); - } - return allVmInfo; - } - - /** - * Gets all valid VMs - */ - public static AvdInfo[] getAllValidVms() { - AvdInfo[] validVmInfo = null; - AvdManager vmManager = getVmManager(); - if (vmManager != null) { - validVmInfo = vmManager.getValidAvds(); - } - return validVmInfo; - } - - /** - * Gets the name of all VMs that are recognized by the configured SDK. - */ - public static Collection getAllVmNames() { - Collection vmNames = new LinkedList(); - for (AvdInfo vm : getAllVms()) { - vmNames.add(vm.getName()); - } - return vmNames; - } - - /** - * Gets the name of all VMs that are recognized by the configured SDK. - */ - public static Collection getAllValidVmNames() { - Collection vmNames = new LinkedList(); - AvdInfo[] allAvds = getAllValidVms(); - if (allAvds != null) { - for (AvdInfo vm : allAvds) { - vmNames.add(vm.getName()); - } - } - - return vmNames; - } - - /** - * Gets a skin name - * - * @param vmInfo - * the VM to get the skin - */ - public static String getSkin(AvdInfo vmInfo) { - String skin = ""; //$NON-NLS-1$ - File configFile = vmInfo.getConfigFile(); - Properties p = new Properties(); - InputStream configFileStream = null; - try { - configFileStream = new FileInputStream(configFile); - p.load(configFileStream); - skin = p.getProperty(EMU_CONFIG_SKIN_NAME_PROPERTY); - } catch (FileNotFoundException e) { - AndmoreLogger.error(SdkUtils.class, - "Error getting VM skin definition. Could not find file " + configFile.getAbsolutePath(), e); //$NON-NLS-1$ - } catch (IOException e) { - AndmoreLogger.error(SdkUtils.class, - "Error getting VM skin definition. Could not access file " + configFile.getAbsolutePath(), e); //$NON-NLS-1$ - } finally { - if (configFileStream != null) { - try { - configFileStream.close(); - } catch (IOException e) { - // nothing to do - } - } - } - - return skin; - } - - /** - * Gets a VM by name. - * - * @param vmName - * The VM name - */ - public static AvdInfo getValidVm(String vmName) { - AvdInfo vmInfo = null; - AvdManager vmManager = getVmManager(); - if (vmManager != null) { - vmInfo = vmManager.getAvd(vmName, true); - } - return vmInfo; - } - - /** - * Gets a VM by name. - * - * @param vmName - * The VM name - */ - public static AvdInfo getVm(String vmName) { - AvdInfo vmInfo = null; - AvdManager vmManager = getVmManager(); - if (vmManager != null) { - vmInfo = vmManager.getAvd(vmName, false); - } - return vmInfo; - } - - /** - * Creates a new VM instance. - * - * @param folder - * Folder where the VM files will be stored - * @param name - * VM Name - * @param target - * VM Target represented by the IAndroidTarget object - * @param skinName - * VM Skin name from the VM Target - * - * @throws CoreException - */ - public static AvdInfo createVm(String folder, String name, IAndroidTarget target, String abiType, String skinName, - String useSnapshot, String sdCard) throws CoreException - - { - AvdInfo vmInfo; - AvdManager vmManager = SdkUtils.getVmManager(); - - // get the abi type - if (abiType == null) { - abiType = SdkConstants.ABI_ARMEABI; - } - - /* - * public VmInfo createVm(String parentFolder, String name, - * IAndroidTarget target, String skinName, String sdcard, Map - * hardwareConfig) - */ - - // TODO: FIX ME commented out for now. - vmInfo = null; - // vmInfo = vmManager.createAvd(new File(folder), name, target, abiType, - // skinName, sdCard, null, Boolean.parseBoolean(useSnapshot), - // true, false, NullSdkLogger.getLogger()); - - if (vmInfo == null) { - String errMsg = NLS.bind(AndroidNLS.EXC_SdkUtils_CannotCreateTheVMInstance, name); - - IStatus status = new Status(IStatus.ERROR, AndmoreAndroidPlugin.PLUGIN_ID, errMsg); - throw new CoreException(status); - } - - return vmInfo; - } - - /** - * Deletes a VM instance. - * - * @param name - * VM Name - */ - public static void deleteVm(String name) { - - AvdManager vmManager = SdkUtils.getVmManager(); - AvdInfo avdToDelete = vmManager != null ? vmManager.getAvd(name, false) : null; - if (avdToDelete != null) { - try { - if ((avdToDelete.getIniFile() != null) && avdToDelete.getIniFile().exists()) { - avdToDelete.getIniFile().delete(); - } - String path = avdToDelete.getDataFolderPath(); - if (path != null) { - File avdDir = new File(path); - if (avdDir.exists()) { - FileUtil.deleteDirRecursively(avdDir); - } - } - vmManager.removeAvd(avdToDelete); - - } catch (Exception e) { - AndmoreLogger.error("Could not delete AVD: " + e.getMessage()); //$NON-NLS-1$ - } - } - - } - - /** - * Get the reference to the File that points to the filesystem location of - * the directory where the user data files of the VM with the given name are - * stored. - * - * @param vmName - * name of the VM whose userdata directory is to be retrieved. - * @return the File object that references the filesystem location of the - * directory where the userdata files of the given VM will be - * stored. Returns a null reference if SDK is not configured or if - * there is no VM with the given name. - */ - public static File getUserdataDir(String vmName) { - AvdInfo vminfo = SdkUtils.getValidVm(vmName); - File userdataDir = null; - - if (vminfo != null) { - String vmpath = vminfo.getDataFolderPath(); - userdataDir = new File(vmpath); - } - - return userdataDir; - } - - /** - * Get the reference to the File that points to the filesystem location - * where the user data file of the VM with the given name is. - * - * @param vmName - * name of the VM whose userdata file is to be retrieved. - * @return the File object that references the filesystem location where the - * userdata of the given VM should be. Returns a null reference if - * SDK is not configured or if there is no VM with the given name. - */ - public static File getUserdataFile(String vmName) { - File userdataDir = getUserdataDir(vmName); - File userdataFile = null; - - if (userdataDir != null) { - userdataFile = new File(userdataDir, USERIMAGE_FILENAME); - } - - return userdataFile; - } - - /** - * Get the reference to the Files that point to the filesystem location - * where the state data files of the VM with the given name are. - * - * @param vmName - * name of the VM whose state data files is to be retrieved. - * @return the File objects that reference the filesystem location where the - * state data files of the given VM should be. Returns a null - * reference if SDK is not configured or if there is no VM with the - * given name. - */ - public static List getStateDataFiles(String vmName) { - File userdataDir = getUserdataDir(vmName); - List stateDataFiles = null; - - if (userdataDir != null) { - stateDataFiles = new ArrayList(); - - for (int i = 0; i < STATEDATA_FILENAMES.length; i++) { - stateDataFiles.add(new File(userdataDir, STATEDATA_FILENAMES[i])); - } - } - - return stateDataFiles; - } - - /** - * Retrieves all sample applications from a target - * - * @param target - * The target - * @return all sample applications from a target - */ - public static Object[] getSamples(IAndroidTarget target) { - List samples = new ArrayList(); - File samplesFolder = new File(target.getPath(IAndroidTarget.SAMPLES)); - samples = findSamples(samplesFolder, target); - return samples.toArray(); - } - - /** - * Find the samples inside an specific directory (recursively) - * - * @param folder - * the folder that can contain samples - * @param target - * the target of the samples in the folder - * @return a list of samples - */ - private static List findSamples(File folder, IAndroidTarget target) { - - List samples = new ArrayList(); - - if (folder.exists() && folder.isDirectory()) { - for (File sampleFolder : folder.listFiles()) { - if (sampleFolder.isDirectory()) { - if (Sample.isSample(sampleFolder)) { - samples.add(new Sample(sampleFolder, target)); - } else { - samples.addAll(findSamples(sampleFolder, target)); - } - } - } - } - - return samples; - } - - /** - * Retrieves all targets for a given SDK - * - * @param sdk - * The sdk - * - * @return all targets for the given SDK - */ - public static Object[] getTargets(Sdk sdk) { - Object[] targets = null; - if (sdk != null) { - targets = sdk.getTargets(); - } - return targets; - } - - /** - * Associates a project to a target - * - * @param project - * The project - * @param target - * The target - */ - public static void associate(IProject project, IAndroidTarget target) { - try { - Sdk.getCurrent().initProject(project, target); - } catch (Exception e) { - AndmoreLogger.error(SdkUtils.class, "Error associating project " + project.getName() //$NON-NLS-1$ - + " with target " + target.getName()); //$NON-NLS-1$ - } - } - - /** - * Retrieves the target for a project - * - * @param project - * the project - * - * @return the target for the project - */ - public static IAndroidTarget getTarget(IProject project) { - IAndroidTarget target = null; - if (project != null) { - target = Sdk.getCurrent().getTarget(project); - } - return target; - } - - /** - * Retrieves the target for a project - * - * @param project - * the project - * - * @return the target for the project - */ - public static String getMinSdkVersion(IProject project) { - String minSdkVersion = ""; - try { - AndroidManifestFile androidManifestFile = AndroidProjectManifestFile.getFromProject(project); - UsesSDKNode usesSdkNode = (UsesSDKNode) androidManifestFile.getNode(AndroidManifestNode.NodeType.UsesSdk); - if (usesSdkNode != null) { - minSdkVersion = usesSdkNode.getMinSdkVersion(); - } - - } catch (AndroidException e) { - AndmoreLogger.error("Error getting min sdk version. " + e.getMessage()); - } catch (CoreException e) { - AndmoreLogger.error("Error getting min sdk version. " + e.getMessage()); - } - return minSdkVersion; - } - - /** - * Retrieves all Activity Actions for a project. - * - * @param project - * The project - * - * @return all Activity Actions for the project. - */ - public static String[] getActivityActions(IProject project) { - String[] attributeValues = new String[0]; - - if ((project != null) && project.isOpen()) { - IAndroidTarget target = SdkUtils.getTarget(project); - AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target); - - if (targetData != null) { - attributeValues = targetData.getAttributeValues("action", "android:name", "activity"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - } - } - - return attributeValues; - } - - /** - * Retrieves all Service Actions for a project. - * - * @param project - * The project - * - * @return all Service Actions for the project. - */ - public static String[] getServiceActions(IProject project) { - String[] attributeValues = new String[0]; - - if ((project != null) && project.isOpen()) { - IAndroidTarget target = SdkUtils.getTarget(project); - AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target); - - if (targetData != null) { - attributeValues = targetData.getAttributeValues("action", "android:name", "service"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - } - } - - return attributeValues; - } - - /** - * Retrieves all Broadcast Receiver Actions for a project. - * - * @param project - * The project - * - * @return all Broadcast Receiver Actions for the project. - */ - public static String[] getReceiverActions(IProject project) { - String[] attributeValues = new String[0]; - - if ((project != null) && project.isOpen()) { - IAndroidTarget target = SdkUtils.getTarget(project); - AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target); - - if (targetData != null) { - attributeValues = targetData.getAttributeValues("action", "android:name", "receiver"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - } - } - - return attributeValues; - } - - /** - * Retrieves all Intent Filter Actions for a project. - * - * @param project - * The project - * - * @return all Intent Filter Actions for the project. - */ - public static String[] getIntentFilterCategories(IProject project) { - String[] attributeValues = new String[0]; - - if ((project != null) && project.isOpen()) { - IAndroidTarget target = SdkUtils.getTarget(project); - AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target); - - if (targetData != null) { - attributeValues = targetData.getAttributeValues("category", "android:name"); //$NON-NLS-1$ //$NON-NLS-2$ - } - } - - return attributeValues; - } - - /** - * Get the api version number for a given project - * - * @param project - * : the project - * @return the api version number or 0 if some error occurs - */ - public static int getApiVersionNumberForProject(IProject project) { - int api = 0; - IAndroidTarget target = SdkUtils.getTarget(project); - if (target != null) { - AndroidVersion version = target.getVersion(); - if (version != null) { - api = version.getApiLevel(); - } - } - return api; - } - - public static String getTargetNameForProject(IProject project) { - IAndroidTarget target = getTarget(project); - return target != null ? target.getName() : ""; //$NON-NLS-1$ - } - - public static boolean isPlatformTarget(String avdName) { - IAndroidTarget target = getValidVm(avdName).getTarget(); - return target != null ? target.isPlatform() : false; - } - - public static boolean isProjectTargetAPlatform(IProject project) { - IAndroidTarget target = getTarget(project); - return target != null ? target.isPlatform() : false; - } - - public static boolean isPlatformTarget(IAndroidTarget target) { - return target != null ? target.isPlatform() : false; - } - - /** - * Retrieves the APK configurations of a project - * - * @param project - * the project - * @return the APK configurations - */ - public static Map getAPKConfigurationsForProject(IProject project) { - - Map apkConfigurations = null; - - if ((project != null) && project.isOpen()) { - Sdk.getCurrent(); - // This is not supported on ADT 14 preview so let's comment it for - // now. - // apkConfigurations = - // Sdk.getProjectState(project).getApkSettings().getLocaleFilters(); - apkConfigurations = new HashMap(0); - } - - return apkConfigurations; - - } - - public static String getBaseTarget(String name) { - IAndroidTarget target = getValidVm(name).getTarget(); - while (!target.isPlatform()) { - target = target.getParent(); - } - return target.getName(); - } - - /** - * Check if an SDK is an OPhone Sdk - * - * @return - */ - public static boolean isOphoneSDK() { - boolean result = false; - - // check if the folder contains the oms jar - FilenameFilter omsFilenameFilter = new FilenameFilter() { - @Override - public boolean accept(File arg0, String arg1) { - return arg1.equals(IAndroidConstants.OPHONE_JAR); - } - }; - - Sdk sdk = getCurrentSdk(); - IAndroidTarget[] targets = sdk.getTargets(); - for (IAndroidTarget target : targets) { - File folder = new File(target.getLocation()); - if (folder.list(omsFilenameFilter).length > 0) { - result = true; - break; - } - } - - return result; - } - - /** - * Check if an SDK is an JIL sdk - * - * @return - */ - public static boolean isJILSdk() { - boolean result = false; - - // check if the folder contains the oms jar - FilenameFilter jilFilenameFilter = new FilenameFilter() { - - @Override - public boolean accept(File arg0, String arg1) { - return arg1.equals(IAndroidConstants.JIL_JAR); - } - }; - - Sdk sdk = getCurrentSdk(); - if (sdk != null) { - IAndroidTarget[] targets = sdk.getTargets(); - for (IAndroidTarget target : targets) { - File folder = new File(target.getLocation()); - if (folder.list(jilFilenameFilter).length > 0) { - result = true; - break; - } - } - } - - return result; - } - - public static String getEmulatorWindowName(String avdName, int port) { - String windowName = ""; //$NON-NLS-1$ - if (isJILSdk()) { - windowName = "JIL Emulator (" + avdName + ":" + port + ")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - } else if (isOphoneSDK()) { - windowName = "OPhone Emulator (" + avdName + ":" + port + ")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - } else { - windowName = port + ":" + avdName; //$NON-NLS-1$ - } - return windowName; - } - - public static boolean isLibraryProject(IProject project) { - return Sdk.getProjectState(project) != null ? Sdk.getProjectState(project).isLibrary() : false; - } - - /** - * Returns all available permissions - * - * @return String array containing the available permissions - */ - public static String[] getIntentFilterPermissions(IProject project) { - String[] attributeValues = new String[0]; - - if ((project != null) && project.isOpen()) { - IAndroidTarget target = SdkUtils.getTarget(project); - AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target); - - if (targetData != null) { - attributeValues = targetData.getAttributeValues("uses-permission", "android:name"); //$NON-NLS-1$ //$NON-NLS-2$ - } - } - - return attributeValues; - } - - /** - * Try to repair an AVD. Currently only avds with wrong image path are - * repariable. Display a message with the changes to the config.ini - * - * @param avdInfo - * @return Status ERROR if an IO exception occured. - */ - public static IStatus repairAvd(AvdInfo avdInfo) { - IStatus status = Status.OK_STATUS; - - AvdManager avdManager = Sdk.getCurrent().getAvdManager(); - Display display = PlatformUI.getWorkbench().getDisplay(); - ILogger log = new MessageBoxLog(String.format("Result of updating AVD '%s':", avdInfo.getName()), //$NON-NLS-1$ - display, false); - try { - avdManager.updateAvd(avdInfo, log); - // display the result - if (log instanceof MessageBoxLog) { - ((MessageBoxLog) log).displayResult(true); - } - SdkUtils.reloadAvds(); - - } catch (IOException e) { - status = new Status(IStatus.ERROR, AndroidPlugin.PLUGIN_ID, AndroidNLS.SdkUtils_COULD_NOT_REPAIR_AVD, e); - } - - return status; - } - - public static String getDefaultSkin(String targetName) { - IAndroidTarget target = getTargetByName(targetName); - target.getDefaultSkin().getName(); - return target != null ? target.getDefaultSkin().getName() : "HVGA"; - } - - /** - * Returns the full absolute OS path to a skin specified by name for a given - * target. - * - * @param skinName - * The name of the skin to find. Case-sensitive. - * @param target - * The target where to find the skin. - * @return a {@link File} that may or may not actually exist. - */ - public static File getSkinFolder(String skinName, IAndroidTarget target) { - String path = target.getPath(IAndroidTarget.SKINS); - File skin = new File(path, skinName); - - if (skin.exists() == false && target.isPlatform() == false) { - target = target.getParent(); - - path = target.getPath(IAndroidTarget.SKINS); - skin = new File(path, skinName); - } - - return skin; - } -} +/* + * Copyright (C) 2012 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 org.eclipse.andmore.android; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import org.eclipse.andmore.AndmoreAndroidPlugin; +import org.eclipse.andmore.android.common.IAndroidConstants; +import org.eclipse.andmore.android.common.exception.AndroidException; +import org.eclipse.andmore.android.common.log.AndmoreLogger; +import org.eclipse.andmore.android.common.utilities.FileUtil; +import org.eclipse.andmore.android.i18n.AndroidNLS; +import org.eclipse.andmore.android.manifest.AndroidProjectManifestFile; +import org.eclipse.andmore.android.model.manifest.AndroidManifestFile; +import org.eclipse.andmore.android.model.manifest.dom.AndroidManifestNode; +import org.eclipse.andmore.android.model.manifest.dom.UsesSDKNode; +import org.eclipse.andmore.internal.sdk.AndroidTargetData; +import org.eclipse.andmore.internal.sdk.Sdk; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.osgi.util.NLS; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.PlatformUI; + +import com.android.SdkConstants; +import com.android.repository.io.FileOpUtils; +import com.android.repository.testframework.FakeProgressIndicator; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.internal.avd.AvdInfo; +import com.android.sdklib.internal.avd.AvdInfo.AvdStatus; +import com.android.sdklib.internal.avd.AvdManager; +import com.android.sdklib.repository.AndroidSdkHandler; +import com.android.sdkuilib.widgets.MessageBoxLog; +import com.android.utils.ILogger; + +/** + * DESCRIPTION: This class provides utility methods related to the Android SDK.
+ * USAGE: See public methods + */ + +public class SdkUtils { + public static final int API_LEVEL_FOR_PLATFORM_VERSION_3_0_0 = 11; + + public static final String VM_CONFIG_FILENAME = "config.ini"; //$NON-NLS-1$ + + public static final String USERIMAGE_FILENAME = "userdata-qemu.img"; //$NON-NLS-1$ + + public static final String[] STATEDATA_FILENAMES = { "cache.img", "userdata.img", "emulator-user.ini" //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + }; + + public static final String EMU_CONFIG_SKIN_NAME_PROPERTY = "skin.name"; //$NON-NLS-1$ + + /** + * Gets the current SDK object + */ + public static Sdk getCurrentSdk() { + return Sdk.getCurrent(); + } + + /** + * Gets the directory where the configured SDK is located + */ + public static String getSdkPath() { + String sdkDir = null; + Sdk sdk = getCurrentSdk(); + if (sdk != null) { + sdkDir = sdk.getSdkFileLocation().getAbsolutePath(); + } + return sdkDir; + } + + /** + * Gets the path to the "tools" folder of the SDK + * + * @return + */ + public static String getSdkToolsPath() { + return AndmoreAndroidPlugin.getOsSdkToolsFolder(); + } + + public static IAndroidTarget getTargetByAPILevel(Integer apiLevel) { + IAndroidTarget returnTarget = null; + + for (IAndroidTarget target : getAllTargets()) { + if (target.getVersion().getApiLevel() == apiLevel) { + returnTarget = target; + } + } + return returnTarget; + } + + /** + * Get the AAPT application path from an android target if null is passed, + * try to get some aapt + * + * @param target + * @return + */ + public static String getTargetAAPTPath(IAndroidTarget target) { + IAndroidTarget realTarget = null; + if (target == null) { + AndmoreLogger.warn(SdkUtils.class, "Trying to find a suitable aapt application to use"); //$NON-NLS-1$ + Collection allTargets = Sdk.getCurrent().getTargets(); + if (allTargets.size() > 0) { + realTarget = allTargets.iterator().next(); + } + } else { + realTarget = target; + } + + while ((realTarget != null) && !realTarget.isPlatform()) { + realTarget = realTarget.getParent(); + } + + if (realTarget == null) { + AndmoreLogger.warn(SdkUtils.class, "No aapt executable found: do you have an Android platform installed?"); //$NON-NLS-1$ + } + + return realTarget != null ? realTarget.getPath(IAndroidTarget.ANDROID_JAR) : null; + } + + /** + * Gets the path to the "adb" executable of the SDK + * + * @return + */ + public static String getAdbPath() { + return AndmoreAndroidPlugin.getOsAbsoluteAdb(); + } + + /** + * Reloads the recognized AVD list + */ + public static void reloadAvds() { + + try { + getVmManager().reloadAvds(NullSdkLogger.getLogger()); + } catch (Exception e) { + AndmoreLogger.error(SdkUtils.class, "Error while reloading AVDs"); //$NON-NLS-1$ + } + + } + + protected static class NullSdkLogger implements ILogger { + + private static NullSdkLogger logger; + + private NullSdkLogger() { + + } + + public static ILogger getLogger() { + if (logger == null) { + logger = new NullSdkLogger(); + } + return logger; + } + + @Override + public void error(Throwable arg0, String arg1, Object... arg2) { + + } + + @Override + public void info(String arg0, Object... arg1) { + + } + + @Override + public void verbose(String arg0, Object... arg1) { + + } + + @Override + public void warning(String arg0, Object... arg1) { + + } + + } + + /** + * Gets the VmManager object + */ + public static AvdManager getVmManager() { + AvdManager vmManager = null; + Sdk sdk = getCurrentSdk(); + if (sdk != null) { + vmManager = sdk.getAvdManager(); + } + + return vmManager; + } + + /** + * Gets all available Targets + */ + public static IAndroidTarget[] getAllTargets() { + IAndroidTarget[] allTargets = null; + Sdk sdk = getCurrentSdk(); + if (sdk != null) { + allTargets = sdk.getTargets().toArray(new IAndroidTarget[0]); + } + return allTargets; + } + + /** + * Gets a Target by name + * + * @param name + * the target name + */ + public static IAndroidTarget getTargetByName(String name) { + IAndroidTarget ret = null; + + if ((name != null) && (name.length() > 0)) { + IAndroidTarget[] allTargets = getAllTargets(); + + for (int i = 0; i < allTargets.length; i++) { + if (name.equals(allTargets[i].getName())) { + ret = allTargets[i]; + break; + } + } + } + + return ret; + } + + /** + * Gets all VMs + */ + public static AvdInfo[] getAllVms() { + AvdInfo[] allVmInfo = null; + AvdManager vmManager = getVmManager(); + if (vmManager != null) { + allVmInfo = vmManager.getAllAvds(); + } + return allVmInfo; + } + + /** + * Gets all valid VMs + */ + public static AvdInfo[] getAllValidVms() { + AvdInfo[] validVmInfo = null; + AvdManager vmManager = getVmManager(); + if (vmManager != null) { + validVmInfo = vmManager.getValidAvds(); + } + return validVmInfo; + } + + /** + * Gets the name of all VMs that are recognized by the configured SDK. + */ + public static Collection getAllVmNames() { + Collection vmNames = new LinkedList(); + for (AvdInfo vm : getAllVms()) { + vmNames.add(vm.getName()); + } + return vmNames; + } + + /** + * Gets the name of all VMs that are recognized by the configured SDK. + */ + public static Collection getAllValidVmNames() { + Collection vmNames = new LinkedList(); + AvdInfo[] allAvds = getAllValidVms(); + if (allAvds != null) { + for (AvdInfo vm : allAvds) { + vmNames.add(vm.getName()); + } + } + + return vmNames; + } + + /** + * Gets a skin name + * + * @param vmInfo + * the VM to get the skin + */ + public static String getSkin(AvdInfo vmInfo) { + String skin = ""; //$NON-NLS-1$ + File configFile = vmInfo.getConfigFile(); + Properties p = new Properties(); + InputStream configFileStream = null; + try { + configFileStream = new FileInputStream(configFile); + p.load(configFileStream); + skin = p.getProperty(EMU_CONFIG_SKIN_NAME_PROPERTY); + } catch (FileNotFoundException e) { + AndmoreLogger.error(SdkUtils.class, + "Error getting VM skin definition. Could not find file " + configFile.getAbsolutePath(), e); //$NON-NLS-1$ + } catch (IOException e) { + AndmoreLogger.error(SdkUtils.class, + "Error getting VM skin definition. Could not access file " + configFile.getAbsolutePath(), e); //$NON-NLS-1$ + } finally { + if (configFileStream != null) { + try { + configFileStream.close(); + } catch (IOException e) { + // nothing to do + } + } + } + + return skin; + } + + /** + * Gets a VM by name. + * + * @param vmName + * The VM name + */ + public static AvdInfo getValidVm(String vmName) { + AvdInfo vmInfo = null; + AvdManager vmManager = getVmManager(); + if (vmManager != null) { + vmInfo = vmManager.getAvd(vmName, true); + } + return vmInfo; + } + + /** + * Gets a VM by name. + * + * @param vmName + * The VM name + */ + public static AvdInfo getVm(String vmName) { + AvdInfo vmInfo = null; + AvdManager vmManager = getVmManager(); + if (vmManager != null) { + vmInfo = vmManager.getAvd(vmName, false); + } + return vmInfo; + } + + /** + * Creates a new VM instance. + * + * @param folder + * Folder where the VM files will be stored + * @param name + * VM Name + * @param target + * VM Target represented by the IAndroidTarget object + * @param skinName + * VM Skin name from the VM Target + * + * @throws CoreException + */ + public static AvdInfo createVm(String folder, String name, IAndroidTarget target, String abiType, String skinName, + String useSnapshot, String sdCard) throws CoreException + + { + AvdInfo vmInfo; + AvdManager vmManager = SdkUtils.getVmManager(); + + // get the abi type + if (abiType == null) { + abiType = SdkConstants.ABI_ARMEABI; + } + + /* + * public VmInfo createVm(String parentFolder, String name, + * IAndroidTarget target, String skinName, String sdcard, Map + * hardwareConfig) + */ + + // TODO: FIX ME commented out for now. + vmInfo = null; + // vmInfo = vmManager.createAvd(new File(folder), name, target, abiType, + // skinName, sdCard, null, Boolean.parseBoolean(useSnapshot), + // true, false, NullSdkLogger.getLogger()); + + if (vmInfo == null) { + String errMsg = NLS.bind(AndroidNLS.EXC_SdkUtils_CannotCreateTheVMInstance, name); + + IStatus status = new Status(IStatus.ERROR, AndmoreAndroidPlugin.PLUGIN_ID, errMsg); + throw new CoreException(status); + } + + return vmInfo; + } + + /** + * Deletes a VM instance. + * + * @param name + * VM Name + */ + public static void deleteVm(String name) { + + AvdManager vmManager = SdkUtils.getVmManager(); + AvdInfo avdToDelete = vmManager != null ? vmManager.getAvd(name, false) : null; + if (avdToDelete != null) { + try { + if ((avdToDelete.getIniFile() != null) && avdToDelete.getIniFile().exists()) { + avdToDelete.getIniFile().delete(); + } + String path = avdToDelete.getDataFolderPath(); + if (path != null) { + File avdDir = new File(path); + if (avdDir.exists()) { + FileUtil.deleteDirRecursively(avdDir); + } + } + vmManager.removeAvd(avdToDelete); + + } catch (Exception e) { + AndmoreLogger.error("Could not delete AVD: " + e.getMessage()); //$NON-NLS-1$ + } + } + + } + + /** + * Get the reference to the File that points to the filesystem location of + * the directory where the user data files of the VM with the given name are + * stored. + * + * @param vmName + * name of the VM whose userdata directory is to be retrieved. + * @return the File object that references the filesystem location of the + * directory where the userdata files of the given VM will be + * stored. Returns a null reference if SDK is not configured or if + * there is no VM with the given name. + */ + public static File getUserdataDir(String vmName) { + AvdInfo vminfo = SdkUtils.getValidVm(vmName); + File userdataDir = null; + + if (vminfo != null) { + String vmpath = vminfo.getDataFolderPath(); + userdataDir = new File(vmpath); + } + + return userdataDir; + } + + /** + * Get the reference to the File that points to the filesystem location + * where the user data file of the VM with the given name is. + * + * @param vmName + * name of the VM whose userdata file is to be retrieved. + * @return the File object that references the filesystem location where the + * userdata of the given VM should be. Returns a null reference if + * SDK is not configured or if there is no VM with the given name. + */ + public static File getUserdataFile(String vmName) { + File userdataDir = getUserdataDir(vmName); + File userdataFile = null; + + if (userdataDir != null) { + userdataFile = new File(userdataDir, USERIMAGE_FILENAME); + } + + return userdataFile; + } + + /** + * Get the reference to the Files that point to the filesystem location + * where the state data files of the VM with the given name are. + * + * @param vmName + * name of the VM whose state data files is to be retrieved. + * @return the File objects that reference the filesystem location where the + * state data files of the given VM should be. Returns a null + * reference if SDK is not configured or if there is no VM with the + * given name. + */ + public static List getStateDataFiles(String vmName) { + File userdataDir = getUserdataDir(vmName); + List stateDataFiles = null; + + if (userdataDir != null) { + stateDataFiles = new ArrayList(); + + for (int i = 0; i < STATEDATA_FILENAMES.length; i++) { + stateDataFiles.add(new File(userdataDir, STATEDATA_FILENAMES[i])); + } + } + + return stateDataFiles; + } + + /** + * Retrieves all sample applications from a target + * + * @param target + * The target + * @return all sample applications from a target + */ + public static Object[] getSamples(IAndroidTarget target) { + List samples = new ArrayList(); + File samplesFolder = new File(target.getPath(IAndroidTarget.SAMPLES)); + samples = findSamples(samplesFolder, target); + return samples.toArray(); + } + + /** + * Find the samples inside an specific directory (recursively) + * + * @param folder + * the folder that can contain samples + * @param target + * the target of the samples in the folder + * @return a list of samples + */ + private static List findSamples(File folder, IAndroidTarget target) { + + List samples = new ArrayList(); + + if (folder.exists() && folder.isDirectory()) { + for (File sampleFolder : folder.listFiles()) { + if (sampleFolder.isDirectory()) { + if (Sample.isSample(sampleFolder)) { + samples.add(new Sample(sampleFolder, target)); + } else { + samples.addAll(findSamples(sampleFolder, target)); + } + } + } + } + + return samples; + } + + /** + * Retrieves all targets for a given SDK + * + * @param sdk + * The sdk + * + * @return all targets for the given SDK + */ + public static Object[] getTargets(Sdk sdk) { + Object[] targets = null; + if (sdk != null) { + targets = sdk.getTargets().toArray(); + } + return targets; + } + + /** + * Associates a project to a target + * + * @param project + * The project + * @param target + * The target + */ + public static void associate(IProject project, IAndroidTarget target) { + try { + Sdk.getCurrent().initProject(project, target); + } catch (Exception e) { + AndmoreLogger.error(SdkUtils.class, "Error associating project " + project.getName() //$NON-NLS-1$ + + " with target " + target.getName()); //$NON-NLS-1$ + } + } + + /** + * Retrieves the target for a project + * + * @param project + * the project + * + * @return the target for the project + */ + public static IAndroidTarget getTarget(IProject project) { + IAndroidTarget target = null; + if (project != null) { + target = Sdk.getCurrent().getTarget(project); + } + return target; + } + + /** + * Retrieves the target for a project + * + * @param project + * the project + * + * @return the target for the project + */ + public static String getMinSdkVersion(IProject project) { + String minSdkVersion = ""; + try { + AndroidManifestFile androidManifestFile = AndroidProjectManifestFile.getFromProject(project); + UsesSDKNode usesSdkNode = (UsesSDKNode) androidManifestFile.getNode(AndroidManifestNode.NodeType.UsesSdk); + if (usesSdkNode != null) { + minSdkVersion = usesSdkNode.getMinSdkVersion(); + } + + } catch (AndroidException e) { + AndmoreLogger.error("Error getting min sdk version. " + e.getMessage()); + } catch (CoreException e) { + AndmoreLogger.error("Error getting min sdk version. " + e.getMessage()); + } + return minSdkVersion; + } + + /** + * Retrieves all Activity Actions for a project. + * + * @param project + * The project + * + * @return all Activity Actions for the project. + */ + public static String[] getActivityActions(IProject project) { + String[] attributeValues = new String[0]; + + if ((project != null) && project.isOpen()) { + IAndroidTarget target = SdkUtils.getTarget(project); + AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target); + + if (targetData != null) { + attributeValues = targetData.getAttributeValues("action", "android:name", "activity"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + } + + return attributeValues; + } + + /** + * Retrieves all Service Actions for a project. + * + * @param project + * The project + * + * @return all Service Actions for the project. + */ + public static String[] getServiceActions(IProject project) { + String[] attributeValues = new String[0]; + + if ((project != null) && project.isOpen()) { + IAndroidTarget target = SdkUtils.getTarget(project); + AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target); + + if (targetData != null) { + attributeValues = targetData.getAttributeValues("action", "android:name", "service"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + } + + return attributeValues; + } + + /** + * Retrieves all Broadcast Receiver Actions for a project. + * + * @param project + * The project + * + * @return all Broadcast Receiver Actions for the project. + */ + public static String[] getReceiverActions(IProject project) { + String[] attributeValues = new String[0]; + + if ((project != null) && project.isOpen()) { + IAndroidTarget target = SdkUtils.getTarget(project); + AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target); + + if (targetData != null) { + attributeValues = targetData.getAttributeValues("action", "android:name", "receiver"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + } + + return attributeValues; + } + + /** + * Retrieves all Intent Filter Actions for a project. + * + * @param project + * The project + * + * @return all Intent Filter Actions for the project. + */ + public static String[] getIntentFilterCategories(IProject project) { + String[] attributeValues = new String[0]; + + if ((project != null) && project.isOpen()) { + IAndroidTarget target = SdkUtils.getTarget(project); + AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target); + + if (targetData != null) { + attributeValues = targetData.getAttributeValues("category", "android:name"); //$NON-NLS-1$ //$NON-NLS-2$ + } + } + + return attributeValues; + } + + /** + * Get the api version number for a given project + * + * @param project + * : the project + * @return the api version number or 0 if some error occurs + */ + public static int getApiVersionNumberForProject(IProject project) { + int api = 0; + IAndroidTarget target = SdkUtils.getTarget(project); + if (target != null) { + AndroidVersion version = target.getVersion(); + if (version != null) { + api = version.getApiLevel(); + } + } + return api; + } + + public static String getTargetNameForProject(IProject project) { + IAndroidTarget target = getTarget(project); + return target != null ? target.getName() : ""; //$NON-NLS-1$ + } + + public static boolean isPlatformTarget(String avdName) { + IAndroidTarget target = getCurrentSdk().getAndroidTargetFor(getValidVm(avdName)); + return target != null ? target.isPlatform() : false; + } + + public static boolean isProjectTargetAPlatform(IProject project) { + IAndroidTarget target = getTarget(project); + return target != null ? target.isPlatform() : false; + } + + public static boolean isPlatformTarget(IAndroidTarget target) { + return target != null ? target.isPlatform() : false; + } + + /** + * Retrieves the APK configurations of a project + * + * @param project + * the project + * @return the APK configurations + */ + public static Map getAPKConfigurationsForProject(IProject project) { + + Map apkConfigurations = null; + + if ((project != null) && project.isOpen()) { + Sdk.getCurrent(); + // This is not supported on ADT 14 preview so let's comment it for + // now. + // apkConfigurations = + // Sdk.getProjectState(project).getApkSettings().getLocaleFilters(); + apkConfigurations = new HashMap(0); + } + + return apkConfigurations; + + } + + public static String getBaseTarget(String name) { + IAndroidTarget target = getCurrentSdk().getAndroidTargetFor(getValidVm(name)); + while (!target.isPlatform()) { + target = target.getParent(); + } + return target.getName(); + } + + /** + * Check if an SDK is an OPhone Sdk + * + * @return + */ + public static boolean isOphoneSDK() { + boolean result = false; + + // check if the folder contains the oms jar + FilenameFilter omsFilenameFilter = new FilenameFilter() { + @Override + public boolean accept(File arg0, String arg1) { + return arg1.equals(IAndroidConstants.OPHONE_JAR); + } + }; + + for (IAndroidTarget target : getCurrentSdk().getTargets()) { + File folder = new File(target.getLocation()); + if (folder.list(omsFilenameFilter).length > 0) { + result = true; + break; + } + } + + return result; + } + + /** + * Check if an SDK is an JIL sdk + * + * @return + */ + public static boolean isJILSdk() { + boolean result = false; + + // check if the folder contains the oms jar + FilenameFilter jilFilenameFilter = new FilenameFilter() { + + @Override + public boolean accept(File arg0, String arg1) { + return arg1.equals(IAndroidConstants.JIL_JAR); + } + }; + + Sdk sdk = getCurrentSdk(); + if (sdk != null) { + for (IAndroidTarget target : sdk.getTargets()) { + File folder = new File(target.getLocation()); + if (folder.list(jilFilenameFilter).length > 0) { + result = true; + break; + } + } + } + + return result; + } + + public static String getEmulatorWindowName(String avdName, int port) { + String windowName = ""; //$NON-NLS-1$ + if (isJILSdk()) { + windowName = "JIL Emulator (" + avdName + ":" + port + ")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } else if (isOphoneSDK()) { + windowName = "OPhone Emulator (" + avdName + ":" + port + ")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } else { + windowName = port + ":" + avdName; //$NON-NLS-1$ + } + return windowName; + } + + public static boolean isLibraryProject(IProject project) { + return Sdk.getProjectState(project) != null ? Sdk.getProjectState(project).isLibrary() : false; + } + + /** + * Returns all available permissions + * + * @return String array containing the available permissions + */ + public static String[] getIntentFilterPermissions(IProject project) { + String[] attributeValues = new String[0]; + + if ((project != null) && project.isOpen()) { + IAndroidTarget target = SdkUtils.getTarget(project); + AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target); + + if (targetData != null) { + attributeValues = targetData.getAttributeValues("uses-permission", "android:name"); //$NON-NLS-1$ //$NON-NLS-2$ + } + } + + return attributeValues; + } + + /** + * Try to repair an AVD. Currently only avds with wrong image path are + * repariable. Display a message with the changes to the config.ini + * + * @param avdInfo + * @return Status ERROR if an IO exception occured. + */ + public static IStatus repairAvd(AvdInfo avdInfo) { + IStatus status = Status.OK_STATUS; + + AvdManager avdManager = Sdk.getCurrent().getAvdManager(); + Display display = PlatformUI.getWorkbench().getDisplay(); + ILogger log = new MessageBoxLog(String.format("Result of updating AVD '%s':", avdInfo.getName()), //$NON-NLS-1$ + display, false); + try { + avdManager.updateAvd(avdInfo, avdInfo.getProperties()); + // display the result + if (log instanceof MessageBoxLog) { + ((MessageBoxLog) log).displayResult(true); + } + SdkUtils.reloadAvds(); + + } catch (IOException e) { + status = new Status(IStatus.ERROR, AndroidPlugin.PLUGIN_ID, AndroidNLS.SdkUtils_COULD_NOT_REPAIR_AVD, e); + } + + return status; + } + + public static String getDefaultSkin(String targetName) { + IAndroidTarget target = getTargetByName(targetName); + target.getDefaultSkin().getName(); + return target != null ? target.getDefaultSkin().getName() : "HVGA"; + } + + /** + * Returns the full absolute OS path to a skin specified by name for a given + * target. + * + * @param skinName + * The name of the skin to find. Case-sensitive. + * @param target + * The target where to find the skin. + * @return a {@link File} that may or may not actually exist. + */ + public static File getSkinFolder(String skinName, IAndroidTarget target) { + String path = target.getPath(IAndroidTarget.SKINS); + File skin = new File(path, skinName); + + if (skin.exists() == false && target.isPlatform() == false) { + target = target.getParent(); + + path = target.getPath(IAndroidTarget.SKINS); + skin = new File(path, skinName); + } + + return skin; + } +} diff --git a/andmore-core/plugins/android/src/org/eclipse/andmore/android/multidex/MultiDexManager.java b/andmore-core/plugins/android/src/org/eclipse/andmore/android/multidex/MultiDexManager.java index 3d730c24..0266e1ac 100644 --- a/andmore-core/plugins/android/src/org/eclipse/andmore/android/multidex/MultiDexManager.java +++ b/andmore-core/plugins/android/src/org/eclipse/andmore/android/multidex/MultiDexManager.java @@ -317,12 +317,12 @@ private static ProjectProperties getProjectProperties(IProject project) { String projectLocation = project.getLocation().toOSString(); // legacy support: look for default.properties - ProjectProperties properties = ProjectProperties.load(projectLocation, - PropertyType.LEGACY_DEFAULT); - if(properties == null) { - properties = ProjectProperties.load(projectLocation, PropertyType.PROJECT); - } + //ProjectProperties properties = ProjectProperties.load(projectLocation, + // PropertyType.LEGACY_DEFAULT); + //if(properties == null) { + // properties = ProjectProperties.load(projectLocation, PropertyType.PROJECT); + //} - return properties; + return ProjectProperties.load(projectLocation, PropertyType.PROJECT); } } diff --git a/andmore-core/plugins/android/src/org/eclipse/andmore/android/wizards/elements/SdkTargetSelector.java b/andmore-core/plugins/android/src/org/eclipse/andmore/android/wizards/elements/SdkTargetSelector.java index 9ebf898d..ec2a397f 100644 --- a/andmore-core/plugins/android/src/org/eclipse/andmore/android/wizards/elements/SdkTargetSelector.java +++ b/andmore-core/plugins/android/src/org/eclipse/andmore/android/wizards/elements/SdkTargetSelector.java @@ -1,316 +1,316 @@ -/* - * Copyright (C) 2012 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 org.eclipse.andmore.android.wizards.elements; - -import org.eclipse.andmore.android.AndroidPlugin; -import org.eclipse.andmore.android.SdkUtils; -import org.eclipse.andmore.android.i18n.AndroidNLS; -import org.eclipse.andmore.android.model.AndroidProject; -import org.eclipse.andmore.android.model.IWizardModel; -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.events.SelectionEvent; -import org.eclipse.swt.events.SelectionListener; -import org.eclipse.swt.graphics.Point; -import org.eclipse.swt.graphics.Rectangle; -import org.eclipse.swt.layout.GridData; -import org.eclipse.swt.layout.GridLayout; -import org.eclipse.swt.widgets.Composite; -import org.eclipse.swt.widgets.Event; -import org.eclipse.swt.widgets.Label; -import org.eclipse.swt.widgets.Listener; -import org.eclipse.swt.widgets.Table; -import org.eclipse.swt.widgets.TableColumn; -import org.eclipse.swt.widgets.TableItem; - -import com.android.sdklib.IAndroidTarget; - -/** - * SDK Selector for New Android Project Wizards - */ -public class SdkTargetSelector extends Composite { - private Table table; - - private Label mDescription; - - final private AndroidProject project; - - private IAndroidTarget selection = null; - - /** - * A selection listener that will check/uncheck items when they are - * double-clicked - */ - private final SelectionListener listener = new SelectionListener() { - /** Default selection means double-click on "most" platforms */ - @Override - public void widgetDefaultSelected(SelectionEvent e) { - if (e.item instanceof TableItem) { - TableItem i = (TableItem) e.item; - i.setChecked(!i.getChecked()); - enforceSingleSelection(i); - updateDescription(i); - IAndroidTarget newSelection = getSelection(); - project.setSdkTarget(newSelection); - - if (newSelection != null) { - project.setMinSdkVersion(getSelection().getVersion().getApiString()); - } - notifyListeners(IWizardModel.MODIFIED, new Event()); - } - - } - - @Override - public void widgetSelected(SelectionEvent e) { - if (e.item instanceof TableItem) { - TableItem i = (TableItem) e.item; - enforceSingleSelection(i); - updateDescription(i); - IAndroidTarget newSelection = getSelection(); - project.setSdkTarget(newSelection); - selection = newSelection; - project.setSample(null); - - /* - * if ((newSelection != null) && - * !selection.getFullName().equals(newSelection.getFullName())) - * { - * - * } - */ - - notifyListeners(IWizardModel.MODIFIED, new Event()); - } - - } - - /** - * If we're not in multiple selection mode, uncheck all other items when - * this one is selected. - */ - private void enforceSingleSelection(TableItem item) { - if (item.getChecked()) { - Table parentTable = item.getParent(); - for (TableItem i2 : parentTable.getItems()) { - if ((i2 != item) && i2.getChecked()) { - i2.setChecked(false); - } - } - } - } - }; - - /** - * Table Tool Tip Listener - */ - private final Listener toolTipListener = new Listener() { - @Override - public void handleEvent(Event event) { - switch (event.type) { - case SWT.MouseHover: - updateDescription(table.getItem(new Point(event.x, event.y))); - break; - case SWT.Selection: - if (event.item instanceof TableItem) { - updateDescription((TableItem) event.item); - } - break; - default: - return; - } - } - }; - - /** - * Creates a new SDK Target Selector. - * - * @param parent - * The parent composite where the selector will be added. - * @param project - * the android project - */ - public SdkTargetSelector(Composite parent, AndroidProject project) { - super(parent, SWT.NONE); - this.project = project; - - createContents(parent); - } - - /** - * Create Contents - * - * @param parent - */ - private void createContents(Composite parent) { - setLayout(new GridLayout()); - setLayoutData(new GridData(GridData.FILL_BOTH)); - setFont(parent.getFont()); - - table = new Table(this, SWT.CHECK | SWT.FULL_SELECTION | SWT.SINGLE | SWT.BORDER); - table.setHeaderVisible(true); - table.setLinesVisible(false); - table.setLayoutData(new GridData(GridData.FILL_BOTH)); - - mDescription = new Label(this, SWT.WRAP); - mDescription.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - - createColumns(table); - table.addSelectionListener(listener); - fillTable(); - setupTooltip(table); - } - - /** - * Create Table Columns - * - * @param table - */ - private void createColumns(final Table table) { - // create the table columns - final TableColumn nameColumn = new TableColumn(table, SWT.NONE); - nameColumn.setText(AndroidNLS.UI_SdkTargetSelector_SdkTargetNameColumn); - final TableColumn vendorColumn = new TableColumn(table, SWT.NONE); - vendorColumn.setText(AndroidNLS.UI_SdkTargetSelector_VendorNameColumn); - final TableColumn apiColumn = new TableColumn(table, SWT.NONE); - apiColumn.setText(AndroidNLS.UI_SdkTargetSelector_APILevelColumn); - final TableColumn sdkColumn = new TableColumn(table, SWT.NONE); - sdkColumn.setText(AndroidNLS.UI_SdkTargetSelector_SDKVersionColumn); - - table.addControlListener(new ControlAdapter() { - @Override - public void controlResized(ControlEvent e) { - Rectangle r = table.getClientArea(); - nameColumn.setWidth((r.width * 25) / 100); // 25% - vendorColumn.setWidth((r.width * 50) / 100); // 50% - apiColumn.setWidth((r.width * 15) / 100); // 15% - sdkColumn.setWidth((r.width * 10) / 100); // 10% - } - }); - } - - /** - * Return table selection. - * - * @return - */ - protected IAndroidTarget getSelection() { - IAndroidTarget selectedItem = null; - for (TableItem item : table.getItems()) { - Object data = item.getData(); - if (item.getChecked() && (data instanceof IAndroidTarget)) { - selectedItem = (IAndroidTarget) data; - break; - } - } - return selectedItem; - } - - /** - * Fills the table with all SDK targets. - */ - private void fillTable() { - // get the targets from the sdk - IAndroidTarget[] targets = null; - if (SdkUtils.getCurrentSdk() != null) { - targets = SdkUtils.getAllTargets(); - } else { - final Runnable listener = new Runnable() { - @Override - public void run() { - table.getDisplay().asyncExec(new Runnable() { - @Override - public void run() { - table.removeAll(); - fillTable(); - AndroidPlugin.getDefault().removeSDKLoaderListener(this); - } - }); - } - }; - AndroidPlugin.getDefault().addSDKLoaderListener(listener); - table.addDisposeListener(new DisposeListener() { - @Override - public void widgetDisposed(DisposeEvent e) { - AndroidPlugin.getDefault().removeSDKLoaderListener(listener); - } - }); - } - - if ((targets != null) && (targets.length > 0)) { - table.setEnabled(true); - for (IAndroidTarget target : targets) { - TableItem item = new TableItem(table, SWT.NONE); - item.setData(target); - item.setText(0, target.getName()); - item.setText(1, target.getVendor()); - item.setText(2, target.getVersion().getApiString()); - item.setText(3, target.getVersionName()); - if (target == project.getSdkTarget()) { - item.setChecked(true); - selection = target; - } - } - } else { - table.setEnabled(false); - TableItem item = new TableItem(table, SWT.NONE); - item.setData(null); - item.setText(0, AndroidNLS.UI_SdkTargetSelector_EmptyValue); - item.setText(1, AndroidNLS.UI_SdkTargetSelector_NoTargetAvailable); - item.setText(2, AndroidNLS.UI_SdkTargetSelector_EmptyValue); - item.setText(3, AndroidNLS.UI_SdkTargetSelector_EmptyValue); - } - } - - /** - * Add Tool tip for table - * - * @param table - */ - private void setupTooltip(final Table table) { - table.addListener(SWT.Dispose, toolTipListener); - table.addListener(SWT.MouseHover, toolTipListener); - table.addListener(SWT.MouseMove, toolTipListener); - table.addListener(SWT.KeyDown, toolTipListener); - } - - /** - * Updates the description label - */ - private void updateDescription(TableItem item) { - if (item != null) { - Object data = item.getData(); - if (data instanceof IAndroidTarget) { - String newTooltip = ((IAndroidTarget) data).getDescription(); - mDescription.setText(newTooltip == null ? "" : newTooltip); //$NON-NLS-1$ - } - } else { - mDescription.setText(""); - } - } - - @Override - public void setEnabled(boolean enabled) { - if (this.getEnabled() != enabled) { - table.setEnabled(enabled); - mDescription.setEnabled(enabled); - super.setEnabled(enabled); - } - } -} +/* + * Copyright (C) 2012 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 org.eclipse.andmore.android.wizards.elements; + +import org.eclipse.andmore.android.AndroidPlugin; +import org.eclipse.andmore.android.SdkUtils; +import org.eclipse.andmore.android.i18n.AndroidNLS; +import org.eclipse.andmore.android.model.AndroidProject; +import org.eclipse.andmore.android.model.IWizardModel; +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.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.TableItem; + +import com.android.sdklib.IAndroidTarget; + +/** + * SDK Selector for New Android Project Wizards + */ +public class SdkTargetSelector extends Composite { + private Table table; + + private Label mDescription; + + final private AndroidProject project; + + private IAndroidTarget selection = null; + + /** + * A selection listener that will check/uncheck items when they are + * double-clicked + */ + private final SelectionListener listener = new SelectionListener() { + /** Default selection means double-click on "most" platforms */ + @Override + public void widgetDefaultSelected(SelectionEvent e) { + if (e.item instanceof TableItem) { + TableItem i = (TableItem) e.item; + i.setChecked(!i.getChecked()); + enforceSingleSelection(i); + updateDescription(i); + IAndroidTarget newSelection = getSelection(); + project.setSdkTarget(newSelection); + + if (newSelection != null) { + project.setMinSdkVersion(getSelection().getVersion().getApiString()); + } + notifyListeners(IWizardModel.MODIFIED, new Event()); + } + + } + + @Override + public void widgetSelected(SelectionEvent e) { + if (e.item instanceof TableItem) { + TableItem i = (TableItem) e.item; + enforceSingleSelection(i); + updateDescription(i); + IAndroidTarget newSelection = getSelection(); + project.setSdkTarget(newSelection); + selection = newSelection; + project.setSample(null); + + /* + * if ((newSelection != null) && + * !selection.getFullName().equals(newSelection.getFullName())) + * { + * + * } + */ + + notifyListeners(IWizardModel.MODIFIED, new Event()); + } + + } + + /** + * If we're not in multiple selection mode, uncheck all other items when + * this one is selected. + */ + private void enforceSingleSelection(TableItem item) { + if (item.getChecked()) { + Table parentTable = item.getParent(); + for (TableItem i2 : parentTable.getItems()) { + if ((i2 != item) && i2.getChecked()) { + i2.setChecked(false); + } + } + } + } + }; + + /** + * Table Tool Tip Listener + */ + private final Listener toolTipListener = new Listener() { + @Override + public void handleEvent(Event event) { + switch (event.type) { + case SWT.MouseHover: + updateDescription(table.getItem(new Point(event.x, event.y))); + break; + case SWT.Selection: + if (event.item instanceof TableItem) { + updateDescription((TableItem) event.item); + } + break; + default: + return; + } + } + }; + + /** + * Creates a new SDK Target Selector. + * + * @param parent + * The parent composite where the selector will be added. + * @param project + * the android project + */ + public SdkTargetSelector(Composite parent, AndroidProject project) { + super(parent, SWT.NONE); + this.project = project; + + createContents(parent); + } + + /** + * Create Contents + * + * @param parent + */ + private void createContents(Composite parent) { + setLayout(new GridLayout()); + setLayoutData(new GridData(GridData.FILL_BOTH)); + setFont(parent.getFont()); + + table = new Table(this, SWT.CHECK | SWT.FULL_SELECTION | SWT.SINGLE | SWT.BORDER); + table.setHeaderVisible(true); + table.setLinesVisible(false); + table.setLayoutData(new GridData(GridData.FILL_BOTH)); + + mDescription = new Label(this, SWT.WRAP); + mDescription.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + createColumns(table); + table.addSelectionListener(listener); + fillTable(); + setupTooltip(table); + } + + /** + * Create Table Columns + * + * @param table + */ + private void createColumns(final Table table) { + // create the table columns + final TableColumn nameColumn = new TableColumn(table, SWT.NONE); + nameColumn.setText(AndroidNLS.UI_SdkTargetSelector_SdkTargetNameColumn); + final TableColumn vendorColumn = new TableColumn(table, SWT.NONE); + vendorColumn.setText(AndroidNLS.UI_SdkTargetSelector_VendorNameColumn); + final TableColumn apiColumn = new TableColumn(table, SWT.NONE); + apiColumn.setText(AndroidNLS.UI_SdkTargetSelector_APILevelColumn); + final TableColumn sdkColumn = new TableColumn(table, SWT.NONE); + sdkColumn.setText(AndroidNLS.UI_SdkTargetSelector_SDKVersionColumn); + + table.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + Rectangle r = table.getClientArea(); + nameColumn.setWidth((r.width * 25) / 100); // 25% + vendorColumn.setWidth((r.width * 50) / 100); // 50% + apiColumn.setWidth((r.width * 15) / 100); // 15% + sdkColumn.setWidth((r.width * 10) / 100); // 10% + } + }); + } + + /** + * Return table selection. + * + * @return + */ + protected IAndroidTarget getSelection() { + IAndroidTarget selectedItem = null; + for (TableItem item : table.getItems()) { + Object data = item.getData(); + if (item.getChecked() && (data instanceof IAndroidTarget)) { + selectedItem = (IAndroidTarget) data; + break; + } + } + return selectedItem; + } + + /** + * Fills the table with all SDK targets. + */ + private void fillTable() { + // get the targets from the sdk + IAndroidTarget[] targets = null; + if (SdkUtils.getCurrentSdk() != null) { + targets = SdkUtils.getAllTargets(); + } else { + final Runnable listener = new Runnable() { + @Override + public void run() { + table.getDisplay().asyncExec(new Runnable() { + @Override + public void run() { + table.removeAll(); + fillTable(); + AndroidPlugin.getDefault().removeSDKLoaderListener(this); + } + }); + } + }; + AndroidPlugin.getDefault().addSDKLoaderListener(listener); + table.addDisposeListener(new DisposeListener() { + @Override + public void widgetDisposed(DisposeEvent e) { + AndroidPlugin.getDefault().removeSDKLoaderListener(listener); + } + }); + } + + if ((targets != null) && (targets.length > 0)) { + table.setEnabled(true); + for (IAndroidTarget target : targets) { + TableItem item = new TableItem(table, SWT.NONE); + item.setData(target); + item.setText(0, target.getName()); + item.setText(1, target.getVendor()); + item.setText(2, target.getVersion().getApiString()); + item.setText(3, target.getVersionName() == null ? target.getVersion().getApiString() : target.getVersionName()); + if (target == project.getSdkTarget()) { + item.setChecked(true); + selection = target; + } + } + } else { + table.setEnabled(false); + TableItem item = new TableItem(table, SWT.NONE); + item.setData(null); + item.setText(0, AndroidNLS.UI_SdkTargetSelector_EmptyValue); + item.setText(1, AndroidNLS.UI_SdkTargetSelector_NoTargetAvailable); + item.setText(2, AndroidNLS.UI_SdkTargetSelector_EmptyValue); + item.setText(3, AndroidNLS.UI_SdkTargetSelector_EmptyValue); + } + } + + /** + * Add Tool tip for table + * + * @param table + */ + private void setupTooltip(final Table table) { + table.addListener(SWT.Dispose, toolTipListener); + table.addListener(SWT.MouseHover, toolTipListener); + table.addListener(SWT.MouseMove, toolTipListener); + table.addListener(SWT.KeyDown, toolTipListener); + } + + /** + * Updates the description label + */ + private void updateDescription(TableItem item) { + if (item != null) { + Object data = item.getData(); + if (data instanceof IAndroidTarget) { + String newTooltip = ((IAndroidTarget) data).getDescription(); + mDescription.setText(newTooltip == null ? "" : newTooltip); //$NON-NLS-1$ + } + } else { + mDescription.setText(""); + } + } + + @Override + public void setEnabled(boolean enabled) { + if (this.getEnabled() != enabled) { + table.setEnabled(enabled); + mDescription.setEnabled(enabled); + super.setEnabled(enabled); + } + } +} diff --git a/andmore-core/plugins/android/src/org/eclipse/andmore/android/wizards/project/TreeContentProvider.java b/andmore-core/plugins/android/src/org/eclipse/andmore/android/wizards/project/TreeContentProvider.java index c7c57b9c..217cea1a 100644 --- a/andmore-core/plugins/android/src/org/eclipse/andmore/android/wizards/project/TreeContentProvider.java +++ b/andmore-core/plugins/android/src/org/eclipse/andmore/android/wizards/project/TreeContentProvider.java @@ -1,129 +1,130 @@ -/* - * Copyright (C) 2012 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 org.eclipse.andmore.android.wizards.project; - -import org.eclipse.andmore.android.SdkUtils; -import org.eclipse.andmore.android.model.AndroidProject; -import org.eclipse.andmore.internal.sdk.Sdk; -import org.eclipse.jface.viewers.ITreeContentProvider; -import org.eclipse.jface.viewers.Viewer; - -import com.android.sdklib.IAndroidTarget; - -/** - * Class that implements a content provider for the Samples Tree viewers. - */ -@SuppressWarnings("restriction") -class TreeContentProvider implements ITreeContentProvider { - AndroidProject project = null; - - public TreeContentProvider(AndroidProject project) { - this.project = project; - } - - /* - * (non-Javadoc) - * - * @see - * org.eclipse.jface.viewers.ITreeContentProvider#getChildren(java.lang. - * Object) - */ - @Override - public Object[] getChildren(Object arg0) { - Object[] objects; - - if (arg0 instanceof IAndroidTarget) { - objects = SdkUtils.getSamples((IAndroidTarget) arg0); - } else { - objects = new Object[0]; - } - return objects; - } - - /* - * (non-Javadoc) - * - * @see - * org.eclipse.jface.viewers.ITreeContentProvider#getParent(java.lang.Object - * ) - */ - @Override - public Object getParent(Object arg0) { - return null; - } - - /* - * (non-Javadoc) - * - * @see - * org.eclipse.jface.viewers.ITreeContentProvider#hasChildren(java.lang. - * Object) - */ - @Override - public boolean hasChildren(Object arg0) { - Object[] obj = getChildren(arg0); - return obj == null ? false : obj.length > 0; - } - - /* - * (non-Javadoc) - * - * @see - * org.eclipse.jface.viewers.IStructuredContentProvider#getElements(java - * .lang.Object) - */ - @Override - public Object[] getElements(Object arg0) { - Object[] objs = null; - - if (arg0 instanceof Sdk) { - Sdk sdk = (Sdk) arg0; - Object[] targets = SdkUtils.getTargets(sdk); - if (targets.length > 0) { - for (IAndroidTarget target : (IAndroidTarget[]) targets) { - if (target.equals(project.getSdkTarget())) { - objs = SdkUtils.getSamples(target); - } - } - } else { - objs = new Object[0]; - } - } - return objs; - } - - /* - * (non-Javadoc) - * - * @see org.eclipse.jface.viewers.IContentProvider#dispose() - */ - @Override - public void dispose() { - // do nothing - } - - /* - * (non-Javadoc) - * - * @see - * org.eclipse.jface.viewers.IContentProvider#inputChanged(org.eclipse.jface - * .viewers.Viewer, java.lang.Object, java.lang.Object) - */ - @Override - public void inputChanged(Viewer arg0, Object arg1, Object arg2) { - // do nothing - } -} +/* + * Copyright (C) 2012 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 org.eclipse.andmore.android.wizards.project; + +import org.eclipse.andmore.android.SdkUtils; +import org.eclipse.andmore.android.model.AndroidProject; +import org.eclipse.andmore.internal.sdk.Sdk; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.Viewer; + +import com.android.sdklib.IAndroidTarget; + +/** + * Class that implements a content provider for the Samples Tree viewers. + */ +@SuppressWarnings("restriction") +class TreeContentProvider implements ITreeContentProvider { + AndroidProject project = null; + + public TreeContentProvider(AndroidProject project) { + this.project = project; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.jface.viewers.ITreeContentProvider#getChildren(java.lang. + * Object) + */ + @Override + public Object[] getChildren(Object arg0) { + Object[] objects; + + if (arg0 instanceof IAndroidTarget) { + objects = SdkUtils.getSamples((IAndroidTarget) arg0); + } else { + objects = new Object[0]; + } + return objects; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.jface.viewers.ITreeContentProvider#getParent(java.lang.Object + * ) + */ + @Override + public Object getParent(Object arg0) { + return null; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.jface.viewers.ITreeContentProvider#hasChildren(java.lang. + * Object) + */ + @Override + public boolean hasChildren(Object arg0) { + Object[] obj = getChildren(arg0); + return obj == null ? false : obj.length > 0; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.jface.viewers.IStructuredContentProvider#getElements(java + * .lang.Object) + */ + @Override + public Object[] getElements(Object arg0) { + Object[] objs = null; + + if (arg0 instanceof Sdk) { + Sdk sdk = (Sdk) arg0; + Object[] targets = SdkUtils.getTargets(sdk); + if (targets.length > 0) { + for (Object o : targets) { + IAndroidTarget target = (IAndroidTarget) o; + if (target.equals(project.getSdkTarget())) { + objs = SdkUtils.getSamples(target); + } + } + } else { + objs = new Object[0]; + } + } + return objs; + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.jface.viewers.IContentProvider#dispose() + */ + @Override + public void dispose() { + // do nothing + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.jface.viewers.IContentProvider#inputChanged(org.eclipse.jface + * .viewers.Viewer, java.lang.Object, java.lang.Object) + */ + @Override + public void inputChanged(Viewer arg0, Object arg1, Object arg2) { + // do nothing + } +} diff --git a/andmore-core/plugins/certmanager/.classpath b/andmore-core/plugins/certmanager/.classpath index 21319ace..a6c5d251 100644 --- a/andmore-core/plugins/certmanager/.classpath +++ b/andmore-core/plugins/certmanager/.classpath @@ -1,9 +1,9 @@ - - - - - - - - - + + + + + + + + + diff --git a/andmore-core/plugins/certmanager/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/certmanager/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/certmanager/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/certmanager/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/certmanager/META-INF/MANIFEST.MF b/andmore-core/plugins/certmanager/META-INF/MANIFEST.MF index c6ff8dd6..e3e12107 100644 --- a/andmore-core/plugins/certmanager/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/certmanager/META-INF/MANIFEST.MF @@ -1,32 +1,32 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.android.certmanager;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.android.certmanager.CertificateManagerActivator -Bundle-Vendor: %providerName -Require-Bundle: org.eclipse.ui, - org.eclipse.core.runtime, - org.eclipse.andmore.android.common, - org.junit, - org.eclipse.equinox.security, - org.eclipse.core.resources, - org.eclipse.ui.ide, - org.eclipse.core.expressions -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Bundle-ActivationPolicy: lazy -Bundle-Localization: plugin -Bundle-ClassPath: ., - lib/bcprov-jdk15on-147.jar, - lib/bcpkix-jdk15on-147.jar -Import-Package: org.eclipse.ui.actions -Export-Package: org.eclipse.andmore.android.certmanager, - org.eclipse.andmore.android.certmanager.command, - org.eclipse.andmore.android.certmanager.core, - org.eclipse.andmore.android.certmanager.exception, - org.eclipse.andmore.android.certmanager.job, - org.eclipse.andmore.android.certmanager.packaging, - org.eclipse.andmore.android.certmanager.packaging.sign, - org.eclipse.andmore.android.certmanager.ui.model, - org.eclipse.andmore.android.certmanager.ui.wizards, - org.eclipse.andmore.android.certmanager.views +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.android.certmanager;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.android.certmanager.CertificateManagerActivator +Bundle-Vendor: %providerName +Require-Bundle: org.eclipse.ui, + org.eclipse.core.runtime, + org.eclipse.andmore.android.common, + org.junit, + org.eclipse.equinox.security, + org.eclipse.core.resources, + org.eclipse.ui.ide, + org.eclipse.core.expressions +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-ActivationPolicy: lazy +Bundle-Localization: plugin +Bundle-ClassPath: ., + lib/bcprov-jdk15on-147.jar, + lib/bcpkix-jdk15on-147.jar +Import-Package: org.eclipse.ui.actions +Export-Package: org.eclipse.andmore.android.certmanager, + org.eclipse.andmore.android.certmanager.command, + org.eclipse.andmore.android.certmanager.core, + org.eclipse.andmore.android.certmanager.exception, + org.eclipse.andmore.android.certmanager.job, + org.eclipse.andmore.android.certmanager.packaging, + org.eclipse.andmore.android.certmanager.packaging.sign, + org.eclipse.andmore.android.certmanager.ui.model, + org.eclipse.andmore.android.certmanager.ui.wizards, + org.eclipse.andmore.android.certmanager.views diff --git a/andmore-core/plugins/common/.classpath b/andmore-core/plugins/common/.classpath index c72d35a0..54f561c7 100644 --- a/andmore-core/plugins/common/.classpath +++ b/andmore-core/plugins/common/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/andmore-core/plugins/common/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/common/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/common/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/common/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/common/META-INF/MANIFEST.MF b/andmore-core/plugins/common/META-INF/MANIFEST.MF index b921b5a7..e0fed49d 100644 --- a/andmore-core/plugins/common/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/common/META-INF/MANIFEST.MF @@ -1,38 +1,38 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.android.common;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.android.common.CommonPlugin -Bundle-Vendor: %providerName -Bundle-Localization: plugin -Require-Bundle: org.eclipse.ui, - org.eclipse.core.runtime, - org.eclipse.core.resources, - org.eclipse.ui.editors, - org.eclipse.ui.browser, - org.eclipse.text, - org.eclipse.andmore.android.logger, - org.eclipse.core.net, - org.eclipse.ui.net, - org.apache.xerces, - org.eclipse.jdt.core, - org.eclipse.ui.console, - org.eclipse.ui.ide, - org.apache.commons.httpclient -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Export-Package: org.eclipse.andmore.android.common, - org.eclipse.andmore.android.common.exception, - org.eclipse.andmore.android.common.log, - org.eclipse.andmore.android.common.preferences, - org.eclipse.andmore.android.common.proxy, - org.eclipse.andmore.android.common.utilities, - org.eclipse.andmore.android.common.utilities.i18n, - org.eclipse.andmore.android.common.utilities.ui, - org.eclipse.andmore.android.manifest, - org.eclipse.andmore.android.model.manifest, - org.eclipse.andmore.android.model.manifest.dom, - org.eclipse.andmore.android.wizards, - org.eclipse.andmore.android.wizards.elements -Bundle-ClassPath: . -Bundle-ActivationPolicy: lazy +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.android.common;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.android.common.CommonPlugin +Bundle-Vendor: %providerName +Bundle-Localization: plugin +Require-Bundle: org.eclipse.ui, + org.eclipse.core.runtime, + org.eclipse.core.resources, + org.eclipse.ui.editors, + org.eclipse.ui.browser, + org.eclipse.text, + org.eclipse.andmore.android.logger, + org.eclipse.core.net, + org.eclipse.ui.net, + org.apache.xerces, + org.eclipse.jdt.core, + org.eclipse.ui.console, + org.eclipse.ui.ide, + org.apache.commons.httpclient +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Export-Package: org.eclipse.andmore.android.common, + org.eclipse.andmore.android.common.exception, + org.eclipse.andmore.android.common.log, + org.eclipse.andmore.android.common.preferences, + org.eclipse.andmore.android.common.proxy, + org.eclipse.andmore.android.common.utilities, + org.eclipse.andmore.android.common.utilities.i18n, + org.eclipse.andmore.android.common.utilities.ui, + org.eclipse.andmore.android.manifest, + org.eclipse.andmore.android.model.manifest, + org.eclipse.andmore.android.model.manifest.dom, + org.eclipse.andmore.android.wizards, + org.eclipse.andmore.android.wizards.elements +Bundle-ClassPath: . +Bundle-ActivationPolicy: lazy diff --git a/andmore-core/plugins/db.core/.classpath b/andmore-core/plugins/db.core/.classpath index 0b1bcf94..7498423d 100644 --- a/andmore-core/plugins/db.core/.classpath +++ b/andmore-core/plugins/db.core/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/andmore-core/plugins/db.core/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/db.core/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/db.core/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/db.core/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/db.core/META-INF/MANIFEST.MF b/andmore-core/plugins/db.core/META-INF/MANIFEST.MF index 41bcb242..01441043 100644 --- a/andmore-core/plugins/db.core/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/db.core/META-INF/MANIFEST.MF @@ -1,32 +1,32 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.android.db.core;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.android.db.core.DbCoreActivator -Bundle-Vendor: %providerName -Require-Bundle: org.eclipse.ui, - org.eclipse.core.runtime, - org.eclipse.datatools.connectivity, - org.eclipse.andmore.android.common, - org.eclipse.core.resources, - org.eclipse.datatools.connectivity.sqm.core, - org.eclipse.datatools.modelbase.sql, - org.junit, - org.eclipse.datatools.sqltools.data.ui, - org.eclipse.andmore.android.codeutils, - org.eclipse.datatools.sqltools.editor.core, - org.eclipse.datatools.sqltools.result, - org.eclipse.core.expressions, - org.eclipse.datatools.sqltools.ddlgen.ui -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Bundle-ActivationPolicy: lazy -Bundle-Localization: plugin -Export-Package: org.eclipse.andmore.android.db.core, - org.eclipse.andmore.android.db.core.command, - org.eclipse.andmore.android.db.core.event, - org.eclipse.andmore.android.db.core.exception, - org.eclipse.andmore.android.db.core.model, - org.eclipse.andmore.android.db.core.ui, - org.eclipse.andmore.android.db.core.ui.action, - org.eclipse.andmore.android.db.core.ui.view +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.android.db.core;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.android.db.core.DbCoreActivator +Bundle-Vendor: %providerName +Require-Bundle: org.eclipse.ui, + org.eclipse.core.runtime, + org.eclipse.datatools.connectivity, + org.eclipse.andmore.android.common, + org.eclipse.core.resources, + org.eclipse.datatools.connectivity.sqm.core, + org.eclipse.datatools.modelbase.sql, + org.junit, + org.eclipse.datatools.sqltools.data.ui, + org.eclipse.andmore.android.codeutils, + org.eclipse.datatools.sqltools.editor.core, + org.eclipse.datatools.sqltools.result, + org.eclipse.core.expressions, + org.eclipse.datatools.sqltools.ddlgen.ui +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-ActivationPolicy: lazy +Bundle-Localization: plugin +Export-Package: org.eclipse.andmore.android.db.core, + org.eclipse.andmore.android.db.core.command, + org.eclipse.andmore.android.db.core.event, + org.eclipse.andmore.android.db.core.exception, + org.eclipse.andmore.android.db.core.model, + org.eclipse.andmore.android.db.core.ui, + org.eclipse.andmore.android.db.core.ui.action, + org.eclipse.andmore.android.db.core.ui.view diff --git a/andmore-core/plugins/db.devices/.classpath b/andmore-core/plugins/db.devices/.classpath index c72d35a0..54f561c7 100644 --- a/andmore-core/plugins/db.devices/.classpath +++ b/andmore-core/plugins/db.devices/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/andmore-core/plugins/db.devices/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/db.devices/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/db.devices/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/db.devices/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/db.devices/META-INF/MANIFEST.MF b/andmore-core/plugins/db.devices/META-INF/MANIFEST.MF index fcd92cc9..675bb1ad 100644 --- a/andmore-core/plugins/db.devices/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/db.devices/META-INF/MANIFEST.MF @@ -1,22 +1,22 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.android.db.devices;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.android.db.devices.DbDevicesPlugin -Bundle-Vendor: %providerName -Require-Bundle: org.eclipse.ui, - org.eclipse.core.runtime, - org.eclipse.andmore.android.db.core, - org.eclipse.andmore.android, - org.eclipse.jdt.core, - org.eclipse.datatools.modelbase.sql, - org.eclipse.andmore.android.common, - org.eclipse.ui.console, - org.eclipse.datatools.sqltools.result, - org.eclipse.sequoyah.device.framework, - org.eclipse.core.resources, - org.eclipse.andmore;bundle-version="0.5.0" -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Bundle-Localization: plugin -Bundle-ActivationPolicy: lazy +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.android.db.devices;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.android.db.devices.DbDevicesPlugin +Bundle-Vendor: %providerName +Require-Bundle: org.eclipse.ui, + org.eclipse.core.runtime, + org.eclipse.andmore.android.db.core, + org.eclipse.andmore.android, + org.eclipse.jdt.core, + org.eclipse.datatools.modelbase.sql, + org.eclipse.andmore.android.common, + org.eclipse.ui.console, + org.eclipse.datatools.sqltools.result, + org.eclipse.sequoyah.device.framework, + org.eclipse.core.resources, + org.eclipse.andmore;bundle-version="0.5.0" +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-Localization: plugin +Bundle-ActivationPolicy: lazy diff --git a/andmore-core/plugins/devices.services/.classpath b/andmore-core/plugins/devices.services/.classpath index c72d35a0..54f561c7 100644 --- a/andmore-core/plugins/devices.services/.classpath +++ b/andmore-core/plugins/devices.services/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/andmore-core/plugins/devices.services/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/devices.services/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/devices.services/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/devices.services/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/devices.services/META-INF/MANIFEST.MF b/andmore-core/plugins/devices.services/META-INF/MANIFEST.MF index 01132bd3..92c2f994 100644 --- a/andmore-core/plugins/devices.services/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/devices.services/META-INF/MANIFEST.MF @@ -1,21 +1,21 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.android.devices.services;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.android.devices.services.DeviceServicesPlugin -Bundle-Vendor: %providerName -Require-Bundle: org.eclipse.andmore.android, - org.eclipse.andmore.android.handset, - org.eclipse.andmore.android.emulator, - org.eclipse.andmore.android.common, - org.eclipse.sequoyah.device.framework, - org.eclipse.sequoyah.device.common.utilities, - org.eclipse.ui.console, - org.eclipse.ui, - org.eclipse.core.expressions, - org.eclipse.core.runtime -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Bundle-ActivationPolicy: lazy -Bundle-Localization: plugin -Export-Package: org.eclipse.andmore.android.devices.services.i18n +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.android.devices.services;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.android.devices.services.DeviceServicesPlugin +Bundle-Vendor: %providerName +Require-Bundle: org.eclipse.andmore.android, + org.eclipse.andmore.android.handset, + org.eclipse.andmore.android.emulator, + org.eclipse.andmore.android.common, + org.eclipse.sequoyah.device.framework, + org.eclipse.sequoyah.device.common.utilities, + org.eclipse.ui.console, + org.eclipse.ui, + org.eclipse.core.expressions, + org.eclipse.core.runtime +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-ActivationPolicy: lazy +Bundle-Localization: plugin +Export-Package: org.eclipse.andmore.android.devices.services.i18n diff --git a/andmore-core/plugins/emulator/.classpath b/andmore-core/plugins/emulator/.classpath index c72d35a0..54f561c7 100644 --- a/andmore-core/plugins/emulator/.classpath +++ b/andmore-core/plugins/emulator/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/andmore-core/plugins/emulator/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/emulator/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/emulator/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/emulator/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/emulator/META-INF/MANIFEST.MF b/andmore-core/plugins/emulator/META-INF/MANIFEST.MF index 7bfdee53..376e03f9 100644 --- a/andmore-core/plugins/emulator/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/emulator/META-INF/MANIFEST.MF @@ -1,47 +1,48 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.android.emulator;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.android.emulator.EmulatorPlugin -Bundle-Vendor: %providerName -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Bundle-Localization: plugin -Require-Bundle: org.eclipse.andmore, - org.eclipse.andmore.ddms, - org.eclipse.andmore.android, - org.eclipse.andmore.android.common, - org.eclipse.ui, - org.eclipse.core.runtime, - org.eclipse.core.net, - org.eclipse.sequoyah.device.common.utilities, - org.eclipse.sequoyah.device.framework, - org.eclipse.sequoyah.device.framework.ui, - org.eclipse.sequoyah.device.framework.wizard, - org.eclipse.sequoyah.vnc.vncviewer, - org.eclipse.sequoyah.vnc.protocol, - org.eclipse.sequoyah.vnc.vncviewer.vncviews, - org.eclipse.andmore.base, - org.apache.commons.net;bundle-version="1.4.1", - org.apache.oro;bundle-version="2.0.8" -Export-Package: org.eclipse.andmore.android.emulator, - org.eclipse.andmore.android.emulator.core.devfrm, - org.eclipse.andmore.android.emulator.core.emulationui, - org.eclipse.andmore.android.emulator.core.exception, - org.eclipse.andmore.android.emulator.core.model, - org.eclipse.andmore.android.emulator.core.skin, - org.eclipse.andmore.android.emulator.core.utils, - org.eclipse.andmore.android.emulator.device, - org.eclipse.andmore.android.emulator.device.definition, - org.eclipse.andmore.android.emulator.device.handlers, - org.eclipse.andmore.android.emulator.device.instance, - org.eclipse.andmore.android.emulator.device.refresh, - org.eclipse.andmore.android.emulator.device.sync, - org.eclipse.andmore.android.emulator.i18n, - org.eclipse.andmore.android.emulator.logic, - org.eclipse.andmore.android.emulator.skin.android.parser, - org.eclipse.andmore.android.emulator.ui.view -Bundle-ClassPath: lib/jakarta-oro-2.0.8.jar, - . -Bundle-ActivationPolicy: lazy - +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.android.emulator;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.android.emulator.EmulatorPlugin +Bundle-Vendor: %providerName +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-Localization: plugin +Require-Bundle: org.eclipse.andmore, + org.eclipse.andmore.ddms, + org.eclipse.andmore.android, + org.eclipse.andmore.android.common, + org.eclipse.ui, + org.eclipse.core.runtime, + org.eclipse.core.net, + org.eclipse.sequoyah.device.common.utilities, + org.eclipse.sequoyah.device.framework, + org.eclipse.sequoyah.device.framework.ui, + org.eclipse.sequoyah.device.framework.wizard, + org.eclipse.sequoyah.vnc.vncviewer, + org.eclipse.sequoyah.vnc.protocol, + org.eclipse.sequoyah.vnc.vncviewer.vncviews, + org.eclipse.andmore.swt, + org.apache.commons.net;bundle-version="1.4.1", + org.apache.oro;bundle-version="2.0.8", + org.eclipse.andmore.ddmuilib;bundle-version="0.5.2" +Export-Package: org.eclipse.andmore.android.emulator, + org.eclipse.andmore.android.emulator.core.devfrm, + org.eclipse.andmore.android.emulator.core.emulationui, + org.eclipse.andmore.android.emulator.core.exception, + org.eclipse.andmore.android.emulator.core.model, + org.eclipse.andmore.android.emulator.core.skin, + org.eclipse.andmore.android.emulator.core.utils, + org.eclipse.andmore.android.emulator.device, + org.eclipse.andmore.android.emulator.device.definition, + org.eclipse.andmore.android.emulator.device.handlers, + org.eclipse.andmore.android.emulator.device.instance, + org.eclipse.andmore.android.emulator.device.refresh, + org.eclipse.andmore.android.emulator.device.sync, + org.eclipse.andmore.android.emulator.i18n, + org.eclipse.andmore.android.emulator.logic, + org.eclipse.andmore.android.emulator.skin.android.parser, + org.eclipse.andmore.android.emulator.ui.view +Bundle-ClassPath: lib/jakarta-oro-2.0.8.jar, + . +Bundle-ActivationPolicy: lazy + diff --git a/andmore-core/plugins/emulator/src/org/eclipse/andmore/android/emulator/device/instance/AndroidDeviceInstance.java b/andmore-core/plugins/emulator/src/org/eclipse/andmore/android/emulator/device/instance/AndroidDeviceInstance.java index 89914bd4..3b360074 100644 --- a/andmore-core/plugins/emulator/src/org/eclipse/andmore/android/emulator/device/instance/AndroidDeviceInstance.java +++ b/andmore-core/plugins/emulator/src/org/eclipse/andmore/android/emulator/device/instance/AndroidDeviceInstance.java @@ -58,6 +58,7 @@ import com.android.sdklib.IAndroidTarget; import com.android.sdklib.internal.avd.AvdInfo; +import com.android.sdklib.repository.targets.AndroidTargetManager; /** * DESCRIPTION: This class represents a Android Emulator instance @@ -366,7 +367,7 @@ public static void populateWithVMInfo(String instanceName, Properties instancePr if (vmInfo != null) { // VM target - instanceProperties.setProperty(IDevicePropertiesConstants.vmTarget, vmInfo.getTarget().getName()); + instanceProperties.setProperty(IDevicePropertiesConstants.vmTarget, SdkUtils.getCurrentSdk().getAndroidTargetFor(vmInfo).getName()); // ABI Type instanceProperties.setProperty(IDevicePropertiesConstants.abiType, vmInfo.getAbiType()); @@ -404,8 +405,8 @@ public File getSkinPath() { AvdInfo avdInfo = SdkUtils.getValidVm(getName()); if (avdInfo != null) { String skinPath = avdInfo.getProperties().get("skin.path"); - skinPath = SdkUtils.getCurrentSdk().getSdkOsLocation() + skinPath; - IAndroidTarget target = avdInfo.getTarget(); + skinPath = new File(SdkUtils.getCurrentSdk().getSdkFileLocation(), skinPath).getAbsolutePath(); + IAndroidTarget target = SdkUtils.getCurrentSdk().getAndroidTargetFor(avdInfo); File candidateFile = new File(skinPath); // If path specified on the skin does not exist, try to retrieve it // from the target. @@ -459,7 +460,7 @@ public IAndroidTarget getAndroidTarget() { IAndroidTarget result = null; AvdInfo avdInfo = SdkUtils.getValidVm(getName()); if (avdInfo != null) { - result = avdInfo.getTarget(); + result = SdkUtils.getCurrentSdk().getAndroidTargetFor(avdInfo); } return result; } diff --git a/andmore-core/plugins/emulator/src/org/eclipse/andmore/android/emulator/device/ui/PropertiesMainComposite.java b/andmore-core/plugins/emulator/src/org/eclipse/andmore/android/emulator/device/ui/PropertiesMainComposite.java index 2a98ed18..fcef847e 100644 --- a/andmore-core/plugins/emulator/src/org/eclipse/andmore/android/emulator/device/ui/PropertiesMainComposite.java +++ b/andmore-core/plugins/emulator/src/org/eclipse/andmore/android/emulator/device/ui/PropertiesMainComposite.java @@ -31,6 +31,7 @@ import org.eclipse.andmore.android.emulator.device.IDevicePropertiesConstants; import org.eclipse.andmore.android.emulator.device.definition.AndroidEmuDefMgr; import org.eclipse.andmore.android.emulator.i18n.EmulatorNLS; +import org.eclipse.andmore.internal.sdk.Sdk; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Platform; import org.eclipse.jface.preference.IPreferenceNode; @@ -59,8 +60,12 @@ import com.android.sdklib.IAndroidTarget; import com.android.sdklib.ISystemImage; import com.android.SdkConstants; +import com.android.repository.testframework.FakeProgressIndicator; import com.android.sdklib.internal.avd.AvdInfo; import com.android.sdklib.internal.avd.AvdManager; +import com.android.sdklib.repository.IdDisplay; +import com.android.sdklib.repository.targets.SystemImage; +import com.android.sdklib.repository.targets.SystemImageManager; /** * DESCRIPTION:
@@ -818,18 +823,24 @@ public void modifyText(ModifyEvent e) { private void populateAbiTypeCombo(Combo abiTypeCombo) { // System Images represents the ABI types - ISystemImage[] images = vmTarget.getSystemImages(); + // TODO these IdDisplay are almost certainly wrong. + Collection images = Sdk.getCurrent().getAndroidSdkHandler().getSystemImageManager(new FakeProgressIndicator()) + .lookup(IdDisplay.create(vmTarget.getName(), vmTarget.getName()), vmTarget.getVersion(), + IdDisplay.create(vmTarget.getVendor(), vmTarget.getVendor())); // in case no images are found, get try its parent - if ((images == null) || ((images.length == 0) && !vmTarget.isPlatform())) { - images = vmTarget.getParent().getSystemImages(); + if ((images == null) || ((images.size() == 0) && !vmTarget.isPlatform())) { + IAndroidTarget parent = vmTarget.getParent(); + images = Sdk.getCurrent().getAndroidSdkHandler().getSystemImageManager(new FakeProgressIndicator()).lookup( + IdDisplay.create(parent.getName(), parent.getName()), parent.getVersion(), + IdDisplay.create(parent.getVendor(), parent.getVendor())); } // always clean abi combo since it will be reloaded abiTypeCombo.removeAll(); int i = 0; - if ((images != null) && (images.length > 0)) { + if ((images != null) && (images.size() > 0)) { for (ISystemImage image : images) { String prettyAbiName = AvdInfo.getPrettyAbiType(image); abiTypeCombo.add(prettyAbiName); diff --git a/andmore-core/plugins/handset/.classpath b/andmore-core/plugins/handset/.classpath index c72d35a0..54f561c7 100644 --- a/andmore-core/plugins/handset/.classpath +++ b/andmore-core/plugins/handset/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/andmore-core/plugins/handset/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/handset/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/handset/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/handset/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/handset/META-INF/MANIFEST.MF b/andmore-core/plugins/handset/META-INF/MANIFEST.MF index e84a4485..3780d952 100644 --- a/andmore-core/plugins/handset/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/handset/META-INF/MANIFEST.MF @@ -1,20 +1,20 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.android.handset;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.android.handset.HandsetPlugin -Bundle-Vendor: %providerName -Require-Bundle: org.eclipse.andmore.android, - org.eclipse.andmore.android.common, - org.eclipse.ui, - org.eclipse.ui.console, - org.eclipse.core.runtime, - org.eclipse.core.resources, - org.eclipse.sequoyah.device.framework, - org.eclipse.sequoyah.device.common.utilities -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Bundle-ActivationPolicy: lazy -Bundle-Localization: plugin -Export-Package: org.eclipse.andmore.android.handset, - org.eclipse.andmore.android.handset.i18n +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.android.handset;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.android.handset.HandsetPlugin +Bundle-Vendor: %providerName +Require-Bundle: org.eclipse.andmore.android, + org.eclipse.andmore.android.common, + org.eclipse.ui, + org.eclipse.ui.console, + org.eclipse.core.runtime, + org.eclipse.core.resources, + org.eclipse.sequoyah.device.framework, + org.eclipse.sequoyah.device.common.utilities +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-ActivationPolicy: lazy +Bundle-Localization: plugin +Export-Package: org.eclipse.andmore.android.handset, + org.eclipse.andmore.android.handset.i18n diff --git a/andmore-core/plugins/installer/.classpath b/andmore-core/plugins/installer/.classpath index 0b1bcf94..7498423d 100644 --- a/andmore-core/plugins/installer/.classpath +++ b/andmore-core/plugins/installer/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/andmore-core/plugins/installer/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/installer/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/installer/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/installer/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/installer/META-INF/MANIFEST.MF b/andmore-core/plugins/installer/META-INF/MANIFEST.MF index b454b444..206ff52d 100644 --- a/andmore-core/plugins/installer/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/installer/META-INF/MANIFEST.MF @@ -1,40 +1,40 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.android.installer;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.android.installer.InstallerPlugin -Bundle-Vendor: %providerName -Require-Bundle: org.eclipse.ui, - org.eclipse.core.runtime, - org.eclipse.core.resources, - org.eclipse.equinox.p2.artifact.repository, - org.eclipse.equinox.p2.core, - org.eclipse.equinox.p2.engine, - org.eclipse.equinox.p2.metadata, - org.eclipse.equinox.p2.metadata.repository, - org.eclipse.equinox.p2.ui, - org.eclipse.equinox.p2.repository, - org.eclipse.equinox.p2.operations, - org.eclipse.equinox.p2.touchpoint.natives, - org.eclipse.andmore.android.common, - org.eclipse.jdt.launching, - org.eclipse.andmore, - org.apache.commons.httpclient, - org.eclipse.ui.browser, - org.eclipse.equinox.p2.ui.sdk, - org.eclipse.andmore.base -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Bundle-ActivationPolicy: lazy -Bundle-Localization: plugin -Export-Package: org.eclipse.andmore.android.installer, - org.eclipse.andmore.android.installer.i18n, - org.eclipse.andmore.android.installer.ui.dialogs, - org.eclipse.andmore.android.installer.utilities -Import-Package: org.eclipse.andmore.android.installer.utilities, - org.eclipse.core.internal.net, - org.eclipse.core.net.proxy, - org.eclipse.equinox.security.storage, - org.eclipse.jdt.core, - org.eclipse.jdt.internal.ui, - org.eclipse.ui.internal.net.auth +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.android.installer;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.android.installer.InstallerPlugin +Bundle-Vendor: %providerName +Require-Bundle: org.eclipse.ui, + org.eclipse.core.runtime, + org.eclipse.core.resources, + org.eclipse.equinox.p2.artifact.repository, + org.eclipse.equinox.p2.core, + org.eclipse.equinox.p2.engine, + org.eclipse.equinox.p2.metadata, + org.eclipse.equinox.p2.metadata.repository, + org.eclipse.equinox.p2.ui, + org.eclipse.equinox.p2.repository, + org.eclipse.equinox.p2.operations, + org.eclipse.equinox.p2.touchpoint.natives, + org.eclipse.andmore.android.common, + org.eclipse.jdt.launching, + org.eclipse.andmore, + org.apache.commons.httpclient, + org.eclipse.ui.browser, + org.eclipse.equinox.p2.ui.sdk, + org.eclipse.andmore.base +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-ActivationPolicy: lazy +Bundle-Localization: plugin +Export-Package: org.eclipse.andmore.android.installer, + org.eclipse.andmore.android.installer.i18n, + org.eclipse.andmore.android.installer.ui.dialogs, + org.eclipse.andmore.android.installer.utilities +Import-Package: org.eclipse.andmore.android.installer.utilities, + org.eclipse.core.internal.net, + org.eclipse.core.net.proxy, + org.eclipse.equinox.security.storage, + org.eclipse.jdt.core, + org.eclipse.jdt.internal.ui, + org.eclipse.ui.internal.net.auth diff --git a/andmore-core/plugins/launch/.classpath b/andmore-core/plugins/launch/.classpath index c72d35a0..54f561c7 100644 --- a/andmore-core/plugins/launch/.classpath +++ b/andmore-core/plugins/launch/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/andmore-core/plugins/launch/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/launch/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/launch/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/launch/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/launch/META-INF/MANIFEST.MF b/andmore-core/plugins/launch/META-INF/MANIFEST.MF index e4746b8f..818eec6f 100644 --- a/andmore-core/plugins/launch/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/launch/META-INF/MANIFEST.MF @@ -1,25 +1,25 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.android.launch;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.android.launch.LaunchPlugin -Bundle-Vendor: %providerName -Require-Bundle: org.eclipse.andmore.android, - org.eclipse.andmore.android.emulator, - org.eclipse.andmore.android.common, - org.eclipse.debug.ui, - org.eclipse.jdt.launching, - org.eclipse.jdt.core, - org.eclipse.sequoyah.device.framework, - org.eclipse.sequoyah.device.common.utilities, - org.eclipse.andmore.ddms, - org.eclipse.andmore, - org.eclipse.core.runtime, - org.eclipse.ui.console, - org.eclipse.ui, - org.eclipse.andmore.base -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Bundle-Localization: plugin -Bundle-ActivationPolicy: lazy -Export-Package: org.eclipse.andmore.android.launch.i18n +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.android.launch;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.android.launch.LaunchPlugin +Bundle-Vendor: %providerName +Require-Bundle: org.eclipse.andmore.android, + org.eclipse.andmore.android.emulator, + org.eclipse.andmore.android.common, + org.eclipse.debug.ui, + org.eclipse.jdt.launching, + org.eclipse.jdt.core, + org.eclipse.sequoyah.device.framework, + org.eclipse.sequoyah.device.common.utilities, + org.eclipse.andmore.ddms, + org.eclipse.andmore, + org.eclipse.core.runtime, + org.eclipse.ui.console, + org.eclipse.ui, + org.eclipse.andmore.swt +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-Localization: plugin +Bundle-ActivationPolicy: lazy +Export-Package: org.eclipse.andmore.android.launch.i18n diff --git a/andmore-core/plugins/logger.collector/.classpath b/andmore-core/plugins/logger.collector/.classpath index c72d35a0..54f561c7 100644 --- a/andmore-core/plugins/logger.collector/.classpath +++ b/andmore-core/plugins/logger.collector/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/andmore-core/plugins/logger.collector/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/logger.collector/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/logger.collector/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/logger.collector/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/logger.collector/META-INF/MANIFEST.MF b/andmore-core/plugins/logger.collector/META-INF/MANIFEST.MF index e66ba7f8..f2ffb600 100644 --- a/andmore-core/plugins/logger.collector/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/logger.collector/META-INF/MANIFEST.MF @@ -1,14 +1,14 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.android.logger.collector;singleton:=true -Bundle-Version: 0.5.2.qualifier -Require-Bundle: org.eclipse.ui, - org.eclipse.core.runtime -Bundle-ActivationPolicy: lazy -Bundle-Vendor: %providerName -Bundle-Localization: plugin -Export-Package: org.eclipse.andmore.android.logger.collector.core -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Import-Package: org.eclipse.andmore.android.common.utilities, - org.eclipse.andmore.android.logger +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.android.logger.collector;singleton:=true +Bundle-Version: 0.5.2.qualifier +Require-Bundle: org.eclipse.ui, + org.eclipse.core.runtime +Bundle-ActivationPolicy: lazy +Bundle-Vendor: %providerName +Bundle-Localization: plugin +Export-Package: org.eclipse.andmore.android.logger.collector.core +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Import-Package: org.eclipse.andmore.android.common.utilities, + org.eclipse.andmore.android.logger diff --git a/andmore-core/plugins/logger/.classpath b/andmore-core/plugins/logger/.classpath index be9ef572..13f61b0e 100644 --- a/andmore-core/plugins/logger/.classpath +++ b/andmore-core/plugins/logger/.classpath @@ -1,8 +1,8 @@ - - - - - - - - + + + + + + + + diff --git a/andmore-core/plugins/logger/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/logger/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/logger/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/logger/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/logger/META-INF/MANIFEST.MF b/andmore-core/plugins/logger/META-INF/MANIFEST.MF index 7feb2958..45349b74 100644 --- a/andmore-core/plugins/logger/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/logger/META-INF/MANIFEST.MF @@ -1,13 +1,13 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: Andmore Android Logger -Bundle-SymbolicName: org.eclipse.andmore.android.logger;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.android.logger.internal.Activator -Bundle-Vendor: Eclipse Andmore -Export-Package: org.eclipse.andmore.android.logger -Bundle-ClassPath: lib/log4j-1.2.14.jar, - . -Require-Bundle: org.eclipse.core.runtime -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Bundle-ActivationPolicy: lazy +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Andmore Android Logger +Bundle-SymbolicName: org.eclipse.andmore.android.logger;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.android.logger.internal.Activator +Bundle-Vendor: Eclipse Andmore +Export-Package: org.eclipse.andmore.android.logger +Bundle-ClassPath: lib/log4j-1.2.14.jar, + . +Require-Bundle: org.eclipse.core.runtime +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-ActivationPolicy: lazy diff --git a/andmore-core/plugins/mat/.classpath b/andmore-core/plugins/mat/.classpath index c72d35a0..54f561c7 100644 --- a/andmore-core/plugins/mat/.classpath +++ b/andmore-core/plugins/mat/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/andmore-core/plugins/mat/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/mat/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/mat/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/mat/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/mat/META-INF/MANIFEST.MF b/andmore-core/plugins/mat/META-INF/MANIFEST.MF index cf28776a..9372c4b5 100644 --- a/andmore-core/plugins/mat/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/mat/META-INF/MANIFEST.MF @@ -1,21 +1,21 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.android.mat;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.android.mat.Activator -Bundle-Vendor: %providerName -Require-Bundle: org.eclipse.ui, - org.eclipse.core.runtime, - org.eclipse.mat.ui, - org.eclipse.sequoyah.device.framework, - org.eclipse.sequoyah.device.common.utilities, - org.eclipse.core.resources, - org.eclipse.andmore.android, - org.eclipse.andmore.android.emulator -Bundle-ActivationPolicy: lazy -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Bundle-Localization: plugin -Export-Package: org.eclipse.andmore.android.mat.i18n -Import-Package: org.eclipse.andmore.android.common.log - +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.android.mat;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.android.mat.Activator +Bundle-Vendor: %providerName +Require-Bundle: org.eclipse.ui, + org.eclipse.core.runtime, + org.eclipse.mat.ui, + org.eclipse.sequoyah.device.framework, + org.eclipse.sequoyah.device.common.utilities, + org.eclipse.core.resources, + org.eclipse.andmore.android, + org.eclipse.andmore.android.emulator +Bundle-ActivationPolicy: lazy +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-Localization: plugin +Export-Package: org.eclipse.andmore.android.mat.i18n +Import-Package: org.eclipse.andmore.android.common.log + diff --git a/andmore-core/plugins/packaging.ui/.classpath b/andmore-core/plugins/packaging.ui/.classpath index c72d35a0..54f561c7 100644 --- a/andmore-core/plugins/packaging.ui/.classpath +++ b/andmore-core/plugins/packaging.ui/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/andmore-core/plugins/packaging.ui/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/packaging.ui/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/packaging.ui/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/packaging.ui/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/packaging.ui/META-INF/MANIFEST.MF b/andmore-core/plugins/packaging.ui/META-INF/MANIFEST.MF index 4a497ac9..872ee7a3 100644 --- a/andmore-core/plugins/packaging.ui/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/packaging.ui/META-INF/MANIFEST.MF @@ -1,22 +1,22 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.android.packaging.ui;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.android.packaging.ui.PackagingUIPlugin -Bundle-Vendor: %providerName -Require-Bundle: org.eclipse.core.resources, - org.eclipse.core.runtime, - org.eclipse.ui, - org.eclipse.ui.forms, - org.eclipse.ui.ide, - org.eclipse.ui.views, - org.eclipse.jdt.core, - org.eclipse.jdt.ui, - org.eclipse.andmore.android.common, - org.eclipse.andmore.android, - org.eclipse.andmore.android.certmanager -Bundle-Localization: plugin -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Bundle-ActivationPolicy: lazy -Export-Package: org.eclipse.andmore.android.packaging.ui.i18n +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.android.packaging.ui;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.android.packaging.ui.PackagingUIPlugin +Bundle-Vendor: %providerName +Require-Bundle: org.eclipse.core.resources, + org.eclipse.core.runtime, + org.eclipse.ui, + org.eclipse.ui.forms, + org.eclipse.ui.ide, + org.eclipse.ui.views, + org.eclipse.jdt.core, + org.eclipse.jdt.ui, + org.eclipse.andmore.android.common, + org.eclipse.andmore.android, + org.eclipse.andmore.android.certmanager +Bundle-Localization: plugin +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-ActivationPolicy: lazy +Export-Package: org.eclipse.andmore.android.packaging.ui.i18n diff --git a/andmore-core/plugins/remote.device/.classpath b/andmore-core/plugins/remote.device/.classpath index c72d35a0..54f561c7 100644 --- a/andmore-core/plugins/remote.device/.classpath +++ b/andmore-core/plugins/remote.device/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/andmore-core/plugins/remote.device/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/remote.device/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/remote.device/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/remote.device/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/remote.device/META-INF/MANIFEST.MF b/andmore-core/plugins/remote.device/META-INF/MANIFEST.MF index dda83e9f..a13cc697 100644 --- a/andmore-core/plugins/remote.device/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/remote.device/META-INF/MANIFEST.MF @@ -1,20 +1,20 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.android.remote;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.android.remote.RemoteDevicePlugin -Bundle-Vendor: %providerName -Require-Bundle: org.eclipse.ui, - org.eclipse.core.runtime, - org.eclipse.sequoyah.device.framework, - org.eclipse.sequoyah.device.framework.ui, - org.eclipse.sequoyah.device.common.utilities, - org.eclipse.andmore.android.common, - org.eclipse.andmore.android -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Bundle-Localization: plugin -Bundle-ActivationPolicy: lazy -Export-Package: org.eclipse.andmore.android.remote, - org.eclipse.andmore.android.remote.i18n, - org.eclipse.andmore.android.remote.instance +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.android.remote;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.android.remote.RemoteDevicePlugin +Bundle-Vendor: %providerName +Require-Bundle: org.eclipse.ui, + org.eclipse.core.runtime, + org.eclipse.sequoyah.device.framework, + org.eclipse.sequoyah.device.framework.ui, + org.eclipse.sequoyah.device.common.utilities, + org.eclipse.andmore.android.common, + org.eclipse.andmore.android +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-Localization: plugin +Bundle-ActivationPolicy: lazy +Export-Package: org.eclipse.andmore.android.remote, + org.eclipse.andmore.android.remote.i18n, + org.eclipse.andmore.android.remote.instance diff --git a/andmore-core/plugins/snippets/.classpath b/andmore-core/plugins/snippets/.classpath index c72d35a0..54f561c7 100644 --- a/andmore-core/plugins/snippets/.classpath +++ b/andmore-core/plugins/snippets/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/andmore-core/plugins/snippets/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/snippets/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/snippets/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/snippets/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/snippets/META-INF/MANIFEST.MF b/andmore-core/plugins/snippets/META-INF/MANIFEST.MF index 3b9eddc8..47be168a 100644 --- a/andmore-core/plugins/snippets/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/snippets/META-INF/MANIFEST.MF @@ -1,19 +1,19 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.android.codesnippets;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Localization: plugin -Require-Bundle: org.eclipse.core.runtime, - org.eclipse.core.resources, - org.eclipse.ui, - org.eclipse.ui.ide, - org.eclipse.ui.editors, - org.eclipse.gef, - org.eclipse.jface.text, - org.eclipse.jdt.ui, - org.eclipse.wst.common.snippets, - org.eclipse.andmore.android.common -Bundle-Vendor: %providerName -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Export-Package: org.eclipse.andmore.android.codesnippets.i18n +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.android.codesnippets;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Localization: plugin +Require-Bundle: org.eclipse.core.runtime, + org.eclipse.core.resources, + org.eclipse.ui, + org.eclipse.ui.ide, + org.eclipse.ui.editors, + org.eclipse.gef, + org.eclipse.jface.text, + org.eclipse.jdt.ui, + org.eclipse.wst.common.snippets, + org.eclipse.andmore.android.common +Bundle-Vendor: %providerName +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Export-Package: org.eclipse.andmore.android.codesnippets.i18n diff --git a/andmore-core/plugins/translation/.classpath b/andmore-core/plugins/translation/.classpath index c72d35a0..54f561c7 100644 --- a/andmore-core/plugins/translation/.classpath +++ b/andmore-core/plugins/translation/.classpath @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + diff --git a/andmore-core/plugins/translation/.settings/org.eclipse.jdt.core.prefs b/andmore-core/plugins/translation/.settings/org.eclipse.jdt.core.prefs index c537b630..295926d9 100644 --- a/andmore-core/plugins/translation/.settings/org.eclipse.jdt.core.prefs +++ b/andmore-core/plugins/translation/.settings/org.eclipse.jdt.core.prefs @@ -1,7 +1,7 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-core/plugins/translation/META-INF/MANIFEST.MF b/andmore-core/plugins/translation/META-INF/MANIFEST.MF index 7a4f1992..4e57a860 100644 --- a/andmore-core/plugins/translation/META-INF/MANIFEST.MF +++ b/andmore-core/plugins/translation/META-INF/MANIFEST.MF @@ -1,26 +1,26 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.android.translation;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Vendor: %providerName -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Require-Bundle: org.eclipse.core.net, - org.eclipse.core.runtime, - org.eclipse.jface, - org.eclipse.sequoyah.localization.tools, - org.eclipse.ui.workbench, - org.eclipse.andmore.android.common, - org.eclipse.ui.net, - org.eclipse.sequoyah.localization.android, - org.eclipse.sequoyah.localization.editor, - org.apache.commons.net;bundle-version="1.4.1", - org.apache.oro;bundle-version="2.0.8" -Bundle-ClassPath: . -Import-Package: org.apache.commons.httpclient, - org.apache.commons.httpclient.auth;version="3.1.0", - org.apache.commons.httpclient.methods;version="3.1.0", - org.apache.commons.httpclient.params;version="3.1.0" -Bundle-Activator: org.eclipse.andmore.android.localization.translators.TranslationPlugin -Bundle-Localization: plugin -Bundle-ActivationPolicy: lazy +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.android.translation;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Vendor: %providerName +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Require-Bundle: org.eclipse.core.net, + org.eclipse.core.runtime, + org.eclipse.jface, + org.eclipse.sequoyah.localization.tools, + org.eclipse.ui.workbench, + org.eclipse.andmore.android.common, + org.eclipse.ui.net, + org.eclipse.sequoyah.localization.android, + org.eclipse.sequoyah.localization.editor, + org.apache.commons.net;bundle-version="1.4.1", + org.apache.oro;bundle-version="2.0.8" +Bundle-ClassPath: . +Import-Package: org.apache.commons.httpclient, + org.apache.commons.httpclient.auth;version="3.1.0", + org.apache.commons.httpclient.methods;version="3.1.0", + org.apache.commons.httpclient.params;version="3.1.0" +Bundle-Activator: org.eclipse.andmore.android.localization.translators.TranslationPlugin +Bundle-Localization: plugin +Bundle-ActivationPolicy: lazy diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/.classpath b/andmore-swt/org.eclipse.andmore.ddmuilib/.classpath index bb504db2..6042c676 100644 --- a/andmore-swt/org.eclipse.andmore.ddmuilib/.classpath +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/.classpath @@ -1,7 +1,6 @@ - diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/META-INF/MANIFEST.MF b/andmore-swt/org.eclipse.andmore.ddmuilib/META-INF/MANIFEST.MF index 412e670c..ebd3e520 100644 --- a/andmore-swt/org.eclipse.andmore.ddmuilib/META-INF/MANIFEST.MF +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/META-INF/MANIFEST.MF @@ -1,31 +1,32 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Localization: plugin -Bundle-Name: %Bundle-Name -Bundle-SymbolicName: org.eclipse.andmore.ddmuilib;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Vendor: %Bundle-Vendor -Bundle-RequiredExecutionEnvironment: JavaSE-1.8 -Require-Bundle: org.eclipse.core.commands;bundle-version="3.8.1", - org.eclipse.equinox.common;bundle-version="3.8.0", - org.eclipse.jface;bundle-version="3.12.2", - org.eclipse.andmore.swt -Export-Package: com.android.ddmuilib, - com.android.ddmuilib.actions, - com.android.ddmuilib.annotation, - com.android.ddmuilib.console, - com.android.ddmuilib.explorer, - com.android.ddmuilib.handler, - com.android.ddmuilib.heap, - com.android.ddmuilib.location, - com.android.ddmuilib.log.event, - com.android.ddmuilib.logcat, - com.android.ddmuilib.net, - com.android.ddmuilib.screenrecord, - com.android.ddmuilib.vmtrace, - images -Bundle-ClassPath: ., - libs/chart_swt-1.0.13.jar, - libs/jcommon-1.0.24.jar, - libs/jfreechart-1.0.19.jar, - libs/jfreechart-swt-1.0.jar +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Localization: plugin +Bundle-Name: %Bundle-Name +Bundle-SymbolicName: org.eclipse.andmore.ddmuilib;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Vendor: %Bundle-Vendor +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Require-Bundle: org.eclipse.core.commands;bundle-version="3.8.1", + org.eclipse.equinox.common;bundle-version="3.8.0", + org.eclipse.jface;bundle-version="3.12.2", + org.eclipse.andmore.swt, + org.eclipse.core.runtime;bundle-version="3.12.0" +Export-Package: com.android.ddmuilib, + com.android.ddmuilib.actions, + com.android.ddmuilib.annotation, + com.android.ddmuilib.console, + com.android.ddmuilib.explorer, + com.android.ddmuilib.handler, + com.android.ddmuilib.heap, + com.android.ddmuilib.location, + com.android.ddmuilib.log.event, + com.android.ddmuilib.logcat, + com.android.ddmuilib.net, + com.android.ddmuilib.screenrecord, + com.android.ddmuilib.vmtrace +Bundle-ClassPath: ., + libs/chart_swt-1.0.13.jar, + libs/jcommon-1.0.24.jar, + libs/jfreechart-1.0.19.jar, + libs/jfreechart-swt-1.0.jar +Import-Package: org.eclipse.ui.plugin diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/plugin.xml b/andmore-swt/org.eclipse.andmore.ddmuilib/plugin.xml new file mode 100644 index 00000000..b60f62ae --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/plugin.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/AllocationPanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/AllocationPanel.java index 04a40405..04e09d46 100644 --- a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/AllocationPanel.java +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/AllocationPanel.java @@ -22,7 +22,7 @@ import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; import com.android.ddmlib.Client; import com.android.ddmlib.ClientData.AllocationTrackingStatus; - +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.viewers.ILabelProviderListener; import org.eclipse.jface.viewers.ISelection; @@ -50,7 +50,6 @@ 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; @@ -94,6 +93,7 @@ public class AllocationPanel extends TablePanel { private Image mSortUpImg; private Image mSortDownImg; private String mFilterText = null; + private ImageFactory mImageFactory; /** * Content Provider to display the allocations of a client. @@ -184,6 +184,12 @@ public void removeListener(ILabelProviderListener listener) { } } + public AllocationPanel(ImageFactory imageFactory) + { + super(); + mImageFactory = imageFactory; + } + /** * Create our control(s). */ @@ -191,11 +197,9 @@ public void removeListener(ILabelProviderListener listener) { protected Control createControl(Composite parent) { final IPreferenceStore store = DdmUiPreferences.getStore(); - Display display = parent.getDisplay(); - // get some images - mSortUpImg = ImageLoader.getDdmUiLibLoader().loadImage("sort_up.png", display); - mSortDownImg = ImageLoader.getDdmUiLibLoader().loadImage("sort_down.png", display); + mSortUpImg = mImageFactory.getImageByName("sort_up.png"); + mSortDownImg = mImageFactory.getImageByName("sort_down.png"); // base composite for selected client with enabled thread update. mAllocationBase = new Composite(parent, SWT.NONE); diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/DevicePanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/DevicePanel.java index 992e0c03..71137146 100644 --- a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/DevicePanel.java +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/DevicePanel.java @@ -30,6 +30,7 @@ import com.android.ddmuilib.vmtrace.VmTraceOptionsDialog; import com.google.common.base.Throwables; +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.viewers.ILabelProviderListener; @@ -44,6 +45,7 @@ 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.layout.FillLayout; import org.eclipse.swt.widgets.Composite; @@ -102,6 +104,7 @@ public final class DevicePanel extends Panel implements IDebugBridgeChangeListen private Image mWaitingImage; private Image mDebuggerImage; private Image mDebugErrorImage; + private ImageFactory mImageFactory; private final ArrayList mListeners = new ArrayList(); @@ -318,10 +321,12 @@ public interface IUiSelectionListener { /** * Creates the {@link DevicePanel} object. * @param advancedPortSupport if true the device panel will add support for selected client port + * @param imageFactory Image loader * and display the ports in the ui. */ - public DevicePanel(boolean advancedPortSupport) { + public DevicePanel(boolean advancedPortSupport, ImageFactory imageFactory) { mAdvancedPortSupport = advancedPortSupport; + mImageFactory = imageFactory; } public void addSelectionListener(IUiSelectionListener listener) { @@ -718,42 +723,68 @@ public void run() { } private void loadImages(Display display) { - ImageLoader loader = ImageLoader.getDdmUiLibLoader(); - if (mDeviceImage == null) { - mDeviceImage = loader.loadImage(display, "device.png", //$NON-NLS-1$ - ICON_WIDTH, ICON_WIDTH, - display.getSystemColor(SWT.COLOR_RED)); + String imageName = "device.png"; //$NON-NLS-1$ + mDeviceImage = mImageFactory.getImageByName(imageName); + if (mDeviceImage == null) { + mDeviceImage = loadImage(imageName, display, + ICON_WIDTH, ICON_WIDTH, + display.getSystemColor(SWT.COLOR_RED)); + } } if (mEmulatorImage == null) { - mEmulatorImage = loader.loadImage(display, - "emulator.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$ - display.getSystemColor(SWT.COLOR_BLUE)); + String imageName = "emulator.png"; //$NON-NLS-1$ + mEmulatorImage = mImageFactory.getImageByName(imageName); + if (mEmulatorImage == null) { + mEmulatorImage = loadImage(imageName, display, + ICON_WIDTH, ICON_WIDTH, + display.getSystemColor(SWT.COLOR_BLUE)); + } } if (mThreadImage == null) { - mThreadImage = loader.loadImage(display, ICON_THREAD, - ICON_WIDTH, ICON_WIDTH, - display.getSystemColor(SWT.COLOR_YELLOW)); + String imageName = ICON_THREAD; + mThreadImage = mImageFactory.getImageByName(imageName); + if (mThreadImage == null) { + mThreadImage = loadImage(imageName, display, + ICON_WIDTH, ICON_WIDTH, + display.getSystemColor(SWT.COLOR_YELLOW)); + } } if (mHeapImage == null) { - mHeapImage = loader.loadImage(display, ICON_HEAP, - ICON_WIDTH, ICON_WIDTH, - display.getSystemColor(SWT.COLOR_BLUE)); + String heapImageName = ICON_HEAP; + mHeapImage = mImageFactory.getImageByName(heapImageName); + if (mHeapImage == null) { + mHeapImage = loadImage(heapImageName, display, + ICON_WIDTH, ICON_WIDTH, + display.getSystemColor(SWT.COLOR_BLUE)); + } } if (mWaitingImage == null) { - mWaitingImage = loader.loadImage(display, - "debug-wait.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$ - display.getSystemColor(SWT.COLOR_RED)); + String waitImageName = "debug-wait.png"; //$NON-NLS-1$ + mWaitingImage = mImageFactory.getImageByName(waitImageName); + if (mWaitingImage == null) { + mWaitingImage = loadImage(waitImageName, display, + ICON_WIDTH, ICON_WIDTH, + display.getSystemColor(SWT.COLOR_RED)); + } } if (mDebuggerImage == null) { - mDebuggerImage = loader.loadImage(display, - "debug-attach.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$ - display.getSystemColor(SWT.COLOR_GREEN)); + String debugImageName = "debug-attach.png"; //$NON-NLS-1$ + mDebuggerImage = mImageFactory.getImageByName(debugImageName); + if (mDebuggerImage == null) { + mDebuggerImage = loadImage(debugImageName, display, + ICON_WIDTH, ICON_WIDTH, + display.getSystemColor(SWT.COLOR_GREEN)); + } } if (mDebugErrorImage == null) { - mDebugErrorImage = loader.loadImage(display, - "debug-error.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$ - display.getSystemColor(SWT.COLOR_RED)); + String deviceImageName = "device.png"; //$NON-NLS-1$ + mDebugErrorImage = mImageFactory.getImageByName(deviceImageName); + if (mDebugErrorImage == null) { + mDebugErrorImage = loadImage(deviceImageName, display, + ICON_WIDTH, ICON_WIDTH, + display.getSystemColor(SWT.COLOR_RED)); + } } } @@ -826,4 +857,8 @@ private void notifyListeners(IDevice selectedDevice, Client selectedClient) { } } + private Image loadImage(String imageName, Display display, int width, int height, Color color) { + return mImageFactory.getImageByName(imageName, new ReplacementImageFactory(display, width, height, color)); + } + } diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/EmulatorControlPanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/EmulatorControlPanel.java index ce6f084d..c10ff3fb 100644 --- a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/EmulatorControlPanel.java +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/EmulatorControlPanel.java @@ -32,6 +32,7 @@ import com.android.ddmuilib.location.WayPointContentProvider; import com.android.ddmuilib.location.WayPointLabelProvider; +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.viewers.ISelection; @@ -223,9 +224,11 @@ public void widgetSelected(SelectionEvent e) { }; private Composite mKmlPlayControls; private Composite mGpxPlayControls; + private ImageFactory mImageFactory; - public EmulatorControlPanel() { + public EmulatorControlPanel(ImageFactory imageFactory) { + mImageFactory = imageFactory; } /** @@ -535,9 +538,8 @@ private void createLocationControls(final Composite top) { createManualLocationControl(manualLocationComp); - ImageLoader loader = ImageLoader.getDdmUiLibLoader(); - mPlayImage = loader.loadImage("play.png", mParent.getDisplay()); //$NON-NLS-1$ - mPauseImage = loader.loadImage("pause.png", mParent.getDisplay()); //$NON-NLS-1$ + mPlayImage = mImageFactory.getImageByName("play.png"); //$NON-NLS-1$ + mPauseImage = mImageFactory.getImageByName("pause.png"); //$NON-NLS-1$ Composite gpxLocationComp = new Composite(mLocationFolders, SWT.NONE); item = new TabItem(mLocationFolders, SWT.NONE); @@ -836,14 +838,12 @@ public void widgetSelected(SelectionEvent e) { separator.setLayoutData(gd = new GridData( GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL)); gd.heightHint = 0; - - ImageLoader loader = ImageLoader.getDdmUiLibLoader(); mGpxBackwardButton = new Button(mGpxPlayControls, SWT.TOGGLE | SWT.FLAT); - mGpxBackwardButton.setImage(loader.loadImage("backward.png", mParent.getDisplay())); //$NON-NLS-1$ + mGpxBackwardButton.setImage(mImageFactory.getImageByName("backward.png")); //$NON-NLS-1$ mGpxBackwardButton.setSelection(false); mGpxBackwardButton.addSelectionListener(mDirectionButtonAdapter); mGpxForwardButton = new Button(mGpxPlayControls, SWT.TOGGLE | SWT.FLAT); - mGpxForwardButton.setImage(loader.loadImage("forward.png", mParent.getDisplay())); //$NON-NLS-1$ + mGpxForwardButton.setImage(mImageFactory.getImageByName("forward.png")); //$NON-NLS-1$ mGpxForwardButton.setSelection(true); mGpxForwardButton.addSelectionListener(mDirectionButtonAdapter); @@ -976,13 +976,12 @@ public void widgetSelected(SelectionEvent e) { GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL)); gd.heightHint = 0; - ImageLoader loader = ImageLoader.getDdmUiLibLoader(); mKmlBackwardButton = new Button(mKmlPlayControls, SWT.TOGGLE | SWT.FLAT); - mKmlBackwardButton.setImage(loader.loadImage("backward.png", mParent.getDisplay())); //$NON-NLS-1$ + mKmlBackwardButton.setImage(mImageFactory.getImageByName("backward.png")); //$NON-NLS-1$ mKmlBackwardButton.setSelection(false); mKmlBackwardButton.addSelectionListener(mDirectionButtonAdapter); mKmlForwardButton = new Button(mKmlPlayControls, SWT.TOGGLE | SWT.FLAT); - mKmlForwardButton.setImage(loader.loadImage("forward.png", mParent.getDisplay())); //$NON-NLS-1$ + mKmlForwardButton.setImage(mImageFactory.getImageByName("forward.png")); //$NON-NLS-1$ mKmlForwardButton.setSelection(true); mKmlForwardButton.addSelectionListener(mDirectionButtonAdapter); diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/HeapPanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/HeapPanel.java index 8900570c..dcbbc7f9 100644 --- a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/HeapPanel.java +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/HeapPanel.java @@ -382,9 +382,11 @@ public void widgetSelected(SelectionEvent e) { { mLinearHeapImage = new Label(mLinearBase, SWT.NONE); mLinearHeapImage.setLayoutData(new GridData()); - mLinearHeapImage.setImage(ImageLoader.createPlaceHolderArt(mDisplay, - PLACEHOLDER_LINEAR_H_SIZE, PLACEHOLDER_LINEAR_V_SIZE, - mDisplay.getSystemColor(SWT.COLOR_BLUE))); + ReplacementImageFactory replacementImageFactory = + new ReplacementImageFactory(mDisplay, + PLACEHOLDER_LINEAR_H_SIZE, PLACEHOLDER_LINEAR_V_SIZE, + mDisplay.getSystemColor(SWT.COLOR_BLUE)); + mLinearHeapImage.setImage(replacementImageFactory.getImage()); // create a composite to contain the bottom part (legend) Composite bottomSection = new Composite(mLinearBase, SWT.NONE); @@ -414,9 +416,11 @@ public void widgetSelected(SelectionEvent e) { if (DISPLAY_HILBERT_BITMAP) { mHilbertHeapImage = new Label(mHilbertBase, SWT.NONE); mHilbertHeapImage.setLayoutData(new GridData()); - mHilbertHeapImage.setImage(ImageLoader.createPlaceHolderArt(mDisplay, - PLACEHOLDER_HILBERT_SIZE, PLACEHOLDER_HILBERT_SIZE, - mDisplay.getSystemColor(SWT.COLOR_BLUE))); + ReplacementImageFactory replacementImageFactory = + new ReplacementImageFactory(mDisplay, + PLACEHOLDER_HILBERT_SIZE, PLACEHOLDER_HILBERT_SIZE, + mDisplay.getSystemColor(SWT.COLOR_BLUE)); + mHilbertHeapImage.setImage(replacementImageFactory.getImage()); // create a composite to contain the right part (legend + zoom) Composite rightSection = new Composite(mHilbertBase, SWT.NONE); diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ImageLoader.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ImageLoader.java deleted file mode 100644 index a6e31b40..00000000 --- a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ImageLoader.java +++ /dev/null @@ -1,206 +0,0 @@ -/* - * 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.jface.resource.ImageDescriptor; -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; - -import java.io.InputStream; -import java.net.URL; -import java.util.HashMap; - -/** - * Class to load images stored in a jar file. - * All images are loaded from /images/filename - * - * Because Java requires to know the jar file in which to load the image from, a class is required - * when getting the instance. Instances are cached and associated to the class passed to - * {@link #getLoader(Class)}. - * - * {@link #getDdmUiLibLoader()} use {@link ImageLoader#getClass()} as the class. This is to be used - * to load images from ddmuilib. - * - * Loaded images are stored so that 2 calls with the same filename will return the same object. - * This also means that {@link Image} object returned by the loader should never be disposed. - * - */ -public class ImageLoader { - - private static final String PATH = "/images/"; //$NON-NLS-1$ - - private final HashMap mLoadedImages = new HashMap(); - private static final HashMap, ImageLoader> mInstances = - new HashMap, ImageLoader>(); - private final Class mClass; - - /** - * Private constructor, creating an instance associated with a class. - * The class is used to identify which jar file the images are loaded from. - */ - private ImageLoader(Class theClass) { - if (theClass == null) { - theClass = ImageLoader.class; - } - mClass = theClass; - } - - /** - * Returns the {@link ImageLoader} instance to load images from ddmuilib.jar - */ - public static ImageLoader getDdmUiLibLoader() { - return getLoader(null); - } - - /** - * Returns an {@link ImageLoader} to load images based on a given class. - * - * The loader will load images from the jar from which the class was loaded. using - * {@link Class#getResource(String)} and {@link Class#getResourceAsStream(String)}. - * - * Since all images are loaded using the path /images/filename, any class from the - * jar will work. However since the loader is cached and reused when the query provides the same - * class instance, and since the loader will also cache the loaded images, it is recommended - * to always use the same class for a given Jar file. - * - */ - public static ImageLoader getLoader(Class theClass) { - ImageLoader instance = mInstances.get(theClass); - if (instance == null) { - instance = new ImageLoader(theClass); - mInstances.put(theClass, instance); - } - - return instance; - } - - /** - * Disposes all images for all instances. - * This should only be called when the program exits. - */ - public static void dispose() { - for (ImageLoader loader : mInstances.values()) { - loader.doDispose(); - } - } - - private synchronized void doDispose() { - for (Image image : mLoadedImages.values()) { - image.dispose(); - } - - mLoadedImages.clear(); - } - - /** - * Returns an {@link ImageDescriptor} for a given filename. - * - * This searches for an image located at /images/filename. - * - * @param filename the filename of the image to load. - */ - public ImageDescriptor loadDescriptor(String filename) { - URL url = mClass.getResource(PATH + filename); - // TODO cache in a map - return ImageDescriptor.createFromURL(url); - } - - /** - * Returns an {@link Image} for a given filename. - * - * This searches for an image located at /images/filename. - * - * @param filename the filename of the image to load. - * @param display the Display object - */ - public synchronized Image loadImage(String filename, Display display) { - Image img = mLoadedImages.get(filename); - if (img == null) { - String tmp = PATH + filename; - InputStream imageStream = mClass.getResourceAsStream(tmp); - - if (imageStream != null) { - img = new Image(display, imageStream); - mLoadedImages.put(filename, img); - } - - if (img == null) { - throw new RuntimeException("Failed to load " + tmp); - } - } - - return img; - } - - /** - * 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 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 Image loadImage(Display display, String fileName, int width, int height, - Color phColor) { - - Image img = 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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ReplacementImageFactory.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ReplacementImageFactory.java new file mode 100644 index 00000000..63c50ca6 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ReplacementImageFactory.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2017 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.andmore.base.resources.ImageFactory.ReplacementImager; +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.graphics.ImageData; +import org.eclipse.swt.widgets.Display; + +public class ReplacementImageFactory implements ReplacementImager { + + private Display display; + private int width; + private int height; + private Color color; + + /** + * @param display Display object + * @param width Width to create replacement Image + * @param height Height to create replacement Image + * @param color Optional color to create replacement Image. If null, Blue + * color will be used. + */ + public ReplacementImageFactory(Display display, int width, int height, Color color) + { + this.display = display; + this.width = width; + this.height = height; + this.color = color != null ? color : display.getSystemColor(SWT.COLOR_BLUE); + } + + @Override + public ImageData create() { + Image img = getImage(); + ImageData imageData = img.getImageData(); + img.dispose(); + return imageData; + } + + public Image getImage() { + 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/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ScreenShotDialog.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ScreenShotDialog.java index fa49cc0e..8a379c1c 100644 --- a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ScreenShotDialog.java +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/ScreenShotDialog.java @@ -204,10 +204,9 @@ public void widgetSelected(SelectionEvent e) { data.horizontalSpan = colCount; mImageLabel.setLayoutData(data); Display display = shell.getDisplay(); - mImageLabel.setImage(ImageLoader.createPlaceHolderArt( - display, 50, 50, display.getSystemColor(SWT.COLOR_BLUE))); - - + ReplacementImageFactory replacementImageFactory = + new ReplacementImageFactory(display, 50, 50, display.getSystemColor(SWT.COLOR_BLUE)); + mImageLabel.setImage(replacementImageFactory.getImage()); shell.setDefaultButton(done); } @@ -244,9 +243,9 @@ private void updateImageDisplay(Shell shell) { Image image; if (mRawImage == null) { Display display = shell.getDisplay(); - image = ImageLoader.createPlaceHolderArt( - display, 320, 240, display.getSystemColor(SWT.COLOR_BLUE)); - + ReplacementImageFactory replacementImageFactory = + new ReplacementImageFactory(display, 320, 240, display.getSystemColor(SWT.COLOR_BLUE)); + image = replacementImageFactory.getImage(); mSave.setEnabled(false); mBusyLabel.setText("Screen not available"); } else { diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceExplorer.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceExplorer.java index cc602c4c..3781dbd5 100644 --- a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceExplorer.java +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/explorer/DeviceExplorer.java @@ -28,7 +28,6 @@ import com.android.ddmlib.SyncService.ISyncProgressMonitor; import com.android.ddmlib.TimeoutException; import com.android.ddmuilib.DdmUiPreferences; -import com.android.ddmuilib.ImageLoader; import com.android.ddmuilib.Panel; import com.android.ddmuilib.SyncProgressHelper; import com.android.ddmuilib.SyncProgressHelper.SyncRunnable; @@ -36,6 +35,7 @@ import com.android.ddmuilib.actions.ICommonAction; import com.android.ddmuilib.console.DdmConsole; +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.jface.dialogs.ErrorDialog; @@ -111,8 +111,10 @@ public class DeviceExplorer extends Panel { private IDevice mCurrentDevice; private String mDefaultSave; + private ImageFactory mImageFactory; - public DeviceExplorer() { + public DeviceExplorer(ImageFactory imageFactory) { + mImageFactory = imageFactory; } /** @@ -159,15 +161,14 @@ protected Control createControl(Composite parent) { mParent = parent; parent.setLayout(new FillLayout()); - ImageLoader loader = ImageLoader.getDdmUiLibLoader(); if (mFileImage == null) { - mFileImage = loader.loadImage("file.png", mParent.getDisplay()); + mFileImage = mImageFactory.getImageByName("file.png"); } if (mFolderImage == null) { - mFolderImage = loader.loadImage("folder.png", mParent.getDisplay()); + mFolderImage = mImageFactory.getImageByName("folder.png"); } if (mPackageImage == null) { - mPackageImage = loader.loadImage("android.png", mParent.getDisplay()); + mPackageImage = mImageFactory.getImageByName("android.png"); } if (mOtherImage == null) { // TODO: find a default image for other. diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapPanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapPanel.java index c845fc13..a11ef73b 100644 --- a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapPanel.java +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/heap/NativeHeapPanel.java @@ -25,9 +25,9 @@ import com.android.ddmuilib.BaseHeapPanel; import com.android.ddmuilib.ITableFocusListener; import com.android.ddmuilib.ITableFocusListener.IFocusedTableActivator; -import com.android.ddmuilib.ImageLoader; import com.android.ddmuilib.TableHelper; +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.dialogs.ProgressMonitorDialog; import org.eclipse.jface.preference.IPreferenceStore; @@ -145,9 +145,11 @@ public class NativeHeapPanel extends BaseHeapPanel { private ToolItem mDiffsOnlyButton; private ToolItem mShowZygoteAllocationsButton; private ToolItem mExportHeapDataButton; + private ImageFactory mImageFactory; - public NativeHeapPanel(IPreferenceStore prefStore) { + public NativeHeapPanel(IPreferenceStore prefStore, ImageFactory imageFactory) { mPrefStore = prefStore; + mImageFactory = imageFactory; mPrefStore.setDefault(PREFS_SASH_HEIGHT_PERCENT, 75); mPrefStore.setDefault(PREFS_SYMBOL_SEARCH_PATH, ""); mPrefStore.setDefault(PREFS_GROUP_BY_LIBRARY, false); @@ -757,8 +759,7 @@ public void handleEvent(Event e) { private void initializeDetailsToolBar(ToolBar toolbar) { mGroupByButton = new ToolItem(toolbar, SWT.CHECK); - mGroupByButton.setImage(ImageLoader.getDdmUiLibLoader().loadImage(GROUPBY_IMAGE, - toolbar.getDisplay())); + mGroupByButton.setImage(mImageFactory.getImageByName(GROUPBY_IMAGE)); mGroupByButton.setToolTipText(TOOLTIP_GROUPBY); mGroupByButton.setSelection(mPrefStore.getBoolean(PREFS_GROUP_BY_LIBRARY)); mGroupByButton.addSelectionListener(new SelectionAdapter() { @@ -769,8 +770,7 @@ public void widgetSelected(SelectionEvent arg0) { }); mDiffsOnlyButton = new ToolItem(toolbar, SWT.CHECK); - mDiffsOnlyButton.setImage(ImageLoader.getDdmUiLibLoader().loadImage(DIFFS_ONLY_IMAGE, - toolbar.getDisplay())); + mDiffsOnlyButton.setImage(mImageFactory.getImageByName(DIFFS_ONLY_IMAGE)); mDiffsOnlyButton.setToolTipText(TOOLTIP_DIFFS_ONLY); mDiffsOnlyButton.setSelection(mPrefStore.getBoolean(PREFS_SHOW_DIFFS_ONLY)); mDiffsOnlyButton.addSelectionListener(new SelectionAdapter() { @@ -784,8 +784,7 @@ public void widgetSelected(SelectionEvent arg0) { }); mShowZygoteAllocationsButton = new ToolItem(toolbar, SWT.CHECK); - mShowZygoteAllocationsButton.setImage(ImageLoader.getDdmUiLibLoader().loadImage( - ZYGOTE_IMAGE, toolbar.getDisplay())); + mShowZygoteAllocationsButton.setImage(mImageFactory.getImageByName(ZYGOTE_IMAGE)); mShowZygoteAllocationsButton.setToolTipText(TOOLTIP_ZYGOTE_ALLOCATIONS); mShowZygoteAllocationsButton.setSelection( mPrefStore.getBoolean(PREFS_SHOW_ZYGOTE_ALLOCATIONS)); @@ -797,8 +796,7 @@ public void widgetSelected(SelectionEvent arg0) { }); mExportHeapDataButton = new ToolItem(toolbar, SWT.PUSH); - mExportHeapDataButton.setImage(ImageLoader.getDdmUiLibLoader().loadImage( - EXPORT_DATA_IMAGE, toolbar.getDisplay())); + mExportHeapDataButton.setImage(mImageFactory.getImageByName(EXPORT_DATA_IMAGE)); mExportHeapDataButton.setToolTipText(TOOLTIP_EXPORT_DATA); mExportHeapDataButton.addSelectionListener(new SelectionAdapter() { @Override diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventDisplayOptions.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventDisplayOptions.java index 33cdc0e9..a9e7addb 100644 --- a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventDisplayOptions.java +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventDisplayOptions.java @@ -20,10 +20,10 @@ import com.android.ddmlib.log.EventLogParser; import com.android.ddmlib.log.EventValueDescription; import com.android.ddmuilib.DdmUiPreferences; -import com.android.ddmuilib.ImageLoader; import com.android.ddmuilib.log.event.EventDisplay.OccurrenceDisplayDescriptor; import com.android.ddmuilib.log.event.EventDisplay.ValueDisplayDescriptor; +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.swt.SWT; import org.eclipse.swt.events.ModifyEvent; @@ -112,9 +112,11 @@ private void setEnabled(boolean enable) { private boolean mProcessTextChanges = true; private Text mTimeLimitText; private Text mHistWidthText; + private ImageFactory mImageFactory; - EventDisplayOptions(Shell parent) { + EventDisplayOptions(Shell parent, ImageFactory imageFactory) { super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL); + mImageFactory = imageFactory; } /** @@ -273,10 +275,8 @@ public void widgetSelected(SelectionEvent e) { gl.verticalSpacing = 0; gl.horizontalSpacing = 0; - ImageLoader loader = ImageLoader.getDdmUiLibLoader(); mEventDisplayNewButton = new Button(bottomControls, SWT.PUSH | SWT.FLAT); - mEventDisplayNewButton.setImage(loader.loadImage("add.png", //$NON-NLS-1$ - leftPanel.getDisplay())); + mEventDisplayNewButton.setImage(mImageFactory.getImageByName("add.png")); //$NON-NLS-1$ mEventDisplayNewButton.setToolTipText("Adds a new event display"); mEventDisplayNewButton.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_CENTER)); mEventDisplayNewButton.addSelectionListener(new SelectionAdapter() { @@ -287,8 +287,7 @@ public void widgetSelected(SelectionEvent e) { }); mEventDisplayDeleteButton = new Button(bottomControls, SWT.PUSH | SWT.FLAT); - mEventDisplayDeleteButton.setImage(loader.loadImage("delete.png", //$NON-NLS-1$ - leftPanel.getDisplay())); + mEventDisplayDeleteButton.setImage(mImageFactory.getImageByName("delete.png")); //$NON-NLS-1$ mEventDisplayDeleteButton.setToolTipText("Deletes the selected event display"); mEventDisplayDeleteButton.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_CENTER)); mEventDisplayDeleteButton.addSelectionListener(new SelectionAdapter() { @@ -299,8 +298,7 @@ public void widgetSelected(SelectionEvent e) { }); mEventDisplayUpButton = new Button(bottomControls, SWT.PUSH | SWT.FLAT); - mEventDisplayUpButton.setImage(loader.loadImage("up.png", //$NON-NLS-1$ - leftPanel.getDisplay())); + mEventDisplayUpButton.setImage(mImageFactory.getImageByName("up.png")); //$NON-NLS-1$ mEventDisplayUpButton.setToolTipText("Moves the selected event display up"); mEventDisplayUpButton.addSelectionListener(new SelectionAdapter() { @Override @@ -327,8 +325,7 @@ public void widgetSelected(SelectionEvent e) { }); mEventDisplayDownButton = new Button(bottomControls, SWT.PUSH | SWT.FLAT); - mEventDisplayDownButton.setImage(loader.loadImage("down.png", //$NON-NLS-1$ - leftPanel.getDisplay())); + mEventDisplayDownButton.setImage(mImageFactory.getImageByName("down.png")); //$NON-NLS-1$ mEventDisplayDownButton.setToolTipText("Moves the selected event display down"); mEventDisplayDownButton.addSelectionListener(new SelectionAdapter() { @Override diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventLogPanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventLogPanel.java index 682971b5..28034760 100644 --- a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventLogPanel.java +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/log/event/EventLogPanel.java @@ -32,6 +32,7 @@ import com.android.ddmuilib.annotation.WorkerThread; import com.android.ddmuilib.log.event.EventDisplay.ILogColumnListener; +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.swt.SWT; import org.eclipse.swt.SWTException; @@ -108,8 +109,11 @@ public class EventLogPanel extends TablePanel implements ILogListener, /** file containing the current log raw data. */ private File mTempFile = null; - public EventLogPanel() { + private ImageFactory mImageFactory; + + public EventLogPanel(ImageFactory imageFactory) { super(); + mImageFactory = imageFactory; mFormatter.setGroupingUsed(true); } @@ -204,7 +208,7 @@ public void run() { @UiThread public void openOptionPanel() { try { - EventDisplayOptions dialog = new EventDisplayOptions(mParent.getShell()); + EventDisplayOptions dialog = new EventDisplayOptions(mParent.getShell(), mImageFactory); if (dialog.open(mCurrentEventLogParser, mEventDisplays, mEvents)) { synchronized (mLock) { // get the new EventDisplay list diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/EditFilterDialog.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/EditFilterDialog.java index 1b3da726..ce9bc2fc 100644 --- a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/EditFilterDialog.java +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/EditFilterDialog.java @@ -16,8 +16,7 @@ package com.android.ddmuilib.logcat; -import com.android.ddmuilib.ImageLoader; - +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.swt.SWT; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; @@ -74,13 +73,15 @@ public class EditFilterDialog extends Dialog { private Label mNameWarning; private Label mTagWarning; private Label mPidWarning; + private ImageFactory mImageFactory; - public EditFilterDialog(Shell parent) { + public EditFilterDialog(Shell parent, ImageFactory imageFactory) { super(parent, SWT.DIALOG_TRIM | SWT.BORDER | SWT.APPLICATION_MODAL); + mImageFactory = imageFactory; } - public EditFilterDialog(Shell shell, LogFilter filter) { - this(shell); + public EditFilterDialog(Shell shell, ImageFactory imageFactory, LogFilter filter) { + this(shell, imageFactory); mFilter = filter; } @@ -180,8 +181,7 @@ public void modifyText(ModifyEvent e) { }); mNameWarning = new Label(nameComposite, SWT.NONE); - mNameWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_EMPTY, - mShell.getDisplay())); + mNameWarning.setImage(mImageFactory.getImageByName(IMAGE_EMPTY)); // separator l = new Label(mShell, SWT.SEPARATOR | SWT.HORIZONTAL); @@ -214,8 +214,7 @@ public void modifyText(ModifyEvent e) { }); mTagWarning = new Label(main, SWT.NONE); - mTagWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_EMPTY, - mShell.getDisplay())); + mTagWarning.setImage(mImageFactory.getImageByName(IMAGE_EMPTY)); l = new Label(main, SWT.NONE); l.setText("by pid:"); @@ -239,8 +238,7 @@ public void modifyText(ModifyEvent e) { }); mPidWarning = new Label(main, SWT.NONE); - mPidWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_EMPTY, - mShell.getDisplay())); + mPidWarning.setImage(mImageFactory.getImageByName(IMAGE_EMPTY)); l = new Label(main, SWT.NONE); l.setText("by Log level:"); @@ -341,19 +339,15 @@ private int getComboIndex(int logLevel) { private void validate() { boolean result = true; - + // then we check it only contains digits. if (mPid != null) { if (mPid.matches("[0-9]*") == false) { //$NON-NLS-1$ - mPidWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage( - IMAGE_WARNING, - mShell.getDisplay())); + mPidWarning.setImage(mImageFactory.getImageByName(IMAGE_WARNING)); mPidWarning.setToolTipText("PID must be a number"); //$NON-NLS-1$ result = false; } else { - mPidWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage( - IMAGE_EMPTY, - mShell.getDisplay())); + mPidWarning.setImage(mImageFactory.getImageByName(IMAGE_EMPTY)); mPidWarning.setToolTipText(null); } } @@ -361,15 +355,11 @@ private void validate() { // then we check it not contains character | or : if (mTag != null) { if (mTag.matches(".*[:|].*") == true) { //$NON-NLS-1$ - mTagWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage( - IMAGE_WARNING, - mShell.getDisplay())); + mTagWarning.setImage(mImageFactory.getImageByName(IMAGE_WARNING)); mTagWarning.setToolTipText("Tag cannot contain | or :"); //$NON-NLS-1$ result = false; } else { - mTagWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage( - IMAGE_EMPTY, - mShell.getDisplay())); + mTagWarning.setImage(mImageFactory.getImageByName(IMAGE_EMPTY)); mTagWarning.setToolTipText(null); } } @@ -377,15 +367,11 @@ private void validate() { // then we check it not contains character | or : if (mName != null && mName.length() > 0) { if (mName.matches(".*[:|].*") == true) { //$NON-NLS-1$ - mNameWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage( - IMAGE_WARNING, - mShell.getDisplay())); + mNameWarning.setImage(mImageFactory.getImageByName(IMAGE_WARNING)); mNameWarning.setToolTipText("Name cannot contain | or :"); //$NON-NLS-1$ result = false; } else { - mNameWarning.setImage(ImageLoader.getDdmUiLibLoader().loadImage( - IMAGE_EMPTY, - mShell.getDisplay())); + mNameWarning.setImage(mImageFactory.getImageByName(IMAGE_EMPTY)); mNameWarning.setToolTipText(null); } } else { diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatPanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatPanel.java index 61c07dfd..02fa27b5 100644 --- a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatPanel.java +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogCatPanel.java @@ -25,10 +25,10 @@ import com.android.ddmuilib.FindDialog; import com.android.ddmuilib.ITableFocusListener; import com.android.ddmuilib.ITableFocusListener.IFocusedTableActivator; -import com.android.ddmuilib.ImageLoader; import com.android.ddmuilib.SelectionDependentPanel; import com.android.ddmuilib.TableHelper; +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.dialogs.MessageDialog; @@ -208,12 +208,15 @@ public final class LogCatPanel extends SelectionDependentPanel // # of messages deleted since last refresh, synchronized on mLogBuffer private int mDeletedLogCount; + private ImageFactory mImageFactory; + /** * Construct a logcat panel. * @param prefStore preference store where UI preferences will be saved */ - public LogCatPanel(IPreferenceStore prefStore) { + public LogCatPanel(IPreferenceStore prefStore, ImageFactory imageFactory) { mPrefStore = prefStore; + mImageFactory = imageFactory; mLogBuffer = new ArrayList(LogCatMessageList.MAX_MESSAGES_DEFAULT); initializeFilters(); @@ -440,7 +443,7 @@ private void createFiltersToolbar(Composite parent) { /* new filter */ mNewFilterToolItem = new ToolItem(t, SWT.PUSH); mNewFilterToolItem.setImage( - ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_ADD_FILTER, t.getDisplay())); + mImageFactory.getImageByName(IMAGE_ADD_FILTER)); mNewFilterToolItem.setToolTipText("Add a new logcat filter"); mNewFilterToolItem.addSelectionListener(new SelectionAdapter() { @Override @@ -452,7 +455,7 @@ public void widgetSelected(SelectionEvent arg0) { /* delete filter */ mDeleteFilterToolItem = new ToolItem(t, SWT.PUSH); mDeleteFilterToolItem.setImage( - ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_DELETE_FILTER, t.getDisplay())); + mImageFactory.getImageByName(IMAGE_DELETE_FILTER)); mDeleteFilterToolItem.setToolTipText("Delete selected logcat filter"); mDeleteFilterToolItem.addSelectionListener(new SelectionAdapter() { @Override @@ -464,7 +467,7 @@ public void widgetSelected(SelectionEvent arg0) { /* edit filter */ mEditFilterToolItem = new ToolItem(t, SWT.PUSH); mEditFilterToolItem.setImage( - ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_EDIT_FILTER, t.getDisplay())); + mImageFactory.getImageByName(IMAGE_EDIT_FILTER)); mEditFilterToolItem.setToolTipText("Edit selected logcat filter"); mEditFilterToolItem.addSelectionListener(new SelectionAdapter() { @Override @@ -671,8 +674,7 @@ public void widgetSelected(SelectionEvent arg0) { ToolBar toolBar = new ToolBar(c, SWT.FLAT); ToolItem saveToLog = new ToolItem(toolBar, SWT.PUSH); - saveToLog.setImage(ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_SAVE_LOG_TO_FILE, - toolBar.getDisplay())); + saveToLog.setImage(mImageFactory.getImageByName(IMAGE_SAVE_LOG_TO_FILE)); saveToLog.setToolTipText("Export Selected Items To Text File.."); saveToLog.addSelectionListener(new SelectionAdapter() { @Override @@ -683,7 +685,7 @@ public void widgetSelected(SelectionEvent arg0) { ToolItem clearLog = new ToolItem(toolBar, SWT.PUSH); clearLog.setImage( - ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_CLEAR_LOG, toolBar.getDisplay())); + mImageFactory.getImageByName(IMAGE_CLEAR_LOG)); clearLog.setToolTipText("Clear Log"); clearLog.addSelectionListener(new SelectionAdapter() { @Override @@ -701,8 +703,7 @@ public void widgetSelected(SelectionEvent arg0) { final ToolItem showFiltersColumn = new ToolItem(toolBar, SWT.CHECK); showFiltersColumn.setImage( - ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_DISPLAY_FILTERS, - toolBar.getDisplay())); + mImageFactory.getImageByName(IMAGE_DISPLAY_FILTERS)); showFiltersColumn.setSelection(mPrefStore.getBoolean(DISPLAY_FILTERS_COLUMN_PREFKEY)); showFiltersColumn.setToolTipText("Display Saved Filters View"); showFiltersColumn.addSelectionListener(new SelectionAdapter() { @@ -716,8 +717,7 @@ public void widgetSelected(SelectionEvent event) { mScrollLockCheckBox = new ToolItem(toolBar, SWT.CHECK); mScrollLockCheckBox.setImage( - ImageLoader.getDdmUiLibLoader().loadImage(IMAGE_SCROLL_LOCK, - toolBar.getDisplay())); + mImageFactory.getImageByName(IMAGE_SCROLL_LOCK)); mScrollLockCheckBox.setSelection(true); mScrollLockCheckBox.setToolTipText("Scroll Lock"); mScrollLockCheckBox.addSelectionListener(new SelectionAdapter() { diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogPanel.java b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogPanel.java index c4c6f8ab..207da3f6 100644 --- a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogPanel.java +++ b/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/com/android/ddmuilib/logcat/LogPanel.java @@ -30,6 +30,7 @@ import com.android.ddmuilib.TableHelper; import com.android.ddmuilib.actions.ICommonAction; +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.swt.SWT; import org.eclipse.swt.SWTException; @@ -197,6 +198,8 @@ protected static class LogMessageInfo { private LogCatViewInterface mLogCatViewInterface = null; + private ImageFactory mImageFactory; + /** message data, separated from content for multi line messages */ protected static class LogMessage { public LogMessageInfo data; @@ -332,10 +335,11 @@ public interface LogCatViewInterface { * @param mode The filtering mode */ public LogPanel(LogColors colors, - ILogFilterStorageManager filterStorage, int mode) { + ILogFilterStorageManager filterStorage, int mode, ImageFactory imageFactory) { mColors = colors; mFilterMode = mode; mFilterStorage = filterStorage; + mImageFactory = imageFactory; mStore = DdmUiPreferences.getStore(); } @@ -573,7 +577,7 @@ public void stopLogCat(boolean inUiThread) { * */ public void addFilter() { - EditFilterDialog dlg = new EditFilterDialog(mFolders.getShell()); + EditFilterDialog dlg = new EditFilterDialog(mFolders.getShell(), mImageFactory); if (dlg.open()) { synchronized (mBuffer) { // get the new filter in the array @@ -645,7 +649,7 @@ public void addFilter() { public void editFilter() { if (mCurrentFilter != null && mCurrentFilter != mDefaultFilter) { EditFilterDialog dlg = new EditFilterDialog( - mFolders.getShell(), mCurrentFilter); + mFolders.getShell(), mImageFactory, mCurrentFilter); if (dlg.open()) { synchronized (mBuffer) { // at this point the filter has been updated. diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/META-INF/MANIFEST.MF b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/META-INF/MANIFEST.MF index 8e2eace2..6e1641b1 100644 --- a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/META-INF/MANIFEST.MF +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/META-INF/MANIFEST.MF @@ -1,20 +1,22 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Localization: plugin -Bundle-Name: %Bundle-Name -Bundle-SymbolicName: org.eclipse.andmore.hierarchyviewer2lib;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Vendor: %Bundle-Vendor -Bundle-RequiredExecutionEnvironment: JavaSE-1.8 -Require-Bundle: org.eclipse.core.commands;bundle-version="3.8.1", - org.eclipse.equinox.common;bundle-version="3.8.0", - org.eclipse.jface;bundle-version="3.12.2", - org.eclipse.andmore.swt, - org.eclipse.andmore.swtmenubar, - org.eclipse.andmore.ddmuilib -Export-Package: com.android.hierarchyviewerlib, - com.android.hierarchyviewerlib.actions, - com.android.hierarchyviewerlib.device, - com.android.hierarchyviewerlib.models, - com.android.hierarchyviewerlib.ui -Bundle-ClassPath: . +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Localization: plugin +Bundle-Name: %Bundle-Name +Bundle-SymbolicName: org.eclipse.andmore.hierarchyviewer2lib;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Vendor: %Bundle-Vendor +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Require-Bundle: org.eclipse.core.commands;bundle-version="3.8.1", + org.eclipse.equinox.common;bundle-version="3.8.0", + org.eclipse.jface;bundle-version="3.12.2", + org.eclipse.andmore.swt, + org.eclipse.andmore.swtmenubar, + org.eclipse.andmore.ddmuilib, + org.eclipse.core.runtime;bundle-version="3.12.0" +Export-Package: com.android.hierarchyviewerlib, + com.android.hierarchyviewerlib.actions, + com.android.hierarchyviewerlib.device, + com.android.hierarchyviewerlib.models, + com.android.hierarchyviewerlib.ui +Bundle-ClassPath: . +Import-Package: org.eclipse.ui.plugin diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/plugin.xml b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/plugin.xml new file mode 100644 index 00000000..b60f62ae --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/plugin.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/HierarchyViewerDirector.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/HierarchyViewerDirector.java index ac4b5bb0..1a299ece 100644 --- a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/HierarchyViewerDirector.java +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/HierarchyViewerDirector.java @@ -38,6 +38,8 @@ import com.android.hierarchyviewerlib.ui.util.DrawableViewNode; import com.android.hierarchyviewerlib.ui.util.PsdFile; +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.andmore.base.resources.JFaceImageLoader; import org.eclipse.swt.SWT; import org.eclipse.swt.SWTException; import org.eclipse.swt.graphics.Image; @@ -68,6 +70,8 @@ public abstract class HierarchyViewerDirector implements IDeviceChangeListener, } protected static HierarchyViewerDirector sDirector; + + private ImageFactory mImageFactory; public static final String TAG = "hierarchyviewer"; @@ -88,6 +92,10 @@ public abstract class HierarchyViewerDirector implements IDeviceChangeListener, private static final Object mDevicesLock = new Object(); private Map mDevices = new HashMap(10); + public HierarchyViewerDirector(ImageFactory imageFactory) { + mImageFactory = imageFactory; + } + public static boolean isUsingDdmProtocol() { return sIsUsingDdmProtocol; } @@ -95,6 +103,7 @@ public static boolean isUsingDdmProtocol() { public void terminate() { WindowUpdater.terminate(); mPixelPerfectRefreshTimer.cancel(); + mImageFactory.dispose(); } public abstract String getAdbLocation(); @@ -788,4 +797,9 @@ public void setPixelPerfectAutoRefreshInterval(int value) { public int getPixelPerfectAutoRefreshInverval() { return mPixelPerfectAutoRefreshInterval; } + + public ImageFactory getImageFactory() { + return mImageFactory; + } + } diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/CapturePSDAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/CapturePSDAction.java index 4331c7d1..cc74a096 100644 --- a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/CapturePSDAction.java +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/CapturePSDAction.java @@ -16,13 +16,12 @@ package com.android.hierarchyviewerlib.actions; -import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; -import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; public class CapturePSDAction extends TreeViewEnabledAction implements ImageAction { @@ -37,8 +36,8 @@ private CapturePSDAction(Shell shell) { super("&Capture Layers"); this.mShell = shell; setAccelerator(SWT.MOD1 + 'C'); - ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); - mImage = imageLoader.loadImage("capture-psd.png", Display.getDefault()); //$NON-NLS-1$ + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mImage = imageFactory.getImageByName("capture-psd.png"); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Capture the window layers as a photoshop document"); } diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DisplayViewAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DisplayViewAction.java index 00976583..c020e813 100644 --- a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DisplayViewAction.java +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DisplayViewAction.java @@ -16,13 +16,12 @@ package com.android.hierarchyviewerlib.actions; -import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; -import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; public class DisplayViewAction extends SelectedNodeEnabledAction implements ImageAction { @@ -37,8 +36,8 @@ private DisplayViewAction(Shell shell) { super("&Display View"); this.mShell = shell; setAccelerator(SWT.MOD1 + 'D'); - ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); - mImage = imageLoader.loadImage("display.png", Display.getDefault()); //$NON-NLS-1$ + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mImage = imageFactory.getImageByName("display.png"); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Display the selected view image in a separate window"); } diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DumpDisplayListAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DumpDisplayListAction.java index 2dc42b18..74372c88 100644 --- a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DumpDisplayListAction.java +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/DumpDisplayListAction.java @@ -16,12 +16,11 @@ package com.android.hierarchyviewerlib.actions; -import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.graphics.Image; -import org.eclipse.swt.widgets.Display; public class DumpDisplayListAction extends SelectedNodeEnabledAction implements ImageAction { @@ -31,8 +30,8 @@ public class DumpDisplayListAction extends SelectedNodeEnabledAction implements private DumpDisplayListAction() { super("Dump DisplayList"); - ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); - mImage = imageLoader.loadImage("load-view-hierarchy.png", Display.getDefault()); //$NON-NLS-1$ + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mImage = imageFactory.getImageByName("load-view-hierarchy.png"); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Request the view to output its displaylist to logcat"); } diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/EvaluateContrastAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/EvaluateContrastAction.java index 1815bb9f..ad80a97b 100644 --- a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/EvaluateContrastAction.java +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/EvaluateContrastAction.java @@ -16,14 +16,11 @@ package com.android.hierarchyviewerlib.actions; -import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import org.eclipse.jface.action.Action; -import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; -import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; public class EvaluateContrastAction extends Action implements ImageAction { diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/InspectScreenshotAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/InspectScreenshotAction.java index 4ef1313a..71f857c2 100644 --- a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/InspectScreenshotAction.java +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/InspectScreenshotAction.java @@ -16,13 +16,13 @@ package com.android.hierarchyviewerlib.actions; -import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import com.android.hierarchyviewerlib.device.IHvDevice; import com.android.hierarchyviewerlib.models.DeviceSelectionModel; import com.android.hierarchyviewerlib.models.DeviceSelectionModel.IWindowChangeListener; import com.android.hierarchyviewerlib.models.Window; +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.jface.action.Action; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; @@ -38,8 +38,8 @@ public class InspectScreenshotAction extends Action implements ImageAction, IWin private InspectScreenshotAction() { super("Inspect &Screenshot"); setAccelerator(SWT.MOD1 + 'S'); - ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); - mImage = imageLoader.loadImage("inspect-screenshot.png", Display.getDefault()); //$NON-NLS-1$ + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mImage = imageFactory.getImageByName("inspect-screenshot.png"); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Inspect a screenshot in the pixel perfect view"); setEnabled( diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/InvalidateAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/InvalidateAction.java index d3de05e9..a6e45552 100644 --- a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/InvalidateAction.java +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/InvalidateAction.java @@ -16,13 +16,12 @@ package com.android.hierarchyviewerlib.actions; -import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; -import org.eclipse.swt.widgets.Display; public class InvalidateAction extends SelectedNodeEnabledAction implements ImageAction { @@ -33,8 +32,8 @@ public class InvalidateAction extends SelectedNodeEnabledAction implements Image private InvalidateAction() { super("&Invalidate Layout"); setAccelerator(SWT.MOD1 + 'I'); - ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); - mImage = imageLoader.loadImage("invalidate.png", Display.getDefault()); //$NON-NLS-1$ + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mImage = imageFactory.getImageByName("invalidate.png"); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Invalidate the layout for the current window"); } diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/LoadOverlayAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/LoadOverlayAction.java index 4d25d657..1ced3c7f 100644 --- a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/LoadOverlayAction.java +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/LoadOverlayAction.java @@ -16,13 +16,12 @@ package com.android.hierarchyviewerlib.actions; -import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; -import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; public class LoadOverlayAction extends PixelPerfectEnabledAction implements ImageAction { @@ -37,8 +36,8 @@ private LoadOverlayAction(Shell shell) { super("Load &Overlay"); this.mShell = shell; setAccelerator(SWT.MOD1 + 'O'); - ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); - mImage = imageLoader.loadImage("load-overlay.png", Display.getDefault()); //$NON-NLS-1$ + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mImage = imageFactory.getImageByName("load-overlay.png"); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Load an image to overlay the screenshot"); } diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/LoadViewHierarchyAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/LoadViewHierarchyAction.java index 81a159c1..b1384d2d 100644 --- a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/LoadViewHierarchyAction.java +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/LoadViewHierarchyAction.java @@ -16,13 +16,13 @@ package com.android.hierarchyviewerlib.actions; -import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import com.android.hierarchyviewerlib.device.IHvDevice; import com.android.hierarchyviewerlib.models.DeviceSelectionModel; import com.android.hierarchyviewerlib.models.DeviceSelectionModel.IWindowChangeListener; import com.android.hierarchyviewerlib.models.Window; +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.jface.action.Action; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; @@ -38,8 +38,8 @@ public class LoadViewHierarchyAction extends Action implements ImageAction, IWin private LoadViewHierarchyAction() { super("Load View &Hierarchy"); setAccelerator(SWT.MOD1 + 'H'); - ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); - mImage = imageLoader.loadImage("load-view-hierarchy.png", Display.getDefault()); //$NON-NLS-1$ + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mImage = imageFactory.getImageByName("load-view-hierarchy.png"); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Load the view hierarchy into the tree view"); setEnabled( diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/PixelPerfectAutoRefreshAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/PixelPerfectAutoRefreshAction.java index 8f337c06..cd8f36d2 100644 --- a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/PixelPerfectAutoRefreshAction.java +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/PixelPerfectAutoRefreshAction.java @@ -16,14 +16,13 @@ package com.android.hierarchyviewerlib.actions; -import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.jface.action.Action; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; -import org.eclipse.swt.widgets.Display; public class PixelPerfectAutoRefreshAction extends PixelPerfectEnabledAction implements ImageAction { @@ -34,8 +33,8 @@ public class PixelPerfectAutoRefreshAction extends PixelPerfectEnabledAction imp private PixelPerfectAutoRefreshAction() { super("Auto &Refresh", Action.AS_CHECK_BOX); setAccelerator(SWT.MOD1 + 'R'); - ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); - mImage = imageLoader.loadImage("auto-refresh.png", Display.getDefault()); //$NON-NLS-1$ + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mImage = imageFactory.getImageByName("auto-refresh.png"); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Automatically refresh the screenshot"); } diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/ProfileNodesAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/ProfileNodesAction.java index 79b08e2a..8e75d3f2 100644 --- a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/ProfileNodesAction.java +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/ProfileNodesAction.java @@ -16,12 +16,11 @@ package com.android.hierarchyviewerlib.actions; -import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.graphics.Image; -import org.eclipse.swt.widgets.Display; public class ProfileNodesAction extends SelectedNodeEnabledAction implements ImageAction { private static ProfileNodesAction sAction; @@ -30,8 +29,8 @@ public class ProfileNodesAction extends SelectedNodeEnabledAction implements Ima public ProfileNodesAction() { super("Profile Node"); - ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); - mImage = imageLoader.loadImage("profile.png", Display.getDefault()); //$NON-NLS-1$ + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mImage = imageFactory.getImageByName("profile.png"); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Obtain layout times for tree rooted at selected node"); } diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshPixelPerfectAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshPixelPerfectAction.java index 0c022857..c2edede8 100644 --- a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshPixelPerfectAction.java +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshPixelPerfectAction.java @@ -16,13 +16,12 @@ package com.android.hierarchyviewerlib.actions; -import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; -import org.eclipse.swt.widgets.Display; public class RefreshPixelPerfectAction extends PixelPerfectEnabledAction implements ImageAction { @@ -33,8 +32,8 @@ public class RefreshPixelPerfectAction extends PixelPerfectEnabledAction impleme private RefreshPixelPerfectAction() { super("&Refresh Screenshot"); setAccelerator(SWT.F5); - ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); - mImage = imageLoader.loadImage("refresh-windows.png", Display.getDefault()); //$NON-NLS-1$ + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mImage = imageFactory.getImageByName("refresh-windows.png"); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Refresh the screenshot"); } diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshPixelPerfectTreeAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshPixelPerfectTreeAction.java index 1823f480..bb1d5b18 100644 --- a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshPixelPerfectTreeAction.java +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshPixelPerfectTreeAction.java @@ -16,13 +16,12 @@ package com.android.hierarchyviewerlib.actions; -import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; -import org.eclipse.swt.widgets.Display; public class RefreshPixelPerfectTreeAction extends PixelPerfectEnabledAction implements ImageAction { @@ -33,8 +32,8 @@ public class RefreshPixelPerfectTreeAction extends PixelPerfectEnabledAction imp private RefreshPixelPerfectTreeAction() { super("Refresh &Tree"); setAccelerator(SWT.MOD1 + 'T'); - ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); - mImage = imageLoader.loadImage("load-view-hierarchy.png", Display.getDefault()); //$NON-NLS-1$ + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mImage = imageFactory.getImageByName("load-view-hierarchy.png"); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Refresh the tree"); } diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshViewAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshViewAction.java index e9b35e5a..2eb65783 100644 --- a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshViewAction.java +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshViewAction.java @@ -16,13 +16,12 @@ package com.android.hierarchyviewerlib.actions; -import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; -import org.eclipse.swt.widgets.Display; public class RefreshViewAction extends TreeViewEnabledAction implements ImageAction { @@ -33,8 +32,8 @@ public class RefreshViewAction extends TreeViewEnabledAction implements ImageAct private RefreshViewAction() { super("Load View &Hierarchy"); setAccelerator(SWT.MOD1 + 'H'); - ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); - mImage = imageLoader.loadImage("load-view-hierarchy.png", Display.getDefault()); //$NON-NLS-1$ + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mImage = imageFactory.getImageByName("load-view-hierarchy.png"); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Reload the view hierarchy"); } diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshWindowsAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshWindowsAction.java index ae6754da..82c5237a 100644 --- a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshWindowsAction.java +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RefreshWindowsAction.java @@ -16,14 +16,13 @@ package com.android.hierarchyviewerlib.actions; -import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.jface.action.Action; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; -import org.eclipse.swt.widgets.Display; public class RefreshWindowsAction extends Action implements ImageAction { @@ -34,8 +33,8 @@ public class RefreshWindowsAction extends Action implements ImageAction { private RefreshWindowsAction() { super("&Refresh"); setAccelerator(SWT.F5); - ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); - mImage = imageLoader.loadImage("refresh-windows.png", Display.getDefault()); //$NON-NLS-1$ + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mImage = imageFactory.getImageByName("refresh-windows.png"); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Refresh the list of devices"); } diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RequestLayoutAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RequestLayoutAction.java index 7621a7c5..a15da7fd 100644 --- a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RequestLayoutAction.java +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/RequestLayoutAction.java @@ -16,13 +16,12 @@ package com.android.hierarchyviewerlib.actions; -import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; -import org.eclipse.swt.widgets.Display; public class RequestLayoutAction extends SelectedNodeEnabledAction implements ImageAction { @@ -33,8 +32,8 @@ public class RequestLayoutAction extends SelectedNodeEnabledAction implements Im private RequestLayoutAction() { super("Request &Layout"); setAccelerator(SWT.MOD1 + 'L'); - ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); - mImage = imageLoader.loadImage("request-layout.png", Display.getDefault()); //$NON-NLS-1$ + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mImage = imageFactory.getImageByName("request-layout.png"); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Request the view to lay out"); } diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SavePixelPerfectAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SavePixelPerfectAction.java index 555ac5b4..1bcdc450 100644 --- a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SavePixelPerfectAction.java +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SavePixelPerfectAction.java @@ -16,13 +16,12 @@ package com.android.hierarchyviewerlib.actions; -import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; -import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; public class SavePixelPerfectAction extends PixelPerfectEnabledAction implements ImageAction { @@ -37,8 +36,8 @@ private SavePixelPerfectAction(Shell shell) { super("&Save as PNG"); this.mShell = shell; setAccelerator(SWT.MOD1 + 'S'); - ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); - mImage = imageLoader.loadImage("save.png", Display.getDefault()); //$NON-NLS-1$ + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mImage = imageFactory.getImageByName("save.png"); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Save the screenshot as a PNG image"); } diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SaveTreeViewAction.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SaveTreeViewAction.java index daefd309..c8c3a036 100644 --- a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SaveTreeViewAction.java +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/actions/SaveTreeViewAction.java @@ -16,13 +16,12 @@ package com.android.hierarchyviewerlib.actions; -import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; -import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; public class SaveTreeViewAction extends TreeViewEnabledAction implements ImageAction { @@ -37,8 +36,8 @@ private SaveTreeViewAction(Shell shell) { super("&Save as PNG"); this.mShell = shell; setAccelerator(SWT.MOD1 + 'S'); - ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); - mImage = imageLoader.loadImage("save.png", Display.getDefault()); //$NON-NLS-1$ + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mImage = imageFactory.getImageByName("save.png"); //$NON-NLS-1$ setImageDescriptor(ImageDescriptor.createFromImage(mImage)); setToolTipText("Save the tree view as a PNG image"); } diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/CaptureDisplay.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/CaptureDisplay.java index a3953fd3..67911dcc 100644 --- a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/CaptureDisplay.java +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/CaptureDisplay.java @@ -16,10 +16,10 @@ package com.android.hierarchyviewerlib.ui; -import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import com.android.hierarchyviewerlib.models.ViewNode; +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.swt.SWT; import org.eclipse.swt.events.PaintEvent; import org.eclipse.swt.events.PaintListener; @@ -121,8 +121,8 @@ private static void createShell() { sShell.addShellListener(sShellListener); - ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); - Image image = imageLoader.loadImage("display.png", Display.getDefault()); //$NON-NLS-1$ + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + Image image = imageFactory.getImageByName("display.png"); //$NON-NLS-1$ sShell.setImage(image); } diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DeviceSelector.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DeviceSelector.java index 567f5dd6..116f5d4a 100644 --- a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DeviceSelector.java +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/DeviceSelector.java @@ -16,13 +16,13 @@ package com.android.hierarchyviewerlib.ui; -import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import com.android.hierarchyviewerlib.device.IHvDevice; import com.android.hierarchyviewerlib.models.DeviceSelectionModel; import com.android.hierarchyviewerlib.models.DeviceSelectionModel.IWindowChangeListener; import com.android.hierarchyviewerlib.models.Window; +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.jface.viewers.IFontProvider; import org.eclipse.jface.viewers.ILabelProvider; import org.eclipse.jface.viewers.ILabelProviderListener; @@ -60,7 +60,7 @@ public class DeviceSelector extends Composite implements IWindowChangeListener, private Image mEmulatorImage; - private final static int ICON_WIDTH = 16; + // private final static int ICON_WIDTH = 16; private boolean mDoTreeViewStuff; @@ -203,14 +203,12 @@ public void loadResources() { } mBoldFont = new Font(Display.getDefault(), newFontData); - ImageLoader loader = ImageLoader.getDdmUiLibLoader(); + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); mDeviceImage = - loader.loadImage(display, "device.png", ICON_WIDTH, ICON_WIDTH, display //$NON-NLS-1$ - .getSystemColor(SWT.COLOR_RED)); + imageFactory.getImageByName("device.png"); mEmulatorImage = - loader.loadImage(display, "emulator.png", ICON_WIDTH, ICON_WIDTH, display //$NON-NLS-1$ - .getSystemColor(SWT.COLOR_BLUE)); + imageFactory.getImageByName("emulator.png"); } private DisposeListener mDisposeListener = new DisposeListener() { diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/EvaluateContrastDisplay.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/EvaluateContrastDisplay.java index 84170ed5..0d1b191d 100644 --- a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/EvaluateContrastDisplay.java +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/EvaluateContrastDisplay.java @@ -16,19 +16,17 @@ package com.android.hierarchyviewerlib.ui; -import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import com.android.hierarchyviewerlib.models.EvaluateContrastModel; import com.android.hierarchyviewerlib.models.ViewNode; import com.android.hierarchyviewerlib.models.EvaluateContrastModel.ContrastResult; import com.android.hierarchyviewerlib.models.ViewNode.Property; +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.ScrolledComposite; import org.eclipse.swt.events.PaintEvent; import org.eclipse.swt.events.PaintListener; -import org.eclipse.swt.events.SelectionEvent; -import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.events.ShellAdapter; import org.eclipse.swt.events.ShellEvent; import org.eclipse.swt.graphics.GC; @@ -39,14 +37,11 @@ import org.eclipse.swt.layout.FillLayout; 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.Canvas; 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.ScrollBar; import org.eclipse.swt.widgets.Shell; @@ -106,10 +101,10 @@ public class EvaluateContrastDisplay { sImageForColor = new HashMap(); sViewNodeForTreeItem = new HashMap(); - ImageLoader loader = ImageLoader.getLoader(EvaluateContrastDisplay.class); - sYellowImage = loader.loadImage("yellow.png", Display.getDefault()); - sRedImage = loader.loadImage("red.png", Display.getDefault()); - sGreenImage = loader.loadImage("green.png", Display.getDefault()); + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + sYellowImage = imageFactory.getImageByName("yellow.png"); + sRedImage = imageFactory.getImageByName("red.png"); + sGreenImage = imageFactory.getImageByName("green.png"); sRectangleForViewNode = new HashMap(); sBorderColorForViewNode = new HashMap(); diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectTree.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectTree.java index 769fc013..a73f900f 100644 --- a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectTree.java +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PixelPerfectTree.java @@ -16,11 +16,12 @@ package com.android.hierarchyviewerlib.ui; -import com.android.ddmuilib.ImageLoader; +import com.android.hierarchyviewerlib.HierarchyViewerDirector; import com.android.hierarchyviewerlib.models.PixelPerfectModel; import com.android.hierarchyviewerlib.models.ViewNode; import com.android.hierarchyviewerlib.models.PixelPerfectModel.IImageChangeListener; +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.jface.viewers.ILabelProvider; import org.eclipse.jface.viewers.ILabelProviderListener; import org.eclipse.jface.viewers.ITreeContentProvider; @@ -161,9 +162,9 @@ public PixelPerfectTree(Composite parent) { } private void loadResources() { - ImageLoader loader = ImageLoader.getDdmUiLibLoader(); - mFileImage = loader.loadImage("file.png", Display.getDefault()); - mFolderImage = loader.loadImage("folder.png", Display.getDefault()); + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mFileImage = imageFactory.getImageByName("file.png"); + mFolderImage = imageFactory.getImageByName("folder.png"); } private DisposeListener mDisposeListener = new DisposeListener() { diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PropertyViewer.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PropertyViewer.java index bf0b6541..08efbe30 100644 --- a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PropertyViewer.java +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/PropertyViewer.java @@ -18,7 +18,6 @@ import com.android.annotations.NonNull; import com.android.annotations.VisibleForTesting; -import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import com.android.hierarchyviewerlib.device.IHvDevice; import com.android.hierarchyviewerlib.models.TreeViewModel; @@ -29,6 +28,7 @@ import com.android.hierarchyviewerlib.ui.util.DrawableViewNode; import com.android.hierarchyviewerlib.ui.util.TreeColumnResizer; +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.jface.viewers.CellEditor; import org.eclipse.jface.viewers.ColumnViewer; import org.eclipse.jface.viewers.ComboBoxCellEditor; @@ -343,8 +343,8 @@ public PropertyViewer(Composite parent) { addControlListener(mControlListener); - ImageLoader imageLoader = ImageLoader.getLoader(HierarchyViewerDirector.class); - mImage = imageLoader.loadImage("picker.png", Display.getDefault()); //$NON-NLS-1$ + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + mImage = imageFactory.getImageByName("picker.png"); //$NON-NLS-1$ treeChanged(); } diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeView.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeView.java index c3798cbc..4fa5691f 100644 --- a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeView.java +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeView.java @@ -16,7 +16,6 @@ package com.android.hierarchyviewerlib.ui; -import com.android.ddmuilib.ImageLoader; import com.android.hierarchyviewerlib.HierarchyViewerDirector; import com.android.hierarchyviewerlib.models.TreeViewModel; import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener; @@ -25,6 +24,7 @@ import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Point; import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Rectangle; +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; @@ -178,14 +178,14 @@ public void keyTraversed(TraverseEvent e) { } private void loadResources() { - ImageLoader loader = ImageLoader.getLoader(this.getClass()); - sRedImage = loader.loadImage("red.png", Display.getDefault()); //$NON-NLS-1$ - sYellowImage = loader.loadImage("yellow.png", Display.getDefault()); //$NON-NLS-1$ - sGreenImage = loader.loadImage("green.png", Display.getDefault()); //$NON-NLS-1$ - sNotSelectedImage = loader.loadImage("not-selected.png", Display.getDefault()); //$NON-NLS-1$ - sSelectedImage = loader.loadImage("selected.png", Display.getDefault()); //$NON-NLS-1$ - sFilteredImage = loader.loadImage("filtered.png", Display.getDefault()); //$NON-NLS-1$ - sFilteredSelectedImage = loader.loadImage("selected-filtered.png", Display.getDefault()); //$NON-NLS-1$ + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + sRedImage = imageFactory.getImageByName("red.png"); //$NON-NLS-1$ + sYellowImage = imageFactory.getImageByName("yellow.png"); //$NON-NLS-1$ + sGreenImage = imageFactory.getImageByName("green.png"); //$NON-NLS-1$ + sNotSelectedImage = imageFactory.getImageByName("not-selected.png"); //$NON-NLS-1$ + sSelectedImage = imageFactory.getImageByName("selected.png"); //$NON-NLS-1$ + sFilteredImage = imageFactory.getImageByName("filtered.png"); //$NON-NLS-1$ + sFilteredSelectedImage = imageFactory.getImageByName("selected-filtered.png"); //$NON-NLS-1$ mBoxColor = new Color(Display.getDefault(), new RGB(225, 225, 225)); mTextBackgroundColor = new Color(Display.getDefault(), new RGB(82, 82, 82)); if (mScaledSelectedImage != null) { diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeViewOverview.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeViewOverview.java index 1bb5594f..87181a3c 100644 --- a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeViewOverview.java +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/com/android/hierarchyviewerlib/ui/TreeViewOverview.java @@ -16,13 +16,14 @@ package com.android.hierarchyviewerlib.ui; -import com.android.ddmuilib.ImageLoader; +import com.android.hierarchyviewerlib.HierarchyViewerDirector; import com.android.hierarchyviewerlib.models.TreeViewModel; import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener; import com.android.hierarchyviewerlib.ui.util.DrawableViewNode; import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Point; import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Rectangle; +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; @@ -90,12 +91,12 @@ public TreeViewOverview(Composite parent) { } private void loadResources() { - ImageLoader loader = ImageLoader.getLoader(this.getClass()); - sNotSelectedImage = loader.loadImage("not-selected.png", Display.getDefault()); //$NON-NLS-1$ - sSelectedImage = loader.loadImage("selected-small.png", Display.getDefault()); //$NON-NLS-1$ - sFilteredImage = loader.loadImage("filtered.png", Display.getDefault()); //$NON-NLS-1$ + ImageFactory imageFactory = HierarchyViewerDirector.getDirector().getImageFactory(); + sNotSelectedImage = imageFactory.getImageByName("not-selected.png"); //$NON-NLS-1$ + sSelectedImage = imageFactory.getImageByName("selected-small.png"); //$NON-NLS-1$ + sFilteredImage = imageFactory.getImageByName("filtered.png"); //$NON-NLS-1$ sFilteredSelectedImage = - loader.loadImage("selected-filtered-small.png", Display.getDefault()); //$NON-NLS-1$ + imageFactory.getImageByName("selected-filtered-small.png"); //$NON-NLS-1$ } private DisposeListener mDisposeListener = new DisposeListener() { diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/META-INF/MANIFEST.MF b/andmore-swt/org.eclipse.andmore.sdkuilib/META-INF/MANIFEST.MF index fa75ea08..c75d3709 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/META-INF/MANIFEST.MF +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/META-INF/MANIFEST.MF @@ -1,28 +1,29 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Localization: plugin -Bundle-Name: %Bundle-Name -Bundle-SymbolicName: org.eclipse.andmore.sdkuilib;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Vendor: %Bundle-Vendor -Bundle-RequiredExecutionEnvironment: JavaSE-1.8 -Require-Bundle: org.eclipse.core.commands;bundle-version="3.8.1", - org.eclipse.equinox.common;bundle-version="3.8.0", - org.eclipse.jface;bundle-version="3.12.2", - org.apache.httpcomponents.httpclient;bundle-version="4.1.3", - org.apache.httpcomponents.httpcore;bundle-version="4.1.4", - org.eclipse.andmore.swt, - org.eclipse.andmore.ddmuilib, - org.eclipse.andmore.swtmenubar, - com.google.gson;bundle-version="2.2.4" -Bundle-ClassPath: ., - libs/commons-compress-1.8.1.jar -Export-Package: com.android.sdkuilib.internal.repository, - com.android.sdkuilib.internal.repository.core, - com.android.sdkuilib.internal.repository.icons, - com.android.sdkuilib.internal.repository.ui, - com.android.sdkuilib.internal.tasks, - com.android.sdkuilib.internal.widgets, - com.android.sdkuilib.repository, - com.android.sdkuilib.ui -Import-Package: com.android.sdklib.repository +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Localization: plugin +Bundle-Name: %Bundle-Name +Bundle-SymbolicName: org.eclipse.andmore.sdkuilib;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Vendor: %Bundle-Vendor +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Require-Bundle: org.eclipse.core.commands;bundle-version="3.8.1", + org.eclipse.equinox.common;bundle-version="3.8.0", + org.eclipse.jface;bundle-version="3.12.2", + org.apache.httpcomponents.httpclient;bundle-version="4.1.3", + org.apache.httpcomponents.httpcore;bundle-version="4.1.4", + org.eclipse.andmore.swt, + org.eclipse.andmore.ddmuilib, + org.eclipse.andmore.swtmenubar, + com.google.gson;bundle-version="2.2.4", + org.eclipse.core.runtime;bundle-version="3.12.0", + org.eclipse.andmore.ddms;bundle-version="0.5.2" +Bundle-ClassPath: ., + libs/commons-compress-1.8.1.jar +Export-Package: com.android.sdkuilib.repository, + com.android.sdkuilib.ui, + com.android.sdkuilib.widgets, + org.eclipse.andmore.sdktool +Import-Package: com.android.sdklib.repository, + org.eclipse.ui.plugin +Bundle-ActivationPolicy: lazy +Bundle-Activator: org.eclipse.andmore.sdktool.SdkUserInterfacePlugin diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/build.properties b/andmore-swt/org.eclipse.andmore.sdkuilib/build.properties index 70d2e7a7..93e4b252 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/build.properties +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/build.properties @@ -3,5 +3,6 @@ output.. = bin/ bin.includes = META-INF/,\ .,\ libs/,\ - plugin.properties + plugin.properties,\ + icons/ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/accept_icon16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/accept_icon16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/accept_icon16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/accept_icon16.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/addon_pkg_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/addon_pkg_16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/addon_pkg_16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/addon_pkg_16.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/android_icon_128.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/android_icon_128.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/android_icon_128.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/android_icon_128.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/android_icon_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/android_icon_16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/android_icon_16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/android_icon_16.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/archive_icon16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/archive_icon16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/archive_icon16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/archive_icon16.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/broken_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/broken_16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/broken_16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/broken_16.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/broken_pkg_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/broken_pkg_16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/broken_pkg_16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/broken_pkg_16.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/buildtool_pkg_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/buildtool_pkg_16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/buildtool_pkg_16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/buildtool_pkg_16.png diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/device.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/device.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/device.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/device.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/doc_pkg_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/doc_pkg_16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/doc_pkg_16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/doc_pkg_16.png diff --git a/andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/emulator.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/emulator.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.ddmuilib/src/main/java/images/emulator.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/emulator.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/error_icon_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/error_icon_16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/error_icon_16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/error_icon_16.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/extra_pkg_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/extra_pkg_16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/extra_pkg_16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/extra_pkg_16.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/incompat_icon16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/incompat_icon16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/incompat_icon16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/incompat_icon16.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/log_off_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/log_off_16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/log_off_16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/log_off_16.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/log_on_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/log_on_16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/log_on_16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/log_on_16.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/nopkg_icon_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/nopkg_icon_16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/nopkg_icon_16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/nopkg_icon_16.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkg_incompat_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/pkg_incompat_16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkg_incompat_16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/pkg_incompat_16.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkg_installed_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/pkg_installed_16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkg_installed_16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/pkg_installed_16.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkg_new_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/pkg_new_16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkg_new_16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/pkg_new_16.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkg_update_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/pkg_update_16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkg_update_16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/pkg_update_16.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkgcat_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/pkgcat_16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkgcat_16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/pkgcat_16.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkgcat_other_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/pkgcat_other_16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/pkgcat_other_16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/pkgcat_other_16.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/platform_pkg_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/platform_pkg_16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/platform_pkg_16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/platform_pkg_16.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/platformtool_pkg_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/platformtool_pkg_16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/platformtool_pkg_16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/platformtool_pkg_16.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/reject_icon16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/reject_icon16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/reject_icon16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/reject_icon16.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/sample_pkg_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/sample_pkg_16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/sample_pkg_16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/sample_pkg_16.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/sdkman_logo_128.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/sdkman_logo_128.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/sdkman_logo_128.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/sdkman_logo_128.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/source_pkg_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/source_pkg_16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/source_pkg_16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/source_pkg_16.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/status_ok_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/status_ok_16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/status_ok_16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/status_ok_16.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/stop_disabled_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/stop_disabled_16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/stop_disabled_16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/stop_disabled_16.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/stop_enabled_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/stop_enabled_16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/stop_enabled_16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/stop_enabled_16.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/sysimg_pkg_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/sysimg_pkg_16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/sysimg_pkg_16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/sysimg_pkg_16.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/tag_android-tv_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/tag_android-tv_16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/tag_android-tv_16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/tag_android-tv_16.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/tag_android-tv_32.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/tag_android-tv_32.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/tag_android-tv_32.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/tag_android-tv_32.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/tag_android-wear_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/tag_android-wear_16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/tag_android-wear_16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/tag_android-wear_16.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/tag_android-wear_32.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/tag_android-wear_32.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/tag_android-wear_32.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/tag_android-wear_32.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/tag_default_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/tag_default_16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/tag_default_16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/tag_default_16.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/tag_default_32.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/tag_default_32.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/tag_default_32.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/tag_default_32.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/tool_pkg_16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/tool_pkg_16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/tool_pkg_16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/tool_pkg_16.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/unknown_icon16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/unknown_icon16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/unknown_icon16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/unknown_icon16.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/warning_icon16.png b/andmore-swt/org.eclipse.andmore.sdkuilib/icons/warning_icon16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/warning_icon16.png rename to andmore-swt/org.eclipse.andmore.sdkuilib/icons/warning_icon16.png diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/libs/.gitignore b/andmore-swt/org.eclipse.andmore.sdkuilib/libs/.gitignore new file mode 100644 index 00000000..cfa4c651 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/libs/.gitignore @@ -0,0 +1 @@ +/commons-compress-1.8.1.jar diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/plugin.xml b/andmore-swt/org.eclipse.andmore.sdkuilib/plugin.xml new file mode 100644 index 00000000..b60f62ae --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/plugin.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/pom.xml b/andmore-swt/org.eclipse.andmore.sdkuilib/pom.xml index 803cf57a..02b253c6 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/pom.xml +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/pom.xml @@ -13,6 +13,11 @@ 0.5.2-SNAPSHOT
+ + org.apache.commons + commons-compress + 1.8.1 + junit junit @@ -77,6 +82,33 @@ + + org.apache.maven.plugins + maven-dependency-plugin + + + copy + initialize + + copy + + + + + org.apache.commons + commons-compress + 1.8.1 + + + ${project.basedir}/libs + false + false + true + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/AndroidTargetHash.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/AndroidTargetHash.java deleted file mode 100644 index e4520beb..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/AndroidTargetHash.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright (C) 2013 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.sdklib; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.sdklib.repository.descriptors.IdDisplay; -import com.google.common.base.Splitter; - -import java.util.List; - - -/** - * Helper methods to manipulate hash strings used by {@link IAndroidTarget#hashString()}. - */ -public abstract class AndroidTargetHash { - - /** - * Prefix used to build hash strings for platform targets - * @see SdkManager#getTargetFromHashString(String) - */ - public static final String PLATFORM_HASH_PREFIX = "android-"; - - /** - * String to compute hash for add-on targets.
- * Format is {@code vendor:name:apiVersion}.
- * - * Important: the vendor and name compontents are the display strings, not the - * newer id strings. - */ - public static final String ADD_ON_FORMAT = "%s:%s:%s"; //$NON-NLS-1$ - - /** - * String used to get a hash to the platform target. - * This format is compatible with the PlatformPackage.installId(). - */ - static final String PLATFORM_HASH = PLATFORM_HASH_PREFIX + "%s"; - - /** - * Returns the hash string for a given platform version. - * - * @param version A non-null platform version. - * @return A non-null hash string uniquely representing this platform target. - */ - @NonNull - public static String getPlatformHashString(@NonNull AndroidVersion version) { - return String.format(AndroidTargetHash.PLATFORM_HASH, version.getApiString()); - } - - /** - * Returns the {@link AndroidVersion} for the given hash string, - * if it represents a platform. If the hash string represents a preview platform, - * the returned {@link AndroidVersion} will have an unknown API level (set to 1 - * or a known matching API level.) - * - * @param hashString the hash string (e.g. "android-19" or "android-CUPCAKE") - * or a pure API level for convenience (e.g. "19" instead of the proper "android-19") - * @return a platform, or null - */ - @Nullable - public static AndroidVersion getPlatformVersion(@NonNull String hashString) { - if (hashString.startsWith(PLATFORM_HASH_PREFIX)) { - String suffix = hashString.substring(PLATFORM_HASH_PREFIX.length()); - if (!suffix.isEmpty()) { - if (Character.isDigit(suffix.charAt(0))) { - try { - int api = Integer.parseInt(suffix); - return new AndroidVersion(api, null); - } catch (NumberFormatException ignore) {} - } else { - int api = SdkVersionInfo.getApiByBuildCode(suffix, false); - if (api < 1) { - api = 1; - } - return new AndroidVersion(api, suffix); - } - } - } else if (!hashString.isEmpty() && Character.isDigit(hashString.charAt(0))) { - // For convenience, interpret a single integer as the proper "android-NN" form. - try { - int api = Integer.parseInt(hashString); - return new AndroidVersion(api, null); - } catch (NumberFormatException ignore) {} - } - - return null; - } - - @Nullable - public static AndroidVersion getAddOnVersion(@NonNull String hashString) { - List parts = Splitter.on(':').splitToList(hashString); - if (parts.size() != 3) { - return null; - } - - String apiLevelPart = parts.get(2); - try { - int apiLevel = Integer.parseInt(apiLevelPart); - return new AndroidVersion(apiLevel, null); - } catch (NumberFormatException e) { - return null; - } - } - - /** - * Gets the API level from a hash string, either a platform version or add-on version. - * - * @see #getAddOnVersion(String) - * @see #getPlatformVersion(String) - */ - @Nullable - public static AndroidVersion getVersionFromHash(@NonNull String hashString) { - if (isPlatform(hashString)) { - return getPlatformVersion(hashString); - } else { - return getAddOnVersion(hashString); - } - } - - /** - * Returns the hash string for a given add-on. - * - * @param addonVendorDisplay A non-null vendor. When using an {@link IdDisplay} source, - * this parameter should be the {@link IdDisplay#getDisplay()}. - * @param addonNameDisplay A non-null add-on name. When using an {@link IdDisplay} source, - * this parameter should be the {@link IdDisplay#getDisplay()}. - * @param version A non-null platform version (the addon's base platform version) - * @return A non-null hash string uniquely representing this add-on target. - */ - public static String getAddonHashString( - @NonNull String addonVendorDisplay, - @NonNull String addonNameDisplay, - @NonNull AndroidVersion version) { - return String.format(ADD_ON_FORMAT, - addonVendorDisplay, - addonNameDisplay, - version.getApiString()); - } - - /** - * Returns the hash string for a given target (add-on or platform.) - * - * @param target A non-null target. - * @return A non-null hash string uniquely representing this target. - */ - public static String getTargetHashString(@NonNull IAndroidTarget target) { - if (target.isPlatform()) { - return getPlatformHashString(target.getVersion()); - } else { - return getAddonHashString( - target.getVendor(), - target.getName(), - target.getVersion()); - } - } - - /** - * Given a hash string, indicates whether this is a platform hash string. - * If not, it's an addon hash string. - * - * @param hashString The hash string to test. - * @return True if this hash string starts by the platform prefix. - */ - public static boolean isPlatform(@NonNull String hashString) { - return hashString.startsWith(PLATFORM_HASH_PREFIX); - } - -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/AndroidVersion.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/AndroidVersion.java deleted file mode 100644 index 49dffbc1..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/AndroidVersion.java +++ /dev/null @@ -1,392 +0,0 @@ -/* - * 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.sdklib; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.sdklib.repository.PkgProps; - -import java.util.Properties; -import java.util.regex.Pattern; - -/** - * Represents the version of a target or device. - *

- * A version is defined by an API level and an optional code name. - *

  • Release versions of the Android platform are identified by their API level (integer), - * (technically the code name for release version is "REL" but this class will return - * null instead.)
  • - *
  • Preview versions of the platform are identified by a code name. Their API level - * is usually set to the value of the previous platform.
- *

- * While this class contains both values, its goal is to abstract them, so that code comparing 2+ - * versions doesn't have to deal with the logic of handle both values. - *

- * There are some cases where ones may want to access the values directly. This can be done - * with {@link #getApiLevel()} and {@link #getCodename()}. - *

- * For generic UI display of the API version, {@link #getApiString()} is to be used. - */ -public final class AndroidVersion implements Comparable { - - private final int mApiLevel; - private final String mCodename; - - /** The default AndroidVersion for minSdkVersion and targetSdkVersion if not specified */ - public static final AndroidVersion DEFAULT = new AndroidVersion(1, null); - - /** - * Thrown when an {@link AndroidVersion} object could not be created. - * @see AndroidVersion#AndroidVersion(Properties) - */ - public static final class AndroidVersionException extends Exception { - private static final long serialVersionUID = 1L; - - AndroidVersionException(String message, Throwable cause) { - super(message, cause); - } - } - - /** - * Creates an {@link AndroidVersion} with the given api level and codename. - * Codename should be null for a release version, otherwise it's a preview codename. - */ - public AndroidVersion(int apiLevel, @Nullable String codename) { - mApiLevel = apiLevel; - mCodename = sanitizeCodename(codename); - } - - /** - * Creates an {@link AndroidVersion} from {@link Properties}, with default values if the - * {@link Properties} object doesn't contain the expected values. - *

The {@link Properties} is expected to have been filled with - * {@link #saveProperties(Properties)}. - */ - public AndroidVersion(@Nullable Properties properties, - int defaultApiLevel, - @Nullable String defaultCodeName) { - if (properties == null) { - mApiLevel = defaultApiLevel; - mCodename = sanitizeCodename(defaultCodeName); - } else { - mApiLevel = Integer.parseInt(properties.getProperty(PkgProps.VERSION_API_LEVEL, - Integer.toString(defaultApiLevel))); - mCodename = sanitizeCodename( - properties.getProperty(PkgProps.VERSION_CODENAME, defaultCodeName)); - } - } - - /** - * Creates an {@link AndroidVersion} from {@link Properties}. The properties must contain - * android version information, or an exception will be thrown. - * @throws AndroidVersionException if no Android version information have been found - * - * @see #saveProperties(Properties) - */ - public AndroidVersion(@NonNull Properties properties) throws AndroidVersionException { - Exception error = null; - - String apiLevel = properties.getProperty(PkgProps.VERSION_API_LEVEL, null/*defaultValue*/); - if (apiLevel != null) { - try { - mApiLevel = Integer.parseInt(apiLevel); - mCodename = sanitizeCodename(properties.getProperty(PkgProps.VERSION_CODENAME, - null/*defaultValue*/)); - return; - } catch (NumberFormatException e) { - error = e; - } - } - - // reaching here means the Properties object did not contain the apiLevel which is required. - throw new AndroidVersionException(PkgProps.VERSION_API_LEVEL + " not found!", error); - } - - /** - * Creates an {@link AndroidVersion} from a string that may be an integer API - * level or a string codename. - *

- * Important: An important limitation of this method is that cannot possible - * recreate the API level integer from a pure string codename. This is only OK to use - * if the caller can guarantee that only {@link #getApiString()} will be used later. - * Wrong things will happen if the caller then tries to resolve the numeric - * {@link #getApiLevel()}. - * - * @param apiOrCodename A non-null API integer or a codename in its "ALL_CAPS" format. - * "REL" is notable not a valid codename. - * @throws AndroidVersionException if the input isn't a pure integer or doesn't look like - * a valid string codename. - */ - public AndroidVersion(@NonNull String apiOrCodename) throws AndroidVersionException { - int apiLevel = 0; - String codename = null; - try { - apiLevel = Integer.parseInt(apiOrCodename); - } catch (NumberFormatException ignore) { - // We don't know the API level. Android platform codenames are all caps. - // REL is a release-reserved keyword which we can use here. - - if (!SdkConstants.CODENAME_RELEASE.equals(apiOrCodename)) { - if (Pattern.matches("[A-Z_]+", apiOrCodename)) { - codename = apiOrCodename; - } - } - } - - mApiLevel = apiLevel; - mCodename = sanitizeCodename(codename); - - if (mApiLevel <= 0 && codename == null) { - throw new AndroidVersionException( - "Invalid android API or codename " + apiOrCodename, //$NON-NLS-1$ - null); - } - } - - public void saveProperties(@NonNull Properties props) { - props.setProperty(PkgProps.VERSION_API_LEVEL, Integer.toString(mApiLevel)); - if (mCodename != null) { - props.setProperty(PkgProps.VERSION_CODENAME, mCodename); - } - } - - /** - * Returns the api level as an integer. - *

For target that are in preview mode, this can be superseded by - * {@link #getCodename()}. - *

To display the API level in the UI, use {@link #getApiString()}, which will use the - * codename if applicable. - * @see #getCodename() - * @see #getApiString() - */ - public int getApiLevel() { - return mApiLevel; - } - - /** - * Returns the API level as an integer. If this is a preview platform, it - * will return the expected final version of the API rather than the current API - * level. This is the "feature level" as opposed to the "release level" returned by - * {@link #getApiLevel()} in the sense that it is useful when you want - * to check the presence of a given feature from an API, and we consider the feature - * present in preview platforms as well. - * - * @return the API level of this version, +1 for preview platforms - */ - public int getFeatureLevel() { - //noinspection VariableNotUsedInsideIf - return mCodename != null ? mApiLevel + 1 : mApiLevel; - } - - /** - * Returns the version code name if applicable, null otherwise. - *

If the codename is non null, then the API level should be ignored, and this should be - * used as a unique identifier of the target instead. - */ - @Nullable - public String getCodename() { - return mCodename; - } - - /** - * Returns a string representing the API level and/or the code name. - */ - @NonNull - public String getApiString() { - if (mCodename != null) { - return mCodename; - } - - return Integer.toString(mApiLevel); - } - - /** - * Returns whether or not the version is a preview version. - */ - public boolean isPreview() { - return mCodename != null; - } - - /** - * Checks whether a device running a version similar to the receiver can run a project compiled - * for the given version. - *

- * Be aware that this is not a perfect test, as other properties could break compatibility - * despite this method returning true. For a more comprehensive test, see - * {@link IAndroidTarget#canRunOn(IAndroidTarget)}. - *

- * Nevertheless, when testing if an application can run on a device (where there is no - * access to the list of optional libraries), this method can give a good indication of whether - * there is a chance the application could run, or if there's a direct incompatibility. - */ - public boolean canRun(@NonNull AndroidVersion appVersion) { - // if the application is compiled for a preview version, the device must be running exactly - // the same. - if (appVersion.mCodename != null) { - return appVersion.mCodename.equals(mCodename); - } - - // otherwise, we check the api level (note that a device running a preview version - // will have the api level of the previous platform). - return mApiLevel >= appVersion.mApiLevel; - } - - /** - * Returns true if the AndroidVersion is an API level equals to - * apiLevel. - */ - public boolean equals(int apiLevel) { - return mCodename == null && apiLevel == mApiLevel; - } - - /** - * Compares the receiver with either an {@link AndroidVersion} object or a {@link String} - * object. - *

If obj is a {@link String}, then the method will first check if it's a string - * representation of a number, in which case it'll compare it to the api level. Otherwise, it'll - * compare it against the code name. - *

For all other type of object give as parameter, this method will return - * false. - */ - @Override - public boolean equals(Object obj) { - if (obj instanceof AndroidVersion) { - AndroidVersion version = (AndroidVersion)obj; - - if (mCodename == null) { - return version.mCodename == null && - mApiLevel == version.mApiLevel; - } else { - return mCodename.equals(version.mCodename) && - mApiLevel == version.mApiLevel; - } - - } else if (obj instanceof String) { - // if we have a code name, this must match. - if (mCodename != null) { - return mCodename.equals(obj); - } - - // else we try to convert to a int and compare to the api level - try { - int value = Integer.parseInt((String)obj); - return value == mApiLevel; - } catch (NumberFormatException e) { - // not a number? we'll return false below. - } - } - - return false; - } - - @Override - public int hashCode() { - if (mCodename != null) { - return mCodename.hashCode(); - } - - // there may be some collisions between the hashcode of the codename and the api level - // but it's acceptable. - return mApiLevel; - } - - /** - * Returns a string with the API Level and optional codename. - * Useful for debugging. - * For display purpose, please use {@link #getApiString()} instead. - */ - @Override - public String toString() { - String s = String.format("API %1$d", mApiLevel); //$NON-NLS-1$ - if (isPreview()) { - s += String.format(", %1$s preview", mCodename); //$NON-NLS-1$ - } - return s; - } - - /** - * Compares this object with the specified object for order. Returns a - * negative integer, zero, or a positive integer as this object is less - * than, equal to, or greater than the specified object. - * - * @param o the Object to be compared. - * @return a negative integer, zero, or a positive integer as this object is - * less than, equal to, or greater than the specified object. - */ - @Override - public int compareTo(@NonNull AndroidVersion o) { - return compareTo(o.mApiLevel, o.mCodename); - } - - public int compareTo(int apiLevel, @Nullable String codename) { - if (mCodename == null) { - if (codename == null) { - return mApiLevel - apiLevel; - } else { - if (mApiLevel == apiLevel) { - return -1; // same api level but argument is a preview for next version - } - - return mApiLevel - apiLevel; - } - } else { - // 'this' is a preview - if (mApiLevel == apiLevel) { - if (codename == null) { - return +1; - } else { - return mCodename.compareTo(codename); // strange case where the 2 previews - // have different codename? - } - } else { - return mApiLevel - apiLevel; - } - } - } - - /** - * Compares this version with the specified API and returns true if this version - * is greater or equal than the requested API -- that is the current version is a - * suitable min-api-level for the argument API. - */ - public boolean isGreaterOrEqualThan(int api) { - return compareTo(api, null /*codename*/) >= 0; - } - - /** - * Sanitizes the codename string according to the following rules: - * - A codename should be {@code null} for a release version or it should be a non-empty - * string for an actual preview. - * - In input, spacing is trimmed since it is irrelevant. - * - An empty string or the special codename "REL" means a release version - * and is converted to {@code null}. - * - * @param codename A possible-null codename. - * @return Null for a release version or a non-empty codename. - */ - @Nullable - private static String sanitizeCodename(@Nullable String codename) { - if (codename != null) { - codename = codename.trim(); - if (codename.isEmpty() || SdkConstants.CODENAME_RELEASE.equals(codename)) { - codename = null; - } - } - return codename; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/BuildToolInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/BuildToolInfo.java deleted file mode 100644 index 39fefd6d..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/BuildToolInfo.java +++ /dev/null @@ -1,391 +0,0 @@ -/* - * Copyright (C) 2013 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.sdklib; - -import static com.android.SdkConstants.FD_LIB; -import static com.android.SdkConstants.FN_AAPT; -import static com.android.SdkConstants.FN_AIDL; -import static com.android.SdkConstants.FN_BCC_COMPAT; -import static com.android.SdkConstants.FN_DEXDUMP; -import static com.android.SdkConstants.FN_DX; -import static com.android.SdkConstants.FN_DX_JAR; -import static com.android.SdkConstants.FN_JACK; -import static com.android.SdkConstants.FN_JILL; -import static com.android.SdkConstants.FN_LD_ARM; -import static com.android.SdkConstants.FN_LD_MIPS; -import static com.android.SdkConstants.FN_LD_X86; -import static com.android.SdkConstants.FN_RENDERSCRIPT; -import static com.android.SdkConstants.FN_SPLIT_SELECT; -import static com.android.SdkConstants.FN_ZIPALIGN; -import static com.android.SdkConstants.OS_FRAMEWORK_RS; -import static com.android.SdkConstants.OS_FRAMEWORK_RS_CLANG; -import static com.android.sdklib.BuildToolInfo.PathId.AAPT; -import static com.android.sdklib.BuildToolInfo.PathId.AIDL; -import static com.android.sdklib.BuildToolInfo.PathId.ANDROID_RS; -import static com.android.sdklib.BuildToolInfo.PathId.ANDROID_RS_CLANG; -import static com.android.sdklib.BuildToolInfo.PathId.BCC_COMPAT; -import static com.android.sdklib.BuildToolInfo.PathId.DEXDUMP; -import static com.android.sdklib.BuildToolInfo.PathId.DX; -import static com.android.sdklib.BuildToolInfo.PathId.DX_JAR; -import static com.android.sdklib.BuildToolInfo.PathId.JACK; -import static com.android.sdklib.BuildToolInfo.PathId.JILL; -import static com.android.sdklib.BuildToolInfo.PathId.LD_ARM; -import static com.android.sdklib.BuildToolInfo.PathId.LD_MIPS; -import static com.android.sdklib.BuildToolInfo.PathId.LD_X86; -import static com.android.sdklib.BuildToolInfo.PathId.LLVM_RS_CC; -import static com.android.sdklib.BuildToolInfo.PathId.SPLIT_SELECT; -import static com.android.sdklib.BuildToolInfo.PathId.ZIP_ALIGN; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.annotations.VisibleForTesting; -import com.android.annotations.VisibleForTesting.Visibility; -import com.android.sdklib.io.LegacyFileOp; -import com.android.sdklib.repository.FullRevision; -import com.android.sdklib.repository.NoPreviewRevision; -import com.android.utils.ILogger; -import com.google.common.collect.Maps; - -import java.io.File; -import java.util.Map; -import java.util.Properties; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Information on a specific build-tool folder. - *

- * For unit tests, see: - * - sdklib/src/test/.../LocalSdkTest - * - sdklib/src/test/.../SdkManagerTest - * - sdklib/src/test/.../BuildToolInfoTest - */ -public class BuildToolInfo { - - /** Name of the file read by {@link #getRuntimeProps()} */ - private static final String FN_RUNTIME_PROPS = "runtime.properties"; - - /** - * Property in {@link #FN_RUNTIME_PROPS} indicating the desired runtime JVM. - * Type: {@link NoPreviewRevision#toShortString()}, e.g. "1.7.0" - */ - private static final String PROP_RUNTIME_JVM = "Runtime.Jvm"; - - /** - * First version with native multi-dex support. - */ - public static final int SDK_LEVEL_FOR_MULTIDEX_NATIVE_SUPPORT = 21; - - public enum PathId { - /** OS Path to the target's version of the aapt tool. */ - AAPT("1.0.0"), - /** OS Path to the target's version of the aidl tool. */ - AIDL("1.0.0"), - /** OS Path to the target's version of the dx tool. */ - DX("1.0.0"), - /** OS Path to the target's version of the dx.jar file. */ - DX_JAR("1.0.0"), - /** OS Path to the llvm-rs-cc binary for Renderscript. */ - LLVM_RS_CC("1.0.0"), - /** OS Path to the Renderscript include folder. */ - ANDROID_RS("1.0.0"), - /** OS Path to the Renderscript(clang) include folder. */ - ANDROID_RS_CLANG("1.0.0"), - - DEXDUMP("1.0.0"), - - // --- NEW IN 18.1.0 --- - - /** OS Path to the bcc_compat tool. */ - BCC_COMPAT("18.1.0"), - /** OS Path to the ARM linker. */ - LD_ARM("18.1.0"), - /** OS Path to the X86 linker. */ - LD_X86("18.1.0"), - /** OS Path to the MIPS linker. */ - LD_MIPS("18.1.0"), - - // --- NEW IN 19.1.0 --- - ZIP_ALIGN("19.1.0"), - - // --- NEW IN 21.x.y --- - JACK("21.1.0"), - JILL("21.1.0"), - - SPLIT_SELECT("22.0.0"); - - /** - * min revision this element was introduced. - * Controls {@link BuildToolInfo#isValid(ILogger)} - */ - private final FullRevision mMinRevision; - - /** - * Creates the enum with a min revision in which this - * tools appeared in the build tools. - * - * @param minRevision the min revision. - */ - PathId(@NonNull String minRevision) { - mMinRevision = FullRevision.parseRevision(minRevision); - } - - /** - * Returns whether the enum of present in a given rev of the build tools. - * - * @param fullRevision the build tools revision. - * @return true if the tool is present. - */ - boolean isPresentIn(@NonNull FullRevision fullRevision) { - return fullRevision.compareTo(mMinRevision) >= 0; - } - } - - /** The build-tool revision. */ - @NonNull - private final FullRevision mRevision; - - /** The path to the build-tool folder specific to this revision. */ - @NonNull - private final File mPath; - - private final Map mPaths = Maps.newEnumMap(PathId.class); - - public BuildToolInfo(@NonNull FullRevision revision, @NonNull File path) { - mRevision = revision; - mPath = path; - - add(AAPT, FN_AAPT); - add(AIDL, FN_AIDL); - add(DX, FN_DX); - add(DX_JAR, FD_LIB + File.separator + FN_DX_JAR); - add(LLVM_RS_CC, FN_RENDERSCRIPT); - add(ANDROID_RS, OS_FRAMEWORK_RS); - add(ANDROID_RS_CLANG, OS_FRAMEWORK_RS_CLANG); - add(DEXDUMP, FN_DEXDUMP); - add(BCC_COMPAT, FN_BCC_COMPAT); - add(LD_ARM, FN_LD_ARM); - add(LD_X86, FN_LD_X86); - add(LD_MIPS, FN_LD_MIPS); - add(ZIP_ALIGN, FN_ZIPALIGN); - add(JACK, FN_JACK); - add(JILL, FN_JILL); - add(SPLIT_SELECT, FN_SPLIT_SELECT); - } - - public BuildToolInfo( - @NonNull FullRevision revision, - @NonNull File mainPath, - @NonNull File aapt, - @NonNull File aidl, - @NonNull File dx, - @NonNull File dxJar, - @NonNull File llmvRsCc, - @NonNull File androidRs, - @NonNull File androidRsClang, - @Nullable File bccCompat, - @Nullable File ldArm, - @Nullable File ldX86, - @Nullable File ldMips, - @NonNull File zipAlign) { - mRevision = revision; - mPath = mainPath; - add(AAPT, aapt); - add(AIDL, aidl); - add(DX, dx); - add(DX_JAR, dxJar); - add(LLVM_RS_CC, llmvRsCc); - add(ANDROID_RS, androidRs); - add(ANDROID_RS_CLANG, androidRsClang); - add(ZIP_ALIGN, zipAlign); - - if (bccCompat != null) { - add(BCC_COMPAT, bccCompat); - } else if (BCC_COMPAT.isPresentIn(revision)) { - throw new IllegalArgumentException("BCC_COMPAT required in " + revision.toString()); - } - if (ldArm != null) { - add(LD_ARM, ldArm); - } else if (LD_ARM.isPresentIn(revision)) { - throw new IllegalArgumentException("LD_ARM required in " + revision.toString()); - } - - if (ldX86 != null) { - add(LD_X86, ldX86); - } else if (LD_X86.isPresentIn(revision)) { - throw new IllegalArgumentException("LD_X86 required in " + revision.toString()); - } - - if (ldMips != null) { - add(LD_MIPS, ldMips); - } else if (LD_MIPS.isPresentIn(revision)) { - throw new IllegalArgumentException("LD_MIPS required in " + revision.toString()); - } - } - - private void add(PathId id, String leaf) { - add(id, new File(mPath, leaf)); - } - - private void add(PathId id, File path) { - String str = path.getAbsolutePath(); - if (path.isDirectory() && str.charAt(str.length() - 1) != File.separatorChar) { - str += File.separatorChar; - } - mPaths.put(id, str); - } - - /** - * Returns the revision. - */ - @NonNull - public FullRevision getRevision() { - return mRevision; - } - - /** - * Returns the build-tool revision-specific folder. - *

- * For compatibility reasons, use {@link #getPath(PathId)} if you need the path to a - * specific tool. - */ - @NonNull - public File getLocation() { - return mPath; - } - - /** - * Returns the path of a build-tool component. - * - * @param pathId the id representing the path to return. - * @return The absolute path for that tool, with a / separator if it's a folder. - * Null if the path-id is unknown. - */ - public String getPath(PathId pathId) { - assert pathId.isPresentIn(mRevision); - - return mPaths.get(pathId); - } - - /** - * Checks whether the build-tool is valid by verifying that the expected binaries - * are actually present. This checks that all known paths point to a valid file - * or directory. - * - * @param log An optional logger. If non-null, errors will be printed there. - * @return True if the build-tool folder contains all the expected tools. - */ - public boolean isValid(@Nullable ILogger log) { - for (Map.Entry entry : mPaths.entrySet()) { - File f = new File(entry.getValue()); - // check if file is missing. It's only ok if the revision of the build-tools - // is lower than the min rev of the element. - if (!f.exists() && entry.getKey().isPresentIn(mRevision)) { - if (log != null) { - log.warning("Build-tool %1$s is missing %2$s at %3$s", //$NON-NLS-1$ - mRevision.toString(), - entry.getKey(), f.getAbsolutePath()); - } - return false; - } - } - return true; - } - - /** - * Parses the build-tools runtime.props file, if present. - * - * @return The properties from runtime.props if present, otherwise an empty properties set. - */ - @NonNull - public Properties getRuntimeProps() { - LegacyFileOp fop = new LegacyFileOp(); - return fop.loadProperties(new File(mPath, FN_RUNTIME_PROPS)); - } - - /** - * Checks whether this build-tools package can run on the current JVM. - * - * @return True if the build-tools package has a Runtime.Jvm property and it is lesser or - * equal to the current JVM version. - * False if the property is present and the requirement is not met. - * True if there's an error parsing either versions and the comparison cannot be made. - */ - public boolean canRunOnJvm() { - Properties props = getRuntimeProps(); - String required = props.getProperty(PROP_RUNTIME_JVM); - if (required == null) { - // No requirement ==> accepts. - return true; - } - try { - NoPreviewRevision requiredVersion = NoPreviewRevision.parseRevision(required); - NoPreviewRevision currentVersion = getCurrentJvmVersion(); - return currentVersion.compareTo(requiredVersion) >= 0; - - } catch (NumberFormatException ignore) { - // Either we failed to parse the property version or the running JVM version. - // Right now take the relaxed policy of accepting it if we can't compare. - return true; - } - } - - @VisibleForTesting(visibility=Visibility.PRIVATE) - @Nullable - protected NoPreviewRevision getCurrentJvmVersion() throws NumberFormatException { - String javav = System.getProperty("java.version"); //$NON-NLS-1$ - // java Version is typically in the form "1.2.3_45" and we just need to keep up to "1.2.3" - // since our revision numbers are in 3-parts form (1.2.3). - Pattern p = Pattern.compile("((\\d+)(\\.\\d+)?(\\.\\d+)?).*"); //$NON-NLS-1$ - Matcher m = p.matcher(javav); - if (m.matches()) { - return NoPreviewRevision.parseRevision(m.group(1)); - } - return null; - } - - /** - * Returns a debug representation suitable for unit-tests. - * Note that unit-tests need to clean up the paths to avoid inconsistent results. - */ - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append(""); //$NON-NLS-1$ - return builder.toString(); - } - - private String getPathString() { - StringBuilder sb = new StringBuilder("{"); - - for (Map.Entry entry : mPaths.entrySet()) { - if (entry.getKey().isPresentIn(mRevision)) { - if (sb.length() > 1) { - sb.append(", "); - } - sb.append(entry.getKey()).append('=').append(entry.getValue()); - } - } - - sb.append('}'); - - return sb.toString(); - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/IAndroidTarget.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/IAndroidTarget.java deleted file mode 100644 index 6606bc84..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/IAndroidTarget.java +++ /dev/null @@ -1,334 +0,0 @@ -/* - * 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.sdklib; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.sdklib.repository.descriptors.IdDisplay; - -import java.io.File; -import java.util.List; -import java.util.Map; - - - -/** - * A version of Android that applications can target when building. - */ -public interface IAndroidTarget extends Comparable { - - /** OS Path to the "android.jar" file. */ - int ANDROID_JAR = 1; - /** OS Path to the "framework.aidl" file. */ - int ANDROID_AIDL = 2; - /** OS Path to the "samples" folder which contains sample projects. */ - int SAMPLES = 4; - /** OS Path to the "skins" folder which contains the emulator skins. */ - int SKINS = 5; - /** OS Path to the "templates" folder which contains the templates for new projects. */ - int TEMPLATES = 6; - /** OS Path to the "data" folder which contains data & libraries for the SDK tools. */ - int DATA = 7; - /** OS Path to the "attrs.xml" file. */ - int ATTRIBUTES = 8; - /** OS Path to the "attrs_manifest.xml" file. */ - int MANIFEST_ATTRIBUTES = 9; - /** OS Path to the "data/layoutlib.jar" library. */ - int LAYOUT_LIB = 10; - /** OS Path to the "data/res" folder. */ - int RESOURCES = 11; - /** OS Path to the "data/fonts" folder. */ - int FONTS = 12; - /** OS Path to the "data/widgets.txt" file. */ - int WIDGETS = 13; - /** OS Path to the "data/activity_actions.txt" file. */ - int ACTIONS_ACTIVITY = 14; - /** OS Path to the "data/broadcast_actions.txt" file. */ - int ACTIONS_BROADCAST = 15; - /** OS Path to the "data/service_actions.txt" file. */ - int ACTIONS_SERVICE = 16; - /** OS Path to the "data/categories.txt" file. */ - int CATEGORIES = 17; - /** OS Path to the "sources" folder. */ - int SOURCES = 18; - /** OS Path to the target specific docs */ - int DOCS = 19; - /** OS Path to the "ant" folder which contains the ant build rules (ver 2 and above) */ - int ANT = 24; - /** OS Path to the "uiautomator.jar" file. */ - int UI_AUTOMATOR_JAR = 27; - - - /** - * Return value for {@link #getUsbVendorId()} meaning no USB vendor IDs are defined by the - * Android target. - */ - int NO_USB_ID = 0; - - /** An optional library provided by an Android Target */ - interface OptionalLibrary { - /** The name of the library, as used in the manifest (<uses-library>). */ - @NonNull - String getName(); - /** Location of the jar file. */ - @NonNull - File getJar(); - /** Description of the library. */ - @NonNull - String getDescription(); - /** Whether the library requires a manifest entry */ - boolean isManifestEntryRequired(); - } - - /** - * Returns the target location. - */ - String getLocation(); - - /** - * Returns the name of the vendor of the target. - */ - String getVendor(); - - /** - * Returns the name of the target. - */ - String getName(); - - /** - * Returns the full name of the target, possibly including vendor name. - */ - String getFullName(); - - /** - * Returns the name to be displayed when representing all the libraries this target contains. - */ - String getClasspathName(); - - /** - * Returns the name to be displayed when representing all the libraries this target contains. - */ - String getShortClasspathName(); - - /** - * Returns the description of the target. - */ - String getDescription(); - - /** - * Returns the version of the target. This is guaranteed to be non-null. - */ - @NonNull - AndroidVersion getVersion(); - - /** - * Returns the platform version as a readable string. - */ - String getVersionName(); - - /** Returns the revision number for the target. */ - int getRevision(); - - /** - * Returns true if the target is a standard Android platform. - */ - boolean isPlatform(); - - /** - * Returns the parent target. This is likely to only be non null if - * {@link #isPlatform()} returns false - */ - IAndroidTarget getParent(); - - /** - * Returns the path of a platform component. - * @param pathId the id representing the path to return. - * Any of the constants defined in the {@link IAndroidTarget} interface can be used. - */ - String getPath(int pathId); - - /** - * Returns the path of a platform component. - *

- * This is like the legacy {@link #getPath(int)} method except it returns a {@link File}. - * - * @param pathId the id representing the path to return. - * Any of the constants defined in the {@link IAndroidTarget} interface can be used. - */ - File getFile(int pathId); - - /** - * Returns a BuildToolInfo for backward compatibility. If an older SDK is used this will return - * paths located in the platform-tools, otherwise it'll return paths located in the latest - * build-tools. - * @return a BuildToolInfo or null if none are available. - */ - BuildToolInfo getBuildToolInfo(); - - /** - * Returns the boot classpath for this target. - * In most case, this is similar to calling {@link #getPath(int)} with - * {@link IAndroidTarget#ANDROID_JAR}. - * - * @return a non null list of the boot classpath. - */ - @NonNull - List getBootClasspath(); - - /** - * Returns a list of optional libraries for this target. - * - * These libraries are not automatically added to the classpath. - * Using them requires adding a uses-library entry in the manifest. - * - * @return a list of libraries. - * - * @see OptionalLibrary#getName() - */ - @NonNull - List getOptionalLibraries(); - - /** - * Returns the additional libraries for this target. - * - * These libraries are automatically added to the classpath, but using them requires - * adding a uses-library entry in the manifest. - * - * @return a list of libraries. - * - * @see OptionalLibrary#getName() - */ - @NonNull - List getAdditionalLibraries(); - - /** - * Returns whether the target is able to render layouts. - */ - boolean hasRenderingLibrary(); - - /** - * Returns the available skin folders for this target. - *

- * To get the skin names, use {@link File#getName()}.
- * Skins come either from: - *

    - *
  • a platform ({@code sdk/platforms/N/skins/name})
  • - *
  • an add-on ({@code sdk/addons/name/skins/name})
  • - *
  • a tagged system-image ({@code sdk/system-images/platform-N/tag/abi/skins/name}.)
  • - *
- * The array can be empty but not null. - */ - @NonNull - File[] getSkins(); - - /** - * Returns the default skin folder for this target. - *

- * To get the skin name, use {@link File#getName()}. - */ - @Nullable - File getDefaultSkin(); - - /** - * Returns the list of libraries available for a given platform. - * - * @return an array of libraries provided by the platform or null if there is none. - */ - String[] getPlatformLibraries(); - - /** - * Return the value of a given property for this target. - * @return the property value or null if it was not found. - */ - String getProperty(String name); - - /** - * Returns the value of a given property for this target as an Integer value. - *

If the value is missing or is not an integer, the method will return the given default - * value. - * @param name the name of the property to return - * @param defaultValue the default value to return. - * - * @see Integer#decode(String) - */ - Integer getProperty(String name, Integer defaultValue); - - /** - * Returns the value of a given property for this target as a Boolean value. - *

If the value is missing or is not an boolean, the method will return the given default - * value. - * - * @param name the name of the property to return - * @param defaultValue the default value to return. - * - * @see Boolean#valueOf(String) - */ - - Boolean getProperty(String name, Boolean defaultValue); - - /** - * Returns all the properties associated with this target. This can be null if the target has - * no properties. - */ - Map getProperties(); - - /** - * Returns the USB Vendor ID for the vendor of this target. - *

If the target defines no USB Vendor ID, then the method return 0. - */ - int getUsbVendorId(); - - /** - * Returns an array of system images for this target. - * The array can be empty but not null. - */ - ISystemImage[] getSystemImages(); - - /** - * Returns the system image information for the given {@code tag} and {@code abiType}. - * - * @param tag A tag id-display. - * @param abiType An ABI type string. - * @return An existing {@link ISystemImage} for the requested {@code abiType} - * or null if none exists for this type. - */ - @Nullable - ISystemImage getSystemImage(@NonNull IdDisplay tag, @NonNull String abiType); - - /** - * Returns whether the given target is compatible with the receiver. - *

- * This means that a project using the receiver's target can run on the given target. - *
- * Example: - *

-     * CupcakeTarget.canRunOn(DonutTarget) == true
-     * 
. - * - * @param target the IAndroidTarget to test. - */ - boolean canRunOn(IAndroidTarget target); - - /** - * Returns a string able to uniquely identify a target. - * Typically the target will encode information such as api level, whether it's a platform - * or add-on, and if it's an add-on vendor and add-on name. - *

- * See {@link AndroidTargetHash} for helper methods to manipulate hash strings. - */ - String hashString(); -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/ISystemImage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/ISystemImage.java deleted file mode 100644 index b39a9081..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/ISystemImage.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (C) 2011 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.sdklib; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.sdklib.devices.Abi; -import com.android.sdklib.repository.descriptors.IdDisplay; - -import java.io.File; - - -/** - * Describes a system image as used by an {@link IAndroidTarget}. - * A system image has an installation path, a location type and an ABI type. - */ -public interface ISystemImage extends Comparable { - - /** Indicates the type of location for the system image folder in the SDK. */ - enum LocationType { - /** - * The system image is located in the legacy platform's {@link SdkConstants#FD_IMAGES} - * folder. - *

- * Used by both platform and add-ons. - */ - IN_LEGACY_FOLDER, - - /** - * The system image is located in a sub-directory of the platform's - * {@link SdkConstants#FD_IMAGES} folder, allowing for multiple system - * images within the platform. - *

- * Used by both platform and add-ons. - */ - IN_IMAGES_SUBFOLDER, - - /** - * The system image is located in the new SDK's {@link SdkConstants#FD_SYSTEM_IMAGES} - * folder. Supported as of Tools R14 and Repository XSD version 5. - *

- * Used only by both platform up to Tools R22.6. - * Supported for add-ons as of Tools R22.8. - */ - IN_SYSTEM_IMAGE, - } - - /** Returns the actual location of an installed system image. */ - @NonNull - File getLocation(); - - /** Indicates the location strategy for this system image in the SDK. */ - @NonNull - LocationType getLocationType(); - - /** Returns the tag of the system image. */ - @NonNull - IdDisplay getTag(); - - /** Returns the vendor for an add-on's system image, or null for a platform system-image. */ - @Nullable - IdDisplay getAddonVendor(); - - /** - * Returns the ABI type. - * See {@link Abi} for a full list. - * Cannot be null nor empty. - */ - @NonNull - String getAbiType(); - - /** - * Returns the skins embedded in the system image.
- * Only supported by system images using {@link LocationType#IN_SYSTEM_IMAGE}.
- * The skins listed here are merged in the {@link IAndroidTarget#getSkins()} list. - * @return A non-null skin list, possibly empty. - */ - @NonNull - File[] getSkins(); -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/LegacyAndroidVersion.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/LegacyAndroidVersion.java deleted file mode 100644 index e6d31e09..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/LegacyAndroidVersion.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.android.sdklib; - -import java.util.Properties; - -public class LegacyAndroidVersion { - - private AndroidVersion androidVersion; - - public LegacyAndroidVersion(Properties properties) - throws AndroidVersion.AndroidVersionException - { - Exception error = null; - - String apiLevel = properties.getProperty("AndroidVersion.ApiLevel", null); - if (apiLevel != null) { - try - { - String codename = sanitizeCodename(properties.getProperty("AndroidVersion.CodeName", null)); - androidVersion = new AndroidVersion(Integer.parseInt(apiLevel), codename); - return; - } - catch (NumberFormatException e) - { - error = e; - } - } - throw new AndroidVersion.AndroidVersionException("AndroidVersion.ApiLevel not found!", error); - } - - public AndroidVersion getAndroidVersion() - { - return androidVersion; - } - - private static String sanitizeCodename(String codename) - { - if (codename != null) - { - codename = codename.trim(); - if ((codename.isEmpty()) || ("REL".equals(codename))) { - codename = null; - } - } - return codename; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/SdkManager.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/SdkManager.java deleted file mode 100644 index 91b57037..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/SdkManager.java +++ /dev/null @@ -1,410 +0,0 @@ -/* - * 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.sdklib; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.annotations.VisibleForTesting; -import com.android.annotations.VisibleForTesting.Visibility; -import com.android.prefs.AndroidLocation; -import com.android.prefs.AndroidLocation.AndroidLocationException; -import com.android.sdklib.internal.androidTarget.AddOnTarget; -import com.android.sdklib.internal.androidTarget.PlatformTarget; -import com.android.sdklib.repository.FullRevision; -import com.android.sdklib.repository.descriptors.IPkgDesc; -import com.android.sdklib.repository.descriptors.PkgType; -import com.android.sdklib.repository.local.LocalExtraPkgInfo; -import com.android.sdklib.repository.local.LocalPkgInfo; -import com.android.sdklib.repository.local.LocalSdk; -import com.android.utils.ILogger; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.util.Collections; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.TreeMap; -import java.util.TreeSet; - -/** - * The SDK manager parses the SDK folder and gives access to the content. - * @see PlatformTarget - * @see AddOnTarget - */ -public class SdkManager { - - @SuppressWarnings("unused") - private static final boolean DEBUG = System.getenv("SDKMAN_DEBUG") != null; //$NON-NLS-1$ - - /** Preference file containing the usb ids for adb */ - private static final String ADB_INI_FILE = "adb_usb.ini"; //$NON-NLS-1$ - //0--------90--------90--------90--------90--------90--------90--------90--------9 - private static final String ADB_INI_HEADER = - "# ANDROID 3RD PARTY USB VENDOR ID LIST -- DO NOT EDIT.\n" + //$NON-NLS-1$ - "# USE 'android update adb' TO GENERATE.\n" + //$NON-NLS-1$ - "# 1 USB VENDOR ID PER LINE.\n"; //$NON-NLS-1$ - - /** Embedded reference to the new local SDK object. */ - private final LocalSdk mLocalSdk; - - /** - * Create a new {@link SdkManager} instance. - * External users should use {@link #createManager(String, ILogger)}. - * - * @param osSdkPath the location of the SDK. - */ - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected SdkManager(@NonNull String osSdkPath) { - mLocalSdk = new LocalSdk(new File(osSdkPath)); - } - - /** - * Creates an @{linkplain SdkManager} for an existing @{link LocalSdk}. - * - * @param localSdk the SDK to use with the SDK manager - */ - private SdkManager(@NonNull LocalSdk localSdk) { - mLocalSdk = localSdk; - } - - /** - * Creates an {@link SdkManager} for a given sdk location. - * @param osSdkPath the location of the SDK. - * @param log the ILogger object receiving warning/error from the parsing. - * @return the created {@link SdkManager} or null if the location is not valid. - */ - @Nullable - public static SdkManager createManager( - @NonNull String osSdkPath, - @NonNull ILogger log) { - try { - SdkManager manager = new SdkManager(osSdkPath); - manager.reloadSdk(log); - - return manager; - } catch (Throwable throwable) { - log.error(throwable, "Error parsing the sdk."); - } - - return null; - } - - /** - * Creates an @{linkplain SdkManager} for an existing @{link LocalSdk}. - * - * @param localSdk the SDK to use with the SDK manager - */ - @NonNull - public static SdkManager createManager(@NonNull LocalSdk localSdk) { - return new SdkManager(localSdk); - } - - @NonNull - public LocalSdk getLocalSdk() { - return mLocalSdk; - } - - /** - * Reloads the content of the SDK. - * - * @param log the ILogger object receiving warning/error from the parsing. - */ - public void reloadSdk(@NonNull ILogger log) { - mLocalSdk.clearLocalPkg(PkgType.PKG_ALL); - } - - /** - * Checks whether any of the SDK platforms/add-ons/build-tools have changed on-disk - * since we last loaded the SDK. This does not reload the SDK nor does it - * change the underlying targets. - * - * @return True if at least one directory or source.prop has changed. - */ - public boolean hasChanged() { - return hasChanged(null); - } - - /** - * Checks whether any of the SDK platforms/add-ons/build-tools have changed on-disk - * since we last loaded the SDK. This does not reload the SDK nor does it - * change the underlying targets. - * - * @param log An optional logger used to print verbose info on what changed. Can be null. - * @return True if at least one directory or source.prop has changed. - */ - public boolean hasChanged(@Nullable ILogger log) { - return mLocalSdk.hasChanged(EnumSet.of(PkgType.PKG_PLATFORM, - PkgType.PKG_ADDON, - PkgType.PKG_BUILD_TOOLS)); - } - - /** - * Returns the location of the SDK. - */ - @NonNull - public String getLocation() { - File f = mLocalSdk.getLocation(); - // Our LocalSdk is created with a file path, so we know the location won't be null. - assert f != null; - return f.getPath(); - } - - /** - * Returns the targets (platforms & addons) that are available in the SDK. - * The target list is created on demand the first time then cached. - * It will not refreshed unless {@link #reloadSdk(ILogger)} is called. - *

- * The array can be empty but not null. - */ - @NonNull - public IAndroidTarget[] getTargets() { - return mLocalSdk.getTargets(); - } - - /** - * Returns an unmodifiable set of known build-tools revisions. Can be empty but not null. - * Deprecated. I don't think anything uses this. - */ - @Deprecated - @NonNull - public Set getBuildTools() { - LocalPkgInfo[] pkgs = mLocalSdk.getPkgsInfos(PkgType.PKG_BUILD_TOOLS); - TreeSet bt = new TreeSet(); - for (LocalPkgInfo pkg : pkgs) { - IPkgDesc d = pkg.getDesc(); - if (d.hasFullRevision()) { - bt.add(d.getFullRevision()); - } - } - return Collections.unmodifiableSet(bt); - } - - /** - * Returns the highest build-tool revision known. Can be null. - * - * @return The highest build-tool revision known, or null. - */ - @Nullable - public BuildToolInfo getLatestBuildTool() { - return mLocalSdk.getLatestBuildTool(); - } - - /** - * Returns the {@link BuildToolInfo} for the given revision. - * - * @param revision The requested revision. - * @return A {@link BuildToolInfo}. Can be null if {@code revision} is null or is - * not part of the known set returned by {@link #getBuildTools()}. - */ - @Nullable - public BuildToolInfo getBuildTool(@Nullable FullRevision revision) { - return mLocalSdk.getBuildTool(revision); - } - - /** - * Returns a target from a hash that was generated by {@link IAndroidTarget#hashString()}. - * - * @param hash the {@link IAndroidTarget} hash string. - * @return The matching {@link IAndroidTarget} or null. - */ - @Nullable - public IAndroidTarget getTargetFromHashString(@Nullable String hash) { - return mLocalSdk.getTargetFromHashString(hash); - } - - /** - * Updates adb with the USB devices declared in the SDK add-ons. - * @throws AndroidLocationException - * @throws IOException - */ - public void updateAdb() throws AndroidLocationException, IOException { - FileWriter writer = null; - try { - // get the android prefs location to know where to write the file. - File adbIni = new File(AndroidLocation.getFolder(), ADB_INI_FILE); - writer = new FileWriter(adbIni); - - // first, put all the vendor id in an HashSet to remove duplicate. - HashSet set = new HashSet(); - IAndroidTarget[] targets = getTargets(); - for (IAndroidTarget target : targets) { - if (target.getUsbVendorId() != IAndroidTarget.NO_USB_ID) { - set.add(target.getUsbVendorId()); - } - } - - // write file header. - writer.write(ADB_INI_HEADER); - - // now write the Id in a text file, one per line. - for (Integer i : set) { - writer.write(String.format("0x%04x\n", i)); //$NON-NLS-1$ - } - } finally { - if (writer != null) { - writer.close(); - } - } - } - - /** - * Returns the greatest {@link LayoutlibVersion} found amongst all platform - * targets currently loaded in the SDK. - *

- * We only started recording Layoutlib Versions recently in the platform meta data - * so it's possible to have an SDK with many platforms loaded but no layoutlib - * version defined. - * - * @return The greatest {@link LayoutlibVersion} or null if none is found. - * @deprecated This does NOT solve the right problem and will be changed later. - */ - @Deprecated - @Nullable - public LayoutlibVersion getMaxLayoutlibVersion() { - LayoutlibVersion maxVersion = null; - - for (IAndroidTarget target : getTargets()) { - if (target instanceof PlatformTarget) { - LayoutlibVersion lv = ((PlatformTarget) target).getLayoutlibVersion(); - if (lv != null) { - if (maxVersion == null || lv.compareTo(maxVersion) > 0) { - maxVersion = lv; - } - } - } - } - - return maxVersion; - } - - /** - * Returns a map of the root samples directories located in the SDK/extras packages. - * No guarantee is made that the extras' samples directory actually contain any valid samples. - * The only guarantee is that the root samples directory actually exists. - * The map is { File: Samples root directory => String: Extra package display name. } - * - * @return A non-null possibly empty map of extra samples directories and their associated - * extra package display name. - */ - @NonNull - public Map getExtraSamples() { - - LocalPkgInfo[] pkgsInfos = mLocalSdk.getPkgsInfos(PkgType.PKG_EXTRA); - Map samples = new HashMap(); - - for (LocalPkgInfo info : pkgsInfos) { - assert info instanceof LocalExtraPkgInfo; - - File root = info.getLocalDir(); - File path = new File(root, SdkConstants.FD_SAMPLES); - if (path.isDirectory()) { - samples.put(path, info.getListDescription()); - continue; - } - // Some old-style extras simply have a single "sample" directory. - // Accept it if it contains an AndroidManifest.xml. - path = new File(root, SdkConstants.FD_SAMPLE); - if (path.isDirectory() && - new File(path, SdkConstants.FN_ANDROID_MANIFEST_XML).isFile()) { - samples.put(path, info.getListDescription()); - } - } - - return samples; - } - - /** - * Returns a map of all the extras found in the local SDK with their major revision. - *

- * Map keys are in the form "vendor-id/path-id". These ids uniquely identify an extra package. - * The version is the incremental integer major revision of the package. - * - * @return A non-null possibly empty map of { string "vendor/path" => integer major revision } - * @deprecated Starting with add-on schema 6, extras can have full revisions instead of just - * major revisions. This API only returns the major revision. Callers should be modified - * to use the new {code LocalSdk.getPkgInfo(PkgType.PKG_EXTRAS)} API instead. - */ - @Deprecated - @NonNull - public Map getExtrasVersions() { - LocalPkgInfo[] pkgsInfos = mLocalSdk.getPkgsInfos(PkgType.PKG_EXTRA); - Map extraVersions = new TreeMap(); - - for (LocalPkgInfo info : pkgsInfos) { - assert info instanceof LocalExtraPkgInfo; - if (info instanceof LocalExtraPkgInfo) { - LocalExtraPkgInfo ei = (LocalExtraPkgInfo) info; - IPkgDesc d = ei.getDesc(); - String vendor = d.getVendor().getId(); - String path = d.getPath(); - int majorRev = d.getFullRevision().getMajor(); - - extraVersions.put(vendor + '/' + path, majorRev); - } - } - - return extraVersions; - } - - /** Returns the platform tools version if installed, null otherwise. */ - @Nullable - public String getPlatformToolsVersion() { - LocalPkgInfo info = mLocalSdk.getPkgInfo(PkgType.PKG_PLATFORM_TOOLS); - IPkgDesc d = info == null ? null : info.getDesc(); - if (d != null && d.hasFullRevision()) { - return d.getFullRevision().toShortString(); - } - - return null; - } - - - // ------------- - - public static class LayoutlibVersion implements Comparable { - private final int mApi; - private final int mRevision; - - public static final int NOT_SPECIFIED = 0; - - public LayoutlibVersion(int api, int revision) { - mApi = api; - mRevision = revision; - } - - public int getApi() { - return mApi; - } - - public int getRevision() { - return mRevision; - } - - @Override - public int compareTo(@NonNull LayoutlibVersion rhs) { - boolean useRev = this.mRevision > NOT_SPECIFIED && rhs.mRevision > NOT_SPECIFIED; - int lhsValue = (this.mApi << 16) + (useRev ? this.mRevision : 0); - int rhsValue = (rhs.mApi << 16) + (useRev ? rhs.mRevision : 0); - return lhsValue - rhsValue; - } - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/SdkVersionInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/SdkVersionInfo.java deleted file mode 100644 index cfedbf1a..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/SdkVersionInfo.java +++ /dev/null @@ -1,351 +0,0 @@ -/* - * Copyright (C) 2013 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.sdklib; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.google.common.base.Strings; - -import java.util.Locale; - -/** Information about available SDK Versions */ -public class SdkVersionInfo { - /** - * The highest known API level. Note that the tools may also look at the - * installed platforms to see if they can find more recently released - * platforms, e.g. when the tools have not yet been updated for a new - * release. This number is used as a baseline and any more recent platforms - * found can be used to increase the highest known number. - */ - public static final int HIGHEST_KNOWN_API = 23; - - /** - * Like {@link #HIGHEST_KNOWN_API} but does not include preview platforms - */ - public static final int HIGHEST_KNOWN_STABLE_API = 22; - - /** - * The lowest active API level in the ecosystem. This number will change over time - * as the distribution of older platforms decreases. - */ - public static final int LOWEST_ACTIVE_API = 8; - - /** - * Returns the Android version and code name of the given API level, or null - * if not known. The highest number (inclusive) that is supported - * is {@link SdkVersionInfo#HIGHEST_KNOWN_API}. - * - * @param api the api level - * @return a suitable version display name - */ - @Nullable - public static String getAndroidName(int api) { - // See http://source.android.com/source/build-numbers.html - String codeName = getCodeName(api); - String name = getVersionString(api); - if (name == null) { - return String.format("API %1$d", api); - } else if (codeName == null) { - return String.format("API %1$d: Android %2$s", api, name); - } else { - return String.format("API %1$d: Android %2$s (%3$s)", api, name, codeName); - } - } - - @Nullable - public static String getVersionString(int api) { - switch (api) { - case 1: return "1.0"; - case 2: return "1.1"; - case 3: return "1.5"; - case 4: return "1.6"; - case 5: return "2.0"; - case 6: return "2.0.1"; - case 7: return "2.1"; - case 8: return "2.2"; - case 9: return "2.3"; - case 10: return "2.3.3"; - case 11: return "3.0"; - case 12: return "3.1"; - case 13: return "3.2"; - case 14: return "4.0"; - case 15: return "4.0.3"; - case 16: return "4.1"; - case 17: return "4.2"; - case 18: return "4.3"; - case 19: return "4.4"; - case 20: return "4.4"; - case 21: return "5.0"; - case 22: return "5.1"; - case 23: return "5.X"; - // If you add more versions here, also update #getBuildCodes and - // #HIGHEST_KNOWN_API - - default: return null; - } - } - - @Nullable - public static String getCodeName(int api) { - switch (api) { - case 1: - case 2: - return null; - case 3: - return "Cupcake"; - case 4: - return "Donut"; - case 5: - case 6: - case 7: - return "Eclair"; - case 8: - return "Froyo"; - case 9: - case 10: - return "Gingerbread"; - case 11: - case 12: - case 13: - return "Honeycomb"; - case 14: - case 15: - return "IceCreamSandwich"; - case 16: - case 17: - case 18: - return "Jelly Bean"; - case 19: - return "KitKat"; - case 20: - return "KitKat Wear"; - case 21: - case 22: - return "Lollipop"; - case 23: - return "MNC"; - - // If you add more versions here, also update #getBuildCodes and - // #HIGHEST_KNOWN_API - - default: return null; - } - } - - /** - * Returns the applicable build code (for - * {@code android.os.Build.VERSION_CODES}) for the corresponding API level, - * or null if it's unknown. The highest number (inclusive) that is supported - * is {@link SdkVersionInfo#HIGHEST_KNOWN_API}. - * - * @param api the API level to look up a version code for - * @return the corresponding build code field name, or null - */ - @Nullable - public static String getBuildCode(int api) { - // See http://developer.android.com/reference/android/os/Build.VERSION_CODES.html - switch (api) { - case 1: return "BASE"; //$NON-NLS-1$ - case 2: return "BASE_1_1"; //$NON-NLS-1$ - case 3: return "CUPCAKE"; //$NON-NLS-1$ - case 4: return "DONUT"; //$NON-NLS-1$ - case 5: return "ECLAIR"; //$NON-NLS-1$ - case 6: return "ECLAIR_0_1"; //$NON-NLS-1$ - case 7: return "ECLAIR_MR1"; //$NON-NLS-1$ - case 8: return "FROYO"; //$NON-NLS-1$ - case 9: return "GINGERBREAD"; //$NON-NLS-1$ - case 10: return "GINGERBREAD_MR1"; //$NON-NLS-1$ - case 11: return "HONEYCOMB"; //$NON-NLS-1$ - case 12: return "HONEYCOMB_MR1"; //$NON-NLS-1$ - case 13: return "HONEYCOMB_MR2"; //$NON-NLS-1$ - case 14: return "ICE_CREAM_SANDWICH"; //$NON-NLS-1$ - case 15: return "ICE_CREAM_SANDWICH_MR1"; //$NON-NLS-1$ - case 16: return "JELLY_BEAN"; //$NON-NLS-1$ - case 17: return "JELLY_BEAN_MR1"; //$NON-NLS-1$ - case 18: return "JELLY_BEAN_MR2"; //$NON-NLS-1$ - case 19: return "KITKAT"; //$NON-NLS-1$ - case 20: return "KITKAT_WATCH"; //$NON-NLS-1$ - case 21: return "LOLLIPOP"; //$NON-NLS-1$ - case 22: return "LOLLIPOP_MR1"; //$NON-NLS-1$ - case 23: return "MNC"; //$NON-NLS-1$ - // If you add more versions here, also update #getAndroidName and - // #HIGHEST_KNOWN_API - } - - return null; - } - - /** - * Returns the API level of the given build code (e.g. JELLY_BEAN_MR1 => 17), or -1 if not - * recognized - * - * @param buildCode the build code name (not case sensitive) - * @param recognizeUnknowns if true, treat an unrecognized code name as a newly released - * platform the tools are not yet aware of, and set its API level to - * some higher number than all the currently known API versions - * @return the API level, or -1 if not recognized (unless recognizeUnknowns is true, in which - * {@link #HIGHEST_KNOWN_API} plus one is returned - */ - public static int getApiByBuildCode(@NonNull String buildCode, boolean recognizeUnknowns) { - for (int api = 1; api <= HIGHEST_KNOWN_API; api++) { - String code = getBuildCode(api); - if (code != null && code.equalsIgnoreCase(buildCode)) { - return api; - } - } - - if (buildCode.equalsIgnoreCase("L")) { - return 21; // For now the Build class also provides this as an alias to Lollipop - } - - return recognizeUnknowns ? HIGHEST_KNOWN_API + 1 : -1; - } - - /** - * Returns the API level of the given preview code name (e.g. JellyBeanMR2 => 17), or -1 if not - * recognized - * - * @param previewName the preview name (not case sensitive) - * @param recognizeUnknowns if true, treat an unrecognized code name as a newly released - * platform the tools are not yet aware of, and set its API level to - * some higher number than all the currently known API versions - * @return the API level, or -1 if not recognized (unless recognizeUnknowns is true, in which - * {@link #HIGHEST_KNOWN_API} plus one is returned - */ - public static int getApiByPreviewName(@NonNull String previewName, boolean recognizeUnknowns) { - // JellyBean => JELLY_BEAN - String codeName = camelCaseToUnderlines(previewName).toUpperCase(Locale.US); - return getApiByBuildCode(codeName, recognizeUnknowns); - } - - /** - * Converts a CamelCase word into an underlined_word - * - * @param string the CamelCase version of the word - * @return the underlined version of the word - */ - @NonNull - public static String camelCaseToUnderlines(@NonNull String string) { - if (string.isEmpty()) { - return string; - } - - StringBuilder sb = new StringBuilder(2 * string.length()); - int n = string.length(); - boolean lastWasUpperCase = Character.isUpperCase(string.charAt(0)); - for (int i = 0; i < n; i++) { - char c = string.charAt(i); - boolean isUpperCase = Character.isUpperCase(c); - if (isUpperCase && !lastWasUpperCase) { - sb.append('_'); - } - lastWasUpperCase = isUpperCase; - c = Character.toLowerCase(c); - sb.append(c); - } - - return sb.toString(); - } - - /** - * Converts an underlined_word into a CamelCase word - * - * @param string the underlined word to convert - * @return the CamelCase version of the word - */ - @NonNull - public static String underlinesToCamelCase(@NonNull String string) { - StringBuilder sb = new StringBuilder(string.length()); - int n = string.length(); - - int i = 0; - @SuppressWarnings("SpellCheckingInspection") - boolean upcaseNext = true; - for (; i < n; i++) { - char c = string.charAt(i); - if (c == '_') { - upcaseNext = true; - } else { - if (upcaseNext) { - c = Character.toUpperCase(c); - } - upcaseNext = false; - sb.append(c); - } - } - - return sb.toString(); - } - - /** - * Returns the {@link AndroidVersion} for a given version string, which is typically an API - * level number, but can also be a codename for a preview platform. Note: This should - * not be used to look up version names for build codes; for that, use {@link - * #getApiByBuildCode(String, boolean)}. The primary difference between this method is that - * {@link #getApiByBuildCode(String, boolean)} will return the final API number for a platform - * (e.g. for "KITKAT" it will return 19) whereas this method will return the API number for the - * codename as a preview platform (e.g. 18). - * - * @param apiOrPreviewName the version string - * @param targets an optional array of installed targets, if available. If the version - * string corresponds to a code name, this is used to search for a - * corresponding API level. - * @return an {@link com.android.sdklib.AndroidVersion}, or null if the version could not be - * determined (e.g. an empty or invalid API number or an unknown code name) - */ - @Nullable - public static AndroidVersion getVersion( - @Nullable String apiOrPreviewName, - @Nullable IAndroidTarget[] targets) { - if (Strings.isNullOrEmpty(apiOrPreviewName)) { - return null; - } - - if (Character.isDigit(apiOrPreviewName.charAt(0))) { - try { - int api = Integer.parseInt(apiOrPreviewName); - if (api >= 1) { - return new AndroidVersion(api, null); - } - return null; - } catch (NumberFormatException e) { - // Invalid version string - return null; - } - } - - // Codename - if (targets != null) { - for (int i = targets.length - 1; i >= 0; i--) { - IAndroidTarget target = targets[i]; - if (target.isPlatform()) { - AndroidVersion version = target.getVersion(); - if (version.isPreview() && apiOrPreviewName.equalsIgnoreCase(version.getCodename())) { - return new AndroidVersion(version.getApiLevel(), version.getCodename()); - } - } - } - } - - int api = getApiByPreviewName(apiOrPreviewName, false); - if (api != -1) { - return new AndroidVersion(api - 1, apiOrPreviewName); - } - - // Must be a future SDK platform - return new AndroidVersion(HIGHEST_KNOWN_API, apiOrPreviewName); - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/SystemImage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/SystemImage.java deleted file mode 100644 index 79c9c41f..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/SystemImage.java +++ /dev/null @@ -1,325 +0,0 @@ -/* - * Copyright (C) 2011 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.sdklib; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.sdklib.devices.Abi; -import com.android.sdklib.internal.androidTarget.PlatformTarget; -import com.android.sdklib.io.LegacyFileOp; -import com.android.sdklib.repository.descriptors.IdDisplay; -import com.google.common.base.Objects; - -import java.io.File; -import java.util.Arrays; -import java.util.Locale; - - -/** - * Describes a system image as used by an {@link IAndroidTarget}. - * A system image has an installation path, a location type, a tag and an ABI type. - */ -public class SystemImage implements ISystemImage { - - public static final IdDisplay DEFAULT_TAG = new IdDisplay("default", //$NON-NLS-1$ - "Default"); //$NON-NLS-1$ - - @Deprecated - private final LocationType mLocationtype; - private final IdDisplay mTag; - private final IdDisplay mAddonVendor; - private final String mAbiType; - private final File mLocation; - private final File[] mSkins; - - /** - * Creates a {@link SystemImage} description for an existing platform system image folder. - * - * @param location The location of an installed system image. - * @param locationType Where the system image folder is located for this ABI. - * @param tag The tag of the system-image. Use {@link #DEFAULT_TAG} for backward compatibility. - * @param abiType The ABI type. For example, one of {@link SdkConstants#ABI_ARMEABI}, - * {@link SdkConstants#ABI_ARMEABI_V7A}, {@link SdkConstants#ABI_INTEL_ATOM} or - * {@link SdkConstants#ABI_MIPS}. - * @param skins A non-null, possibly empty list of skins specific to this system image. - */ - public SystemImage( - @NonNull File location, - @NonNull LocationType locationType, - @NonNull IdDisplay tag, - @NonNull String abiType, - @NonNull File[] skins) { - this(location, locationType, tag, null /*addonVendor*/, abiType, skins); - } - - /** - * Creates a {@link SystemImage} description for an existing system image folder, - * for either platform or add-on. - * - * @param location The location of an installed system image. - * @param locationType Where the system image folder is located for this ABI. - * @param tagName The tag of the system-image. - * For an add-on, the tag-id must match the add-on's name-id. - * @param addonVendor Non-null add-on vendor name. Null for platforms. - * @param abiType The ABI type. For example, one of {@link SdkConstants#ABI_ARMEABI}, - * {@link SdkConstants#ABI_ARMEABI_V7A}, {@link SdkConstants#ABI_INTEL_ATOM} or - * {@link SdkConstants#ABI_MIPS}. - * @param skins A non-null, possibly empty list of skins specific to this system image. - */ - public SystemImage( - @NonNull File location, - @NonNull LocationType locationType, - @NonNull IdDisplay tagName, - @Nullable IdDisplay addonVendor, - @NonNull String abiType, - @NonNull File[] skins) { - mLocation = location; - mLocationtype = locationType; - mTag = tagName; - mAddonVendor = addonVendor; - mAbiType = abiType; - mSkins = skins; - } - - /** - * Creates a {@link SystemImage} description for a non-existing platform system image folder. - * The actual location is computed based on the {@code locationType}. - * - * @param sdkManager The current SDK manager. - * @param locationType Where the system image folder is located for this ABI. - * @param tag The tag of the system-image. Use {@link #DEFAULT_TAG} for backward compatibility. - * @param abiType The ABI type. For example, one of {@link SdkConstants#ABI_ARMEABI}, - * {@link SdkConstants#ABI_ARMEABI_V7A}, {@link SdkConstants#ABI_INTEL_ATOM} or - * {@link SdkConstants#ABI_MIPS}. - * @param skins A non-null, possibly empty list of skins specific to this system image. - * @throws IllegalArgumentException if the {@code target} used for - * {@link ISystemImage.LocationType#IN_SYSTEM_IMAGE} is not a {@link PlatformTarget}. - */ - public SystemImage( - @NonNull SdkManager sdkManager, - @NonNull IAndroidTarget target, - @NonNull LocationType locationType, - @NonNull IdDisplay tag, - @NonNull String abiType, - @NonNull File[] skins) { - this(sdkManager, target, locationType, tag, null /*addonVendor*/, abiType, skins); - } - - - /** - * Creates a {@link SystemImage} description for a non-existing system image folder, - * for either platform or add-on. - * The actual location is computed based on the {@code locationType}. - * - * @param sdkManager The current SDK manager. - * @param locationType Where the system image folder is located for this ABI. - * @param tag The tag of the system-image. Use {@link #DEFAULT_TAG} for backward compatibility. - * @param addonVendor Non-null add-on vendor name. Null for platforms. - * @param abiType The ABI type. For example, one of {@link SdkConstants#ABI_ARMEABI}, - * {@link SdkConstants#ABI_ARMEABI_V7A}, {@link SdkConstants#ABI_INTEL_ATOM} or - * {@link SdkConstants#ABI_MIPS}. - * @param skins A non-null, possibly empty list of skins specific to this system image. - * @throws IllegalArgumentException if the {@code target} used for - * {@link ISystemImage.LocationType#IN_SYSTEM_IMAGE} is not a {@link PlatformTarget}. - */ - public SystemImage( - @NonNull SdkManager sdkManager, - @NonNull IAndroidTarget target, - @NonNull LocationType locationType, - @NonNull IdDisplay tag, - @Nullable IdDisplay addonVendor, - @NonNull String abiType, - @NonNull File[] skins) { - mLocationtype = locationType; - mTag = tag; - mAddonVendor = addonVendor; - mAbiType = abiType; - mSkins = skins; - - File location = null; - switch(locationType) { - case IN_LEGACY_FOLDER: - location = new File(target.getLocation(), SdkConstants.OS_IMAGES_FOLDER); - break; - - case IN_IMAGES_SUBFOLDER: - location = LegacyFileOp.append(target.getLocation(), SdkConstants.OS_IMAGES_FOLDER, abiType); - break; - - case IN_SYSTEM_IMAGE: - location = getCanonicalFolder(sdkManager.getLocation(), - target.getVersion(), - tag.getId(), - addonVendor == null ? null : addonVendor.getId(), - abiType); - break; - default: - // This is not supposed to happen unless LocationType is - // extended without adjusting this code. - assert false : "SystemImage used with an incorrect locationType"; //$NON-NLS-1$ - } - mLocation = location; - } - - /** - * Static helper method that returns the canonical path for a system-image that uses - * the {@link ISystemImage.LocationType#IN_SYSTEM_IMAGE} location type. - *

- * Such an image is located in {@code SDK/system-images/android-N/tag/abiType}. - * For this reason this method requires the root SDK as well as the platform, tag abd ABI type. - * - * @param sdkOsPath The OS path to the SDK. - * @param platformVersion The platform version. - * @param tagId An optional tag. If null, not tag folder is used. - * For legacy, use {@code SystemImage.DEFAULT_TAG.getId()}. - * @param addonVendorId An optional vendor-id for an add-on. If null, it's a platform sys-img. - * @param abiType An optional ABI type. If null, the parent directory is returned. - * @return A file that represents the location of the canonical system-image folder - * for this configuration. - */ - @NonNull - private static File getCanonicalFolder( - @NonNull String sdkOsPath, - @NonNull AndroidVersion platformVersion, - @Nullable String tagId, - @Nullable String addonVendorId, - @Nullable String abiType) { - File root; - if (addonVendorId == null) { - root = LegacyFileOp.append( - sdkOsPath, - SdkConstants.FD_SYSTEM_IMAGES, - AndroidTargetHash.getPlatformHashString(platformVersion)); - if (tagId != null) { - root = LegacyFileOp.append(root, tagId); - } - } else { - root = LegacyFileOp.append( - sdkOsPath, - SdkConstants.FD_SYSTEM_IMAGES, - AndroidTargetHash.getAddonHashString(addonVendorId, tagId, platformVersion)); - } - if (abiType == null) { - return root; - } else { - return LegacyFileOp.append(root, abiType); - } - } - - /** Returns the actual location of an installed system image. */ - @NonNull - @Override - public File getLocation() { - return mLocation; - } - - /** - * Indicates the location strategy for this system image in the SDK. - * @deprecated - */ - @NonNull - @Override - public LocationType getLocationType() { - return mLocationtype; - } - - /** Returns the tag of the system image. */ - @NonNull - @Override - public IdDisplay getTag() { - return mTag; - } - - /** Returns the vendor for an add-on's system image, or null for a platform system-image. */ - @Nullable - @Override - public IdDisplay getAddonVendor() { - return mAddonVendor; - } - - /** - * Returns the ABI type. - * See {@link Abi} for a full list. - * Cannot be null nor empty. - */ - @NonNull - @Override - public String getAbiType() { - return mAbiType; - } - - @NonNull - @Override - public File[] getSkins() { - return mSkins; - } - - /** - * Sort by tag & ABI name only. This is what matters from a user point of view. - */ - @Override - public int compareTo(ISystemImage other) { - int t = this.getTag().compareTo(other.getTag()); - if (t != 0) { - return t; - } - return this.getAbiType().compareToIgnoreCase(other.getAbiType()); - } - - /** - * Generates a string representation suitable for debug purposes. - * The string is not intended to be displayed to the user. - * - * {@inheritDoc} - */ - @NonNull - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("SystemImage"); - if (mAddonVendor != null) { - sb.append(" addon-vendor=").append(mAddonVendor.getId()).append(','); - } - sb.append(" tag=").append(mTag.getId()); - sb.append(", ABI=").append(mAbiType); - sb.append(", location ") - .append(mLocationtype.toString().replace('_', ' ').toLowerCase(Locale.US)) - .append("='") - .append(mLocation) - .append("'"); - return sb.toString(); - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof SystemImage)) { - return false; - } - SystemImage other = (SystemImage)o; - return mTag.equals(other.mTag) && mAbiType.equals(other.getAbiType()) && Objects.equal(mAddonVendor, other.mAddonVendor) - && mLocation.equals(other.mLocation) && Arrays.equals(mSkins, other.mSkins); - } - - @Override - public int hashCode() { - int hashCode = Objects.hashCode(mTag, mAbiType, mAddonVendor, mLocation); - hashCode *= 37; - hashCode += Arrays.hashCode(mSkins); - return hashCode; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/androidTarget/AddOnTarget.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/androidTarget/AddOnTarget.java deleted file mode 100644 index 83a3e554..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/androidTarget/AddOnTarget.java +++ /dev/null @@ -1,463 +0,0 @@ -/* - * 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.sdklib.internal.androidTarget; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.sdklib.AndroidTargetHash; -import com.android.sdklib.AndroidVersion; -import com.android.sdklib.BuildToolInfo; -import com.android.sdklib.IAndroidTarget; -import com.android.sdklib.ISystemImage; -import com.android.sdklib.repository.descriptors.IdDisplay; -import com.google.common.collect.ImmutableList; - -import java.io.File; -import java.io.FileFilter; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; - -/** - * Represents an add-on target in the SDK. - * An add-on extends a standard {@link PlatformTarget}. - */ -public final class AddOnTarget implements IAndroidTarget { - - private final String mLocation; - private final PlatformTarget mBasePlatform; - private final String mName; - private final ISystemImage[] mSystemImages; - private final String mVendor; - private final int mRevision; - private final String mDescription; - private final boolean mHasRenderingLibrary; - private final boolean mHasRenderingResources; - - private File[] mSkins; - private File mDefaultSkin; - private ImmutableList mLibraries; - private int mVendorId = NO_USB_ID; - - /** - * Creates a new add-on - * @param location the OS path location of the add-on - * @param name the name of the add-on - * @param vendor the vendor name of the add-on - * @param revision the revision of the add-on - * @param description the add-on description - * @param systemImages list of supported system images. Can be null or empty. - * @param libMap A map containing the optional libraries. The map key is the fully-qualified - * library name. The value is a 2 string array with the .jar filename, and the description. - * @param hasRenderingLibrary whether the addon has a custom layoutlib.jar - * @param hasRenderingResources whether the add has custom framework resources. - * @param basePlatform the platform the add-on is extending. - */ - public AddOnTarget( - String location, - String name, - String vendor, - int revision, - String description, - ISystemImage[] systemImages, - Map libMap, - boolean hasRenderingLibrary, - boolean hasRenderingResources, - PlatformTarget basePlatform) { - if (!location.endsWith(File.separator)) { - location = location + File.separator; - } - - mLocation = location; - mName = name; - mVendor = vendor; - mRevision = revision; - mDescription = description; - mHasRenderingLibrary = hasRenderingLibrary; - mHasRenderingResources = hasRenderingResources; - mBasePlatform = basePlatform; - - // If the add-on does not have any system-image of its own, the list here - // is empty and it's up to the callers to query the parent platform. - mSystemImages = systemImages == null ? new ISystemImage[0] : systemImages; - Arrays.sort(mSystemImages); - - // handle the optional libraries. - if (libMap != null) { - ImmutableList.Builder builder = ImmutableList.builder(); - for (Entry entry : libMap.entrySet()) { - String jarFile = entry.getValue()[0]; - String desc = entry.getValue()[1]; - builder.add(new OptionalLibraryImpl( - entry.getKey(), - new File(mLocation, SdkConstants.OS_ADDON_LIBS_FOLDER + jarFile), - desc, - true /*requireManifestEntry*/)); - } - mLibraries = builder.build(); - } else { - mLibraries = ImmutableList.of(); - } - } - - @Override - public String getLocation() { - return mLocation; - } - - @Override - public String getName() { - return mName; - } - - @Override - @Nullable - public ISystemImage getSystemImage(@NonNull IdDisplay tag, @NonNull String abiType) { - for (ISystemImage sysImg : mSystemImages) { - if (sysImg.getTag().equals(tag) && sysImg.getAbiType().equals(abiType)) { - return sysImg; - } - } - return null; - } - - @Override - public ISystemImage[] getSystemImages() { - return mSystemImages; - } - - @Override - public String getVendor() { - return mVendor; - } - - @Override - public String getFullName() { - return String.format("%1$s (%2$s)", mName, mVendor); - } - - @Override - public String getClasspathName() { - return String.format("%1$s [%2$s]", mName, mBasePlatform.getClasspathName()); - } - - @Override - public String getShortClasspathName() { - return String.format("%1$s [%2$s]", mName, mBasePlatform.getVersionName()); - } - - @Override - public String getDescription() { - return mDescription; - } - - @NonNull - @Override - public AndroidVersion getVersion() { - // this is always defined by the base platform - return mBasePlatform.getVersion(); - } - - @Override - public String getVersionName() { - return mBasePlatform.getVersionName(); - } - - @Override - public int getRevision() { - return mRevision; - } - - @Override - public boolean isPlatform() { - return false; - } - - @Override - public IAndroidTarget getParent() { - return mBasePlatform; - } - - @Override - public String getPath(int pathId) { - switch (pathId) { - case SKINS: - return mLocation + SdkConstants.OS_SKINS_FOLDER; - case DOCS: - return mLocation + SdkConstants.FD_DOCS + File.separator - + SdkConstants.FD_DOCS_REFERENCE; - - case LAYOUT_LIB: - if (mHasRenderingLibrary) { - return mLocation + SdkConstants.FD_DATA + File.separator - + SdkConstants.FN_LAYOUTLIB_JAR; - } - return mBasePlatform.getPath(pathId); - - case RESOURCES: - if (mHasRenderingResources) { - return mLocation + SdkConstants.FD_DATA + File.separator - + SdkConstants.FD_RES; - } - return mBasePlatform.getPath(pathId); - - case FONTS: - if (mHasRenderingResources) { - return mLocation + SdkConstants.FD_DATA + File.separator - + SdkConstants.FD_FONTS; - } - return mBasePlatform.getPath(pathId); - - case SAMPLES: - // only return the add-on samples folder if there is actually a sample (or more) - File sampleLoc = new File(mLocation, SdkConstants.FD_SAMPLES); - if (sampleLoc.isDirectory()) { - File[] files = sampleLoc.listFiles(new FileFilter() { - @Override - public boolean accept(File pathname) { - return pathname.isDirectory(); - } - - }); - if (files != null && files.length > 0) { - return sampleLoc.getAbsolutePath(); - } - } - //$FALL-THROUGH$ - default : - return mBasePlatform.getPath(pathId); - } - } - - @Override - public File getFile(int pathId) { - return new File(getPath(pathId)); - } - - @Override - public BuildToolInfo getBuildToolInfo() { - return mBasePlatform.getBuildToolInfo(); - } - - @Override @NonNull - public List getBootClasspath() { - return mBasePlatform.getBootClasspath(); - } - - @NonNull - @Override - public List getOptionalLibraries() { - return mBasePlatform.getOptionalLibraries(); - } - - @NonNull - @Override - public List getAdditionalLibraries() { - return mLibraries; - } - - @Override - public boolean hasRenderingLibrary() { - return mHasRenderingLibrary || mHasRenderingResources; - } - - @NonNull - @Override - public File[] getSkins() { - return mSkins; - } - - @Nullable - @Override - public File getDefaultSkin() { - return mDefaultSkin; - } - - /** - * Returns the list of libraries of the underlying platform. - * - * {@inheritDoc} - */ - @Override - public String[] getPlatformLibraries() { - return mBasePlatform.getPlatformLibraries(); - } - - @Override - public String getProperty(String name) { - return mBasePlatform.getProperty(name); - } - - @Override - public Integer getProperty(String name, Integer defaultValue) { - return mBasePlatform.getProperty(name, defaultValue); - } - - @Override - public Boolean getProperty(String name, Boolean defaultValue) { - return mBasePlatform.getProperty(name, defaultValue); - } - - @Override - public Map getProperties() { - return mBasePlatform.getProperties(); - } - - @Override - public int getUsbVendorId() { - return mVendorId; - } - - @Override - public boolean canRunOn(IAndroidTarget target) { - // basic test - if (target == this) { - return true; - } - - /* - * The method javadoc indicates: - * Returns whether the given target is compatible with the receiver. - *

A target is considered compatible if applications developed for the receiver can - * run on the given target. - */ - - // The receiver is an add-on. There are 2 big use cases: The add-on has libraries - // or the add-on doesn't (in which case we consider it a platform). - if (mLibraries.isEmpty()) { - return mBasePlatform.canRunOn(target); - } else { - // the only targets that can run the receiver are the same add-on in the same or later - // versions. - // first check: vendor/name - if (!mVendor.equals(target.getVendor()) || !mName.equals(target.getName())) { - return false; - } - - // now check the version. At this point since we checked the add-on part, - // we can revert to the basic check on version/codename which are done by the - // base platform already. - return mBasePlatform.canRunOn(target); - } - - } - - @Override - public String hashString() { - return String.format(AndroidTargetHash.ADD_ON_FORMAT, mVendor, mName, - mBasePlatform.getVersion().getApiString()); - } - - @Override - public int hashCode() { - return hashString().hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof AddOnTarget) { - AddOnTarget addon = (AddOnTarget)obj; - - return mVendor.equals(addon.mVendor) && mName.equals(addon.mName) && - mBasePlatform.getVersion().equals(addon.mBasePlatform.getVersion()); - } - - return false; - } - - /* - * Order by API level (preview/n count as between n and n+1). - * At the same API level, order as: Platform first, then add-on ordered by vendor and then name - * (non-Javadoc) - * @see java.lang.Comparable#compareTo(java.lang.Object) - */ - @Override - public int compareTo(@NonNull IAndroidTarget target) { - // quick check. - if (this == target) { - return 0; - } - - int versionDiff = getVersion().compareTo(target.getVersion()); - - // only if the version are the same do we care about platform/add-ons. - if (versionDiff == 0) { - // platforms go before add-ons. - if (target.isPlatform()) { - return +1; - } else { - AddOnTarget targetAddOn = (AddOnTarget)target; - - // both are add-ons of the same version. Compare per vendor then by name - int vendorDiff = mVendor.compareTo(targetAddOn.mVendor); - if (vendorDiff == 0) { - return mName.compareTo(targetAddOn.mName); - } else { - return vendorDiff; - } - } - - } - - return versionDiff; - } - - /** - * Returns a string representation suitable for debugging. - * The representation is not intended for display to the user. - * - * The representation is also purposely compact. It does not describe _all_ the properties - * of the target, only a few key ones. - * - * @see #getDescription() - */ - @Override - public String toString() { - return String.format("AddonTarget %1$s rev %2$d (based on %3$s)", //$NON-NLS-1$ - getVersion(), - getRevision(), - getParent().toString()); - } - - // ---- local methods. - - public void setSkins(@NonNull File[] skins, @NonNull File defaultSkin) { - mDefaultSkin = defaultSkin; - - // we mix the add-on and base platform skins - HashSet skinSet = new HashSet(); - skinSet.addAll(Arrays.asList(skins)); - skinSet.addAll(Arrays.asList(mBasePlatform.getSkins())); - - mSkins = skinSet.toArray(new File[skinSet.size()]); - Arrays.sort(mSkins); - } - - /** - * Sets the USB vendor id in the add-on. - */ - public void setUsbVendorId(int vendorId) { - if (vendorId == 0) { - throw new IllegalArgumentException( "VendorId must be > 0"); - } - - mVendorId = vendorId; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/androidTarget/MissingTarget.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/androidTarget/MissingTarget.java deleted file mode 100644 index 4454040d..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/androidTarget/MissingTarget.java +++ /dev/null @@ -1,244 +0,0 @@ -/* - * Copyright (C) 2015 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.sdklib.internal.androidTarget; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.sdklib.AndroidTargetHash; -import com.android.sdklib.AndroidVersion; -import com.android.sdklib.BuildToolInfo; -import com.android.sdklib.IAndroidTarget; -import com.android.sdklib.ISystemImage; -import com.android.sdklib.SdkVersionInfo; -import com.android.sdklib.repository.descriptors.IdDisplay; -import com.google.common.base.Objects; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Lists; - -import java.io.File; -import java.util.List; -import java.util.Map; - -/** - * A target that we don't have, but is referenced (e.g. by a system image). - */ -public class MissingTarget implements IAndroidTarget { - - private final String mVendor; - - private final AndroidVersion mVersion; - - private final List mSystemImages = Lists.newArrayList(); - - private final String mName; - - public MissingTarget(String vendor, String name, AndroidVersion version) { - mVendor = vendor; - mVersion = version; - mName = name; - } - - @Override - public String getLocation() { - return null; - } - - @Override - public String getVendor() { - return mVendor; - } - - @Override - public String getName() { - return mName; - } - - @Override - public String getFullName() { - return getName(); - } - - @Override - public String getClasspathName() { - return null; - } - - @Override - public String getShortClasspathName() { - return null; - } - - @Override - public String getDescription() { - return null; - } - - @NonNull - @Override - public AndroidVersion getVersion() { - return mVersion; - } - - @Override - public String getVersionName() { - return SdkVersionInfo.getAndroidName(getVersion().getApiLevel()); - } - - @Override - public int getRevision() { - return 0; - } - - @Override - public boolean isPlatform() { - return mVendor == null; - } - - @Override - public IAndroidTarget getParent() { - return null; - } - - @Override - public String getPath(int pathId) { - return null; - } - - @Override - public File getFile(int pathId) { - return null; - } - - @Override - public BuildToolInfo getBuildToolInfo() { - return null; - } - - @NonNull - @Override - public List getBootClasspath() { - return ImmutableList.of(); - } - - @NonNull - @Override - public List getOptionalLibraries() { - return ImmutableList.of(); - } - - @NonNull - @Override - public List getAdditionalLibraries() { - return ImmutableList.of(); - } - - @Override - public boolean hasRenderingLibrary() { - return false; - } - - @NonNull - @Override - public File[] getSkins() { - return new File[0]; - } - - @Nullable - @Override - public File getDefaultSkin() { - return null; - } - - @Override - public String[] getPlatformLibraries() { - return new String[0]; - } - - @Override - public String getProperty(String name) { - return null; - } - - @Override - public Integer getProperty(String name, Integer defaultValue) { - return null; - } - - @Override - public Boolean getProperty(String name, Boolean defaultValue) { - return null; - } - - @Override - public Map getProperties() { - return null; - } - - @Override - public int getUsbVendorId() { - return 0; - } - - @Override - public ISystemImage[] getSystemImages() { - return mSystemImages.toArray(new ISystemImage[mSystemImages.size()]); - } - - public void addSystemImage(ISystemImage image) { - mSystemImages.add(image); - } - - @Nullable - @Override - public ISystemImage getSystemImage(@NonNull IdDisplay tag, @NonNull String abiType) { - for (ISystemImage image : mSystemImages) { - if (tag.equals(image.getTag()) && abiType.equals(image.getAbiType())) { - return image; - } - } - return null; - } - - @Override - public boolean canRunOn(IAndroidTarget target) { - return false; - } - - @Override - public String hashString() { - return AndroidTargetHash.getTargetHashString(this); - } - - @Override - public int compareTo(IAndroidTarget o) { - return 0; - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof MissingTarget)) { - return false; - } - MissingTarget other = (MissingTarget) obj; - return Objects.equal(mVendor, other.mVendor) && Objects.equal(mVersion, other.mVersion); - } - - @Override - public int hashCode() { - return Objects.hashCode(mVendor, mVersion); - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/androidTarget/OptionalLibraryImpl.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/androidTarget/OptionalLibraryImpl.java deleted file mode 100644 index 97f7efff..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/androidTarget/OptionalLibraryImpl.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2015 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.sdklib.internal.androidTarget; - -import com.android.annotations.NonNull; -import com.android.sdklib.IAndroidTarget; - -import java.io.File; - -/** - * Internal implementation of OptionalLibrary - */ -public class OptionalLibraryImpl implements IAndroidTarget.OptionalLibrary { - - @NonNull - private final String mLibraryName; - @NonNull - private final File mJarFile; - @NonNull - private final String mDescription; - private final boolean mRequireManifestEntry; - - public OptionalLibraryImpl( - @NonNull String libraryName, - @NonNull File jarFile, - @NonNull String description, - boolean requireManifestEntry) { - mLibraryName = libraryName; - mJarFile = jarFile; - mDescription = description; - mRequireManifestEntry = requireManifestEntry; - } - - @Override - @NonNull - public String getName() { - return mLibraryName; - } - - @Override - @NonNull - public File getJar() { - return mJarFile; - } - - @Override - @NonNull - public String getDescription() { - return mDescription; - } - - @Override - public boolean isManifestEntryRequired() { - return mRequireManifestEntry; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/androidTarget/PlatformTarget.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/androidTarget/PlatformTarget.java deleted file mode 100644 index cdd5a9ec..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/androidTarget/PlatformTarget.java +++ /dev/null @@ -1,456 +0,0 @@ -/* - * 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.sdklib.internal.androidTarget; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.sdklib.AndroidTargetHash; -import com.android.sdklib.AndroidVersion; -import com.android.sdklib.BuildToolInfo; -import com.android.sdklib.IAndroidTarget; -import com.android.sdklib.ISystemImage; -import com.android.sdklib.SdkManager.LayoutlibVersion; -import com.android.sdklib.repository.descriptors.IdDisplay; -import com.android.utils.SparseArray; -import com.google.common.collect.ImmutableList; - -import java.io.File; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -/** - * Represents a platform target in the SDK. - */ -public final class PlatformTarget implements IAndroidTarget { - - private static final String PLATFORM_VENDOR = "Android Open Source Project"; - - private static final String PLATFORM_NAME = "Android %s"; - private static final String PLATFORM_NAME_PREVIEW = "Android %s (Preview)"; - - /** the OS path to the root folder of the platform component. */ - private final String mRootFolderOsPath; - private final String mName; - private final AndroidVersion mVersion; - private final String mVersionName; - private final int mRevision; - private final Map mProperties; - private final SparseArray mPaths = new SparseArray(); - private File[] mSkins; - private final ISystemImage[] mSystemImages; - private final List mOptionalLibraries; - private final LayoutlibVersion mLayoutlibVersion; - private final BuildToolInfo mBuildToolInfo; - - /** - * Creates a Platform target. - * - * @param sdkOsPath the root folder of the SDK - * @param platformOSPath the root folder of the platform component - * @param apiVersion the API Level + codename. - * @param versionName the version name of the platform. - * @param revision the revision of the platform component. - * @param layoutlibVersion The {@link LayoutlibVersion}. May be null. - * @param systemImages list of supported system images - * @param properties the platform properties - */ - public PlatformTarget( - String sdkOsPath, - String platformOSPath, - AndroidVersion apiVersion, - String versionName, - int revision, - LayoutlibVersion layoutlibVersion, - ISystemImage[] systemImages, - Map properties, - List optionalLibraries, - @NonNull BuildToolInfo buildToolInfo) { - if (!platformOSPath.endsWith(File.separator)) { - platformOSPath = platformOSPath + File.separator; - } - mRootFolderOsPath = platformOSPath; - mProperties = Collections.unmodifiableMap(properties); - mVersion = apiVersion; - mVersionName = versionName; - mRevision = revision; - mLayoutlibVersion = layoutlibVersion; - mBuildToolInfo = buildToolInfo; - mSystemImages = systemImages == null ? new ISystemImage[0] : systemImages; - Arrays.sort(mSystemImages); - mOptionalLibraries = ImmutableList.copyOf(optionalLibraries); - - if (mVersion.isPreview()) { - mName = String.format(PLATFORM_NAME_PREVIEW, mVersionName); - } else { - mName = String.format(PLATFORM_NAME, mVersionName); - } - - // pre-build the path to the platform components - mPaths.put(ANDROID_JAR, mRootFolderOsPath + SdkConstants.FN_FRAMEWORK_LIBRARY); - mPaths.put(UI_AUTOMATOR_JAR, mRootFolderOsPath + SdkConstants.FN_UI_AUTOMATOR_LIBRARY); - mPaths.put(SOURCES, mRootFolderOsPath + SdkConstants.FD_ANDROID_SOURCES); - mPaths.put(ANDROID_AIDL, mRootFolderOsPath + SdkConstants.FN_FRAMEWORK_AIDL); - mPaths.put(SAMPLES, mRootFolderOsPath + SdkConstants.OS_PLATFORM_SAMPLES_FOLDER); - mPaths.put(SKINS, mRootFolderOsPath + SdkConstants.OS_SKINS_FOLDER); - mPaths.put(TEMPLATES, mRootFolderOsPath + SdkConstants.OS_PLATFORM_TEMPLATES_FOLDER); - mPaths.put(DATA, mRootFolderOsPath + SdkConstants.OS_PLATFORM_DATA_FOLDER); - mPaths.put(ATTRIBUTES, mRootFolderOsPath + SdkConstants.OS_PLATFORM_ATTRS_XML); - mPaths.put(MANIFEST_ATTRIBUTES, - mRootFolderOsPath + SdkConstants.OS_PLATFORM_ATTRS_MANIFEST_XML); - mPaths.put(RESOURCES, mRootFolderOsPath + SdkConstants.OS_PLATFORM_RESOURCES_FOLDER); - mPaths.put(FONTS, mRootFolderOsPath + SdkConstants.OS_PLATFORM_FONTS_FOLDER); - mPaths.put(LAYOUT_LIB, mRootFolderOsPath + SdkConstants.OS_PLATFORM_DATA_FOLDER + - SdkConstants.FN_LAYOUTLIB_JAR); - mPaths.put(WIDGETS, mRootFolderOsPath + SdkConstants.OS_PLATFORM_DATA_FOLDER + - SdkConstants.FN_WIDGETS); - mPaths.put(ACTIONS_ACTIVITY, mRootFolderOsPath + SdkConstants.OS_PLATFORM_DATA_FOLDER + - SdkConstants.FN_INTENT_ACTIONS_ACTIVITY); - mPaths.put(ACTIONS_BROADCAST, mRootFolderOsPath + SdkConstants.OS_PLATFORM_DATA_FOLDER + - SdkConstants.FN_INTENT_ACTIONS_BROADCAST); - mPaths.put(ACTIONS_SERVICE, mRootFolderOsPath + SdkConstants.OS_PLATFORM_DATA_FOLDER + - SdkConstants.FN_INTENT_ACTIONS_SERVICE); - mPaths.put(CATEGORIES, mRootFolderOsPath + SdkConstants.OS_PLATFORM_DATA_FOLDER + - SdkConstants.FN_INTENT_CATEGORIES); - mPaths.put(ANT, mRootFolderOsPath + SdkConstants.OS_PLATFORM_ANT_FOLDER); - } - - /** - * Returns the {@link LayoutlibVersion}. May be null. - */ - public LayoutlibVersion getLayoutlibVersion() { - return mLayoutlibVersion; - } - - @Override - @Nullable - public ISystemImage getSystemImage(@NonNull IdDisplay tag, @NonNull String abiType) { - for (ISystemImage sysImg : mSystemImages) { - if (sysImg.getTag().equals(tag) && sysImg.getAbiType().equals(abiType)) { - return sysImg; - } - } - return null; - } - - @Override - public ISystemImage[] getSystemImages() { - return mSystemImages; - } - - @Override - public String getLocation() { - return mRootFolderOsPath; - } - - /** - * {@inheritDoc} - *

- * For Platform, the vendor name is always "Android". - * - * @see com.android.sdklib.IAndroidTarget#getVendor() - */ - @Override - public String getVendor() { - return PLATFORM_VENDOR; - } - - @Override - public String getName() { - return mName; - } - - @Override - public String getFullName() { - return mName; - } - - @Override - public String getClasspathName() { - return mName; - } - - @Override - public String getShortClasspathName() { - return mName; - } - - /* - * (non-Javadoc) - * - * Description for the Android platform is dynamically generated. - * - * @see com.android.sdklib.IAndroidTarget#getDescription() - */ - @Override - public String getDescription() { - return String.format("Standard Android platform %s", mVersionName); - } - - @NonNull - @Override - public AndroidVersion getVersion() { - return mVersion; - } - - @Override - public String getVersionName() { - return mVersionName; - } - - @Override - public int getRevision() { - return mRevision; - } - - @Override - public boolean isPlatform() { - return true; - } - - @Override - public IAndroidTarget getParent() { - return null; - } - - @Override - public String getPath(int pathId) { - return mPaths.get(pathId); - } - - @Override - public File getFile(int pathId) { - return new File(getPath(pathId)); - } - - @Override - public BuildToolInfo getBuildToolInfo() { - return mBuildToolInfo; - } - - @Override @NonNull - public List getBootClasspath() { - return ImmutableList.of(getPath(IAndroidTarget.ANDROID_JAR)); - } - - @NonNull - @Override - public List getOptionalLibraries() { - return mOptionalLibraries; - } - - /** - * Always returns null, as a standard platform has no additional libraries. - * - * {@inheritDoc} - * @see com.android.sdklib.IAndroidTarget#getAdditionalLibraries() - */ - @NonNull - @Override - public List getAdditionalLibraries() { - return ImmutableList.of(); - } - - /** - * Returns whether the target is able to render layouts. This is always true for platforms. - */ - @Override - public boolean hasRenderingLibrary() { - return true; - } - - @NonNull - @Override - public File[] getSkins() { - return mSkins; - } - - @Nullable - @Override - public File getDefaultSkin() { - // only one skin? easy. - if (mSkins.length == 1) { - return mSkins[0]; - } - - // look for the skin name in the platform props - String skinName = mProperties.get(SdkConstants.PROP_SDK_DEFAULT_SKIN); - if (skinName == null) { - // otherwise try to find a good default. - if (mVersion.getApiLevel() >= 4) { - // at this time, this is the default skin for all older platforms that had 2+ skins. - skinName = "WVGA800"; //$NON-NLS-1$ - } else { - skinName = "HVGA"; // this is for 1.5 and earlier. //$NON-NLS-1$ - } - } - - return new File(getFile(IAndroidTarget.SKINS), skinName); - } - - /** - * Currently always return a fixed list with "android.test.runner" in it. - *

- * TODO change the fixed library list to be build-dependent later. - * {@inheritDoc} - */ - @Override - public String[] getPlatformLibraries() { - return new String[] { SdkConstants.ANDROID_TEST_RUNNER_LIB }; - } - - /** - * The platform has no USB Vendor Id: always return {@link IAndroidTarget#NO_USB_ID}. - * {@inheritDoc} - */ - @Override - public int getUsbVendorId() { - return NO_USB_ID; - } - - @Override - public boolean canRunOn(IAndroidTarget target) { - // basic test - if (target == this) { - return true; - } - - // if the platform has a codename (ie it's a preview of an upcoming platform), then - // both platforms must be exactly identical. - if (mVersion.getCodename() != null) { - return mVersion.equals(target.getVersion()); - } - - // target is compatible wit the receiver as long as its api version number is greater or - // equal. - return target.getVersion().getApiLevel() >= mVersion.getApiLevel(); - } - - @Override - public String hashString() { - return AndroidTargetHash.getPlatformHashString(mVersion); - } - - @Override - public int hashCode() { - return hashString().hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof PlatformTarget) { - PlatformTarget platform = (PlatformTarget)obj; - - return mVersion.equals(platform.getVersion()); - } - - return false; - } - - /* - * Order by API level (preview/n count as between n and n+1). - * At the same API level, order as: Platform first, then add-on ordered by vendor and then name - * (non-Javadoc) - * @see java.lang.Comparable#compareTo(java.lang.Object) - */ - @Override - public int compareTo(IAndroidTarget target) { - // quick check. - if (this == target) { - return 0; - } - - int versionDiff = mVersion.compareTo(target.getVersion()); - - // only if the version are the same do we care about add-ons. - if (versionDiff == 0) { - // platforms go before add-ons. - if (target.isPlatform() == false) { - return -1; - } - } - - return versionDiff; - } - - /** - * Returns a string representation suitable for debugging. - * The representation is not intended for display to the user. - * - * The representation is also purposely compact. It does not describe _all_ the properties - * of the target, only a few key ones. - * - * @see #getDescription() - */ - @Override - public String toString() { - return String.format("PlatformTarget %1$s rev %2$d", //$NON-NLS-1$ - getVersion(), - getRevision()); - } - - @Override - public String getProperty(String name) { - return mProperties.get(name); - } - - @Override - public Integer getProperty(String name, Integer defaultValue) { - try { - String value = getProperty(name); - if (value != null) { - return Integer.decode(value); - } - } catch (NumberFormatException e) { - // ignore, return default value; - } - - return defaultValue; - } - - @Override - public Boolean getProperty(String name, Boolean defaultValue) { - String value = getProperty(name); - if (value != null) { - return Boolean.valueOf(value); - } - - return defaultValue; - } - - @Override - public Map getProperties() { - return mProperties; // mProperties is unmodifiable. - } - - // ---- platform only methods. - - public void setSkins(@NonNull File[] skins) { - mSkins = skins; - Arrays.sort(mSkins); - } - - public void setSamplesPath(String osLocation) { - mPaths.put(SAMPLES, osLocation); - } - - public void setSourcesPath(String osLocation) { - mPaths.put(SOURCES, osLocation); - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/avd/AvdInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/avd/AvdInfo.java deleted file mode 100644 index 1c682b32..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/avd/AvdInfo.java +++ /dev/null @@ -1,410 +0,0 @@ -/* - * 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.sdklib.internal.avd; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.prefs.AndroidLocation.AndroidLocationException; -import com.android.sdklib.IAndroidTarget; -import com.android.sdklib.ISystemImage; -import com.android.sdklib.SystemImage; -import com.android.sdklib.devices.Abi; -import com.android.sdklib.devices.Device; -import com.android.sdklib.repository.descriptors.IdDisplay; -import com.google.common.base.Charsets; -import com.google.common.io.Files; - -import java.io.File; -import java.io.IOException; -import java.util.Collections; -import java.util.Map; - -/** - * An immutable structure describing an Android Virtual Device. - */ -public final class AvdInfo implements Comparable { - - /** - * Status for an {@link AvdInfo}. Indicates whether or not this AVD is valid. - */ - public enum AvdStatus { - /** No error */ - OK, - /** Missing 'path' property in the ini file */ - ERROR_PATH, - /** Missing config.ini file in the AVD data folder */ - ERROR_CONFIG, - /** Missing 'target' property in the ini file */ - ERROR_TARGET_HASH, - /** Target was not resolved from its hash */ - ERROR_TARGET, - /** Unable to parse config.ini */ - ERROR_PROPERTIES, - /** System Image folder in config.ini doesn't exist */ - ERROR_IMAGE_DIR, - /** The {@link Device} this AVD is based on has changed from its original configuration*/ - ERROR_DEVICE_CHANGED, - /** The {@link Device} this AVD is based on is no longer available */ - ERROR_DEVICE_MISSING, - /** the {@link SystemImage} this AVD is based on is no longer available */ - ERROR_IMAGE_MISSING - } - - private final String mName; - private final File mIniFile; - private final String mFolderPath; - private final String mTargetHash; - private final IAndroidTarget mTarget; - private final String mAbiType; - /** An immutable map of properties. This must not be modified. Map can be empty. Never null. */ - private final Map mProperties; - private final AvdStatus mStatus; - private final IdDisplay mTag; - - /** - * Creates a new valid AVD info. Values are immutable. - *

- * Such an AVD is available and can be used. - * The error string is set to null. - * - * @param name The name of the AVD (for display or reference) - * @param iniFile The path to the config.ini file - * @param folderPath The path to the data directory - * @param targetHash the target hash - * @param target The target. Can be null, if the target was not resolved. - * @param tag The tag id/display. - * @param abiType Name of the abi. - * @param properties The property map. If null, an empty map will be created. - */ - public AvdInfo(@NonNull String name, - @NonNull File iniFile, - @NonNull String folderPath, - @NonNull String targetHash, - @Nullable IAndroidTarget target, - @NonNull IdDisplay tag, - @NonNull String abiType, - @Nullable Map properties) { - this(name, iniFile, folderPath, - targetHash, target, tag, abiType, - properties, AvdStatus.OK); - } - - /** - * Creates a new invalid AVD info. Values are immutable. - *

- * Such an AVD is not complete and cannot be used. - * The error string must be non-null. - * - * @param name The name of the AVD (for display or reference) - * @param iniFile The path to the config.ini file - * @param folderPath The path to the data directory - * @param targetHash the target hash - * @param target The target. Can be null, if the target was not resolved. - * @param tag The tag id/display. - * @param abiType Name of the abi. - * @param properties The property map. If null, an empty map will be created. - * @param status The {@link AvdStatus} of this AVD. Cannot be null. - */ - public AvdInfo(@NonNull String name, - @NonNull File iniFile, - @NonNull String folderPath, - @NonNull String targetHash, - @Nullable IAndroidTarget target, - @NonNull IdDisplay tag, - @NonNull String abiType, - @Nullable Map properties, - @NonNull AvdStatus status) { - mName = name; - mIniFile = iniFile; - mFolderPath = folderPath; - mTargetHash = targetHash; - mTarget = target; - mTag = tag; - mAbiType = abiType; - mProperties = properties == null ? Collections.emptyMap() - : Collections.unmodifiableMap(properties); - mStatus = status; - } - - /** Returns the name of the AVD. */ - @NonNull - public String getName() { - return mName; - } - - /** Returns the path of the AVD data directory. */ - @NonNull - public String getDataFolderPath() { - return mFolderPath; - } - - /** Returns the tag id/display of the AVD. */ - @NonNull - public IdDisplay getTag() { - return mTag; - } - - /** Returns the processor type of the AVD. */ - @NonNull - public String getAbiType() { - return mAbiType; - } - - @NonNull - public String getCpuArch() { - String cpuArch = mProperties.get(AvdManager.AVD_INI_CPU_ARCH); - if (cpuArch != null) { - return cpuArch; - } - - // legacy - return SdkConstants.CPU_ARCH_ARM; - } - - @NonNull - public String getDeviceManufacturer() { - String deviceManufacturer = mProperties.get(AvdManager.AVD_INI_DEVICE_MANUFACTURER); - if (deviceManufacturer != null && !deviceManufacturer.isEmpty()) { - return deviceManufacturer; - } - - return ""; // $NON-NLS-1$ - } - - @NonNull - public String getDeviceName() { - String deviceName = mProperties.get(AvdManager.AVD_INI_DEVICE_NAME); - if (deviceName != null && !deviceName.isEmpty()) { - return deviceName; - } - - return ""; // $NON-NLS-1$ - } - - /** Convenience function to return a more user friendly name of the abi type. */ - @NonNull - public static String getPrettyAbiType(@NonNull AvdInfo avdInfo) { - return getPrettyAbiType(avdInfo.getTag(), avdInfo.getAbiType()); - } - - /** Convenience function to return a more user friendly name of the abi type. */ - @NonNull - public static String getPrettyAbiType(@NonNull ISystemImage sysImg) { - return getPrettyAbiType(sysImg.getTag(), sysImg.getAbiType()); - } - - /** Convenience function to return a more user friendly name of the abi type. */ - @NonNull - public static String getPrettyAbiType(@NonNull IdDisplay tag, @NonNull String rawAbi) { - String s = ""; // $NON-NLS-1$ - - if (!SystemImage.DEFAULT_TAG.equals(tag)) { - s = tag.getDisplay() + ' '; - } - - Abi abi = Abi.getEnum(rawAbi); - s += (abi == null ? rawAbi : abi.getDisplayName()) + " (" + rawAbi + ')'; - - return s; - } - - /** - * Returns the target hash string. - */ - @NonNull - public String getTargetHash() { - return mTargetHash; - } - - /** Returns the target of the AVD, or null if it has not been resolved. */ - @Nullable - public IAndroidTarget getTarget() { - return mTarget; - } - - /** Returns the {@link AvdStatus} of the receiver. */ - @NonNull - public AvdStatus getStatus() { - return mStatus; - } - - /** - * Helper method that returns the default AVD folder that would be used for a given - * AVD name if and only if the AVD was created with the default choice. - *

- * Callers must NOT use this to "guess" the actual folder from an actual AVD since - * the purpose of the AVD .ini file is to be able to change this folder. Callers - * should however use this to create a new {@link AvdInfo} to setup its data folder - * to the default. - *

- * The default is {@code getDefaultAvdFolder()/avdname.avd/}. - *

- * For an actual existing AVD, callers must use {@link #getDataFolderPath()} instead. - * - * @param manager The AVD Manager, used to get the AVD storage path. - * @param avdName The name of the desired AVD. - * @param unique Whether to return the default or a unique variation of the default. - * @throws AndroidLocationException if there's a problem getting android root directory. - */ - @NonNull - public static File getDefaultAvdFolder(@NonNull AvdManager manager, @NonNull String avdName, - boolean unique) - throws AndroidLocationException { - String base = manager.getBaseAvdFolder(); - File result = new File(base, avdName + AvdManager.AVD_FOLDER_EXTENSION); - if (unique) { - int suffix = 0; - while (result.exists()) { - result = new File(base, String.format("%s_%d%s", avdName, (++suffix), - AvdManager.AVD_FOLDER_EXTENSION)); - } - } - return result; - } - - /** Compatibility forwarding until the usages in tools/swt are updated */ - @Deprecated - @NonNull - public static File getDefaultAvdFolder(@NonNull AvdManager manager, @NonNull String avdName) - throws AndroidLocationException{ - return getDefaultAvdFolder(manager, avdName, false); - } - - /** - * Helper method that returns the .ini {@link File} for a given AVD name. - *

- * The default is {@code getDefaultAvdFolder()/avdname.ini}. - * - * @param manager The AVD Manager, used to get the AVD storage path. - * @param avdName The name of the desired AVD. - * @throws AndroidLocationException if there's a problem getting android root directory. - */ - @NonNull - public static File getDefaultIniFile(@NonNull AvdManager manager, @NonNull String avdName) - throws AndroidLocationException { - String avdRoot = manager.getBaseAvdFolder(); - return new File(avdRoot, avdName + AvdManager.INI_EXTENSION); - } - - /** - * Returns the .ini {@link File} for this AVD. - */ - @NonNull - public File getIniFile() { - return mIniFile; - } - - /** - * Helper method that returns the Config {@link File} for a given AVD name. - */ - @NonNull - public static File getConfigFile(@NonNull String path) { - return new File(path, AvdManager.CONFIG_INI); - } - - /** - * Returns the Config {@link File} for this AVD. - */ - @NonNull - public File getConfigFile() { - return getConfigFile(mFolderPath); - } - - /** - * Returns an unmodifiable map of properties for the AVD. - * This can be empty but not null. - * Callers must NOT try to modify this immutable map. - */ - @NonNull - public Map getProperties() { - return mProperties; - } - - /** - * Returns the error message for the AVD or null if {@link #getStatus()} - * returns {@link AvdStatus#OK} - */ - @Nullable - public String getErrorMessage() { - switch (mStatus) { - case ERROR_PATH: - return String.format("Missing AVD 'path' property in %1$s", getIniFile()); - case ERROR_CONFIG: - return String.format("Missing config.ini file in %1$s", mFolderPath); - case ERROR_TARGET_HASH: - return String.format("Missing 'target' property in %1$s", getIniFile()); - case ERROR_TARGET: - return String.format("Unknown target '%1$s' in %2$s", - mTargetHash, getIniFile()); - case ERROR_PROPERTIES: - return String.format("Failed to parse properties from %1$s", - getConfigFile()); - case ERROR_IMAGE_DIR: - return String.format( - "Missing system image for %1$s%2$s %3$s. Run 'android update avd -n %4$s'", - SystemImage.DEFAULT_TAG.equals(mTag) ? "" : (mTag.getDisplay() + " "), - mAbiType, - mTarget.getFullName(), - mName); - case ERROR_DEVICE_CHANGED: - return String.format("%1$s %2$s configuration has changed since AVD creation", - mProperties.get(AvdManager.AVD_INI_DEVICE_MANUFACTURER), - mProperties.get(AvdManager.AVD_INI_DEVICE_NAME)); - case ERROR_DEVICE_MISSING: - return String.format("%1$s %2$s no longer exists as a device", - mProperties.get(AvdManager.AVD_INI_DEVICE_MANUFACTURER), - mProperties.get(AvdManager.AVD_INI_DEVICE_NAME)); - case OK: - assert false; - return null; - } - - return null; - } - - /** - * Compares this object with the specified object for order. Returns a - * negative integer, zero, or a positive integer as this object is less - * than, equal to, or greater than the specified object. - * - * @param o the Object to be compared. - * @return a negative integer, zero, or a positive integer as this object is - * less than, equal to, or greater than the specified object. - */ - @Override - public int compareTo(AvdInfo o) { - // first handle possible missing targets (if the AVD failed to load for unresolved targets) - if (mTarget == null && o != null && o.mTarget == null) { - return 0; - } if (mTarget == null) { - return +1; - } else if (o == null || o.mTarget == null) { - return -1; - } - - // then compare the targets - int targetDiff = mTarget.compareTo(o.mTarget); - - if (targetDiff == 0) { - // same target? compare on the avd name - return mName.compareTo(o.mName); - } - - return targetDiff; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/avd/AvdManager.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/avd/AvdManager.java deleted file mode 100644 index 11e6bbb5..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/avd/AvdManager.java +++ /dev/null @@ -1,2196 +0,0 @@ -/* - * 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.sdklib.internal.avd; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.annotations.VisibleForTesting; -import com.android.annotations.VisibleForTesting.Visibility; -import com.android.io.FileWrapper; -import com.android.io.IAbstractFile; -import com.android.io.StreamException; -import com.android.prefs.AndroidLocation; -import com.android.prefs.AndroidLocation.AndroidLocationException; -import com.android.sdklib.IAndroidTarget; -import com.android.sdklib.ISystemImage; -import com.android.sdklib.SdkManager; -import com.android.sdklib.SystemImage; -import com.android.sdklib.devices.Abi; -import com.android.sdklib.devices.Device; -import com.android.sdklib.devices.DeviceManager; -import com.android.sdklib.devices.DeviceManager.DeviceStatus; -import com.android.sdklib.internal.avd.AvdInfo.AvdStatus; -import com.android.sdklib.internal.project.ProjectProperties; -import com.android.sdklib.io.LegacyFileOp; -import com.android.sdklib.repository.descriptors.IdDisplay; -import com.android.sdklib.repository.local.LocalSdk; -import com.android.sdklib.repository.local.LocalSysImgPkgInfo; -import com.android.utils.GrabProcessOutput; -import com.android.utils.GrabProcessOutput.IProcessOutput; -import com.android.utils.GrabProcessOutput.Wait; -import com.android.utils.ILogger; -import com.android.utils.NullLogger; -import com.android.utils.Pair; -import com.google.common.base.Charsets; -import com.google.common.io.Closeables; -import com.google.common.io.Files; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.FileWriter; -import java.io.FilenameFilter; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.WeakHashMap; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Android Virtual Device Manager to manage AVDs. - */ -public class AvdManager { - - /** - * Exception thrown when something is wrong with a target path. - */ - private static final class InvalidTargetPathException extends Exception { - private static final long serialVersionUID = 1L; - - InvalidTargetPathException(String message) { - super(message); - } - } - - private static final Pattern INI_LINE_PATTERN = - Pattern.compile("^([a-zA-Z0-9._-]+)\\s*=\\s*(.*)\\s*$"); //$NON-NLS-1$ - - public static final String AVD_FOLDER_EXTENSION = ".avd"; //$NON-NLS-1$ - - /** Charset encoding used by the avd.ini/config.ini. */ - public static final String AVD_INI_ENCODING = "avd.ini.encoding"; //$NON-NLS-1$ - - /** - * The *absolute* path to the AVD folder (which contains the #CONFIG_INI file). - */ - public static final String AVD_INFO_ABS_PATH = "path"; //$NON-NLS-1$ - - /** - * The path to the AVD folder (which contains the #CONFIG_INI file) relative to - * the {@link AndroidLocation#FOLDER_DOT_ANDROID}. This information is written - * in the avd ini only if the AVD folder is located under the .android path - * (that is the relative that has no backward {@code ..} references). - */ - public static final String AVD_INFO_REL_PATH = "path.rel"; //$NON-NLS-1$ - - /** - * The {@link IAndroidTarget#hashString()} of the AVD. - */ - public static final String AVD_INFO_TARGET = "target"; //$NON-NLS-1$ - - /** - * AVD/config.ini key name representing the tag id of the specific avd - */ - public static final String AVD_INI_TAG_ID = "tag.id"; //$NON-NLS-1$ - - /** - * AVD/config.ini key name representing the tag display of the specific avd - */ - public static final String AVD_INI_TAG_DISPLAY = "tag.display"; //$NON-NLS-1$ - - /** - * AVD/config.ini key name representing the abi type of the specific avd - */ - public static final String AVD_INI_ABI_TYPE = "abi.type"; //$NON-NLS-1$ - - /** - * AVD/config.ini key name representing the CPU architecture of the specific avd - */ - public static final String AVD_INI_CPU_ARCH = "hw.cpu.arch"; //$NON-NLS-1$ - - /** - * AVD/config.ini key name representing the CPU architecture of the specific avd - */ - public static final String AVD_INI_CPU_MODEL = "hw.cpu.model"; //$NON-NLS-1$ - - /** - * AVD/config.ini key name representing the manufacturer of the device this avd was based on. - */ - public static final String AVD_INI_DEVICE_MANUFACTURER = "hw.device.manufacturer"; //$NON-NLS-1$ - - /** - * AVD/config.ini key name representing the name of the device this avd was based on. - */ - public static final String AVD_INI_DEVICE_NAME = "hw.device.name"; //$NON-NLS-1$ - - /** - * AVD/config.ini key name representing the SDK-relative path of the skin folder, if any, - * or a 320x480 like constant for a numeric skin size. - * - * @see #NUMERIC_SKIN_SIZE - */ - public static final String AVD_INI_SKIN_PATH = "skin.path"; //$NON-NLS-1$ - - /** - * AVD/config.ini key name representing the SDK-relative path of the skin folder to be selected if - * skins for this device become enabled. - */ - public static final String AVD_INI_BACKUP_SKIN_PATH = "skin.path.backup"; //$NON-NLS-1$ - - /** - * AVD/config.ini key name representing an UI name for the skin. - * This config key is ignored by the emulator. It is only used by the SDK manager or - * tools to give a friendlier name to the skin. - * If missing, use the {@link #AVD_INI_SKIN_PATH} key instead. - */ - public static final String AVD_INI_SKIN_NAME = "skin.name"; //$NON-NLS-1$ - - /** - * AVD/config.ini key name representing whether a dynamic skin should be displayed. - */ - public static final String AVD_INI_SKIN_DYNAMIC = "skin.dynamic"; //$NON-NLS-1$ - - /** - * AVD/config.ini key name representing the path to the sdcard file. - * If missing, the default name "sdcard.img" will be used for the sdcard, if there's such - * a file. - * - * @see #SDCARD_IMG - */ - public static final String AVD_INI_SDCARD_PATH = "sdcard.path"; //$NON-NLS-1$ - /** - * AVD/config.ini key name representing the size of the SD card. - * This property is for UI purposes only. It is not used by the emulator. - * - * @see #SDCARD_SIZE_PATTERN - * @see #parseSdcardSize(String, String[]) - */ - public static final String AVD_INI_SDCARD_SIZE = "sdcard.size"; //$NON-NLS-1$ - /** - * AVD/config.ini key name representing the first path where the emulator looks - * for system images. Typically this is the path to the add-on system image or - * the path to the platform system image if there's no add-on. - *

- * The emulator looks at {@link #AVD_INI_IMAGES_1} before {@link #AVD_INI_IMAGES_2}. - */ - public static final String AVD_INI_IMAGES_1 = "image.sysdir.1"; //$NON-NLS-1$ - /** - * AVD/config.ini key name representing the second path where the emulator looks - * for system images. Typically this is the path to the platform system image. - * - * @see #AVD_INI_IMAGES_1 - */ - public static final String AVD_INI_IMAGES_2 = "image.sysdir.2"; //$NON-NLS-1$ - /** - * AVD/config.ini key name representing the presence of the snapshots file. - * This property is for UI purposes only. It is not used by the emulator. - * - * @see #SNAPSHOTS_IMG - */ - public static final String AVD_INI_SNAPSHOT_PRESENT = "snapshot.present"; //$NON-NLS-1$ - - /** - * AVD/config.ini key name representing whether hardware OpenGLES emulation is enabled - */ - public static final String AVD_INI_GPU_EMULATION = "hw.gpu.enabled"; //$NON-NLS-1$ - - /** - * AVD/config.ini key name representing how to emulate the front facing camera - */ - public static final String AVD_INI_CAMERA_FRONT = "hw.camera.front"; //$NON-NLS-1$ - - /** - * AVD/config.ini key name representing how to emulate the rear facing camera - */ - public static final String AVD_INI_CAMERA_BACK = "hw.camera.back"; //$NON-NLS-1$ - - /** - * AVD/config.ini key name representing the amount of RAM the emulated device should have - */ - public static final String AVD_INI_RAM_SIZE = "hw.ramSize"; - - /** - * AVD/config.ini key name representing the amount of memory available to applications by default - */ - public static final String AVD_INI_VM_HEAP_SIZE = "vm.heapSize"; - - /** - * AVD/config.ini key name representing the size of the data partition - */ - public static final String AVD_INI_DATA_PARTITION_SIZE = "disk.dataPartition.size"; - - /** - * AVD/config.ini key name representing the hash of the device this AVD is based on.
- * This old hash is deprecated and shouldn't be used anymore. - * It represents the Device.hashCode() and is not stable accross implementations. - * @see #AVD_INI_DEVICE_HASH_V2 - */ - public static final String AVD_INI_DEVICE_HASH_V1 = "hw.device.hash"; - - /** - * AVD/config.ini key name representing the hash of the device hardware properties - * actually present in the config.ini. This replaces {@link #AVD_INI_DEVICE_HASH_V1}. - *

- * To find this hash, use - * {@code DeviceManager.getHardwareProperties(device).get(AVD_INI_DEVICE_HASH_V2)}. - */ - public static final String AVD_INI_DEVICE_HASH_V2 = "hw.device.hash2"; - - /** - * Pattern to match pixel-sized skin "names", e.g. "320x480". - */ - public static final Pattern NUMERIC_SKIN_SIZE = Pattern.compile("([0-9]{2,})x([0-9]{2,})"); //$NON-NLS-1$ - - private static final String USERDATA_IMG = "userdata.img"; //$NON-NLS-1$ - private static final String BOOT_PROP = "boot.prop"; //$NON-NLS-1$ - static final String CONFIG_INI = "config.ini"; //$NON-NLS-1$ - private static final String SDCARD_IMG = "sdcard.img"; //$NON-NLS-1$ - private static final String SNAPSHOTS_IMG = "snapshots.img"; //$NON-NLS-1$ - - static final String INI_EXTENSION = ".ini"; //$NON-NLS-1$ - private static final Pattern INI_NAME_PATTERN = Pattern.compile("(.+)\\" + //$NON-NLS-1$ - INI_EXTENSION + "$", //$NON-NLS-1$ - Pattern.CASE_INSENSITIVE); - - private static final Pattern IMAGE_NAME_PATTERN = Pattern.compile("(.+)\\.img$", //$NON-NLS-1$ - Pattern.CASE_INSENSITIVE); - - /** - * Pattern for matching SD Card sizes, e.g. "4K" or "16M". - * Callers should use {@link #parseSdcardSize(String, String[])} instead of using this directly. - */ - private static final Pattern SDCARD_SIZE_PATTERN = Pattern.compile("(\\d+)([KMG])"); //$NON-NLS-1$ - - /** - * Minimal size of an SDCard image file in bytes. Currently 9 MiB. - */ - - public static final long SDCARD_MIN_BYTE_SIZE = 9<<20; - /** - * Maximal size of an SDCard image file in bytes. Currently 1023 GiB. - */ - public static final long SDCARD_MAX_BYTE_SIZE = 1023L<<30; - - /** The sdcard string represents a valid number but the size is outside of the allowed range. */ - public static final int SDCARD_SIZE_NOT_IN_RANGE = 0; - /** The sdcard string looks like a size number+suffix but the number failed to decode. */ - public static final int SDCARD_SIZE_INVALID = -1; - /** The sdcard string doesn't look like a size, it might be a path instead. */ - public static final int SDCARD_NOT_SIZE_PATTERN = -2; - - /** Regex used to validate characters that compose an AVD name. */ - public static final Pattern RE_AVD_NAME = Pattern.compile("[a-zA-Z0-9._-]+"); //$NON-NLS-1$ - - /** List of valid characters for an AVD name. Used for display purposes. */ - public static final String CHARS_AVD_NAME = "a-z A-Z 0-9 . _ -"; //$NON-NLS-1$ - - public static final String HARDWARE_INI = "hardware.ini"; //$NON-NLS-1$ - - /** - * Status returned by {@link AvdManager#isAvdNameConflicting(String)}. - */ - public enum AvdConflict { - /** There is no known conflict for the given AVD name. */ - NO_CONFLICT, - /** The AVD name conflicts with an existing valid AVD. */ - CONFLICT_EXISTING_AVD, - /** The AVD name conflicts with an existing invalid AVD. */ - CONFLICT_INVALID_AVD, - /** - * The AVD name does not conflict with any known AVD however there are - * files or directory that would cause a conflict if this were to be created. - */ - CONFLICT_EXISTING_PATH, - } - - // A map where the keys are the locations of the SDK and the values are the corresponding - // AvdManagers. This prevents us from creating multiple AvdManagers for the same SDK and having - // them get out of sync. - private static final Map mManagers = - Collections.synchronizedMap(new WeakHashMap()); - - private final ArrayList mAllAvdList = new ArrayList(); - private AvdInfo[] mValidAvdList; - private AvdInfo[] mBrokenAvdList; - private final LocalSdk myLocalSdk; - private final Map myDeviceManagers = - new HashMap(); - - /** - * Creates an AVD Manager for a given SDK represented by a {@link LocalSdk}. - * @param localSdk The SDK. - * @param log The log object to receive the log of the initial loading of the AVDs. - * This log object is not kept by this instance of AvdManager and each - * method takes its own logger. The rationale is that the AvdManager - * might be called from a variety of context, each with different - * logging needs. Cannot be null. - * @throws AndroidLocationException - */ - protected AvdManager(@NonNull LocalSdk localSdk, @NonNull ILogger log) - throws AndroidLocationException { - myLocalSdk = localSdk; - buildAvdList(mAllAvdList, log); - } - - /** - * Returns an AVD Manager for a given SDK represented by a {@link LocalSdk}. - * One AVD Manager instance is created by SDK location and then cached and reused. - * - * @param localSdk The SDK. - * @param log The log object to receive the log of the initial loading of the AVDs. - * This log object is not kept by this instance of AvdManager and each - * method takes its own logger. The rationale is that the AvdManager - * might be called from a variety of context, each with different - * logging needs. Cannot be null. - * @throws AndroidLocationException - */ - @NonNull - public static AvdManager getInstance(@NonNull LocalSdk localSdk, @NonNull ILogger log) - throws AndroidLocationException { - synchronized(mManagers) { - AvdManager manager; - if ((manager = mManagers.get(localSdk.getLocation().getPath())) != null) { - return manager; - } - manager = new AvdManager(localSdk, log); - - mManagers.put(localSdk.getLocation().getPath(), manager); - return manager; - } - } - - /** - * Returns the base folder where AVDs are created. - * - * @throws AndroidLocationException - */ - @NonNull - public String getBaseAvdFolder() throws AndroidLocationException { - assert AndroidLocation.getFolder().endsWith(File.separator); - return AndroidLocation.getFolder() + AndroidLocation.FOLDER_AVD; - } - - /** - * Returns the {@link LocalSdk} associated with the {@link AvdManager}. - */ - @NonNull - public LocalSdk getLocalSdk() { - return myLocalSdk; - } - - /** - * Returns the {@link SdkManager} associated with the {@link AvdManager}. - * Note: This is temporary and will be removed as SdkManager is phased out. - * TODO: Remove this when SdkManager is removed - */ - @NonNull - @Deprecated - public SdkManager getSdkManager() { - return SdkManager.createManager(myLocalSdk.getPath(), NullLogger.getLogger()); - } - - /** - * Parse the sdcard string to decode the size. - * Returns: - *

    - *
  • The size in bytes > 0 if the sdcard string is a valid size in the allowed range. - *
  • {@link #SDCARD_SIZE_NOT_IN_RANGE} (0) - * if the sdcard string is a valid size NOT in the allowed range. - *
  • {@link #SDCARD_SIZE_INVALID} (-1) - * if the sdcard string is number that fails to parse correctly. - *
  • {@link #SDCARD_NOT_SIZE_PATTERN} (-2) - * if the sdcard string is not a number, in which case it's probably a file path. - *
- * - * @param sdcard The sdcard string, which can be a file path, a size string or something else. - * @param parsedStrings If non-null, an array of 2 strings. The first string will be - * filled with the parsed numeric size and the second one will be filled with the - * parsed suffix. This is filled even if the returned size is deemed out of range or - * failed to parse. The values are null if the sdcard is not a size pattern. - * @return A size in byte if > 0, or {@link #SDCARD_SIZE_NOT_IN_RANGE}, - * {@link #SDCARD_SIZE_INVALID} or {@link #SDCARD_NOT_SIZE_PATTERN} as error codes. - */ - public static long parseSdcardSize(@NonNull String sdcard, @Nullable String[] parsedStrings) { - - if (parsedStrings != null) { - assert parsedStrings.length == 2; - parsedStrings[0] = null; - parsedStrings[1] = null; - } - - Matcher m = SDCARD_SIZE_PATTERN.matcher(sdcard); - if (m.matches()) { - if (parsedStrings != null) { - assert parsedStrings.length == 2; - parsedStrings[0] = m.group(1); - parsedStrings[1] = m.group(2); - } - - // get the sdcard values for checks - try { - long sdcardSize = Long.parseLong(m.group(1)); - - String sdcardSizeModifier = m.group(2); - if ("K".equals(sdcardSizeModifier)) { //$NON-NLS-1$ - sdcardSize <<= 10; - } else if ("M".equals(sdcardSizeModifier)) { //$NON-NLS-1$ - sdcardSize <<= 20; - } else if ("G".equals(sdcardSizeModifier)) { //$NON-NLS-1$ - sdcardSize <<= 30; - } - - if (sdcardSize < SDCARD_MIN_BYTE_SIZE || - sdcardSize > SDCARD_MAX_BYTE_SIZE) { - return SDCARD_SIZE_NOT_IN_RANGE; - } - - return sdcardSize; - } catch (NumberFormatException e) { - // This could happen if the number is too large to fit in a long. - return SDCARD_SIZE_INVALID; - } - } - - return SDCARD_NOT_SIZE_PATTERN; - } - - /** - * Returns all the existing AVDs. - * @return a newly allocated array containing all the AVDs. - */ - @NonNull - public AvdInfo[] getAllAvds() { - synchronized (mAllAvdList) { - return mAllAvdList.toArray(new AvdInfo[mAllAvdList.size()]); - } - } - - /** - * Returns all the valid AVDs. - * @return a newly allocated array containing all valid the AVDs. - */ - @NonNull - public AvdInfo[] getValidAvds() { - synchronized (mAllAvdList) { - if (mValidAvdList == null) { - ArrayList list = new ArrayList(); - for (AvdInfo avd : mAllAvdList) { - if (avd.getStatus() == AvdStatus.OK) { - list.add(avd); - } - } - - mValidAvdList = list.toArray(new AvdInfo[list.size()]); - } - return mValidAvdList; - } - } - - /** - * Returns all the broken AVDs. - * @return a newly allocated array containing all the broken AVDs. - */ - @NonNull - public AvdInfo[] getBrokenAvds() { - synchronized (mAllAvdList) { - if (mBrokenAvdList == null) { - ArrayList list = new ArrayList(); - for (AvdInfo avd : mAllAvdList) { - if (avd.getStatus() != AvdStatus.OK) { - list.add(avd); - } - } - mBrokenAvdList = list.toArray(new AvdInfo[list.size()]); - } - return mBrokenAvdList; - } - } - - /** - * Returns the {@link AvdInfo} matching the given name. - *

- * The search is case-insensitive. - * - * @param name the name of the AVD to return - * @param validAvdOnly if true, only look through the list of valid AVDs. - * @return the matching AvdInfo or null if none were found. - */ - @Nullable - public AvdInfo getAvd(@Nullable String name, boolean validAvdOnly) { - - boolean ignoreCase = SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS; - - if (validAvdOnly) { - for (AvdInfo info : getValidAvds()) { - String name2 = info.getName(); - if (name2.equals(name) || (ignoreCase && name2.equalsIgnoreCase(name))) { - return info; - } - } - } else { - synchronized (mAllAvdList) { - for (AvdInfo info : mAllAvdList) { - String name2 = info.getName(); - if (name2.equals(name) || (ignoreCase && name2.equalsIgnoreCase(name))) { - return info; - } - } - } - } - - return null; - } - - /** - * Returns whether an emulator is currently running the AVD. - */ - public boolean isAvdRunning(@NonNull AvdInfo info) { - try { - String pid = getAvdPid(info); - if (pid != null) { - String command; - if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS) { - command = "cmd /c \"tasklist /FI \"PID eq " + pid + "\" | findstr " + pid - + "\""; - } else { - command = "kill -0 " + pid; - } - try { - Process p = Runtime.getRuntime().exec(command); - // If the process ends with non-0 it means the process doesn't exist - return p.waitFor() == 0; - } catch (IOException e) { - // To be safe return true - return true; - } catch (InterruptedException e) { - // To be safe return true - return true; - } - } - } - catch (IOException e) { - // To be safe return true - return true; - } - return false; - } - - public void stopAvd(@NonNull AvdInfo info) { - try { - String pid = getAvdPid(info); - if (pid != null) { - String command; - if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS) { - command = "cmd /c \"taskkill /PID " + pid + "\""; - } else { - command = "kill " + pid; - } - try { - Process p = Runtime.getRuntime().exec(command); - // If the process ends with non-0 it means the process doesn't exist - p.waitFor(); - } catch (IOException e) { - } catch (InterruptedException e) { - } - } - } - catch (IOException e) { - } - } - - private String getAvdPid(@NonNull AvdInfo info) throws IOException { - // this is a file on Unix, and a directory on Windows. - File f = new File(info.getDataFolderPath(), "userdata-qemu.img.lock"); //$NON-NLS-1$ - if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS) { - f = new File(f, "pid"); - } - if (f.exists()) { - return Files.toString(f, Charsets.UTF_8); - } - return null; - } - - - - /** - * Returns whether this AVD name would generate a conflict. - * - * @param name the name of the AVD to return - * @return A pair of {@link AvdConflict} and the path or AVD name that conflicts. - */ - @NonNull - public Pair isAvdNameConflicting(@Nullable String name) { - - boolean ignoreCase = SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS; - - // Check whether we have a conflict with an existing or invalid AVD - // known to the manager. - synchronized (mAllAvdList) { - for (AvdInfo info : mAllAvdList) { - String name2 = info.getName(); - if (name2.equals(name) || (ignoreCase && name2.equalsIgnoreCase(name))) { - if (info.getStatus() == AvdStatus.OK) { - return Pair.of(AvdConflict.CONFLICT_EXISTING_AVD, name2); - } else { - return Pair.of(AvdConflict.CONFLICT_INVALID_AVD, name2); - } - } - } - } - - // No conflict with known AVDs. - // Are some existing files/folders in the way of creating this AVD? - - try { - File file = AvdInfo.getDefaultIniFile(this, name); - if (file.exists()) { - return Pair.of(AvdConflict.CONFLICT_EXISTING_PATH, file.getPath()); - } - - file = AvdInfo.getDefaultAvdFolder(this, name, false); - if (file.exists()) { - return Pair.of(AvdConflict.CONFLICT_EXISTING_PATH, file.getPath()); - } - - } catch (AndroidLocationException e) { - // ignore - } - - - return Pair.of(AvdConflict.NO_CONFLICT, null); - } - - /** - * Reloads the AVD list. - * @param log the log object to receive action logs. Cannot be null. - * @throws AndroidLocationException if there was an error finding the location of the - * AVD folder. - */ - public void reloadAvds(@NonNull ILogger log) throws AndroidLocationException { - // build the list in a temp list first, in case the method throws an exception. - // It's better than deleting the whole list before reading the new one. - ArrayList allList = new ArrayList(); - buildAvdList(allList, log); - - synchronized (mAllAvdList) { - mAllAvdList.clear(); - mAllAvdList.addAll(allList); - mValidAvdList = mBrokenAvdList = null; - } - } - - /** - * Creates a new AVD. It is expected that there is no existing AVD with this name already. - * - * @param avdFolder the data folder for the AVD. It will be created as needed. - * Unless you want to locate it in a specific directory, the ideal default is - * {@code AvdManager.AvdInfo.getAvdFolder}. - * @param avdName the name of the AVD - * @param target the target of the AVD - * @param tag the tag of the AVD - * @param abiType the abi type of the AVD - * @param skinFolder the skin folder path to use, if specified. Can be null. - * @param skinName the name of the skin. Can be null. Must have been verified by caller. - * Can be a size in the form "NNNxMMM" or a directory name matching skinFolder. - * @param sdcard the parameter value for the sdCard. Can be null. This is either a path to - * an existing sdcard image or a sdcard size (\d+, \d+K, \dM). - * @param hardwareConfig the hardware setup for the AVD. Can be null to use defaults. - * @param bootProps the optional boot properties for the AVD. Can be null. - * @param createSnapshot If true copy a blank snapshot image into the AVD. - * @param removePrevious If true remove any previous files. - * @param editExisting If true, edit an existing AVD, changing only the minimum required. - * This won't remove files unless required or unless {@code removePrevious} is set. - * @param log the log object to receive action logs. Cannot be null. - * @return The new {@link AvdInfo} in case of success (which has just been added to the - * internal list) or null in case of failure. - */ - @Nullable - public AvdInfo createAvd( - @NonNull File avdFolder, - @NonNull String avdName, - @NonNull IAndroidTarget target, - @NonNull IdDisplay tag, - @NonNull String abiType, - @Nullable File skinFolder, - @Nullable String skinName, - @Nullable String sdcard, - @Nullable Map hardwareConfig, - @Nullable Map bootProps, - boolean createSnapshot, - boolean removePrevious, - boolean editExisting, - @NonNull ILogger log) { - if (log == null) { - throw new IllegalArgumentException("log cannot be null"); - } - - File iniFile = null; - boolean needCleanup = false; - try { - if (avdFolder.exists()) { - if (removePrevious) { - // AVD already exists and removePrevious is set, try to remove the - // directory's content first (but not the directory itself). - try { - deleteContentOf(avdFolder); - } catch (SecurityException e) { - log.error(e, "Failed to delete %1$s", avdFolder.getAbsolutePath()); - } - } else if (!editExisting) { - // AVD shouldn't already exist if removePrevious is false and - // we're not editing an existing AVD. - log.error(null, - "Folder %1$s is in the way. Use --force if you want to overwrite.", - avdFolder.getAbsolutePath()); - return null; - } - } else { - // create the AVD folder. - avdFolder.mkdir(); - // We're not editing an existing AVD. - editExisting = false; - } - - // actually write the ini file - iniFile = createAvdIniFile(avdName, avdFolder, target, removePrevious); - - // writes the userdata.img in it. - - File userdataSrc = null; - - // Look for a system image in the add-on. - // If we don't find one there, look in the base platform. - ISystemImage systemImage = target.getSystemImage(tag, abiType); - - if (systemImage != null) { - File imageFolder = systemImage.getLocation(); - userdataSrc = new File(imageFolder, USERDATA_IMG); - } - - if ((userdataSrc == null || !userdataSrc.exists()) && !target.isPlatform()) { - // If we don't find a system-image in the add-on, look into the platform. - - systemImage = target.getParent().getSystemImage(tag, abiType); - if (systemImage != null) { - File imageFolder = systemImage.getLocation(); - userdataSrc = new File(imageFolder, USERDATA_IMG); - } - } - - if (userdataSrc == null || !userdataSrc.exists()) { - log.error(null, - "Unable to find a '%1$s' file for ABI %2$s to copy into the AVD folder.", - USERDATA_IMG, - abiType); - needCleanup = true; - return null; - } - - File userdataDest = new File(avdFolder, USERDATA_IMG); - - copyImageFile(userdataSrc, userdataDest); - - if (userdataDest.exists() == false) { - log.error(null, "Unable to create '%1$s' file in the AVD folder.", - userdataDest); - needCleanup = true; - return null; - } - - // Config file. - HashMap values = new HashMap(); - - if (setImagePathProperties(target, tag, abiType, values, log) == false) { - log.error(null, "Failed to set image path properties in the AVD folder."); - needCleanup = true; - return null; - } - - // Create the snapshot file - if (createSnapshot) { - File snapshotDest = new File(avdFolder, SNAPSHOTS_IMG); - if (snapshotDest.isFile() && editExisting) { - log.info("Snapshot image already present, was not changed.\n"); - - } else { - File toolsLib = new File(myLocalSdk.getLocation(), - SdkConstants.OS_SDK_TOOLS_LIB_EMULATOR_FOLDER); - File snapshotBlank = new File(toolsLib, SNAPSHOTS_IMG); - if (snapshotBlank.exists() == false) { - log.error(null, - "Unable to find a '%2$s%1$s' file to copy into the AVD folder.", - SNAPSHOTS_IMG, toolsLib); - needCleanup = true; - return null; - } - - copyImageFile(snapshotBlank, snapshotDest); - } - values.put(AVD_INI_SNAPSHOT_PRESENT, "true"); - } - - // Now the tag & abi type - values.put(AVD_INI_TAG_ID, tag.getId()); - values.put(AVD_INI_TAG_DISPLAY, tag.getDisplay()); - values.put(AVD_INI_ABI_TYPE, abiType); - - // and the cpu arch. - Abi abi = Abi.getEnum(abiType); - if (abi != null) { - values.put(AVD_INI_CPU_ARCH, abi.getCpuArch()); - - String model = abi.getCpuModel(); - if (model != null) { - values.put(AVD_INI_CPU_MODEL, model); - } - } else { - log.error(null, - "ABI %1$s is not supported by this version of the SDK Tools", abiType); - needCleanup = true; - return null; - } - - // Now the skin. - String skinPath = null; - - if (skinFolder == null && skinName == null) { - // Nothing specified. Use the default from the target. - skinFolder = target.getDefaultSkin(); - } - - if (skinFolder == null && skinName != null && - NUMERIC_SKIN_SIZE.matcher(skinName).matches()) { - // Numeric skin size. Set both skinPath and skinName to the same size. - skinPath = skinName; - - } else if (skinFolder != null && skinName == null) { - // Skin folder is specified, but not skin name. Adjust it. - skinName = skinFolder.getName(); - - } else if (skinFolder == null && skinName != null) { - // skin folder is not specified, but there's a non-numeric skin name. - // check whether the skin is in the target. - skinFolder = getSkinFolder(skinName, target); - } - - if (skinFolder != null) { - // skin does not exist! - if (!skinFolder.exists()) { - log.error(null, "Skin '%1$s' does not exist.", skinName); - return null; - } - - // if skinFolder is in the sdk, use the relative path - if (skinFolder.getPath().startsWith(myLocalSdk.getLocation().getPath())) { - try { - skinPath = LegacyFileOp.makeRelative(myLocalSdk.getLocation(), skinFolder); - } catch (IOException e) { - // In case it fails, just use the absolute path - skinPath = skinFolder.getAbsolutePath(); - } - } else { - // Skin isn't in the sdk. Just use the absolute path. - skinPath = skinFolder.getAbsolutePath(); - } - } - - // Set skin.name for display purposes in the AVD manager and - // set skin.path for use by the emulator. - if (skinName != null) { - values.put(AVD_INI_SKIN_NAME, skinName); - } - if (skinPath != null) { - values.put(AVD_INI_SKIN_PATH, skinPath); - } - - if (sdcard != null && !sdcard.isEmpty()) { - // Sdcard is possibly a size. In that case we create a file called 'sdcard.img' - // in the AVD folder, and do not put any value in config.ini. - - long sdcardSize = parseSdcardSize(sdcard, null/*parsedStrings*/); - - if (sdcardSize == SDCARD_SIZE_NOT_IN_RANGE) { - log.error(null, "SD Card size must be in the range 9 MiB..1023 GiB."); - needCleanup = true; - return null; - - } else if (sdcardSize == SDCARD_SIZE_INVALID) { - log.error(null, "Unable to parse SD Card size"); - needCleanup = true; - return null; - - } else if (sdcardSize == SDCARD_NOT_SIZE_PATTERN) { - File sdcardFile = new File(sdcard); - if (sdcardFile.isFile()) { - // sdcard value is an external sdcard, so we put its path into the config.ini - values.put(AVD_INI_SDCARD_PATH, sdcard); - } else { - log.error(null, "'%1$s' is not recognized as a valid sdcard value.\n" - + "Value should be:\n" + "1. path to an sdcard.\n" - + "2. size of the sdcard to create: [K|M]", sdcard); - needCleanup = true; - return null; - } - } else { - // create the sdcard. - File sdcardFile = new File(avdFolder, SDCARD_IMG); - - boolean runMkSdcard = true; - if (sdcardFile.exists()) { - if (sdcardFile.length() == sdcardSize && editExisting) { - // There's already an sdcard file with the right size and we're - // not overriding it... so don't remove it. - runMkSdcard = false; - log.info("SD Card already present with same size, was not changed.\n"); - } - } - - if (runMkSdcard) { - String path = sdcardFile.getAbsolutePath(); - - // execute mksdcard with the proper parameters. - File toolsFolder = new File(myLocalSdk.getLocation(), - SdkConstants.FD_TOOLS); - File mkSdCard = new File(toolsFolder, SdkConstants.mkSdCardCmdName()); - - if (mkSdCard.isFile() == false) { - log.error(null, "'%1$s' is missing from the SDK tools folder.", - mkSdCard.getName()); - needCleanup = true; - return null; - } - - if (createSdCard(mkSdCard.getAbsolutePath(), sdcard, path, log) == false) { - log.error(null, "Failed to create sdcard in the AVD folder."); - needCleanup = true; - return null; // mksdcard output has already been displayed, no need to - // output anything else. - } - } - - // add a property containing the size of the sdcard for display purpose - // only when the dev does 'android list avd' - values.put(AVD_INI_SDCARD_SIZE, sdcard); - } - } - - // add the hardware config to the config file. - // priority order is: - // - values provided by the user - // - values provided by the skin - // - values provided by the target (add-on only). - // - values provided by the sys img - // In order to follow this priority, we'll add the lowest priority values first and then - // override by higher priority values. - // In the case of a platform with override values from the user, the skin value might - // already be there, but it's ok. - - HashMap finalHardwareValues = new HashMap(); - - FileWrapper sysImgHardwareFile = new FileWrapper(systemImage.getLocation(), - AvdManager.HARDWARE_INI); - if (sysImgHardwareFile.isFile()) { - Map targetHardwareConfig = ProjectProperties.parsePropertyFile( - sysImgHardwareFile, log); - - if (targetHardwareConfig != null) { - finalHardwareValues.putAll(targetHardwareConfig); - values.putAll(targetHardwareConfig); - } - } - - FileWrapper targetHardwareFile = new FileWrapper(target.getLocation(), - AvdManager.HARDWARE_INI); - if (targetHardwareFile.isFile()) { - Map targetHardwareConfig = ProjectProperties.parsePropertyFile( - targetHardwareFile, log); - - if (targetHardwareConfig != null) { - finalHardwareValues.putAll(targetHardwareConfig); - values.putAll(targetHardwareConfig); - } - } - - // get the hardware properties for this skin - if (skinFolder != null) { - FileWrapper skinHardwareFile = new FileWrapper(skinFolder, AvdManager.HARDWARE_INI); - if (skinHardwareFile.isFile()) { - Map skinHardwareConfig = - ProjectProperties.parsePropertyFile(skinHardwareFile, log); - - if (skinHardwareConfig != null) { - finalHardwareValues.putAll(skinHardwareConfig); - values.putAll(skinHardwareConfig); - } - } - } - - // finally put the hardware provided by the user. - if (hardwareConfig != null) { - finalHardwareValues.putAll(hardwareConfig); - values.putAll(hardwareConfig); - } - - File configIniFile = new File(avdFolder, CONFIG_INI); - writeIniFile(configIniFile, values, true); - - if (bootProps != null && !bootProps.isEmpty()) { - File bootPropsFile = new File(avdFolder, BOOT_PROP); - writeIniFile(bootPropsFile, bootProps, false); - } - - // Generate the log report first because we want to control where line breaks - // are located when generating the hardware config list. - StringBuilder report = new StringBuilder(); - - if (target.isPlatform()) { - if (editExisting) { - report.append(String.format("Updated AVD '%1$s' based on %2$s", - avdName, target.getName())); - } else { - report.append(String.format("Created AVD '%1$s' based on %2$s", - avdName, target.getName())); - } - } else { - if (editExisting) { - report.append(String.format("Updated AVD '%1$s' based on %2$s (%3$s)", avdName, - target.getName(), target.getVendor())); - } else { - report.append(String.format("Created AVD '%1$s' based on %2$s (%3$s)", avdName, - target.getName(), target.getVendor())); - } - } - report.append(String.format(", %s processor", AvdInfo.getPrettyAbiType(tag, abiType))); - - // display the chosen hardware config - if (!finalHardwareValues.isEmpty()) { - report.append(",\nwith the following hardware config:\n"); - List keys = new ArrayList(finalHardwareValues.keySet()); - Collections.sort(keys); - for (String key : keys) { - String value = finalHardwareValues.get(key); - report.append(String.format("%s=%s\n", key, value)); - } - } else { - report.append("\n"); - } - - log.info(report.toString()); - - // create the AvdInfo object, and add it to the list - AvdInfo newAvdInfo = new AvdInfo( - avdName, - iniFile, - avdFolder.getAbsolutePath(), - target.hashString(), - target, - tag, abiType, - values); - - AvdInfo oldAvdInfo = getAvd(avdName, false /*validAvdOnly*/); - - synchronized (mAllAvdList) { - if (oldAvdInfo != null && (removePrevious || editExisting)) { - mAllAvdList.remove(oldAvdInfo); - } - mAllAvdList.add(newAvdInfo); - mValidAvdList = mBrokenAvdList = null; - } - - if ((removePrevious || editExisting) && - newAvdInfo != null && - oldAvdInfo != null && - !oldAvdInfo.getDataFolderPath().equals(newAvdInfo.getDataFolderPath())) { - log.warning("Removing previous AVD directory at %s", - oldAvdInfo.getDataFolderPath()); - // Remove the old data directory - File dir = new File(oldAvdInfo.getDataFolderPath()); - try { - deleteContentOf(dir); - dir.delete(); - } catch (SecurityException e) { - log.error(e, "Failed to delete %1$s", dir.getAbsolutePath()); - } - } - - return newAvdInfo; - } catch (AndroidLocationException e) { - log.error(e, null); - } catch (IOException e) { - log.error(e, null); - } catch (SecurityException e) { - log.error(e, null); - } finally { - if (needCleanup) { - if (iniFile != null && iniFile.exists()) { - iniFile.delete(); - } - - try { - deleteContentOf(avdFolder); - avdFolder.delete(); - } catch (SecurityException e) { - log.error(e, "Failed to delete %1$s", avdFolder.getAbsolutePath()); - } - } - } - - return null; - } - - /** - * Copy the nominated file to the given destination. - * - * @throws FileNotFoundException - * @throws IOException - */ - private void copyImageFile(@NonNull File source, @NonNull File destination) - throws FileNotFoundException, IOException { - FileInputStream fis = new FileInputStream(source); - FileOutputStream fos = new FileOutputStream(destination); - - byte[] buffer = new byte[4096]; - int count; - while ((count = fis.read(buffer)) != -1) { - fos.write(buffer, 0, count); - } - - fos.close(); - fis.close(); - } - - /** - * Returns the path to the target images folder as a relative path to the SDK, if the folder - * is not empty. If the image folder is empty or does not exist, null is returned. - * @throws InvalidTargetPathException if the target image folder is not in the current SDK. - */ - private String getImageRelativePath(@NonNull IAndroidTarget target, - @NonNull IdDisplay tag, - @NonNull String abiType) - throws InvalidTargetPathException { - - ISystemImage systemImage = target.getSystemImage(tag, abiType); - if (systemImage == null) { - // ABI Type is unknown for target - return null; - } - - File folder = systemImage.getLocation(); - String imageFullPath = folder.getAbsolutePath(); - - // make this path relative to the SDK location - String sdkLocation = myLocalSdk.getPath(); - if (!imageFullPath.startsWith(sdkLocation)) { - // this really really should not happen. - assert false; - throw new InvalidTargetPathException("Target location is not inside the SDK."); - } - - if (folder.isDirectory()) { - String[] list = folder.list(new FilenameFilter() { - @Override - public boolean accept(File dir, String name) { - return IMAGE_NAME_PATTERN.matcher(name).matches(); - } - }); - - if (list.length > 0) { - // Remove the SDK root path, e.g. /sdk/dir1/dir2 => /dir1/dir2 - imageFullPath = imageFullPath.substring(sdkLocation.length()); - // The path is relative, so it must not start with a file separator - if (imageFullPath.charAt(0) == File.separatorChar) { - imageFullPath = imageFullPath.substring(1); - } - // For compatibility with previous versions, we denote folders - // by ending the path with file separator - if (!imageFullPath.endsWith(File.separator)) { - imageFullPath += File.separator; - } - - return imageFullPath; - } - } - - return null; - } - - /** - * Returns the path to the skin, as a relative path to the SDK. - * @param skinName The name of the skin to find. Case-sensitive. - * @param target The target where to find the skin. - * @param log the log object to receive action logs. Cannot be null. - */ - @Deprecated - private String getSkinRelativePath(@NonNull String skinName, - @NonNull IAndroidTarget target, - @NonNull ILogger log) { - if (log == null) { - throw new IllegalArgumentException("log cannot be null"); - } - - // first look to see if the skin is in the target - File skin = getSkinFolder(skinName, target); - - // skin really does not exist! - if (skin.exists() == false) { - log.error(null, "Skin '%1$s' does not exist.", skinName); - return null; - } - - // get the skin path - String path = skin.getAbsolutePath(); - - // make this path relative to the SDK location - - String sdkLocation = myLocalSdk.getPath(); - if (path.startsWith(sdkLocation) == false) { - // this really really should not happen. - log.error(null, "Target location is not inside the SDK."); - assert false; - return null; - } - - path = path.substring(sdkLocation.length()); - if (path.charAt(0) == File.separatorChar) { - path = path.substring(1); - } - return path; - } - - /** - * Returns the full absolute OS path to a skin specified by name for a given target. - * @param skinName The name of the skin to find. Case-sensitive. - * @param target The target where to find the skin. - * @return a {@link File} that may or may not actually exist. - */ - private File getSkinFolder(@NonNull String skinName, @NonNull IAndroidTarget target) { - String path = target.getPath(IAndroidTarget.SKINS); - File skin = new File(path, skinName); - - if (skin.exists() == false && target.isPlatform() == false) { - target = target.getParent(); - - path = target.getPath(IAndroidTarget.SKINS); - skin = new File(path, skinName); - } - - return skin; - } - - /** - * Creates the ini file for an AVD. - * - * @param name of the AVD. - * @param avdFolder path for the data folder of the AVD. - * @param target of the AVD. - * @param removePrevious True if an existing ini file should be removed. - * @throws AndroidLocationException if there's a problem getting android root directory. - * @throws IOException if {@link File#getAbsolutePath()} fails. - */ - private File createAvdIniFile(@NonNull String name, - @NonNull File avdFolder, - @NonNull IAndroidTarget target, - boolean removePrevious) - throws AndroidLocationException, IOException { - File iniFile = AvdInfo.getDefaultIniFile(this, name); - - if (removePrevious) { - if (iniFile.isFile()) { - iniFile.delete(); - } else if (iniFile.isDirectory()) { - deleteContentOf(iniFile); - iniFile.delete(); - } - } - - String absPath = avdFolder.getAbsolutePath(); - String relPath = null; - String androidPath = AndroidLocation.getFolder(); - if (absPath.startsWith(androidPath)) { - // Compute the AVD path relative to the android path. - assert androidPath.endsWith(File.separator); - relPath = absPath.substring(androidPath.length()); - } - - HashMap values = new HashMap(); - if (relPath != null) { - values.put(AVD_INFO_REL_PATH, relPath); - } - values.put(AVD_INFO_ABS_PATH, absPath); - values.put(AVD_INFO_TARGET, target.hashString()); - writeIniFile(iniFile, values, true); - - return iniFile; - } - - /** - * Creates the ini file for an AVD. - * - * @param info of the AVD. - * @throws AndroidLocationException if there's a problem getting android root directory. - * @throws IOException if {@link File#getAbsolutePath()} fails. - */ - private File createAvdIniFile(@NonNull AvdInfo info) - throws AndroidLocationException, IOException { - return createAvdIniFile(info.getName(), - new File(info.getDataFolderPath()), - info.getTarget(), - false /*removePrevious*/); - } - - /** - * Actually deletes the files of an existing AVD. - *

- * This also remove it from the manager's list, The caller does not need to - * call {@link #removeAvd(AvdInfo)} afterwards. - *

- * This method is designed to somehow work with an unavailable AVD, that is an AVD that - * could not be loaded due to some error. That means this method still tries to remove - * the AVD ini file or its folder if it can be found. An error will be output if any of - * these operations fail. - * - * @param avdInfo the information on the AVD to delete - * @param log the log object to receive action logs. Cannot be null. - * @return True if the AVD was deleted with no error. - */ - public boolean deleteAvd(@NonNull AvdInfo avdInfo, @NonNull ILogger log) { - try { - boolean error = false; - - File f = avdInfo.getIniFile(); - if (f != null && f.exists()) { - log.info("Deleting file %1$s\n", f.getCanonicalPath()); - if (!f.delete()) { - log.error(null, "Failed to delete %1$s\n", f.getCanonicalPath()); - error = true; - } - } - - String path = avdInfo.getDataFolderPath(); - if (path != null) { - f = new File(path); - if (f.exists()) { - log.info("Deleting folder %1$s\n", f.getCanonicalPath()); - if (deleteContentOf(f) == false || f.delete() == false) { - log.error(null, "Failed to delete %1$s\n", f.getCanonicalPath()); - error = true; - } - } - } - - removeAvd(avdInfo); - - if (error) { - log.info("\nAVD '%1$s' deleted with errors. See errors above.\n", - avdInfo.getName()); - } else { - log.info("\nAVD '%1$s' deleted.\n", avdInfo.getName()); - return true; - } - - } catch (IOException e) { - log.error(e, null); - } catch (SecurityException e) { - log.error(e, null); - } - return false; - } - - /** - * Moves and/or rename an existing AVD and its files. - * This also change it in the manager's list. - *

- * The caller should make sure the name or path given are valid, do not exist and are - * actually different than current values. - * - * @param avdInfo the information on the AVD to move. - * @param newName the new name of the AVD if non null. - * @param paramFolderPath the new data folder if non null. - * @param log the log object to receive action logs. Cannot be null. - * @return True if the move succeeded or there was nothing to do. - * If false, this method will have had already output error in the log. - */ - public boolean moveAvd(@NonNull AvdInfo avdInfo, - @Nullable String newName, - @Nullable String paramFolderPath, - @NonNull ILogger log) { - - try { - if (paramFolderPath != null) { - File f = new File(avdInfo.getDataFolderPath()); - log.warning("Moving '%1$s' to '%2$s'.", - avdInfo.getDataFolderPath(), - paramFolderPath); - if (!f.renameTo(new File(paramFolderPath))) { - log.error(null, "Failed to move '%1$s' to '%2$s'.", - avdInfo.getDataFolderPath(), paramFolderPath); - return false; - } - - // update AVD info - AvdInfo info = new AvdInfo( - avdInfo.getName(), - avdInfo.getIniFile(), - paramFolderPath, - avdInfo.getTargetHash(), - avdInfo.getTarget(), - avdInfo.getTag(), - avdInfo.getAbiType(), - avdInfo.getProperties()); - replaceAvd(avdInfo, info); - - // update the ini file - createAvdIniFile(info); - } - - if (newName != null) { - File oldIniFile = avdInfo.getIniFile(); - File newIniFile = AvdInfo.getDefaultIniFile(this, newName); - - log.warning("Moving '%1$s' to '%2$s'.", oldIniFile.getPath(), newIniFile.getPath()); - if (!oldIniFile.renameTo(newIniFile)) { - log.error(null, "Failed to move '%1$s' to '%2$s'.", - oldIniFile.getPath(), newIniFile.getPath()); - return false; - } - - // update AVD info - AvdInfo info = new AvdInfo( - newName, - avdInfo.getIniFile(), - avdInfo.getDataFolderPath(), - avdInfo.getTargetHash(), - avdInfo.getTarget(), - avdInfo.getTag(), - avdInfo.getAbiType(), - avdInfo.getProperties()); - replaceAvd(avdInfo, info); - } - - log.info("AVD '%1$s' moved.\n", avdInfo.getName()); - - } catch (AndroidLocationException e) { - log.error(e, null); - } catch (IOException e) { - log.error(e, null); - } - - // nothing to do or succeeded - return true; - } - - /** - * Helper method to recursively delete a folder's content (but not the folder itself). - * - * @throws SecurityException like {@link File#delete()} does if file/folder is not writable. - */ - private boolean deleteContentOf(File folder) throws SecurityException { - File[] files = folder.listFiles(); - if (files != null) { - for (File f : files) { - if (f.isDirectory()) { - if (deleteContentOf(f) == false) { - return false; - } - } - if (f.delete() == false) { - return false; - } - - } - } - - return true; - } - - /** - * Returns a list of files that are potential AVD ini files. - *

- * This lists the $HOME/.android/avd/.ini files. - * Such files are properties file than then indicate where the AVD folder is located. - *

- * Note: the method is to be considered private. It is made protected so that - * unit tests can easily override the AVD root. - * - * @return A new {@link File} array or null. The array might be empty. - * @throws AndroidLocationException if there's a problem getting android root directory. - */ - private File[] buildAvdFilesList() throws AndroidLocationException { - File folder = new File(getBaseAvdFolder()); - - // ensure folder validity. - if (folder.isFile()) { - throw new AndroidLocationException( - String.format("%1$s is not a valid folder.", folder.getAbsolutePath())); - } else if (folder.exists() == false) { - // folder is not there, we create it and return - folder.mkdirs(); - return null; - } - - File[] avds = folder.listFiles(new FilenameFilter() { - @Override - public boolean accept(File parent, String name) { - if (INI_NAME_PATTERN.matcher(name).matches()) { - // check it's a file and not a folder - boolean isFile = new File(parent, name).isFile(); - return isFile; - } - - return false; - } - }); - - return avds; - } - - /** - * Computes the internal list of available AVDs - * @param allList the list to contain all the AVDs - * @param log the log object to receive action logs. Cannot be null. - * - * @throws AndroidLocationException if there's a problem getting android root directory. - */ - private void buildAvdList(ArrayList allList, ILogger log) - throws AndroidLocationException { - File[] avds = buildAvdFilesList(); - if (avds != null) { - for (File avd : avds) { - AvdInfo info = parseAvdInfo(avd, log); - if (info != null && !allList.contains(info)) { - allList.add(info); - } - } - } - } - - private DeviceManager getDeviceManager(ILogger logger) { - DeviceManager manager = myDeviceManagers.get(logger); - if (manager == null) { - manager = DeviceManager.createInstance(myLocalSdk.getLocation(), logger); - manager.registerListener(new DeviceManager.DevicesChangedListener() { - @Override - public void onDevicesChanged() { - myDeviceManagers.clear(); - } - }); - myDeviceManagers.put(logger, manager); - } - return manager; - } - - /** - * Parses an AVD .ini file to create an {@link AvdInfo}. - * - * @param iniPath The path to the AVD .ini file - * @param log the log object to receive action logs. Cannot be null. - * @return A new {@link AvdInfo} with an {@link AvdStatus} indicating whether this AVD is - * valid or not. - */ - private AvdInfo parseAvdInfo(File iniPath, ILogger log) { - Map map = parseIniFile( - new FileWrapper(iniPath), - log); - - String avdPath = map.get(AVD_INFO_ABS_PATH); - String targetHash = map.get(AVD_INFO_TARGET); - - if (!(new File(avdPath).isDirectory())) { - // Try to fallback on the relative path, if present. - String relPath = map.get(AVD_INFO_REL_PATH); - if (relPath != null) { - try { - String androidPath = AndroidLocation.getFolder(); - File f = new File(androidPath, relPath); - if (f.isDirectory()) { - avdPath = f.getAbsolutePath(); - } - } catch (AndroidLocationException ignore) {} - } - } - - IAndroidTarget target = null; - FileWrapper configIniFile = null; - Map properties = null; - - if (targetHash != null) { - target = myLocalSdk.getTargetFromHashString(targetHash); - } - - // load the AVD properties. - if (avdPath != null) { - configIniFile = new FileWrapper(avdPath, CONFIG_INI); - } - - if (configIniFile != null) { - if (!configIniFile.isFile()) { - log.warning("Missing file '%1$s'.", configIniFile.getPath()); - } else { - properties = parseIniFile(configIniFile, log); - } - } - - // get name - String name = iniPath.getName(); - Matcher matcher = INI_NAME_PATTERN.matcher(iniPath.getName()); - if (matcher.matches()) { - name = matcher.group(1); - } - - // get tag - IdDisplay tag = SystemImage.DEFAULT_TAG; - String tagId = properties == null ? null : properties.get(AVD_INI_TAG_ID); - if (tagId != null) { - String tagDisp = properties == null ? null : properties.get(AVD_INI_TAG_DISPLAY); - if (tagDisp == null || tagDisp.isEmpty()) { - tagDisp = LocalSysImgPkgInfo.tagIdToDisplay(tagId); - } - tag = new IdDisplay(tagId, tagDisp); - } - - // get abi type - String abiType = properties == null ? null : properties.get(AVD_INI_ABI_TYPE); - // for the avds created previously without enhancement, i.e. They are created based - // on previous API Levels. They are supposed to have ARM processor type - if (abiType == null) { - abiType = SdkConstants.ABI_ARMEABI; - } - - // check the image.sysdir are valid - boolean validImageSysdir = true; - if (properties != null) { - String imageSysDir = properties.get(AVD_INI_IMAGES_1); - if (imageSysDir != null) { - File f = new File(myLocalSdk.getLocation(), imageSysDir); - if (f.isDirectory() == false) { - validImageSysdir = false; - } else { - imageSysDir = properties.get(AVD_INI_IMAGES_2); - if (imageSysDir != null) { - f = new File(myLocalSdk.getLocation(), imageSysDir); - if (f.isDirectory() == false) { - validImageSysdir = false; - } - } - } - } - } - - // Check the system image from the target - ISystemImage sysImage = target != null ? target.getSystemImage(tag, abiType) : null; - - // Get the device status if this AVD is associated with a device - DeviceStatus deviceStatus = null; - boolean updateHashV2 = false; - if (properties != null) { - String deviceName = properties.get(AVD_INI_DEVICE_NAME); - String deviceMfctr = properties.get(AVD_INI_DEVICE_MANUFACTURER); - - Device d = null; - - if (deviceName != null && deviceMfctr != null) { - DeviceManager devMan = getDeviceManager(log); - d = devMan.getDevice(deviceName, deviceMfctr); - deviceStatus = d == null ? DeviceStatus.MISSING : DeviceStatus.EXISTS; - - if (d != null) { - updateHashV2 = true; - String hashV2 = properties.get(AVD_INI_DEVICE_HASH_V2); - if (hashV2 != null) { - String newHashV2 = DeviceManager.hasHardwarePropHashChanged(d, hashV2); - if (newHashV2 == null) { - updateHashV2 = false; - } else { - properties.put(AVD_INI_DEVICE_HASH_V2, newHashV2); - } - } - - String hashV1 = properties.get(AVD_INI_DEVICE_HASH_V1); - if (hashV1 != null) { - // will recompute a hash v2 and save it below - properties.remove(AVD_INI_DEVICE_HASH_V1); - } - } - } - } - - - // TODO: What about missing sdcard, skins, etc? - - AvdStatus status; - - if (avdPath == null) { - status = AvdStatus.ERROR_PATH; - } else if (configIniFile == null) { - status = AvdStatus.ERROR_CONFIG; - } else if (targetHash == null) { - status = AvdStatus.ERROR_TARGET_HASH; - } else if (target == null) { - status = AvdStatus.ERROR_TARGET; - } else if (properties == null) { - status = AvdStatus.ERROR_PROPERTIES; - } else if (validImageSysdir == false) { - status = AvdStatus.ERROR_IMAGE_DIR; - } else if (deviceStatus == DeviceStatus.CHANGED) { - status = AvdStatus.ERROR_DEVICE_CHANGED; - } else if (deviceStatus == DeviceStatus.MISSING) { - status = AvdStatus.ERROR_DEVICE_MISSING; - } else if (sysImage == null) { - status = AvdStatus.ERROR_IMAGE_MISSING; - } else { - status = AvdStatus.OK; - } - - AvdInfo info = new AvdInfo( - name, - iniPath, - avdPath, - targetHash, - target, - tag, - abiType, - properties, - status); - - if (updateHashV2) { - try { - return updateDeviceChanged(info, log); - } catch (IOException ignore) {} - } - - return info; - } - - /** - * Writes a .ini file from a set of properties, using UTF-8 encoding. - * The keys are sorted. - * The file should be read back later by {@link #parseIniFile(IAbstractFile, ILogger)}. - * - * @param iniFile The file to generate. - * @param values The properties to place in the ini file. - * @param addEncoding When true, add a property {@link #AVD_INI_ENCODING} indicating the - * encoding used to write the file. - * @throws IOException if {@link FileWriter} fails to open, write or close the file. - */ - private static void writeIniFile(File iniFile, Map values, boolean addEncoding) - throws IOException { - - Charset charset = Charsets.UTF_8; - OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(iniFile), charset); - try { - if (addEncoding) { - // Write down the charset used in case we want to use it later. - writer.write(String.format("%1$s=%2$s\n", AVD_INI_ENCODING, charset.name())); - } - - ArrayList keys = new ArrayList(values.keySet()); - Collections.sort(keys); - - for (String key : keys) { - String value = values.get(key); - writer.write(String.format("%1$s=%2$s\n", key, value)); - } - } finally { - writer.close(); - } - } - - /** - * Parses a property file and returns a map of the content. - *

- * If the file is not present, null is returned with no error messages sent to the log. - *

- * Charset encoding will be either the system's default or the one specified by the - * {@link #AVD_INI_ENCODING} key if present. - * - * @param propFile the property file to parse - * @param log the ILogger object receiving warning/error from the parsing. - * @return the map of (key,value) pairs, or null if the parsing failed. - */ - private static Map parseIniFile( - @NonNull IAbstractFile propFile, - @Nullable ILogger log) { - return parseIniFileImpl(propFile, log, null /*charset*/); - } - - /** - * Implementation helper for the {@link #parseIniFile(IAbstractFile, ILogger)} method. - * Don't call this one directly. - * - * @param propFile the property file to parse - * @param log the ILogger object receiving warning/error from the parsing. - * @param charset When a specific charset is specified, this will be used as-is. - * When null, the default charset will first be used and if the key - * {@link #AVD_INI_ENCODING} is found the parsing will restart using that specific - * charset. - * @return the map of (key,value) pairs, or null if the parsing failed. - */ - private static Map parseIniFileImpl( - @NonNull IAbstractFile propFile, - @Nullable ILogger log, - @Nullable Charset charset) { - - BufferedReader reader = null; - try { - boolean canChangeCharset = false; - if (charset == null) { - canChangeCharset = true; - charset = Charsets.ISO_8859_1; - } - reader = new BufferedReader(new InputStreamReader(propFile.getContents(), charset)); - - String line = null; - Map map = new HashMap(); - while ((line = reader.readLine()) != null) { - line = line.trim(); - if (!line.isEmpty() && line.charAt(0) != '#') { - - Matcher m = INI_LINE_PATTERN.matcher(line); - if (m.matches()) { - // Note: we do NOT escape values. - String key = m.group(1); - String value = m.group(2); - - // If we find the charset encoding and it's not the same one and - // it's a valid one, re-read the file using that charset. - if (canChangeCharset && - AVD_INI_ENCODING.equals(key) && - !charset.name().equals(value) && - Charset.isSupported(value)) { - charset = Charset.forName(value); - return parseIniFileImpl(propFile, log, charset); - } - - map.put(key, value); - } else { - if (log != null) { - log.warning("Error parsing '%1$s': \"%2$s\" is not a valid syntax", - propFile.getOsLocation(), - line); - } - return null; - } - } - } - - return map; - } catch (FileNotFoundException e) { - // this should not happen since we usually test the file existence before - // calling the method. - // Return null below. - } catch (IOException e) { - if (log != null) { - log.warning("Error parsing '%1$s': %2$s.", - propFile.getOsLocation(), - e.getMessage()); - } - } catch (StreamException e) { - if (log != null) { - log.warning("Error parsing '%1$s': %2$s.", - propFile.getOsLocation(), - e.getMessage()); - } - } finally { - try { - Closeables.close(reader, true /* swallowIOException */); - } catch (IOException e) { - // cannot happen. - } - } - - return null; - } - - /** - * Invokes the tool to create a new SD card image file. - * - * @param toolLocation The path to the mksdcard tool. - * @param size The size of the new SD Card, compatible with {@link #SDCARD_SIZE_PATTERN}. - * @param location The path of the new sdcard image file to generate. - * @param log the log object to receive action logs. Cannot be null. - * @return True if the sdcard could be created. - */ - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected boolean createSdCard(String toolLocation, String size, String location, ILogger log) { - try { - String[] command = new String[3]; - command[0] = toolLocation; - command[1] = size; - command[2] = location; - Process process = Runtime.getRuntime().exec(command); - - final ArrayList errorOutput = new ArrayList(); - final ArrayList stdOutput = new ArrayList(); - - int status = GrabProcessOutput.grabProcessOutput( - process, - Wait.WAIT_FOR_READERS, - new IProcessOutput() { - @Override - public void out(@Nullable String line) { - if (line != null) { - stdOutput.add(line); - } - } - - @Override - public void err(@Nullable String line) { - if (line != null) { - errorOutput.add(line); - } - } - }); - - if (status == 0) { - return true; - } else { - for (String error : errorOutput) { - log.error(null, error); - } - } - - } catch (InterruptedException e) { - // pass, print error below - } catch (IOException e) { - // pass, print error below - } - - log.error(null, "Failed to create the SD card."); - return false; - } - - /** - * Removes an {@link AvdInfo} from the internal list. - * - * @param avdInfo The {@link AvdInfo} to remove. - * @return true if this {@link AvdInfo} was present and has been removed. - */ - public boolean removeAvd(AvdInfo avdInfo) { - synchronized (mAllAvdList) { - if (mAllAvdList.remove(avdInfo)) { - mValidAvdList = mBrokenAvdList = null; - return true; - } - } - - return false; - } - - /** - * Updates an AVD with new path to the system image folders. - * @param name the name of the AVD to update. - * @param log the log object to receive action logs. Cannot be null. - * @throws IOException - */ - public void updateAvd(String name, ILogger log) throws IOException { - // find the AVD to update. It should be be in the broken list. - AvdInfo avd = null; - synchronized (mAllAvdList) { - for (AvdInfo info : mAllAvdList) { - if (info.getName().equals(name)) { - avd = info; - break; - } - } - } - - if (avd == null) { - // not in the broken list, just return. - log.error(null, "There is no Android Virtual Device named '%s'.", name); - return; - } - - updateAvd(avd, log); - } - - - /** - * Updates an AVD with new path to the system image folders. - * @param avd the AVD to update. - * @param log the log object to receive action logs. Cannot be null. - * @throws IOException - */ - public AvdInfo updateAvd(AvdInfo avd, ILogger log) throws IOException { - // get the properties. This is a unmodifiable Map. - Map oldProperties = avd.getProperties(); - - // create a new map - Map properties = new HashMap(); - if (oldProperties != null) { - properties.putAll(oldProperties); - } - - AvdStatus status; - - // create the path to the new system images. - if (setImagePathProperties(avd.getTarget(), - avd.getTag(), - avd.getAbiType(), - properties, - log)) { - if (properties.containsKey(AVD_INI_IMAGES_1)) { - log.info("Updated '%1$s' with value '%2$s'\n", AVD_INI_IMAGES_1, - properties.get(AVD_INI_IMAGES_1)); - } - - if (properties.containsKey(AVD_INI_IMAGES_2)) { - log.info("Updated '%1$s' with value '%2$s'\n", AVD_INI_IMAGES_2, - properties.get(AVD_INI_IMAGES_2)); - } - - status = AvdStatus.OK; - } else { - log.error(null, "Unable to find non empty system images folders for %1$s", - avd.getName()); - //FIXME: display paths to empty image folders? - status = AvdStatus.ERROR_IMAGE_DIR; - } - - return updateAvd(avd, properties, status, log); - } - - public AvdInfo updateAvd(AvdInfo avd, - Map newProperties, - AvdStatus status, - ILogger log) throws IOException { - // now write the config file - File configIniFile = new File(avd.getDataFolderPath(), CONFIG_INI); - writeIniFile(configIniFile, newProperties, true); - - // finally create a new AvdInfo for this unbroken avd and add it to the list. - // instead of creating the AvdInfo object directly we reparse it, to detect other possible - // errors - // FIXME: We may want to create this AvdInfo by reparsing the AVD instead. This could detect other errors. - AvdInfo newAvd = new AvdInfo( - avd.getName(), - avd.getIniFile(), - avd.getDataFolderPath(), - avd.getTargetHash(), - avd.getTarget(), - avd.getTag(), - avd.getAbiType(), - newProperties); - - replaceAvd(avd, newAvd); - - return newAvd; - } - - /** - * Updates the device-specific part of an AVD ini. - * @param avd the AVD to update. - * @param log the log object to receive action logs. Cannot be null. - * @return The new AVD on success. - * @throws IOException - */ - public AvdInfo updateDeviceChanged(AvdInfo avd, ILogger log) throws IOException { - - // Overwrite the properties derived from the device and nothing else - Map properties = new HashMap(avd.getProperties()); - - DeviceManager devMan = getDeviceManager(log); - Collection devices = devMan.getDevices(DeviceManager.ALL_DEVICES); - String name = properties.get(AvdManager.AVD_INI_DEVICE_NAME); - String manufacturer = properties.get(AvdManager.AVD_INI_DEVICE_MANUFACTURER); - - if (properties != null && devices != null && name != null && manufacturer != null) { - for (Device d : devices) { - if (d.getId().equals(name) && d.getManufacturer().equals(manufacturer)) { - properties.putAll(DeviceManager.getHardwareProperties(d)); - try { - return updateAvd(avd, properties, AvdStatus.OK, log); - } catch (IOException e) { - log.error(e, null); - } - } - } - } else { - log.error(null, "Base device information incomplete or missing."); - } - return null; - } - - /** - * Sets the paths to the system images in a properties map. - * - * @param target the target in which to find the system images. - * @param abiType the abi type of the avd to find - * the architecture-dependent system images. - * @param properties the properties in which to set the paths. - * @param log the log object to receive action logs. Cannot be null. - * @return true if success, false if some path are missing. - */ - private boolean setImagePathProperties(IAndroidTarget target, - IdDisplay tag, - String abiType, - Map properties, - ILogger log) { - properties.remove(AVD_INI_IMAGES_1); - properties.remove(AVD_INI_IMAGES_2); - - try { - String property = AVD_INI_IMAGES_1; - - // First the image folders of the target itself - String imagePath = getImageRelativePath(target, tag, abiType); - if (imagePath != null) { - properties.put(property, imagePath); - property = AVD_INI_IMAGES_2; - } - - // If the target is an add-on we need to add the Platform image as a backup. - IAndroidTarget parent = target.getParent(); - if (parent != null) { - imagePath = getImageRelativePath(parent, tag, abiType); - if (imagePath != null) { - properties.put(property, imagePath); - } - } - - // we need at least one path! - return properties.containsKey(AVD_INI_IMAGES_1); - } catch (InvalidTargetPathException e) { - log.error(e, e.getMessage()); - } - - return false; - } - - /** - * Replaces an old {@link AvdInfo} with a new one in the lists storing them. - * @param oldAvd the {@link AvdInfo} to remove. - * @param newAvd the {@link AvdInfo} to add. - */ - private void replaceAvd(AvdInfo oldAvd, AvdInfo newAvd) { - synchronized (mAllAvdList) { - mAllAvdList.remove(oldAvd); - mAllAvdList.add(newAvd); - mValidAvdList = mBrokenAvdList = null; - } - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/avd/HardwareProperties.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/avd/HardwareProperties.java deleted file mode 100644 index 6b91b459..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/avd/HardwareProperties.java +++ /dev/null @@ -1,327 +0,0 @@ -/* - * 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.sdklib.internal.avd; - -import com.android.utils.ILogger; -import com.google.common.base.Charsets; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStreamReader; -import java.util.Map; -import java.util.TreeMap; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class HardwareProperties { - /** AVD/config.ini key for whether hardware buttons are present. */ - public static final String HW_MAINKEYS = "hw.mainKeys"; - - /** AVD/config.ini key indicating whether trackball is present. */ - public static final String HW_TRACKBALL = "hw.trackBall"; - - /** AVD/config.ini key indicating whether qwerty keyboard is present. */ - public static final String HW_KEYBOARD = "hw.keyboard"; - - /** AVD/config.ini key indicating whether dpad is present. */ - public static final String HW_DPAD = "hw.dPad"; - - /** AVD/config.ini key indicating whether gps is present. */ - public static final String HW_GPS = "hw.gps"; - - /** AVD/config.ini key indicating whether the device is running on battery. */ - public static final String HW_BATTERY = "hw.battery"; - - /** AVD/config.ini key indicating whether accelerometer is present. */ - public static final String HW_ACCELEROMETER = "hw.accelerometer"; - - /** AVD/config.ini key indicating whether gyroscope is present. */ - public static final String HW_ORIENTATION_SENSOR = "hw.sensors.orientation"; - - /** AVD/config.ini key indicating whether h/w mic is present. */ - public static final String HW_AUDIO_INPUT = "hw.audioInput"; - - /** AVD/config.ini key indicating whether sdcard is present. */ - public static final String HW_SDCARD = "hw.sdCard"; - - /** AVD/config.ini key for LCD density. */ - public static final String HW_LCD_DENSITY = "hw.lcd.density"; - - /** AVD/config.ini key indicating whether proximity sensor present. */ - public static final String HW_PROXIMITY_SENSOR = "hw.sensors.proximity"; - - /** AVD/config.ini key for initial device orientation. */ - public static final String HW_INITIAL_ORIENTATION = "hw.initialOrientation"; - - - private static final Pattern PATTERN_PROP = Pattern.compile( - "^([a-zA-Z0-9._-]+)\\s*=\\s*(.*)\\s*$"); - - /** Property name in the generated avd config file; String; e.g. "hw.screen" */ - private static final String HW_PROP_NAME = "name"; //$NON-NLS-1$ - /** Property type, one of {@link HardwarePropertyType} */ - private static final String HW_PROP_TYPE = "type"; //$NON-NLS-1$ - /** Default value of the property. String matching the property type. */ - private static final String HW_PROP_DEFAULT = "default"; //$NON-NLS-1$ - /** User-visible name of the property. String. */ - private static final String HW_PROP_ABSTRACT = "abstract"; //$NON-NLS-1$ - /** User-visible description of the property. String. */ - private static final String HW_PROP_DESC = "description"; //$NON-NLS-1$ - /** Comma-separate values for a property of type "enum" */ - private static final String HW_PROP_ENUM = "enum"; //$NON-NLS-1$ - - public static final String BOOLEAN_YES = "yes"; - public static final String BOOLEAN_NO = "no"; - public static final Pattern DISKSIZE_PATTERN = Pattern.compile("\\d+[MK]B"); //$NON-NLS-1$ - - /** Represents the type of a hardware property value. */ - public enum HardwarePropertyType { - INTEGER ("integer", false /*isEnum*/), //$NON-NLS-1$ - BOOLEAN ("boolean", false /*isEnum*/), //$NON-NLS-1$ - DISKSIZE ("diskSize", false /*isEnum*/), //$NON-NLS-1$ - STRING ("string", false /*isEnum*/), //$NON-NLS-1$ - INTEGER_ENUM("integer", true /*isEnum*/), //$NON-NLS-1$ - STRING_ENUM ("string", true /*isEnum*/); //$NON-NLS-1$ - - - private String mName; - private boolean mIsEnum; - - HardwarePropertyType(String name, boolean isEnum) { - mName = name; - mIsEnum = isEnum; - } - - /** Returns the name of the type (e.g. "string", "boolean", etc.) */ - public String getName() { - return mName; - } - - /** Indicates whether this type is an enum (e.g. "enum of strings"). */ - public boolean isEnum() { - return mIsEnum; - } - - /** Returns the internal HardwarePropertyType object matching the given type name. */ - public static HardwarePropertyType getEnum(String name, boolean isEnum) { - for (HardwarePropertyType type : values()) { - if (type.mName.equals(name) && type.mIsEnum == isEnum) { - return type; - } - } - - return null; - } - } - - public static final class HardwareProperty { - private String mName; - private HardwarePropertyType mType; - /** the string representation of the default value. can be null. */ - private String mDefault; - /** the choices for an enum. Null if not an enum. */ - private String[] mEnum; - private String mAbstract; - private String mDescription; - - public HardwareProperty() { - // initialize strings to sane defaults, as not all properties will be set from - // the ini file - mName = ""; - mDefault = ""; - mAbstract = ""; - mDescription = ""; - } - - /** Returns the hardware config name of the property, e.g. "hw.screen" */ - public String getName() { - return mName; - } - - /** Returns the property type, one of {@link HardwarePropertyType} */ - public HardwarePropertyType getType() { - return mType; - } - - /** - * Returns the default value of the property. - * String matching the property type. - * Can be null. - */ - public String getDefault() { - return mDefault; - } - - /** Returns the user-visible name of the property. */ - public String getAbstract() { - return mAbstract; - } - - /** Returns the user-visible description of the property. */ - public String getDescription() { - return mDescription; - } - - /** Returns the possible values for an enum property. Can be null. */ - public String[] getEnum() { - return mEnum; - } - - public boolean isValidForUi() { - // don't display single string type for now. - return mType != HardwarePropertyType.STRING || mType.isEnum(); - } - } - - /** - * Parses the hardware definition file. - * @param file the property file to parse - * @param log the ILogger object receiving warning/error from the parsing. Cannot be null. - * @return the map of (key,value) pairs, or null if the parsing failed. - */ - public static Map parseHardwareDefinitions(File file, ILogger log) { - BufferedReader reader = null; - try { - FileInputStream fis = new FileInputStream(file); - reader = new BufferedReader(new InputStreamReader(fis, Charsets.UTF_8)); - - Map map = new TreeMap(); - - String line = null; - HardwareProperty prop = null; - while ((line = reader.readLine()) != null) { - if (!line.isEmpty() && line.charAt(0) != '#') { - Matcher m = PATTERN_PROP.matcher(line); - if (m.matches()) { - String key = m.group(1); - String value = m.group(2); - - if (HW_PROP_NAME.equals(key)) { - prop = new HardwareProperty(); - prop.mName = value; - map.put(prop.mName, prop); - } - - if (prop == null) { - log.warning("Error parsing '%1$s': missing '%2$s'", - file.getAbsolutePath(), HW_PROP_NAME); - return null; - } - - if (HW_PROP_TYPE.equals(key)) { - // Note: we don't know yet whether this type is an enum. - // This is indicated by the "enum = value" line that is parsed later. - prop.mType = HardwarePropertyType.getEnum(value, false); - assert (prop.mType != null); - } else if (HW_PROP_DEFAULT.equals(key)) { - prop.mDefault = value; - } else if (HW_PROP_ABSTRACT.equals(key)) { - prop.mAbstract = value; - } else if (HW_PROP_DESC.equals(key)) { - prop.mDescription = value; - } else if (HW_PROP_ENUM.equals(key)) { - if (!prop.mType.isEnum()) { - // Change the type to an enum, if valid. - prop.mType = HardwarePropertyType.getEnum(prop.mType.getName(), - true); - assert (prop.mType != null); - } - - // Sanitize input: trim spaces, ignore empty entries. - String[] v = value.split(","); - int n = 0; - for (int i = 0; i < v.length; i++) { - String s = v[i] = v[i].trim(); - if (!s.isEmpty()) { - n++; - } - } - prop.mEnum = new String[n]; - n = 0; - for (int i = 0; i < v.length; i++) { - String s = v[i]; - if (!s.isEmpty()) { - prop.mEnum[n++] = s; - } - } - } - } else { - log.warning("Error parsing '%1$s': \"%2$s\" is not a valid syntax", - file.getAbsolutePath(), line); - return null; - } - } - } - - return map; - } catch (FileNotFoundException e) { - // this should not happen since we usually test the file existence before - // calling the method. - // Return null below. - } catch (IOException e) { - log.warning("Error parsing '%1$s': %2$s.", file.getAbsolutePath(), - e.getMessage()); - } finally { - if (reader != null) { - try { - reader.close(); - } catch (IOException e) { - // ignore - } - } - } - - return null; - } - - /** - * Returns the boolean value matching the given index. - * This is the reverse of {@link #getBooleanValueIndex(String)}. - * - * @param index 0 or 1. - * @return {@link #BOOLEAN_YES} for 0 or {@link #BOOLEAN_NO} for 1. - * @throws IndexOutOfBoundsException if index is neither 0 nor 1. - */ - public static String getBooleanValue(int index) { - if (index == 0) { - return BOOLEAN_YES; - } else if (index == 1) { - return BOOLEAN_NO; - } - throw new IndexOutOfBoundsException("HardwareProperty boolean index must 0 (true) or 1 (false) but was " + index); - } - - /** - * Returns the index of a boolean value. - * This if the reverse of {@link #getBooleanValue(int)}. - * - * @param value Either {@link #BOOLEAN_YES} or {@link #BOOLEAN_NO}. - * @return 0 for {@link #BOOLEAN_YES}, 1 for {@link #BOOLEAN_NO} or -1 for anything else. - */ - public static int getBooleanValueIndex(String value) { - if (BOOLEAN_YES.equals(value)) { - return 0; - } else if (BOOLEAN_NO.equals(value)) { - return 1; - } - - return -1; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/BuildConfig.template b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/BuildConfig.template deleted file mode 100644 index 0344b55c..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/BuildConfig.template +++ /dev/null @@ -1,6 +0,0 @@ -/** Automatically generated file. DO NOT MODIFY */ -package #PACKAGE#; - -public final class BuildConfig { - public final static boolean DEBUG = #DEBUG#; -} \ No newline at end of file diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/BuildConfigGenerator.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/BuildConfigGenerator.java deleted file mode 100644 index 96fa976d..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/BuildConfigGenerator.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright (C) 2011 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.sdklib.internal.build; - -import com.android.SdkConstants; - -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; - -/** - * Class able to generate a BuildConfig class in Android project. - * The BuildConfig class contains constants related to the build target. - * - * @deprecated Use Android-Builder instead - */ -@Deprecated -public class BuildConfigGenerator { - - public static final String BUILD_CONFIG_NAME = "BuildConfig.java"; - - private static final String PH_PACKAGE = "#PACKAGE#"; - private static final String PH_DEBUG = "#DEBUG#"; - - private final String mGenFolder; - private final String mAppPackage; - private final boolean mDebug; - - /** - * Creates a generator - * @param genFolder the gen folder of the project - * @param appPackage the application package - * @param debug whether it's a debug build - */ - public BuildConfigGenerator(String genFolder, String appPackage, boolean debug) { - mGenFolder = genFolder; - mAppPackage = appPackage; - mDebug = debug; - } - - /** - * Returns a File representing where the BuildConfig class will be. - */ - public File getFolderPath() { - File genFolder = new File(mGenFolder); - return new File(genFolder, mAppPackage.replace('.', File.separatorChar)); - } - - public File getBuildConfigFile() { - File folder = getFolderPath(); - return new File(folder, BUILD_CONFIG_NAME); - } - - /** - * Generates the BuildConfig class. - */ - public void generate() throws IOException { - String template = readEmbeddedTextFile("BuildConfig.template"); - - Map map = new HashMap(); - map.put(PH_PACKAGE, mAppPackage); - map.put(PH_DEBUG, Boolean.toString(mDebug)); - - String content = replaceParameters(template, map); - - File pkgFolder = getFolderPath(); - if (pkgFolder.isDirectory() == false) { - pkgFolder.mkdirs(); - } - - File buildConfigJava = new File(pkgFolder, BUILD_CONFIG_NAME); - writeFile(buildConfigJava, content); - } - - /** - * Reads and returns the content of a text file embedded in the jar file. - * @param filepath the file path to the text file - * @return null if the file could not be read - * @throws IOException - */ - private String readEmbeddedTextFile(String filepath) throws IOException { - InputStream is = BuildConfigGenerator.class.getResourceAsStream(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(); - } - - // this really shouldn't happen unless the sdklib packaging is broken. - throw new IOException("BuildConfig template is missing!"); - } - - private void writeFile(File file, String content) throws IOException { - FileOutputStream fos = null; - try { - fos = new FileOutputStream(file); - InputStream source = new ByteArrayInputStream(content.getBytes(SdkConstants.UTF_8)); - - byte[] buffer = new byte[1024]; - int count = 0; - while ((count = source.read(buffer)) != -1) { - fos.write(buffer, 0, count); - } - } finally { - if (fos != null) { - fos.close(); - } - } - } - - /** - * Replaces placeholders found in a string with values. - * - * @param str the string to search for placeholders. - * @param parameters a map of to search for in the string - * @return A new String object with the placeholder replaced by the values. - */ - private String replaceParameters(String str, Map parameters) { - - for (Entry entry : parameters.entrySet()) { - String value = entry.getValue(); - if (value != null) { - str = str.replaceAll(entry.getKey(), value); - } - } - - return str; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/DebugKeyProvider.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/DebugKeyProvider.java deleted file mode 100644 index 0f492eab..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/DebugKeyProvider.java +++ /dev/null @@ -1,215 +0,0 @@ -/* - * 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.sdklib.internal.build; - -import com.android.prefs.AndroidLocation; -import com.android.prefs.AndroidLocation.AndroidLocationException; - -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.UnrecoverableEntryException; -import java.security.UnrecoverableKeyException; -import java.security.cert.Certificate; -import java.security.cert.CertificateException; - -/** - * A provider of a dummy key to sign Android application for debugging purpose. - *

This provider uses a custom keystore to create and store a key with a known password. - * - * @deprecated Use Android-Builder instead - */ -@Deprecated -public class DebugKeyProvider { - - public interface IKeyGenOutput { - void out(String message); - void err(String message); - } - - private static final String PASSWORD_STRING = "android"; - private static final char[] PASSWORD_CHAR = PASSWORD_STRING.toCharArray(); - private static final String DEBUG_ALIAS = "AndroidDebugKey"; - - // Certificate CN value. This is a hard-coded value for the debug key. - // Android Market checks against this value in order to refuse applications signed with - // debug keys. - private static final String CERTIFICATE_DESC = "CN=Android Debug,O=Android,C=US"; - - private KeyStore.PrivateKeyEntry mEntry; - - public static class KeytoolException extends Exception { - /** default serial uid */ - private static final long serialVersionUID = 1L; - private String mJavaHome = null; - private String mCommandLine = null; - - KeytoolException(String message) { - super(message); - } - - KeytoolException(String message, String javaHome, String commandLine) { - super(message); - - mJavaHome = javaHome; - mCommandLine = commandLine; - } - - public String getJavaHome() { - return mJavaHome; - } - - public String getCommandLine() { - return mCommandLine; - } - } - - /** - * Creates a provider using a keystore at the given location. - *

The keystore, and a new random android debug key are created if they do not yet exist. - *

Password for the store/key is android, and the key alias is - * AndroidDebugKey. - * @param osKeyStorePath the OS path to the keystore, or null if the default one - * is to be used. - * @param storeType an optional keystore type, or null if the default is to - * be used. - * @param output an optional {@link IKeyGenOutput} object to get the stdout and stderr - * of the keytool process call. - * @throws KeytoolException If the creation of the debug key failed. - * @throws AndroidLocationException - */ - public DebugKeyProvider(String osKeyStorePath, String storeType, IKeyGenOutput output) - throws KeyStoreException, NoSuchAlgorithmException, CertificateException, - UnrecoverableEntryException, IOException, KeytoolException, AndroidLocationException { - - if (osKeyStorePath == null) { - osKeyStorePath = getDefaultKeyStoreOsPath(); - } - - if (loadKeyEntry(osKeyStorePath, storeType) == false) { - // create the store with the key - createNewStore(osKeyStorePath, storeType, output); - } - } - - /** - * Returns the OS path to the default debug keystore. - * - * @return The OS path to the default debug keystore. - * @throws KeytoolException - * @throws AndroidLocationException - */ - public static String getDefaultKeyStoreOsPath() - throws KeytoolException, AndroidLocationException { - String folder = AndroidLocation.getFolder(); - if (folder == null) { - throw new KeytoolException("Failed to get HOME directory!\n"); - } - String osKeyStorePath = folder + "debug.keystore"; - - return osKeyStorePath; - } - - /** - * Returns the debug {@link PrivateKey} to use to sign applications for debug purpose. - * @return the private key or null if its creation failed. - */ - @SuppressWarnings("unused") // the thrown Exceptions are not actually thrown - public PrivateKey getDebugKey() throws KeyStoreException, NoSuchAlgorithmException, - UnrecoverableKeyException, UnrecoverableEntryException { - if (mEntry != null) { - return mEntry.getPrivateKey(); - } - - return null; - } - - /** - * Returns the debug {@link Certificate} to use to sign applications for debug purpose. - * @return the certificate or null if its creation failed. - */ - @SuppressWarnings("unused") // the thrown Exceptions are not actually thrown - public Certificate getCertificate() throws KeyStoreException, NoSuchAlgorithmException, - UnrecoverableKeyException, UnrecoverableEntryException { - if (mEntry != null) { - return mEntry.getCertificate(); - } - - return null; - } - - /** - * Loads the debug key from the keystore. - * @param osKeyStorePath the OS path to the keystore. - * @param storeType an optional keystore type, or null if the default is to - * be used. - * @return true if success, false if the keystore does not exist. - */ - private boolean loadKeyEntry(String osKeyStorePath, String storeType) throws KeyStoreException, - NoSuchAlgorithmException, CertificateException, IOException, - UnrecoverableEntryException { - FileInputStream fis = null; - try { - KeyStore keyStore = KeyStore.getInstance( - storeType != null ? storeType : KeyStore.getDefaultType()); - fis = new FileInputStream(osKeyStorePath); - keyStore.load(fis, PASSWORD_CHAR); - mEntry = (KeyStore.PrivateKeyEntry)keyStore.getEntry( - DEBUG_ALIAS, new KeyStore.PasswordProtection(PASSWORD_CHAR)); - } catch (FileNotFoundException e) { - return false; - } finally { - if (fis != null) { - try { - fis.close(); - } catch (IOException e) { - // pass - } - } - } - - return true; - } - - /** - * Creates a new store - * @param osKeyStorePath the location of the store - * @param storeType an optional keystore type, or null if the default is to - * be used. - * @param output an optional {@link IKeyGenOutput} object to get the stdout and stderr - * of the keytool process call. - * @throws KeyStoreException - * @throws NoSuchAlgorithmException - * @throws CertificateException - * @throws UnrecoverableEntryException - * @throws IOException - * @throws KeytoolException - */ - private void createNewStore(String osKeyStorePath, String storeType, IKeyGenOutput output) - throws KeyStoreException, NoSuchAlgorithmException, CertificateException, - UnrecoverableEntryException, IOException, KeytoolException { - - if (KeystoreHelper.createNewStore(osKeyStorePath, storeType, PASSWORD_STRING, DEBUG_ALIAS, - PASSWORD_STRING, CERTIFICATE_DESC, 30 /* validity*/, output)) { - loadKeyEntry(osKeyStorePath, storeType); - } - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/KeystoreHelper.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/KeystoreHelper.java deleted file mode 100644 index 6ba4e723..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/KeystoreHelper.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * 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.sdklib.internal.build; - -import com.android.annotations.Nullable; -import com.android.sdklib.internal.build.DebugKeyProvider.IKeyGenOutput; -import com.android.sdklib.internal.build.DebugKeyProvider.KeytoolException; -import com.android.utils.GrabProcessOutput; -import com.android.utils.GrabProcessOutput.IProcessOutput; -import com.android.utils.GrabProcessOutput.Wait; - -import java.io.File; -import java.io.IOException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.UnrecoverableEntryException; -import java.security.cert.CertificateException; -import java.util.ArrayList; - -/** - * A Helper to create new keystore/key. - * - * @deprecated Use Android-Builder instead - */ -@Deprecated -public final class KeystoreHelper { - - /** - * Creates a new store - * @param osKeyStorePath the location of the store - * @param storeType an optional keystore type, or null if the default is to - * be used. - * @param output an optional {@link IKeyGenOutput} object to get the stdout and stderr - * of the keytool process call. - * @throws KeyStoreException - * @throws NoSuchAlgorithmException - * @throws CertificateException - * @throws UnrecoverableEntryException - * @throws IOException - * @throws KeytoolException - */ - public static boolean createNewStore( - String osKeyStorePath, - String storeType, - String storePassword, - String alias, - String keyPassword, - String description, - int validityYears, - final IKeyGenOutput output) - throws KeyStoreException, NoSuchAlgorithmException, CertificateException, - UnrecoverableEntryException, IOException, KeytoolException { - - // get the executable name of keytool depending on the platform. - String os = System.getProperty("os.name"); - - String keytoolCommand; - if (os.startsWith("Windows")) { - keytoolCommand = "keytool.exe"; - } else { - keytoolCommand = "keytool"; - } - - String javaHome = System.getProperty("java.home"); - - if (javaHome != null && !javaHome.isEmpty()) { - keytoolCommand = javaHome + File.separator + "bin" + File.separator + keytoolCommand; - } - - // create the command line to call key tool to build the key with no user input. - ArrayList commandList = new ArrayList(); - commandList.add(keytoolCommand); - commandList.add("-genkey"); - commandList.add("-alias"); - commandList.add(alias); - commandList.add("-keyalg"); - commandList.add("RSA"); - commandList.add("-dname"); - commandList.add(description); - commandList.add("-validity"); - commandList.add(Integer.toString(validityYears * 365)); - commandList.add("-keypass"); - commandList.add(keyPassword); - commandList.add("-keystore"); - commandList.add(osKeyStorePath); - commandList.add("-storepass"); - commandList.add(storePassword); - if (storeType != null) { - commandList.add("-storetype"); - commandList.add(storeType); - } - - String[] commandArray = commandList.toArray(new String[commandList.size()]); - - // launch the command line process - int result = 0; - try { - Process process = Runtime.getRuntime().exec(commandArray); - result = GrabProcessOutput.grabProcessOutput( - process, - Wait.WAIT_FOR_READERS, - new IProcessOutput() { - @Override - public void out(@Nullable String line) { - if (line != null) { - if (output != null) { - output.out(line); - } else { - System.out.println(line); - } - } - } - - @Override - public void err(@Nullable String line) { - if (line != null) { - if (output != null) { - output.err(line); - } else { - System.err.println(line); - } - } - } - }); - } catch (Exception e) { - // create the command line as one string for debugging purposes - StringBuilder builder = new StringBuilder(); - boolean firstArg = true; - for (String arg : commandArray) { - boolean hasSpace = arg.indexOf(' ') != -1; - - if (firstArg == true) { - firstArg = false; - } else { - builder.append(' '); - } - - if (hasSpace) { - builder.append('"'); - } - - builder.append(arg); - - if (hasSpace) { - builder.append('"'); - } - } - - throw new KeytoolException("Failed to create key: " + e.getMessage(), - javaHome, builder.toString()); - } - - if (result != 0) { - return false; - } - - return true; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/SignedJarBuilder.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/SignedJarBuilder.java deleted file mode 100644 index afb7e658..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/SignedJarBuilder.java +++ /dev/null @@ -1,399 +0,0 @@ -/* - * 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.sdklib.internal.build; - -import com.android.SdkConstants; -import com.android.sdklib.internal.build.SignedJarBuilder.IZipEntryFilter.ZipAbortException; - -import sun.misc.BASE64Encoder; -import sun.security.pkcs.ContentInfo; -import sun.security.pkcs.PKCS7; -import sun.security.pkcs.SignerInfo; -import sun.security.x509.AlgorithmId; -import sun.security.x509.X500Name; - -import java.io.BufferedOutputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FilterOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintStream; -import java.security.DigestOutputStream; -import java.security.GeneralSecurityException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.Signature; -import java.security.SignatureException; -import java.security.cert.X509Certificate; -import java.util.Map; -import java.util.jar.Attributes; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; -import java.util.jar.JarOutputStream; -import java.util.jar.Manifest; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; - -/** - * A Jar file builder with signature support. - * - * @deprecated Use Android-Builder instead - */ -@Deprecated -public class SignedJarBuilder { - private static final String DIGEST_ALGORITHM = "SHA1"; - private static final String DIGEST_ATTR = "SHA1-Digest"; - private static final String DIGEST_MANIFEST_ATTR = "SHA1-Digest-Manifest"; - - /** Write to another stream and also feed it to the Signature object. */ - private static class SignatureOutputStream extends FilterOutputStream { - private Signature mSignature; - private int mCount = 0; - - public SignatureOutputStream(OutputStream out, Signature sig) { - super(out); - mSignature = sig; - } - - @Override - public void write(int b) throws IOException { - try { - mSignature.update((byte) b); - } catch (SignatureException e) { - throw new IOException("SignatureException: " + e); - } - super.write(b); - mCount++; - } - - @Override - public void write(byte[] b, int off, int len) throws IOException { - try { - mSignature.update(b, off, len); - } catch (SignatureException e) { - throw new IOException("SignatureException: " + e); - } - super.write(b, off, len); - mCount += len; - } - - public int size() { - return mCount; - } - } - - private JarOutputStream mOutputJar; - private PrivateKey mKey; - private X509Certificate mCertificate; - private Manifest mManifest; - private BASE64Encoder mBase64Encoder; - private MessageDigest mMessageDigest; - - private byte[] mBuffer = new byte[4096]; - - /** - * Classes which implement this interface provides a method to check whether a file should - * be added to a Jar file. - */ - public interface IZipEntryFilter { - - /** - * An exception thrown during packaging of a zip file into APK file. - * This is typically thrown by implementations of - * {@link IZipEntryFilter#checkEntry(String)}. - */ - class ZipAbortException extends Exception { - private static final long serialVersionUID = 1L; - - public ZipAbortException() { - super(); - } - - public ZipAbortException(String format, Object... args) { - super(String.format(format, args)); - } - - public ZipAbortException(Throwable cause, String format, Object... args) { - super(String.format(format, args), cause); - } - - public ZipAbortException(Throwable cause) { - super(cause); - } - } - - - /** - * Checks a file for inclusion in a Jar archive. - * @param archivePath the archive file path of the entry - * @return true if the file should be included. - * @throws ZipAbortException if writing the file should be aborted. - */ - boolean checkEntry(String archivePath) throws ZipAbortException; - } - - /** - * Creates a {@link SignedJarBuilder} with a given output stream, and signing information. - *

If either key or certificate is null then - * the archive will not be signed. - * @param out the {@link OutputStream} where to write the Jar archive. - * @param key the {@link PrivateKey} used to sign the archive, or null. - * @param certificate the {@link X509Certificate} used to sign the archive, or - * null. - * @throws IOException - * @throws NoSuchAlgorithmException - */ - public SignedJarBuilder(OutputStream out, PrivateKey key, X509Certificate certificate) - throws IOException, NoSuchAlgorithmException { - mOutputJar = new JarOutputStream(new BufferedOutputStream(out)); - mOutputJar.setLevel(9); - mKey = key; - mCertificate = certificate; - - if (mKey != null && mCertificate != null) { - mManifest = new Manifest(); - Attributes main = mManifest.getMainAttributes(); - main.putValue("Manifest-Version", "1.0"); - main.putValue("Created-By", "1.0 (Android)"); - - mBase64Encoder = new BASE64Encoder(); - mMessageDigest = MessageDigest.getInstance(DIGEST_ALGORITHM); - } - } - - /** - * Writes a new {@link File} into the archive. - * @param inputFile the {@link File} to write. - * @param jarPath the filepath inside the archive. - * @throws IOException - */ - public void writeFile(File inputFile, String jarPath) throws IOException { - // Get an input stream on the file. - FileInputStream fis = new FileInputStream(inputFile); - try { - - // create the zip entry - JarEntry entry = new JarEntry(jarPath); - entry.setTime(inputFile.lastModified()); - - writeEntry(fis, entry); - } finally { - // close the file stream used to read the file - fis.close(); - } - } - - /** - * Copies the content of a Jar/Zip archive into the receiver archive. - *

An optional {@link IZipEntryFilter} allows to selectively choose which files - * to copy over. - * @param input the {@link InputStream} for the Jar/Zip to copy. - * @param filter the filter or null - * @throws IOException - * @throws ZipAbortException if the {@link IZipEntryFilter} filter indicated that the write - * must be aborted. - */ - public void writeZip(InputStream input, IZipEntryFilter filter) - throws IOException, ZipAbortException { - ZipInputStream zis = new ZipInputStream(input); - - try { - // loop on the entries of the intermediary package and put them in the final package. - ZipEntry entry; - while ((entry = zis.getNextEntry()) != null) { - String name = entry.getName(); - - // do not take directories or anything inside a potential META-INF folder. - if (entry.isDirectory() || name.startsWith("META-INF/")) { - continue; - } - - // if we have a filter, we check the entry against it - if (filter != null && filter.checkEntry(name) == false) { - continue; - } - - JarEntry newEntry; - - // Preserve the STORED method of the input entry. - if (entry.getMethod() == JarEntry.STORED) { - newEntry = new JarEntry(entry); - } else { - // Create a new entry so that the compressed len is recomputed. - newEntry = new JarEntry(name); - } - - writeEntry(zis, newEntry); - - zis.closeEntry(); - } - } finally { - zis.close(); - } - } - - /** - * Closes the Jar archive by creating the manifest, and signing the archive. - * @throws IOException - * @throws GeneralSecurityException - */ - public void close() throws IOException, GeneralSecurityException { - if (mManifest != null) { - // write the manifest to the jar file - mOutputJar.putNextEntry(new JarEntry(JarFile.MANIFEST_NAME)); - mManifest.write(mOutputJar); - - // CERT.SF - Signature signature = Signature.getInstance("SHA1with" + mKey.getAlgorithm()); - signature.initSign(mKey); - mOutputJar.putNextEntry(new JarEntry("META-INF/CERT.SF")); - SignatureOutputStream out = new SignatureOutputStream(mOutputJar, signature); - writeSignatureFile(out); - - // CERT.* - mOutputJar.putNextEntry(new JarEntry("META-INF/CERT." + mKey.getAlgorithm())); - writeSignatureBlock(signature, mCertificate, mKey); - - // close out at the end because it can also close mOutputJar. - // (there's some timing issue here I think, because it's worked before with out - // being closed after writing CERT.SF). - out.close(); - } - - mOutputJar.close(); - mOutputJar = null; - } - - /** - * Clean up of the builder for interrupted workflow. - * This does nothing if {@link #close()} was called successfully. - */ - public void cleanUp() { - if (mOutputJar != null) { - try { - mOutputJar.close(); - } catch (IOException e) { - // pass - } - } - } - - /** - * Adds an entry to the output jar, and write its content from the {@link InputStream} - * @param input The input stream from where to write the entry content. - * @param entry the entry to write in the jar. - * @throws IOException - */ - private void writeEntry(InputStream input, JarEntry entry) throws IOException { - // add the entry to the jar archive - mOutputJar.putNextEntry(entry); - - // read the content of the entry from the input stream, and write it into the archive. - int count; - while ((count = input.read(mBuffer)) != -1) { - mOutputJar.write(mBuffer, 0, count); - - // update the digest - if (mMessageDigest != null) { - mMessageDigest.update(mBuffer, 0, count); - } - } - - // close the entry for this file - mOutputJar.closeEntry(); - - if (mManifest != null) { - // update the manifest for this entry. - Attributes attr = mManifest.getAttributes(entry.getName()); - if (attr == null) { - attr = new Attributes(); - mManifest.getEntries().put(entry.getName(), attr); - } - attr.putValue(DIGEST_ATTR, mBase64Encoder.encode(mMessageDigest.digest())); - } - } - - /** Writes a .SF file with a digest to the manifest. */ - private void writeSignatureFile(SignatureOutputStream out) - throws IOException, GeneralSecurityException { - Manifest sf = new Manifest(); - Attributes main = sf.getMainAttributes(); - main.putValue("Signature-Version", "1.0"); - main.putValue("Created-By", "1.0 (Android)"); - - BASE64Encoder base64 = new BASE64Encoder(); - MessageDigest md = MessageDigest.getInstance(DIGEST_ALGORITHM); - PrintStream print = new PrintStream( - new DigestOutputStream(new ByteArrayOutputStream(), md), - true, SdkConstants.UTF_8); - - // Digest of the entire manifest - mManifest.write(print); - print.flush(); - main.putValue(DIGEST_MANIFEST_ATTR, base64.encode(md.digest())); - - Map entries = mManifest.getEntries(); - for (Map.Entry entry : entries.entrySet()) { - // Digest of the manifest stanza for this entry. - print.print("Name: " + entry.getKey() + "\r\n"); - for (Map.Entry att : entry.getValue().entrySet()) { - print.print(att.getKey() + ": " + att.getValue() + "\r\n"); - } - print.print("\r\n"); - print.flush(); - - Attributes sfAttr = new Attributes(); - sfAttr.putValue(DIGEST_ATTR, base64.encode(md.digest())); - sf.getEntries().put(entry.getKey(), sfAttr); - } - - sf.write(out); - - // A bug in the java.util.jar implementation of Android platforms - // up to version 1.6 will cause a spurious IOException to be thrown - // if the length of the signature file is a multiple of 1024 bytes. - // As a workaround, add an extra CRLF in this case. - if ((out.size() % 1024) == 0) { - out.write('\r'); - out.write('\n'); - } - } - - /** Write the certificate file with a digital signature. */ - private void writeSignatureBlock(Signature signature, X509Certificate publicKey, - PrivateKey privateKey) - throws IOException, GeneralSecurityException { - SignerInfo signerInfo = new SignerInfo( - new X500Name(publicKey.getIssuerX500Principal().getName()), - publicKey.getSerialNumber(), - AlgorithmId.get(DIGEST_ALGORITHM), - AlgorithmId.get(privateKey.getAlgorithm()), - signature.sign()); - - PKCS7 pkcs7 = new PKCS7( - new AlgorithmId[] { AlgorithmId.get(DIGEST_ALGORITHM) }, - new ContentInfo(ContentInfo.DATA_OID, null), - new X509Certificate[] { publicKey }, - new SignerInfo[] { signerInfo }); - - pkcs7.encodeSignedData(mOutputJar); - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/SymbolLoader.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/SymbolLoader.java deleted file mode 100644 index cbb41920..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/SymbolLoader.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (C) 2012 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.sdklib.internal.build; - -import com.google.common.base.Charsets; -import com.google.common.collect.HashBasedTable; -import com.google.common.collect.Table; -import com.google.common.io.Files; - -import java.io.File; -import java.io.IOException; -import java.util.List; - -/** - * A class to load the text symbol file generated by aapt with the - * --output-text-symbols option. - * - * @deprecated Use Android-Builder instead - */ -@Deprecated -public class SymbolLoader { - - private final File mSymbolFile; - private Table mSymbols; - - public static class SymbolEntry { - private final String mName; - private final String mType; - private final String mValue; - - public SymbolEntry(String name, String type, String value) { - mName = name; - mType = type; - mValue = value; - } - - public String getValue() { - return mValue; - } - - public String getName() { - return mName; - } - - public String getType() { - return mType; - } - } - - public SymbolLoader(File symbolFile) { - mSymbolFile = symbolFile; - } - - public void load() throws IOException { - List lines = Files.readLines(mSymbolFile, Charsets.UTF_8); - - mSymbols = HashBasedTable.create(); - - int lineIndex = 1; - String line = null; - try { - final int count = lines.size(); - for (; lineIndex <= count ; lineIndex++) { - line = lines.get(lineIndex-1); - - // format is " " - // don't want to split on space as value could contain spaces. - int pos = line.indexOf(' '); - String type = line.substring(0, pos); - int pos2 = line.indexOf(' ', pos + 1); - String className = line.substring(pos + 1, pos2); - int pos3 = line.indexOf(' ', pos2 + 1); - String name = line.substring(pos2 + 1, pos3); - String value = line.substring(pos3 + 1); - - mSymbols.put(className, name, new SymbolEntry(name, type, value)); - } - } catch (Exception e) { - String s = String.format("File format error reading %s\tline %d: '%s'", - mSymbolFile.getAbsolutePath(), lineIndex, line); - throw new IOException(s, e); - } - } - - Table getSymbols() { - return mSymbols; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/SymbolWriter.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/SymbolWriter.java deleted file mode 100644 index 3dfaa6fe..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/build/SymbolWriter.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (C) 2012 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.sdklib.internal.build; - -import com.android.SdkConstants; -import com.android.sdklib.internal.build.SymbolLoader.SymbolEntry; -import com.google.common.base.Charsets; -import com.google.common.base.Splitter; -import com.google.common.collect.HashBasedTable; -import com.google.common.collect.Lists; -import com.google.common.collect.Table; -import com.google.common.io.Closer; -import com.google.common.io.Files; - -import java.io.BufferedWriter; -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * A class to write R.java classes based on data read from text symbol files generated by - * aapt with the --output-text-symbols option. - * - * @deprecated Use Android-Builder instead - */ -@Deprecated -public class SymbolWriter { - - private final String mOutFolder; - private final String mPackageName; - private final List mSymbols = Lists.newArrayList(); - private final SymbolLoader mValues; - - public SymbolWriter(String outFolder, String packageName, SymbolLoader values) { - mOutFolder = outFolder; - mPackageName = packageName; - mValues = values; - } - - public void addSymbolsToWrite(SymbolLoader symbols) { - mSymbols.add(symbols); - } - - private Table getAllSymbols() { - Table symbols = HashBasedTable.create(); - - for (SymbolLoader symbolLoader : mSymbols) { - symbols.putAll(symbolLoader.getSymbols()); - } - - return symbols; - } - - public void write() throws IOException { - Splitter splitter = Splitter.on('.'); - Iterable folders = splitter.split(mPackageName); - File file = new File(mOutFolder); - for (String folder : folders) { - file = new File(file, folder); - } - file.mkdirs(); - file = new File(file, SdkConstants.FN_RESOURCE_CLASS); - - Closer closer = Closer.create(); - try { - BufferedWriter writer = closer.register(Files.newWriter(file, Charsets.UTF_8)); - - writer.write("/* AUTO-GENERATED FILE. DO NOT MODIFY.\n"); - writer.write(" *\n"); - writer.write(" * This class was automatically generated by the\n"); - writer.write(" * aapt tool from the resource data it found. It\n"); - writer.write(" * should not be modified by hand.\n"); - writer.write(" */\n"); - - writer.write("package "); - writer.write(mPackageName); - writer.write(";\n\npublic final class R {\n"); - - Table symbols = getAllSymbols(); - Table values = mValues.getSymbols(); - - Set rowSet = symbols.rowKeySet(); - List rowList = Lists.newArrayList(rowSet); - Collections.sort(rowList); - - for (String row : rowList) { - writer.write("\tpublic static final class "); - writer.write(row); - writer.write(" {\n"); - - Map rowMap = symbols.row(row); - Set symbolSet = rowMap.keySet(); - ArrayList symbolList = Lists.newArrayList(symbolSet); - Collections.sort(symbolList); - - for (String symbolName : symbolList) { - // get the matching SymbolEntry from the values Table. - SymbolEntry value = values.get(row, symbolName); - if (value != null) { - writer.write("\t\tpublic static final "); - writer.write(value.getType()); - writer.write(" "); - writer.write(value.getName()); - writer.write(" = "); - writer.write(value.getValue()); - writer.write(";\n"); - } - } - - writer.write("\t}\n"); - } - - writer.write("}\n"); - } catch (Throwable e) { - throw closer.rethrow(e); - } finally { - closer.close(); - } - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/project/ProjectCreator.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/project/ProjectCreator.java deleted file mode 100644 index c5712d75..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/project/ProjectCreator.java +++ /dev/null @@ -1,1571 +0,0 @@ -/* - * 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.sdklib.internal.project; - -import com.android.SdkConstants; -import com.android.io.FileWrapper; -import com.android.io.FolderWrapper; -import com.android.sdklib.IAndroidTarget; -import com.android.sdklib.SdkManager; -import com.android.sdklib.internal.project.ProjectProperties.PropertyType; -import com.android.utils.ILogger; -import com.android.xml.AndroidManifest; -import com.android.xml.AndroidXPathFactory; - -import org.w3c.dom.NodeList; -import org.xml.sax.InputSource; - -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import javax.xml.xpath.XPath; -import javax.xml.xpath.XPathConstants; -import javax.xml.xpath.XPathExpressionException; -import javax.xml.xpath.XPathFactory; - -/** - * Creates the basic files needed to get an Android project up and running. - */ -public class ProjectCreator { - - /** Version of the build.xml. Stored in version-tag */ - private static final int MIN_BUILD_VERSION_TAG = 1; - - /** Package path substitution string used in template files, i.e. "PACKAGE_PATH" */ - private static final String PH_PACKAGE_PATH = "PACKAGE_PATH"; - /** Package name substitution string used in template files, i.e. "PACKAGE" */ - private static final String PH_PACKAGE = "PACKAGE"; - /** Activity name substitution string used in template files, i.e. "ACTIVITY_NAME". - * @deprecated This is only used for older templates. For new ones see - * {@link #PH_ACTIVITY_ENTRY_NAME}, and {@link #PH_ACTIVITY_CLASS_NAME}. */ - @Deprecated - private static final String PH_ACTIVITY_NAME = "ACTIVITY_NAME"; - /** Activity name substitution string used in manifest templates, i.e. "ACTIVITY_ENTRY_NAME".*/ - private static final String PH_ACTIVITY_ENTRY_NAME = "ACTIVITY_ENTRY_NAME"; - /** Activity name substitution string used in class templates, i.e. "ACTIVITY_CLASS_NAME".*/ - private static final String PH_ACTIVITY_CLASS_NAME = "ACTIVITY_CLASS_NAME"; - /** Activity FQ-name substitution string used in class templates, i.e. "ACTIVITY_FQ_NAME".*/ - private static final String PH_ACTIVITY_FQ_NAME = "ACTIVITY_FQ_NAME"; - /** Original Activity class name substitution string used in class templates, i.e. - * "ACTIVITY_TESTED_CLASS_NAME".*/ - private static final String PH_ACTIVITY_TESTED_CLASS_NAME = "ACTIVITY_TESTED_CLASS_NAME"; - /** Project name substitution string used in template files, i.e. "PROJECT_NAME". */ - public static final String PH_PROJECT_NAME = "PROJECT_NAME"; - /** Application icon substitution string used in the manifest template */ - private static final String PH_ICON = "ICON"; - /** Version tag name substitution string used in template files, i.e. "VERSION_TAG". */ - private static final String PH_VERSION_TAG = "VERSION_TAG"; - /** Target name substitution string used in template files, i.e. "TARGET". */ - private static final String PH_TARGET = "TARGET"; - /** Gradle plugin substitution string used in the build.gradle template */ - private static final String PH_PLUGIN = "PLUGIN"; - /** Gradle artifact version substitution string used in the build.gradle template */ - private static final String PH_ARTIFACT_VERSION = "ARTIFACT_VERSION"; - /** Build tool revision substitution string used in the build.gradle template */ - private static final String PH_BUILD_TOOL_REV = "BUILD_TOOL_REV"; - - /** The xpath to find a project name in an Ant build file. */ - private static final String XPATH_PROJECT_NAME = "/project/@name"; - - /** Pattern for characters accepted in a project name. Since this will be used as a - * directory name, we're being a bit conservative on purpose: dot and space cannot be used. */ - public static final Pattern RE_PROJECT_NAME = Pattern.compile("[a-zA-Z0-9_]+"); - /** List of valid characters for a project name. Used for display purposes. */ - public static final String CHARS_PROJECT_NAME = "a-z A-Z 0-9 _"; - - /** Pattern for characters accepted in a package name. A package is list of Java identifier - * separated by a dot. We need to have at least one dot (e.g. a two-level package name). - * A Java identifier cannot start by a digit. */ - public static final Pattern RE_PACKAGE_NAME = - Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)+"); - /** List of valid characters for a project name. Used for display purposes. */ - public static final String CHARS_PACKAGE_NAME = "a-z A-Z 0-9 _"; - - /** Pattern for characters accepted in an activity name, which is a Java identifier. */ - public static final Pattern RE_ACTIVITY_NAME = - Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*"); - /** List of valid characters for a project name. Used for display purposes. */ - public static final String CHARS_ACTIVITY_NAME = "a-z A-Z 0-9 _"; - - /** Gradle plugin to use with standard projects */ - private static final String PLUGIN_PROJECT = "android"; - /** Gradle plugin to use with library projects */ - private static final String PLUGIN_LIB_PROJECT = "android-library"; - - - public enum OutputLevel { - /** Silent mode. Project creation will only display errors. */ - SILENT, - /** Normal mode. Project creation will display what's being done, display - * error but not warnings. */ - NORMAL, - /** Verbose mode. Project creation will display what's being done, errors and warnings. */ - VERBOSE - } - - /** - * Exception thrown when a project creation fails, typically because a template - * file cannot be written. - */ - private static class ProjectCreateException extends Exception { - /** default UID. This will not be serialized anyway. */ - private static final long serialVersionUID = 1L; - - @SuppressWarnings("unused") - ProjectCreateException(String message) { - super(message); - } - - ProjectCreateException(Throwable t, String format, Object... args) { - super(format != null ? String.format(format, args) : format, t); - } - - ProjectCreateException(String format, Object... args) { - super(String.format(format, args)); - } - } - - /** The {@link OutputLevel} verbosity. */ - private final OutputLevel mLevel; - /** Logger for errors and output. Cannot be null. */ - private final ILogger mLog; - /** The OS path of the SDK folder. */ - private final String mSdkFolder; - /** The {@link SdkManager} instance. */ - private final SdkManager mSdkManager; - - /** - * Helper class to create android projects. - * - * @param sdkManager The {@link SdkManager} instance. - * @param sdkFolder The OS path of the SDK folder. - * @param level The {@link OutputLevel} verbosity. - * @param log Logger for errors and output. Cannot be null. - */ - public ProjectCreator(SdkManager sdkManager, String sdkFolder, OutputLevel level, ILogger log) { - mSdkManager = sdkManager; - mSdkFolder = sdkFolder; - mLevel = level; - mLog = log; - } - - /** - * Creates a new (ant) project. - *

- * The caller should have already checked and sanitized the parameters. - * - * @param folderPath the folder of the project to create. - * @param projectName the name of the project. The name must match the - * {@link #RE_PROJECT_NAME} regex. - * @param packageName the package of the project. The name must match the - * {@link #RE_PACKAGE_NAME} regex. - * @param activityEntry the activity of the project as it will appear in the manifest. Can be - * null if no activity should be created. The name must match the - * {@link #RE_ACTIVITY_NAME} regex. - * @param target the project target. - * @param library whether the project is a library. - * @param pathToMainProject if non-null the project will be setup to test a main project - * located at the given path. - */ - public void createProject(String folderPath, String projectName, - String packageName, String activityEntry, IAndroidTarget target, boolean library, - String pathToMainProject) { - - // create project folder if it does not exist - File projectFolder = checkNewProjectLocation(folderPath); - if (projectFolder == null) { - return; - } - - try { - boolean isTestProject = pathToMainProject != null; - - // first create the project properties. - - // location of the SDK goes in localProperty - ProjectPropertiesWorkingCopy localProperties = ProjectProperties.create(folderPath, - PropertyType.LOCAL); - localProperties.setProperty(ProjectProperties.PROPERTY_SDK, mSdkFolder); - localProperties.save(); - - // target goes in project properties - ProjectPropertiesWorkingCopy projectProperties = ProjectProperties.create(folderPath, - PropertyType.PROJECT); - projectProperties.setProperty(ProjectProperties.PROPERTY_TARGET, target.hashString()); - if (library) { - projectProperties.setProperty(ProjectProperties.PROPERTY_LIBRARY, "true"); - } - projectProperties.save(); - - // create a ant.properties file with just the application package - ProjectPropertiesWorkingCopy antProperties = ProjectProperties.create(folderPath, - PropertyType.ANT); - - if (isTestProject) { - antProperties.setProperty(ProjectProperties.PROPERTY_TESTED_PROJECT, - pathToMainProject); - } - - antProperties.save(); - - // create the map for place-holders of values to replace in the templates - final HashMap keywords = new HashMap(); - - // create the required folders. - // compute src folder path - final String packagePath = - stripString(packageName.replace(".", File.separator), - File.separatorChar); - - // put this path in the place-holder map for project files that needs to list - // files manually. - keywords.put(PH_PACKAGE_PATH, packagePath); - keywords.put(PH_PACKAGE, packageName); - keywords.put(PH_VERSION_TAG, Integer.toString(MIN_BUILD_VERSION_TAG)); - - - // compute some activity related information - String fqActivityName = null, activityPath = null, activityClassName = null; - String originalActivityEntry = activityEntry; - String originalActivityClassName = null; - if (activityEntry != null) { - if (isTestProject) { - // append Test so that it doesn't collide with the main project activity. - activityEntry += "Test"; - - // get the classname from the original activity entry. - int pos = originalActivityEntry.lastIndexOf('.'); - if (pos != -1) { - originalActivityClassName = originalActivityEntry.substring(pos + 1); - } else { - originalActivityClassName = originalActivityEntry; - } - } - - // get the fully qualified name of the activity - fqActivityName = AndroidManifest.combinePackageAndClassName(packageName, - activityEntry); - - // get the activity path (replace the . to /) - activityPath = stripString(fqActivityName.replace(".", File.separator), - File.separatorChar); - - // remove the last segment, so that we only have the path to the activity, but - // not the activity filename itself. - activityPath = activityPath.substring(0, - activityPath.lastIndexOf(File.separatorChar)); - - // finally, get the class name for the activity - activityClassName = fqActivityName.substring(fqActivityName.lastIndexOf('.') + 1); - } - - // at this point we have the following for the activity: - // activityEntry: this is the manifest entry. For instance .MyActivity - // fqActivityName: full-qualified class name: com.foo.MyActivity - // activityClassName: only the classname: MyActivity - // originalActivityClassName: the classname of the activity being tested (if applicable) - - // Add whatever activity info is needed in the place-holder map. - // Older templates only expect ACTIVITY_NAME to be the same (and unmodified for tests). - if (target.getVersion().getApiLevel() < 4) { // legacy - if (originalActivityEntry != null) { - keywords.put(PH_ACTIVITY_NAME, originalActivityEntry); - } - } else { - // newer templates make a difference between the manifest entries, classnames, - // as well as the main and test classes. - if (activityEntry != null) { - keywords.put(PH_ACTIVITY_ENTRY_NAME, activityEntry); - keywords.put(PH_ACTIVITY_CLASS_NAME, activityClassName); - keywords.put(PH_ACTIVITY_FQ_NAME, fqActivityName); - if (originalActivityClassName != null) { - keywords.put(PH_ACTIVITY_TESTED_CLASS_NAME, originalActivityClassName); - } - } - } - - // Take the project name from the command line if there's one - if (projectName != null) { - keywords.put(PH_PROJECT_NAME, projectName); - } else { - if (activityClassName != null) { - // Use the activity class name as project name - keywords.put(PH_PROJECT_NAME, activityClassName); - } else { - // We need a project name. Just pick up the basename of the project - // directory. - projectName = projectFolder.getName(); - keywords.put(PH_PROJECT_NAME, projectName); - } - } - - // create the source folder for the activity - if (activityClassName != null) { - String srcActivityFolderPath = - SdkConstants.FD_SOURCES + File.separator + activityPath; - File sourceFolder = createDirs(projectFolder, srcActivityFolderPath); - - String javaTemplate = isTestProject ? "java_tests_file.template" - : "java_file.template"; - String activityFileName = activityClassName + ".java"; - - installTargetTemplate(javaTemplate, new File(sourceFolder, activityFileName), - keywords, target); - } else { - // we should at least create 'src' - createDirs(projectFolder, SdkConstants.FD_SOURCES); - } - - // create other useful folders - File resourceFolder = createDirs(projectFolder, SdkConstants.FD_RESOURCES); - createDirs(projectFolder, SdkConstants.FD_OUTPUT); - createDirs(projectFolder, SdkConstants.FD_NATIVE_LIBS); - - if (isTestProject == false) { - /* Make res files only for non test projects */ - File valueFolder = createDirs(resourceFolder, SdkConstants.FD_RES_VALUES); - installTargetTemplate("strings.template", new File(valueFolder, "strings.xml"), - keywords, target); - - File layoutFolder = createDirs(resourceFolder, SdkConstants.FD_RES_LAYOUT); - installTargetTemplate("layout.template", new File(layoutFolder, "main.xml"), - keywords, target); - - // create the icons - if (installIcons(resourceFolder, target)) { - keywords.put(PH_ICON, "android:icon=\"@drawable/ic_launcher\""); - } else { - keywords.put(PH_ICON, ""); - } - } - - /* Make AndroidManifest.xml and build.xml files */ - String manifestTemplate = "AndroidManifest.template"; - if (isTestProject) { - manifestTemplate = "AndroidManifest.tests.template"; - } - - installTargetTemplate(manifestTemplate, - new File(projectFolder, SdkConstants.FN_ANDROID_MANIFEST_XML), - keywords, target); - - installTemplate("build.template", - new File(projectFolder, SdkConstants.FN_BUILD_XML), - keywords); - - // install the proguard config file. - installTemplate(SdkConstants.FN_PROJECT_PROGUARD_FILE, - new File(projectFolder, SdkConstants.FN_PROJECT_PROGUARD_FILE), - null /*keywords*/); - } catch (Exception e) { - mLog.error(e, null); - } - } - - /** - * Creates a new (gradle) project. - *

- * The caller should have already checked and sanitized the parameters. - * - * @param folderPath the folder of the project to create. - * @param projectName the name of the project. The name must match the - * {@link #RE_PROJECT_NAME} regex. - * @param packageName the package of the project. The name must match the - * {@link #RE_PACKAGE_NAME} regex. - * @param activityEntry the activity of the project as it will appear in the manifest. Can be - * null if no activity should be created. The name must match the - * {@link #RE_ACTIVITY_NAME} regex. - * @param target the project target. - * @param library whether the project is a library. - * @param artifactVersion the version of the gradle artifact in maven. - */ - public void createGradleProject(String folderPath, String projectName, - String packageName, String activityEntry, IAndroidTarget target, boolean library, - String artifactVersion) { - - // create project folder if it does not exist - File projectFolder = checkNewProjectLocation(folderPath); - if (projectFolder == null) { - return; - } - - try { - // first create the project properties. - - // location of the SDK goes in localProperty - ProjectPropertiesWorkingCopy localProperties = ProjectProperties.create(folderPath, - PropertyType.LOCAL); - localProperties.setProperty(ProjectProperties.PROPERTY_SDK, mSdkFolder); - localProperties.save(); - - // create the map for place-holders of values to replace in the templates - final HashMap keywords = new HashMap(); - final HashMap testKeywords = new HashMap(); - - // create the required folders. - // compute src folder path - final String packagePath = - stripString(packageName.replace(".", File.separator), - File.separatorChar); - - // put this path in the place-holder map for project files that needs to list - // files manually. - keywords.put(PH_PACKAGE_PATH, packagePath); - keywords.put(PH_PACKAGE, packageName); - - testKeywords.put(PH_PACKAGE_PATH, packagePath); - testKeywords.put(PH_PACKAGE, packageName); - - // compute some activity related information - String activityPath = null, activityClassName = null; - String testActivityPath = null, testActivityClassName = null; - if (activityEntry != null) { - // get the fully qualified name of the activity - String fqActivityName = AndroidManifest.combinePackageAndClassName(packageName, - activityEntry); - - // get the activity path (replace the . to /) - activityPath = stripString(fqActivityName.replace(".", File.separator), - File.separatorChar); - - // remove the last segment, so that we only have the path to the activity, but - // not the activity filename itself. - activityPath = activityPath.substring(0, - activityPath.lastIndexOf(File.separatorChar)); - - // finally, get the class name for the activity - activityClassName = fqActivityName.substring(fqActivityName.lastIndexOf('.') + 1); - - // at this point we have the following for the activity: - // activityEntry: this is the manifest entry. For instance .MyActivity - // fqActivityName: full-qualified class name: com.foo.MyActivity - // activityClassName: only the classname: MyActivity - - // append Test so that it doesn't collide with the main project activity. - String testActivityEntry = activityEntry + "Test"; - - // get the fully qualified name of the test - String testFqActivityName = AndroidManifest.combinePackageAndClassName(packageName, - testActivityEntry); - - // get the test path (replace the . to /) - testActivityPath = stripString(testFqActivityName.replace(".", File.separator), - File.separatorChar); - - // remove the last segment, so that we only have the path to the test, but - // not the test filename itself. - testActivityPath = testActivityPath.substring(0, - testActivityPath.lastIndexOf(File.separatorChar)); - - // finally, get the class name for the test - testActivityClassName = testFqActivityName.substring(testFqActivityName.lastIndexOf('.') + 1); - - // at this point we have the following for the test: - // testActivityEntry: this is the manifest entry. For instance .MyActivityTest - // testFqActivityName: full-qualified class name: com.foo.MyActivityTest - // testActivityClassName: only the classname: MyActivityTest - - // Add whatever activity info is needed in the place-holder map. - // Older templates only expect ACTIVITY_NAME to be the same (and unmodified for tests). - if (target.getVersion().getApiLevel() < 4) { // legacy - keywords.put(PH_ACTIVITY_NAME, activityEntry); - testKeywords.put(PH_ACTIVITY_NAME, activityEntry); - } else { - // newer templates make a difference between the manifest entries, classnames, - // as well as the main and test classes. - keywords.put(PH_ACTIVITY_ENTRY_NAME, activityEntry); - keywords.put(PH_ACTIVITY_CLASS_NAME, activityClassName); - keywords.put(PH_ACTIVITY_FQ_NAME, fqActivityName); - - testKeywords.put(PH_ACTIVITY_ENTRY_NAME, testActivityEntry); - testKeywords.put(PH_ACTIVITY_CLASS_NAME, testActivityClassName); - testKeywords.put(PH_ACTIVITY_FQ_NAME, testFqActivityName); - testKeywords.put(PH_ACTIVITY_TESTED_CLASS_NAME, activityClassName); - } - } - - // Take the project name from the command line if there's one - if (projectName != null) { - keywords.put(PH_PROJECT_NAME, projectName); - testKeywords.put(PH_PROJECT_NAME, projectName); - } else { - // Use the activity class name as project name, else just - // pick up the basename of the project directory. - keywords.put(PH_PROJECT_NAME, (activityClassName != null) ? - activityClassName : projectFolder.getName()); - testKeywords.put(PH_PROJECT_NAME, (testActivityClassName != null) ? - testActivityClassName : projectFolder.getName()); - } - - String srcMainPath = SdkConstants.FD_SOURCES + File.separator + - SdkConstants.FD_MAIN; - String srcTestPath = SdkConstants.FD_SOURCES + File.separator + - SdkConstants.FD_TEST; - - // create the source folders for the activity - String srcMainCodePath = srcMainPath + File.separator + SdkConstants.FD_JAVA; - createDirs(projectFolder, srcMainCodePath); - if (activityClassName != null) { - String srcActivityFolderPath = - srcMainCodePath + File.separator + activityPath; - File sourceFolder = createDirs(projectFolder, srcActivityFolderPath); - - String activityFileName = activityClassName + ".java"; - - installTargetTemplate("java_file.template", - new File(sourceFolder, activityFileName), keywords, target); - } - - // create the source folders for the test - String srcTestCodePath = srcTestPath + File.separator + SdkConstants.FD_JAVA; - createDirs(projectFolder, srcTestCodePath); - if (testActivityClassName != null) { - String srcActivityFolderPath = - srcTestCodePath + File.separator + testActivityPath; - File sourceFolder = createDirs(projectFolder, srcActivityFolderPath); - - String activityFileName = testActivityClassName + ".java"; - - installTargetTemplate("java_tests_file.template", - new File(sourceFolder, activityFileName), testKeywords, target); - } - - // create the res xml files - String srcMainResPath = srcMainPath + File.separator + SdkConstants.FD_RES; - File resourceFolder = createDirs(projectFolder, srcMainResPath); - - File valueFolder = createDirs(resourceFolder, SdkConstants.FD_RES_VALUES); - installTargetTemplate("strings.template", new File(valueFolder, "strings.xml"), - keywords, target); - - File layoutFolder = createDirs(resourceFolder, SdkConstants.FD_RES_LAYOUT); - installTargetTemplate("layout.template", new File(layoutFolder, "main.xml"), - keywords, target); - - // create the icons - if (installIcons(resourceFolder, target)) { - keywords.put(PH_ICON, "android:icon=\"@drawable/ic_launcher\""); - } else { - keywords.put(PH_ICON, ""); - } - - // Create the AndroidManifest.xml and build.gradle files - installTargetTemplate("AndroidManifest.template", - new File(projectFolder, srcMainPath + File.separator + - SdkConstants.FN_ANDROID_MANIFEST_XML), - keywords, target); - - String buildToolRev = mSdkManager.getLatestBuildTool().getRevision().toString(); - - keywords.put(PH_BUILD_TOOL_REV, buildToolRev); - keywords.put(PH_ARTIFACT_VERSION, artifactVersion); - keywords.put(PH_TARGET, target.hashString()); - keywords.put(PH_PLUGIN, (library) ? PLUGIN_LIB_PROJECT : PLUGIN_PROJECT); - - installTemplate("build_gradle.template", - new File(projectFolder, SdkConstants.FN_BUILD_GRADLE), - keywords); - - // Create the gradle wrapper files - createDirs(projectFolder, SdkConstants.FD_GRADLE_WRAPPER); - installGradleWrapperFile(SdkConstants.FD_GRADLE_WRAPPER + File.separator - + SdkConstants.FN_GRADLE_WRAPPER_JAR, - projectFolder); - installGradleWrapperFile(SdkConstants.FD_GRADLE_WRAPPER + File.separator - + SdkConstants.FN_GRADLE_WRAPPER_PROPERTIES, - projectFolder); - installGradleWrapperFile(SdkConstants.FN_GRADLE_WRAPPER_WIN, projectFolder); - installGradleWrapperFile(SdkConstants.FN_GRADLE_WRAPPER_UNIX, projectFolder); - new File(projectFolder, SdkConstants.FN_GRADLE_WRAPPER_UNIX).setExecutable(true, false); - } catch (Exception e) { - mLog.error(e, null); - } - } - - private File checkNewProjectLocation(String folderPath) { - File projectFolder = new File(folderPath); - if (!projectFolder.exists()) { - - boolean created = false; - Throwable t = null; - try { - created = projectFolder.mkdirs(); - } catch (Exception e) { - t = e; - } - - if (created) { - println("Created project directory: %1$s", projectFolder); - } else { - mLog.error(t, "Could not create directory: %1$s", projectFolder); - return null; - } - } else { - Exception e = null; - String error = null; - try { - String[] content = projectFolder.list(); - if (content == null) { - error = "Project folder '%1$s' is not a directory."; - } else if (content.length != 0) { - error = "Project folder '%1$s' is not empty. Please consider using '%2$s update' instead."; - } - } catch (Exception e1) { - e = e1; - } - - if (e != null || error != null) { - mLog.error(e, error, projectFolder, SdkConstants.androidCmdName()); - } - } - return projectFolder; - } - - /** - * Updates an existing project. - *

- * Workflow: - *

    - *
  • Check AndroidManifest.xml is present (required) - *
  • Check if there's a legacy properties file and convert it - *
  • Check there's a project.properties with a target *or* --target was specified - *
  • Update default.prop if --target was specified - *
  • Refresh/create "sdk" in local.properties - *
  • Build.xml: create if not present or if version-tag is found or not. version-tag:custom - * prevent any overwrite. version-tag:[integer] will override. missing version-tag will query - * the dev. - *
- * - * @param folderPath the folder of the project to update. This folder must exist. - * @param target the project target. Can be null. - * @param projectName The project name from --name. Can be null. - * @param libraryPath the path to a library to add to the references. Can be null. - * @return true if the project was successfully updated. - */ - @SuppressWarnings("deprecation") - public boolean updateProject(String folderPath, IAndroidTarget target, String projectName, - String libraryPath) { - // since this is an update, check the folder does point to a project - FileWrapper androidManifest = checkProjectFolder(folderPath, - SdkConstants.FN_ANDROID_MANIFEST_XML); - if (androidManifest == null) { - return false; - } - - // get the parent folder. - FolderWrapper projectFolder = (FolderWrapper) androidManifest.getParentFolder(); - - boolean hasProguard = false; - - // Check there's a project.properties with a target *or* --target was specified - IAndroidTarget originalTarget = null; - boolean writeProjectProp = false; - ProjectProperties props = ProjectProperties.load(projectFolder, PropertyType.PROJECT); - - if (props == null) { - // no project.properties, try to load default.properties - props = ProjectProperties.load(projectFolder, PropertyType.LEGACY_DEFAULT); - writeProjectProp = true; - } - - if (props != null) { - String targetHash = props.getProperty(ProjectProperties.PROPERTY_TARGET); - originalTarget = mSdkManager.getTargetFromHashString(targetHash); - - // if the project is already setup with proguard, we won't copy the proguard config. - hasProguard = props.getProperty(ProjectProperties.PROPERTY_PROGUARD_CONFIG) != null; - } - - if (originalTarget == null && target == null) { - mLog.error(null, - "The project either has no target set or the target is invalid.\n" + - "Please provide a --target to the '%1$s update' command.", - SdkConstants.androidCmdName()); - return false; - } - - boolean saveProjectProps = false; - - ProjectPropertiesWorkingCopy propsWC = null; - - // Update default.prop if --target was specified - if (target != null || writeProjectProp) { - // we already attempted to load the file earlier, if that failed, create it. - if (props == null) { - propsWC = ProjectProperties.create(projectFolder, PropertyType.PROJECT); - } else { - propsWC = props.makeWorkingCopy(PropertyType.PROJECT); - } - - // set or replace the target - if (target != null) { - propsWC.setProperty(ProjectProperties.PROPERTY_TARGET, target.hashString()); - } - saveProjectProps = true; - } - - if (libraryPath != null) { - // At this point, the default properties already exists, either because they were - // already there or because they were created with a new target - if (propsWC == null) { - assert props != null; - propsWC = props.makeWorkingCopy(); - } - - // check the reference is valid - File libProject = new File(libraryPath); - String resolvedPath; - if (libProject.isAbsolute() == false) { - libProject = new File(projectFolder, libraryPath); - try { - resolvedPath = libProject.getCanonicalPath(); - } catch (IOException e) { - mLog.error(e, "Unable to resolve path to library project: %1$s", libraryPath); - return false; - } - } else { - resolvedPath = libProject.getAbsolutePath(); - } - - println("Resolved location of library project to: %1$s", resolvedPath); - - // check the lib project exists - if (checkProjectFolder(resolvedPath, SdkConstants.FN_ANDROID_MANIFEST_XML) == null) { - mLog.error(null, "No Android Manifest at: %1$s", resolvedPath); - return false; - } - - // look for other references to figure out the index - int index = 1; - while (true) { - String propName = ProjectProperties.PROPERTY_LIB_REF + Integer.toString(index); - assert props != null; - if (props == null) { - // This should not happen yet SDK bug 20535 says it can, not sure how. - break; - } - String ref = props.getProperty(propName); - if (ref == null) { - break; - } else { - index++; - } - } - - String propName = ProjectProperties.PROPERTY_LIB_REF + Integer.toString(index); - propsWC.setProperty(propName, libraryPath); - saveProjectProps = true; - } - - // save the default props if needed. - if (saveProjectProps) { - try { - assert propsWC != null; - propsWC.save(); - if (writeProjectProp) { - println("Updated and renamed %1$s to %2$s", - PropertyType.LEGACY_DEFAULT.getFilename(), - PropertyType.PROJECT.getFilename()); - } else { - println("Updated %1$s", PropertyType.PROJECT.getFilename()); - } - } catch (Exception e) { - mLog.error(e, "Failed to write %1$s file in '%2$s'", - PropertyType.PROJECT.getFilename(), - folderPath); - return false; - } - - if (writeProjectProp) { - // need to delete the default prop file. - ProjectProperties.delete(projectFolder, PropertyType.LEGACY_DEFAULT); - } - } - - // Refresh/create "sdk" in local.properties - // because the file may already exists and contain other values (like apk config), - // we first try to load it. - props = ProjectProperties.load(projectFolder, PropertyType.LOCAL); - if (props == null) { - propsWC = ProjectProperties.create(projectFolder, PropertyType.LOCAL); - } else { - propsWC = props.makeWorkingCopy(); - } - - // set or replace the sdk location. - propsWC.setProperty(ProjectProperties.PROPERTY_SDK, mSdkFolder); - try { - propsWC.save(); - println("Updated %1$s", PropertyType.LOCAL.getFilename()); - } catch (Exception e) { - mLog.error(e, "Failed to write %1$s file in '%2$s'", - PropertyType.LOCAL.getFilename(), - folderPath); - return false; - } - - // legacy: check if build.properties must be renamed to ant.properties. - props = ProjectProperties.load(projectFolder, PropertyType.ANT); - if (props == null) { - props = ProjectProperties.load(projectFolder, PropertyType.LEGACY_BUILD); - if (props != null) { - try { - // get a working copy with the new property type - propsWC = props.makeWorkingCopy(PropertyType.ANT); - propsWC.save(); - - // delete the old file - ProjectProperties.delete(projectFolder, PropertyType.LEGACY_BUILD); - - println("Renamed %1$s to %2$s", - PropertyType.LEGACY_BUILD.getFilename(), - PropertyType.ANT.getFilename()); - } catch (Exception e) { - mLog.error(e, "Failed to write %1$s file in '%2$s'", - PropertyType.ANT.getFilename(), - folderPath); - return false; - } - } - } - - // Build.xml: create if not present or no in it - File buildXml = new File(projectFolder, SdkConstants.FN_BUILD_XML); - boolean needsBuildXml = projectName != null || !buildXml.exists(); - - // if it seems there's no need for a new build.xml, look for inside the file - // to try to detect old ones that may need updating. - if (!needsBuildXml) { - // we are looking for version-tag: followed by either an integer or "custom". - if (checkFileContainsRegexp(buildXml, "version-tag:\\s*custom") != null) { //$NON-NLS-1$ - println("%1$s: Found version-tag: custom. File will not be updated.", - SdkConstants.FN_BUILD_XML); - } else { - Matcher m = checkFileContainsRegexp(buildXml, "version-tag:\\s*(\\d+)"); //$NON-NLS-1$ - if (m == null) { - println("----------\n" + - "%1$s: Failed to find version-tag string. File must be updated.\n" + - "In order to not erase potential customizations, the file will not be automatically regenerated.\n" + - "If no changes have been made to the file, delete it manually and run the command again.\n" + - "If you have made customizations to the build process, the file must be manually updated.\n" + - "It is recommended to:\n" + - "\t* Copy current file to a safe location.\n" + - "\t* Delete original file.\n" + - "\t* Run command again to generate a new file.\n" + - "\t* Port customizations to the new file, by looking at the new rules file\n" + - "\t located at /tools/ant/build.xml\n" + - "\t* Update file to contain\n" + - "\t version-tag: custom\n" + - "\t to prevent file from being rewritten automatically by the SDK tools.\n" + - "----------\n", - SdkConstants.FN_BUILD_XML); - } else { - String versionStr = m.group(1); - if (versionStr != null) { - // can't fail due to regexp above. - int version = Integer.parseInt(versionStr); - if (version < MIN_BUILD_VERSION_TAG) { - println("%1$s: Found version-tag: %2$d. Expected version-tag: %3$d: file must be updated.", - SdkConstants.FN_BUILD_XML, version, MIN_BUILD_VERSION_TAG); - needsBuildXml = true; - } - } - } - } - } - - if (needsBuildXml) { - // create the map for place-holders of values to replace in the templates - final HashMap keywords = new HashMap(); - - // put the current version-tag value - keywords.put(PH_VERSION_TAG, Integer.toString(MIN_BUILD_VERSION_TAG)); - - // if there was no project name on the command line, figure one out. - if (projectName == null) { - // otherwise, take it from the existing build.xml if it exists already. - if (buildXml.exists()) { - try { - XPathFactory factory = XPathFactory.newInstance(); - XPath xpath = factory.newXPath(); - - projectName = xpath.evaluate(XPATH_PROJECT_NAME, - new InputSource(new FileInputStream(buildXml))); - } catch (XPathExpressionException e) { - // this is ok since we're going to recreate the file. - mLog.error(e, "Unable to find existing project name from %1$s", - SdkConstants.FN_BUILD_XML); - } catch (FileNotFoundException e) { - // can't happen since we check above. - } - } - - // if the project is still null, then we find another way. - if (projectName == null) { - extractPackageFromManifest(androidManifest, keywords); - if (keywords.containsKey(PH_ACTIVITY_ENTRY_NAME)) { - String activity = keywords.get(PH_ACTIVITY_ENTRY_NAME); - // keep only the last segment if applicable - int pos = activity.lastIndexOf('.'); - if (pos != -1) { - activity = activity.substring(pos + 1); - } - - // Use the activity as project name - projectName = activity; - - println("No project name specified, using Activity name '%1$s'.\n" + - "If you wish to change it, edit the first line of %2$s.", - activity, SdkConstants.FN_BUILD_XML); - } else { - // We need a project name. Just pick up the basename of the project - // directory. - File projectCanonicalFolder = projectFolder; - try { - projectCanonicalFolder = projectCanonicalFolder.getCanonicalFile(); - } catch (IOException e) { - // ignore, keep going - } - - // Use the folder name as project name - projectName = projectCanonicalFolder.getName(); - - println("No project name specified, using project folder name '%1$s'.\n" + - "If you wish to change it, edit the first line of %2$s.", - projectName, SdkConstants.FN_BUILD_XML); - } - } - } - - // put the project name in the map for replacement during the template installation. - keywords.put(PH_PROJECT_NAME, projectName); - - if (mLevel == OutputLevel.VERBOSE) { - println("Regenerating %1$s with project name %2$s", - SdkConstants.FN_BUILD_XML, - keywords.get(PH_PROJECT_NAME)); - } - - try { - installTemplate("build.template", buildXml, keywords); - } catch (ProjectCreateException e) { - mLog.error(e, null); - return false; - } - } - - if (hasProguard == false) { - try { - installTemplate(SdkConstants.FN_PROJECT_PROGUARD_FILE, - // Write ProGuard config files with the extension .pro which - // is what is used in the ProGuard documentation and samples - new File(projectFolder, SdkConstants.FN_PROJECT_PROGUARD_FILE), - null /*placeholderMap*/); - } catch (ProjectCreateException e) { - mLog.error(e, null); - return false; - } - } - - return true; - } - - /** - * Updates a test project with a new path to the main (tested) project. - * @param folderPath the path of the test project. - * @param pathToMainProject the path to the main project, relative to the test project. - */ - @SuppressWarnings("deprecation") - public void updateTestProject(final String folderPath, final String pathToMainProject, - final SdkManager sdkManager) { - // since this is an update, check the folder does point to a project - if (checkProjectFolder(folderPath, SdkConstants.FN_ANDROID_MANIFEST_XML) == null) { - return; - } - - // check the path to the main project is valid. - File mainProject = new File(pathToMainProject); - String resolvedPath; - if (mainProject.isAbsolute() == false) { - mainProject = new File(folderPath, pathToMainProject); - try { - resolvedPath = mainProject.getCanonicalPath(); - } catch (IOException e) { - mLog.error(e, "Unable to resolve path to main project: %1$s", pathToMainProject); - return; - } - } else { - resolvedPath = mainProject.getAbsolutePath(); - } - - println("Resolved location of main project to: %1$s", resolvedPath); - - // check the main project exists - if (checkProjectFolder(resolvedPath, SdkConstants.FN_ANDROID_MANIFEST_XML) == null) { - mLog.error(null, "No Android Manifest at: %1$s", resolvedPath); - return; - } - - // now get the target from the main project - ProjectProperties projectProp = ProjectProperties.load(resolvedPath, PropertyType.PROJECT); - if (projectProp == null) { - // legacy support for older file name. - projectProp = ProjectProperties.load(resolvedPath, PropertyType.LEGACY_DEFAULT); - if (projectProp == null) { - mLog.error(null, "No %1$s at: %2$s", PropertyType.PROJECT.getFilename(), - resolvedPath); - return; - } - } - - String targetHash = projectProp.getProperty(ProjectProperties.PROPERTY_TARGET); - if (targetHash == null) { - mLog.error(null, "%1$s in the main project has no target property.", - PropertyType.PROJECT.getFilename()); - return; - } - - IAndroidTarget target = sdkManager.getTargetFromHashString(targetHash); - if (target == null) { - mLog.error(null, "Main project target %1$s is not a valid target.", targetHash); - return; - } - - // update test-project does not support the --name parameter, therefore the project - // name should generally not be passed to updateProject(). - // However if build.xml does not exist then updateProject() will recreate it. In this - // case we will need the project name. - // To do this, we look for the parent project name and add "test" to it. - // If the main project does not have a project name (yet), then the default behavior - // will be used (look for activity and then folder name) - String projectName = null; - XPathFactory factory = XPathFactory.newInstance(); - XPath xpath = factory.newXPath(); - - File testBuildXml = new File(folderPath, SdkConstants.FN_BUILD_XML); - if (testBuildXml.isFile() == false) { - File mainBuildXml = new File(resolvedPath, SdkConstants.FN_BUILD_XML); - if (mainBuildXml.isFile()) { - try { - // get the name of the main project and add Test to it. - String mainProjectName = xpath.evaluate(XPATH_PROJECT_NAME, - new InputSource(new FileInputStream(mainBuildXml))); - projectName = mainProjectName + "Test"; - } catch (XPathExpressionException e) { - // it's ok, updateProject() will figure out a name automatically. - // We do log the error though as the build.xml file may be broken. - mLog.warning("Failed to parse %1$s.\n" + - "File may not be valid. Consider running 'android update project' on the main project.", - mainBuildXml.getPath()); - } catch (FileNotFoundException e) { - // should not happen since we check first. - } - } - } - - // now update the project as if it's a normal project - if (updateProject(folderPath, target, projectName, null /*libraryPath*/) == false) { - // error message has already been displayed. - return; - } - - // add the test project specific properties. - // At this point, we know build.prop has been renamed ant.prop - ProjectProperties antProps = ProjectProperties.load(folderPath, PropertyType.ANT); - ProjectPropertiesWorkingCopy antWorkingCopy; - if (antProps == null) { - antWorkingCopy = ProjectProperties.create(folderPath, PropertyType.ANT); - } else { - antWorkingCopy = antProps.makeWorkingCopy(); - } - - // set or replace the path to the main project - antWorkingCopy.setProperty(ProjectProperties.PROPERTY_TESTED_PROJECT, pathToMainProject); - try { - antWorkingCopy.save(); - println("Updated %1$s", PropertyType.ANT.getFilename()); - } catch (Exception e) { - mLog.error(e, "Failed to write %1$s file in '%2$s'", - PropertyType.ANT.getFilename(), - folderPath); - return; - } - } - - /** - * Checks whether the give folderPath is a valid project folder, and returns - * a {@link FileWrapper} to the required file. - *

This checks that the folder exists and contains an AndroidManifest.xml file in it. - *

Any error are output using {@link #mLog}. - * @param folderPath the folder to check - * @param requiredFilename the file name of the file that's required. - * @return a {@link FileWrapper} to the AndroidManifest.xml file, or null otherwise. - */ - private FileWrapper checkProjectFolder(String folderPath, String requiredFilename) { - // project folder must exist and be a directory, since this is an update - FolderWrapper projectFolder = new FolderWrapper(folderPath); - if (!projectFolder.isDirectory()) { - mLog.error(null, "Project folder '%1$s' is not a valid directory.", - projectFolder); - return null; - } - - // Check AndroidManifest.xml is present - FileWrapper requireFile = new FileWrapper(projectFolder, requiredFilename); - if (!requireFile.isFile()) { - mLog.error(null, - "%1$s is not a valid project (%2$s not found).", - folderPath, requiredFilename); - return null; - } - - return requireFile; - } - - /** - * Looks for a given regex in a file and returns the matcher if any line of the input file - * contains the requested regexp. - * - * @param file the file to search. - * @param regexp the regexp to search for. - * - * @return a Matcher or null if the regexp is not found. - */ - private Matcher checkFileContainsRegexp(File file, String regexp) { - Pattern p = Pattern.compile(regexp); - - BufferedReader in = null; - try { - in = new BufferedReader(new FileReader(file)); - String line; - - while ((line = in.readLine()) != null) { - Matcher m = p.matcher(line); - if (m.find()) { - return m; - } - } - - in.close(); - } catch (Exception e) { - // ignore - } finally { - if (in != null) { - try { - in.close(); - } catch (IOException e) { - // ignore - } - } - } - - return null; - } - - /** - * Extracts a "full" package & activity name from an AndroidManifest.xml. - *

- * The keywords dictionary is always filed the package name under the key {@link #PH_PACKAGE}. - * If an activity name can be found, it is filed under the key {@link #PH_ACTIVITY_ENTRY_NAME}. - * When no activity is found, this key is not created. - * - * @param manifestFile The AndroidManifest.xml file - * @param outKeywords Place where to put the out parameters: package and activity names. - * @return True if the package/activity was parsed and updated in the keyword dictionary. - */ - private boolean extractPackageFromManifest(File manifestFile, - Map outKeywords) { - try { - XPath xpath = AndroidXPathFactory.newXPath(); - - InputSource source = new InputSource(new FileReader(manifestFile)); - String packageName = xpath.evaluate("/manifest/@package", source); - - source = new InputSource(new FileReader(manifestFile)); - - // Select the "android:name" attribute of all nodes but only if they - // contain a sub-node with an "android:name" attribute which - // is 'android.intent.action.MAIN' and an with an - // "android:name" attribute which is 'android.intent.category.LAUNCHER' - String expression = String.format("/manifest/application/activity" + - "[intent-filter/action/@%1$s:name='android.intent.action.MAIN' and " + - "intent-filter/category/@%1$s:name='android.intent.category.LAUNCHER']" + - "/@%1$s:name", AndroidXPathFactory.DEFAULT_NS_PREFIX); - - NodeList activityNames = (NodeList) xpath.evaluate(expression, source, - XPathConstants.NODESET); - - // If we get here, both XPath expressions were valid so we're most likely dealing - // with an actual AndroidManifest.xml file. The nodes may not have the requested - // attributes though, if which case we should warn. - - if (packageName == null || packageName.isEmpty()) { - mLog.error(null, - "Missing in '%1$s'", - manifestFile.getName()); - return false; - } - - // Get the first activity that matched earlier. If there is no activity, - // activityName is set to an empty string and the generated "combined" name - // will be in the form "package." (with a dot at the end). - String activityName = ""; - if (activityNames.getLength() > 0) { - activityName = activityNames.item(0).getNodeValue(); - } - - if (mLevel == OutputLevel.VERBOSE && activityNames.getLength() > 1) { - println("WARNING: There is more than one activity defined in '%1$s'.\n" + - "Only the first one will be used. If this is not appropriate, you need\n" + - "to specify one of these values manually instead:", - manifestFile.getName()); - - for (int i = 0; i < activityNames.getLength(); i++) { - String name = activityNames.item(i).getNodeValue(); - name = combinePackageActivityNames(packageName, name); - println("- %1$s", name); - } - } - - if (activityName.isEmpty()) { - mLog.warning("Missing in '%2$s'.\n" + - "No activity will be generated.", - AndroidXPathFactory.DEFAULT_NS_PREFIX, manifestFile.getName()); - } else { - outKeywords.put(PH_ACTIVITY_ENTRY_NAME, activityName); - } - - outKeywords.put(PH_PACKAGE, packageName); - return true; - - } catch (IOException e) { - mLog.error(e, "Failed to read %1$s", manifestFile.getName()); - } catch (XPathExpressionException e) { - Throwable t = e.getCause(); - mLog.error(t == null ? e : t, - "Failed to parse %1$s", - manifestFile.getName()); - } - - return false; - } - - private String combinePackageActivityNames(String packageName, String activityName) { - // Activity Name can have 3 forms: - // - ".Name" means this is a class name in the given package name. - // The full FQCN is thus packageName + ".Name" - // - "Name" is an older variant of the former. Full FQCN is packageName + "." + "Name" - // - "com.blah.Name" is a full FQCN. Ignore packageName and use activityName as-is. - // To be valid, the package name should have at least two components. This is checked - // later during the creation of the build.xml file, so we just need to detect there's - // a dot but not at pos==0. - - int pos = activityName.indexOf('.'); - if (pos == 0) { - return packageName + activityName; - } else if (pos > 0) { - return activityName; - } else { - return packageName + "." + activityName; - } - } - - /** - * Installs a new file that is based on a template file provided by a given target. - * Each match of each key from the place-holder map in the template will be replaced with its - * corresponding value in the created file. - * - * @param templateName the name of to the template file - * @param destFile the path to the destination file, relative to the project - * @param placeholderMap a map of (place-holder, value) to create the file from the template. - * @param target the Target of the project that will be providing the template. - * @throws ProjectCreateException - */ - private void installTargetTemplate(String templateName, File destFile, - Map placeholderMap, IAndroidTarget target) - throws ProjectCreateException { - // query the target for its template directory - String templateFolder = target.getPath(IAndroidTarget.TEMPLATES); - final String sourcePath = templateFolder + File.separator + templateName; - - installFullPathTemplate(sourcePath, destFile, placeholderMap); - } - - /** - * Installs a new file from the gradle wrapper template. - * - * @param templateName the name of the template file - * @param projectFolder the path to the project folder - * @throws ProjectCreateException - */ - public void installGradleWrapperFile(String templateName, File projectFolder) - throws ProjectCreateException { - String templateFolder = mSdkFolder + File.separator + - SdkConstants.OS_SDK_TOOLS_TEMPLATES_GRADLE_WRAPPER_FOLDER; - - installBinaryFile(new File(templateFolder, templateName), - new File(projectFolder, templateName)); - } - - /** - * Installs a new file that is based on a template file provided by the tools folder. - * Each match of each key from the place-holder map in the template will be replaced with its - * corresponding value in the created file. - * - * @param templateName the name of to the template file - * @param destFile the path to the destination file, relative to the project - * @param placeholderMap a map of (place-holder, value) to create the file from the template. - * @throws ProjectCreateException - */ - public void installTemplate(String templateName, File destFile, - Map placeholderMap) - throws ProjectCreateException { - // query the target for its template directory - String templateFolder = mSdkFolder + File.separator + SdkConstants.OS_SDK_TOOLS_LIB_FOLDER; - final String sourcePath = templateFolder + File.separator + templateName; - - installFullPathTemplate(sourcePath, destFile, placeholderMap); - } - - /** - * Installs a new file that is based on a template. - * Each match of each key from the place-holder map in the template will be replaced with its - * corresponding value in the created file. - * - * @param sourcePath the full path to the source template file - * @param destFile the destination file - * @param placeholderMap a map of (place-holder, value) to create the file from the template. - * @throws ProjectCreateException - */ - private void installFullPathTemplate(String sourcePath, File destFile, - Map placeholderMap) throws ProjectCreateException { - - boolean existed = destFile.exists(); - - try { - BufferedWriter out = new BufferedWriter(new FileWriter(destFile)); - BufferedReader in = new BufferedReader(new FileReader(sourcePath)); - String line; - - while ((line = in.readLine()) != null) { - if (placeholderMap != null) { - for (Map.Entry entry : placeholderMap.entrySet()) { - line = line.replace(entry.getKey(), entry.getValue()); - } - } - - out.write(line); - out.newLine(); - } - - out.close(); - in.close(); - } catch (Exception e) { - throw new ProjectCreateException(e, "Could not access %1$s: %2$s", - destFile, e.getMessage()); - } - - println("%1$s file %2$s", - existed ? "Updated" : "Added", - destFile); - } - - /** - * Installs the project icons. - * @param resourceFolder the resource folder - * @param target the target of the project. - * @return true if any icon was installed. - */ - private boolean installIcons(File resourceFolder, IAndroidTarget target) - throws ProjectCreateException { - // query the target for its template directory - String templateFolder = target.getPath(IAndroidTarget.TEMPLATES); - - boolean installedIcon = false; - - installedIcon |= installIcon(templateFolder, "ic_launcher_xhdpi.png", resourceFolder, - "drawable-xhdpi"); - installedIcon |= installIcon(templateFolder, "ic_launcher_hdpi.png", resourceFolder, - "drawable-hdpi"); - installedIcon |= installIcon(templateFolder, "ic_launcher_mdpi.png", resourceFolder, - "drawable-mdpi"); - installedIcon |= installIcon(templateFolder, "ic_launcher_ldpi.png", resourceFolder, - "drawable-ldpi"); - - return installedIcon; - } - - /** - * Installs an Icon in the project. - * @return true if the icon was installed. - */ - private boolean installIcon(String templateFolder, String iconName, File resourceFolder, - String folderName) throws ProjectCreateException { - File icon = new File(templateFolder, iconName); - if (icon.exists()) { - File drawable = createDirs(resourceFolder, folderName); - installBinaryFile(icon, new File(drawable, "ic_launcher.png")); - return true; - } - - return false; - } - - /** - * Installs a binary file - * @param source the source file to copy - * @param destination the destination file to write - * @throws ProjectCreateException - */ - private void installBinaryFile(File source, File destination) throws ProjectCreateException { - byte[] buffer = new byte[8192]; - - FileInputStream fis = null; - FileOutputStream fos = null; - try { - fis = new FileInputStream(source); - fos = new FileOutputStream(destination); - - int read; - while ((read = fis.read(buffer)) != -1) { - fos.write(buffer, 0, read); - } - - } catch (FileNotFoundException e) { - // shouldn't happen since we check before. - } catch (IOException e) { - throw new ProjectCreateException(e, "Failed to read binary file: %1$s", - source.getAbsolutePath()); - } finally { - if (fis != null) { - try { - fis.close(); - } catch (IOException e) { - // ignore - } - } - if (fos != null) { - try { - fos.close(); - } catch (IOException e) { - // ignore - } - } - } - - } - - /** - * Prints a message unless silence is enabled. - *

- * This is just a convenience wrapper around {@link ILogger#info(String, Object...)} from - * {@link #mLog} after testing if output level is {@link OutputLevel#VERBOSE}. - * - * @param format Format for String.format - * @param args Arguments for String.format - */ - private void println(String format, Object... args) { - if (mLevel != OutputLevel.SILENT) { - if (!format.endsWith("\n")) { - format += "\n"; - } - mLog.info(format, args); - } - } - - /** - * Creates a new folder, along with any parent folders that do not exists. - * - * @param parent the parent folder - * @param name the name of the directory to create. - * @throws ProjectCreateException - */ - private File createDirs(File parent, String name) throws ProjectCreateException { - final File newFolder = new File(parent, name); - boolean existedBefore = true; - - if (!newFolder.exists()) { - if (!newFolder.mkdirs()) { - throw new ProjectCreateException("Could not create directory: %1$s", newFolder); - } - existedBefore = false; - } - - if (newFolder.isDirectory()) { - if (!newFolder.canWrite()) { - throw new ProjectCreateException("Path is not writable: %1$s", newFolder); - } - } else { - throw new ProjectCreateException("Path is not a directory: %1$s", newFolder); - } - - if (!existedBefore) { - try { - println("Created directory %1$s", newFolder.getCanonicalPath()); - } catch (IOException e) { - throw new ProjectCreateException( - "Could not determine canonical path of created directory", e); - } - } - - return newFolder; - } - - /** - * Strips the string of beginning and trailing characters (multiple - * characters will be stripped, example stripString("..test...", '.') - * results in "test"; - * - * @param s the string to strip - * @param strip the character to strip from beginning and end - * @return the stripped string or the empty string if everything is stripped. - */ - private static String stripString(String s, char strip) { - final int sLen = s.length(); - int newStart = 0, newEnd = sLen - 1; - - while (newStart < sLen && s.charAt(newStart) == strip) { - newStart++; - } - while (newEnd >= 0 && s.charAt(newEnd) == strip) { - newEnd--; - } - - /* - * newEnd contains a char we want, and substring takes end as being - * exclusive - */ - newEnd++; - - if (newStart >= sLen || newEnd < 0) { - return ""; - } - - return s.substring(newStart, newEnd); - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/project/ProjectProperties.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/project/ProjectProperties.java deleted file mode 100644 index 7c574694..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/project/ProjectProperties.java +++ /dev/null @@ -1,594 +0,0 @@ -/* - * 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.sdklib.internal.project; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.io.FolderWrapper; -import com.android.io.IAbstractFile; -import com.android.io.IAbstractFolder; -import com.android.io.StreamException; -import com.android.utils.ILogger; -import com.google.common.io.Closeables; - -import java.io.BufferedReader; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Properties; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Class representing project properties for both ADT and Ant-based build. - *

The class is associated to a {@link PropertyType} that indicate which of the project - * property file is represented. - *

To load an existing file, use {@link #load(IAbstractFolder, PropertyType)}. - *

The class is meant to be always in sync (or at least not newer) than the file it represents. - * Once created, it can only be updated through {@link #reload()} - * - *

The make modification or make new file, use a {@link ProjectPropertiesWorkingCopy} instance, - * either through {@link #create(IAbstractFolder, PropertyType)} or through - * {@link #makeWorkingCopy()}. - * - */ -public class ProjectProperties implements IPropertySource { - protected static final Pattern PATTERN_PROP = Pattern.compile( - "^([a-zA-Z0-9._-]+)\\s*=\\s*(.*)\\s*$"); - - /** The property name for the project target */ - public static final String PROPERTY_TARGET = "target"; - /** The property name for the renderscript build target */ - public static final String PROPERTY_RS_TARGET = "renderscript.target"; - /** The property name for the renderscript support mode */ - public static final String PROPERTY_RS_SUPPORT = "renderscript.support.mode"; - /** The version of the build tools to use to compile */ - public static final String PROPERTY_BUILD_TOOLS = "sdk.buildtools"; - - public static final String PROPERTY_LIBRARY = "android.library"; - public static final String PROPERTY_LIB_REF = "android.library.reference."; - private static final String PROPERTY_LIB_REF_REGEX = "android.library.reference.\\d+"; - - public static final String PROPERTY_PROGUARD_CONFIG = "proguard.config"; - public static final String PROPERTY_RULES_PATH = "layoutrules.jars"; - - public static final String PROPERTY_SDK = "sdk.dir"; - public static final String PROPERTY_NDK = "ndk.dir"; - // LEGACY - Kept so that we can actually remove it from local.properties. - private static final String PROPERTY_SDK_LEGACY = "sdk-location"; - - public static final String PROPERTY_SPLIT_BY_DENSITY = "split.density"; - public static final String PROPERTY_SPLIT_BY_ABI = "split.abi"; - public static final String PROPERTY_SPLIT_BY_LOCALE = "split.locale"; - - public static final String PROPERTY_TESTED_PROJECT = "tested.project.dir"; - - public static final String PROPERTY_BUILD_SOURCE_DIR = "source.dir"; - public static final String PROPERTY_BUILD_OUT_DIR = "out.dir"; - - public static final String PROPERTY_PACKAGE = "package"; - public static final String PROPERTY_VERSIONCODE = "versionCode"; - public static final String PROPERTY_PROJECTS = "projects"; - public static final String PROPERTY_KEY_STORE = "key.store"; - public static final String PROPERTY_KEY_ALIAS = "key.alias"; - - public enum PropertyType { - ANT(SdkConstants.FN_ANT_PROPERTIES, BUILD_HEADER, new String[] { - PROPERTY_BUILD_SOURCE_DIR, PROPERTY_BUILD_OUT_DIR - }, null), - PROJECT(SdkConstants.FN_PROJECT_PROPERTIES, DEFAULT_HEADER, new String[] { - PROPERTY_TARGET, PROPERTY_LIBRARY, PROPERTY_LIB_REF_REGEX, - PROPERTY_KEY_STORE, PROPERTY_KEY_ALIAS, PROPERTY_PROGUARD_CONFIG, - PROPERTY_RULES_PATH - }, null), - LOCAL(SdkConstants.FN_LOCAL_PROPERTIES, LOCAL_HEADER, new String[] { - PROPERTY_SDK - }, - new String[] { PROPERTY_SDK_LEGACY }), - @Deprecated - LEGACY_DEFAULT("default.properties", null, null, null), - @Deprecated - LEGACY_BUILD("build.properties", null, null, null); - - - private final String mFilename; - private final String mHeader; - private final Set mKnownProps; - private final Set mRemovedProps; - - /** - * Returns the PropertyTypes ordered the same way Ant order them. - */ - public static PropertyType[] getOrderedTypes() { - return new PropertyType[] { - PropertyType.LOCAL, PropertyType.ANT, PropertyType.PROJECT - }; - } - - PropertyType(String filename, String header, String[] validProps, String[] removedProps) { - mFilename = filename; - mHeader = header; - HashSet s = new HashSet(); - if (validProps != null) { - s.addAll(Arrays.asList(validProps)); - } - mKnownProps = Collections.unmodifiableSet(s); - - s = new HashSet(); - if (removedProps != null) { - s.addAll(Arrays.asList(removedProps)); - } - mRemovedProps = Collections.unmodifiableSet(s); - - } - - public String getFilename() { - return mFilename; - } - - public String getHeader() { - return mHeader; - } - - /** - * Returns whether a given property is known for the property type. - */ - public boolean isKnownProperty(String name) { - for (String propRegex : mKnownProps) { - if (propRegex.equals(name) || Pattern.matches(propRegex, name)) { - return true; - } - } - - return false; - } - - /** - * Returns whether a given property should be removed for the property type. - */ - public boolean isRemovedProperty(String name) { - for (String propRegex : mRemovedProps) { - if (propRegex.equals(name) || Pattern.matches(propRegex, name)) { - return true; - } - } - - return false; - } - } - - private static final String LOCAL_HEADER = -// 1-------10--------20--------30--------40--------50--------60--------70--------80 - "# This file is automatically generated by Android Tools.\n" + - "# Do not modify this file -- YOUR CHANGES WILL BE ERASED!\n" + - "#\n" + - "# This file must *NOT* be checked into Version Control Systems,\n" + - "# as it contains information specific to your local configuration.\n" + - "\n"; - - private static final String DEFAULT_HEADER = -// 1-------10--------20--------30--------40--------50--------60--------70--------80 - "# This file is automatically generated by Android Tools.\n" + - "# Do not modify this file -- YOUR CHANGES WILL BE ERASED!\n" + - "#\n" + - "# This file must be checked in Version Control Systems.\n" + - "#\n" + - "# To customize properties used by the Ant build system edit\n" + - "# \"ant.properties\", and override values to adapt the script to your\n" + - "# project structure.\n" + - "#\n" + - "# To enable ProGuard to shrink and obfuscate your code, uncomment this " - + "(available properties: sdk.dir, user.home):\n" + - // Note: always use / separators in the properties paths. Both Ant and - // our ExportHelper will convert them properly according to the platform. - "#" + PROPERTY_PROGUARD_CONFIG + "=${" + PROPERTY_SDK +"}/" - + SdkConstants.FD_TOOLS + '/' + SdkConstants.FD_PROGUARD + '/' - + SdkConstants.FN_ANDROID_PROGUARD_FILE + ':' - + SdkConstants.FN_PROJECT_PROGUARD_FILE +'\n' + - "\n"; - - private static final String BUILD_HEADER = -// 1-------10--------20--------30--------40--------50--------60--------70--------80 - "# This file is used to override default values used by the Ant build system.\n" + - "#\n" + - "# This file must be checked into Version Control Systems, as it is\n" + - "# integral to the build system of your project.\n" + - "\n" + - "# This file is only used by the Ant script.\n" + - "\n" + - "# You can use this to override default values such as\n" + - "# 'source.dir' for the location of your java source folder and\n" + - "# 'out.dir' for the location of your output folder.\n" + - "\n" + - "# You can also use it define how the release builds are signed by declaring\n" + - "# the following properties:\n" + - "# 'key.store' for the location of your keystore and\n" + - "# 'key.alias' for the name of the key to use.\n" + - "# The password will be asked during the build when you use the 'release' target.\n" + - "\n"; - - protected final IAbstractFolder mProjectFolder; - protected final Map mProperties; - protected final PropertyType mType; - - /** - * Loads a project properties file and return a {@link ProjectProperties} object - * containing the properties. - * - * @param projectFolderOsPath the project folder. - * @param type One the possible {@link PropertyType}s. - */ - public static ProjectProperties load(String projectFolderOsPath, PropertyType type) { - IAbstractFolder wrapper = new FolderWrapper(projectFolderOsPath); - return load(wrapper, type); - } - - /** - * Loads a project properties file and return a {@link ProjectProperties} object - * containing the properties. - * - * @param projectFolder the project folder. - * @param type One the possible {@link PropertyType}s. - */ - public static ProjectProperties load(IAbstractFolder projectFolder, PropertyType type) { - if (projectFolder.exists()) { - IAbstractFile propFile = projectFolder.getFile(type.mFilename); - if (propFile.exists()) { - Map map = parsePropertyFile(propFile, null /* log */); - if (map != null) { - return new ProjectProperties(projectFolder, map, type); - } - } - } - return null; - } - - /** - * Deletes a project properties file. - * - * @param projectFolder the project folder. - * @param type One the possible {@link PropertyType}s. - * @return true if success. - */ - public static boolean delete(IAbstractFolder projectFolder, PropertyType type) { - if (projectFolder.exists()) { - IAbstractFile propFile = projectFolder.getFile(type.mFilename); - if (propFile.exists()) { - return propFile.delete(); - } - } - - return false; - } - - /** - * Deletes a project properties file. - * - * @param projectFolderOsPath the project folder. - * @param type One the possible {@link PropertyType}s. - * @return true if success. - */ - public static boolean delete(String projectFolderOsPath, PropertyType type) { - IAbstractFolder wrapper = new FolderWrapper(projectFolderOsPath); - return delete(wrapper, type); - } - - - /** - * Creates a new project properties object, with no properties. - *

The file is not created until {@link ProjectPropertiesWorkingCopy#save()} is called. - * @param projectFolderOsPath the project folder. - * @param type the type of property file to create - * - * @see #createEmpty(String, PropertyType) - */ - public static ProjectPropertiesWorkingCopy create(@NonNull String projectFolderOsPath, - @NonNull PropertyType type) { - // create and return a ProjectProperties with an empty map. - IAbstractFolder folder = new FolderWrapper(projectFolderOsPath); - return create(folder, type); - } - - /** - * Creates a new project properties object, with no properties. - *

The file is not created until {@link ProjectPropertiesWorkingCopy#save()} is called. - * @param projectFolder the project folder. - * @param type the type of property file to create - * - * @see #createEmpty(IAbstractFolder, PropertyType) - */ - public static ProjectPropertiesWorkingCopy create(@NonNull IAbstractFolder projectFolder, - @NonNull PropertyType type) { - // create and return a ProjectProperties with an empty map. - return new ProjectPropertiesWorkingCopy(projectFolder, new HashMap(), type); - } - - /** - * Creates a new project properties object, with no properties. - *

Nothing can be added to it, unless a {@link ProjectPropertiesWorkingCopy} is created - * first with {@link #makeWorkingCopy()}. - * @param projectFolderOsPath the project folder. - * @param type the type of property file to create - * - * @see #create(String, PropertyType) - */ - public static ProjectProperties createEmpty(@NonNull String projectFolderOsPath, - @NonNull PropertyType type) { - // create and return a ProjectProperties with an empty map. - IAbstractFolder folder = new FolderWrapper(projectFolderOsPath); - return createEmpty(folder, type); - } - - /** - * Creates a new project properties object, with no properties. - *

Nothing can be added to it, unless a {@link ProjectPropertiesWorkingCopy} is created - * first with {@link #makeWorkingCopy()}. - * @param projectFolder the project folder. - * @param type the type of property file to create - * - * @see #create(IAbstractFolder, PropertyType) - */ - public static ProjectProperties createEmpty(@NonNull IAbstractFolder projectFolder, - @NonNull PropertyType type) { - // create and return a ProjectProperties with an empty map. - return new ProjectProperties(projectFolder, new HashMap(), type); - } - - /** - * Returns the location of this property file. - */ - public IAbstractFile getFile() { - return mProjectFolder.getFile(mType.mFilename); - } - - /** - * Creates and returns a copy of the current properties as a - * {@link ProjectPropertiesWorkingCopy} that can be modified and saved. - * @return a new instance of {@link ProjectPropertiesWorkingCopy} - */ - public ProjectPropertiesWorkingCopy makeWorkingCopy() { - return makeWorkingCopy(mType); - } - - /** - * Creates and returns a copy of the current properties as a - * {@link ProjectPropertiesWorkingCopy} that can be modified and saved. This also allows - * converting to a new type, by specifying a different {@link PropertyType}. - * - * @param type the {@link PropertyType} of the prop file to save. - * - * @return a new instance of {@link ProjectPropertiesWorkingCopy} - */ - public ProjectPropertiesWorkingCopy makeWorkingCopy(PropertyType type) { - // copy the current properties in a new map - Map propList = new HashMap(mProperties); - - return new ProjectPropertiesWorkingCopy(mProjectFolder, propList, type); - } - - /** - * Returns the type of the property file. - * - * @see PropertyType - */ - public PropertyType getType() { - return mType; - } - - /** - * Returns the value of a property. - * @param name the name of the property. - * @return the property value or null if the property is not set. - */ - @Override - public synchronized String getProperty(String name) { - return mProperties.get(name); - } - - /** - * Returns a set of the property keys. Unlike {@link Map#keySet()} this is not a view of the - * map keys. Modifying the returned {@link Set} will not impact the underlying {@link Map}. - */ - public synchronized Set keySet() { - return new HashSet(mProperties.keySet()); - } - - /** - * Reloads the properties from the underlying file. - */ - public synchronized void reload() { - if (mProjectFolder.exists()) { - IAbstractFile propFile = mProjectFolder.getFile(mType.mFilename); - if (propFile.exists()) { - Map map = parsePropertyFile(propFile, null /* log */); - if (map != null) { - mProperties.clear(); - mProperties.putAll(map); - } - } - } - } - - /** - * Parses a property file (using UTF-8 encoding) and returns a map of the content. - *

- * If the file is not present, null is returned with no error messages sent to the log. - *

- * IMPORTANT: This method is now unfortunately used in multiple places to parse random - * property files. This is NOT a safe practice since there is no corresponding method - * to write property files unless you use {@link ProjectPropertiesWorkingCopy#save()}. - * Code that writes INI or properties without at least using {@link #escape(String)} will - * certainly not load back correct data.
- * Unless there's a strong legacy need to support existing files, new callers should - * probably just use Java's {@link Properties} which has well defined semantics. - * It's also a mistake to write/read property files using this code and expect it to - * work with Java's {@link Properties} or external tools (e.g. ant) since there can be - * differences in escaping and in character encoding. - * - * @param propFile the property file to parse - * @param log the ILogger object receiving warning/error from the parsing. - * @return the map of (key,value) pairs, or null if the parsing failed. - */ - public static Map parsePropertyFile( - @NonNull IAbstractFile propFile, - @Nullable ILogger log) { - InputStream is = null; - try { - is = propFile.getContents(); - return parsePropertyStream(is, - propFile.getOsLocation(), - log); - } catch (StreamException e) { - if (log != null) { - log.warning("Error parsing '%1$s': %2$s.", - propFile.getOsLocation(), - e.getMessage()); - } - } finally { - try { - Closeables.close(is, true /* swallowIOException */); - } catch (IOException e) { - // cannot happen - } - } - - - return null; - } - - /** - * Parses a property file (using UTF-8 encoding) and returns a map of the content. - *

- * Always closes the given input stream on exit. - *

- * IMPORTANT: This method is now unfortunately used in multiple places to parse random - * property files. This is NOT a safe practice since there is no corresponding method - * to write property files unless you use {@link ProjectPropertiesWorkingCopy#save()}. - * Code that writes INI or properties without at least using {@link #escape(String)} will - * certainly not load back correct data.
- * Unless there's a strong legacy need to support existing files, new callers should - * probably just use Java's {@link Properties} which has well defined semantics. - * It's also a mistake to write/read property files using this code and expect it to - * work with Java's {@link Properties} or external tools (e.g. ant) since there can be - * differences in escaping and in character encoding. - * - * @param propStream the input stream of the property file to parse. - * @param propPath the file path, for display purposed in case of error. - * @param log the ILogger object receiving warning/error from the parsing. - * @return the map of (key,value) pairs, or null if the parsing failed. - */ - public static Map parsePropertyStream( - @NonNull InputStream propStream, - @NonNull String propPath, - @Nullable ILogger log) { - BufferedReader reader = null; - try { - //noinspection IOResourceOpenedButNotSafelyClosed - reader = new BufferedReader( - new InputStreamReader(propStream, SdkConstants.INI_CHARSET)); - - String line = null; - Map map = new HashMap(); - while ((line = reader.readLine()) != null) { - line = line.trim(); - if (!line.isEmpty() && line.charAt(0) != '#') { - - Matcher m = PATTERN_PROP.matcher(line); - if (m.matches()) { - map.put(m.group(1), unescape(m.group(2))); - } else { - if (log != null) { - log.warning("Error parsing '%1$s': \"%2$s\" is not a valid syntax", - propPath, - line); - } - return null; - } - } - } - - return map; - } catch (FileNotFoundException e) { - // this should not happen since we usually test the file existence before - // calling the method. - // Return null below. - } catch (IOException e) { - if (log != null) { - log.warning("Error parsing '%1$s': %2$s.", - propPath, - e.getMessage()); - } - } finally { - try { - Closeables.close(reader, true /* swallowIOException */); - } catch (IOException e) { - // cannot happen - } - try { - Closeables.close(propStream, true /* swallowIOException */); - } catch (IOException e) { - // cannot happen - } - } - - return null; - } - - /** - * Private constructor. - *

- * Use {@link #load(String, PropertyType)} or {@link #create(String, PropertyType)} - * to instantiate. - */ - protected ProjectProperties( - @NonNull IAbstractFolder projectFolder, - @NonNull Map map, - @NonNull PropertyType type) { - mProjectFolder = projectFolder; - mProperties = map; - mType = type; - } - - private static String unescape(String value) { - return value.replaceAll("\\\\\\\\", "\\\\"); - } - - protected static String escape(String value) { - return value.replaceAll("\\\\", "\\\\\\\\"); - } - - @Override - public void debugPrint() { - System.out.println("DEBUG PROJECTPROPERTIES: " + mProjectFolder); - System.out.println("type: " + mType); - for (Entry entry : mProperties.entrySet()) { - System.out.println(entry.getKey() + " -> " + entry.getValue()); - } - System.out.println("<<< DEBUG PROJECTPROPERTIES"); - - } - -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/project/ProjectPropertiesWorkingCopy.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/project/ProjectPropertiesWorkingCopy.java deleted file mode 100644 index 4fac8f9d..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/project/ProjectPropertiesWorkingCopy.java +++ /dev/null @@ -1,280 +0,0 @@ -/* - * Copyright (C) 2010 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.sdklib.internal.project; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.io.IAbstractFile; -import com.android.io.IAbstractFolder; -import com.android.io.StreamException; -import com.google.common.io.Closeables; - -import java.io.BufferedReader; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.regex.Matcher; - -/** - * A modifiable and savable copy of a {@link ProjectProperties}. - *

This copy gives access to modification method such as {@link #setProperty(String, String)} - * and {@link #removeProperty(String)}. - * - * To get access to an instance, use {@link ProjectProperties#makeWorkingCopy()} or - * {@link ProjectProperties#create(IAbstractFolder, PropertyType)}. - */ -public class ProjectPropertiesWorkingCopy extends ProjectProperties { - - private static final Map COMMENT_MAP = new HashMap(); - static { -// 1-------10--------20--------30--------40--------50--------60--------70--------80 - COMMENT_MAP.put(PROPERTY_TARGET, - "# Project target.\n"); - COMMENT_MAP.put(PROPERTY_SPLIT_BY_DENSITY, - "# Indicates whether an apk should be generated for each density.\n"); - COMMENT_MAP.put(PROPERTY_SDK, - "# location of the SDK. This is only used by Ant\n" + - "# For customization when using a Version Control System, please read the\n" + - "# header note.\n"); - COMMENT_MAP.put(PROPERTY_PACKAGE, - "# Package of the application being exported\n"); - COMMENT_MAP.put(PROPERTY_VERSIONCODE, - "# Major version code\n"); - COMMENT_MAP.put(PROPERTY_PROJECTS, - "# List of the Android projects being used for the export.\n" + - "# The list is made of paths that are relative to this project,\n" + - "# using forward-slash (/) as separator, and are separated by colons (:).\n"); - } - - - /** - * Sets a new properties. If a property with the same name already exists, it is replaced. - * @param name the name of the property. - * @param value the value of the property. - */ - public synchronized void setProperty(String name, String value) { - mProperties.put(name, value); - } - - /** - * Removes a property and returns its previous value (or null if the property did not exist). - * @param name the name of the property to remove. - */ - public synchronized String removeProperty(String name) { - return mProperties.remove(name); - } - - /** - * Merges all properties from the given file into the current properties. - *

- * This emulates the Ant behavior: existing properties are not overridden. - * Only new undefined properties become defined. - *

- * Typical usage: - *

    - *
  • Create a ProjectProperties with {@code PropertyType#ANT} - *
  • Merge in values using {@code PropertyType#PROJECT} - *
  • The result is that this contains all the properties from default plus those - * overridden by the build.properties file. - *
- * - * @param type One the possible {@link ProjectProperties.PropertyType}s. - * @return this object, for chaining. - */ - public synchronized ProjectPropertiesWorkingCopy merge(PropertyType type) { - if (mProjectFolder.exists() && mType != type) { - IAbstractFile propFile = mProjectFolder.getFile(type.getFilename()); - if (propFile.exists()) { - Map map = parsePropertyFile(propFile, null /* log */); - if (map != null) { - for (Entry entry : map.entrySet()) { - String key = entry.getKey(); - String value = entry.getValue(); - if (!mProperties.containsKey(key) && value != null) { - mProperties.put(key, value); - } - } - } - } - } - return this; - } - - - /** - * Saves the property file, using UTF-8 encoding. - * @throws IOException - * @throws StreamException - */ - public synchronized void save() throws IOException, StreamException { - IAbstractFile toSave = mProjectFolder.getFile(mType.getFilename()); - - // write the whole file in a byte array before dumping it in the file. This - // This is so that if the file already existing - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - OutputStreamWriter writer = new OutputStreamWriter(baos, SdkConstants.INI_CHARSET); - - if (toSave.exists()) { - InputStream contentStream = toSave.getContents(); - InputStreamReader isr = null; - BufferedReader reader = null; - - try { - contentStream = toSave.getContents(); - //noinspection IOResourceOpenedButNotSafelyClosed - isr = new InputStreamReader(contentStream, SdkConstants.INI_CHARSET); - //noinspection IOResourceOpenedButNotSafelyClosed - reader = new BufferedReader(isr); - - // since we're reading the existing file and replacing values with new ones, or skipping - // removed values, we need to record what properties have been visited, so that - // we can figure later what new properties need to be added at the end of the file. - Set visitedProps = new HashSet(); - - String line = null; - while ((line = reader.readLine()) != null) { - // check if this is a line containing a property. - if (!line.isEmpty() && line.charAt(0) != '#') { - - Matcher m = PATTERN_PROP.matcher(line); - if (m.matches()) { - String key = m.group(1); - String value = m.group(2); - - // record the prop - visitedProps.add(key); - - // check if this property must be removed. - if (mType.isRemovedProperty(key)) { - value = null; - } else if (mProperties.containsKey(key)) { // if the property still exists. - // put the new value. - value = mProperties.get(key); - } else { - // property doesn't exist. Check if it's a known property. - // if it's a known one, we'll remove it, otherwise, leave it untouched. - if (mType.isKnownProperty(key)) { - value = null; - } - } - - // if the value is still valid, write it down. - if (value != null) { - writeValue(writer, key, value, false /*addComment*/); - } - } else { - // the line was wrong, let's just ignore it so that it's removed from the - // file. - } - } else { - // non-property line: just write the line in the output as-is. - writer.append(line).append('\n'); - } - } - - // now add the new properties. - for (Entry entry : mProperties.entrySet()) { - if (!visitedProps.contains(entry.getKey())) { - String value = entry.getValue(); - if (value != null) { - writeValue(writer, entry.getKey(), value, true /*addComment*/); - } - } - } - } finally { - try { - Closeables.close(reader, true /* swallowIOException */); - } catch (IOException e) { - // cannot happen - } - try { - Closeables.close(isr, true /* swallowIOException */); - } catch (IOException e) { - // cannot happen - } - try { - Closeables.close(contentStream, true /* swallowIOException */); - } catch (IOException e) { - // cannot happen - } - } - - } else { - // new file, just write it all - - // write the header (can be null, for example for PropertyType.LEGACY_BUILD) - if (mType.getHeader() != null) { - writer.write(mType.getHeader()); - } - - // write the properties. - for (Entry entry : mProperties.entrySet()) { - String value = entry.getValue(); - if (value != null) { - writeValue(writer, entry.getKey(), value, true /*addComment*/); - } - } - } - - writer.flush(); - - // now put the content in the file. - OutputStream filestream = toSave.getOutputStream(); - filestream.write(baos.toByteArray()); - filestream.flush(); - filestream.close(); - } - - private void writeValue(OutputStreamWriter writer, String key, String value, - boolean addComment) throws IOException { - if (addComment) { - String comment = COMMENT_MAP.get(key); - if (comment != null) { - writer.write(comment); - } - } - - writer.write(String.format("%s=%s\n", key, escape(value))); - } - - /** - * Private constructor. - *

- * Use {@link #load(String, PropertyType)} or {@link #create(String, PropertyType)} - * to instantiate. - */ - ProjectPropertiesWorkingCopy(IAbstractFolder projectFolder, Map map, - PropertyType type) { - super(projectFolder, map, type); - } - - @NonNull - public ProjectProperties makeReadOnlyCopy() { - // copy the current properties in a new map - Map propList = new HashMap(mProperties); - - return new ProjectProperties(mProjectFolder, propList, mType); - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/AdbWrapper.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/AdbWrapper.java deleted file mode 100644 index ae256eb3..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/AdbWrapper.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * 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.sdklib.internal.repository; - -import com.android.SdkConstants; - -import java.io.File; -import java.io.IOException; - -/** - * A lightweight wrapper to start & stop ADB. - * This is specific to the SDK Manager install process. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class AdbWrapper { - - /* - * Note: we could bring ddmlib in SdkManager for that purpose, however this allows us to - * specialize the start/stop methods to our needs (e.g. a task monitor, etc.) - */ - - private final String mAdbOsLocation; - private final ITaskMonitor mMonitor; - - /** - * Creates a new lightweight ADB wrapper. - * - * @param osSdkPath The root OS path of the SDK. Cannot be null. - * @param monitor A logger object. Cannot be null. - */ - public AdbWrapper(String osSdkPath, ITaskMonitor monitor) { - mMonitor = monitor; - - if (!osSdkPath.endsWith(File.separator)) { - osSdkPath += File.separator; - } - mAdbOsLocation = osSdkPath + SdkConstants.OS_SDK_PLATFORM_TOOLS_FOLDER - + SdkConstants.FN_ADB; - } - - private void display(String format, Object...args) { - mMonitor.log(format, args); - } - - private void displayError(String format, Object...args) { - mMonitor.logError(format, args); - } - - /** - * Starts the adb host side server. - * @return true if success - */ - public synchronized boolean startAdb() { - if (mAdbOsLocation == null) { - displayError("Error: missing path to ADB."); //$NON-NLS-1$ - return false; - } - - Process proc; - int status = -1; - - try { - ProcessBuilder processBuilder = new ProcessBuilder( - mAdbOsLocation, - "start-server"); //$NON-NLS-1$ - proc = processBuilder.start(); - status = proc.waitFor(); - - // Implementation note: normally on Windows we need to capture stderr/stdout - // to make sure the process isn't blocked if it's output isn't read. However - // in this case this happens to hang when reading stdout with no proper way - // to properly close the streams. On the other hand the output from start - // server is rather short and not very interesting so we just drop it. - - } catch (IOException ioe) { - displayError("Unable to run 'adb': %1$s.", ioe.getMessage()); //$NON-NLS-1$ - // we'll return false; - } catch (InterruptedException ie) { - displayError("Unable to run 'adb': %1$s.", ie.getMessage()); //$NON-NLS-1$ - // we'll return false; - } - - if (status != 0) { - displayError(String.format( - "Starting ADB server failed (code %d).", //$NON-NLS-1$ - status)); - return false; - } - - display("Starting ADB server succeeded."); //$NON-NLS-1$ - - return true; - } - - /** - * Stops the adb host side server. - * @return true if success - */ - public synchronized boolean stopAdb() { - if (mAdbOsLocation == null) { - displayError("Error: missing path to 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(); - - // See comment in startAdb about not needing/wanting to capture stderr/stdout. - } - catch (IOException ioe) { - // we'll return false; - } - catch (InterruptedException ie) { - // we'll return false; - } - - // adb kill-server returns: - // 0 if adb was running and was correctly killed. - // 1 if adb wasn't running and thus wasn't killed. - // This error case is not worth reporting. - - if (status != 0 && status != 1) { - displayError(String.format( - "Stopping ADB server failed (code %d).", //$NON-NLS-1$ - status)); - return false; - } - - display("Stopping ADB server succeeded."); //$NON-NLS-1$ - return true; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/AddonsListFetcher.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/AddonsListFetcher.java deleted file mode 100644 index 816c7f0e..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/AddonsListFetcher.java +++ /dev/null @@ -1,614 +0,0 @@ -/* - * Copyright (C) 2010 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.sdklib.internal.repository; - -import com.android.annotations.VisibleForTesting; -import com.android.annotations.VisibleForTesting.Visibility; -import com.android.io.NonClosingInputStream; -import com.android.io.NonClosingInputStream.CloseBehavior; -import com.android.sdklib.repository.SdkAddonsListConstants; -import com.android.sdklib.repository.SdkRepoConstants; - -import org.w3c.dom.Document; -import org.w3c.dom.NamedNodeMap; -import org.w3c.dom.Node; -import org.xml.sax.ErrorHandler; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; -import org.xml.sax.SAXParseException; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import javax.net.ssl.SSLKeyException; -import javax.xml.XMLConstants; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.transform.stream.StreamSource; -import javax.xml.validation.Schema; -import javax.xml.validation.SchemaFactory; -import javax.xml.validation.Validator; - -/** - * Fetches and loads an sdk-addons-list XML. - *

- * Such an XML contains a simple list of add-ons site that are to be loaded by default by the - * SDK Manager.
- * The XML must conform to the sdk-addons-list-N.xsd.
- * Constants used in the XML are defined in {@link SdkAddonsListConstants}. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class AddonsListFetcher { - - public enum SiteType { - ADDON_SITE, - SYS_IMG_SITE - } - - /** - * An immutable structure representing an add-on site. - */ - public static class Site { - private final String mUrl; - private final String mUiName; - private final SiteType mType; - - private Site(String url, String uiName, SiteType type) { - mType = type; - mUrl = url.trim(); - mUiName = uiName; - } - - public String getUrl() { - return mUrl; - } - - public String getUiName() { - return mUiName; - } - - public SiteType getType() { - return mType; - } - - /** Returns a debug string representation of this object. Not for user display. */ - @Override - public String toString() { - return String.format("<%1$s URL='%2$s' Name='%3$s'>", //$NON-NLS-1$ - mType, mUrl, mUiName); - } - } - - /** - * Fetches the addons list from the given URL. - * - * @param url The URL of an XML file resource that conforms to the latest sdk-addons-list-N.xsd. - * For the default operation, use {@link SdkAddonsListConstants#URL_ADDON_LIST}. - * Cannot be null. - * @param cache The {@link DownloadCache} instance to use. Cannot be null. - * @param monitor A monitor to report errors. Cannot be null. - * @return An array of {@link Site} on success (possibly empty), or null on error. - */ - public Site[] fetch(String url, DownloadCache cache, ITaskMonitor monitor) { - - url = url == null ? "" : url.trim(); - - monitor.setProgressMax(6); - monitor.setDescription("Fetching %1$s", url); - monitor.incProgress(1); - - Exception[] exception = new Exception[] { null }; - Boolean[] validatorFound = new Boolean[] { Boolean.FALSE }; - String[] validationError = new String[] { null }; - Document validatedDoc = null; - String validatedUri = null; - - String[] defaultNames = new String[SdkAddonsListConstants.NS_LATEST_VERSION]; - for (int version = SdkAddonsListConstants.NS_LATEST_VERSION, i = 0; - version >= 1; - version--, i++) { - defaultNames[i] = SdkAddonsListConstants.getDefaultName(version); - } - - InputStream xml = fetchXmlUrl(url, cache, monitor.createSubMonitor(1), exception); - if (xml != null) { - int version = getXmlSchemaVersion(xml); - if (version == 0) { - closeStream(xml); - xml = null; - } - } - - String baseUrl = url; - if (!baseUrl.endsWith("/")) { //$NON-NLS-1$ - int pos = baseUrl.lastIndexOf('/'); - if (pos > 0) { - baseUrl = baseUrl.substring(0, pos + 1); - } - } - - // If we can't find the latest version, try earlier schema versions. - if (xml == null && defaultNames.length > 0) { - ITaskMonitor subMonitor = monitor.createSubMonitor(1); - subMonitor.setProgressMax(defaultNames.length); - - for (String name : defaultNames) { - String newUrl = baseUrl + name; - if (newUrl.equals(url)) { - continue; - } - xml = fetchXmlUrl(newUrl, cache, subMonitor.createSubMonitor(1), exception); - if (xml != null) { - int version = getXmlSchemaVersion(xml); - if (version == 0) { - closeStream(xml); - xml = null; - } else { - url = newUrl; - subMonitor.incProgress( - subMonitor.getProgressMax() - subMonitor.getProgress()); - break; - } - } - } - } else { - monitor.incProgress(1); - } - - if (xml != null) { - monitor.setDescription("Validate XML"); - - // Explore the XML to find the potential XML schema version - int version = getXmlSchemaVersion(xml); - - if (version >= 1 && version <= SdkAddonsListConstants.NS_LATEST_VERSION) { - // This should be a version we can handle. Try to validate it - // and report any error as invalid XML syntax, - - String uri = validateXml(xml, url, version, validationError, validatorFound); - if (uri != null) { - // Validation was successful - validatedDoc = getDocument(xml, monitor); - validatedUri = uri; - - } - } else if (version > SdkAddonsListConstants.NS_LATEST_VERSION) { - // The schema used is more recent than what is supported by this tool. - // We don't have an upgrade-path support yet, so simply ignore the document. - closeStream(xml); - return null; - } - } - - // If any exception was handled during the URL fetch, display it now. - if (exception[0] != null) { - String reason = null; - if (exception[0] instanceof FileNotFoundException) { - // FNF has no useful getMessage, so we need to special handle it. - reason = "File not found"; - } else if (exception[0] instanceof UnknownHostException && - exception[0].getMessage() != null) { - // This has no useful getMessage yet could really use one - reason = String.format("Unknown Host %1$s", exception[0].getMessage()); - } else if (exception[0] instanceof SSLKeyException) { - // That's a common error and we have a pref for it. - reason = "HTTPS SSL error. You might want to force download through HTTP in the settings."; - } else if (exception[0].getMessage() != null) { - reason = exception[0].getMessage(); - } else { - // We don't know what's wrong. Let's give the exception class at least. - reason = String.format("Unknown (%1$s)", exception[0].getClass().getName()); - } - - monitor.logError("Failed to fetch URL %1$s, reason: %2$s", url, reason); - } - - if (validationError[0] != null) { - monitor.logError("%s", validationError[0]); //$NON-NLS-1$ - } - - // Stop here if we failed to validate the XML. We don't want to load it. - if (validatedDoc == null) { - closeStream(xml); - return null; - } - - monitor.incProgress(1); - - Site[] result = null; - - if (xml != null) { - monitor.setDescription("Parse XML"); - monitor.incProgress(1); - result = parseAddonsList(validatedDoc, validatedUri, baseUrl, monitor); - } - - // done - monitor.incProgress(1); - - closeStream(xml); - return result; - } - - /** - * Fetches the document at the given URL and returns it as a stream. Returns - * null if anything wrong happens. - * - * @param urlString The URL to load, as a string. - * @param monitor {@link ITaskMonitor} related to this URL. - * @param outException If non null, where to store any exception that - * happens during the fetch. - * @see UrlOpener UrlOpener, which handles all URL logic. - */ - private InputStream fetchXmlUrl(String urlString, - DownloadCache cache, - ITaskMonitor monitor, - Exception[] outException) { - try { - InputStream xml = cache.openCachedUrl(urlString, monitor); - if (xml != null) { - xml.mark(500000); - xml = new NonClosingInputStream(xml); - ((NonClosingInputStream) xml).setCloseBehavior(CloseBehavior.RESET); - } - return xml; - } catch (Exception e) { - if (outException != null) { - outException[0] = e; - } - } - - return null; - } - - /** - * Closes the stream, ignore any exception from InputStream.close(). - * If the stream is a NonClosingInputStream, sets it to CloseBehavior.CLOSE first. - */ - private void closeStream(InputStream is) { - if (is != null) { - if (is instanceof NonClosingInputStream) { - ((NonClosingInputStream) is).setCloseBehavior(CloseBehavior.CLOSE); - } - try { - is.close(); - } catch (IOException ignore) {} - } - } - - /** - * Manually parses the root element of the XML to extract the schema version - * at the end of the xmlns:sdk="http://schemas.android.com/sdk/android/addons-list/$N" - * declaration. - * - * @return 1..{@link SdkAddonsListConstants#NS_LATEST_VERSION} for a valid schema version - * or 0 if no schema could be found. - */ - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected int getXmlSchemaVersion(InputStream xml) { - if (xml == null) { - return 0; - } - - // Get an XML document - Document doc = null; - try { - assert xml.markSupported(); - xml.reset(); - - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setIgnoringComments(false); - factory.setValidating(false); - - // Parse the old document using a non namespace aware builder - factory.setNamespaceAware(false); - DocumentBuilder builder = factory.newDocumentBuilder(); - - // We don't want the default handler which prints errors to stderr. - builder.setErrorHandler(new ErrorHandler() { - @Override - public void warning(SAXParseException e) throws SAXException { - // pass - } - @Override - public void fatalError(SAXParseException e) throws SAXException { - throw e; - } - @Override - public void error(SAXParseException e) throws SAXException { - throw e; - } - }); - - doc = builder.parse(xml); - - // Prepare a new document using a namespace aware builder - factory.setNamespaceAware(true); - builder = factory.newDocumentBuilder(); - - } catch (Exception e) { - // Failed to reset XML stream - // Failed to get builder factor - // Failed to create XML document builder - // Failed to parse XML document - // Failed to read XML document - //--For debug--System.err.println("getXmlSchemaVersion exception: " + e.toString()); - } - - if (doc == null) { - return 0; - } - - // Check the root element is an XML with at least the following properties: - // - // - // Note that we don't have namespace support enabled, we just do it manually. - - Pattern nsPattern = Pattern.compile(SdkAddonsListConstants.NS_PATTERN); - - String prefix = null; - for (Node child = doc.getFirstChild(); child != null; child = child.getNextSibling()) { - if (child.getNodeType() == Node.ELEMENT_NODE) { - prefix = null; - String name = child.getNodeName(); - int pos = name.indexOf(':'); - if (pos > 0 && pos < name.length() - 1) { - prefix = name.substring(0, pos); - name = name.substring(pos + 1); - } - if (SdkAddonsListConstants.NODE_SDK_ADDONS_LIST.equals(name)) { - NamedNodeMap attrs = child.getAttributes(); - String xmlns = "xmlns"; //$NON-NLS-1$ - if (prefix != null) { - xmlns += ":" + prefix; //$NON-NLS-1$ - } - Node attr = attrs.getNamedItem(xmlns); - if (attr != null) { - String uri = attr.getNodeValue(); - if (uri != null) { - Matcher m = nsPattern.matcher(uri); - if (m.matches()) { - String version = m.group(1); - try { - return Integer.parseInt(version); - } catch (NumberFormatException e) { - return 0; - } - } - } - } - } - } - } - - return 0; - } - - /** - * Validates this XML against one of the requested SDK Repository schemas. - * If the XML was correctly validated, returns the schema that worked. - * If it doesn't validate, returns null and stores the error in outError[0]. - * If we can't find a validator, returns null and set validatorFound[0] to false. - */ - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected String validateXml(InputStream xml, String url, int version, - String[] outError, Boolean[] validatorFound) { - - if (xml == null) { - return null; - } - - try { - Validator validator = getValidator(version); - - if (validator == null) { - validatorFound[0] = Boolean.FALSE; - outError[0] = String.format( - "XML verification failed for %1$s.\nNo suitable XML Schema Validator could be found in your Java environment. Please consider updating your version of Java.", - url); - return null; - } - - validatorFound[0] = Boolean.TRUE; - - // Reset the stream if it supports that operation. - assert xml.markSupported(); - xml.reset(); - - // Validation throws a bunch of possible Exceptions on failure. - validator.validate(new StreamSource(xml)); - return SdkAddonsListConstants.getSchemaUri(version); - - } catch (SAXParseException e) { - outError[0] = String.format( - "XML verification failed for %1$s.\nLine %2$d:%3$d, Error: %4$s", - url, - e.getLineNumber(), - e.getColumnNumber(), - e.toString()); - - } catch (Exception e) { - outError[0] = String.format( - "XML verification failed for %1$s.\nError: %2$s", - url, - e.toString()); - } - return null; - } - - /** - * Helper method that returns a validator for our XSD, or null if the current Java - * implementation can't process XSD schemas. - * - * @param version The version of the XML Schema. - * See {@link SdkAddonsListConstants#getXsdStream(int)} - */ - private Validator getValidator(int version) throws SAXException { - InputStream xsdStream = SdkAddonsListConstants.getXsdStream(version); - SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); - - if (factory == null) { - return null; - } - - // This may throw a SAX Exception if the schema itself is not a valid XSD - Schema schema = factory.newSchema(new StreamSource(xsdStream)); - - Validator validator = schema == null ? null : schema.newValidator(); - - return validator; - } - - /** - * Takes an XML document as a string as parameter and returns a DOM for it. - * - * On error, returns null and prints a (hopefully) useful message on the monitor. - */ - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected Document getDocument(InputStream xml, ITaskMonitor monitor) { - try { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setIgnoringComments(true); - factory.setNamespaceAware(true); - - DocumentBuilder builder = factory.newDocumentBuilder(); - assert xml.markSupported(); - xml.reset(); - Document doc = builder.parse(new InputSource(xml)); - - return doc; - } catch (ParserConfigurationException e) { - monitor.logError("Failed to create XML document builder"); - - } catch (SAXException e) { - monitor.logError("Failed to parse XML document"); - - } catch (IOException e) { - monitor.logError("Failed to read XML document"); - } - - return null; - } - - /** - * Parse all sites defined in the Addons list XML and returns an array of sites. - * - * @param doc The XML DOM to parse. - * @param nsUri The addons-list schema URI of the document. - * @param baseUrl The base URL of the caller (e.g. where addons-list-N.xml was fetched from.) - * @param monitor A non-null monitor to print to. - */ - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected Site[] parseAddonsList( - Document doc, - String nsUri, - String baseUrl, - ITaskMonitor monitor) { - - String testBaseUrl = System.getenv("SDK_TEST_BASE_URL"); //$NON-NLS-1$ - if (testBaseUrl != null) { - if (testBaseUrl.length() <= 0 || !testBaseUrl.endsWith("/")) { //$NON-NLS-1$ - testBaseUrl = null; - } - } - - Node root = getFirstChild(doc, nsUri, SdkAddonsListConstants.NODE_SDK_ADDONS_LIST); - if (root != null) { - ArrayList sites = new ArrayList(); - - for (Node child = root.getFirstChild(); - child != null; - child = child.getNextSibling()) { - if (child.getNodeType() == Node.ELEMENT_NODE && - nsUri.equals(child.getNamespaceURI())) { - - String elementName = child.getLocalName(); - SiteType type = null; - - if (SdkAddonsListConstants.NODE_SYS_IMG_SITE.equals(elementName)) { - type = SiteType.SYS_IMG_SITE; - - } else if (SdkAddonsListConstants.NODE_ADDON_SITE.equals(elementName)) { - type = SiteType.ADDON_SITE; - } - - // Not an addon-site nor a sys-img-site, don't process this. - if (type == null) { - continue; - } - - Node url = getFirstChild(child, nsUri, SdkAddonsListConstants.NODE_URL); - Node name = getFirstChild(child, nsUri, SdkAddonsListConstants.NODE_NAME); - - if (name != null && url != null) { - String strUrl = url.getTextContent().trim(); - String strName = name.getTextContent().trim(); - - if (testBaseUrl != null && - strUrl.startsWith(SdkRepoConstants.URL_GOOGLE_SDK_SITE)) { - strUrl = testBaseUrl + - strUrl.substring(SdkRepoConstants.URL_GOOGLE_SDK_SITE.length()); - } else if (!strUrl.startsWith("http://") && //$NON-NLS-1$ - !strUrl.startsWith("https://")) { //$NON-NLS-1$ - // This looks like a relative URL, add the fetcher's base URL to it. - strUrl = baseUrl + strUrl; - } - - if (!strUrl.isEmpty() && !strName.isEmpty()) { - sites.add(new Site(strUrl, strName, type)); - } - } - } - } - - return sites.toArray(new Site[sites.size()]); - } - - return null; - } - - /** - * Returns the first child element with the given XML local name. - * If xmlLocalName is null, returns the very first child element. - */ - private Node getFirstChild(Node node, String nsUri, String xmlLocalName) { - - for(Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) { - if (child.getNodeType() == Node.ELEMENT_NODE && - nsUri.equals(child.getNamespaceURI())) { - if (xmlLocalName == null || child.getLocalName().equals(xmlLocalName)) { - return child; - } - } - } - - return null; - } - - -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/CanceledByUserException.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/CanceledByUserException.java deleted file mode 100644 index 64ed8b12..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/CanceledByUserException.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (C) 2011 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.sdklib.internal.repository; - -/** - * Exception thrown by {@link DownloadCache} and {@link UrlOpener} when a user - * cancels an HTTP Basic authentication or NTML authentication dialog. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class CanceledByUserException extends Exception { - private static final long serialVersionUID = -7669346110926032403L; - - public CanceledByUserException(String message) { - super(message); - } -} - diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/DownloadCache.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/DownloadCache.java deleted file mode 100644 index 274c64ce..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/DownloadCache.java +++ /dev/null @@ -1,848 +0,0 @@ -/* - * Copyright (C) 2012 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.sdklib.internal.repository; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.annotations.VisibleForTesting; -import com.android.annotations.VisibleForTesting.Visibility; -import com.android.prefs.AndroidLocation; -import com.android.prefs.AndroidLocation.AndroidLocationException; -import com.android.sdklib.io.LegacyFileOp; -import com.android.sdklib.io.IFileOp; -import com.android.utils.Pair; - -import org.apache.http.Header; -import org.apache.http.HttpHeaders; -import org.apache.http.HttpResponse; -import org.apache.http.HttpStatus; -import org.apache.http.message.BasicHeader; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; -import java.util.Properties; -import java.util.concurrent.atomic.AtomicInteger; - - -/** - * A simple cache for the XML resources handled by the SDK Manager. - *

- * Callers should use {@link #openDirectUrl} to download "large files" - * that should not be cached (like actual installation packages which are several MBs big) - * and call {@link #openCachedUrl(String, ITaskMonitor)} to download small XML files. - *

- * The cache can work in 3 different strategies (direct is a pass-through, fresh-cache is the - * default and tries to update resources if they are older than 10 minutes by respecting - * either ETag or Last-Modified, and finally server-cache is a strategy to always serve - * cached entries if present.) - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class DownloadCache { - - /* - * HTTP/1.1 references: - * - Possible headers: - * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html - * - Rules about conditional requests: - * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4 - * - Error codes: - * http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1.1 - */ - - private static final boolean DEBUG = System.getenv("SDKMAN_DEBUG_CACHE") != null; //$NON-NLS-1$ - - /** Key for the Status-Code in the info properties. */ - private static final String KEY_STATUS_CODE = "Status-Code"; //$NON-NLS-1$ - /** Key for the URL in the info properties. */ - private static final String KEY_URL = "URL"; //$NON-NLS-1$ - - /** Prefix of binary files stored in the {@link SdkConstants#FD_CACHE} directory. */ - private static final String BIN_FILE_PREFIX = "sdkbin"; //$NON-NLS-1$ - /** Prefix of meta info files stored in the {@link SdkConstants#FD_CACHE} directory. */ - private static final String INFO_FILE_PREFIX = "sdkinf"; //$NON-NLS-1$ - /* Revision suffixed to the prefix. */ - private static final String REV_FILE_PREFIX = "-1_"; //$NON-NLS-1$ - - /** - * Minimum time before we consider a cached entry is potentially stale. - * Expressed in milliseconds. - *

- * When using the {@link Strategy#FRESH_CACHE}, the cache will not try to refresh - * a cached file if it's has been saved more recently than this time. - * When using the direct mode or the serve mode, the cache either doesn't serve - * cached files or always serves caches files so this expiration delay is not used. - *

- * Default is 10 minutes. - *

- * TODO: change for a dynamic preference later. - */ - private static final long MIN_TIME_EXPIRED_MS = 10*60*1000; - /** - * Maximum time before we consider a cache entry to be stale. - * Expressed in milliseconds. - *

- * When using the {@link Strategy#FRESH_CACHE}, entries that have no ETag - * or Last-Modified will be refreshed if their file timestamp is older than - * this value. - *

- * Default is 4 hours. - *

- * TODO: change for a dynamic preference later. - */ - private static final long MAX_TIME_EXPIRED_MS = 4*60*60*1000; - - /** - * The maximum file size we'll cache for "small" files. - * 640KB is more than enough and is already a stretch since these are read in memory. - * (The actual typical size of the files handled here is in the 4-64KB range.) - */ - private static final int MAX_SMALL_FILE_SIZE = 640 * 1024; - - /** - * HTTP Headers that are saved in an info file. - * For HTTP/1.1 header names, see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html - */ - private static final String[] INFO_HTTP_HEADERS = { - HttpHeaders.LAST_MODIFIED, - HttpHeaders.ETAG, - HttpHeaders.CONTENT_LENGTH, - HttpHeaders.DATE - }; - - private final IFileOp mFileOp; - private final File mCacheRoot; - private final Strategy mStrategy; - - public enum Strategy { - /** - * Exclusively serves data from the cache. If files are available in the - * cache, serve them as is (without trying to refresh them). If files are - * not available, they are not fetched at all. - */ - ONLY_CACHE, - /** - * If the files are available in the cache, serve them as-is, otherwise - * download them and return the cached version. No expiration or refresh - * is attempted if a file is in the cache. - */ - SERVE_CACHE, - /** - * If the files are available in the cache, check if there's an update - * (either using an e-tag check or comparing to the default time expiration). - * If files have expired or are not in the cache then download them and return - * the cached version. - */ - FRESH_CACHE, - /** - * Disables caching. URLs are always downloaded and returned directly. - * Downloaded streams aren't cached locally. - */ - DIRECT - } - - /** Creates a default instance of the URL cache */ - public DownloadCache(@NonNull Strategy strategy) { - this(new LegacyFileOp(), strategy); - } - - /** Creates a default instance of the URL cache */ - public DownloadCache(@NonNull IFileOp LegacyFileOp, @NonNull Strategy strategy) { - mFileOp = LegacyFileOp; - mCacheRoot = initCacheRoot(); - - // If this is defined in the environment, never use the cache. Useful for testing. - if (System.getenv("SDKMAN_DISABLE_CACHE") != null) { //$NON-NLS-1$ - strategy = Strategy.DIRECT; - } - - mStrategy = mCacheRoot == null ? Strategy.DIRECT : strategy; - } - - @NonNull - public Strategy getStrategy() { - return mStrategy; - } - - @Nullable - public File getCacheRoot() { - return mCacheRoot; - } - - /** - * Computes the size of the cached files. - * - * @return The sum of the byte size of the cached files. - */ - public long getCurrentSize() { - long size = 0; - - if (mCacheRoot != null) { - File[] files = mFileOp.listFiles(mCacheRoot); - for (File f : files) { - if (mFileOp.isFile(f)) { - String name = f.getName(); - if (name.startsWith(BIN_FILE_PREFIX) || name.startsWith(INFO_FILE_PREFIX)) { - size += f.length(); - } - } - } - } - - return size; - } - - /** - * Removes all cached files from the cache directory. - */ - public void clearCache() { - if (mCacheRoot != null) { - File[] files = mFileOp.listFiles(mCacheRoot); - for (File f : files) { - if (mFileOp.isFile(f)) { - String name = f.getName(); - if (name.startsWith(BIN_FILE_PREFIX) || name.startsWith(INFO_FILE_PREFIX)) { - mFileOp.delete(f); - } - } - } - } - } - - /** - * Removes all obsolete cached files from the cache directory - * that do not match the latest revision. - */ - public void clearOldCache() { - String prefix1 = BIN_FILE_PREFIX + REV_FILE_PREFIX; - String prefix2 = INFO_FILE_PREFIX + REV_FILE_PREFIX; - if (mCacheRoot != null) { - File[] files = mFileOp.listFiles(mCacheRoot); - for (File f : files) { - if (mFileOp.isFile(f)) { - String name = f.getName(); - if (name.startsWith(BIN_FILE_PREFIX) || - name.startsWith(INFO_FILE_PREFIX)) { - if (!name.startsWith(prefix1) && !name.startsWith(prefix2)) { - mFileOp.delete(f); - } - } - } - } - } - } - - /** - * Returns the directory to be used as a cache. - * Creates it if necessary. - * Makes it possible to disable or override the cache location in unit tests. - * - * @return An existing directory to use as a cache root dir, - * or null in case of error in which case the cache will be disabled. - */ - @VisibleForTesting(visibility=Visibility.PRIVATE) - @Nullable - protected File initCacheRoot() { - try { - File root = new File(AndroidLocation.getFolder()); - root = new File(root, SdkConstants.FD_CACHE); - if (!mFileOp.exists(root)) { - mFileOp.mkdirs(root); - } - return root; - } catch (AndroidLocationException e) { - // No root? Disable the cache. - return null; - } - } - - /** - * Calls {@link UrlOpener#openUrl(String, boolean, ITaskMonitor, Header[])} - * to actually perform a download. - *

- * Isolated so that it can be overridden by unit tests. - */ - @VisibleForTesting(visibility=Visibility.PRIVATE) - @NonNull - protected Pair openUrl( - @NonNull String url, - boolean needsMarkResetSupport, - @NonNull ITaskMonitor monitor, - @Nullable Header[] headers) throws IOException, CanceledByUserException { - return UrlOpener.openUrl(url, needsMarkResetSupport, monitor, headers); - } - - - /** - * Does a direct download of the given URL using {@link UrlOpener}. - * This does not check the download cache and does not attempt to cache the file. - * Instead the HttpClient library returns a progressive download stream. - *

- * For details on realm authentication and user/password handling, - * check the underlying {@link UrlOpener#openUrl(String, boolean, ITaskMonitor, Header[])} - * documentation. - *

- * The resulting input stream may not support mark/reset. - * - * @param urlString the URL string to be opened. - * @param headers An optional set of headers to pass when requesting the resource. Can be null. - * @param monitor {@link ITaskMonitor} which is related to this URL - * fetching. - * @return Returns a pair with a {@link InputStream} and an {@link HttpResponse}. - * The pair is never null. - * The input stream can be null in case of error, although in general the - * method will probably throw an exception instead. - * The caller should look at the response code's status and only accept the - * input stream if it's the desired code (e.g. 200 or 206). - * @throws IOException Exception thrown when there are problems retrieving - * the URL or its content. - * @throws CanceledByUserException Exception thrown if the user cancels the - * authentication dialog. - */ - @NonNull - public Pair openDirectUrl( - @NonNull String urlString, - @Nullable Header[] headers, - @NonNull ITaskMonitor monitor) - throws IOException, CanceledByUserException { - if (DEBUG) { - System.out.println(String.format("%s : Direct download", urlString)); //$NON-NLS-1$ - } - return openUrl( - urlString, - false /*needsMarkResetSupport*/, - monitor, - headers); - } - - /** - * This is a simplified convenience method that calls - * {@link #openDirectUrl(String, Header[], ITaskMonitor)} - * without passing any specific HTTP headers and returns the resulting input stream - * and the HTTP status code. - * See the original method's description for details on its behavior. - *

- * {@link #openDirectUrl(String, Header[], ITaskMonitor)} can accept customized - * HTTP headers to send with the requests and also returns the full HTTP - * response -- status line with code and protocol and all headers. - *

- * The resulting input stream may not support mark/reset. - * - * @param urlString the URL string to be opened. - * @param monitor {@link ITaskMonitor} which is related to this URL - * fetching. - * @return Returns a pair with a {@link InputStream} and an HTTP status code. - * The pair is never null. - * The input stream can be null in case of error, although in general the - * method will probably throw an exception instead. - * The caller should look at the response code's status and only accept the - * input stream if it's the desired code (e.g. 200 or 206). - * @throws IOException Exception thrown when there are problems retrieving - * the URL or its content. - * @throws CanceledByUserException Exception thrown if the user cancels the - * authentication dialog. - * @see #openDirectUrl(String, Header[], ITaskMonitor) - */ - @NonNull - public Pair openDirectUrl( - @NonNull String urlString, - @NonNull ITaskMonitor monitor) - throws IOException, CanceledByUserException { - if (DEBUG) { - System.out.println(String.format("%s : Direct download", urlString)); //$NON-NLS-1$ - } - Pair result = openUrl( - urlString, - false /*needsMarkResetSupport*/, - monitor, - null /*headers*/); - return Pair.of(result.getFirst(), result.getSecond().getStatusLine().getStatusCode()); - } - - /** - * Downloads a small file, typically XML manifests. - * The current {@link Strategy} governs whether the file is served as-is - * from the cache, potentially updated first or directly downloaded. - *

- * For large downloads (e.g. installable archives) please do not invoke the - * cache and instead use the {@link #openDirectUrl} method. - *

- * For details on realm authentication and user/password handling, - * check the underlying {@link UrlOpener#openUrl(String, boolean, ITaskMonitor, Header[])} - * documentation. - * - * @param urlString the URL string to be opened. - * @param monitor {@link ITaskMonitor} which is related to this URL - * fetching. - * @return Returns an {@link InputStream} holding the URL content. - * Returns null if there's no content (e.g. resource not found.) - * Returns null if the document is not cached and strategy is {@link Strategy#ONLY_CACHE}. - * @throws IOException Exception thrown when there are problems retrieving - * the URL or its content. - * @throws CanceledByUserException Exception thrown if the user cancels the - * authentication dialog. - */ - @NonNull - public InputStream openCachedUrl(@NonNull String urlString, @NonNull ITaskMonitor monitor) - throws IOException, CanceledByUserException { - // Don't cache in direct mode. - if (mStrategy == Strategy.DIRECT) { - Pair result = openUrl( - urlString, - true /*needsMarkResetSupport*/, - monitor, - null /*headers*/); - return result.getFirst(); - } - - File cached = new File(mCacheRoot, getCacheFilename(urlString)); - File info = new File(mCacheRoot, getInfoFilename(cached.getName())); - - boolean useCached = mFileOp.exists(cached); - - if (useCached && mStrategy == Strategy.FRESH_CACHE) { - // Check whether the file should be served from the cache or - // refreshed first. - - long cacheModifiedMs = mFileOp.lastModified(cached); /* last mod time in epoch/millis */ - boolean checkCache = true; - - Properties props = readInfo(info); - if (props == null) { - // No properties, no chocolate for you. - useCached = false; - } else { - long minExpiration = System.currentTimeMillis() - MIN_TIME_EXPIRED_MS; - checkCache = cacheModifiedMs < minExpiration; - - if (!checkCache && DEBUG) { - System.out.println(String.format( - "%s : Too fresh [%,d ms], not checking yet.", //$NON-NLS-1$ - urlString, cacheModifiedMs - minExpiration)); - } - } - - if (useCached && checkCache) { - assert props != null; - - // Right now we only support 200 codes and will requery all 404s. - String code = props.getProperty(KEY_STATUS_CODE, ""); //$NON-NLS-1$ - useCached = Integer.toString(HttpStatus.SC_OK).equals(code); - - if (!useCached && DEBUG) { - System.out.println(String.format( - "%s : cache disabled by code %s", //$NON-NLS-1$ - urlString, code)); - } - - if (useCached) { - // Do we have a valid Content-Length? If so, it should match the file size. - try { - long length = Long.parseLong(props.getProperty(HttpHeaders.CONTENT_LENGTH, - "-1")); //$NON-NLS-1$ - if (length >= 0) { - useCached = length == mFileOp.length(cached); - - if (!useCached && DEBUG) { - System.out.println(String.format( - "%s : cache disabled by length mismatch %d, expected %d", //$NON-NLS-1$ - urlString, length, cached.length())); - } - } - } catch (NumberFormatException ignore) {} - } - - if (useCached) { - // Do we have an ETag and/or a Last-Modified? - String etag = props.getProperty(HttpHeaders.ETAG); - String lastMod = props.getProperty(HttpHeaders.LAST_MODIFIED); - - if (etag != null || lastMod != null) { - // Details on how to use them is defined at - // http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4 - // Bottom line: - // - if there's an ETag, it should be used first with an - // If-None-Match header. That's a strong comparison for HTTP/1.1 servers. - // - otherwise use a Last-Modified if an If-Modified-Since header exists. - // In this case, we place both and the rules indicates a spec-abiding - // server should strongly match ETag and weakly the Modified-Since. - - // TODO there are some servers out there which report ETag/Last-Mod - // yet don't honor them when presented with a precondition. In this - // case we should identify it in the reply and invalidate ETag support - // for these servers and instead fallback on the pure-timeout case below. - - AtomicInteger statusCode = new AtomicInteger(0); - InputStream is = null; - List

headers = new ArrayList
(2); - - if (etag != null) { - headers.add(new BasicHeader(HttpHeaders.IF_NONE_MATCH, etag)); - } - - if (lastMod != null) { - headers.add(new BasicHeader(HttpHeaders.IF_MODIFIED_SINCE, lastMod)); - } - - if (!headers.isEmpty()) { - is = downloadAndCache(urlString, monitor, cached, info, - headers.toArray(new Header[headers.size()]), - statusCode); - } - - if (is != null && statusCode.get() == HttpStatus.SC_OK) { - // The resource was modified, the server said there was something - // new, which has been cached. We can return that to the caller. - return is; - } - - // If we get here, we should have is == null and code - // could be: - // - 304 for not-modified -- same resource, still available, in - // which case we'll use the cached one. - // - 404 -- resource doesn't exist anymore in which case there's - // no point in retrying. - // - For any other code, just retry a download. - - if (is != null) { - try { - is.close(); - } catch (Exception ignore) {} - is = null; - } - - if (statusCode.get() == HttpStatus.SC_NOT_MODIFIED) { - // Cached file was not modified. - // Change its timestamp for the next MIN_TIME_EXPIRED_MS check. - cached.setLastModified(System.currentTimeMillis()); - - // At this point useCached==true so we'll return - // the cached file below. - } else { - // URL fetch returned something other than 200 or 304. - // For 404, we're done, no need to check the server again. - // For all other codes, we'll retry a download below. - useCached = false; - if (statusCode.get() == HttpStatus.SC_NOT_FOUND) { - return null; - } - } - } else { - // If we don't have an Etag nor Last-Modified, let's use a - // basic file timestamp and compare to a 1 hour threshold. - - long maxExpiration = System.currentTimeMillis() - MAX_TIME_EXPIRED_MS; - useCached = cacheModifiedMs >= maxExpiration; - - if (!useCached && DEBUG) { - System.out.println(String.format( - "[%1$s] cache disabled by timestamp %2$tD %2$tT < %3$tD %3$tT", //$NON-NLS-1$ - urlString, cacheModifiedMs, maxExpiration)); - } - } - } - } - } - - if (useCached) { - // The caller needs an InputStream that supports the reset() operation. - // The default FileInputStream does not, so load the file into a byte - // array and return that. - try { - InputStream is = readCachedFile(cached); - if (is != null) { - if (DEBUG) { - System.out.println(String.format("%s : Use cached file", urlString)); //$NON-NLS-1$ - } - - return is; - } - } catch (IOException ignore) {} - } - - if (!useCached && mStrategy == Strategy.ONLY_CACHE) { - // We don't have a document to serve from the cache. - if (DEBUG) { - System.out.println(String.format("%s : file not in cache", urlString)); //$NON-NLS-1$ - } - return null; - } - - // If we're not using the cache, try to remove the cache and download again. - try { - mFileOp.delete(cached); - mFileOp.delete(info); - } catch (SecurityException ignore) {} - - return downloadAndCache(urlString, monitor, cached, info, - null /*headers*/, null /*statusCode*/); - } - - - - // -------------- - - @Nullable - private InputStream readCachedFile(@NonNull File cached) throws IOException { - InputStream is = null; - - int inc = 65536; - int curr = 0; - long len = cached.length(); - assert len < Integer.MAX_VALUE; - if (len >= MAX_SMALL_FILE_SIZE) { - // This is supposed to cache small files, not 2+ GB files. - return null; - } - byte[] result = new byte[(int) (len > 0 ? len : inc)]; - - try { - is = mFileOp.newFileInputStream(cached); - - int n; - while ((n = is.read(result, curr, result.length - curr)) != -1) { - curr += n; - if (curr == result.length) { - byte[] temp = new byte[curr + inc]; - System.arraycopy(result, 0, temp, 0, curr); - result = temp; - } - } - - return new ByteArrayInputStream(result, 0, curr); - - } finally { - if (is != null) { - try { - is.close(); - } catch (IOException ignore) {} - } - } - } - - /** - * Download, cache and return as an in-memory byte stream. - * The download is only done if the server returns 200/OK. - * On success, store an info file next to the download with - * a few headers. - *

- * This method deletes the cached file and the info file ONLY if it - * attempted a download and it failed to complete. It doesn't erase - * anything if there's no download because the server returned a 404 - * or 304 or similar. - * - * @return An in-memory byte buffer input stream for the downloaded - * and locally cached file, or null if nothing was downloaded - * (including if it was a 304 Not-Modified status code.) - */ - @Nullable - private InputStream downloadAndCache( - @NonNull String urlString, - @NonNull ITaskMonitor monitor, - @NonNull File cached, - @NonNull File info, - @Nullable Header[] headers, - @Nullable AtomicInteger outStatusCode) - throws FileNotFoundException, IOException, CanceledByUserException { - InputStream is = null; - OutputStream os = null; - - int inc = 65536; - int curr = 0; - byte[] result = new byte[inc]; - - try { - Pair r = - openUrl(urlString, true /*needsMarkResetSupport*/, monitor, headers); - - is = r.getFirst(); - HttpResponse response = r.getSecond(); - - if (DEBUG) { - System.out.println(String.format("%s : fetch: %s => %s", //$NON-NLS-1$ - urlString, - headers == null ? "" : Arrays.toString(headers), //$NON-NLS-1$ - response.getStatusLine())); - } - - int code = response.getStatusLine().getStatusCode(); - - if (outStatusCode != null) { - outStatusCode.set(code); - } - - if (code != HttpStatus.SC_OK) { - // Only a 200 response code makes sense here. - // Even the other 20x codes should not apply, e.g. no content or partial - // content are not statuses we want to handle and should never happen. - // (see http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1.1 for list) - return null; - } - - os = mFileOp.newFileOutputStream(cached); - - int n; - while ((n = is.read(result, curr, result.length - curr)) != -1) { - if (os != null && n > 0) { - os.write(result, curr, n); - } - - curr += n; - - if (os != null && curr > MAX_SMALL_FILE_SIZE) { - // If the file size exceeds our "small file size" threshold, - // stop caching. We don't want to fill the disk. - try { - os.close(); - } catch (IOException ignore) {} - try { - cached.delete(); - info.delete(); - } catch (SecurityException ignore) {} - os = null; - } - if (curr == result.length) { - byte[] temp = new byte[curr + inc]; - System.arraycopy(result, 0, temp, 0, curr); - result = temp; - } - } - - // Close the output stream, signaling it was stored properly. - if (os != null) { - try { - os.close(); - os = null; - - saveInfo(urlString, response, info); - } catch (IOException ignore) {} - } - - return new ByteArrayInputStream(result, 0, curr); - - } finally { - if (is != null) { - try { - is.close(); - } catch (IOException ignore) {} - } - if (os != null) { - try { - os.close(); - } catch (IOException ignore) {} - // If we get here with the output stream not null, it means there - // was an issue and we don't want to keep that file. We'll try to - // delete it. - try { - mFileOp.delete(cached); - mFileOp.delete(info); - } catch (SecurityException ignore) {} - } - } - } - - /** - * Saves part of the HTTP Response to the info file. - */ - private void saveInfo( - @NonNull String urlString, - @NonNull HttpResponse response, - @NonNull File info) throws IOException { - Properties props = new Properties(); - - // we don't need the status code & URL right now. - // Save it in case we want to have it later (e.g. to differentiate 200 and 404.) - props.setProperty(KEY_URL, urlString); - props.setProperty(KEY_STATUS_CODE, - Integer.toString(response.getStatusLine().getStatusCode())); - - for (String name : INFO_HTTP_HEADERS) { - Header h = response.getFirstHeader(name); - if (h != null) { - props.setProperty(name, h.getValue()); - } - } - - mFileOp.saveProperties(info, props, "## Meta data for SDK Manager cache. Do not modify."); //$NON-NLS-1$ - } - - /** - * Reads the info properties file. - * @return The properties found or null if there's no file or it can't be read. - */ - @Nullable - private Properties readInfo(@NonNull File info) { - if (mFileOp.exists(info)) { - return mFileOp.loadProperties(info); - } - return null; - } - - /** - * Computes the cache filename for the given URL. - * The filename uses the {@link #BIN_FILE_PREFIX}, the full URL string's hashcode and - * a sanitized portion of the URL filename. The returned filename is never - * more than 64 characters to ensure maximum file system compatibility. - * - * @param urlString The download URL. - * @return A leaf filename for the cached download file. - */ - @NonNull - private String getCacheFilename(@NonNull String urlString) { - - int code = 0; - for (int i = 0, j = urlString.length(); i < j; i++) { - code = code * 31 + urlString.charAt(i); - } - String hash = String.format("%08x", code); - - String leaf = urlString.toLowerCase(Locale.US); - if (leaf.length() >= 2) { - int index = urlString.lastIndexOf('/', leaf.length() - 2); - leaf = urlString.substring(index + 1); - } - - leaf = leaf.replaceAll("[^a-z0-9_-]+", "_"); - leaf = leaf.replaceAll("__+", "_"); - - leaf = hash + '-' + leaf; - String prefix = BIN_FILE_PREFIX + REV_FILE_PREFIX; - int n = 64 - prefix.length(); - if (leaf.length() > n) { - leaf = leaf.substring(0, n); - } - - return prefix + leaf; - } - - @NonNull - private String getInfoFilename(@NonNull String cacheFilename) { - return cacheFilename.replaceFirst(BIN_FILE_PREFIX, INFO_FILE_PREFIX); - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/LocalSdkParser.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/LocalSdkParser.java deleted file mode 100644 index 69064eff..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/LocalSdkParser.java +++ /dev/null @@ -1,804 +0,0 @@ -/* - * 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.sdklib.internal.repository; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.io.FileWrapper; -import com.android.sdklib.IAndroidTarget; -import com.android.sdklib.ISystemImage; -import com.android.sdklib.ISystemImage.LocationType; -import com.android.sdklib.SdkManager; -import com.android.sdklib.internal.androidTarget.PlatformTarget; -import com.android.sdklib.internal.project.ProjectProperties; -import com.android.sdklib.internal.repository.packages.AddonPackage; -import com.android.sdklib.internal.repository.packages.BuildToolPackage; -import com.android.sdklib.internal.repository.packages.DocPackage; -import com.android.sdklib.internal.repository.packages.ExtraPackage; -import com.android.sdklib.internal.repository.packages.Package; -import com.android.sdklib.internal.repository.packages.PlatformPackage; -import com.android.sdklib.internal.repository.packages.PlatformToolPackage; -import com.android.sdklib.internal.repository.packages.SamplePackage; -import com.android.sdklib.internal.repository.packages.SourcePackage; -import com.android.sdklib.internal.repository.packages.SystemImagePackage; -import com.android.sdklib.internal.repository.packages.ToolPackage; -import com.android.sdklib.io.LegacyFileOp; -import com.android.sdklib.repository.AddonManifestIniProps; -import com.android.sdklib.repository.descriptors.PkgType; -import com.android.utils.ILogger; -import com.android.utils.Pair; -import com.google.common.collect.Lists; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Properties; - -/** - * Scans a local SDK to find which packages are currently installed. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class LocalSdkParser { - - private Package[] mPackages; - - /** Parse all SDK folders. */ - public static final int PARSE_ALL = PkgType.PKG_ALL_INT; - /** Parse the SDK/tools folder. */ - public static final int PARSE_TOOLS = PkgType.PKG_TOOLS.getIntValue(); - /** Parse the SDK/platform-tools folder */ - public static final int PARSE_PLATFORM_TOOLS = PkgType.PKG_PLATFORM_TOOLS.getIntValue(); - /** Parse the SDK/docs folder. */ - public static final int PARSE_DOCS = PkgType.PKG_DOC.getIntValue(); - /** - * Equivalent to parsing the SDK/platforms folder but does so - * by using the valid targets loaded by the {@link SdkManager}. - * Parsing the platforms also parses the SDK/system-images folder. - */ - public static final int PARSE_PLATFORMS = PkgType.PKG_PLATFORM.getIntValue(); - /** - * Equivalent to parsing the SDK/addons folder but does so - * by using the valid targets loaded by the {@link SdkManager}. - */ - public static final int PARSE_ADDONS = PkgType.PKG_ADDON.getIntValue(); - /** Parse the SDK/samples folder. - * Note: this will not detect samples located in the SDK/extras packages. */ - public static final int PARSE_SAMPLES = PkgType.PKG_SAMPLE.getIntValue(); - /** Parse the SDK/sources folder. */ - public static final int PARSE_SOURCES = PkgType.PKG_SOURCE.getIntValue(); - /** Parse the SDK/extras folder. */ - public static final int PARSE_EXTRAS = PkgType.PKG_EXTRA.getIntValue(); - /** Parse the SDK/build-tools folder. */ - public static final int PARSE_BUILD_TOOLS = PkgType.PKG_BUILD_TOOLS.getIntValue(); - - public LocalSdkParser() { - // pass - } - - /** - * Returns the packages found by the last call to {@link #parseSdk}. - *

- * This returns initially returns null. - * Once the parseSdk() method has been called, this returns a possibly empty but non-null array. - */ - public Package[] getPackages() { - return mPackages; - } - - /** - * Clear the internal packages list. After this call, {@link #getPackages()} will return - * null till {@link #parseSdk} is called. - */ - public void clearPackages() { - mPackages = null; - } - - /** - * Scan the give SDK to find all the packages already installed at this location. - *

- * Store the packages internally. You can use {@link #getPackages()} to retrieve them - * at any time later. - *

- * Equivalent to calling {@code parseSdk(..., PARSE_ALL, ...); } - * - * @param osSdkRoot The path to the SDK folder, typically {@code sdkManager.getLocation()}. - * @param sdkManager An existing SDK manager to list current platforms and addons. - * @param monitor A monitor to track progress. Cannot be null. - * @return The packages found. Can be retrieved later using {@link #getPackages()}. - */ - @NonNull - public Package[] parseSdk( - @NonNull String osSdkRoot, - @NonNull SdkManager sdkManager, - @NonNull ITaskMonitor monitor) { - return parseSdk(osSdkRoot, sdkManager, PARSE_ALL, monitor); - } - - /** - * Scan the give SDK to find all the packages already installed at this location. - *

- * Store the packages internally. You can use {@link #getPackages()} to retrieve them - * at any time later. - * - * @param osSdkRoot The path to the SDK folder, typically {@code sdkManager.getLocation()}. - * @param sdkManager An existing SDK manager to list current platforms and addons. - * @param parseFilter Either {@link #PARSE_ALL} or an ORed combination of the other - * {@code PARSE_} constants to indicate what should be parsed. - * @param monitor A monitor to track progress. Cannot be null. - * @return The packages found. Can be retrieved later using {@link #getPackages()}. - */ - @NonNull - public Package[] parseSdk( - @NonNull String osSdkRoot, - @NonNull SdkManager sdkManager, - int parseFilter, - @NonNull ITaskMonitor monitor) { - ArrayList packages = new ArrayList(); - HashSet visited = new HashSet(); - - monitor.setProgressMax(11); - - File dir = null; - Package pkg = null; - - if ((parseFilter & PARSE_DOCS) != 0) { - dir = new File(osSdkRoot, SdkConstants.FD_DOCS); - pkg = scanDoc(dir, monitor); - if (pkg != null) { - packages.add(pkg); - visited.add(dir); - } - } - monitor.incProgress(1); - - if ((parseFilter & PARSE_TOOLS) != 0) { - dir = new File(osSdkRoot, SdkConstants.FD_TOOLS); - pkg = scanTools(dir, monitor); - if (pkg != null) { - packages.add(pkg); - visited.add(dir); - } - } - monitor.incProgress(1); - - if ((parseFilter & PARSE_PLATFORM_TOOLS) != 0) { - dir = new File(osSdkRoot, SdkConstants.FD_PLATFORM_TOOLS); - pkg = scanPlatformTools(dir, monitor); - if (pkg != null) { - packages.add(pkg); - visited.add(dir); - } - } - monitor.incProgress(1); - - if ((parseFilter & PARSE_BUILD_TOOLS) != 0) { - scanBuildTools(sdkManager, visited, packages, monitor); - } - monitor.incProgress(1); - - // for platforms, add-ons and samples, rely on the SdkManager parser - if ((parseFilter & (PARSE_ADDONS | PARSE_PLATFORMS)) != 0) { - File samplesRoot = new File(osSdkRoot, SdkConstants.FD_SAMPLES); - - for(IAndroidTarget target : sdkManager.getTargets()) { - Properties props = parseProperties(new File(target.getLocation(), - SdkConstants.FN_SOURCE_PROP)); - - try { - pkg = null; - if (target.isPlatform() && (parseFilter & PARSE_PLATFORMS) != 0) { - pkg = PlatformPackage.create(target, props); - - if (samplesRoot.isDirectory()) { - // Get the samples dir for a platform if it is located in the new - // root /samples dir. We purposely ignore "old" samples that are - // located under the platform dir. - File samplesDir = new File(target.getPath(IAndroidTarget.SAMPLES)); - if (samplesDir.exists() && - samplesDir.getParentFile().equals(samplesRoot)) { - Properties samplesProps = parseProperties( - new File(samplesDir, SdkConstants.FN_SOURCE_PROP)); - if (samplesProps != null) { - Package pkg2 = SamplePackage.create(target, samplesProps); - packages.add(pkg2); - } - visited.add(samplesDir); - } - } - } else if ((parseFilter & PARSE_ADDONS) != 0) { - pkg = AddonPackage.create(target, props); - } - - if (pkg != null) { - for (ISystemImage systemImage : target.getSystemImages()) { - if (systemImage.getLocationType() == LocationType.IN_SYSTEM_IMAGE) { - File siDir = systemImage.getLocation(); - if (siDir.isDirectory()) { - Properties siProps = parseProperties( - new File(siDir, SdkConstants.FN_SOURCE_PROP)); - Package pkg2 = new SystemImagePackage( - target.getVersion(), - 0 /*rev*/, // use the one from siProps - systemImage.getAbiType(), - siProps, - siDir.getAbsolutePath()); - packages.add(pkg2); - visited.add(siDir); - } - } - } - } - - } catch (Exception e) { - monitor.error(e, null); - } - - if (pkg != null) { - packages.add(pkg); - visited.add(new File(target.getLocation())); - } - } - } - monitor.incProgress(1); - - if ((parseFilter & PARSE_PLATFORMS) != 0) { - scanMissingSystemImages(sdkManager, visited, packages, monitor); - } - monitor.incProgress(1); - if ((parseFilter & PARSE_ADDONS) != 0) { - scanMissingAddons(sdkManager, visited, packages, monitor); - } - monitor.incProgress(1); - if ((parseFilter & PARSE_SAMPLES) != 0) { - scanMissingSamples(sdkManager, visited, packages, monitor); - } - monitor.incProgress(1); - if ((parseFilter & PARSE_EXTRAS) != 0) { - scanExtras(sdkManager, visited, packages, monitor); - } - monitor.incProgress(1); - if ((parseFilter & PARSE_EXTRAS) != 0) { - scanExtrasDirectory(osSdkRoot, visited, packages, monitor); - } - monitor.incProgress(1); - if ((parseFilter & PARSE_SOURCES) != 0) { - scanSources(sdkManager, visited, packages, monitor); - } - monitor.incProgress(1); - - Collections.sort(packages); - - mPackages = packages.toArray(new Package[packages.size()]); - return mPackages; - } - - /** - * Find any directory in the /extras/vendors/path folders for extra packages. - * This isn't a recursive search. - */ - private void scanExtras(SdkManager sdkManager, - HashSet visited, - ArrayList packages, - ILogger log) { - File root = new File(sdkManager.getLocation(), SdkConstants.FD_EXTRAS); - - for (File vendor : listFilesNonNull(root)) { - if (vendor.isDirectory()) { - scanExtrasDirectory(vendor.getAbsolutePath(), visited, packages, log); - } - } - } - - /** - * Find any other directory in the given "root" directory that hasn't been visited yet - * and assume they contain extra packages. This is not a recursive search. - */ - private void scanExtrasDirectory(String extrasRoot, - HashSet visited, - ArrayList packages, - ILogger log) { - File root = new File(extrasRoot); - - for (File dir : listFilesNonNull(root)) { - if (dir.isDirectory() && !visited.contains(dir)) { - Properties props = parseProperties(new File(dir, SdkConstants.FN_SOURCE_PROP)); - if (props != null) { - try { - Package pkg = ExtraPackage.create( - null, //source - props, //properties - null, //vendor - dir.getName(), //path - 0, //revision - null, //license - null, //description - null, //descUrl - dir.getPath() //archiveOsPath - ); - - packages.add(pkg); - visited.add(dir); - } catch (Exception e) { - log.error(e, null); - } - } - } - } - } - - /** - * Find any other sub-directories under the /samples root that hasn't been visited yet - * and assume they contain sample packages. This is not a recursive search. - *

- * The use case is to find samples dirs under /samples when their target isn't loaded. - */ - private void scanMissingSamples(SdkManager sdkManager, - HashSet visited, - ArrayList packages, - ILogger log) { - File root = new File(sdkManager.getLocation()); - root = new File(root, SdkConstants.FD_SAMPLES); - - for (File dir : listFilesNonNull(root)) { - if (dir.isDirectory() && !visited.contains(dir)) { - Properties props = parseProperties(new File(dir, SdkConstants.FN_SOURCE_PROP)); - if (props != null) { - try { - Package pkg = SamplePackage.create(dir.getAbsolutePath(), props); - packages.add(pkg); - visited.add(dir); - } catch (Exception e) { - log.error(e, null); - } - } - } - } - } - - /** - * The sdk manager only lists valid addons. However here we also want to find "broken" - * addons, i.e. addons that failed to load for some reason. - *

- * Find any other sub-directories under the /add-ons root that hasn't been visited yet - * and assume they contain broken addons. - */ - private void scanMissingAddons(SdkManager sdkManager, - HashSet visited, - ArrayList packages, - ILogger log) { - File addons = new File(new File(sdkManager.getLocation()), SdkConstants.FD_ADDONS); - - for (File dir : listFilesNonNull(addons)) { - if (dir.isDirectory() && !visited.contains(dir)) { - Pair, String> infos = - parseAddonProperties(dir, sdkManager.getTargets(), log); - Properties sourceProps = - parseProperties(new File(dir, SdkConstants.FN_SOURCE_PROP)); - - Map addonProps = infos.getFirst(); - String error = infos.getSecond(); - try { - Package pkg = AddonPackage.createBroken(dir.getAbsolutePath(), - sourceProps, - addonProps, - error); - packages.add(pkg); - visited.add(dir); - } catch (Exception e) { - log.error(e, null); - } - } - } - } - - /** - * Parses the add-on properties and decodes any error that occurs when - * loading an addon. - * - * @param addonDir the location of the addon directory. - * @param targetList The list of Android target that were already loaded - * from the SDK. - * @param log the ILogger object receiving warning/error from the parsing. - * @return A pair with the property map and an error string. Both can be - * null but not at the same time. If a non-null error is present - * then the property map must be ignored. The error should be - * translatable as it might show up in the SdkManager UI. - */ - @Deprecated // Copied from SdkManager.java, dup of LocalAddonPkgInfo.parseAddonProperties. - @NonNull - public static Pair, String> parseAddonProperties( - @NonNull File addonDir, @NonNull IAndroidTarget[] targetList, - @NonNull ILogger log) { - Map propertyMap = null; - String error = null; - - FileWrapper addOnManifest = new FileWrapper(addonDir, - SdkConstants.FN_MANIFEST_INI); - - do { - if (!addOnManifest.isFile()) { - error = String.format("File not found: %1$s", - SdkConstants.FN_MANIFEST_INI); - break; - } - - propertyMap = ProjectProperties.parsePropertyFile(addOnManifest, - log); - if (propertyMap == null) { - error = String.format("Failed to parse properties from %1$s", - SdkConstants.FN_MANIFEST_INI); - break; - } - - // look for some specific values in the map. - // we require name, vendor, and api - String name = propertyMap.get(AddonManifestIniProps.ADDON_NAME); - if (name == null) { - error = String.format("'%1$s' is missing from %2$s.", - AddonManifestIniProps.ADDON_NAME, - SdkConstants.FN_MANIFEST_INI); - break; - } - - String vendor = propertyMap.get(AddonManifestIniProps.ADDON_VENDOR); - if (vendor == null) { - error = String.format("'%1$s' is missing from %2$s.", - AddonManifestIniProps.ADDON_VENDOR, - SdkConstants.FN_MANIFEST_INI); - break; - } - - String api = propertyMap.get(AddonManifestIniProps.ADDON_API); - if (api == null) { - error = String.format("'%1$s' is missing from %2$s.", - AddonManifestIniProps.ADDON_API, - SdkConstants.FN_MANIFEST_INI); - break; - } - - // Look for a platform that has a matching api level or codename. - PlatformTarget baseTarget = null; - for (IAndroidTarget target : targetList) { - if (target.isPlatform() && target.getVersion().equals(api)) { - baseTarget = (PlatformTarget) target; - break; - } - } - - if (baseTarget == null) { - error = String.format( - "Unable to find base platform with API level '%1$s'", - api); - break; - } - - // get the add-on revision - String revision = propertyMap.get(AddonManifestIniProps.ADDON_REVISION); - if (revision == null) { - revision = propertyMap.get(AddonManifestIniProps.ADDON_REVISION_OLD); - } - if (revision != null) { - try { - Integer.parseInt(revision); - } catch (NumberFormatException e) { - // looks like revision does not parse to a number. - error = String.format( - "%1$s is not a valid number in %2$s.", - AddonManifestIniProps.ADDON_REVISION, - SdkConstants.FN_BUILD_PROP); - break; - } - } - - } while (false); - - return Pair.of(propertyMap, error); - } - - - /** - * The sdk manager only lists valid system image via its addons or platform targets. - * However here we also want to find "broken" system images, that is system images - * that are located in the sdk/system-images folder but somehow not loaded properly. - */ - private void scanMissingSystemImages(SdkManager sdkManager, - HashSet visited, - ArrayList packages, - ILogger log) { - File siRoot = new File(sdkManager.getLocation(), SdkConstants.FD_SYSTEM_IMAGES); - - // The system-images folder contains a list of platform folders. - for (File platformDir : listFilesNonNull(siRoot)) { - if (platformDir.isDirectory() && !visited.contains(platformDir)) { - visited.add(platformDir); - - // In the platform directory, we expect a list of abi folders - // or a list of tag/abi folders. Basically parse any folder that has - // a source.prop file within 2 levels. - List propFiles = Lists.newArrayList(); - - for (File dir1 : listFilesNonNull(platformDir)) { - if (dir1.isDirectory() && !visited.contains(dir1)) { - visited.add(dir1); - File prop1 = new File(dir1, SdkConstants.FN_SOURCE_PROP); - if (prop1.isFile()) { - propFiles.add(prop1); - } else { - for (File dir2 : listFilesNonNull(dir1)) { - if (dir2.isDirectory() && !visited.contains(dir2)) { - visited.add(dir2); - File prop2 = new File(dir2, SdkConstants.FN_SOURCE_PROP); - if (prop2.isFile()) { - propFiles.add(prop2); - } - } - } - } - } - } - - for (File propFile : propFiles) { - Properties props = parseProperties(propFile); - try { - Package pkg = SystemImagePackage.createBroken(propFile.getParentFile(), - props); - packages.add(pkg); - } catch (Exception e) { - log.error(e, null); - } - } - } - } - } - - /** - * Scan the sources/folders and register valid as well as broken source packages. - */ - private void scanSources(SdkManager sdkManager, - HashSet visited, - ArrayList packages, - ILogger log) { - File srcRoot = new File(sdkManager.getLocation(), SdkConstants.FD_PKG_SOURCES); - - // The sources folder contains a list of platform folders. - for (File platformDir : listFilesNonNull(srcRoot)) { - if (platformDir.isDirectory() && !visited.contains(platformDir)) { - visited.add(platformDir); - - // Ignore empty directories - File[] srcFiles = platformDir.listFiles(); - if (srcFiles != null && srcFiles.length > 0) { - Properties props = - parseProperties(new File(platformDir, SdkConstants.FN_SOURCE_PROP)); - - try { - Package pkg = SourcePackage.create(platformDir, props); - packages.add(pkg); - } catch (Exception e) { - log.error(e, null); - } - } - } - } - } - - /** - * Try to find a tools package at the given location. - * Returns null if not found. - */ - private Package scanTools(File toolFolder, ILogger log) { - // Can we find some properties? - Properties props = parseProperties(new File(toolFolder, SdkConstants.FN_SOURCE_PROP)); - - // We're not going to check that all tools are present. At the very least - // we should expect to find android and an emulator adapted to the current OS. - boolean hasEmulator = false; - boolean hasAndroid = false; - String android1 = SdkConstants.androidCmdName().replace(".bat", ".exe"); - String android2 = android1.indexOf('.') == -1 ? null : android1.replace(".exe", ".bat"); - for (File file : listFilesNonNull(toolFolder)) { - String name = file.getName(); - if (SdkConstants.FN_EMULATOR.equals(name)) { - hasEmulator = true; - } - if (android1.equals(name) || (android2 != null && android2.equals(name))) { - hasAndroid = true; - } - } - - if (!hasAndroid || !hasEmulator) { - return null; - } - - // Create our package. use the properties if we found any. - try { - Package pkg = ToolPackage.create( - null, //source - props, //properties - 0, //revision - null, //license - "Tools", //description - null, //descUrl - toolFolder.getPath() //archiveOsPath - ); - - return pkg; - } catch (Exception e) { - log.error(e, null); - } - return null; - } - - /** - * Try to find a platform-tools package at the given location. - * Returns null if not found. - */ - private Package scanPlatformTools(File platformToolsFolder, ILogger log) { - // Can we find some properties? - Properties props = parseProperties(new File(platformToolsFolder, - SdkConstants.FN_SOURCE_PROP)); - - // We're not going to check that all tools are present. At the very least - // we should expect to find adb, aidl, aapt and dx (adapted to the current OS). - - if (platformToolsFolder.listFiles() == null) { - // ListFiles is null if the directory doesn't even exist. - // Not going to find anything in there... - return null; - } - - // Create our package. use the properties if we found any. - try { - Package pkg = PlatformToolPackage.create( - null, //source - props, //properties - 0, //revision - null, //license - "Platform Tools", //description - null, //descUrl - platformToolsFolder.getPath() //archiveOsPath - ); - - return pkg; - } catch (Exception e) { - log.error(e, null); - } - return null; - } - - /** - * Scan the build-tool/folders and register valid as well as broken build tool packages. - */ - private void scanBuildTools( - SdkManager sdkManager, - HashSet visited, - ArrayList packages, - ILogger log) { - File buildToolRoot = new File(sdkManager.getLocation(), SdkConstants.FD_BUILD_TOOLS); - - // The build-tool root folder contains a list of revisioned folders. - for (File buildToolDir : listFilesNonNull(buildToolRoot)) { - if (buildToolDir.isDirectory() && !visited.contains(buildToolDir)) { - visited.add(buildToolDir); - - // Ignore empty directories - File[] srcFiles = buildToolDir.listFiles(); - if (srcFiles != null && srcFiles.length > 0) { - Properties props = - parseProperties(new File(buildToolDir, SdkConstants.FN_SOURCE_PROP)); - - try { - Package pkg = BuildToolPackage.create(buildToolDir, props); - packages.add(pkg); - } catch (Exception e) { - log.error(e, null); - } - } - } - } - } - - /** - * Try to find a docs package at the given location. - * Returns null if not found. - */ - private Package scanDoc(File docFolder, ILogger log) { - // Can we find some properties? - Properties props = parseProperties(new File(docFolder, SdkConstants.FN_SOURCE_PROP)); - - // To start with, a doc folder should have an "index.html" to be acceptable. - // We don't actually check the content of the file. - if (new File(docFolder, "index.html").isFile()) { - try { - Package pkg = DocPackage.create( - null, //source - props, //properties - 0, //apiLevel - null, //codename - 0, //revision - null, //license - null, //description - null, //descUrl - docFolder.getPath() //archiveOsPath - ); - - return pkg; - } catch (Exception e) { - log.error(e, null); - } - } - - return null; - } - - /** - * Parses the given file as properties file if it exists. - * Returns null if the file does not exist, cannot be parsed or has no properties. - */ - private Properties parseProperties(File propsFile) { - FileInputStream fis = null; - try { - if (propsFile.exists()) { - fis = new FileInputStream(propsFile); - - Properties props = new Properties(); - props.load(fis); - - // To be valid, there must be at least one property in it. - if (!props.isEmpty()) { - return props; - } - } - - } catch (IOException e) { - e.printStackTrace(); - } finally { - if (fis != null) { - try { - fis.close(); - } catch (IOException e) { - } - } - } - return null; - } - - /** - * Helper method that calls {@link File#listFiles()} and returns - * a non-null empty list if the input is not a directory or has - * no files. - */ - @NonNull - private static File[] listFilesNonNull(@NonNull File dir) { - if (dir.isDirectory()) { - File[] files = dir.listFiles(); - if (files != null) { - return files; - } - } - return LegacyFileOp.EMPTY_FILE_ARRAY; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/NullTaskMonitor.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/NullTaskMonitor.java deleted file mode 100644 index 27269541..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/NullTaskMonitor.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright (C) 2011 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.sdklib.internal.repository; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.utils.ILogger; -import com.android.utils.NullLogger; - - -/** - * A no-op implementation of the {@link ITaskMonitor} interface. - *

- * This can be passed to methods that require a monitor when the caller doesn't - * have any UI to update or means to report tracked progress. - * A custom {@link ILogger} is used. Clients could use {@link NullLogger} if - * they really don't care about the logging either. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class NullTaskMonitor implements ITaskMonitor { - - private final ILogger mLog; - - /** - * Creates a no-op {@link ITaskMonitor} that defers logging to the specified - * logger. - *

- * This can be passed to methods that require a monitor when the caller doesn't - * have any UI to update or means to report tracked progress. - * - * @param log An {@link ILogger}. Must not be null. Consider using {@link NullLogger}. - */ - public NullTaskMonitor(ILogger log) { - mLog = log; - } - - @Override - public void setDescription(String format, Object...args) { - // pass - } - - @Override - public void log(String format, Object...args) { - mLog.info(format, args); - } - - @Override - public void logError(String format, Object...args) { - mLog.error(null /*throwable*/, format, args); - } - - @Override - public void logVerbose(String format, Object...args) { - mLog.verbose(format, args); - } - - @Override - public void setProgressMax(int max) { - // pass - } - - @Override - public int getProgressMax() { - return 0; - } - - @Override - public void incProgress(int delta) { - // pass - } - - /** Always return 1. */ - @Override - public int getProgress() { - return 1; - } - - /** Always return false. */ - @Override - public boolean isCancelRequested() { - return false; - } - - @Override - public ITaskMonitor createSubMonitor(int tickCount) { - return this; - } - - /** Always return false. */ - @Override - public boolean displayPrompt(final String title, final String message) { - return false; - } - - /** Always return null. */ - @Override - public UserCredentials displayLoginCredentialsPrompt(String title, String message) { - return null; - } - - // --- ILogger --- - - @Override - public void error(@Nullable Throwable t, @Nullable String errorFormat, Object... args) { - mLog.error(t, errorFormat, args); - } - - @Override - public void warning(@NonNull String warningFormat, Object... args) { - mLog.warning(warningFormat, args); - } - - @Override - public void info(@NonNull String msgFormat, Object... args) { - mLog.info(msgFormat, args); - } - - @Override - public void verbose(@NonNull String msgFormat, Object... args) { - mLog.verbose(msgFormat, args); - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/SdkStats.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/SdkStats.java deleted file mode 100644 index cb1af741..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/SdkStats.java +++ /dev/null @@ -1,627 +0,0 @@ -/* - * Copyright (C) 2012 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.sdklib.internal.repository; - -import com.android.annotations.VisibleForTesting; -import com.android.annotations.VisibleForTesting.Visibility; -import com.android.io.NonClosingInputStream; -import com.android.io.NonClosingInputStream.CloseBehavior; -import com.android.sdklib.repository.SdkStatsConstants; -import com.android.utils.SparseArray; - -import org.w3c.dom.Document; -import org.w3c.dom.NamedNodeMap; -import org.w3c.dom.Node; -import org.xml.sax.ErrorHandler; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; -import org.xml.sax.SAXParseException; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.net.UnknownHostException; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import javax.net.ssl.SSLKeyException; -import javax.xml.XMLConstants; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.transform.stream.StreamSource; -import javax.xml.validation.Schema; -import javax.xml.validation.SchemaFactory; -import javax.xml.validation.Validator; - - -/** - * Retrieves stats on platforms. - *

- * This returns information stored on the repository in a different XML file - * and isn't directly tied to the existence of the listed platforms. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class SdkStats { - - public static class PlatformStatBase { - private final int mApiLevel; - private final String mVersionName; - private final String mCodeName; - private final float mShare; - - public PlatformStatBase(int apiLevel, - String versionName, - String codeName, - float share) { - mApiLevel = apiLevel; - mVersionName = versionName; - mCodeName = codeName; - mShare = share; - } - - /** The Android API Level for the platform. An int > 0. */ - public int getApiLevel() { - return mApiLevel; - } - - /** The official codename for this platform, for example "Cupcake". */ - public String getCodeName() { - return mCodeName; - } - - /** The official version name of this platform, for example "Android 1.5". */ - public String getVersionName() { - return mVersionName; - } - - /** An approximate share percentage of this platform and all the - * platforms of lower API level. */ - public float getShare() { - return mShare; - } - - /** Returns a string representation of this object, for debugging purposes. */ - @Override - public String toString() { - return String.format("api=%d, code=%s, vers=%s, share=%.1f%%", //$NON-NLS-1$ - mApiLevel, mCodeName, mVersionName, mShare); - } - } - - public static class PlatformStat extends PlatformStatBase { - private final float mAccumShare; - - public PlatformStat(int apiLevel, - String versionName, - String codeName, - float share, - float accumShare) { - super(apiLevel, versionName, codeName, share); - mAccumShare = accumShare; - } - - public PlatformStat(PlatformStatBase base, float accumShare) { - super(base.getApiLevel(), - base.getVersionName(), - base.getCodeName(), - base.getShare()); - mAccumShare = accumShare; - } - - /** The accumulated approximate share percentage of that platform. */ - public float getAccumShare() { - return mAccumShare; - } - - /** Returns a string representation of this object, for debugging purposes. */ - @Override - public String toString() { - return String.format("", super.toString(), mAccumShare); - } - } - - private final SparseArray mStats = new SparseArray(); - - public SdkStats() { - } - - public SparseArray getStats() { - return mStats; - } - - public void load(DownloadCache cache, boolean forceHttp, ITaskMonitor monitor) { - - String url = SdkStatsConstants.URL_STATS; - - if (forceHttp) { - url = url.replaceAll("https://", "http://"); //$NON-NLS-1$ //$NON-NLS-2$ - } - - monitor.setProgressMax(5); - monitor.setDescription("Fetching %1$s", url); - monitor.incProgress(1); - - Exception[] exception = new Exception[] { null }; - Boolean[] validatorFound = new Boolean[] { Boolean.FALSE }; - String[] validationError = new String[] { null }; - Document validatedDoc = null; - String validatedUri = null; - - InputStream xml = fetchXmlUrl(url, cache, monitor.createSubMonitor(1), exception); - - if (xml != null) { - monitor.setDescription("Validate XML"); - - // Explore the XML to find the potential XML schema version - int version = getXmlSchemaVersion(xml); - - if (version >= 1 && version <= SdkStatsConstants.NS_LATEST_VERSION) { - // This should be a version we can handle. Try to validate it - // and report any error as invalid XML syntax, - - String uri = validateXml(xml, url, version, validationError, validatorFound); - if (uri != null) { - // Validation was successful - validatedDoc = getDocument(xml, monitor); - validatedUri = uri; - - } - } else if (version > SdkStatsConstants.NS_LATEST_VERSION) { - // The schema used is more recent than what is supported by this tool. - // We don't have an upgrade-path support yet, so simply ignore the document. - closeStream(xml); - return; - } - } - - // If any exception was handled during the URL fetch, display it now. - if (exception[0] != null) { - String reason = null; - if (exception[0] instanceof FileNotFoundException) { - // FNF has no useful getMessage, so we need to special handle it. - reason = "File not found"; - } else if (exception[0] instanceof UnknownHostException && - exception[0].getMessage() != null) { - // This has no useful getMessage yet could really use one - reason = String.format("Unknown Host %1$s", exception[0].getMessage()); - } else if (exception[0] instanceof SSLKeyException) { - // That's a common error and we have a pref for it. - reason = "HTTPS SSL error. You might want to force download through HTTP in the settings."; - } else if (exception[0].getMessage() != null) { - reason = exception[0].getMessage(); - } else { - // We don't know what's wrong. Let's give the exception class at least. - reason = String.format("Unknown (%1$s)", exception[0].getClass().getName()); - } - - monitor.logError("Failed to fetch URL %1$s, reason: %2$s", url, reason); - } - - if (validationError[0] != null) { - monitor.logError("%s", validationError[0]); //$NON-NLS-1$ - } - - // Stop here if we failed to validate the XML. We don't want to load it. - if (validatedDoc == null) { - closeStream(xml); - return; - } - - monitor.incProgress(1); - - if (xml != null) { - monitor.setDescription("Parse XML"); - monitor.incProgress(1); - parseStatsDocument(validatedDoc, validatedUri, monitor); - } - - // done - monitor.incProgress(1); - closeStream(xml); - } - - /** - * Fetches the document at the given URL and returns it as a stream. Returns - * null if anything wrong happens. - * - * @param urlString The URL to load, as a string. - * @param monitor {@link ITaskMonitor} related to this URL. - * @param outException If non null, where to store any exception that - * happens during the fetch. - * @see UrlOpener UrlOpener, which handles all URL logic. - */ - private InputStream fetchXmlUrl(String urlString, - DownloadCache cache, - ITaskMonitor monitor, - Exception[] outException) { - try { - InputStream xml = cache.openCachedUrl(urlString, monitor); - if (xml != null) { - xml.mark(500000); - xml = new NonClosingInputStream(xml); - ((NonClosingInputStream) xml).setCloseBehavior(CloseBehavior.RESET); - } - return xml; - } catch (Exception e) { - if (outException != null) { - outException[0] = e; - } - } - - return null; - } - - /** - * Closes the stream, ignore any exception from InputStream.close(). - * If the stream is a NonClosingInputStream, sets it to CloseBehavior.CLOSE first. - */ - private void closeStream(InputStream is) { - if (is != null) { - if (is instanceof NonClosingInputStream) { - ((NonClosingInputStream) is).setCloseBehavior(CloseBehavior.CLOSE); - } - try { - is.close(); - } catch (IOException ignore) {} - } - } - - /** - * Manually parses the root element of the XML to extract the schema version - * at the end of the xmlns:sdk="http://schemas.android.com/sdk/android/addons-list/$N" - * declaration. - * - * @return 1..{@link SdkStatsConstants#NS_LATEST_VERSION} for a valid schema version - * or 0 if no schema could be found. - */ - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected int getXmlSchemaVersion(InputStream xml) { - if (xml == null) { - return 0; - } - - // Get an XML document - Document doc = null; - try { - xml.reset(); - - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setIgnoringComments(false); - factory.setValidating(false); - - // Parse the old document using a non namespace aware builder - factory.setNamespaceAware(false); - DocumentBuilder builder = factory.newDocumentBuilder(); - - // We don't want the default handler which prints errors to stderr. - builder.setErrorHandler(new ErrorHandler() { - @Override - public void warning(SAXParseException e) throws SAXException { - // pass - } - @Override - public void fatalError(SAXParseException e) throws SAXException { - throw e; - } - @Override - public void error(SAXParseException e) throws SAXException { - throw e; - } - }); - - doc = builder.parse(xml); - - // Prepare a new document using a namespace aware builder - factory.setNamespaceAware(true); - builder = factory.newDocumentBuilder(); - - } catch (Exception e) { - // Failed to reset XML stream - // Failed to get builder factor - // Failed to create XML document builder - // Failed to parse XML document - // Failed to read XML document - } - - if (doc == null) { - return 0; - } - - // Check the root element is an XML with at least the following properties: - // - // - // Note that we don't have namespace support enabled, we just do it manually. - - Pattern nsPattern = Pattern.compile(SdkStatsConstants.NS_PATTERN); - - String prefix = null; - for (Node child = doc.getFirstChild(); child != null; child = child.getNextSibling()) { - if (child.getNodeType() == Node.ELEMENT_NODE) { - prefix = null; - String name = child.getNodeName(); - int pos = name.indexOf(':'); - if (pos > 0 && pos < name.length() - 1) { - prefix = name.substring(0, pos); - name = name.substring(pos + 1); - } - if (SdkStatsConstants.NODE_SDK_STATS.equals(name)) { - NamedNodeMap attrs = child.getAttributes(); - String xmlns = "xmlns"; //$NON-NLS-1$ - if (prefix != null) { - xmlns += ":" + prefix; //$NON-NLS-1$ - } - Node attr = attrs.getNamedItem(xmlns); - if (attr != null) { - String uri = attr.getNodeValue(); - if (uri != null) { - Matcher m = nsPattern.matcher(uri); - if (m.matches()) { - String version = m.group(1); - try { - return Integer.parseInt(version); - } catch (NumberFormatException e) { - return 0; - } - } - } - } - } - } - } - - return 0; - } - - /** - * Validates this XML against one of the requested SDK Repository schemas. - * If the XML was correctly validated, returns the schema that worked. - * If it doesn't validate, returns null and stores the error in outError[0]. - * If we can't find a validator, returns null and set validatorFound[0] to false. - */ - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected String validateXml(InputStream xml, String url, int version, - String[] outError, Boolean[] validatorFound) { - - if (xml == null) { - return null; - } - - try { - Validator validator = getValidator(version); - - if (validator == null) { - validatorFound[0] = Boolean.FALSE; - outError[0] = String.format( - "XML verification failed for %1$s.\nNo suitable XML Schema Validator could be found in your Java environment. Please consider updating your version of Java.", - url); - return null; - } - - validatorFound[0] = Boolean.TRUE; - - // Reset the stream if it supports that operation. - xml.reset(); - - // Validation throws a bunch of possible Exceptions on failure. - validator.validate(new StreamSource(xml)); - return SdkStatsConstants.getSchemaUri(version); - - } catch (SAXParseException e) { - outError[0] = String.format( - "XML verification failed for %1$s.\nLine %2$d:%3$d, Error: %4$s", - url, - e.getLineNumber(), - e.getColumnNumber(), - e.toString()); - - } catch (Exception e) { - outError[0] = String.format( - "XML verification failed for %1$s.\nError: %2$s", - url, - e.toString()); - } - return null; - } - - /** - * Helper method that returns a validator for our XSD, or null if the current Java - * implementation can't process XSD schemas. - * - * @param version The version of the XML Schema. - * See {@link SdkStatsConstants#getXsdStream(int)} - */ - private Validator getValidator(int version) throws SAXException { - InputStream xsdStream = SdkStatsConstants.getXsdStream(version); - try { - SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); - - if (factory == null) { - return null; - } - - // This may throw a SAX Exception if the schema itself is not a valid XSD - Schema schema = factory.newSchema(new StreamSource(xsdStream)); - - Validator validator = schema == null ? null : schema.newValidator(); - - return validator; - } finally { - if (xsdStream != null) { - try { - xsdStream.close(); - } catch (IOException ignore) {} - } - } - } - - /** - * Takes an XML document as a string as parameter and returns a DOM for it. - * - * On error, returns null and prints a (hopefully) useful message on the monitor. - */ - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected Document getDocument(InputStream xml, ITaskMonitor monitor) { - try { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setIgnoringComments(true); - factory.setNamespaceAware(true); - - DocumentBuilder builder = factory.newDocumentBuilder(); - xml.reset(); - Document doc = builder.parse(new InputSource(xml)); - - return doc; - } catch (ParserConfigurationException e) { - monitor.logError("Failed to create XML document builder"); - - } catch (SAXException e) { - monitor.logError("Failed to parse XML document"); - - } catch (IOException e) { - monitor.logError("Failed to read XML document"); - } - - return null; - } - - /** - * Parses all valid platforms found in the XML. - * Changes the stats array returned by {@link #getStats()} - * (also returns the value directly, useful for unit tests.) - */ - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected SparseArray parseStatsDocument( - Document doc, - String nsUri, - ITaskMonitor monitor) { - - String baseUrl = System.getenv("SDK_TEST_BASE_URL"); //$NON-NLS-1$ - if (baseUrl != null) { - if (baseUrl.length() <= 0 || !baseUrl.endsWith("/")) { //$NON-NLS-1$ - baseUrl = null; - } - } - - SparseArray platforms = new SparseArray(); - int maxApi = 0; - - Node root = getFirstChild(doc, nsUri, SdkStatsConstants.NODE_SDK_STATS); - if (root != null) { - for (Node child = root.getFirstChild(); - child != null; - child = child.getNextSibling()) { - if (child.getNodeType() == Node.ELEMENT_NODE && - nsUri.equals(child.getNamespaceURI()) && - child.getLocalName().equals(SdkStatsConstants.NODE_PLATFORM)) { - - try { - Node node = getFirstChild(child, nsUri, SdkStatsConstants.NODE_API_LEVEL); - int apiLevel = Integer.parseInt(node.getTextContent().trim()); - - if (apiLevel < 1) { - // bad API level, ignore it. - continue; - } - - if (platforms.indexOfKey(apiLevel) >= 0) { - // if we already loaded that API, ignore duplicates - continue; - } - - String codeName = - getFirstChild(child, nsUri, SdkStatsConstants.NODE_CODENAME). - getTextContent().trim(); - String versName = - getFirstChild(child, nsUri, SdkStatsConstants.NODE_VERSION). - getTextContent().trim(); - - if (codeName == null || versName == null || - codeName.isEmpty() || versName.isEmpty()) { - // bad names. ignore. - continue; - } - - node = getFirstChild(child, nsUri, SdkStatsConstants.NODE_SHARE); - float percent = Float.parseFloat(node.getTextContent().trim()); - - if (percent < 0 || percent > 100) { - // invalid percentage. ignore. - continue; - } - - PlatformStatBase p = new PlatformStatBase( - apiLevel, versName, codeName, percent); - platforms.put(apiLevel, p); - - maxApi = apiLevel > maxApi ? apiLevel : maxApi; - - } catch (Exception ignore) { - // Error parsing this platform. Ignore it. - continue; - } - } - } - } - - mStats.clear(); - - // Compute cumulative share percents & fill in final map - for (int api = 1; api <= maxApi; api++) { - PlatformStatBase p = platforms.get(api); - if (p == null) { - continue; - } - - float sum = p.getShare(); - for (int j = api + 1; j <= maxApi; j++) { - PlatformStatBase pj = platforms.get(j); - if (pj != null) { - sum += pj.getShare(); - } - } - - mStats.put(api, new PlatformStat(p, sum)); - } - - return mStats; - } - - /** - * Returns the first child element with the given XML local name. - * If xmlLocalName is null, returns the very first child element. - */ - private Node getFirstChild(Node node, String nsUri, String xmlLocalName) { - - for(Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) { - if (child.getNodeType() == Node.ELEMENT_NODE && - nsUri.equals(child.getNamespaceURI())) { - if (xmlLocalName == null || child.getLocalName().equals(xmlLocalName)) { - return child; - } - } - } - - return null; - } - -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/UrlOpener.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/UrlOpener.java deleted file mode 100644 index 338463cb..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/UrlOpener.java +++ /dev/null @@ -1,530 +0,0 @@ -/* - * Copyright (C) 2011 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.sdklib.internal.repository; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.utils.Pair; - -import org.apache.http.Header; -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.HttpStatus; -import org.apache.http.ProtocolVersion; -import org.apache.http.auth.AuthScope; -import org.apache.http.auth.AuthState; -import org.apache.http.auth.Credentials; -import org.apache.http.auth.NTCredentials; -import org.apache.http.auth.params.AuthPNames; -import org.apache.http.client.ClientProtocolException; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.params.AuthPolicy; -import org.apache.http.client.protocol.ClientContext; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.impl.conn.ProxySelectorRoutePlanner; -import org.apache.http.message.BasicHttpResponse; -import org.apache.http.params.BasicHttpParams; -import org.apache.http.params.HttpConnectionParams; -import org.apache.http.params.HttpParams; -import org.apache.http.protocol.BasicHttpContext; -import org.apache.http.protocol.HttpContext; - -import java.io.BufferedInputStream; -import java.io.ByteArrayInputStream; -import java.io.FileNotFoundException; -import java.io.FilterInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.Proxy; -import java.net.ProxySelector; -import java.net.URI; -import java.net.URL; -import java.net.URLConnection; -import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Properties; - -/** - * This class holds static methods for downloading URL resources. - * @see #openUrl(String, boolean, ITaskMonitor, Header[]) - *

- * Implementation detail: callers should use {@link DownloadCache} instead of this class. - * {@link DownloadCache#openDirectUrl} is a direct pass-through to {@link UrlOpener} since - * there's no caching. However from an implementation perspective it's still recommended - * to pass down a {@link DownloadCache} instance, which will let us override the implementation - * later on (for testing, for example.) - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -class UrlOpener { - - private static final boolean DEBUG = - System.getenv("ANDROID_DEBUG_URL_OPENER") != null; //$NON-NLS-1$ - - private static Map sRealmCache = - new HashMap(); - - /** Timeout to establish a connection, in milliseconds. */ - private static int sConnectionTimeoutMs; - /** Timeout waiting for data on a socket, in milliseconds. */ - private static int sSocketTimeoutMs; - - static { - if (DEBUG) { - Properties props = System.getProperties(); - for (String key : new String[] { - "http.proxyHost", //$NON-NLS-1$ - "http.proxyPort", //$NON-NLS-1$ - "https.proxyHost", //$NON-NLS-1$ - "https.proxyPort" }) { //$NON-NLS-1$ - String prop = props.getProperty(key); - if (prop != null) { - System.out.printf( - "SdkLib.UrlOpener Java.Prop %s='%s'\n", //$NON-NLS-1$ - key, prop); - } - } - } - - try { - sConnectionTimeoutMs = Integer.parseInt(System.getenv("ANDROID_SDKMAN_CONN_TIMEOUT")); - } catch (Exception ignore) { - sConnectionTimeoutMs = 2 * 60 * 1000; - } - - try { - sSocketTimeoutMs = Integer.parseInt(System.getenv("ANDROID_SDKMAN_READ_TIMEOUT")); - } catch (Exception ignore) { - sSocketTimeoutMs = 1 * 60 * 1000; - } - } - - /** - * This class cannot be instantiated. - * @see #openUrl(String, boolean, ITaskMonitor, Header[]) - */ - private UrlOpener() { - } - - /** - * Opens a URL. It can be a simple URL or one which requires basic - * authentication. - *

- * Tries to access the given URL. If http response is either - * {@code HttpStatus.SC_UNAUTHORIZED} or - * {@code HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED}, asks for - * login/password and tries to authenticate into proxy server and/or URL. - *

- * This implementation relies on the Apache Http Client due to its - * capabilities of proxy/http authentication.
- * Proxy configuration is determined by {@link ProxySelectorRoutePlanner} using the JVM proxy - * settings by default. - *

- * For more information see:
- * - {@code http://hc.apache.org/httpcomponents-client-ga/}
- * - {@code http://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/impl/conn/ProxySelectorRoutePlanner.html} - *

- * There's a very simple realm cache implementation. - * Login/Password for each realm are stored in a static {@link Map}. - * Before asking the user the method verifies if the information is already - * available in the memory cache. - * - * @param url the URL string to be opened. - * @param needsMarkResetSupport Indicates the caller must have an input stream that - * supports the mark/reset operations (as indicated by {@link InputStream#markSupported()}. - * Implementation detail: If the original stream does not, it will be fetched and wrapped - * into a {@link ByteArrayInputStream}. This can only work sanely if the resource is a - * small file that can fit in memory. It also means the caller has no chance of showing - * a meaningful download progress. If unsure, callers should set this to false. - * @param monitor {@link ITaskMonitor} to output status. - * @param headers An optional array of HTTP headers to use in the GET request. - * @return Returns a {@link Pair} with {@code first} holding an {@link InputStream} - * and {@code second} holding an {@link HttpResponse}. - * The returned pair is never null and contains - * at least a code; for http requests that provide them the response - * also contains locale, headers and an status line. - * The input stream can be null, especially in case of error. - * The caller must only accept the stream if the response code is 200 or similar. - * @throws IOException Exception thrown when there are problems retrieving - * the URL or its content. - * @throws CanceledByUserException Exception thrown if the user cancels the - * authentication dialog. - */ - @NonNull - static Pair openUrl( - @NonNull String url, - boolean needsMarkResetSupport, - @NonNull ITaskMonitor monitor, - @Nullable Header[] headers) - throws IOException, CanceledByUserException { - - Exception fallbackOnJavaUrlConnect = null; - Pair result = null; - - try { - result = openWithHttpClient(url, monitor, headers); - - } catch (UnknownHostException e) { - // Host in unknown. No need to even retry with the Url object, - // if it's broken, it's broken. It's already an IOException but - // it could use a better message. - throw new IOException("Unknown Host " + e.getMessage(), e); - - } catch (ClientProtocolException e) { - // We get this when HttpClient fails to accept the current protocol, - // e.g. when processing file:// URLs. - fallbackOnJavaUrlConnect = e; - - } catch (IOException e) { - throw e; - - } catch (CanceledByUserException e) { - // HTTP Basic Auth or NTLM login was canceled by user. - throw e; - - } catch (Exception e) { - if (DEBUG) { - System.out.printf("[HttpClient Error] %s : %s\n", url, e.toString()); - } - - fallbackOnJavaUrlConnect = e; - } - - if (fallbackOnJavaUrlConnect != null) { - // If the protocol is not supported by HttpClient (e.g. file:///), - // revert to the standard java.net.Url.open. - - try { - result = openWithUrl(url, headers); - } catch (IOException e) { - throw e; - } catch (Exception e) { - if (DEBUG && !fallbackOnJavaUrlConnect.equals(e)) { - System.out.printf("[Url Error] %s : %s\n", url, e.toString()); - } - } - } - - // If the caller requires an InputStream that supports mark/reset, let's - // make sure we have such a stream. - if (result != null && needsMarkResetSupport) { - InputStream is = result.getFirst(); - if (is != null) { - if (!is.markSupported()) { - try { - // Consume the whole input stream and offer a byte array stream instead. - // This can only work sanely if the resource is a small file that can - // fit in memory. It also means the caller has no chance of showing - // a meaningful download progress. - InputStream is2 = toByteArrayInputStream(is); - if (is2 != null) { - result = Pair.of(is2, result.getSecond()); - try { - is.close(); - } catch (Exception ignore) {} - } - } catch (Exception e3) { - // Ignore. If this can't work, caller will fail later. - } - } - } - } - - if (result == null) { - // Make up an error code if we don't have one already. - HttpResponse outResponse = new BasicHttpResponse( - new ProtocolVersion("HTTP", 1, 0), //$NON-NLS-1$ - HttpStatus.SC_METHOD_FAILURE, ""); //$NON-NLS-1$; // 420=Method Failure - result = Pair.of(null, outResponse); - } - - return result; - } - - // ByteArrayInputStream is the duct tape of input streams. - private static InputStream toByteArrayInputStream(InputStream is) throws IOException { - int inc = 4096; - int curr = 0; - byte[] result = new byte[inc]; - - int n; - while ((n = is.read(result, curr, result.length - curr)) != -1) { - curr += n; - if (curr == result.length) { - byte[] temp = new byte[curr + inc]; - System.arraycopy(result, 0, temp, 0, curr); - result = temp; - } - } - - return new ByteArrayInputStream(result, 0, curr); - } - - private static Pair openWithUrl( - String url, - Header[] inHeaders) throws IOException { - URL u = new URL(url); - - URLConnection c = u.openConnection(); - - c.setConnectTimeout(sConnectionTimeoutMs); - c.setReadTimeout(sSocketTimeoutMs); - - if (inHeaders != null) { - for (Header header : inHeaders) { - c.setRequestProperty(header.getName(), header.getValue()); - } - } - - // Trigger the access to the resource - // (at which point setRequestProperty can't be used anymore.) - int code = 200; - - if (c instanceof HttpURLConnection) { - code = ((HttpURLConnection) c).getResponseCode(); - } - - // Get the input stream. That can fail for a file:// that doesn't exist - // in which case we set the response code to 404. - // Also we need a buffered input stream since the caller need to use is.reset(). - InputStream is = null; - try { - is = new BufferedInputStream(c.getInputStream()); - } catch (Exception ignore) { - if (is == null && code == 200) { - code = 404; - } - } - - HttpResponse outResponse = new BasicHttpResponse( - new ProtocolVersion(u.getProtocol(), 1, 0), // make up the protocol version - code, ""); //$NON-NLS-1$; - - Map> outHeaderMap = c.getHeaderFields(); - - for (Entry> entry : outHeaderMap.entrySet()) { - String name = entry.getKey(); - if (name != null) { - List values = entry.getValue(); - if (!values.isEmpty()) { - outResponse.setHeader(name, values.get(0)); - } - } - } - - return Pair.of(is, outResponse); - } - - @NonNull - private static Pair openWithHttpClient( - @NonNull String url, - @NonNull ITaskMonitor monitor, - Header[] inHeaders) - throws IOException, ClientProtocolException, CanceledByUserException { - UserCredentials result = null; - String realm = null; - - HttpParams params = new BasicHttpParams(); - HttpConnectionParams.setConnectionTimeout(params, sConnectionTimeoutMs); - HttpConnectionParams.setSoTimeout(params, sSocketTimeoutMs); - - // use the simple one - final DefaultHttpClient httpClient = new DefaultHttpClient(params); - - - // create local execution context - HttpContext localContext = new BasicHttpContext(); - final HttpGet httpGet = new HttpGet(url); - if (inHeaders != null) { - for (Header header : inHeaders) { - httpGet.addHeader(header); - } - } - - // retrieve local java configured network in case there is the need to - // authenticate a proxy - ProxySelectorRoutePlanner routePlanner = new ProxySelectorRoutePlanner( - httpClient.getConnectionManager().getSchemeRegistry(), - ProxySelector.getDefault()); - httpClient.setRoutePlanner(routePlanner); - - // Set preference order for authentication options. - // In particular, we don't add AuthPolicy.SPNEGO, which is given preference over NTLM in - // servers that support both, as it is more secure. However, we don't seem to handle it - // very well, so we leave it off the list. - // See http://hc.apache.org/httpcomponents-client-ga/tutorial/html/authentication.html for - // more info. - List authpref = new ArrayList(); - authpref.add(AuthPolicy.BASIC); - authpref.add(AuthPolicy.DIGEST); - authpref.add(AuthPolicy.NTLM); - httpClient.getParams().setParameter(AuthPNames.PROXY_AUTH_PREF, authpref); - httpClient.getParams().setParameter(AuthPNames.TARGET_AUTH_PREF, authpref); - - if (DEBUG) { - try { - URI uri = new URI(url); - ProxySelector sel = routePlanner.getProxySelector(); - if (sel != null && uri.getScheme().startsWith("httP")) { //$NON-NLS-1$ - List list = sel.select(uri); - System.out.printf( - "SdkLib.UrlOpener:\n Connect to: %s\n Proxy List: %s\n", //$NON-NLS-1$ - url, - list == null ? "(null)" : Arrays.toString(list.toArray()));//$NON-NLS-1$ - } - } catch (Exception e) { - System.out.printf( - "SdkLib.UrlOpener: Failed to get proxy info for %s: %s\n", //$NON-NLS-1$ - url, e.toString()); - } - } - - boolean trying = true; - // loop while the response is being fetched - while (trying) { - // connect and get status code - HttpResponse response = httpClient.execute(httpGet, localContext); - int statusCode = response.getStatusLine().getStatusCode(); - - if (DEBUG) { - System.out.printf(" Status: %d\n", statusCode); //$NON-NLS-1$ - } - - // check whether any authentication is required - AuthState authenticationState = null; - if (statusCode == HttpStatus.SC_UNAUTHORIZED) { - // Target host authentication required - authenticationState = (AuthState) localContext - .getAttribute(ClientContext.TARGET_AUTH_STATE); - } - if (statusCode == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED) { - // Proxy authentication required - authenticationState = (AuthState) localContext - .getAttribute(ClientContext.PROXY_AUTH_STATE); - } - if (statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_NOT_MODIFIED) { - // in case the status is OK and there is a realm and result, - // cache it - if (realm != null && result != null) { - sRealmCache.put(realm, result); - } - } - - // there is the need for authentication - if (authenticationState != null) { - - // get scope and realm - AuthScope authScope = authenticationState.getAuthScope(); - - // If the current realm is different from the last one it means - // a pass was performed successfully to the last URL, therefore - // cache the last realm - if (realm != null && !realm.equals(authScope.getRealm())) { - sRealmCache.put(realm, result); - } - - realm = authScope.getRealm(); - - // in case there is cache for this Realm, use it to authenticate - if (sRealmCache.containsKey(realm)) { - result = sRealmCache.get(realm); - } else { - // since there is no cache, request for login and password - result = monitor.displayLoginCredentialsPrompt("Site Authentication", - "Please login to the following domain: " + realm + - "\n\nServer requiring authentication:\n" + authScope.getHost()); - if (result == null) { - throw new CanceledByUserException("User canceled login dialog."); - } - } - - // retrieve authentication data - String user = result.getUserName(); - String password = result.getPassword(); - String workstation = result.getWorkstation(); - String domain = result.getDomain(); - - // proceed in case there is indeed a user - if (user != null && !user.isEmpty()) { - Credentials credentials = new NTCredentials(user, password, - workstation, domain); - httpClient.getCredentialsProvider().setCredentials(authScope, credentials); - trying = true; - } else { - trying = false; - } - } else { - trying = false; - } - - HttpEntity entity = response.getEntity(); - - if (entity != null) { - if (trying) { - // in case another pass to the Http Client will be performed, close the entity. - entity.getContent().close(); - } else { - // since no pass to the Http Client is needed, retrieve the - // entity's content. - - // Note: don't use something like a BufferedHttpEntity since it would consume - // all content and store it in memory, resulting in an OutOfMemory exception - // on a large download. - InputStream is = new FilterInputStream(entity.getContent()) { - @Override - public void close() throws IOException { - // Since Http Client is no longer needed, close it. - - // Bug #21167: we need to tell http client to shutdown - // first, otherwise the super.close() would continue - // downloading and not return till complete. - - httpClient.getConnectionManager().shutdown(); - super.close(); - } - }; - - HttpResponse outResponse = new BasicHttpResponse(response.getStatusLine()); - outResponse.setHeaders(response.getAllHeaders()); - outResponse.setLocale(response.getLocale()); - - return Pair.of(is, outResponse); - } - } else if (statusCode == HttpStatus.SC_NOT_MODIFIED) { - // It's ok to not have an entity (e.g. nothing to download) for a 304 - HttpResponse outResponse = new BasicHttpResponse(response.getStatusLine()); - outResponse.setHeaders(response.getAllHeaders()); - outResponse.setLocale(response.getLocale()); - - return Pair.of(null, outResponse); - } - } - - // We get here if we did not succeed. Callers do not expect a null result. - httpClient.getConnectionManager().shutdown(); - throw new FileNotFoundException(url); - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/ArchFilter.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/ArchFilter.java deleted file mode 100644 index e4401de0..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/ArchFilter.java +++ /dev/null @@ -1,324 +0,0 @@ -/* - * Copyright (C) 2014 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.sdklib.internal.repository.archives; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.sdklib.repository.NoPreviewRevision; - -import java.util.Properties; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class ArchFilter { - - private static final String PROP_HOST_OS = "Archive.HostOs"; //$NON-NLS-1$ - private static final String PROP_HOST_BITS = "Archive.HostBits"; //$NON-NLS-1$ - private static final String PROP_JVM_BITS = "Archive.JvmBits"; //$NON-NLS-1$ - private static final String PROP_MIN_JVM_VERSION = "Archive.MinJvmVers"; //$NON-NLS-1$ - - /** - * The legacy property used to serialize {@link LegacyOs} in source.properties files. - *

- * Replaced by {@code ArchFilter.PROP_HOST_OS}. - */ - public static final String LEGACY_PROP_OS = "Archive.Os"; //$NON-NLS-1$ - - /** - * The legacy property used to serialize {@link LegacyArch} in source.properties files. - *

- * Replaced by {@code ArchFilter.PROP_HOST_BITS} and {@code ArchFilter.PROP_JVM_BITS}. - */ - public static final String LEGACY_PROP_ARCH = "Archive.Arch"; //$NON-NLS-1$ - - private final HostOs mHostOs; - private final BitSize mHostBits; - private final BitSize mJvmBits; - private final NoPreviewRevision mMinJvmVersion; - - /** - * Creates a new {@link ArchFilter} with the specified filter attributes. - *

- * This filters represents the attributes requires for a package's {@link Archive} to - * be installable on the current architecture. Not all fields are required -- those that - * are not specified imply there is no limitation on that particular attribute. - * - * - * @param hostOs The host OS or null if there's no limitation for this package. - * @param hostBits The host bit size or null if there's no limitation for this package. - * @param jvmBits The JVM bit size or null if there's no limitation for this package. - * @param minJvmVersion The minimal JVM version required by this package - * or null if there's no limitation for this package. - */ - public ArchFilter(@Nullable HostOs hostOs, - @Nullable BitSize hostBits, - @Nullable BitSize jvmBits, - @Nullable NoPreviewRevision minJvmVersion) { - mHostOs = hostOs; - mHostBits = hostBits; - mJvmBits = jvmBits; - mMinJvmVersion = minJvmVersion; - } - - /** - * Creates an {@link ArchFilter} using properties previously saved in a {@link Properties} - * object, typically by the {@link ArchFilter#saveProperties(Properties)} method. - *

- * Missing properties are set to null and will not filter. - * - * @param props A properties object previously filled by {@link #saveProperties(Properties)}. - * If null, a default empty {@link ArchFilter} is created. - */ - public ArchFilter(@Nullable Properties props) { - HostOs hostOs = null; - BitSize hostBits = null; - BitSize jvmBits = null; - NoPreviewRevision minJvmVers = null; - - if (props != null) { - hostOs = HostOs .fromXmlName(props.getProperty(PROP_HOST_OS)); - hostBits = BitSize.fromXmlName(props.getProperty(PROP_HOST_BITS)); - jvmBits = BitSize.fromXmlName(props.getProperty(PROP_JVM_BITS)); - - try { - minJvmVers = NoPreviewRevision.parseRevision(props.getProperty(PROP_MIN_JVM_VERSION)); - } catch (NumberFormatException ignore) {} - - // Backward compatibility with older PROP_OS and PROP_ARCH values - if (!props.containsKey(PROP_HOST_OS) && props.containsKey(LEGACY_PROP_OS)) { - hostOs = HostOs.fromXmlName(props.getProperty(LEGACY_PROP_OS)); - } - if (!props.containsKey(PROP_HOST_BITS) && - !props.containsKey(PROP_HOST_BITS) && - props.containsKey(LEGACY_PROP_ARCH)) { - // We'll only handle the typical x86 and x86_64 values of the old PROP_ARCH - // value and ignore the PPC value. "Any" is equivalent to keeping the new - // attributes to null. - String v = props.getProperty(LEGACY_PROP_ARCH).toLowerCase(); - - if (v.indexOf("x86_64") > 0) { - // JVM in 64-bit x86_64 mode so host-bits should be 64 too. - hostBits = jvmBits = BitSize._64; - } else if (v.indexOf("x86") > 0) { - // JVM in 32-bit x86 mode, but host-bits could be either 32 or 64 - // so we don't set this one. - jvmBits = BitSize._32; - } - } - } - - mHostOs = hostOs; - mHostBits = hostBits; - mJvmBits = jvmBits; - mMinJvmVersion = minJvmVers; - } - - /** @return the host OS or null if there's no limitation for this package. */ - @Nullable - public HostOs getHostOS() { - return mHostOs; - } - - /** @return the host bit size or null if there's no limitation for this package. */ - @Nullable - public BitSize getHostBits() { - return mHostBits; - } - - /** @return the JVM bit size or null if there's no limitation for this package. */ - @Nullable - public BitSize getJvmBits() { - return mJvmBits; - } - - /** @return the minimal JVM version required by this package - * or null if there's no limitation for this package. */ - @Nullable - public NoPreviewRevision getMinJvmVersion() { - return mMinJvmVersion; - } - - /** - * Checks whether {@code this} {@link ArchFilter} is compatible with the right-hand side one. - *

- * Typically this is used to check whether "this downloaded package is compatible with the - * current architecture", which would be expressed as: - *

-     * DownloadedArchive.filter.isCompatibleWith(ArhFilter.getCurrent())
-     * 
- * For the host OS & bit size attribute, if the attributes are non-null they must be equal. - * For the min-jvm-version, "this" version (the package we want to install) needs to be lower - * or equal to the "required" (current host) version. - * - * @param required The requirements to meet. - * @return True if this filter meets or exceeds the given requirements. - */ - public boolean isCompatibleWith(@NonNull ArchFilter required) { - if (mHostOs != null - && required.mHostOs != null - && !mHostOs.equals(required.mHostOs)) { - return false; - } - - if (mHostBits != null - && required.mHostBits != null - && !mHostBits.equals(required.mHostBits)) { - return false; - } - - if (mJvmBits != null - && required.mJvmBits != null - && !mJvmBits.equals(required.mJvmBits)) { - return false; - } - - if (mMinJvmVersion != null - && required.mMinJvmVersion != null - && mMinJvmVersion.compareTo(required.mMinJvmVersion) > 0) { - return false; - } - - return true; - } - - /** - * Returns an {@link ArchFilter} that represents the current host platform. - * @return an {@link ArchFilter} that represents the current host platform. - */ - @NonNull - public static ArchFilter getCurrent() { - String os = System.getProperty("os.name"); //$NON-NLS-1$ - HostOs hostOS = null; - if (os.startsWith("Mac")) { //$NON-NLS-1$ - hostOS = HostOs.MACOSX; - } else if (os.startsWith("Windows")) { //$NON-NLS-1$ - hostOS = HostOs.WINDOWS; - } else if (os.startsWith("Linux")) { //$NON-NLS-1$ - hostOS = HostOs.LINUX; - } - - BitSize jvmBits; - String arch = System.getProperty("os.arch"); //$NON-NLS-1$ - - if (arch.equalsIgnoreCase("x86_64") || //$NON-NLS-1$ - arch.equalsIgnoreCase("ia64") || //$NON-NLS-1$ - arch.equalsIgnoreCase("amd64")) { //$NON-NLS-1$ - jvmBits = BitSize._64; - } else { - jvmBits = BitSize._32; - } - - // TODO figure out the host bit size. - // When jvmBits is 64 we know it's surely 64 - // but that's not necessarily obvious when jvmBits is 32. - BitSize hostBits = jvmBits; - - NoPreviewRevision minJvmVersion = null; - String javav = System.getProperty("java.version"); //$NON-NLS-1$ - // java Version is typically in the form "1.2.3_45" and we just need to keep up to "1.2.3" - // since our revision numbers are in 3-parts form (1.2.3). - Pattern p = Pattern.compile("((\\d+)(\\.\\d+)?(\\.\\d+)?).*"); //$NON-NLS-1$ - Matcher m = p.matcher(javav); - if (m.matches()) { - minJvmVersion = NoPreviewRevision.parseRevision(m.group(1)); - } - - return new ArchFilter(hostOS, hostBits, jvmBits, minJvmVersion); - } - - /** - * Save this {@link ArchFilter} attributes into the the given {@link Properties} object. - * These properties can later be given to the constructor that takes a {@link Properties} object. - *

- * Null attributes are not saved in the properties. - * - * @param props A non-null properties object to fill with non-null attributes. - */ - void saveProperties(@NonNull Properties props) { - if (mHostOs != null) { - props.setProperty(PROP_HOST_OS, mHostOs.getXmlName()); - } - if (mHostBits != null) { - props.setProperty(PROP_HOST_BITS, mHostBits.getXmlName()); - } - if (mJvmBits != null) { - props.setProperty(PROP_JVM_BITS, mJvmBits.getXmlName()); - } - if (mMinJvmVersion != null) { - props.setProperty(PROP_MIN_JVM_VERSION, mMinJvmVersion.toShortString()); - } - } - - /** String for debug purposes. */ - @Override - public String toString() { - return ""; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((mHostOs == null) ? 0 : mHostOs.hashCode()); - result = prime * result + ((mHostBits == null) ? 0 : mHostBits.hashCode()); - result = prime * result + ((mJvmBits == null) ? 0 : mJvmBits.hashCode()); - result = prime * result + ((mMinJvmVersion == null) ? 0 : mMinJvmVersion.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - ArchFilter other = (ArchFilter) obj; - if (mHostBits != other.mHostBits) { - return false; - } - if (mHostOs != other.mHostOs) { - return false; - } - if (mJvmBits != other.mJvmBits) { - return false; - } - if (mMinJvmVersion == null) { - if (other.mMinJvmVersion != null) { - return false; - } - } else if (!mMinJvmVersion.equals(other.mMinJvmVersion)) { - return false; - } - return true; - } - -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/Archive.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/Archive.java deleted file mode 100644 index 69ed686c..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/Archive.java +++ /dev/null @@ -1,375 +0,0 @@ -/* - * 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.sdklib.internal.repository.archives; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.annotations.VisibleForTesting; -import com.android.annotations.VisibleForTesting.Visibility; -import com.android.sdklib.repository.IDescription; -import com.android.sdklib.internal.repository.packages.Package; -import com.android.sdklib.internal.repository.sources.SdkSource; -import com.android.sdklib.io.LegacyFileOp; - -import java.io.File; -import java.util.Properties; - - -/** - * A {@link Archive} is the base class for "something" that can be downloaded from - * the SDK repository. - *

- * A package has some attributes (revision, description) and a list of archives - * which represent the downloadable bits. - *

- * Packages are offered by a {@link SdkSource} (a download site). - * The {@link ArchiveInstaller} takes care of downloading, unpacking and installing an archive. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. -*/ -@Deprecated -public class Archive implements IDescription, Comparable { - - private final String mUrl; - private final long mSize; - private final String mChecksum; - private final ChecksumType mChecksumType = ChecksumType.SHA1; - private final Package mPackage; - private final String mLocalOsPath; - private final boolean mIsLocal; - private final ArchFilter mArchFilter; - - /** - * Creates a new remote archive. - * This is typically called when inflating a remote-package info from XML meta-data. - * - * @param pkg The package that contains this archive. Typically not null. - * @param archFilter The {@link ArchFilter} for the archive. Typically not null. - * @param url The URL where the archive is available. - * Typically not null but code should be able to handles both. - * @param size The expected size in bytes of the archive to download. - * @param checksum The expected checksum string of the archive. Currently only the - * {@link ChecksumType#SHA1} format is supported. - */ - public Archive(@Nullable Package pkg, - @Nullable ArchFilter archFilter, - @Nullable String url, - long size, - @NonNull String checksum) { - mPackage = pkg; - mArchFilter = archFilter != null ? archFilter : new ArchFilter(null); - mUrl = url == null ? null : url.trim(); - mLocalOsPath = null; - mSize = size; - mChecksum = checksum; - mIsLocal = false; - } - - /** - * Creates a new local archive. - * This is typically called when inflating a local-package info by reading a local - * source.properties file. In this case a few properties like the URL, checksum and - * size are not defined. - * - * @param pkg The package that contains this archive. Cannot be null. - * @param props A set of properties. Can be null. - * @param localOsPath The OS path where the archive is installed if this represents a - * local package. Null for a remote package. - */ - @VisibleForTesting(visibility=Visibility.PACKAGE) - public Archive(@NonNull Package pkg, - @Nullable Properties props, - @Nullable String localOsPath) { - mPackage = pkg; - mArchFilter = new ArchFilter(props); - mUrl = null; - mLocalOsPath = localOsPath; - mSize = 0; - mChecksum = ""; - mIsLocal = localOsPath != null; - } - - /** - * Save the properties of the current archive in the give {@link Properties} object. - * These properties will later be give the constructor that takes a {@link Properties} object. - */ - void saveProperties(@NonNull Properties props) { - mArchFilter.saveProperties(props); - } - - /** - * Returns true if this is a locally installed archive. - * Returns false if this is a remote archive that needs to be downloaded. - */ - public boolean isLocal() { - return mIsLocal; - } - - /** - * Returns the package that created and owns this archive. - * It should generally not be null. - */ - @Nullable - public Package getParentPackage() { - return mPackage; - } - - /** - * Returns the archive size, an int > 0. - * Size will be 0 if this a local installed folder of unknown size. - */ - public long getSize() { - return mSize; - } - - /** - * Returns the SHA1 archive checksum, as a 40-char hex. - * Can be empty but not null for local installed folders. - */ - @NonNull - public String getChecksum() { - return mChecksum; - } - - /** - * Returns the checksum type, always {@link ChecksumType#SHA1} right now. - */ - @NonNull - public ChecksumType getChecksumType() { - return mChecksumType; - } - - /** - * Returns the download archive URL, either absolute or relative to the repository xml. - * Always return null for a local installed folder. - * @see #getLocalOsPath() - */ - @Nullable - public String getUrl() { - return mUrl; - } - - /** - * Returns the local OS folder where a local archive is installed. - * Always return null for remote archives. - * @see #getUrl() - */ - @Nullable - public String getLocalOsPath() { - return mLocalOsPath; - } - - /** - * Returns the architecture filter. - * This non-null filter indicates which host/jvm this archive is compatible with. - */ - @NonNull - public ArchFilter getArchFilter() { - return mArchFilter; - } - - /** - * Generates a description of the {@link ArchFilter} supported by this archive. - */ - public String getOsDescription() { - StringBuilder sb = new StringBuilder(); - - HostOs hos = mArchFilter.getHostOS(); - sb.append(hos == null ? "any OS" : hos.getUiName()); - - BitSize jvmBits = mArchFilter.getJvmBits(); - if (jvmBits != null) { - sb.append(", JVM ").append(jvmBits.getSize()).append("-bits"); - } - - BitSize hostBits = mArchFilter.getJvmBits(); - if (hostBits != null) { - sb.append(", Host ").append(hostBits.getSize()).append("-bits"); - } - - return sb.toString(); - } - - /** - * Returns the short description of the source, if not null. - * Otherwise returns the default Object toString result. - *

- * This is mostly helpful for debugging. - * For UI display, use the {@link IDescription} interface. - */ - @Override - public String toString() { - String s = getShortDescription(); - if (s != null) { - return s; - } - return super.toString(); - } - - /** - * Generates a short description for this archive. - */ - @Override - public String getShortDescription() { - return String.format("Archive for %1$s", getOsDescription()); - } - - /** - * Generates a longer description for this archive. - */ - @Override - public String getLongDescription() { - return String.format("%1$s\n%2$s\n%3$s", - getShortDescription(), - getSizeDescription(), - getSha1Description()); - } - - public String getSizeDescription() { - long size = getSize(); - String sizeStr; - if (size < 1024) { - sizeStr = String.format("%d Bytes", size); - } else if (size < 1024 * 1024) { - sizeStr = String.format("%d KiB", Math.round(size / 1024.0)); - } else if (size < 1024 * 1024 * 1024) { - sizeStr = String.format("%.1f MiB", - Math.round(10.0 * size / (1024 * 1024.0))/ 10.0); - } else { - sizeStr = String.format("%.1f GiB", - Math.round(10.0 * size / (1024 * 1024 * 1024.0))/ 10.0); - } - - return String.format("Size: %1$s", sizeStr); - } - - public String getSha1Description() { - return String.format("SHA1: %1$s", getChecksum()); - } - - /** - * Returns true if this archive can be installed on the current platform. - */ - public boolean isCompatible() { - ArchFilter current = ArchFilter.getCurrent(); - return mArchFilter.isCompatibleWith(current); - } - - /** - * Delete the archive folder if this is a local archive. - */ - public void deleteLocal() { - if (isLocal()) { - new LegacyFileOp().deleteFileOrFolder(new File(getLocalOsPath())); - } - } - - /** - * Archives are compared using their {@link Package} ordering. - * - * @see Package#compareTo(Package) - */ - @Override - public int compareTo(Archive rhs) { - if (mPackage != null && rhs != null) { - return mPackage.compareTo(rhs.getParentPackage()); - } - return 0; - } - - /** - * Note: An {@link Archive}'s hash code does NOT depend on the parent {@link Package} hash code. - *

- * {@inheritDoc} - */ - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((mArchFilter == null) ? 0 : mArchFilter.hashCode()); - result = prime * result + ((mChecksum == null) ? 0 : mChecksum.hashCode()); - result = prime * result + ((mChecksumType == null) ? 0 : mChecksumType.hashCode()); - result = prime * result + (mIsLocal ? 1231 : 1237); - result = prime * result + ((mLocalOsPath == null) ? 0 : mLocalOsPath.hashCode()); - result = prime * result + (int) (mSize ^ (mSize >>> 32)); - result = prime * result + ((mUrl == null) ? 0 : mUrl.hashCode()); - return result; - } - - /** - * Note: An {@link Archive}'s equality does NOT depend on the parent {@link Package} equality. - *

- * {@inheritDoc} - */ - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (!(obj instanceof Archive)) { - return false; - } - Archive other = (Archive) obj; - if (mArchFilter == null) { - if (other.mArchFilter != null) { - return false; - } - } else if (!mArchFilter.equals(other.mArchFilter)) { - return false; - } - if (mChecksum == null) { - if (other.mChecksum != null) { - return false; - } - } else if (!mChecksum.equals(other.mChecksum)) { - return false; - } - if (mChecksumType == null) { - if (other.mChecksumType != null) { - return false; - } - } else if (!mChecksumType.equals(other.mChecksumType)) { - return false; - } - if (mIsLocal != other.mIsLocal) { - return false; - } - if (mLocalOsPath == null) { - if (other.mLocalOsPath != null) { - return false; - } - } else if (!mLocalOsPath.equals(other.mLocalOsPath)) { - return false; - } - if (mSize != other.mSize) { - return false; - } - if (mUrl == null) { - if (other.mUrl != null) { - return false; - } - } else if (!mUrl.equals(other.mUrl)) { - return false; - } - return true; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/ArchiveInstaller.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/ArchiveInstaller.java deleted file mode 100644 index 7a34385e..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/ArchiveInstaller.java +++ /dev/null @@ -1,1220 +0,0 @@ -/* - * Copyright (C) 2010 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.sdklib.internal.repository.archives; - -import com.android.SdkConstants; -import com.android.annotations.Nullable; -import com.android.annotations.VisibleForTesting; -import com.android.annotations.VisibleForTesting.Visibility; -import com.android.sdklib.SdkManager; -import com.android.sdklib.internal.repository.CanceledByUserException; -import com.android.sdklib.internal.repository.DownloadCache; -import com.android.sdklib.internal.repository.ITaskMonitor; -import com.android.sdklib.internal.repository.packages.Package; -import com.android.sdklib.internal.repository.sources.SdkSource; -import com.android.sdklib.io.LegacyFileOp; -import com.android.sdklib.io.IFileOp; -import com.android.sdklib.repository.RepoConstants; -import com.android.utils.GrabProcessOutput; -import com.android.utils.GrabProcessOutput.IProcessOutput; -import com.android.utils.GrabProcessOutput.Wait; -import com.android.utils.Pair; - -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; -import org.apache.commons.compress.archivers.zip.ZipFile; -import org.apache.http.Header; -import org.apache.http.HttpHeaders; -import org.apache.http.HttpResponse; -import org.apache.http.HttpStatus; -import org.apache.http.message.BasicHeader; - -import java.io.EOFException; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Enumeration; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Properties; -import java.util.Set; -import java.util.TreeSet; -import java.util.regex.Pattern; - -/** - * Performs the work of installing a given {@link Archive}. - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class ArchiveInstaller { - - private static final String PROP_STATUS_CODE = "StatusCode"; //$NON-NLS-1$ - public static final String ENV_VAR_IGNORE_COMPAT = "ANDROID_SDK_IGNORE_COMPAT"; //$NON-NLS-1$ - - public static final int NUM_MONITOR_INC = 100; - - /** The current {@link LegacyFileOp} to use. Never null. */ - private final IFileOp mFileOp; - - /** - * Generates an {@link ArchiveInstaller} that relies on the default {@link LegacyFileOp}. - */ - public ArchiveInstaller() { - mFileOp = new LegacyFileOp(); - } - - /** - * Generates an {@link ArchiveInstaller} that relies on the given {@link LegacyFileOp}. - * - * @param fileUtils An alternate version of {@link LegacyFileOp} to use for file operations. - */ - protected ArchiveInstaller(IFileOp fileUtils) { - mFileOp = fileUtils; - } - - /** Returns current {@link LegacyFileOp} to use. Never null. */ - protected IFileOp getFileOp() { - return mFileOp; - } - - /** - * Install this {@link ArchiveReplacement}s. - * A "replacement" is composed of the actual new archive to install - * (c.f. {@link ArchiveReplacement#getNewArchive()} and an optional - * archive being replaced (c.f. {@link ArchiveReplacement#getReplaced()}. - * In the case of a new install, the later should be null. - *

- * The new archive to install will be skipped if it is incompatible. - * - * @return True if the archive was installed, false otherwise. - */ - public boolean install(ArchiveReplacement archiveInfo, - String osSdkRoot, - boolean forceHttp, - SdkManager sdkManager, - DownloadCache cache, - ITaskMonitor monitor) { - - Archive newArchive = archiveInfo.getNewArchive(); - Package pkg = newArchive.getParentPackage(); - - String name = pkg.getShortDescription(); - - if (newArchive.isLocal()) { - // This should never happen. - monitor.log("Skipping already installed archive: %1$s for %2$s", - name, - newArchive.getOsDescription()); - return false; - } - - // In detail mode, give us a way to force install of incompatible archives. - boolean checkIsCompatible = System.getenv(ENV_VAR_IGNORE_COMPAT) == null; - - if (checkIsCompatible && !newArchive.isCompatible()) { - monitor.log("Skipping incompatible archive: %1$s for %2$s", - name, - newArchive.getOsDescription()); - return false; - } - - Pair files = downloadFile(newArchive, osSdkRoot, cache, monitor, forceHttp); - File tmpFile = files == null ? null : files.getFirst(); - File propsFile = files == null ? null : files.getSecond(); - if (tmpFile != null) { - // Unarchive calls the pre/postInstallHook methods. - if (unarchive(archiveInfo, osSdkRoot, tmpFile, sdkManager, monitor)) { - monitor.log("Installed %1$s", name); - // Delete the temp archive if it exists, only on success - mFileOp.deleteFileOrFolder(tmpFile); - mFileOp.deleteFileOrFolder(propsFile); - return true; - } - } - - return false; - } - - /** - * Downloads an archive and returns the temp file with it. - * Caller is responsible with deleting the temp file when done. - */ - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected Pair downloadFile(Archive archive, - String osSdkRoot, - DownloadCache cache, - ITaskMonitor monitor, - boolean forceHttp) { - - String pkgName = archive.getParentPackage().getShortDescription(); - monitor.setDescription("Downloading %1$s", pkgName); - monitor.log("Downloading %1$s", pkgName); - - String link = archive.getUrl(); - if (!link.startsWith("http://") //$NON-NLS-1$ - && !link.startsWith("https://") //$NON-NLS-1$ - && !link.startsWith("ftp://")) { //$NON-NLS-1$ - // Make the URL absolute by prepending the source - Package pkg = archive.getParentPackage(); - SdkSource src = pkg.getParentSource(); - if (src == null) { - monitor.logError("Internal error: no source for archive %1$s", pkgName); - return null; - } - - // take the URL to the repository.xml and remove the last component - // to get the base - String repoXml = src.getUrl(); - int pos = repoXml.lastIndexOf('/'); - String base = repoXml.substring(0, pos + 1); - - link = base + link; - } - - if (forceHttp) { - link = link.replaceAll("https://", "http://"); //$NON-NLS-1$ //$NON-NLS-2$ - } - - // Get the basename of the file we're downloading, i.e. the last component - // of the URL - int pos = link.lastIndexOf('/'); - String base = link.substring(pos + 1); - - // Rather than create a real temp file in the system, we simply use our - // temp folder (in the SDK base folder) and use the archive name for the - // download. This allows us to reuse or continue downloads. - - File tmpFolder = getTempFolder(osSdkRoot); - if (!mFileOp.isDirectory(tmpFolder)) { - if (mFileOp.isFile(tmpFolder)) { - mFileOp.deleteFileOrFolder(tmpFolder); - } - if (!mFileOp.mkdirs(tmpFolder)) { - monitor.logError("Failed to create directory %1$s", tmpFolder.getPath()); - return null; - } - } - File tmpFile = new File(tmpFolder, base); - - // property file were we'll keep partial/resume information for reuse. - File propsFile = new File(tmpFolder, base + ".inf"); //$NON-NLS-1$ - - // if the file exists, check its checksum & size. Use it if complete - if (mFileOp.exists(tmpFile)) { - if (mFileOp.length(tmpFile) == archive.getSize()) { - String chksum = ""; //$NON-NLS-1$ - try { - chksum = fileChecksum(archive.getChecksumType().getMessageDigest(), - tmpFile, - monitor); - } catch (NoSuchAlgorithmException e) { - // Ignore. - } - if (chksum.equalsIgnoreCase(archive.getChecksum())) { - // File is good, let's use it. - return Pair.of(tmpFile, propsFile); - } else { - // The file has the right size but the wrong content. - // Just remove it and this will trigger a full download below. - mFileOp.deleteFileOrFolder(tmpFile); - } - } - } - - Header[] resumeHeaders = preparePartialDownload(archive, tmpFile, propsFile); - - if (fetchUrl(archive, resumeHeaders, tmpFile, propsFile, link, pkgName, cache, monitor)) { - // Fetching was successful, let's use this file. - return Pair.of(tmpFile, propsFile); - } - return null; - } - - /** - * Prepares to do a partial/resume download. - * - * @param archive The archive we're trying to download. - * @param tmpFile The destination file to download (e.g. something.zip) - * @param propsFile A properties file generated by the last partial download (e.g. .zip.inf) - * @return Null in case we should perform a full download, or a set of headers - * to resume a partial download. - */ - private Header[] preparePartialDownload(Archive archive, File tmpFile, File propsFile) { - // We need both the destination file and its properties to do a resume. - if (mFileOp.isFile(tmpFile) && mFileOp.isFile(propsFile)) { - // The caller already checked the case were the destination file has the - // right size _and_ checksum, so we know at this point one of them is wrong - // here. - // We can obviously only resume a file if its size is smaller than expected. - if (mFileOp.length(tmpFile) < archive.getSize()) { - Properties props = mFileOp.loadProperties(propsFile); - - List

headers = new ArrayList
(2); - headers.add(new BasicHeader(HttpHeaders.RANGE, - String.format("bytes=%d-", mFileOp.length(tmpFile)))); - - // Don't use the properties if there's not at least a 200 or 206 code from - // the last download. - int status = 0; - try { - status = Integer.parseInt(props.getProperty(PROP_STATUS_CODE)); - } catch (Exception ignore) {} - - if (status == HttpStatus.SC_OK || status == HttpStatus.SC_PARTIAL_CONTENT) { - // Do we have an ETag and/or a Last-Modified? - String etag = props.getProperty(HttpHeaders.ETAG); - String lastMod = props.getProperty(HttpHeaders.LAST_MODIFIED); - - if (etag != null && !etag.isEmpty()) { - headers.add(new BasicHeader(HttpHeaders.IF_MATCH, etag)); - } else if (lastMod != null && !lastMod.isEmpty()) { - headers.add(new BasicHeader(HttpHeaders.IF_MATCH, lastMod)); - } - - return headers.toArray(new Header[headers.size()]); - } - } - } - - // Existing file is either of different size or content. - // Remove the existing file and request a full download. - mFileOp.deleteFileOrFolder(tmpFile); - mFileOp.deleteFileOrFolder(propsFile); - - return null; - } - - /** - * Computes the SHA-1 checksum of the content of the given file. - * Returns an empty string on error (rather than null). - */ - private String fileChecksum(MessageDigest digester, File tmpFile, ITaskMonitor monitor) { - InputStream is = null; - try { - is = new FileInputStream(tmpFile); - - byte[] buf = new byte[65536]; - int n; - - while ((n = is.read(buf)) >= 0) { - if (n > 0) { - digester.update(buf, 0, n); - } - } - - return getDigestChecksum(digester); - - } catch (FileNotFoundException e) { - // The FNF message is just the URL. Make it a bit more useful. - monitor.logError("File not found: %1$s", e.getMessage()); - - } catch (Exception e) { - monitor.logError("%1$s", e.getMessage()); //$NON-NLS-1$ - - } finally { - if (is != null) { - try { - is.close(); - } catch (IOException e) { - // pass - } - } - } - - return ""; //$NON-NLS-1$ - } - - /** - * Returns the SHA-1 from a {@link MessageDigest} as an hex string - * that can be compared with {@link Archive#getChecksum()}. - */ - private String getDigestChecksum(MessageDigest digester) { - int n; - // Create an hex string from the digest - byte[] digest = digester.digest(); - n = digest.length; - String hex = "0123456789abcdef"; //$NON-NLS-1$ - char[] hexDigest = new char[n * 2]; - for (int i = 0; i < n; i++) { - int b = digest[i] & 0x0FF; - hexDigest[i*2 + 0] = hex.charAt(b >>> 4); - hexDigest[i*2 + 1] = hex.charAt(b & 0x0f); - } - - return new String(hexDigest); - } - - /** - * Actually performs the download. - * Also computes the SHA1 of the file on the fly. - *

- * Success is defined as downloading as many bytes as was expected and having the same - * SHA1 as expected. Returns true on success or false if any of those checks fail. - *

- * Increments the monitor by {@link #NUM_MONITOR_INC}. - * - * @param archive The archive we're trying to download. - * @param resumeHeaders The headers to use for a partial resume, or null when fetching - * a whole new file. - * @param tmpFile The destination file to download (e.g. something.zip) - * @param propsFile A properties file generated by the last partial download (e.g. .zip.inf) - * @param urlString The URL as a string - * @param pkgName The archive's package name, used for progress output. - * @param cache The {@link DownloadCache} instance to use. - * @param monitor The monitor to output the progress and errors. - * @return True if we fetched the file successfully. - * False if the download failed or was aborted. - */ - private boolean fetchUrl(Archive archive, - Header[] resumeHeaders, - File tmpFile, - File propsFile, - String urlString, - String pkgName, - DownloadCache cache, - ITaskMonitor monitor) { - - FileOutputStream os = null; - InputStream is = null; - int inc_remain = NUM_MONITOR_INC; - try { - Pair result = - cache.openDirectUrl(urlString, resumeHeaders, monitor); - - is = result.getFirst(); - HttpResponse resp = result.getSecond(); - int status = resp.getStatusLine().getStatusCode(); - if (status == HttpStatus.SC_NOT_FOUND) { - throw new Exception("URL not found."); - } - if (is == null) { - throw new Exception("No content."); - } - - - Properties props = new Properties(); - props.setProperty(PROP_STATUS_CODE, Integer.toString(status)); - if (resp.containsHeader(HttpHeaders.ETAG)) { - props.setProperty(HttpHeaders.ETAG, - resp.getFirstHeader(HttpHeaders.ETAG).getValue()); - } - if (resp.containsHeader(HttpHeaders.LAST_MODIFIED)) { - props.setProperty(HttpHeaders.LAST_MODIFIED, - resp.getFirstHeader(HttpHeaders.LAST_MODIFIED).getValue()); - } - - try { - mFileOp.saveProperties(propsFile, props, "## Android SDK Download."); //$NON-NLS-1$ - } catch (IOException ignore) {} - - // On success, status can be: - // - 206 (Partial content), if resumeHeaders is not null (we asked for a partial - // download, and we get partial content for that download => we'll need to append - // to the existing file.) - // - 200 (OK) meaning we're getting whole new content from scratch. This can happen - // even if resumeHeaders is not null (typically means the server has a new version - // of the file to serve.) In this case we reset the file and write from scratch. - - boolean append = status == HttpStatus.SC_PARTIAL_CONTENT; - if (status != HttpStatus.SC_OK && !(append && resumeHeaders != null)) { - throw new Exception(String.format("Unexpected HTTP Status %1$d", status)); - } - MessageDigest digester = archive.getChecksumType().getMessageDigest(); - - if (append) { - // Seed the digest with the existing content. - InputStream temp = null; - try { - temp = new FileInputStream(tmpFile); - - byte[] buf = new byte[65536]; - int n; - - while ((n = temp.read(buf)) >= 0) { - if (n > 0) { - digester.update(buf, 0, n); - } - } - } catch (Exception ignore) { - } finally { - if (temp != null) { - try { - temp.close(); - } catch (IOException ignore) {} - } - } - } - - // Open the output stream in append for a resume, or reset for a full download. - os = new FileOutputStream(tmpFile, append); - - byte[] buf = new byte[65536]; - int n; - - long total = 0; - long size = archive.getSize(); - if (append) { - long len = mFileOp.length(tmpFile); - int percent = (int) (len * 100 / size); - size -= len; - monitor.logVerbose( - "Resuming %1$s download at %2$d (%3$d%%)", pkgName, len, percent); - } - long inc = size / NUM_MONITOR_INC; - long next_inc = inc; - - long startMs = System.currentTimeMillis(); - long nextMs = startMs + 2000; // start update after 2 seconds - - while ((n = is.read(buf)) >= 0) { - if (n > 0) { - os.write(buf, 0, n); - digester.update(buf, 0, n); - } - - long timeMs = System.currentTimeMillis(); - - total += n; - if (total >= next_inc) { - monitor.incProgress(1); - inc_remain--; - next_inc += inc; - } - - if (timeMs > nextMs) { - long delta = timeMs - startMs; - if (total > 0 && delta > 0) { - // percent left to download - int percent = (int) (100 * total / size); - // speed in KiB/s - float speed = (float)total / (float)delta * (1000.f / 1024.f); - // time left to download the rest at the current KiB/s rate - int timeLeft = (speed > 1e-3) ? - (int)(((size - total) / 1024.0f) / speed) : - 0; - String timeUnit = "seconds"; - if (timeLeft > 120) { - timeUnit = "minutes"; - timeLeft /= 60; - } - - monitor.setDescription( - "Downloading %1$s (%2$d%%, %3$.0f KiB/s, %4$d %5$s left)", - pkgName, - percent, - speed, - timeLeft, - timeUnit); - } - nextMs = timeMs + 1000; // update every second - } - - if (monitor.isCancelRequested()) { - monitor.log("Download aborted by user at %1$d bytes.", total); - return false; - } - - } - - if (total != size) { - monitor.logError( - "Download finished with wrong size. Expected %1$d bytes, got %2$d bytes.", - size, total); - return false; - } - - // Create an hex string from the digest - String actual = getDigestChecksum(digester); - String expected = archive.getChecksum(); - if (!actual.equalsIgnoreCase(expected)) { - monitor.logError("Download finished with wrong checksum. Expected %1$s, got %2$s.", - expected, actual); - return false; - } - - return true; - - } catch (CanceledByUserException e) { - // HTTP Basic Auth or NTLM login was canceled by user. - // Don't output an error in the log. - - } catch (FileNotFoundException e) { - // The FNF message is just the URL. Make it a bit more useful. - monitor.logError("URL not found: %1$s", e.getMessage()); - - } catch (Exception e) { - monitor.logError("Download interrupted: %1$s", e.getMessage()); //$NON-NLS-1$ - - } finally { - if (os != null) { - try { - os.close(); - } catch (IOException e) { - // pass - } - } - - if (is != null) { - try { - is.close(); - } catch (IOException e) { - // pass - } - } - if (inc_remain > 0) { - monitor.incProgress(inc_remain); - } - } - - return false; - } - - /** - * Install the given archive in the given folder. - */ - private boolean unarchive(ArchiveReplacement archiveInfo, - String osSdkRoot, - File archiveFile, - SdkManager sdkManager, - ITaskMonitor monitor) { - boolean success = false; - Archive newArchive = archiveInfo.getNewArchive(); - Package pkg = newArchive.getParentPackage(); - String pkgName = pkg.getShortDescription(); - monitor.setDescription("Installing %1$s", pkgName); - monitor.log("Installing %1$s", pkgName); - - // Ideally we want to always unzip in a temp folder which name depends on the package - // type (e.g. addon, tools, etc.) and then move the folder to the destination folder. - // If the destination folder exists, it will be renamed and deleted at the very - // end if everything succeeded. This provides a nice atomic swap and should leave the - // original folder untouched in case something wrong (e.g. program crash) in the - // middle of the unzip operation. - // - // However that doesn't work on Windows, we always end up not being able to move the - // new folder. There are actually 2 cases: - // A- A process such as a the explorer is locking the *old* folder or a file inside - // (e.g. adb.exe) - // In this case we really shouldn't be tried to work around it and we need to let - // the user know and let it close apps that access that folder. - // B- A process is locking the *new* folder. Very often this turns to be a file indexer - // or an anti-virus that is busy scanning the new folder that we just unzipped. - // - // So we're going to change the strategy: - // 1- Try to move the old folder to a temp/old folder. This might fail in case of issue A. - // Note: for platform-tools, we can try killing adb first. - // If it still fails, we do nothing and ask the user to terminate apps that can be - // locking that folder. - // 2- Once the old folder is out of the way, we unzip the archive directly into the - // optimal new location. We no longer unzip it in a temp folder and move it since we - // know that's what fails in most of the cases. - // 3- If the unzip fails, remove everything and try to restore the old folder by doing - // a *copy* in place and not a folder move (which will likely fail too). - - String pkgKind = pkg.getClass().getSimpleName(); - - File destFolder = null; - File oldDestFolder = null; - - try { - // -0- Compute destination directory and check install pre-conditions - - destFolder = pkg.getInstallFolder(osSdkRoot, sdkManager); - - if (destFolder == null) { - // this should not seriously happen. - monitor.log("Failed to compute installation directory for %1$s.", pkgName); - return false; - } - - if (!pkg.preInstallHook(newArchive, monitor, osSdkRoot, destFolder)) { - monitor.log("Skipping archive: %1$s", pkgName); - return false; - } - - // -1- move old folder. - - if (mFileOp.exists(destFolder)) { - // Create a new temp/old dir - if (oldDestFolder == null) { - oldDestFolder = getNewTempFolder(osSdkRoot, pkgKind, "old"); //$NON-NLS-1$ - } - if (oldDestFolder == null) { - // this should not seriously happen. - monitor.logError("Failed to find a temp directory in %1$s.", osSdkRoot); - return false; - } - - // Try to move the current dest dir to the temp/old one. Tell the user if it failed. - while(true) { - if (!moveFolder(destFolder, oldDestFolder)) { - monitor.logError("Failed to rename directory %1$s to %2$s.", - destFolder.getPath(), oldDestFolder.getPath()); - - if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) { - boolean tryAgain = true; - - tryAgain = windowsDestDirLocked(osSdkRoot, destFolder, monitor); - - if (tryAgain) { - // loop, trying to rename the temp dir into the destination - continue; - } else { - return false; - } - } - } - break; - } - } - - assert !mFileOp.exists(destFolder); - - // -2- Unzip new content directly in place. - - if (!mFileOp.mkdirs(destFolder)) { - monitor.logError("Failed to create directory %1$s", destFolder.getPath()); - return false; - } - - if (!unzipFolder(archiveInfo, - archiveFile, - destFolder, - monitor)) { - return false; - } - - if (!generateSourceProperties(newArchive, destFolder)) { - monitor.logError("Failed to generate source.properties in directory %1$s", - destFolder.getPath()); - return false; - } - - // In case of success, if we were replacing an archive - // and the older one had a different path, remove it now. - Archive oldArchive = archiveInfo.getReplaced(); - if (oldArchive != null && oldArchive.isLocal()) { - String oldPath = oldArchive.getLocalOsPath(); - File oldFolder = oldPath == null ? null : new File(oldPath); - if (oldFolder == null && oldArchive.getParentPackage() != null) { - oldFolder = oldArchive.getParentPackage().getInstallFolder( - osSdkRoot, sdkManager); - } - if (oldFolder != null && mFileOp.exists(oldFolder) && - !oldFolder.equals(destFolder)) { - monitor.logVerbose("Removing old archive at %1$s", oldFolder.getAbsolutePath()); - mFileOp.deleteFileOrFolder(oldFolder); - } - } - - success = true; - pkg.postInstallHook(newArchive, monitor, destFolder); - return true; - - } finally { - if (!success) { - // In case of failure, we try to restore the old folder content. - if (oldDestFolder != null) { - restoreFolder(oldDestFolder, destFolder); - } - - // We also call the postInstallHool with a null directory to give a chance - // to the archive to cleanup after preInstallHook. - pkg.postInstallHook(newArchive, monitor, null /*installDir*/); - } - - // Cleanup if the unzip folder is still set. - mFileOp.deleteFileOrFolder(oldDestFolder); - } - } - - private boolean windowsDestDirLocked( - String osSdkRoot, - File destFolder, - final ITaskMonitor monitor) { - String msg = null; - - assert SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS; - - File findLockExe = LegacyFileOp.append( - osSdkRoot, SdkConstants.FD_TOOLS, SdkConstants.FD_LIB, SdkConstants.FN_FIND_LOCK); - - if (mFileOp.exists(findLockExe)) { - try { - final StringBuilder result = new StringBuilder(); - String command[] = new String[] { - findLockExe.getAbsolutePath(), - destFolder.getAbsolutePath() - }; - Process process = Runtime.getRuntime().exec(command); - int retCode = GrabProcessOutput.grabProcessOutput( - process, - Wait.WAIT_FOR_READERS, - new IProcessOutput() { - @Override - public void out(@Nullable String line) { - if (line != null) { - result.append(line).append("\n"); - } - } - - @Override - public void err(@Nullable String line) { - if (line != null) { - monitor.logError("[find_lock] Error: %1$s", line); - } - } - }); - - if (retCode == 0 && result.length() > 0) { - // TODO create a better dialog - - String found = result.toString().trim(); - monitor.logError("[find_lock] Directory locked by %1$s", found); - - TreeSet apps = new TreeSet(Arrays.asList( - found.split(Pattern.quote(";")))); //$NON-NLS-1$ - StringBuilder appStr = new StringBuilder(); - for (String app : apps) { - appStr.append("\n - ").append(app.trim()); //$NON-NLS-1$ - } - - msg = String.format( - "-= Warning ! =-\n" + - "The following processes: %1$s\n" + - "are locking the following directory: \n" + - " %2$s\n" + - "Please close these applications so that the installation can continue.\n" + - "When ready, press YES to try again.", - appStr.toString(), - destFolder.getPath()); - } - - } catch (Exception e) { - monitor.error(e, "[find_lock failed]"); - } - - - } - - if (msg == null) { - // Old way: simply display a generic text and let user figure it out. - msg = String.format( - "-= Warning ! =-\n" + - "A folder failed to be moved. On Windows this " + - "typically means that a program is using that folder (for " + - "example Windows Explorer or your anti-virus software.)\n" + - "Please momentarily deactivate your anti-virus software or " + - "close any running programs that may be accessing the " + - "directory '%1$s'.\n" + - "When ready, press YES to try again.", - destFolder.getPath()); - } - - boolean tryAgain = monitor.displayPrompt("SDK Manager: failed to install", msg); - return tryAgain; - } - - /** - * Tries to rename/move a folder. - *

- * Contract: - *

    - *
  • When we start, oldDir must exist and be a directory. newDir must not exist.
  • - *
  • On successful completion, oldDir must not exists. - * newDir must exist and have the same content.
  • - *
  • On failure completion, oldDir must have the same content as before. - * newDir must not exist.
  • - *
- *

- * The simple "rename" operation on a folder can typically fail on Windows for a variety - * of reason, in fact as soon as a single process holds a reference on a directory. The - * most common case are the Explorer, the system's file indexer, Tortoise SVN cache or - * an anti-virus that are busy indexing a new directory having been created. - * - * @param oldDir The old location to move. It must exist and be a directory. - * @param newDir The new location where to move. It must not exist. - * @return True if the move succeeded. On failure, we try hard to not have touched the old - * directory in order not to loose its content. - */ - private boolean moveFolder(File oldDir, File newDir) { - // This is a simple folder rename that works on Linux/Mac all the time. - // - // On Windows this might fail if an indexer is busy looking at a new directory - // (e.g. right after we unzip our archive), so it fails let's be nice and give - // it a bit of time to succeed. - for (int i = 0; i < 5; i++) { - if (mFileOp.renameTo(oldDir, newDir)) { - return true; - } - try { - Thread.sleep(500 /*ms*/); - } catch (InterruptedException e) { - // ignore - } - } - - return false; - } - - /** - * Unzips a zip file into the given destination directory. - * - * The archive file MUST have a unique "root" folder. - * This root folder is skipped when unarchiving. - */ - @SuppressWarnings("unchecked") - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected boolean unzipFolder( - ArchiveReplacement archiveInfo, - File archiveFile, - File unzipDestFolder, - ITaskMonitor monitor) { - - Archive newArchive = archiveInfo.getNewArchive(); - Package pkg = newArchive.getParentPackage(); - String pkgName = pkg.getShortDescription(); - long compressedSize = newArchive.getSize(); - - ZipFile zipFile = null; - try { - zipFile = new ZipFile(archiveFile); - - // To advance the percent and the progress bar, we don't know the number of - // items left to unzip. However we know the size of the archive and the size of - // each uncompressed item. The zip file format overhead is negligible so that's - // a good approximation. - long incStep = compressedSize / NUM_MONITOR_INC; - long incTotal = 0; - long incCurr = 0; - int lastPercent = 0; - - byte[] buf = new byte[65536]; - - Enumeration entries = zipFile.getEntries(); - while (entries.hasMoreElements()) { - ZipArchiveEntry entry = entries.nextElement(); - - String name = entry.getName(); - - // ZipFile entries should have forward slashes, but not all Zip - // implementations can be expected to do that. - name = name.replace('\\', '/'); - - // Zip entries are always packages in a top-level directory (e.g. docs/index.html). - int pos = name.indexOf('/'); - if (pos == -1) { - // All zip entries should have a root folder. - // This zip entry seems located at the root of the zip. - // Rather than ignore the file, just place it at the root. - } else if (pos == name.length() - 1) { - // This is a zip *directory* entry in the form dir/, so essentially - // it's the root directory of the SDK. It's safe to ignore that one - // since we want to use our own root directory and we'll recreate - // root directories as needed. - // A direct consequence is that if a malformed archive has multiple - // root directories, their content will all be merged together. - continue; - } else { - // This is the expected behavior: the zip entry is in the form root/file - // or root/dir/. We want to use our top-level directory so we drop the - // first segment of the path name. - name = name.substring(pos + 1); - } - - File destFile = new File(unzipDestFolder, name); - - if (name.endsWith("/")) { //$NON-NLS-1$ - // Create directory if it doesn't exist yet. This allows us to create - // empty directories. - if (!mFileOp.isDirectory(destFile) && !mFileOp.mkdirs(destFile)) { - monitor.logError("Failed to create directory %1$s", - destFile.getPath()); - return false; - } - continue; - } else if (name.indexOf('/') != -1) { - // Otherwise it's a file in a sub-directory. - - // Sanity check: since we're always unzipping in a fresh temp folder - // the destination file shouldn't already exist. - if (mFileOp.exists(destFile)) { - monitor.logVerbose("Duplicate file found: %1$s", name); - } - - // Make sure the parent directory has been created. - File parentDir = destFile.getParentFile(); - if (!mFileOp.isDirectory(parentDir)) { - if (!mFileOp.mkdirs(parentDir)) { - monitor.logError("Failed to create directory %1$s", - parentDir.getPath()); - return false; - } - } - } - - FileOutputStream fos = null; - long remains = entry.getSize(); - try { - fos = new FileOutputStream(destFile); - - // Java bug 4040920: do not rely on the input stream EOF and don't - // try to read more than the entry's size. - InputStream entryContent = zipFile.getInputStream(entry); - int n; - while (remains > 0 && - (n = entryContent.read( - buf, 0, (int) Math.min(remains, buf.length))) != -1) { - remains -= n; - if (n > 0) { - fos.write(buf, 0, n); - } - } - } catch (EOFException e) { - monitor.logError("Error uncompressing file %s. Size: %d bytes, Unwritten: %d bytes.", - entry.getName(), entry.getSize(), remains); - throw e; - } finally { - if (fos != null) { - fos.close(); - } - } - - pkg.postUnzipFileHook(newArchive, monitor, mFileOp, destFile, entry); - - // Increment progress bar to match. We update only between files. - for(incTotal += entry.getCompressedSize(); incCurr < incTotal; incCurr += incStep) { - monitor.incProgress(1); - } - - int percent = (int) (100 * incTotal / compressedSize); - if (percent != lastPercent) { - monitor.setDescription("Unzipping %1$s (%2$d%%)", pkgName, percent); - lastPercent = percent; - } - - if (monitor.isCancelRequested()) { - return false; - } - } - - return true; - - } catch (IOException e) { - monitor.logError("Unzip failed: %1$s", e.getMessage()); - - } finally { - if (zipFile != null) { - try { - zipFile.close(); - } catch (IOException e) { - // pass - } - } - } - - return false; - } - - /** - * Returns an unused temp folder path in the form of osBasePath/temp/prefix.suffixNNN. - *

- * This does not actually create the folder. It just scan the base path for - * a free folder name to use and returns the file to use to reference it. - *

- * This operation is not atomic so there's no guarantee the folder can't get - * created in between. This is however unlikely and the caller can assume the - * returned folder does not exist yet. - *

- * Returns null if no such folder can be found (e.g. if all candidates exist, - * which is rather unlikely) or if the base temp folder cannot be created. - */ - private File getNewTempFolder(String osBasePath, String prefix, String suffix) { - File baseTempFolder = getTempFolder(osBasePath); - - if (!mFileOp.isDirectory(baseTempFolder)) { - if (mFileOp.isFile(baseTempFolder)) { - mFileOp.deleteFileOrFolder(baseTempFolder); - } - if (!mFileOp.mkdirs(baseTempFolder)) { - return null; - } - } - - for (int i = 1; i < 100; i++) { - File folder = new File(baseTempFolder, - String.format("%1$s.%2$s%3$02d", prefix, suffix, i)); //$NON-NLS-1$ - if (!mFileOp.exists(folder)) { - return folder; - } - } - return null; - } - - /** - * Returns the single fixed "temp" folder used by the SDK Manager. - * This folder is always at osBasePath/temp. - *

- * This does not actually create the folder. - */ - private File getTempFolder(String osBasePath) { - File baseTempFolder = new File(osBasePath, RepoConstants.FD_TEMP); - return baseTempFolder; - } - - /** - * Generates a source.properties in the destination folder that contains all the infos - * relevant to this archive, this package and the source so that we can reload them - * locally later. - */ - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected boolean generateSourceProperties(Archive archive, File unzipDestFolder) { - - // Create a version of Properties that returns a sorted key set. - // This is used by Properties#saveProperties and should ensure the - // properties are in a stable order. Unit tests rely on this fact. - @SuppressWarnings("serial") - Properties props = new Properties() { - @Override - public synchronized Enumeration keys() { - Set sortedSet = new TreeSet(keySet()); - final Iterator it = sortedSet.iterator(); - return new Enumeration() { - @Override - public boolean hasMoreElements() { - return it.hasNext(); - } - - @Override - public Object nextElement() { - return it.next(); - } - - }; - } - }; - - archive.saveProperties(props); - - Package pkg = archive.getParentPackage(); - if (pkg != null) { - pkg.saveProperties(props); - } - - try { - mFileOp.saveProperties( - new File(unzipDestFolder, SdkConstants.FN_SOURCE_PROP), - props, - "## Android Tool: Source of this archive."); //$NON-NLS-1$ - return true; - } catch (IOException ignore) { - return false; - } - } - - /** - * Recursively restore srcFolder into destFolder by performing a copy of the file - * content rather than rename/moves. - * - * @param srcFolder The source folder to restore. - * @param destFolder The destination folder where to restore. - * @return True if the folder was successfully restored, false if it was not at all or - * only partially restored. - */ - private boolean restoreFolder(File srcFolder, File destFolder) { - boolean result = true; - - // Process sub-folders first - File[] srcFiles = mFileOp.listFiles(srcFolder); - if (srcFiles == null) { - // Source does not exist. That is quite odd. - return false; - } - - if (mFileOp.isFile(destFolder)) { - if (!mFileOp.delete(destFolder)) { - // There's already a file in there where we want a directory and - // we can't delete it. This is rather unexpected. Just give up on - // that folder. - return false; - } - } else if (!mFileOp.isDirectory(destFolder)) { - mFileOp.mkdirs(destFolder); - } - - // Get all the files and dirs of the current destination. - // We are not going to clean up the destination first. - // Instead we'll copy over and just remove any remaining files or directories. - Set destDirs = new HashSet(); - Set destFiles = new HashSet(); - File[] files = mFileOp.listFiles(destFolder); - if (files != null) { - for (File f : files) { - if (mFileOp.isDirectory(f)) { - destDirs.add(f); - } else { - destFiles.add(f); - } - } - } - - // First restore all source directories. - for (File dir : srcFiles) { - if (mFileOp.isDirectory(dir)) { - File d = new File(destFolder, dir.getName()); - destDirs.remove(d); - if (!restoreFolder(dir, d)) { - result = false; - } - } - } - - // Remove any remaining directories not processed above. - for (File dir : destDirs) { - mFileOp.deleteFileOrFolder(dir); - } - - // Copy any source files over to the destination. - for (File file : srcFiles) { - if (mFileOp.isFile(file)) { - File f = new File(destFolder, file.getName()); - destFiles.remove(f); - try { - mFileOp.copyFile(file, f); - } catch (IOException e) { - result = false; - } - } - } - - // Remove any remaining files not processed above. - for (File file : destFiles) { - mFileOp.deleteFileOrFolder(file); - } - - return result; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/ArchiveReplacement.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/ArchiveReplacement.java deleted file mode 100644 index 4cf85f2c..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/ArchiveReplacement.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (C) 2011 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.sdklib.internal.repository.archives; - -import com.android.sdklib.repository.IDescription; -import com.android.sdklib.internal.repository.packages.Package; - - -/** - * Represents an archive that we want to install and the archive that it is - * going to replace, if any. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class ArchiveReplacement implements IDescription { - - private final Archive mNewArchive; - private final Archive mReplaced; - - /** - * Creates a new replacement where the {@code newArchive} will replace the - * currently installed {@code replaced} archive. - * When {@code newArchive} is not intended to replace anything (e.g. because - * the user is installing a new package not present on her system yet), then - * {@code replace} shall be null. - * - * @param newArchive A "new archive" to be installed. This is always an archive - * that comes from a remote site. This may be null. - * @param replaced An optional local archive that the new one will replace. - * Can be null if this archive does not replace anything. - */ - public ArchiveReplacement(Archive newArchive, Archive replaced) { - mNewArchive = newArchive; - mReplaced = replaced; - } - - /** - * Returns the "new archive" to be installed. - * This may be null for missing archives. - */ - public Archive getNewArchive() { - return mNewArchive; - } - - /** - * Returns an optional local archive that the new one will replace. - * Can be null if this archive does not replace anything. - */ - public Archive getReplaced() { - return mReplaced; - } - - /** - * Returns the long description of the parent package of the new archive, if not null. - * Otherwise returns an empty string. - */ - @Override - public String getLongDescription() { - if (mNewArchive != null) { - Package p = mNewArchive.getParentPackage(); - if (p != null) { - return p.getLongDescription(); - } - } - return ""; - } - - /** - * Returns the short description of the parent package of the new archive, if not null. - * Otherwise returns an empty string. - */ - @Override - public String getShortDescription() { - if (mNewArchive != null) { - Package p = mNewArchive.getParentPackage(); - if (p != null) { - return p.getShortDescription(); - } - } - return ""; - } - - /** - * Returns the short description of the parent package of the new archive, if not null. - * Otherwise returns the default Object toString result. - *

- * This is mostly helpful for debugging. For UI display, use the {@link IDescription} - * interface. - */ - @Override - public String toString() { - if (mNewArchive != null) { - Package p = mNewArchive.getParentPackage(); - if (p != null) { - return p.getShortDescription(); - } - } - return super.toString(); - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/BitSize.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/BitSize.java deleted file mode 100644 index 5f72c76f..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/BitSize.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2014 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.sdklib.internal.repository.archives; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; - -/** - * The Architecture that this archive can be downloaded on. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public enum BitSize { - _32(32), - _64(64); - - private final int mSize; - - BitSize(int size) { - mSize = size; - } - - /** Returns the size of the architecture. */ - public int getSize() { - return mSize; - } - - /** Returns the XML name of the bit size. */ - @NonNull - public String getXmlName() { - return Integer.toString(mSize); - } - /** - * Returns the enum value matching the given XML name. - * @return A valid {@link HostOs} constant or null if not a valid XML name. - */ - @Nullable - public static BitSize fromXmlName(@Nullable String xmlName) { - if (xmlName != null) { - for (BitSize v : values()) { - if (v.getXmlName().equalsIgnoreCase(xmlName)) { - return v; - } - } - } - return null; - } - -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/ChecksumType.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/ChecksumType.java deleted file mode 100644 index 93049345..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/ChecksumType.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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.sdklib.internal.repository.archives; - -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -/** - * The checksum type. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public enum ChecksumType { - /** A SHA1 checksum, represented as a 40-hex string. */ - SHA1("SHA-1"); //$NON-NLS-1$ - - private final String mAlgorithmName; - - /** - * Constructs a {@link ChecksumType} with the algorithm name - * suitable for {@link MessageDigest#getInstance(String)}. - *

- * These names are officially documented at - * http://java.sun.com/javase/6/docs/technotes/guides/security/StandardNames.html#MessageDigest - */ - ChecksumType(String algorithmName) { - mAlgorithmName = algorithmName; - } - - /** - * Returns a new {@link MessageDigest} instance for this checksum type. - * @throws NoSuchAlgorithmException if this algorithm is not available. - */ - public MessageDigest getMessageDigest() throws NoSuchAlgorithmException { - return MessageDigest.getInstance(mAlgorithmName); - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/HostOs.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/HostOs.java deleted file mode 100644 index 6c7d4431..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/HostOs.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (C) 2014 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.sdklib.internal.repository.archives; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; - -import java.util.Locale; - -/** - * The OS that this archive can be downloaded on.
- * The represents a "host" where the SDK tools and the SDK Manager can run, - * not the Android device targets. - *

- * The actual OS requirements for the SDK are listed at - * http://d.android.com/sdk - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public enum HostOs { - /** Any of the Unix-like host OSes. */ - LINUX("Linux"), - /** Any variation of MacOS X. */ - MACOSX("MacOS X"), - /** Any variation of Windows. */ - WINDOWS("Windows"); - - private final String mUiName; - - HostOs(@NonNull String uiName) { - mUiName = uiName; - } - - /** - * Returns the UI name of the OS. - */ - @NonNull - public String getUiName() { - return mUiName; - } - - /** - * Returns the XML name of the OS. - * @returns Null, windows, macosx or linux. - */ - @NonNull - public String getXmlName() { - return toString().toLowerCase(Locale.US); - } - - /** - * Returns the enum value matching the given XML name. - * @return A valid {@link HostOs} constnat or null if not a valid XML name. - */ - @Nullable - public static HostOs fromXmlName(@Nullable String xmlName) { - if (xmlName != null) { - for (HostOs v : values()) { - if (v.getXmlName().equalsIgnoreCase(xmlName)) { - return v; - } - } - } - return null; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/LegacyArch.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/LegacyArch.java deleted file mode 100644 index 51d78dc7..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/LegacyArch.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) 2014 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.sdklib.internal.repository.archives; - - - -/** - * The legacy Architecture that this archive can be downloaded on. - *

- * This attribute was used for the <archive> element in repo schema 1-9. - * add-on schema 1-6 and sys-img schema 1-2. - * Starting with repo schema 10, add-on schema 7 and sys-img schema 3, this is replaced - * by the <host-bit> and <jvm-bit> elements and {@link ArchFilter}. - * - * @see HostOs - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public enum LegacyArch { - ANY("Any"), - PPC("PowerPC"), - X86("x86"), - X86_64("x86_64"); - - private final String mUiName; - - LegacyArch(String uiName) { - mUiName = uiName; - } - - /** Returns the UI name of the architecture. */ - public String getUiName() { - return mUiName; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/LegacyOs.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/LegacyOs.java deleted file mode 100644 index 2f096b47..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/archives/LegacyOs.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) 2014 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.sdklib.internal.repository.archives; - - - -/** - * The legacy OS that this archive can be downloaded on. - *

- * This attribute was used for the <archive> element in repo schema 1-9. - * add-on schema 1-6 and sys-img schema 1-2. - * Starting with repo schema 10, add-on schema 7 and sys-img schema 3, this is replaced - * by the <host-os> element and {@link ArchFilter}. - * - * @see HostOs - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public enum LegacyOs { - ANY("Any"), - LINUX("Linux"), - MACOSX("MacOS X"), - WINDOWS("Windows"); - - private final String mUiName; - - LegacyOs(String uiName) { - mUiName = uiName; - } - - /** Returns the UI name of the OS. */ - public String getUiName() { - return mUiName; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/AddonPackage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/AddonPackage.java deleted file mode 100644 index f5c404db..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/AddonPackage.java +++ /dev/null @@ -1,765 +0,0 @@ -/* - * 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.sdklib.internal.repository.packages; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.annotations.VisibleForTesting; -import com.android.annotations.VisibleForTesting.Visibility; -import com.android.sdklib.AndroidVersion; -import com.android.sdklib.IAndroidTarget; -import com.android.sdklib.IAndroidTarget.OptionalLibrary; -import com.android.sdklib.SdkManager; -import com.android.sdklib.internal.repository.sources.SdkSource; -import com.android.sdklib.repository.AddonManifestIniProps; -import com.android.sdklib.repository.IDescription; -import com.android.sdklib.repository.MajorRevision; -import com.android.sdklib.repository.PkgProps; -import com.android.sdklib.repository.SdkAddonConstants; -import com.android.sdklib.repository.SdkRepoConstants; -import com.android.sdklib.repository.descriptors.IPkgDesc; -import com.android.sdklib.repository.descriptors.IdDisplay; -import com.android.sdklib.repository.descriptors.PkgDesc; -import com.android.sdklib.repository.local.LocalAddonPkgInfo; -import com.android.utils.Pair; -import com.google.common.collect.ImmutableList; - -import org.w3c.dom.Node; - -import java.io.File; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Properties; - -/** - * Represents an add-on XML node in an SDK repository. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class AddonPackage extends MajorRevisionPackage - implements IAndroidVersionProvider, IPlatformDependency, - IExactApiLevelDependency, ILayoutlibVersion { - - private final String mVendorId; - private final String mVendorDisplay; - private final String mNameId; - private final String mDisplayName; - private final AndroidVersion mVersion; - private final IPkgDesc mPkgDesc; - - /** - * The helper handling the layoutlib version. - */ - private final LayoutlibVersionMixin mLayoutlibVersion; - - /** An add-on library. */ - public static class Lib { - private final String mName; - private final String mDescription; - - public Lib(String name, String description) { - mName = name; - mDescription = description; - } - - public String getName() { - return mName; - } - - public String getDescription() { - return mDescription; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((mDescription == null) ? 0 : mDescription.hashCode()); - result = prime * result + ((mName == null) ? 0 : mName.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (!(obj instanceof Lib)) { - return false; - } - Lib other = (Lib) obj; - if (mDescription == null) { - if (other.mDescription != null) { - return false; - } - } else if (!mDescription.equals(other.mDescription)) { - return false; - } - if (mName == null) { - if (other.mName != null) { - return false; - } - } else if (!mName.equals(other.mName)) { - return false; - } - return true; - } - } - - private final Lib[] mLibs; - - /** - * Creates a new add-on package from the attributes and elements of the given XML node. - * This constructor should throw an exception if the package cannot be created. - * - * @param source The {@link SdkSource} where this is loaded from. - * @param packageNode The XML element being parsed. - * @param nsUri The namespace URI of the originating XML document, to be able to deal with - * parameters that vary according to the originating XML schema. - * @param licenses The licenses loaded from the XML originating document. - */ - public AddonPackage( - SdkSource source, - Node packageNode, - String nsUri, - Map licenses) { - super(source, packageNode, nsUri, licenses); - - // --- name id/display --- - // addon-4.xsd introduces the name-id, name-display, vendor-id and vendor-display. - // These are not optional but we still need to support a fallback for older addons - // that only provide name and vendor. If the addon provides neither set of fields, - // it will simply not work as expected. - - String nameId = PackageParserUtils.getXmlString(packageNode, - SdkRepoConstants.NODE_NAME_ID); - String nameDisp = PackageParserUtils.getXmlString(packageNode, - SdkRepoConstants.NODE_NAME_DISPLAY); - String name = PackageParserUtils.getXmlString(packageNode, - SdkRepoConstants.NODE_NAME); - - // The old is equivalent to the new - if (nameDisp.isEmpty()) { - nameDisp = name; - } - - // For a missing id, we simply use a sanitized version of the display name - if (nameId.isEmpty()) { - nameId = LocalAddonPkgInfo.sanitizeDisplayToNameId(!name.isEmpty() ? name : nameDisp); - } - - assert !nameId.isEmpty(); - assert !nameDisp.isEmpty(); - - mNameId = nameId.trim(); - mDisplayName = nameDisp.trim(); - - // --- vendor id/display --- - // Same processing for vendor id vs display - - String vendorId = PackageParserUtils.getXmlString(packageNode, - SdkAddonConstants.NODE_VENDOR_ID); - String vendorDisp = PackageParserUtils.getXmlString(packageNode, - SdkAddonConstants.NODE_VENDOR_DISPLAY); - String vendor = PackageParserUtils.getXmlString(packageNode, - SdkAddonConstants.NODE_VENDOR); - - // The old is equivalent to the new - if (vendorDisp.isEmpty()) { - vendorDisp = vendor; - } - - // For a missing id, we simply use a sanitized version of the display vendor - if (vendorId.isEmpty()) { - boolean hasVendor = !vendor.isEmpty(); - vendorId = LocalAddonPkgInfo.sanitizeDisplayToNameId(hasVendor ? vendor : vendorDisp); - } - - assert !vendorId.isEmpty(); - assert !vendorDisp.isEmpty(); - - mVendorId = vendorId.trim(); - mVendorDisplay = vendorDisp.trim(); - - // --- other attributes - - int apiLevel = - PackageParserUtils.getXmlInt(packageNode, SdkAddonConstants.NODE_API_LEVEL, 0); - mVersion = new AndroidVersion(apiLevel, null /*codeName*/); - - mLibs = parseLibs( - PackageParserUtils.findChildElement(packageNode, SdkAddonConstants.NODE_LIBS)); - - mLayoutlibVersion = new LayoutlibVersionMixin(packageNode); - - mPkgDesc = setDescriptions( - PkgDesc.Builder.newAddon(mVersion, (MajorRevision) getRevision(), - new IdDisplay(mVendorId, mVendorDisplay), - new IdDisplay(mNameId, mDisplayName))) - .create(); - } - - /** - * Creates a new platform package based on an actual {@link IAndroidTarget} (which - * {@link IAndroidTarget#isPlatform()} false) from the {@link SdkManager}. - * This is used to list local SDK folders in which case there is one archive which - * URL is the actual target location. - *

- * By design, this creates a package with one and only one archive. - */ - public static Package create(IAndroidTarget target, Properties props) { - return new AddonPackage(target, props); - } - - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected AddonPackage(IAndroidTarget target, Properties props) { - this(null /*source*/, target, props); - } - - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected AddonPackage(SdkSource source, IAndroidTarget target, Properties props) { - super( source, //source - props, //properties - target.getRevision(), //revision - null, //license - target.getDescription(), //description - null, //descUrl - target.getLocation() //archiveOsPath - ); - - // --- name id/display --- - // addon-4.xsd introduces the name-id, name-display, vendor-id and vendor-display. - // These are not optional but we still need to support a fallback for older addons - // that only provide name and vendor. If the addon provides neither set of fields, - // it will simply not work as expected. - - String nameId = getProperty(props, PkgProps.ADDON_NAME_ID, ""); //$NON-NLS-1$ - String nameDisp = getProperty(props, PkgProps.ADDON_NAME_DISPLAY, ""); //$NON-NLS-1$ - String name = getProperty(props, PkgProps.ADDON_NAME, target.getName()); - - // The old is equivalent to the new - //noinspection ConstantConditions - if (nameDisp.isEmpty()) { - nameDisp = name; - } - - // For a missing id, we simply use a sanitized version of the display name - //noinspection ConstantConditions - if (nameId.isEmpty()) { - nameId = LocalAddonPkgInfo.sanitizeDisplayToNameId(!name.isEmpty() ? name : nameDisp); - } - - assert !nameId.isEmpty(); - assert !nameDisp.isEmpty(); - - mNameId = nameId.trim(); - mDisplayName = nameDisp.trim(); - - // --- vendor id/display --- - // Same processing for vendor id vs display - - String vendorId = getProperty(props, PkgProps.ADDON_VENDOR_ID, ""); //$NON-NLS-1$ - String vendorDisp = getProperty(props, PkgProps.ADDON_VENDOR_DISPLAY, ""); //$NON-NLS-1$ - String vendor = getProperty(props, PkgProps.ADDON_VENDOR, target.getVendor()); - - // The old is equivalent to the new - //noinspection ConstantConditions - if (vendorDisp.isEmpty()) { - vendorDisp = vendor; - } - - // For a missing id, we simply use a sanitized version of the display vendor - //noinspection ConstantConditions - if (vendorId.isEmpty()) { - //noinspection ConstantConditions - boolean hasVendor = !vendor.isEmpty(); - vendorId = LocalAddonPkgInfo.sanitizeDisplayToNameId(hasVendor ? vendor : vendorDisp); - } - - assert !vendorId.isEmpty(); - assert !vendorDisp.isEmpty(); - - mVendorId = vendorId.trim(); - mVendorDisplay = vendorDisp.trim(); - - // --- other attributes - - mVersion = target.getVersion(); - mLayoutlibVersion = new LayoutlibVersionMixin(props); - - List optLibs = target.getAdditionalLibraries(); - if (optLibs.isEmpty()) { - mLibs = new Lib[0]; - } else { - mLibs = new Lib[optLibs.size()]; - for (int i = 0; i < optLibs.size(); i++) { - OptionalLibrary optionalLibrary = optLibs.get(i); - mLibs[i] = new Lib(optionalLibrary.getName(), optionalLibrary.getDescription()); - } - } - - mPkgDesc = setDescriptions( - PkgDesc.Builder.newAddon(mVersion, (MajorRevision) getRevision(), - new IdDisplay(mVendorId, mVendorDisplay), - new IdDisplay(mNameId, mDisplayName))) - .create(); - } - - @Override - @NonNull - public IPkgDesc getPkgDesc() { - return mPkgDesc; - } - - /** - * Creates a broken addon which we know failed to load properly. - * - * @param archiveOsPath The absolute OS path of the addon folder. - * @param sourceProps The properties parsed from the addon's source.properties. Can be null. - * @param addonProps The properties parsed from the addon manifest (NOT the source.properties). - * @param error The error indicating why this addon failed to be loaded. - */ - public static Package createBroken( - String archiveOsPath, - Properties sourceProps, - Map addonProps, - String error) { - - - String nameId = getProperty(sourceProps, PkgProps.ADDON_NAME_ID, null); - String nameDisp = getProperty(sourceProps, PkgProps.ADDON_NAME_DISPLAY, null); - if (nameDisp == null) { - nameDisp = getProperty(sourceProps, PkgProps.ADDON_NAME_DISPLAY, null); - } - if (nameDisp == null) { - nameDisp = addonProps.get(AddonManifestIniProps.ADDON_NAME); - } - if (nameDisp == null) { - nameDisp = "Unknown"; - } - if (nameId == null) { - nameId = LocalAddonPkgInfo.sanitizeDisplayToNameId(nameDisp); - } - - String vendorId = getProperty(sourceProps, PkgProps.ADDON_VENDOR_ID, null); - String vendorDisp = getProperty(sourceProps, PkgProps.ADDON_VENDOR_DISPLAY, null); - if (vendorDisp == null) { - vendorDisp = getProperty(sourceProps, PkgProps.ADDON_VENDOR_DISPLAY, null); - } - if (vendorDisp == null) { - vendorDisp = addonProps.get(AddonManifestIniProps.ADDON_VENDOR); - } - if (vendorDisp == null) { - vendorDisp = "Unknown"; - } - if (vendorId == null) { - vendorId = LocalAddonPkgInfo.sanitizeDisplayToNameId(vendorDisp); - } - - String api = addonProps.get(AddonManifestIniProps.ADDON_API); - String revision = addonProps.get(AddonManifestIniProps.ADDON_REVISION); - - String shortDesc = String.format("%1$s by %2$s, Android API %3$s, revision %4$s [*]", - nameDisp, - vendorDisp, - api, - revision); - - String longDesc = String.format( - "%1$s\n" + - "[*] Addon failed to load: %2$s", - shortDesc, - error); - - int apiLevel = IExactApiLevelDependency.API_LEVEL_INVALID; - - try { - apiLevel = Integer.parseInt(api); - } catch(NumberFormatException ignore) {} - - int intRevision = MajorRevision.MISSING_MAJOR_REV; - try { - intRevision = Integer.parseInt(revision); - } catch (NumberFormatException ignore) {} - - IPkgDesc desc = PkgDesc.Builder - .newAddon(new AndroidVersion(apiLevel, null), - new MajorRevision(intRevision), - new IdDisplay(vendorId, vendorDisp), - new IdDisplay(nameId, nameDisp)) - .setDescriptionShort(shortDesc) - .create(); - - return new BrokenPackage(null/*props*/, shortDesc, longDesc, - IMinApiLevelDependency.MIN_API_LEVEL_NOT_SPECIFIED, - apiLevel, - archiveOsPath, - desc); - } - - @Override - public int getExactApiLevel() { - return mVersion.getApiLevel(); - } - - /** - * Save the properties of the current packages in the given {@link Properties} object. - * These properties will later be given to a constructor that takes a {@link Properties} object. - */ - @Override - public void saveProperties(Properties props) { - super.saveProperties(props); - - mVersion.saveProperties(props); - mLayoutlibVersion.saveProperties(props); - - props.setProperty(PkgProps.ADDON_NAME_ID, mNameId); - props.setProperty(PkgProps.ADDON_NAME_DISPLAY, mDisplayName); - props.setProperty(PkgProps.ADDON_VENDOR_ID, mVendorId); - props.setProperty(PkgProps.ADDON_VENDOR_DISPLAY, mVendorDisplay); - } - - /** - * Parses a element. - */ - private Lib[] parseLibs(Node libsNode) { - ArrayList libs = new ArrayList(); - - if (libsNode != null) { - String nsUri = libsNode.getNamespaceURI(); - for(Node child = libsNode.getFirstChild(); - child != null; - child = child.getNextSibling()) { - - if (child.getNodeType() == Node.ELEMENT_NODE && - nsUri.equals(child.getNamespaceURI()) && - SdkRepoConstants.NODE_LIB.equals(child.getLocalName())) { - libs.add(parseLib(child)); - } - } - } - - return libs.toArray(new Lib[libs.size()]); - } - - /** - * Parses a element from a container. - */ - private Lib parseLib(Node libNode) { - return new Lib(PackageParserUtils.getXmlString(libNode, SdkRepoConstants.NODE_NAME), - PackageParserUtils.getXmlString(libNode, SdkRepoConstants.NODE_DESCRIPTION)); - } - - /** Returns the vendor id, a string, for add-on packages. */ - @NonNull - public String getVendorId() { - return mVendorId; - } - - /** Returns the vendor, a string for display purposes. */ - @NonNull - public String getDisplayVendor() { - return mVendorDisplay; - } - - /** Returns the name id, a string, for add-on packages or for libraries. */ - @NonNull - public String getNameId() { - return mNameId; - } - - /** Returns the name, a string for display purposes. */ - @NonNull - public String getDisplayName() { - return mDisplayName; - } - - /** - * Returns the version of the platform dependency of this package. - *

- * An add-on has the same {@link AndroidVersion} as the platform it depends on. - */ - @Override @NonNull - public AndroidVersion getAndroidVersion() { - return mVersion; - } - - /** Returns the libs defined in this add-on. Can be an empty array but not null. */ - @NonNull - public Lib[] getLibs() { - return mLibs; - } - - /** - * Returns the layoutlib version. - *

- * The first integer is the API of layoublib, which should be > 0. - * It will be equal to {@link ILayoutlibVersion#LAYOUTLIB_API_NOT_SPECIFIED} (0) - * if the layoutlib version isn't specified. - *

- * The second integer is the revision for that given API. It is >= 0 - * and works as a minor revision number, incremented for the same API level. - * - * @since sdk-addon-2.xsd - */ - @NonNull - @Override - public Pair getLayoutlibVersion() { - return mLayoutlibVersion.getLayoutlibVersion(); - } - - /** - * Returns a string identifier to install this package from the command line. - * For add-ons, we use "addon-vendor-name-N" where N is the base platform API. - *

- * {@inheritDoc} - */ - @NonNull - @Override - public String installId() { - return encodeAddonName(); - } - - /** - * Returns a description of this package that is suitable for a list display. - *

- * {@inheritDoc} - */ - @Override - public String getListDescription() { - String ld = getListDisplay(); - if (!ld.isEmpty()) { - return String.format("%1$s%2$s", ld, isObsolete() ? " (Obsolete)" : ""); - } - - return String.format("%1$s%2$s", - getDisplayName(), - isObsolete() ? " (Obsolete)" : ""); - } - - /** - * Returns a short description for an {@link IDescription}. - */ - @Override - public String getShortDescription() { - String ld = getListDisplay(); - if (!ld.isEmpty()) { - return String.format("%1$s, revision %2$s%3$s", - ld, - getRevision().toShortString(), - isObsolete() ? " (Obsolete)" : ""); - } - - return String.format("%1$s, Android API %2$s, revision %3$s%4$s", - getDisplayName(), - mVersion.getApiString(), - getRevision().toShortString(), - isObsolete() ? " (Obsolete)" : ""); - } - - /** - * Returns a long description for an {@link IDescription}. - * - * The long description is whatever the XML contains for the <description> field, - * or the short description if the former is empty. - */ - @Override - public String getLongDescription() { - String s = String.format("%1$s, Android API %2$s, revision %3$s%4$s\nBy %5$s", - getDisplayName(), - mVersion.getApiString(), - getRevision().toShortString(), - isObsolete() ? " (Obsolete)" : "", //$NON-NLS-2$ - getDisplayVendor()); - - String d = getDescription(); - if (d != null && !d.isEmpty()) { - s += '\n' + d; - } - - s += String.format("\nRequires SDK Platform Android API %1$s", - mVersion.getApiString()); - return s; - } - - /** - * Computes a potential installation folder if an archive of this package were - * to be installed right away in the given SDK root. - *

- * An add-on package is typically installed in SDK/add-ons/"addon-name"-"api-level". - * The name needs to be sanitized to be acceptable as a directory name. - * However if we can find a different directory under SDK/add-ons that already - * has this add-ons installed, we'll use that one. - * - * @param osSdkRoot The OS path of the SDK root folder. - * @param sdkManager An existing SDK manager to list current platforms and addons. - * @return A new {@link File} corresponding to the directory to use to install this package. - */ - @Override - public File getInstallFolder(String osSdkRoot, SdkManager sdkManager) { - File addons = new File(osSdkRoot, SdkConstants.FD_ADDONS); - - // First find if this add-on is already installed. If so, reuse the same directory. - for (IAndroidTarget target : sdkManager.getTargets()) { - if (!target.isPlatform() && target.getVersion().equals(mVersion)) { - // Starting with addon-4.xsd, the addon source.properties differentiate - // between ids and display strings. However the addon target which relies - // on the manifest.ini does not so we need to cover both cases. - // TODO fix when we get rid of manifest.ini for addons - if ((target.getName().equals(getNameId()) && - target.getVendor().equals(getVendorId())) || - (target.getName().equals(getDisplayName()) && - target.getVendor().equals(getDisplayVendor()))) { - return new File(target.getLocation()); - } - } - } - - // Compute a folder directory using the addon declared name and vendor strings. - String name = encodeAddonName(); - - for (int i = 0; i < 100; i++) { - String name2 = i == 0 ? name : String.format("%s-%d", name, i); //$NON-NLS-1$ - File folder = new File(addons, name2); - if (!folder.exists()) { - return folder; - } - } - - // We shouldn't really get here. I mean, seriously, we tried hard enough. - return null; - } - - private String encodeAddonName() { - String name = String.format("addon-%s-%s-%s", //$NON-NLS-1$ - getNameId(), getVendorId(), mVersion.getApiString()); - name = name.toLowerCase(Locale.US); - name = name.replaceAll("[^a-z0-9_-]+", "_"); //$NON-NLS-1$ //$NON-NLS-2$ - name = name.replaceAll("_+", "_"); //$NON-NLS-1$ //$NON-NLS-2$ - return name; - } - - @Override - public boolean sameItemAs(Package pkg) { - if (pkg instanceof AddonPackage) { - AddonPackage newPkg = (AddonPackage)pkg; - - // check they are the same add-on. - if (getNameId().equals(newPkg.getNameId()) && - getAndroidVersion().equals(newPkg.getAndroidVersion())) { - // Check the vendor-id field. - if (getVendorId().equals(newPkg.getVendorId())) { - return true; - } - - // When loading addons from the v3 schema that only had a - // field, the vendor field has been converted to vendor-display so - // as a transition mechanism we should test this also. - // TODO: in a couple iterations of the SDK Manager, remove this check - // and only compare using the vendor-id field. - return getDisplayVendor().equals(newPkg.getDisplayVendor()); - } - } - - return false; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = super.hashCode(); - result = prime * result + ((mLayoutlibVersion == null) ? 0 : mLayoutlibVersion.hashCode()); - result = prime * result + Arrays.hashCode(mLibs); - result = prime * result + ((mDisplayName == null) ? 0 : mDisplayName.hashCode()); - result = prime * result + ((mVendorDisplay == null) ? 0 : mVendorDisplay.hashCode()); - result = prime * result + ((mVersion == null) ? 0 : mVersion.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (!super.equals(obj)) { - return false; - } - if (!(obj instanceof AddonPackage)) { - return false; - } - AddonPackage other = (AddonPackage) obj; - if (mLayoutlibVersion == null) { - if (other.mLayoutlibVersion != null) { - return false; - } - } else if (!mLayoutlibVersion.equals(other.mLayoutlibVersion)) { - return false; - } - if (!Arrays.equals(mLibs, other.mLibs)) { - return false; - } - if (mNameId == null) { - if (other.mNameId != null) { - return false; - } - } else if (!mNameId.equals(other.mNameId)) { - return false; - } - if (mVendorId == null) { - if (other.mVendorId != null) { - return false; - } - } else if (!mVendorId.equals(other.mVendorId)) { - return false; - } - if (mVersion == null) { - if (other.mVersion != null) { - return false; - } - } else if (!mVersion.equals(other.mVersion)) { - return false; - } - return true; - } - - /** - * For addon packages, we want to add vendor|name to the sorting key - * before the revision number. - *

- * {@inheritDoc} - */ - @Override - protected String comparisonKey() { - String s = super.comparisonKey(); - int pos = s.indexOf("|r:"); //$NON-NLS-1$ - assert pos > 0; - s = s.substring(0, pos) + - "|vid:" + getVendorId() + //$NON-NLS-1$ - "|nid:" + getNameId() + //$NON-NLS-1$ - s.substring(pos); - return s; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/BrokenPackage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/BrokenPackage.java deleted file mode 100644 index 3ee5ed28..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/BrokenPackage.java +++ /dev/null @@ -1,241 +0,0 @@ -/* - * Copyright (C) 2010 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.sdklib.internal.repository.packages; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.sdklib.SdkManager; -import com.android.sdklib.repository.IDescription; -import com.android.sdklib.internal.repository.ITaskMonitor; -import com.android.sdklib.internal.repository.archives.Archive; -import com.android.sdklib.repository.FullRevision; -import com.android.sdklib.repository.descriptors.IPkgDesc; - -import java.io.File; -import java.util.Properties; - -/** - * Represents an SDK repository package that is incomplete. - * It has a distinct icon and a specific error that is supposed to help the user on how to fix it. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class BrokenPackage extends MajorRevisionPackage - implements IExactApiLevelDependency, IMinApiLevelDependency { - - /** - * The minimal API level required by this package, if > 0, - * or {@link #MIN_API_LEVEL_NOT_SPECIFIED} if there is no such requirement. - */ - private final int mMinApiLevel; - - /** - * The exact API level required by this package, if > 0, - * or {@link #API_LEVEL_INVALID} if there is no such requirement. - */ - private final int mExactApiLevel; - - private final String mShortDescription; - private final String mLongDescription; - private final IPkgDesc mPkgDesc; - - /** - * Creates a new "broken" package that represents a package that we failed to load, - * for whatever error indicated in {@code longDescription}. - * There is also an optional API level dependency that can be specified. - *

- * By design, this creates a package with one and only one archive. - */ - BrokenPackage(@Nullable Properties props, - @NonNull String shortDescription, - @NonNull String longDescription, - int minApiLevel, - int exactApiLevel, - @Nullable String archiveOsPath, - @NonNull IPkgDesc pkgDesc) { - super( null, //source - props, //properties - 0, //revision will be taken from props - null, //license - longDescription, //description - null, //descUrl - archiveOsPath //archiveOsPath - ); - mShortDescription = shortDescription; - mLongDescription = longDescription; - mMinApiLevel = minApiLevel; - mExactApiLevel = exactApiLevel; - mPkgDesc = pkgDesc; - } - - @Override - @NonNull - public IPkgDesc getPkgDesc() { - return mPkgDesc; - } - - /** - * Save the properties of the current packages in the given {@link Properties} object. - * These properties will later be given to a constructor that takes a {@link Properties} object. - *

- * Base implementation override: We don't actually save properties for a broken package. - */ - @Override - public void saveProperties(Properties props) { - // Nop. We don't actually save properties for a broken package. - } - - /** - * Returns the minimal API level required by this package, if > 0, - * or {@link #MIN_API_LEVEL_NOT_SPECIFIED} if there is no such requirement. - */ - @Override - public int getMinApiLevel() { - return mMinApiLevel; - } - - /** - * Returns the exact API level required by this package, if > 0, - * or {@link #API_LEVEL_INVALID} if the value was missing. - */ - @Override - public int getExactApiLevel() { - return mExactApiLevel; - } - - /** - * Returns a string identifier to install this package from the command line. - * For broken packages, we return an empty string. These are not installable. - *

- * {@inheritDoc} - */ - @Override - public String installId() { - return ""; //$NON-NLS-1$ - } - - /** - * Returns a description of this package that is suitable for a list display. - *

- * {@inheritDoc} - */ - @Override - public String getListDescription() { - String ld = getListDisplay(); - if (!ld.isEmpty()) { - return String.format("%1$s%2$s", ld, isObsolete() ? " (Obsolete)" : ""); - } - - return mShortDescription; - } - - /** - * Returns a short description for an {@link IDescription}. - */ - @Override - public String getShortDescription() { - return mShortDescription; - } - - /** - * Returns a long description for an {@link IDescription}. - * - * The long description uses what was given to the constructor. - * If it's missing, it will use whatever the XML contains for the <description> field, - * or the short description if the former is empty. - */ - @Override - public String getLongDescription() { - - String s = mLongDescription; - if (s != null && !s.isEmpty()) { - return s; - } - - s = getDescription(); - if (s != null && !s.isEmpty()) { - return s; - } - return getShortDescription(); - } - - /** - * We should not be attempting to install a broken package. - * - * {@inheritDoc} - */ - @Override - public File getInstallFolder(String osSdkRoot, SdkManager sdkManager) { - // We should not be attempting to install a broken package. - return null; - } - - @Override - public boolean sameItemAs(Package pkg) { - if (pkg instanceof BrokenPackage) { - return mShortDescription.equals(((BrokenPackage) pkg).mShortDescription) && - getDescription().equals(pkg.getDescription()) && - getMinApiLevel() == ((BrokenPackage) pkg).getMinApiLevel(); - } - - return false; - } - - @Override - public boolean preInstallHook(Archive archive, - ITaskMonitor monitor, - String osSdkRoot, - File installFolder) { - // Nothing specific to do. - return super.preInstallHook(archive, monitor, osSdkRoot, installFolder); - } - - /** - * Computes a hash of the installed content (in case of successful install.) - * - * {@inheritDoc} - */ - @Override - public void postInstallHook(Archive archive, ITaskMonitor monitor, File installFolder) { - // Nothing specific to do. - super.postInstallHook(archive, monitor, installFolder); - } - - /** - * Similar to {@link BuildToolPackage#comparisonKey()}, but we need to use - * {@link #getPkgDesc} instead of {@link #getRevision()} - */ - @Override - protected String comparisonKey() { - String s = super.comparisonKey(); - FullRevision rev = getPkgDesc().getFullRevision(); - if (rev != null) { - int pos = s.indexOf("|r:"); //$NON-NLS-1$ - assert pos > 0; - String reverseSort = String.format("|rr:%1$04d.%2$04d.%3$04d.", //$NON-NLS-1$ - 9999 - rev.getMajor(), - 9999 - rev.getMinor(), - 9999 - rev.getMicro()); - - s = s.substring(0, pos) + reverseSort + s.substring(pos); - } - return s; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/BuildToolPackage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/BuildToolPackage.java deleted file mode 100644 index 2aa30aeb..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/BuildToolPackage.java +++ /dev/null @@ -1,367 +0,0 @@ -/* - * Copyright (C) 2013 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.sdklib.internal.repository.packages; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.annotations.VisibleForTesting; -import com.android.annotations.VisibleForTesting.Visibility; -import com.android.sdklib.SdkManager; -import com.android.sdklib.internal.repository.sources.SdkSource; -import com.android.sdklib.repository.FullRevision; -import com.android.sdklib.repository.FullRevision.PreviewComparison; -import com.android.sdklib.repository.IDescription; -import com.android.sdklib.repository.PkgProps; -import com.android.sdklib.repository.PreciseRevision; -import com.android.sdklib.repository.descriptors.IPkgDesc; -import com.android.sdklib.repository.descriptors.PkgDesc; - -import org.w3c.dom.Node; - -import java.io.File; -import java.util.HashSet; -import java.util.Map; -import java.util.Properties; -import java.util.Set; - -/** - * Represents a build-tool XML node in an SDK repository. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class BuildToolPackage extends FullRevisionPackage { - - /** The base value returned by {@link BuildToolPackage#installId()}. */ - private static final String INSTALL_ID_BASE = SdkConstants.FD_BUILD_TOOLS + '-'; - - private final IPkgDesc mPkgDesc; - - /** - * Creates a new build-tool package from the attributes and elements of the given XML node. - * This constructor should throw an exception if the package cannot be created. - * - * @param source The {@link SdkSource} where this is loaded from. - * @param packageNode The XML element being parsed. - * @param nsUri The namespace URI of the originating XML document, to be able to deal with - * parameters that vary according to the originating XML schema. - * @param licenses The licenses loaded from the XML originating document. - */ - public BuildToolPackage( - SdkSource source, - Node packageNode, - String nsUri, - Map licenses) { - super(source, packageNode, nsUri, licenses); - - mPkgDesc = setDescriptions(PkgDesc.Builder - .newBuildTool(getRevision()) - ) - .create(); - } - - /** - * Creates either a valid {@link BuildToolPackage} or a {@link BrokenPackage}. - *

- * If the build-tool directory contains valid properties, - * this creates a new {@link BuildToolPackage} with the reversion listed in the properties. - * Otherwise returns a new {@link BrokenPackage} with some explanation on what failed. - *

- * Note that the folder name is not enforced. A build-tool directory must have a - * a source.props with a revision property and a few expected binaries inside to be - * valid. - * - * @param buildToolDir The SDK/build-tool/revision folder - * @param props The properties located in {@code buildToolDir} or null if not found. - * @return A new {@link BuildToolPackage} or a new {@link BrokenPackage}. - */ - public static Package create(File buildToolDir, Properties props) { - String error = null; - - // Try to load the reversion from the sources.props. - // If we don't find them, the package is broken. - if (props == null) { - error = String.format("Missing file %1$s in build-tool/%2$s", - SdkConstants.FN_SOURCE_PROP, - buildToolDir.getName()); - } - - // Check we can find the revision in the source properties - FullRevision rev = null; - if (error == null) { - String revStr = getProperty(props, PkgProps.PKG_REVISION, null); - - if (revStr != null) { - try { - rev = FullRevision.parseRevision(revStr); - } catch (NumberFormatException ignore) {} - } - - if (rev == null) { - error = String.format("Missing revision property in %1$s", - SdkConstants.FN_SOURCE_PROP); - } - } - - if (error == null) { - // Check the directory contains the expected binaries. - - if (!buildToolDir.isDirectory()) { - error = String.format("build-tool/%1$s folder is missing", - buildToolDir.getName()); - } else { - File[] files = buildToolDir.listFiles(); - if (files == null || files.length == 0) { - error = String.format("build-tool/%1$s folder is empty", - buildToolDir.getName()); - } else { - Set names = new HashSet(); - for (File file : files) { - names.add(file.getName()); - } - for (String name : new String[] { SdkConstants.FN_AAPT, - SdkConstants.FN_AIDL, - SdkConstants.FN_DX } ) { - if (!names.contains(name)) { - if (error == null) { - error = String.format("build-tool/%1$s folder is missing ", - buildToolDir.getName()); - } else { - error += ", "; - } - error += name; - } - } - } - } - } - - if (error == null && rev != null) { - BuildToolPackage pkg = new BuildToolPackage( - null, //source - props, - 0, //revision (extracted from props) - null, //license - null, //description - null, //descUrl - buildToolDir.getAbsolutePath()); - - if (pkg.hasCompatibleArchive()) { - return pkg; - } else { - error = "Package is not compatible with current OS"; - } - } - - - StringBuilder sb = new StringBuilder("Broken Build-Tools Package"); - if (rev != null) { - sb.append(String.format(", revision %1$s", rev.toShortString())); - } - - String shortDesc = sb.toString(); - - if (error != null) { - sb.append('\n').append(error); - } - - String longDesc = sb.toString(); - - IPkgDesc desc = PkgDesc.Builder - .newBuildTool(rev != null ? rev : new FullRevision(FullRevision.MISSING_MAJOR_REV)) - .setDescriptionShort(shortDesc) - .create(); - - return new BrokenPackage(props, shortDesc, longDesc, - IMinApiLevelDependency.MIN_API_LEVEL_NOT_SPECIFIED, - IExactApiLevelDependency.API_LEVEL_INVALID, - buildToolDir.getAbsolutePath(), - desc); - } - - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected BuildToolPackage( - SdkSource source, - Properties props, - int revision, - String license, - String description, - String descUrl, - String archiveOsPath) { - super(source, - props, - revision, - license, - description, - descUrl, - archiveOsPath); - - mPkgDesc = setDescriptions(PkgDesc.Builder.newBuildTool(getRevision())).create(); - } - - @Override - @NonNull - public IPkgDesc getPkgDesc() { - return mPkgDesc; - } - - /** - * Returns a string identifier to install this package from the command line. - * For build-tools, we use "build-tools-" followed by the full revision string - * where spaces are changed to underscore to be more script-friendly. - *

- * {@inheritDoc} - */ - @Override - public String installId() { - return getPkgDesc().getInstallId(); - } - - /** - * Returns a description of this package that is suitable for a list display. - *

- * {@inheritDoc} - */ - @Override - public String getListDescription() { - String ld = getListDisplay(); - if (!ld.isEmpty()) { - return String.format("%1$s%2$s", ld, isObsolete() ? " (Obsolete)" : ""); - } - - return String.format("Android SDK Build-tools%1$s", - isObsolete() ? " (Obsolete)" : ""); - } - - /** - * Returns a short description for an {@link IDescription}. - */ - @Override - public String getShortDescription() { - String ld = getListDisplay(); - if (!ld.isEmpty()) { - return String.format("%1$s, revision %2$s%3$s", - ld, - getRevision().toShortString(), - isObsolete() ? " (Obsolete)" : ""); - } - - return String.format("Android SDK Build-tools, revision %1$s%2$s", - getRevision().toShortString(), - isObsolete() ? " (Obsolete)" : ""); - } - - /** Returns a long description for an {@link IDescription}. */ - @Override - public String getLongDescription() { - String s = getDescription(); - if (s == null || s.isEmpty()) { - s = getShortDescription(); - } - - if (s.indexOf("revision") == -1) { - s += String.format("\nRevision %1$s%2$s", - getRevision().toShortString(), - isObsolete() ? " (Obsolete)" : ""); - } - - return s; - } - - /** - * Computes a potential installation folder if an archive of this package were - * to be installed right away in the given SDK root. - *

- * A build-tool package is typically installed in SDK/build-tools/revision. - * Revision spaces are replaced by underscores for ease of use in command-line. - * - * @param osSdkRoot The OS path of the SDK root folder. - * @param sdkManager An existing SDK manager to list current platforms and addons. - * @return A new {@link File} corresponding to the directory to use to install this package. - */ - @Override - public File getInstallFolder(String osSdkRoot, SdkManager sdkManager) { - File folder = new File(osSdkRoot, SdkConstants.FD_BUILD_TOOLS); - StringBuilder sb = new StringBuilder(); - - PreciseRevision revision = getPkgDesc().getPreciseRevision(); - int[] version = revision.toIntArray(false); - for (int i = 0; i < version.length; i++) { - sb.append(version[i]); - if (i != version.length - 1) { - sb.append('.'); - } - } - if (getPkgDesc().getPreciseRevision().isPreview()) { - sb.append(PkgDesc.PREVIEW_SUFFIX); - } - - folder = new File(folder, sb.toString()); - return folder; - } - - /** - * Check whether 2 platform-tool packages are the same and have the - * same preview bit. - */ - @Override - public boolean sameItemAs(Package pkg) { - // Implementation note: here we don't want to care about the preview number - // so we ignore the preview when calling sameItemAs(); however we do care - // about both packages being either previews or not previews (i.e. the type - // must match but the preview number doesn't need to.) - // The end result is that a package such as "1.2 rc 4" will be an update for "1.2 rc 3". - return sameItemAs(pkg, PreviewComparison.COMPARE_TYPE); - } - - @Override - public boolean sameItemAs(Package pkg, PreviewComparison comparePreview) { - // Contrary to other package types, build-tools do not "update themselves" - // so 2 build tools with 2 different revisions are not the same item. - if (pkg instanceof BuildToolPackage) { - BuildToolPackage rhs = (BuildToolPackage) pkg; - return rhs.getRevision().compareTo(getRevision(), comparePreview) == 0; - } - return false; - } - - /** - * For build-tool package use their revision number like version numbers and - * we want them sorted from higher to lower. To do that, insert a fake revision - * number using 9999-value into the sorting key. - *

- * {@inheritDoc} - */ - @Override - protected String comparisonKey() { - String s = super.comparisonKey(); - int pos = s.indexOf("|r:"); //$NON-NLS-1$ - assert pos > 0; - - FullRevision rev = getRevision(); - String reverseSort = String.format("|rr:%1$04d.%2$04d.%3$04d.", //$NON-NLS-1$ - 9999 - rev.getMajor(), - 9999 - rev.getMinor(), - 9999 - rev.getMicro()); - - s = s.substring(0, pos) + reverseSort + s.substring(pos); - return s; - } - -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/DocPackage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/DocPackage.java deleted file mode 100644 index 3582f5b1..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/DocPackage.java +++ /dev/null @@ -1,333 +0,0 @@ -/* - * 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.sdklib.internal.repository.packages; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.sdklib.AndroidVersion; -import com.android.sdklib.SdkManager; -import com.android.sdklib.internal.repository.sources.SdkSource; -import com.android.sdklib.repository.IDescription; -import com.android.sdklib.repository.MajorRevision; -import com.android.sdklib.repository.SdkRepoConstants; -import com.android.sdklib.repository.descriptors.IPkgDesc; -import com.android.sdklib.repository.descriptors.PkgDesc; - -import org.w3c.dom.Node; - -import java.io.File; -import java.util.Map; -import java.util.Properties; - -/** - * Represents a doc XML node in an SDK repository. - *

- * Note that a doc package has a version and thus implements {@link IAndroidVersionProvider}. - * However there is no mandatory dependency that limits installation so this does not - * implement {@link IPlatformDependency}. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class DocPackage extends MajorRevisionPackage implements IAndroidVersionProvider { - - private final AndroidVersion mVersion; - private final IPkgDesc mPkgDesc; - - /** - * Creates a new doc package from the attributes and elements of the given XML node. - * This constructor should throw an exception if the package cannot be created. - * - * @param source The {@link SdkSource} where this is loaded from. - * @param packageNode The XML element being parsed. - * @param nsUri The namespace URI of the originating XML document, to be able to deal with - * parameters that vary according to the originating XML schema. - * @param licenses The licenses loaded from the XML originating document. - */ - public DocPackage(SdkSource source, - Node packageNode, - String nsUri, - Map licenses) { - super(source, packageNode, nsUri, licenses); - - int apiLevel = - PackageParserUtils.getXmlInt (packageNode, SdkRepoConstants.NODE_API_LEVEL, 0); - String codeName = - PackageParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_CODENAME); - if (codeName.isEmpty()) { - codeName = null; - } - mVersion = new AndroidVersion(apiLevel, codeName); - - mPkgDesc = setDescriptions( - PkgDesc.Builder.newDoc(mVersion, (MajorRevision) getRevision())) - .create(); - } - - /** - * Manually create a new package with one archive and the given attributes. - * This is used to create packages from local directories in which case there must be - * one archive which URL is the actual target location. - *

- * By design, this creates a package with one and only one archive. - */ - public static Package create(SdkSource source, - Properties props, - int apiLevel, - String codename, - int revision, - String license, - String description, - String descUrl, - String archiveOsPath) { - return new DocPackage(source, props, apiLevel, codename, revision, license, description, - descUrl, archiveOsPath); - } - - private DocPackage(SdkSource source, - Properties props, - int apiLevel, - String codename, - int revision, - String license, - String description, - String descUrl, - String archiveOsPath) { - super(source, - props, - revision, - license, - description, - descUrl, - archiveOsPath); - mVersion = new AndroidVersion(props, apiLevel, codename); - - mPkgDesc = setDescriptions(PkgDesc.Builder.newDoc(mVersion, (MajorRevision) getRevision())) - .create(); - } - - @Override - @NonNull - public IPkgDesc getPkgDesc() { - return mPkgDesc; - } - - /** - * Save the properties of the current packages in the given {@link Properties} object. - * These properties will later be give the constructor that takes a {@link Properties} object. - */ - @Override - public void saveProperties(Properties props) { - super.saveProperties(props); - - mVersion.saveProperties(props); - } - - /** - * Returns the version, for platform, add-on and doc packages. - * Can be 0 if this is a local package of unknown api-level. - */ - @Override @NonNull - public AndroidVersion getAndroidVersion() { - return mVersion; - } - - /** - * Returns a string identifier to install this package from the command line. - * For docs, we use "doc-N" where N is the API or the preview codename. - *

- * {@inheritDoc} - */ - @Override - public String installId() { - return "doc-" + mVersion.getApiString(); //$NON-NLS-1$ - } - - /** - * Returns a description of this package that is suitable for a list display. - *

- * {@inheritDoc} - */ - @Override - public String getListDescription() { - String ld = getListDisplay(); - if (!ld.isEmpty()) { - return String.format("%1$s%2$s", ld, isObsolete() ? " (Obsolete)" : ""); - } - if (mVersion.isPreview()) { - return String.format("Documentation for Android '%1$s' Preview SDK%2$s", - mVersion.getCodename(), - isObsolete() ? " (Obsolete)" : ""); - } else { - return String.format("Documentation for Android SDK%2$s", - mVersion.getApiLevel(), - isObsolete() ? " (Obsolete)" : ""); - } - } - - /** - * Returns a short description for an {@link IDescription}. - */ - @Override - public String getShortDescription() { - String ld = getListDisplay(); - if (!ld.isEmpty()) { - return String.format("%1$s, revision %2$s%3$s", - ld, - getRevision().toShortString(), - isObsolete() ? " (Obsolete)" : ""); - } - - if (mVersion.isPreview()) { - return String.format("Documentation for Android '%1$s' Preview SDK, revision %2$s%3$s", - mVersion.getCodename(), - getRevision().toShortString(), - isObsolete() ? " (Obsolete)" : ""); - } else { - return String.format("Documentation for Android SDK, API %1$d, revision %2$s%3$s", - mVersion.getApiLevel(), - getRevision().toShortString(), - isObsolete() ? " (Obsolete)" : ""); - } - } - - /** - * Returns a long description for an {@link IDescription}. - * - * The long description is whatever the XML contains for the <description> field, - * or the short description if the former is empty. - */ - @Override - public String getLongDescription() { - String s = getDescription(); - if (s == null || s.isEmpty()) { - s = getShortDescription(); - } - - if (s.indexOf("revision") == -1) { - s += String.format("\nRevision %1$s%2$s", - getRevision().toShortString(), - isObsolete() ? " (Obsolete)" : ""); - } - - return s; - } - - /** - * Computes a potential installation folder if an archive of this package were - * to be installed right away in the given SDK root. - *

- * A "doc" package should always be located in SDK/docs. - * - * @param osSdkRoot The OS path of the SDK root folder. - * @param sdkManager An existing SDK manager to list current platforms and addons. - * @return A new {@link File} corresponding to the directory to use to install this package. - */ - @Override - public File getInstallFolder(String osSdkRoot, SdkManager sdkManager) { - return new File(osSdkRoot, SdkConstants.FD_DOCS); - } - - /** - * Consider doc packages to be the same if they cover the same API level, - * regardless of their revision number. - */ - @Override - public boolean sameItemAs(Package pkg) { - if (pkg instanceof DocPackage) { - AndroidVersion rev2 = ((DocPackage) pkg).getAndroidVersion(); - return this.getAndroidVersion().equals(rev2); - } - - return false; - } - - /** - * {@inheritDoc} - *


- * Doc packages are a bit different since there can only be one doc installed at - * the same time. - *

- * We now consider that docs for different APIs are NOT updates, e.g. doc for API N+1 - * is no longer considered an update for doc API N. - * However docs that have the same API version (API level + codename) are considered - * updates if they have a higher revision number (so 15 rev 2 is an update for 15 rev 1, - * but is not an update for 14 rev 1.) - */ - @Override - public UpdateInfo canBeUpdatedBy(Package replacementPackage) { - // check they are the same kind of object - if (!(replacementPackage instanceof DocPackage)) { - return UpdateInfo.INCOMPATIBLE; - } - - DocPackage replacementDoc = (DocPackage)replacementPackage; - - AndroidVersion replacementVersion = replacementDoc.getAndroidVersion(); - - // Check if they're the same exact (api and codename) - if (replacementVersion.equals(mVersion)) { - // exact same version, so check the revision level - if (replacementPackage.getRevision().compareTo(this.getRevision()) > 0) { - return UpdateInfo.UPDATE; - } - } else { - // not the same version? we check if they have the same api level and the new one - // is a preview, in which case it's also an update (since preview have the api level - // of the _previous_ version.) - if (replacementVersion.getApiLevel() == mVersion.getApiLevel() && - replacementVersion.isPreview()) { - return UpdateInfo.UPDATE; - } - } - - // not an upgrade but not incompatible either. - return UpdateInfo.NOT_UPDATE; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = super.hashCode(); - result = prime * result + ((mVersion == null) ? 0 : mVersion.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (!super.equals(obj)) { - return false; - } - if (!(obj instanceof DocPackage)) { - return false; - } - DocPackage other = (DocPackage) obj; - if (mVersion == null) { - if (other.mVersion != null) { - return false; - } - } else if (!mVersion.equals(other.mVersion)) { - return false; - } - return true; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/ExtraPackage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/ExtraPackage.java deleted file mode 100644 index fccbd211..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/ExtraPackage.java +++ /dev/null @@ -1,692 +0,0 @@ -/* - * 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.sdklib.internal.repository.packages; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.annotations.VisibleForTesting; -import com.android.annotations.VisibleForTesting.Visibility; -import com.android.sdklib.SdkManager; -import com.android.sdklib.internal.repository.LocalSdkParser; -import com.android.sdklib.internal.repository.NullTaskMonitor; -import com.android.sdklib.internal.repository.archives.Archive; -import com.android.sdklib.internal.repository.sources.SdkSource; -import com.android.sdklib.repository.FullRevision; -import com.android.sdklib.repository.IDescription; -import com.android.sdklib.repository.PkgProps; -import com.android.sdklib.repository.RepoConstants; -import com.android.sdklib.repository.descriptors.IPkgDescExtra; -import com.android.sdklib.repository.descriptors.IdDisplay; -import com.android.sdklib.repository.descriptors.PkgDesc; -import com.android.sdklib.repository.descriptors.PkgDescExtra; -import com.android.sdklib.repository.local.LocalExtraPkgInfo; -import com.android.utils.NullLogger; -import org.w3c.dom.Node; - -import java.io.File; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Map; -import java.util.Properties; -import java.util.regex.Pattern; - -/** - * Represents a extra XML node in an SDK repository. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class ExtraPackage extends NoPreviewRevisionPackage - implements IMinApiLevelDependency, IMinToolsDependency { - - /** Mixin handling the min-tools dependency. */ - private final MinToolsMixin mMinToolsMixin; - - /** - * The extra display name. Used in the UI to represent the package. It can be anything. - */ - private final String mDisplayName; - - /** - * The vendor id + name. - * The id is a simple alphanumeric string [a-zA-Z0-9_-]. - * The display name is used in the UI to represent the vendor. It can be anything. - */ - private final IdDisplay mVendor; - - /** - * The sub-folder name. It must be a non-empty single-segment path. - */ - private final String mPath; - - /** - * The optional old_paths, if any. If present, this is a list of old "path" values that - * we'd like to migrate to the current "path" name for this extra. - */ - private final String mOldPaths; - - /** - * The minimal API level required by this extra package, if > 0, - * or {@link #MIN_API_LEVEL_NOT_SPECIFIED} if there is no such requirement. - */ - private final int mMinApiLevel; - - /** - * The project-files listed by this extra package. - * The array can be empty but not null. - */ - private final String[] mProjectFiles; - - private final IPkgDescExtra mPkgDesc; - - /** - * Creates a new tool package from the attributes and elements of the given XML node. - * This constructor should throw an exception if the package cannot be created. - * - * @param source The {@link SdkSource} where this is loaded from. - * @param packageNode The XML element being parsed. - * @param nsUri The namespace URI of the originating XML document, to be able to deal with - * parameters that vary according to the originating XML schema. - * @param licenses The licenses loaded from the XML originating document. - */ - public ExtraPackage( - SdkSource source, - Node packageNode, - String nsUri, - Map licenses) { - super(source, packageNode, nsUri, licenses); - - mMinToolsMixin = new MinToolsMixin(packageNode); - - mPath = PackageParserUtils.getXmlString(packageNode, RepoConstants.NODE_PATH); - - // Read name-display, vendor-display and vendor-id, introduced in addon-4.xsd. - // These are not optional, they are mandatory in addon-4 but we still treat them - // as optional so that we can fallback on using which was the only one - // defined in addon-3.xsd. - String name = - PackageParserUtils.getXmlString(packageNode, RepoConstants.NODE_NAME_DISPLAY); - String vname = - PackageParserUtils.getXmlString(packageNode, RepoConstants.NODE_VENDOR_DISPLAY); - String vid = - PackageParserUtils.getXmlString(packageNode, RepoConstants.NODE_VENDOR_ID); - - if (vid.isEmpty()) { - // If vid is missing, use the old attribute. - // Note that in a valid XML, vendor-id cannot be an empty string. - // The only reason vid can be empty is when is missing, which - // happens in an addon-3 schema, in which case the old needs to be used. - String vendor = PackageParserUtils.getXmlString(packageNode, RepoConstants.NODE_VENDOR); - vid = sanitizeLegacyVendor(vendor); - if (vname.isEmpty()) { - vname = vendor; - } - } - if (vname.isEmpty()) { - // The vendor-display name can be empty, in which case we use the vendor-id. - vname = vid; - } - mVendor = new IdDisplay(vid.trim(), vname.trim()); - - if (name.isEmpty()) { - // If name is missing, use the attribute as done in an addon-3 schema. - name = LocalExtraPkgInfo.getPrettyName(mVendor, mPath); - } - mDisplayName = name.trim(); - - mMinApiLevel = PackageParserUtils.getXmlInt( - packageNode, RepoConstants.NODE_MIN_API_LEVEL, MIN_API_LEVEL_NOT_SPECIFIED); - - mProjectFiles = parseProjectFiles( - PackageParserUtils.findChildElement(packageNode, RepoConstants.NODE_PROJECT_FILES)); - - mOldPaths = PackageParserUtils.getXmlString(packageNode, RepoConstants.NODE_OLD_PATHS); - - mPkgDesc = - (IPkgDescExtra)setDescriptions(PkgDesc.Builder.newExtra(mVendor, mPath, mDisplayName, - getOldPaths(), getRevision())).create(); - } - - private String[] parseProjectFiles(Node projectFilesNode) { - ArrayList paths = new ArrayList(); - - if (projectFilesNode != null) { - String nsUri = projectFilesNode.getNamespaceURI(); - for(Node child = projectFilesNode.getFirstChild(); - child != null; - child = child.getNextSibling()) { - - if (child.getNodeType() == Node.ELEMENT_NODE && - nsUri.equals(child.getNamespaceURI()) && - RepoConstants.NODE_PATH.equals(child.getLocalName())) { - String path = child.getTextContent(); - if (path != null) { - path = path.trim(); - if (!path.isEmpty()) { - paths.add(path); - } - } - } - } - } - - return paths.toArray(new String[paths.size()]); - } - - /** - * Manually create a new package with one archive and the given attributes or properties. - * This is used to create packages from local directories in which case there must be - * one archive which URL is the actual target location. - *

- * By design, this creates a package with one and only one archive. - */ - public static Package create(SdkSource source, - Properties props, - String vendor, - String path, - int revision, - String license, - String description, - String descUrl, - String archiveOsPath) { - ExtraPackage ep = new ExtraPackage(source, props, vendor, path, revision, license, - description, descUrl, archiveOsPath); - return ep; - } - - /** - * Constructor used to create a mock {@link ExtraPackage}. - * Most of the attributes here are optional. - * When not defined, they will be extracted from the {@code props} properties. - */ - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected ExtraPackage(SdkSource source, - Properties props, - String vendorId, - String path, - int revision, - String license, - String description, - String descUrl, - String archiveOsPath) { - super(source, - props, - revision, - license, - description, - descUrl, - archiveOsPath); - - mMinToolsMixin = new MinToolsMixin( - source, - props, - revision, - license, - description, - descUrl, - archiveOsPath); - - // The path argument comes before whatever could be in the properties - mPath = path != null ? path : getProperty(props, PkgProps.EXTRA_PATH, path); - - String name = getProperty(props, PkgProps.EXTRA_NAME_DISPLAY, ""); //$NON-NLS-1$ - String vname = getProperty(props, PkgProps.EXTRA_VENDOR_DISPLAY, ""); //$NON-NLS-1$ - String vid = vendorId != null ? vendorId : - getProperty(props, PkgProps.EXTRA_VENDOR_ID, ""); //$NON-NLS-1$ - - if (vid == null || vid.isEmpty()) { - // If vid is missing, use the old attribute. - // did not exist prior to schema repo-v3 and tools r8. - String vendor = getProperty(props, PkgProps.EXTRA_VENDOR, ""); //$NON-NLS-1$ - vid = sanitizeLegacyVendor(vendor); - if (vname == null || vname.isEmpty()) { - vname = vendor; - } - } - if (vname == null || vname.isEmpty()) { - // The vendor-display name can be empty, in which case we use the vendor-id. - vname = vid; - } - mVendor = new IdDisplay(vid.trim(), vname.trim()); - - if (name == null || name.isEmpty()) { - // If name is missing, use the attribute as done in an addon-3 schema. - name = LocalExtraPkgInfo.getPrettyName(mVendor, mPath); - } - mDisplayName = name.trim(); - - mOldPaths = getProperty(props, PkgProps.EXTRA_OLD_PATHS, null); - - mMinApiLevel = getPropertyInt(props, PkgProps.EXTRA_MIN_API_LEVEL, - MIN_API_LEVEL_NOT_SPECIFIED); - - String projectFiles = getProperty(props, PkgProps.EXTRA_PROJECT_FILES, null); - ArrayList filePaths = new ArrayList(); - if (projectFiles != null && !projectFiles.isEmpty()) { - for (String filePath : projectFiles.split(Pattern.quote(File.pathSeparator))) { - filePath = filePath.trim(); - if (!filePath.isEmpty()) { - filePaths.add(filePath); - } - } - } - - mProjectFiles = filePaths.toArray(new String[filePaths.size()]); - - mPkgDesc = (IPkgDescExtra) setDescriptions(PkgDesc.Builder - .newExtra(mVendor, mPath, mDisplayName, getOldPaths(), getRevision())) - .create(); - } - - @Override - @NonNull - public IPkgDescExtra getPkgDesc() { - return mPkgDesc; - } - - /** - * Save the properties of the current packages in the given {@link Properties} object. - * These properties will later be give the constructor that takes a {@link Properties} object. - */ - @Override - public void saveProperties(Properties props) { - super.saveProperties(props); - mMinToolsMixin.saveProperties(props); - - props.setProperty(PkgProps.EXTRA_PATH, mPath); - props.setProperty(PkgProps.EXTRA_NAME_DISPLAY, mDisplayName); - props.setProperty(PkgProps.EXTRA_VENDOR_DISPLAY, mVendor.getDisplay()); - props.setProperty(PkgProps.EXTRA_VENDOR_ID, mVendor.getId()); - - if (getMinApiLevel() != MIN_API_LEVEL_NOT_SPECIFIED) { - props.setProperty(PkgProps.EXTRA_MIN_API_LEVEL, Integer.toString(getMinApiLevel())); - } - - if (mProjectFiles.length > 0) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < mProjectFiles.length; i++) { - if (i > 0) { - sb.append(File.pathSeparatorChar); - } - sb.append(mProjectFiles[i]); - } - props.setProperty(PkgProps.EXTRA_PROJECT_FILES, sb.toString()); - } - - if (mOldPaths != null && !mOldPaths.isEmpty()) { - props.setProperty(PkgProps.EXTRA_OLD_PATHS, mOldPaths); - } - } - - /** - * The minimal revision of the tools package required by this extra package, if > 0, - * or {@link #MIN_TOOLS_REV_NOT_SPECIFIED} if there is no such requirement. - */ - @Override - public FullRevision getMinToolsRevision() { - return mMinToolsMixin.getMinToolsRevision(); - } - - /** - * Returns the minimal API level required by this extra package, if > 0, - * or {@link #MIN_API_LEVEL_NOT_SPECIFIED} if there is no such requirement. - */ - @Override - public int getMinApiLevel() { - return mMinApiLevel; - } - - /** - * The project-files listed by this extra package. - * The array can be empty but not null. - *

- * IMPORTANT: directory separators are NOT translated and may not match - * the {@link File#separatorChar} of the current platform. It's up to the - * user to adequately interpret the paths. - * Similarly, no guarantee is made on the validity of the paths. - * Users are expected to apply all usual sanity checks such as removing - * "./" and "../" and making sure these paths don't reference files outside - * of the installed archive. - * - * @since sdk-repository-4.xsd or sdk-addon-2.xsd - */ - public String[] getProjectFiles() { - return mProjectFiles; - } - - /** - * Returns the old_paths, a list of obsolete path names for the extra package. - *

- * These can be used by the installer to migrate an extra package using one of the - * old paths into the new path. - *

- * These can also be used to recognize "old" renamed packages as the same as - * the current one. - * - * @return A list of old paths. Can be empty but not null. - */ - public String[] getOldPaths() { - return PkgDescExtra.convertOldPaths(mOldPaths); - } - - /** - * Returns the sanitized path folder name. It is a single-segment path. - *

- * The package is installed in SDK/extras/vendor_name/path_name. - */ - public String getPath() { - // The XSD specifies the XML vendor and path should only contain [a-zA-Z0-9]+ - // and cannot be empty. Let's be defensive and enforce that anyway since things - // like "____" are still valid values that we don't want to allow. - - // Sanitize the path - String path = mPath.replaceAll("[^a-zA-Z0-9-]+", "_"); //$NON-NLS-1$ - if (path.isEmpty() || path.equals("_")) { //$NON-NLS-1$ - int h = path.hashCode(); - path = String.format("extra%08x", h); //$NON-NLS-1$ - } - - return path; - } - - /** - * Returns the vendor id. - */ - public String getVendorId() { - return mVendor.getId(); - } - - public String getVendorDisplay() { - return mVendor.getDisplay(); - } - - public String getDisplayName() { - return mDisplayName; - } - - /** Transforms the legacy vendor name into a usable vendor id. */ - private String sanitizeLegacyVendor(String vendorDisplay) { - // The XSD specifies the XML vendor and path should only contain [a-zA-Z0-9]+ - // and cannot be empty. Let's be defensive and enforce that anyway since things - // like "____" are still valid values that we don't want to allow. - - if (vendorDisplay != null && !vendorDisplay.isEmpty()) { - String vendor = vendorDisplay.trim(); - // Sanitize the vendor - vendor = vendor.replaceAll("[^a-zA-Z0-9-]+", "_"); //$NON-NLS-1$ - if (vendor.equals("_")) { //$NON-NLS-1$ - int h = vendor.hashCode(); - vendor = String.format("vendor%08x", h); //$NON-NLS-1$ - } - - return vendor; - } - - return ""; //$NON-NLS-1$ - - } - - /** - * Returns a string identifier to install this package from the command line. - * For extras, we use "extra-vendor-path". - *

- * {@inheritDoc} - */ - @Override - public String installId() { - return String.format("extra-%1$s-%2$s", //$NON-NLS-1$ - getVendorId(), - getPath()); - } - - /** - * Returns a description of this package that is suitable for a list display. - *

- * {@inheritDoc} - */ - @Override - public String getListDescription() { - String ld = getListDisplay(); - if (!ld.isEmpty()) { - return String.format("%1$s%2$s", ld, isObsolete() ? " (Obsolete)" : ""); - } - - String s = String.format("%1$s%2$s", - getDisplayName(), - isObsolete() ? " (Obsolete)" : ""); //$NON-NLS-2$ - - return s; - } - - /** - * Returns a short description for an {@link IDescription}. - */ - @Override - public String getShortDescription() { - String ld = getListDisplay(); - if (!ld.isEmpty()) { - return String.format("%1$s, revision %2$s%3$s", - ld, - getRevision().toShortString(), - isObsolete() ? " (Obsolete)" : ""); - } - - String s = String.format("%1$s, revision %2$s%3$s", - getDisplayName(), - getRevision().toShortString(), - isObsolete() ? " (Obsolete)" : ""); //$NON-NLS-2$ - - return s; - } - - /** - * Returns a long description for an {@link IDescription}. - * - * The long description is whatever the XML contains for the <description> field, - * or the short description if the former is empty. - */ - @Override - public String getLongDescription() { - String s = String.format("%1$s, revision %2$s%3$s\nBy %4$s", - getDisplayName(), - getRevision().toShortString(), - isObsolete() ? " (Obsolete)" : "", //$NON-NLS-2$ - getVendorDisplay()); - - String d = getDescription(); - if (d != null && !d.isEmpty()) { - s += '\n' + d; - } - - if (!getMinToolsRevision().equals(MIN_TOOLS_REV_NOT_SPECIFIED)) { - s += String.format("\nRequires tools revision %1$s", - getMinToolsRevision().toShortString()); - } - - if (getMinApiLevel() != MIN_API_LEVEL_NOT_SPECIFIED) { - s += String.format("\nRequires SDK Platform Android API %1$s", getMinApiLevel()); - } - - File localPath = getLocalArchivePath(); - if (localPath != null) { - // For a local archive, also put the install path in the long description. - // This should help users locate the extra on their drive. - s += String.format("\nLocation: %1$s", localPath.getAbsolutePath()); - } else { - // For a non-installed archive, indicate where it would be installed. - s += String.format("\nInstall path: %1$s", - getInstallSubFolder(null/*sdk root*/).getPath()); - } - - return s; - } - - /** - * Computes a potential installation folder if an archive of this package were - * to be installed right away in the given SDK root. - *

- * A "tool" package should always be located in SDK/tools. - * - * @param osSdkRoot The OS path of the SDK root folder. Must NOT be null. - * @param sdkManager An existing SDK manager to list current platforms and addons. - * Not used in this implementation. - * @return A new {@link File} corresponding to the directory to use to install this package. - */ - @Override - public File getInstallFolder(String osSdkRoot, SdkManager sdkManager) { - - // First find if this extra is already installed. If so, reuse the same directory. - LocalSdkParser localParser = new LocalSdkParser(); - Package[] pkgs = localParser.parseSdk( - osSdkRoot, - sdkManager, - LocalSdkParser.PARSE_EXTRAS, - new NullTaskMonitor(NullLogger.getLogger())); - - for (Package pkg : pkgs) { - if (sameItemAs(pkg) && pkg instanceof ExtraPackage) { - File localPath = ((ExtraPackage) pkg).getLocalArchivePath(); - if (localPath != null) { - return localPath; - } - } - } - - return getInstallSubFolder(osSdkRoot); - } - - /** - * Computes the "sub-folder" install path, relative to the given SDK root. - * For an extra package, this is generally ".../extra/vendor-id/path". - * - * @param osSdkRoot The OS path of the SDK root folder if known. - * This CAN be null, in which case the path will start at /extra. - * @return Either /extra/vendor/path or sdk-root/extra/vendor-id/path. - */ - private File getInstallSubFolder(@Nullable String osSdkRoot) { - // The /extras dir at the root of the SDK - File path = new File(osSdkRoot, SdkConstants.FD_EXTRAS); - - String vendor = getVendorId(); - if (vendor != null && !vendor.isEmpty()) { - path = new File(path, vendor); - } - - String name = getPath(); - if (name != null && !name.isEmpty()) { - path = new File(path, name); - } - - return path; - } - - @Override - public boolean sameItemAs(Package pkg) { - // Extra packages are similar if they have the same path and vendor - if (pkg instanceof ExtraPackage) { - ExtraPackage ep = (ExtraPackage) pkg; - return PkgDescExtra.compatibleVendorAndPath(mPkgDesc, ep.mPkgDesc); - } - - return false; - } - - /** - * For extra packages, we want to add vendor|path to the sorting key - * before the revision number. - *

- * {@inheritDoc} - */ - @Override - protected String comparisonKey() { - String s = super.comparisonKey(); - int pos = s.indexOf("|r:"); //$NON-NLS-1$ - assert pos > 0; - s = s.substring(0, pos) + - "|ve:" + getVendorId() + //$NON-NLS-1$ - "|pa:" + getPath() + //$NON-NLS-1$ - s.substring(pos); - return s; - } - - // --- - - /** - * If this package is installed, returns the install path of the archive if valid. - * Returns null if not installed or if the path does not exist. - */ - private File getLocalArchivePath() { - Archive[] archives = getArchives(); - if (archives.length == 1 && archives[0].isLocal()) { - File path = new File(archives[0].getLocalOsPath()); - if (path.isDirectory()) { - return path; - } - } - - return null; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = mMinToolsMixin.hashCode(super.hashCode()); - result = prime * result + mMinApiLevel; - result = prime * result + ((mPath == null) ? 0 : mPath.hashCode()); - result = prime * result + Arrays.hashCode(mProjectFiles); - result = prime * result + ((mVendor == null) ? 0 : mVendor.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (!super.equals(obj)) { - return false; - } - if (!(obj instanceof ExtraPackage)) { - return false; - } - ExtraPackage other = (ExtraPackage) obj; - if (mMinApiLevel != other.mMinApiLevel) { - return false; - } - if (mPath == null) { - if (other.mPath != null) { - return false; - } - } else if (!mPath.equals(other.mPath)) { - return false; - } - if (!Arrays.equals(mProjectFiles, other.mProjectFiles)) { - return false; - } - if (mVendor == null) { - if (other.mVendor != null) { - return false; - } - } else if (!mVendor.equals(other.mVendor)) { - return false; - } - return mMinToolsMixin.equals(obj); - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/FullRevisionPackage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/FullRevisionPackage.java deleted file mode 100644 index 5e9b6316..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/FullRevisionPackage.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright (C) 2012 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.sdklib.internal.repository.packages; - -import com.android.sdklib.internal.repository.sources.SdkSource; -import com.android.sdklib.repository.FullRevision; -import com.android.sdklib.repository.FullRevision.PreviewComparison; -import com.android.sdklib.repository.PkgProps; -import com.android.sdklib.repository.SdkRepoConstants; - -import org.w3c.dom.Node; - -import java.util.Map; -import java.util.Properties; - -/** - * Represents a package in an SDK repository that has a {@link FullRevision}, - * which is a multi-part revision number (major.minor.micro) and an optional preview revision. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public abstract class FullRevisionPackage extends Package - implements IFullRevisionProvider { - - private final FullRevision mPreviewVersion; - - /** - * Creates a new package from the attributes and elements of the given XML node. - * This constructor should throw an exception if the package cannot be created. - * - * @param source The {@link SdkSource} where this is loaded from. - * @param packageNode The XML element being parsed. - * @param nsUri The namespace URI of the originating XML document, to be able to deal with - * parameters that vary according to the originating XML schema. - * @param licenses The licenses loaded from the XML originating document. - */ - FullRevisionPackage(SdkSource source, - Node packageNode, - String nsUri, - Map licenses) { - super(source, packageNode, nsUri, licenses); - - mPreviewVersion = PackageParserUtils.parseFullRevisionElement( - PackageParserUtils.findChildElement(packageNode, SdkRepoConstants.NODE_REVISION)); - } - - /** - * Manually create a new package with one archive and the given attributes. - * This is used to create packages from local directories in which case there must be - * one archive which URL is the actual target location. - *

- * Properties from props are used first when possible, e.g. if props is non null. - *

- * By design, this creates a package with one and only one archive. - */ - public FullRevisionPackage( - SdkSource source, - Properties props, - int revision, - String license, - String description, - String descUrl, - String archiveOsPath) { - super(source, props, revision, license, description, descUrl, archiveOsPath); - - FullRevision rev = PackageParserUtils.getPropertyFull(props, PkgProps.PKG_REVISION); - if (rev == null) { - rev = new FullRevision(revision); - } - mPreviewVersion = rev; - } - - @Override - public FullRevision getRevision() { - return mPreviewVersion; - } - - @Override - public void saveProperties(Properties props) { - super.saveProperties(props); - props.setProperty(PkgProps.PKG_REVISION, mPreviewVersion.toShortString()); - } - - @Override - public int hashCode() { - final int prime = 31; - int result = super.hashCode(); - result = prime * result + ((mPreviewVersion == null) ? 0 : mPreviewVersion.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (!super.equals(obj)) { - return false; - } - if (!(obj instanceof FullRevisionPackage)) { - return false; - } - FullRevisionPackage other = (FullRevisionPackage) obj; - if (mPreviewVersion == null) { - if (other.mPreviewVersion != null) { - return false; - } - } else if (!mPreviewVersion.equals(other.mPreviewVersion)) { - return false; - } - return true; - } - - /** - * Computes whether the given package is a suitable update for the current package. - *

- * A specific case here is that a release package can update a preview, whereas - * a preview can only update another preview. - *

- * {@inheritDoc} - */ - @Override - public UpdateInfo canBeUpdatedBy(Package replacementPackage) { - if (replacementPackage == null) { - return UpdateInfo.INCOMPATIBLE; - } - - // check they are the same item, ignoring the preview bit. - if (!sameItemAs(replacementPackage, PreviewComparison.IGNORE)) { - return UpdateInfo.INCOMPATIBLE; - } - - // a preview cannot update a non-preview - if (!getRevision().isPreview() && replacementPackage.getRevision().isPreview()) { - return UpdateInfo.INCOMPATIBLE; - } - - // check revision number - if (replacementPackage.getRevision().compareTo(this.getRevision()) > 0) { - return UpdateInfo.UPDATE; - } - - // not an upgrade but not incompatible either. - return UpdateInfo.NOT_UPDATE; - } - -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IAndroidVersionProvider.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IAndroidVersionProvider.java deleted file mode 100644 index 0f1a53f0..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IAndroidVersionProvider.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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.sdklib.internal.repository.packages; - -import com.android.annotations.NonNull; -import com.android.sdklib.AndroidVersion; - -/** - * Interface for packages that provide an {@link AndroidVersion}. - *

- * Note that {@link IPlatformDependency} is a similar interface, but with a different semantic. - * The {@link IPlatformDependency} denotes that a given package can only be installed if the - * requested platform is present, whereas this interface denotes that the given package simply - * has a version, which is not necessarily a dependency. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public interface IAndroidVersionProvider { - - /** - * Returns the android version, for platform, add-on and doc packages. - * Can be 0 if this is a local package of unknown api-level. - */ - @NonNull - AndroidVersion getAndroidVersion(); -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IExactApiLevelDependency.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IExactApiLevelDependency.java deleted file mode 100644 index 7ed20659..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IExactApiLevelDependency.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (C) 2011 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.sdklib.internal.repository.packages; - -import com.android.sdklib.repository.RepoConstants; - -/** - * Interface used to decorate a {@link Package} that has a dependency - * on a specific API level, e.g. which XML has a {@code } element. - *

- * For example an add-on package requires a platform with an exact API level to be installed - * at the same time. - * This is not the same as {@link IMinApiLevelDependency} which requests that a platform with at - * least the requested API level be present or installed at the same time. - *

- * Such package requires the {@code } element. It is not an optional - * property, however it can be invalid. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public interface IExactApiLevelDependency { - - /** - * The value of {@link #getExactApiLevel()} when the {@link RepoConstants#NODE_API_LEVEL} - * was not specified in the XML source. - */ - int API_LEVEL_INVALID = 0; - - /** - * Returns the exact API level required by this package, if > 0, - * or {@link #API_LEVEL_INVALID} if the value was missing. - *

- * This attribute is mandatory and should not be normally missing. - * It can only happen when dealing with an invalid repository XML. - */ - int getExactApiLevel(); -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IFullRevisionProvider.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IFullRevisionProvider.java deleted file mode 100644 index 6da200cb..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IFullRevisionProvider.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (C) 2012 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.sdklib.internal.repository.packages; - -import com.android.sdklib.repository.FullRevision; -import com.android.sdklib.repository.MajorRevision; -import com.android.sdklib.repository.FullRevision.PreviewComparison; - - - -/** - * Interface for packages that provide a {@link FullRevision}, - * which is a multi-part revision number (major.minor.micro) and an optional preview revision. - *

- * This interface is a tag. It indicates that {@link Package#getRevision()} returns a - * {@link FullRevision} instead of a limited {@link MajorRevision}.
- * The preview version number is available via {@link Package#getRevision()}. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public interface IFullRevisionProvider { - - /** - * Returns whether the given package represents the same item as the current package. - *

- * Two packages are considered the same if they represent the same thing, except for the - * revision number. - * @param pkg The package to compare - * @param comparePreview How to compare previews. - * @return true if the items are the same. - */ - boolean sameItemAs(Package pkg, PreviewComparison comparePreview); - -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/ILayoutlibVersion.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/ILayoutlibVersion.java deleted file mode 100644 index 30b2112d..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/ILayoutlibVersion.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2011 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.sdklib.internal.repository.packages; - -import com.android.utils.Pair; - -/** - * Interface used to decorate a {@link Package} that provides a version for layout lib. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public interface ILayoutlibVersion { - - int LAYOUTLIB_API_NOT_SPECIFIED = 0; - int LAYOUTLIB_REV_NOT_SPECIFIED = 0; - - /** - * Returns the layoutlib version. Mandatory starting with repository XSD rev 4. - *

- * The first integer is the API of layoublib, which should be > 0. - * It will be equal to {@link #LAYOUTLIB_API_NOT_SPECIFIED} (0) if the layoutlib - * version isn't specified. - *

- * The second integer is the revision for that given API. It is >= 0 - * and works as a minor revision number, incremented for the same API level. - * - * @since sdk-repository-4.xsd - */ - Pair getLayoutlibVersion(); -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IMinApiLevelDependency.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IMinApiLevelDependency.java deleted file mode 100644 index 60e6d344..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IMinApiLevelDependency.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2010 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.sdklib.internal.repository.packages; - -import com.android.sdklib.repository.SdkRepoConstants; - -/** - * Interface used to decorate a {@link Package} that has a dependency - * on a minimal API level, e.g. which XML has a <min-api-level> element. - *

- * A package that has this dependency can only be installed if a platform with at least the - * requested API level is present or installed at the same time. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public interface IMinApiLevelDependency { - - /** - * The value of {@link #getMinApiLevel()} when the {@link SdkRepoConstants#NODE_MIN_API_LEVEL} - * was not specified in the XML source. - */ - int MIN_API_LEVEL_NOT_SPECIFIED = 0; - - /** - * Returns the minimal API level required by this package, if > 0, - * or {@link #MIN_API_LEVEL_NOT_SPECIFIED} if there is no such requirement. - */ - int getMinApiLevel(); -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IMinPlatformToolsDependency.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IMinPlatformToolsDependency.java deleted file mode 100644 index 49c84f81..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IMinPlatformToolsDependency.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2010 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.sdklib.internal.repository.packages; - -import com.android.sdklib.repository.FullRevision; -import com.android.sdklib.repository.SdkRepoConstants; - -/** - * Interface used to decorate a {@link Package} that has a dependency - * on a minimal platform-tools revision, e.g. which XML has a - * <min-platform-tools-rev> element. - *

- * A package that has this dependency can only be installed if the requested platform-tools - * revision is present or installed at the same time. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public interface IMinPlatformToolsDependency { - - /** - * The value of {@link #getMinPlatformToolsRevision()} when the - * {@link SdkRepoConstants#NODE_MIN_PLATFORM_TOOLS_REV} was not specified in the XML source. - * Since this is a required attribute in the XML schema, it can only happen when dealing - * with an invalid repository XML. - */ - FullRevision MIN_PLATFORM_TOOLS_REV_INVALID = - new FullRevision(FullRevision.MISSING_MAJOR_REV); - - /** - * The minimal revision of the tools package required by this package if > 0, - * or {@link #MIN_PLATFORM_TOOLS_REV_INVALID} if the value was missing. - *

- * This attribute is mandatory and should not be normally missing. - * It can only happen when dealing with an invalid repository XML. - */ - FullRevision getMinPlatformToolsRevision(); - -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IMinToolsDependency.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IMinToolsDependency.java deleted file mode 100644 index ebdbf591..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IMinToolsDependency.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) 2010 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.sdklib.internal.repository.packages; - -import com.android.sdklib.repository.FullRevision; -import com.android.sdklib.repository.SdkRepoConstants; - -/** - * Interface used to decorate a {@link Package} that has a dependency - * on a minimal tools revision, e.g. which XML has a <min-tools-rev> element. - *

- * A package that has this dependency can only be installed if the requested tools revision - * is present or installed at the same time. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public interface IMinToolsDependency { - - /** - * The value of {@link #getMinToolsRevision()} when the - * {@link SdkRepoConstants#NODE_MIN_TOOLS_REV} was not specified in the XML source. - */ - FullRevision MIN_TOOLS_REV_NOT_SPECIFIED = - new FullRevision(FullRevision.MISSING_MAJOR_REV); - - /** - * The minimal revision of the tools package required by this extra package if > 0, - * or {@link #MIN_TOOLS_REV_NOT_SPECIFIED} if there is no such requirement. - */ - FullRevision getMinToolsRevision(); -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IPlatformDependency.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IPlatformDependency.java deleted file mode 100644 index ccbb267c..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/IPlatformDependency.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2010 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.sdklib.internal.repository.packages; - -import com.android.sdklib.AndroidVersion; - -/** - * Interface used to decorate a {@link Package} that has a dependency - * on a specific platform (API level and/or code name). - *

- * A package that has this dependency can only be installed if a platform with at least the - * requested API level is present or installed at the same time. - *

- * Note that although this interface looks like {@link IAndroidVersionProvider}, it does - * not convey the same semantic since {@link IAndroidVersionProvider} does not - * imply any dependency being a limiting factor as far as installation is concerned. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public interface IPlatformDependency { - - /** Returns the version of the platform dependency of this package. */ - AndroidVersion getAndroidVersion(); - -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/LayoutlibVersionMixin.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/LayoutlibVersionMixin.java deleted file mode 100644 index 7d18edaa..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/LayoutlibVersionMixin.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright (C) 2011 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.sdklib.internal.repository.packages; - -import com.android.sdklib.repository.PkgProps; -import com.android.sdklib.repository.RepoConstants; -import com.android.utils.Pair; - -import org.w3c.dom.Node; - -import java.util.Properties; - -/** - * Helper class to handle the layoutlib version provided by a package. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class LayoutlibVersionMixin implements ILayoutlibVersion { - - /** - * The layoutlib version. - * The first integer is the API of layoublib, which should be > 0. - * It will be equal to {@link #LAYOUTLIB_API_NOT_SPECIFIED} (0) if the layoutlib - * version isn't specified. - * The second integer is the revision for that given API. It is >= 0 - * and works as a minor revision number, incremented for the same API level. - */ - private final Pair mLayoutlibVersion; - - /** - * Parses an XML node to process the {@code } element. - * - * The layoutlib element is new in the XSD rev 4, so we need to cope with it missing - * in earlier XMLs. - */ - public LayoutlibVersionMixin(Node pkgNode) { - - int api = LAYOUTLIB_API_NOT_SPECIFIED; - int rev = LAYOUTLIB_REV_NOT_SPECIFIED; - - Node layoutlibNode = - PackageParserUtils.findChildElement(pkgNode, RepoConstants.NODE_LAYOUT_LIB); - - if (layoutlibNode != null) { - api = PackageParserUtils.getXmlInt(layoutlibNode, RepoConstants.NODE_API, 0); - rev = PackageParserUtils.getXmlInt(layoutlibNode, RepoConstants.NODE_REVISION, 0); - } - - mLayoutlibVersion = Pair.of(api, rev); - } - - /** - * Parses the layoutlib version optionally available in the given {@link Properties}. - */ - public LayoutlibVersionMixin(Properties props) { - int layoutlibApi = Package.getPropertyInt(props, PkgProps.LAYOUTLIB_API, - LAYOUTLIB_API_NOT_SPECIFIED); - int layoutlibRev = Package.getPropertyInt(props, PkgProps.LAYOUTLIB_REV, - LAYOUTLIB_REV_NOT_SPECIFIED); - mLayoutlibVersion = Pair.of(layoutlibApi, layoutlibRev); - } - - /** - * Stores the layoutlib version in the given {@link Properties}. - */ - void saveProperties(Properties props) { - if (mLayoutlibVersion.getFirst().intValue() != LAYOUTLIB_API_NOT_SPECIFIED) { - props.setProperty(PkgProps.LAYOUTLIB_API, mLayoutlibVersion.getFirst().toString()); - props.setProperty(PkgProps.LAYOUTLIB_REV, mLayoutlibVersion.getSecond().toString()); - } - } - - /** - * Returns the layoutlib version. - *

- * The first integer is the API of layoublib, which should be > 0. - * It will be equal to {@link #LAYOUTLIB_API_NOT_SPECIFIED} (0) if the layoutlib - * version isn't specified. - *

- * The second integer is the revision for that given API. It is >= 0 - * and works as a minor revision number, incremented for the same API level. - * - * @since sdk-repository-4.xsd and sdk-addon-2.xsd - */ - @Override - public Pair getLayoutlibVersion() { - return mLayoutlibVersion; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((mLayoutlibVersion == null) ? 0 : mLayoutlibVersion.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (!(obj instanceof LayoutlibVersionMixin)) { - return false; - } - LayoutlibVersionMixin other = (LayoutlibVersionMixin) obj; - if (mLayoutlibVersion == null) { - if (other.mLayoutlibVersion != null) { - return false; - } - } else if (!mLayoutlibVersion.equals(other.mLayoutlibVersion)) { - return false; - } - return true; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/MajorRevisionPackage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/MajorRevisionPackage.java deleted file mode 100644 index 38bc3a0e..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/MajorRevisionPackage.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright (C) 2012 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.sdklib.internal.repository.packages; - -import com.android.sdklib.internal.repository.sources.SdkSource; -import com.android.sdklib.repository.FullRevision; -import com.android.sdklib.repository.MajorRevision; -import com.android.sdklib.repository.PkgProps; -import com.android.sdklib.repository.SdkRepoConstants; - -import org.w3c.dom.Node; - -import java.util.Map; -import java.util.Properties; - -/** - * Represents a package in an SDK repository that has a {@link MajorRevision}, - * which is a single major revision number (not minor, micro or previews). - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public abstract class MajorRevisionPackage extends Package { - - private final MajorRevision mRevision; - - /** - * Creates a new package from the attributes and elements of the given XML node. - * This constructor should throw an exception if the package cannot be created. - * - * @param source The {@link SdkSource} where this is loaded from. - * @param packageNode The XML element being parsed. - * @param nsUri The namespace URI of the originating XML document, to be able to deal with - * parameters that vary according to the originating XML schema. - * @param licenses The licenses loaded from the XML originating document. - */ - MajorRevisionPackage(SdkSource source, - Node packageNode, - String nsUri, - Map licenses) { - super(source, packageNode, nsUri, licenses); - - mRevision = new MajorRevision( - PackageParserUtils.getXmlInt(packageNode, SdkRepoConstants.NODE_REVISION, 0)); - } - - /** - * Manually create a new package with one archive and the given attributes. - * This is used to create packages from local directories in which case there must be - * one archive which URL is the actual target location. - *

- * Properties from props are used first when possible, e.g. if props is non null. - *

- * By design, this creates a package with one and only one archive. - */ - public MajorRevisionPackage( - SdkSource source, - Properties props, - int revision, - String license, - String description, - String descUrl, - String archiveOsPath) { - super(source, props, revision, license, description, descUrl, archiveOsPath); - - String revStr = getProperty(props, PkgProps.PKG_REVISION, null); - - MajorRevision rev = null; - if (revStr != null) { - try { - rev = MajorRevision.parseRevision(revStr); - } catch (NumberFormatException ignore) {} - } - if (rev == null) { - rev = new MajorRevision(revision); - } - - mRevision = rev; - } - - /** - * Returns the revision, an int > 0, for all packages (platform, add-on, tool, doc). - * Can be 0 if this is a local package of unknown revision. - */ - @Override - public FullRevision getRevision() { - return mRevision; - } - - - @Override - public void saveProperties(Properties props) { - super.saveProperties(props); - props.setProperty(PkgProps.PKG_REVISION, mRevision.toString()); - } - - @Override - public int hashCode() { - final int prime = 31; - int result = super.hashCode(); - result = prime * result + ((mRevision == null) ? 0 : mRevision.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (!super.equals(obj)) { - return false; - } - if (!(obj instanceof MajorRevisionPackage)) { - return false; - } - MajorRevisionPackage other = (MajorRevisionPackage) obj; - if (mRevision == null) { - if (other.mRevision != null) { - return false; - } - } else if (!mRevision.equals(other.mRevision)) { - return false; - } - return true; - } - - @Override - public UpdateInfo canBeUpdatedBy(Package replacementPackage) { - if (replacementPackage == null) { - return UpdateInfo.INCOMPATIBLE; - } - - // check they are the same item. - if (!sameItemAs(replacementPackage)) { - return UpdateInfo.INCOMPATIBLE; - } - - // check revision number - if (replacementPackage.getRevision().compareTo(this.getRevision()) > 0) { - return UpdateInfo.UPDATE; - } - - // not an upgrade but not incompatible either. - return UpdateInfo.NOT_UPDATE; - } - - -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/MinToolsMixin.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/MinToolsMixin.java deleted file mode 100644 index b85ba8a8..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/MinToolsMixin.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright (C) 2013 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.sdklib.internal.repository.packages; - -import com.android.sdklib.internal.repository.sources.SdkSource; -import com.android.sdklib.repository.FullRevision; -import com.android.sdklib.repository.PkgProps; -import com.android.sdklib.repository.SdkRepoConstants; - -import org.w3c.dom.Node; - -import java.util.Properties; - -/** - * Represents an XML node in an SDK repository that has a min-tools-rev requirement. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -class MinToolsMixin implements IMinToolsDependency { - - /** - * The minimal revision of the tools package required by this extra package, if > 0, - * or {@link #MIN_TOOLS_REV_NOT_SPECIFIED} if there is no such requirement. - */ - private final FullRevision mMinToolsRevision; - - /** - * Creates a new mixin from the attributes and elements of the given XML node. - * This constructor should throw an exception if the package cannot be created. - * - * @param packageNode The XML element being parsed. - */ - MinToolsMixin(Node packageNode) { - - mMinToolsRevision = PackageParserUtils.parseFullRevisionElement( - PackageParserUtils.findChildElement(packageNode, SdkRepoConstants.NODE_MIN_TOOLS_REV)); - } - - /** - * Manually create a new mixin with one archive and the given attributes. - * This is used to create packages from local directories in which case there must be - * one archive which URL is the actual target location. - *

- * Properties from props are used first when possible, e.g. if props is non null. - *

- * By design, this creates a package with one and only one archive. - */ - public MinToolsMixin( - SdkSource source, - Properties props, - int revision, - String license, - String description, - String descUrl, - String archiveOsPath) { - - String revStr = Package.getProperty(props, PkgProps.MIN_TOOLS_REV, null); - - FullRevision rev = MIN_TOOLS_REV_NOT_SPECIFIED; - if (revStr != null) { - try { - rev = FullRevision.parseRevision(revStr); - } catch (NumberFormatException ignore) {} - } - - mMinToolsRevision = rev; - } - - /** - * The minimal revision of the tools package required by this extra package, if > 0, - * or {@link #MIN_TOOLS_REV_NOT_SPECIFIED} if there is no such requirement. - */ - @Override - public FullRevision getMinToolsRevision() { - return mMinToolsRevision; - } - - public void saveProperties(Properties props) { - if (!getMinToolsRevision().equals(MIN_TOOLS_REV_NOT_SPECIFIED)) { - props.setProperty(PkgProps.MIN_TOOLS_REV, getMinToolsRevision().toShortString()); - } - } - - @Override - public int hashCode() { - return hashCode(super.hashCode()); - } - - int hashCode(int superHashCode) { - final int prime = 31; - int result = superHashCode; - result = prime * result + ((mMinToolsRevision == null) ? 0 : mMinToolsRevision.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (!super.equals(obj)) { - return false; - } - if (!(obj instanceof IMinToolsDependency)) { - return false; - } - IMinToolsDependency other = (IMinToolsDependency) obj; - if (mMinToolsRevision == null) { - if (other.getMinToolsRevision() != null) { - return false; - } - } else if (!mMinToolsRevision.equals(other.getMinToolsRevision())) { - return false; - } - return true; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/MinToolsPackage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/MinToolsPackage.java deleted file mode 100644 index 8ea682af..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/MinToolsPackage.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * 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.sdklib.internal.repository.packages; - -import com.android.sdklib.internal.repository.sources.SdkSource; -import com.android.sdklib.repository.FullRevision; - -import org.w3c.dom.Node; - -import java.util.Map; -import java.util.Properties; - -/** - * Represents an XML node in an SDK repository that has a min-tools-rev requirement. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public abstract class MinToolsPackage extends MajorRevisionPackage implements IMinToolsDependency { - - private final MinToolsMixin mMinToolsMixin; - - /** - * Creates a new package from the attributes and elements of the given XML node. - * This constructor should throw an exception if the package cannot be created. - * - * @param source The {@link SdkSource} where this is loaded from. - * @param packageNode The XML element being parsed. - * @param nsUri The namespace URI of the originating XML document, to be able to deal with - * parameters that vary according to the originating XML schema. - * @param licenses The licenses loaded from the XML originating document. - */ - MinToolsPackage(SdkSource source, Node packageNode, String nsUri, Map licenses) { - super(source, packageNode, nsUri, licenses); - - mMinToolsMixin = new MinToolsMixin(packageNode); - } - - /** - * Manually create a new package with one archive and the given attributes. - * This is used to create packages from local directories in which case there must be - * one archive which URL is the actual target location. - *

- * Properties from props are used first when possible, e.g. if props is non null. - *

- * By design, this creates a package with one and only one archive. - */ - public MinToolsPackage( - SdkSource source, - Properties props, - int revision, - String license, - String description, - String descUrl, - String archiveOsPath) { - super(source, props, revision, license, description, descUrl, archiveOsPath); - - mMinToolsMixin = new MinToolsMixin( - source, - props, - revision, - license, - description, - descUrl, - archiveOsPath); - } - - /** - * The minimal revision of the tools package required by this extra package, if > 0, - * or {@link #MIN_TOOLS_REV_NOT_SPECIFIED} if there is no such requirement. - */ - @Override - public FullRevision getMinToolsRevision() { - return mMinToolsMixin.getMinToolsRevision(); - } - - @Override - public void saveProperties(Properties props) { - super.saveProperties(props); - mMinToolsMixin.saveProperties(props); - } - - @Override - public int hashCode() { - return mMinToolsMixin.hashCode(super.hashCode()); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (!super.equals(obj)) { - return false; - } - if (!(obj instanceof MinToolsPackage)) { - return false; - } - return mMinToolsMixin.equals(obj); - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/NoPreviewRevisionPackage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/NoPreviewRevisionPackage.java deleted file mode 100644 index fc5a5dd1..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/NoPreviewRevisionPackage.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright (C) 2013 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.sdklib.internal.repository.packages; - -import com.android.sdklib.internal.repository.sources.SdkSource; -import com.android.sdklib.repository.NoPreviewRevision; -import com.android.sdklib.repository.PkgProps; -import com.android.sdklib.repository.SdkRepoConstants; - -import org.w3c.dom.Node; - -import java.util.Map; -import java.util.Properties; - -/** - * Represents a package in an SDK repository that has a {@link NoPreviewRevision}, - * which is a single major.minor.micro revision number and no preview. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public abstract class NoPreviewRevisionPackage extends Package { - - private final NoPreviewRevision mRevision; - - /** - * Creates a new package from the attributes and elements of the given XML node. - * This constructor should throw an exception if the package cannot be created. - * - * @param source The {@link SdkSource} where this is loaded from. - * @param packageNode The XML element being parsed. - * @param nsUri The namespace URI of the originating XML document, to be able to deal with - * parameters that vary according to the originating XML schema. - * @param licenses The licenses loaded from the XML originating document. - */ - NoPreviewRevisionPackage(SdkSource source, - Node packageNode, - String nsUri, - Map licenses) { - super(source, packageNode, nsUri, licenses); - - mRevision = PackageParserUtils.parseNoPreviewRevisionElement( - PackageParserUtils.findChildElement(packageNode, SdkRepoConstants.NODE_REVISION)); - } - - /** - * Manually create a new package with one archive and the given attributes. - * This is used to create packages from local directories in which case there must be - * one archive which URL is the actual target location. - *

- * Properties from props are used first when possible, e.g. if props is non null. - *

- * By design, this creates a package with one and only one archive. - */ - public NoPreviewRevisionPackage( - SdkSource source, - Properties props, - int revision, - String license, - String description, - String descUrl, - String archiveOsPath) { - super(source, props, revision, license, description, descUrl, archiveOsPath); - - String revStr = getProperty(props, PkgProps.PKG_REVISION, null); - - NoPreviewRevision rev = null; - if (revStr != null) { - try { - rev = NoPreviewRevision.parseRevision(revStr); - } catch (NumberFormatException ignore) {} - } - if (rev == null) { - rev = new NoPreviewRevision(revision); - } - - mRevision = rev; - } - - /** - * Returns the revision, an int > 0, for all packages (platform, add-on, tool, doc). - * Can be 0 if this is a local package of unknown revision. - */ - @Override - public NoPreviewRevision getRevision() { - return mRevision; - } - - - @Override - public void saveProperties(Properties props) { - super.saveProperties(props); - props.setProperty(PkgProps.PKG_REVISION, mRevision.toString()); - } - - @Override - public int hashCode() { - final int prime = 31; - int result = super.hashCode(); - result = prime * result + ((mRevision == null) ? 0 : mRevision.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (!super.equals(obj)) { - return false; - } - if (!(obj instanceof NoPreviewRevisionPackage)) { - return false; - } - NoPreviewRevisionPackage other = (NoPreviewRevisionPackage) obj; - if (mRevision == null) { - if (other.mRevision != null) { - return false; - } - } else if (!mRevision.equals(other.mRevision)) { - return false; - } - return true; - } - - @Override - public UpdateInfo canBeUpdatedBy(Package replacementPackage) { - if (replacementPackage == null) { - return UpdateInfo.INCOMPATIBLE; - } - - // check they are the same item. - if (!sameItemAs(replacementPackage)) { - return UpdateInfo.INCOMPATIBLE; - } - - // check revision number - if (replacementPackage.getRevision().compareTo(this.getRevision()) > 0) { - return UpdateInfo.UPDATE; - } - - // not an upgrade but not incompatible either. - return UpdateInfo.NOT_UPDATE; - } - - -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/Package.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/Package.java deleted file mode 100644 index 4cf6f9b6..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/Package.java +++ /dev/null @@ -1,902 +0,0 @@ -/* - * 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.sdklib.internal.repository.packages; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.annotations.VisibleForTesting; -import com.android.annotations.VisibleForTesting.Visibility; -import com.android.sdklib.AndroidVersion; -import com.android.sdklib.SdkManager; -import com.android.sdklib.repository.IDescription; -import com.android.sdklib.repository.IListDescription; -import com.android.sdklib.internal.repository.ITaskMonitor; -import com.android.sdklib.internal.repository.archives.Archive; -import com.android.sdklib.internal.repository.sources.SdkAddonSource; -import com.android.sdklib.internal.repository.sources.SdkRepoSource; -import com.android.sdklib.internal.repository.sources.SdkSource; -import com.android.sdklib.io.IFileOp; -import com.android.sdklib.repository.*; -import com.android.sdklib.repository.descriptors.IPkgDesc; - -import com.android.sdklib.repository.descriptors.PkgDesc; -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; -import org.w3c.dom.Node; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Map; -import java.util.Properties; - -/** - * A {@link Package} is the base class for "something" that can be downloaded from - * the SDK repository. - *

- * A package has some attributes (revision, description) and a list of archives - * which represent the downloadable bits. - *

- * Packages are contained by a {@link SdkSource} (a download site). - *

- * Derived classes must implement the {@link IDescription} methods. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public abstract class Package implements IDescription, IListDescription, Comparable { - - private final String mObsolete; - private final License mLicense; - private final String mListDisplay; - private final String mDescription; - private final String mDescUrl; - @Deprecated - private final String mReleaseNote; - @Deprecated - private final String mReleaseUrl; - private final Archive[] mArchives; - private final SdkSource mSource; - - - // figure if we'll need to set the unix permissions - private static final boolean sUsingUnixPerm = - SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN || - SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_LINUX; - - /** - * Enum for the result of {@link Package#canBeUpdatedBy(Package)}. This used so that we can - * differentiate between a package that is totally incompatible, and one that is the same item - * but just not an update. - * @see #canBeUpdatedBy(Package) - */ - public enum UpdateInfo { - /** Means that the 2 packages are not the same thing */ - INCOMPATIBLE, - /** Means that the 2 packages are the same thing but one does not upgrade the other. - *

- * TODO: this name is confusing. We need to dig deeper. */ - NOT_UPDATE, - /** Means that the 2 packages are the same thing, and one is the upgrade of the other */ - UPDATE - } - - /** - * Creates a new package from the attributes and elements of the given XML node. - * This constructor should throw an exception if the package cannot be created. - * - * @param source The {@link SdkSource} where this is loaded from. - * @param packageNode The XML element being parsed. - * @param nsUri The namespace URI of the originating XML document, to be able to deal with - * parameters that vary according to the originating XML schema. - * @param licenses The licenses loaded from the XML originating document. - */ - Package(SdkSource source, Node packageNode, String nsUri, Map licenses) { - mSource = source; - mListDisplay = - PackageParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_LIST_DISPLAY); - mDescription = - PackageParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_DESCRIPTION); - mDescUrl = - PackageParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_DESC_URL); - mReleaseNote = - PackageParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_RELEASE_NOTE); - mReleaseUrl = - PackageParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_RELEASE_URL); - mObsolete = - PackageParserUtils.getOptionalXmlString(packageNode, SdkRepoConstants.NODE_OBSOLETE); - - mLicense = parseLicense(packageNode, licenses); - mArchives = parseArchives( - PackageParserUtils.findChildElement(packageNode, SdkRepoConstants.NODE_ARCHIVES)); - } - - /** - * Manually create a new package with one archive and the given attributes. - * This is used to create packages from local directories in which case there must be - * one archive which URL is the actual target location. - *

- * Properties from props are used first when possible, e.g. if props is non null. - *

- * By design, this creates a package with one and only one archive. - */ - public Package( - SdkSource source, - Properties props, - int revision, - String license, - String description, - String descUrl, - String archiveOsPath) { - - if (description == null) { - description = ""; - } - if (descUrl == null) { - descUrl = ""; - } - - license = getProperty(props, PkgProps.PKG_LICENSE, license); - if (license != null) { - mLicense = new License(license, getProperty(props, PkgProps.PKG_LICENSE_REF, null)); - } - else { - mLicense = null; - } - mListDisplay = getProperty(props, PkgProps.PKG_LIST_DISPLAY, ""); //$NON-NLS-1$ - mDescription = getProperty(props, PkgProps.PKG_DESC, description); - mDescUrl = getProperty(props, PkgProps.PKG_DESC_URL, descUrl); - mReleaseNote = getProperty(props, PkgProps.PKG_RELEASE_NOTE, ""); //$NON-NLS-1$ - mReleaseUrl = getProperty(props, PkgProps.PKG_RELEASE_URL, ""); //$NON-NLS-1$ - mObsolete = getProperty(props, PkgProps.PKG_OBSOLETE, null); - - // If source is null and we can find a source URL in the properties, generate - // a dummy source just to store the URL. This allows us to easily remember where - // a package comes from. - String srcUrl = getProperty(props, PkgProps.PKG_SOURCE_URL, null); - if (props != null && source == null && srcUrl != null) { - // Both Addon and Extra packages can come from an addon source. - // For Extras, we can tell by looking at the source URL. - if (this instanceof AddonPackage || - ((this instanceof ExtraPackage) && - srcUrl.endsWith(SdkAddonConstants.URL_DEFAULT_FILENAME))) { - source = new SdkAddonSource(srcUrl, null /*uiName*/); - } else { - source = new SdkRepoSource(srcUrl, null /*uiName*/); - } - } - mSource = source; - - // Note: if archiveOsPath is non-null, this makes a local archive (e.g. a locally - // installed package.) If it's null, this makes a remote archive. - mArchives = initializeArchives(props, archiveOsPath); - } - - /** - * Returns the {@link IPkgDesc} describing this package's meta data. - * - * @return A non-null {@link IPkgDesc}. - */ - @NonNull - public abstract IPkgDesc getPkgDesc(); - - /** - * Called by the constructor to get the initial {@link #mArchives} array. - *

- * This is invoked by the local-package constructor and allows mock testing - * classes to override the archives created. - * This is an implementation details and clients must not - * rely on this. - * - * @return Always return a non-null array. The array may be empty. - */ - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected Archive[] initializeArchives( - Properties props, - String archiveOsPath) { - return new Archive[] { - new Archive(this, - props, - archiveOsPath) }; - } - - /** - * Utility method that returns a property from a {@link Properties} object. - * Returns the default value if props is null or if the property is not defined. - * - * @param props The {@link Properties} to search into. - * If null, the default value is returned. - * @param propKey The name of the property. Must not be null. - * @param defaultValue The default value to return if {@code props} is null or if the - * key is not found. Can be null. - * @return The string value of the given key in the properties, or null if the key - * isn't found or if {@code props} is null. - */ - @Nullable - static String getProperty( - @Nullable Properties props, - @NonNull String propKey, - @Nullable String defaultValue) { - return PackageParserUtils.getProperty(props, propKey, defaultValue); - } - - /** - * Utility method that returns an integer property from a {@link Properties} object. - * Returns the default value if props is null or if the property is not defined or - * cannot be parsed to an integer. - * - * @param props The {@link Properties} to search into. - * If null, the default value is returned. - * @param propKey The name of the property. Must not be null. - * @param defaultValue The default value to return if {@code props} is null or if the - * key is not found. Can be null. - * @return The integer value of the given key in the properties, or the {@code defaultValue}. - */ - static int getPropertyInt( - @Nullable Properties props, - @NonNull String propKey, - int defaultValue) { - return PackageParserUtils.getPropertyInt(props, propKey, defaultValue); - } - - /** - * Save the properties of the current packages in the given {@link Properties} object. - * These properties will later be give the constructor that takes a {@link Properties} object. - */ - public void saveProperties(@NonNull Properties props) { - if (mLicense != null) { - String license = mLicense.getLicense(); - if (license != null && !license.isEmpty()) { - props.setProperty(PkgProps.PKG_LICENSE, license); - } - String licenseRef = mLicense.getLicenseRef(); - if (licenseRef != null && !licenseRef.isEmpty()) { - props.setProperty(PkgProps.PKG_LICENSE_REF, licenseRef); - } - } - if (mListDisplay != null && !mListDisplay.isEmpty()) { - props.setProperty(PkgProps.PKG_LIST_DISPLAY, mListDisplay); - } - if (mDescription != null && !mDescription.isEmpty()) { - props.setProperty(PkgProps.PKG_DESC, mDescription); - } - if (mDescUrl != null && !mDescUrl.isEmpty()) { - props.setProperty(PkgProps.PKG_DESC_URL, mDescUrl); - } - - if (mReleaseNote != null && !mReleaseNote.isEmpty()) { - props.setProperty(PkgProps.PKG_RELEASE_NOTE, mReleaseNote); - } - if (mReleaseUrl != null && !mReleaseUrl.isEmpty()) { - props.setProperty(PkgProps.PKG_RELEASE_URL, mReleaseUrl); - } - if (mObsolete != null) { - props.setProperty(PkgProps.PKG_OBSOLETE, mObsolete); - } - if (mSource != null) { - props.setProperty(PkgProps.PKG_SOURCE_URL, mSource.getUrl()); - } - } - - /** - * Parses the uses-licence node of this package, if any, and returns the license - * definition if there's one. Returns null if there's no uses-license element or no - * license of this name defined. - */ - @Nullable - private License parseLicense(@NonNull Node packageNode, @NonNull Map licenses) { - Node usesLicense = - PackageParserUtils.findChildElement(packageNode, SdkRepoConstants.NODE_USES_LICENSE); - if (usesLicense != null) { - Node ref = usesLicense.getAttributes().getNamedItem(SdkRepoConstants.ATTR_REF); - if (ref != null) { - String licenseRef = ref.getNodeValue(); - return new License(licenses.get(licenseRef), licenseRef); - } - } - return null; - } - - /** - * Parses an XML node to process the element. - * Always return a non-null array. The array may be empty. - */ - @NonNull - private Archive[] parseArchives(@NonNull Node archivesNode) { - ArrayList archives = new ArrayList(); - - if (archivesNode != null) { - String nsUri = archivesNode.getNamespaceURI(); - for(Node child = archivesNode.getFirstChild(); - child != null; - child = child.getNextSibling()) { - - if (child.getNodeType() == Node.ELEMENT_NODE && - nsUri.equals(child.getNamespaceURI()) && - SdkRepoConstants.NODE_ARCHIVE.equals(child.getLocalName())) { - archives.add(parseArchive(child)); - } - } - } - - return archives.toArray(new Archive[archives.size()]); - } - - /** - * Parses one element from an container. - */ - @NonNull - private Archive parseArchive(@NonNull Node archiveNode) { - Archive a = new Archive( - this, - PackageParserUtils.parseArchFilter(archiveNode), - PackageParserUtils.getXmlString(archiveNode, SdkRepoConstants.NODE_URL), - PackageParserUtils.getXmlLong (archiveNode, SdkRepoConstants.NODE_SIZE, 0), - PackageParserUtils.getXmlString(archiveNode, SdkRepoConstants.NODE_CHECKSUM) - ); - - return a; - } - - /** - * Returns the source that created (and owns) this package. Can be null. - */ - @Nullable - public SdkSource getParentSource() { - return mSource; - } - - /** - * Returns true if the package is deemed obsolete, that is it contains an - * actual <obsolete> element. - */ - public boolean isObsolete() { - return mObsolete != null; - } - - /** - * Returns the revision, an int > 0, for all packages (platform, add-on, tool, doc). - * Can be 0 if this is a local package of unknown revision. - */ - @NonNull - public abstract FullRevision getRevision(); - - /** - * Returns the optional description for all packages (platform, add-on, tool, doc) or - * for a lib. It is null if the element has not been specified in the repository XML. - */ - @Nullable - public License getLicense() { - return mLicense; - } - - /** - * Returns the optional description for all packages (platform, add-on, tool, doc) or - * for a lib. This is the raw description available from the XML meta data and is typically - * only used internally. - *

- * For actual display in the UI, use the methods from {@link IDescription} instead. - *

- * Can be empty but not null. - */ - @NonNull - public String getDescription() { - return mDescription; - } - - /** - * Returns the optional list-display for all packages as defined in the XML meta data - * and is typically only used internally. - *

- * For actual display in the UI, use {@link IListDescription} instead. - *

- * Can be empty but not null. - */ - @NonNull - public String getListDisplay() { - return mListDisplay; - } - - /** - * Returns the optional description URL for all packages (platform, add-on, tool, doc). - * Can be empty but not null. - */ - @NonNull - public String getDescUrl() { - return mDescUrl; - } - - /** - * Returns the optional release note for all packages (platform, add-on, tool, doc) or - * for a lib. Can be empty but not null. - */ - @NonNull - public String getReleaseNote() { - return mReleaseNote; - } - - /** - * Returns the optional release note URL for all packages (platform, add-on, tool, doc). - * Can be empty but not null. - */ - @NonNull - public String getReleaseNoteUrl() { - return mReleaseUrl; - } - - /** - * Returns the archives defined in this package. - * Can be an empty array but not null. - */ - @NonNull - public Archive[] getArchives() { - return mArchives; - } - - /** - * Returns true if this package contains the exact given archive. - * Important: This compares object references, not object equality. - */ - public boolean hasArchive(Archive archive) { - for (Archive a : mArchives) { - if (a == archive) { - return true; - } - } - return false; - } - - /** - * Returns whether the {@link Package} has at least one {@link Archive} compatible with - * the host platform. - */ - public boolean hasCompatibleArchive() { - for (Archive archive : mArchives) { - if (archive.isCompatible()) { - return true; - } - } - - return false; - } - - /** - * Returns a short, reasonably unique string identifier that can be used - * to identify this package when installing from the command-line interface. - * {@code 'android list sdk'} will show these IDs and then in turn they can - * be provided to {@code 'android update sdk --no-ui --filter'} to select - * some specific packages. - *

- * The identifiers must have the following properties:
- * - They must contain only simple alphanumeric characters.
- * - Commas, whitespace and any special character that could be obviously problematic - * to a shell interface should be avoided (so dash/underscore are OK, but things - * like colon, pipe or dollar should be avoided.)
- * - The name must be consistent across calls and reasonably unique for the package - * type. Collisions can occur but should be rare.
- * - Different package types should have a clearly different name pattern.
- * - The revision number should not be included, as this would prevent updates - * from being automated (which is the whole point.)
- * - It must remain reasonably human readable.
- * - If no such id can exist (for example for a local package that cannot be installed) - * then an empty string should be returned. Don't return null. - *

- * Important: This is not a strong unique identifier for the package. - * If you need a strong unique identifier, you should use {@link #comparisonKey()} - * and the {@link Comparable} interface. - */ - @NonNull - public abstract String installId(); - - /** - * Returns the short description of the source, if not null. - * Otherwise returns the default Object toString result. - *

- * This is mostly helpful for debugging. - * For UI display, use the {@link IDescription} interface. - */ - @NonNull - @Override - public String toString() { - String s = getShortDescription(); - if (s != null) { - return s; - } - return super.toString(); - } - - /** - * Returns a description of this package that is suitable for a list display. - * Should not be empty. Can never be null. - *

- * Derived classes should use {@link #getListDisplay()} if it's not empty. - *

- * When it is empty, the default behavior is to recompute a string that depends - * on the package type. - *

- * In both cases, the string should indicate whether the package is marked as obsolete. - *

- * Note that this is the "base" name for the package with no specific revision nor API - * mentioned as this is likely used in a table that will already provide these details. - * In contrast, {@link #getShortDescription()} should be used if you want more details - * such as the package revision number or the API, if applicable, all in the same string. - */ - @NonNull - @Override - public abstract String getListDescription(); - - /** - * Returns a short description for an {@link IDescription}. - * Can be empty but not null. - */ - @NonNull - @Override - public abstract String getShortDescription(); - - /** - * Returns a long description for an {@link IDescription}. - * Can be empty but not null. - */ - @NonNull - @Override - public String getLongDescription() { - StringBuilder sb = new StringBuilder(); - - String s = getDescription(); - if (s != null) { - sb.append(s); - } - if (sb.length() > 0) { - sb.append("\n"); - } - - sb.append(String.format("Revision %1$s%2$s", - getRevision().toShortString(), - isObsolete() ? " (Obsolete)" : "")); - - s = getDescUrl(); - if (s != null && !s.isEmpty()) { - sb.append(String.format("\n\nMore information at %1$s", s)); - } - - s = getReleaseNote(); - if (s != null && !s.isEmpty()) { - sb.append("\n\nRelease note:\n").append(s); - } - - s = getReleaseNoteUrl(); - if (s != null && !s.isEmpty()) { - sb.append("\nRelease note URL: ").append(s); - } - - return sb.toString(); - } - - /** - * A package is local (that is 'installed locally') if it contains a single - * archive that is local. If not local, it's a remote package, only available - * on a remote source for download and installation. - */ - public boolean isLocal() { - return mArchives.length == 1 && mArchives[0].isLocal(); - } - - /** - * Computes a potential installation folder if an archive of this package were - * to be installed right away in the given SDK root. - *

- * Some types of packages install in a fix location, for example docs and tools. - * In this case the returned folder may already exist with a different archive installed - * at the desired location.
- * For other packages types, such as add-on or platform, the folder name is only partially - * relevant to determine the content and thus a real check will be done to provide an - * existing or new folder depending on the current content of the SDK. - *

- * Note that the installer *will* create all directories returned here just before - * installation so this method must not attempt to create them. - * - * @param osSdkRoot The OS path of the SDK root folder. - * @param sdkManager An existing SDK manager to list current platforms and addons. - * @return A new {@link File} corresponding to the directory to use to install this package. - */ - @NonNull - public abstract File getInstallFolder(String osSdkRoot, SdkManager sdkManager); - - /** - * Hook called right before an archive is installed. The archive has already - * been downloaded successfully and will be installed in the directory specified by - * installFolder when this call returns. - *

- * The hook lets the package decide if installation of this specific archive should - * be continue. The installer will still install the remaining packages if possible. - *

- * The base implementation always return true. - *

- * Note that the installer *will* create all directories specified by - * {@link #getInstallFolder} just before installation, so they must not be - * created here. This is also called before the previous install dir is removed - * so the previous content is still there during upgrade. - * - * @param archive The archive that will be installed - * @param monitor The {@link ITaskMonitor} to display errors. - * @param osSdkRoot The OS path of the SDK root folder. - * @param installFolder The folder where the archive will be installed. Note that this - * is not the folder where the archive was temporary - * unzipped. The installFolder, if it exists, contains the old - * archive that will soon be replaced by the new one. - * @return True if installing this archive shall continue, false if it should be skipped. - */ - public boolean preInstallHook(Archive archive, ITaskMonitor monitor, - String osSdkRoot, File installFolder) { - // Nothing to do in base class. - return true; - } - - /** - * Hook called right after a file has been unzipped (during an install). - *

- * The base class implementation makes sure to properly adjust set executable - * permission on Linux and MacOS system if the zip entry was marked as +x. - * - * @param archive The archive that is being installed. - * @param monitor The {@link ITaskMonitor} to display errors. - * @param fileOp The {@link IFileOp} used by the archive installer. - * @param unzippedFile The file that has just been unzipped in the install temp directory. - * @param zipEntry The {@link ZipArchiveEntry} that has just been unzipped. - */ - public void postUnzipFileHook( - Archive archive, - ITaskMonitor monitor, - IFileOp fileOp, - File unzippedFile, - ZipArchiveEntry zipEntry) { - - // if needed set the permissions. - if (sUsingUnixPerm && fileOp.isFile(unzippedFile)) { - // get the mode and test if it contains the executable bit - int mode = zipEntry.getUnixMode(); - if ((mode & 0111) != 0) { - try { - fileOp.setExecutablePermission(unzippedFile); - } catch (IOException ignore) {} - } - } - - } - - /** - * Hook called right after an archive has been installed. - * - * @param archive The archive that has been installed. - * @param monitor The {@link ITaskMonitor} to display errors. - * @param installFolder The folder where the archive was successfully installed. - * Null if the installation failed, in case the archive needs to - * do some cleanup after preInstallHook. - */ - public void postInstallHook(Archive archive, ITaskMonitor monitor, File installFolder) { - // Nothing to do in base class. - } - - /** - * Returns whether the give package represents the same item as the current package. - *

- * Two packages are considered the same if they represent the same thing, except for the - * revision number. - * @param pkg the package to compare. - * @return true if the item as equivalent. - */ - public abstract boolean sameItemAs(Package pkg); - - /** - * Computes whether the given package is a suitable update for the current package. - *

- * An update is just that: a new package that supersedes the current one. If the new - * package does not represent the same item or if it has the same or lower revision as the - * current one, it's not an update. - * - * @param replacementPackage The potential replacement package. - * @return One of the {@link UpdateInfo} values. - * - * @see #sameItemAs(Package) - */ - @NonNull - public abstract UpdateInfo canBeUpdatedBy(Package replacementPackage); - - /** - * Returns an ordering suitable for display like this:
- * - Tools
- * - Platform-Tools
- * - Built-Tools
- * - Docs.
- * - Platform n preview
- * - Platform n
- * - Platform n-1
- * - Samples packages
- * - Add-on based on n preview
- * - Add-on based on n
- * - Add-on based on n-1
- * - Extra packages
- *

- * Important: this must NOT be used to compare if two packages are the same thing. - * This is achieved by {@link #sameItemAs(Package)} or {@link #canBeUpdatedBy(Package)}. - *

- * The order done here is suitable for display, and this may not be the appropriate - * order when comparing whether packages are equal or of greater revision -- if you need - * to compare revisions, then use {@link #getRevision()}{@code .compareTo(rev)} directly. - *

- * This {@link #compareTo(Package)} method is purely an implementation detail to - * perform the right ordering of the packages in the list of available or installed packages. - *

- * Important: Derived classes should consider overriding {@link #comparisonKey()} - * instead of this method. - */ - @Override - public int compareTo(Package other) { - String s1 = this.comparisonKey(); - String s2 = other.comparisonKey(); - - int r = s1.compareTo(s2); - return r; - } - - /** - * Computes a comparison key for each package used by {@link #compareTo(Package)}. - * The key is a string. - * The base package class return a string that encodes the package type, - * the revision number and the platform version, if applicable, in the form: - *

-     *      t:N|v:NNNN.P|r:NNNN|
-     * 
- * All fields must start by a "letter colon" prefix and end with a vertical pipe (|, ASCII 124). - *

- * The string format may change between releases and clients should not - * store them outside of the session or expect them to be consistent between - * different releases. They are purely an internal implementation details of the - * {@link #compareTo(Package)} method. - *

- * Derived classes should get the string from the super class and then append - * or insert their own |-separated content. - * For example an extra vendor name & path can be inserted before the revision - * number, since it has more sorting weight. - */ - @NonNull - protected String comparisonKey() { - - StringBuilder sb = new StringBuilder(); - - sb.append("t:"); //$NON-NLS-1$ - if (this instanceof ToolPackage) { - sb.append(0); - } else if (this instanceof PlatformToolPackage) { - sb.append(1); - } else if (this instanceof BuildToolPackage) { - sb.append(2); - } else if (this instanceof DocPackage) { - sb.append(3); - } else if (this instanceof PlatformPackage) { - sb.append(4); - } else if (this instanceof SamplePackage) { - sb.append(5); - } else if ((this instanceof SystemImagePackage) && ((SystemImagePackage) this).isPlatform()) { - sb.append(6); - } else if (this instanceof AddonPackage) { - sb.append(7); - } else if (this instanceof SystemImagePackage) { - sb.append(8); - } else { - // extras and everything else - sb.append(9); - } - - - // We insert the package version here because it is more important - // than the revision number. - // In the list display, we want package version to be sorted - // top-down, so we'll use 10k-api as the sorting key. The day we - // reach 10k APIs, we'll need to revisit this. - sb.append("|v:"); //$NON-NLS-1$ - if (this instanceof IAndroidVersionProvider) { - AndroidVersion v = ((IAndroidVersionProvider) this).getAndroidVersion(); - - sb.append(String.format("%1$04d.%2$d", //$NON-NLS-1$ - 10000 - v.getApiLevel(), - v.isPreview() ? 1 : 0 - )); - } - - // Append revision number - // Note: pad revision numbers to 4 digits (e.g. make sure 12>3 by comparing 0012 and 0003) - sb.append("|r:"); //$NON-NLS-1$ - FullRevision rev = getRevision(); - sb.append(String.format("%1$04d.%2$04d.%3$04d.", //$NON-NLS-1$ - rev.getMajor(), - rev.getMinor(), - rev.getMicro())); - // Hack: When comparing packages for installation purposes, we want to treat - // "final releases" packages as more important than rc/preview packages. - // However like for the API level above, when sorting for list display purposes - // we want the final release package listed before its rc/preview packages. - if (rev.isPreview()) { - sb.append(rev.getPreview()); - } else { - sb.append('0'); // 0=Final (!preview), to make "18.0" < "18.1" (18 Final < 18 RC1) - } - - sb.append('|'); - return sb.toString(); - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + Arrays.hashCode(mArchives); - result = prime * result + ((mObsolete == null) ? 0 : mObsolete.hashCode()); - result = prime * result + getRevision().hashCode(); - result = prime * result + ((mSource == null) ? 0 : mSource.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (!(obj instanceof Package)) { - return false; - } - Package other = (Package) obj; - if (!Arrays.equals(mArchives, other.mArchives)) { - return false; - } - if (mObsolete == null) { - if (other.mObsolete != null) { - return false; - } - } else if (!mObsolete.equals(other.mObsolete)) { - return false; - } - if (!getRevision().equals(other.getRevision())) { - return false; - } - if (mSource == null) { - if (other.mSource != null) { - return false; - } - } else if (!mSource.equals(other.mSource)) { - return false; - } - return true; - } - - // TODO(jbakermalone): This is moved here from the more logical location in PkgDesc.Builder since Package will soon be forked into - // studio and this version deprecated, whereas PkgDesc will not. - protected PkgDesc.Builder setDescriptions(PkgDesc.Builder builder) { - builder.setDescriptionShort(getShortDescription()); - builder.setDescriptionUrl(getDescUrl()); - builder.setListDisplay(getListDisplay()); - builder.setIsObsolete(isObsolete()); - builder.setLicense(getLicense()); - return builder; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/PackageParserUtils.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/PackageParserUtils.java deleted file mode 100644 index eeee4a9e..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/PackageParserUtils.java +++ /dev/null @@ -1,427 +0,0 @@ -/* - * 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.sdklib.internal.repository.packages; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.sdklib.internal.repository.archives.ArchFilter; -import com.android.sdklib.internal.repository.archives.BitSize; -import com.android.sdklib.internal.repository.archives.HostOs; -import com.android.sdklib.internal.repository.archives.LegacyArch; -import com.android.sdklib.internal.repository.archives.LegacyOs; -import com.android.sdklib.repository.FullRevision; -import com.android.sdklib.repository.NoPreviewRevision; -import com.android.sdklib.repository.MajorRevision; -import com.android.sdklib.repository.PkgProps; -import com.android.sdklib.repository.SdkRepoConstants; - -import org.w3c.dom.Node; - -import java.util.Properties; - -/** - * Misc utilities to help extracting elements and attributes out of a repository XML document. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class PackageParserUtils { - - /** - * Parse the {@link ArchFilter} of an <archive> element.. - *

- * Starting with repo schema 10, add-on schema 7 and sys-img schema 3, this is done using - * specific optional elements contained within the <archive> element. - *

- * If none of the new element are defined, for backward compatibility we try to find - * the previous style XML attributes "os" and "arch" in the <archive> element. - * - * @param archiveNode - * @return A new {@link ArchFilter} - */ - @NonNull - public static ArchFilter parseArchFilter(@NonNull Node archiveNode) { - String hos = PackageParserUtils.getOptionalXmlString(archiveNode, SdkRepoConstants.NODE_HOST_OS); - String hb = PackageParserUtils.getOptionalXmlString(archiveNode, SdkRepoConstants.NODE_HOST_BITS); - String jb = PackageParserUtils.getOptionalXmlString(archiveNode, SdkRepoConstants.NODE_JVM_BITS); - String mjv = PackageParserUtils.getOptionalXmlString(archiveNode, SdkRepoConstants.NODE_MIN_JVM_VERSION); - - if (hos != null || hb != null || jb != null || mjv != null) { - NoPreviewRevision rev = null; - try { - rev = NoPreviewRevision.parseRevision(mjv); - } catch (NumberFormatException ignore) {} - - return new ArchFilter( - HostOs.fromXmlName(hos), - BitSize.fromXmlName(hb), - BitSize.fromXmlName(jb), - rev); - } - - Properties props = new Properties(); - - LegacyOs o = (LegacyOs) PackageParserUtils.getEnumAttribute( - archiveNode, SdkRepoConstants.LEGACY_ATTR_OS, LegacyOs.values(), null); - if (o != null) { - props.setProperty(ArchFilter.LEGACY_PROP_OS, o.toString()); - } - - LegacyArch a = (LegacyArch) PackageParserUtils.getEnumAttribute( - archiveNode, SdkRepoConstants.LEGACY_ATTR_ARCH, LegacyArch.values(), null); - if (a != null) { - props.setProperty(ArchFilter.LEGACY_PROP_ARCH, a.toString()); - } - - return new ArchFilter(props); - } - - /** - * Parses a full revision element such as or . - * This supports both the single-integer format as well as the full revision - * format with major/minor/micro/preview sub-elements. - * - * @param revisionNode The node to parse. - * @return A new {@link FullRevision}. If parsing failed, major is set to - * {@link FullRevision#MISSING_MAJOR_REV}. - */ - public static FullRevision parseFullRevisionElement(Node revisionNode) { - // This needs to support two modes: - // - For repository XSD >= 7, contains sub-elements such as or . - // - Otherwise for repository XSD < 7, contains an integer. - // The element is mandatory, so it's easy to distinguish between both cases. - int major = FullRevision.MISSING_MAJOR_REV, - minor = FullRevision.IMPLICIT_MINOR_REV, - micro = FullRevision.IMPLICIT_MICRO_REV, - preview = FullRevision.NOT_A_PREVIEW; - - if (revisionNode != null) { - if (PackageParserUtils.findChildElement(revisionNode, - SdkRepoConstants.NODE_MAJOR_REV) != null) { - // has a sub-element, so it's a repository XSD >= 7. - major = PackageParserUtils.getXmlInt(revisionNode, - SdkRepoConstants.NODE_MAJOR_REV, FullRevision.MISSING_MAJOR_REV); - minor = PackageParserUtils.getXmlInt(revisionNode, - SdkRepoConstants.NODE_MINOR_REV, FullRevision.IMPLICIT_MINOR_REV); - micro = PackageParserUtils.getXmlInt(revisionNode, - SdkRepoConstants.NODE_MICRO_REV, FullRevision.IMPLICIT_MICRO_REV); - preview = PackageParserUtils.getXmlInt(revisionNode, - SdkRepoConstants.NODE_PREVIEW, FullRevision.NOT_A_PREVIEW); - } else { - try { - String majorStr = revisionNode.getTextContent().trim(); - major = Integer.parseInt(majorStr); - } catch (Exception e) { - } - } - } - - return new FullRevision(major, minor, micro, preview); - } - - /** - * Parses a no-preview revision element such as >. - * This supports both the single-integer format as well as the full revision - * format with major/minor/micro sub-elements. - * - * @param revisionNode The node to parse. - * @return A new {@link NoPreviewRevision}. If parsing failed, major is set to - * {@link FullRevision#MISSING_MAJOR_REV}. - */ - public static NoPreviewRevision parseNoPreviewRevisionElement(Node revisionNode) { - // This needs to support two modes: - // - For addon XSD >= 6, contains sub-elements such as or . - // - Otherwise for addon XSD < 6, contains an integer. - // The element is mandatory, so it's easy to distinguish between both cases. - int major = FullRevision.MISSING_MAJOR_REV, - minor = FullRevision.IMPLICIT_MINOR_REV, - micro = FullRevision.IMPLICIT_MICRO_REV; - - if (revisionNode != null) { - if (PackageParserUtils.findChildElement(revisionNode, - SdkRepoConstants.NODE_MAJOR_REV) != null) { - // has a sub-element, so it's a repository XSD >= 7. - major = PackageParserUtils.getXmlInt(revisionNode, - SdkRepoConstants.NODE_MAJOR_REV, FullRevision.MISSING_MAJOR_REV); - minor = PackageParserUtils.getXmlInt(revisionNode, - SdkRepoConstants.NODE_MINOR_REV, FullRevision.IMPLICIT_MINOR_REV); - micro = PackageParserUtils.getXmlInt(revisionNode, - SdkRepoConstants.NODE_MICRO_REV, FullRevision.IMPLICIT_MICRO_REV); - } else { - try { - String majorStr = revisionNode.getTextContent().trim(); - major = Integer.parseInt(majorStr); - } catch (Exception e) { - } - } - } - - return new NoPreviewRevision(major, minor, micro); - } - - /** - * Returns the first child element with the given XML local name and the same NS URI. - * If xmlLocalName is null, returns the very first child element. - */ - public static Node findChildElement(Node node, String xmlLocalName) { - if (node != null) { - String nsUri = node.getNamespaceURI(); - for(Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) { - if (child.getNodeType() == Node.ELEMENT_NODE) { - String nsUriChild = child.getNamespaceURI(); - if ((nsUri == null && nsUriChild == null) || - (nsUri != null && nsUri.equals(nsUriChild))) { - if (xmlLocalName == null || xmlLocalName.equals(child.getLocalName())) { - return child; - } - } - } - } - } - return null; - } - - /** - * Retrieves the value of that XML element as a string. - * Returns an empty string whether the element is missing or empty, - * so you can't tell the difference. - *

- * Note: use {@link #getOptionalXmlString(Node, String)} if you need to know when the - * element is missing versus empty. - * - * @param node The XML parent node to parse. - * @param xmlLocalName The XML local name to find in the parent node. - * @return The text content of the element. Returns an empty string whether the element - * is missing or empty, so you can't tell the difference. - */ - public static String getXmlString(Node node, String xmlLocalName) { - return getXmlString(node, xmlLocalName, ""); //$NON-NLS-1$ - } - - /** - * Retrieves the value of that XML element as a string. - * Returns the defaultValue if the element is missing or empty. - *

- * Note: use {@link #getOptionalXmlString(Node, String)} if you need to know when the - * element is missing versus empty. - * - * @param node The XML parent node to parse. - * @param xmlLocalName The XML local name to find in the parent node. - * @param defaultValue A default value to return if the element is missing. - * @return The text content of the element - * or the defaultValue if the element is missing or empty. - */ - public static String getXmlString(Node node, String xmlLocalName, String defaultValue) { - Node child = findChildElement(node, xmlLocalName); - String content = child == null ? null : child.getTextContent(); - return content == null || content.isEmpty() ? defaultValue : content; - } - - /** - * Retrieves the value of that XML element as a string. - * Returns null when the element is missing, so you can tell between a missing element - * and an empty one. - *

- * Note: use {@link #getXmlString(Node, String)} if you don't need to know when the - * element is missing versus empty. - * - * @param node The XML parent node to parse. - * @param xmlLocalName The XML local name to find in the parent node. - * @return The text content of the element. Returns null when the element is missing. - * Returns an empty string whether the element is present but empty. - */ - public static String getOptionalXmlString(Node node, String xmlLocalName) { - Node child = findChildElement(node, xmlLocalName); - return child == null ? null : child.getTextContent(); //$NON-NLS-1$ - } - - /** - * Retrieves the value of that XML element as an integer. - * Returns the default value when the element is missing or is not an integer. - */ - public static int getXmlInt(Node node, String xmlLocalName, int defaultValue) { - String s = getXmlString(node, xmlLocalName); - try { - return Integer.parseInt(s); - } catch (NumberFormatException e) { - return defaultValue; - } - } - - /** - * Retrieves the value of that XML element as a long. - * Returns the default value when the element is missing or is not an integer. - */ - public static long getXmlLong(Node node, String xmlLocalName, long defaultValue) { - String s = getXmlString(node, xmlLocalName); - try { - return Long.parseLong(s); - } catch (NumberFormatException e) { - return defaultValue; - } - } - - /** - * Retrieve an attribute which value must match one of the given enums using a - * case-insensitive name match. - * - * Returns defaultValue if the attribute does not exist or its value does not match - * the given enum values. - */ - public static Object getEnumAttribute( - Node archiveNode, - String attrName, - Object[] values, - Object defaultValue) { - - Node attr = archiveNode.getAttributes().getNamedItem(attrName); - if (attr != null) { - String found = attr.getNodeValue(); - for (Object value : values) { - if (value.toString().equalsIgnoreCase(found)) { - return value; - } - } - } - - return defaultValue; - } - - /** - * Utility method that returns a property from a {@link Properties} object. - * Returns the default value if props is null or if the property is not defined. - * - * @param props The {@link Properties} to search into. - * If null, the default value is returned. - * @param propKey The name of the property. Must not be null. - * @param defaultValue The default value to return if {@code props} is null or if the - * key is not found. Can be null. - * @return The string value of the given key in the properties, or null if the key - * isn't found or if {@code props} is null. - */ - @Nullable - public static String getProperty( - @Nullable Properties props, - @NonNull String propKey, - @Nullable String defaultValue) { - if (props == null) { - return defaultValue; - } - return props.getProperty(propKey, defaultValue); - } - - /** - * Utility method that returns an integer property from a {@link Properties} object. - * Returns the default value if props is null or if the property is not defined or - * cannot be parsed to an integer. - * - * @param props The {@link Properties} to search into. - * If null, the default value is returned. - * @param propKey The name of the property. Must not be null. - * @param defaultValue The default value to return if {@code props} is null or if the - * key is not found. Can be null. - * @return The integer value of the given key in the properties, or the {@code defaultValue}. - */ - public static int getPropertyInt( - @Nullable Properties props, - @NonNull String propKey, - int defaultValue) { - String s = props != null ? props.getProperty(propKey, null) : null; - if (s != null) { - try { - return Integer.parseInt(s); - } catch (Exception ignore) {} - } - return defaultValue; - } - - /** - * Utility method to parse the {@link PkgProps#PKG_REVISION} property as a full - * revision (major.minor.micro.preview). - * - * @param props The properties to parse. - * @return A {@link FullRevision} or null if there is no such property or it couldn't be parsed. - * @param propKey The name of the property. Must not be null. - */ - @Nullable - public static FullRevision getPropertyFull( - @Nullable Properties props, - @NonNull String propKey) { - String revStr = getProperty(props, propKey, null); - - FullRevision rev = null; - if (revStr != null) { - try { - rev = FullRevision.parseRevision(revStr); - } catch (NumberFormatException ignore) {} - } - - return rev; - } - - /** - * Utility method to parse the {@link PkgProps#PKG_REVISION} property as a major - * revision (major integer, no minor/micro/preview parts.) - * - * @param props The properties to parse. - * @return A {@link MajorRevision} or null if there is no such property or it couldn't be parsed. - * @param propKey The name of the property. Must not be null. - */ - @Nullable - public static MajorRevision getPropertyMajor( - @Nullable Properties props, - @NonNull String propKey) { - String revStr = getProperty(props, propKey, null); - - MajorRevision rev = null; - if (revStr != null) { - try { - rev = MajorRevision.parseRevision(revStr); - } catch (NumberFormatException ignore) {} - } - - return rev; - } - - /** - * Utility method to parse the {@link PkgProps#PKG_REVISION} property as a no-preview - * revision (major.minor.micro integers but no preview part.) - * - * @param props The properties to parse. - * @return A {@link NoPreviewRevision} or - * null if there is no such property or it couldn't be parsed. - * @param propKey The name of the property. Must not be null. - */ - @Nullable - public static NoPreviewRevision getPropertyNoPreview( - @Nullable Properties props, - @NonNull String propKey) { - String revStr = getProperty(props, propKey, null); - - NoPreviewRevision rev = null; - if (revStr != null) { - try { - rev = NoPreviewRevision.parseRevision(revStr); - } catch (NumberFormatException ignore) {} - } - - return rev; - } - -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/PlatformPackage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/PlatformPackage.java deleted file mode 100644 index ff4e4bb7..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/PlatformPackage.java +++ /dev/null @@ -1,388 +0,0 @@ -/* - * 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.sdklib.internal.repository.packages; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.annotations.VisibleForTesting; -import com.android.annotations.VisibleForTesting.Visibility; -import com.android.sdklib.AndroidTargetHash; -import com.android.sdklib.AndroidVersion; -import com.android.sdklib.IAndroidTarget; -import com.android.sdklib.SdkManager; -import com.android.sdklib.repository.IDescription; -import com.android.sdklib.internal.repository.sources.SdkSource; -import com.android.sdklib.repository.MajorRevision; -import com.android.sdklib.repository.PkgProps; -import com.android.sdklib.repository.SdkRepoConstants; -import com.android.sdklib.repository.descriptors.IPkgDesc; -import com.android.sdklib.repository.descriptors.PkgDesc; -import com.android.utils.Pair; - -import org.w3c.dom.Node; - -import java.io.File; -import java.util.Map; -import java.util.Properties; - -/** - * Represents a platform XML node in an SDK repository. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class PlatformPackage extends MinToolsPackage - implements IAndroidVersionProvider, ILayoutlibVersion { - - /** The package version, for platform, add-on and doc packages. */ - private final AndroidVersion mVersion; - - /** The version, a string, for platform packages. */ - private final String mVersionName; - - /** The ABI of the system-image included in this platform. Can be null but not empty. */ - private final String mIncludedAbi; - - /** The helper handling the layoutlib version. */ - private final LayoutlibVersionMixin mLayoutlibVersion; - - private final IPkgDesc mPkgDesc; - - /** - * Creates a new platform package from the attributes and elements of the given XML node. - * This constructor should throw an exception if the package cannot be created. - * - * @param source The {@link SdkSource} where this is loaded from. - * @param packageNode The XML element being parsed. - * @param nsUri The namespace URI of the originating XML document, to be able to deal with - * parameters that vary according to the originating XML schema. - * @param licenses The licenses loaded from the XML originating document. - */ - public PlatformPackage( - SdkSource source, - Node packageNode, - String nsUri, - Map licenses) { - super(source, packageNode, nsUri, licenses); - - mVersionName = - PackageParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_VERSION); - - int apiLevel = - PackageParserUtils.getXmlInt (packageNode, SdkRepoConstants.NODE_API_LEVEL, 0); - String codeName = - PackageParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_CODENAME); - if (codeName.isEmpty()) { - codeName = null; - } - mVersion = new AndroidVersion(apiLevel, codeName); - - mIncludedAbi = PackageParserUtils.getOptionalXmlString(packageNode, - SdkRepoConstants.NODE_ABI_INCLUDED); - - mLayoutlibVersion = new LayoutlibVersionMixin(packageNode); - - mPkgDesc = setDescriptions(PkgDesc.Builder - .newPlatform(mVersion, (MajorRevision) getRevision(), getMinToolsRevision())) - .create(); - } - - /** - * Creates a new platform package based on an actual {@link IAndroidTarget} (which - * must have {@link IAndroidTarget#isPlatform()} true) from the {@link SdkManager}. - * This is used to list local SDK folders in which case there is one archive which - * URL is the actual target location. - *

- * By design, this creates a package with one and only one archive. - */ - public static Package create(@NonNull IAndroidTarget target, @Nullable Properties props) { - return new PlatformPackage(target, props); - } - - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected PlatformPackage(@NonNull IAndroidTarget target, @Nullable Properties props) { - this(null /*source*/, target, props); - } - - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected PlatformPackage(@Nullable SdkSource source, - @NonNull IAndroidTarget target, - @Nullable Properties props) { - super( source, //source - props, //properties - target.getRevision(), //revision - null, //license - target.getDescription(), //description - null, //descUrl - target.getLocation() //archiveOsPath - ); - - mVersion = target.getVersion(); - mVersionName = target.getVersionName(); - mLayoutlibVersion = new LayoutlibVersionMixin(props); - mIncludedAbi = props == null ? null : props.getProperty(PkgProps.PLATFORM_INCLUDED_ABI); - - mPkgDesc = setDescriptions(PkgDesc.Builder - .newPlatform(mVersion, (MajorRevision) getRevision(), getMinToolsRevision())) - .create(); - } - - @Override - @NonNull - public IPkgDesc getPkgDesc() { - return mPkgDesc; - } - - /** - * Save the properties of the current packages in the given {@link Properties} object. - * These properties will later be given to a constructor that takes a {@link Properties} object. - */ - @Override - public void saveProperties(Properties props) { - super.saveProperties(props); - - mVersion.saveProperties(props); - mLayoutlibVersion.saveProperties(props); - - if (mVersionName != null) { - props.setProperty(PkgProps.PLATFORM_VERSION, mVersionName); - } - - if (mIncludedAbi != null) { - props.setProperty(PkgProps.PLATFORM_INCLUDED_ABI, mIncludedAbi); - } - - } - - /** Returns the version, a string, for platform packages. */ - public String getVersionName() { - return mVersionName; - } - - /** Returns the package version, for platform, add-on and doc packages. */ - @Override @NonNull - public AndroidVersion getAndroidVersion() { - return mVersion; - } - - /** - * Returns the ABI of the system-image included in this platform. - * - * @return Null if the platform does not include any system-image. - * Otherwise should be a valid non-empty ABI string (e.g. "x86" or "armeabi-v7a"). - */ - public String getIncludedAbi() { - return mIncludedAbi; - } - - /** - * Returns the layoutlib version. Mandatory starting with repository XSD rev 4. - *

- * The first integer is the API of layoublib, which should be > 0. - * It will be equal to {@link ILayoutlibVersion#LAYOUTLIB_API_NOT_SPECIFIED} (0) - * if the layoutlib version isn't specified. - *

- * The second integer is the revision for that given API. It is >= 0 - * and works as a minor revision number, incremented for the same API level. - * - * @since sdk-repository-4.xsd - */ - @Override - public Pair getLayoutlibVersion() { - return mLayoutlibVersion.getLayoutlibVersion(); - } - - /** - * Returns a string identifier to install this package from the command line. - * For platforms, we use "android-N" where N is the API or the preview codename. - *

- * {@inheritDoc} - */ - @Override - public String installId() { - return AndroidTargetHash.getPlatformHashString(mVersion); - } - - /** - * Returns a description of this package that is suitable for a list display. - *

- * {@inheritDoc} - */ - @Override - public String getListDescription() { - String ld = getListDisplay(); - if (!ld.isEmpty()) { - return String.format("%1$s%2$s", ld, isObsolete() ? " (Obsolete)" : ""); - } - - String s; - if (mVersion.isPreview()) { - s = String.format("SDK Platform Android %1$s Preview%2$s", - getVersionName(), - isObsolete() ? " (Obsolete)" : ""); //$NON-NLS-2$ - } else { - s = String.format("SDK Platform Android %1$s%2$s", - getVersionName(), - isObsolete() ? " (Obsolete)" : ""); //$NON-NLS-2$ - } - - return s; - } - - /** - * Returns a short description for an {@link IDescription}. - */ - @Override - public String getShortDescription() { - String ld = getListDisplay(); - if (!ld.isEmpty()) { - return String.format("%1$s, revision %2$s%3$s", - ld, - getRevision().toShortString(), - isObsolete() ? " (Obsolete)" : ""); - } - - String s; - if (mVersion.isPreview()) { - s = String.format("SDK Platform Android %1$s Preview, revision %2$s%3$s", - getVersionName(), - getRevision().toShortString(), - isObsolete() ? " (Obsolete)" : ""); //$NON-NLS-2$ - } else { - s = String.format("SDK Platform Android %1$s, API %2$d, revision %3$s%4$s", - getVersionName(), - mVersion.getApiLevel(), - getRevision().toShortString(), - isObsolete() ? " (Obsolete)" : ""); //$NON-NLS-2$ - } - - return s; - } - - /** - * Returns a long description for an {@link IDescription}. - * - * The long description is whatever the XML contains for the <description> field, - * or the short description if the former is empty. - */ - @Override - public String getLongDescription() { - String s = getDescription(); - if (s == null || s.isEmpty()) { - s = getShortDescription(); - } - - if (s.indexOf("revision") == -1) { - s += String.format("\nRevision %1$s%2$s", - getRevision().toShortString(), - isObsolete() ? " (Obsolete)" : ""); - } - - return s; - } - - /** - * Computes a potential installation folder if an archive of this package were - * to be installed right away in the given SDK root. - *

- * A platform package is typically installed in SDK/platforms/android-"version". - * However if we can find a different directory under SDK/platform that already - * has this platform version installed, we'll use that one. - * - * @param osSdkRoot The OS path of the SDK root folder. - * @param sdkManager An existing SDK manager to list current platforms and addons. - * @return A new {@link File} corresponding to the directory to use to install this package. - */ - @Override - public File getInstallFolder(String osSdkRoot, SdkManager sdkManager) { - - // First find if this platform is already installed. If so, reuse the same directory. - for (IAndroidTarget target : sdkManager.getTargets()) { - if (target.isPlatform() && target.getVersion().equals(mVersion)) { - return new File(target.getLocation()); - } - } - - File platforms = new File(osSdkRoot, SdkConstants.FD_PLATFORMS); - File folder = new File(platforms, - String.format("android-%s", getAndroidVersion().getApiString())); //$NON-NLS-1$ - - return folder; - } - - @Override - public boolean sameItemAs(Package pkg) { - if (pkg instanceof PlatformPackage) { - PlatformPackage newPkg = (PlatformPackage)pkg; - - // check they are the same version. - return newPkg.getAndroidVersion().equals(this.getAndroidVersion()); - } - - return false; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = super.hashCode(); - result = prime * result + - ((mLayoutlibVersion == null) ? 0 : mLayoutlibVersion.hashCode()); - result = prime * result + ((mVersion == null) ? 0 : mVersion.hashCode()); - result = prime * result + ((mVersionName == null) ? 0 : mVersionName.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (!super.equals(obj)) { - return false; - } - if (!(obj instanceof PlatformPackage)) { - return false; - } - PlatformPackage other = (PlatformPackage) obj; - if (mLayoutlibVersion == null) { - if (other.mLayoutlibVersion != null) { - return false; - } - } else if (!mLayoutlibVersion.equals(other.mLayoutlibVersion)) { - return false; - } - if (mVersion == null) { - if (other.mVersion != null) { - return false; - } - } else if (!mVersion.equals(other.mVersion)) { - return false; - } - if (mVersionName == null) { - if (other.mVersionName != null) { - return false; - } - } else if (!mVersionName.equals(other.mVersionName)) { - return false; - } - return true; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/PlatformToolPackage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/PlatformToolPackage.java deleted file mode 100644 index 85c69532..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/PlatformToolPackage.java +++ /dev/null @@ -1,313 +0,0 @@ -/* - * Copyright (C) 2010 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.sdklib.internal.repository.packages; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.annotations.VisibleForTesting; -import com.android.annotations.VisibleForTesting.Visibility; -import com.android.sdklib.SdkManager; -import com.android.sdklib.internal.repository.AdbWrapper; -import com.android.sdklib.internal.repository.ITaskMonitor; -import com.android.sdklib.internal.repository.archives.Archive; -import com.android.sdklib.internal.repository.sources.SdkSource; -import com.android.sdklib.repository.FullRevision.PreviewComparison; -import com.android.sdklib.repository.IDescription; -import com.android.sdklib.repository.descriptors.IPkgDesc; -import com.android.sdklib.repository.descriptors.PkgDesc; -import org.w3c.dom.Node; - -import java.io.File; -import java.util.HashSet; -import java.util.Map; -import java.util.Properties; -import java.util.Set; - -/** - * Represents a platform-tool XML node in an SDK repository. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class PlatformToolPackage extends FullRevisionPackage { - - /** The value returned by {@link PlatformToolPackage#installId()}. */ - public static final String INSTALL_ID = "platform-tools"; //$NON-NLS-1$ - /** The value returned by {@link PlatformToolPackage#installId()}. */ - public static final String INSTALL_ID_PREVIEW = "platform-tools-preview"; //$NON-NLS-1$ - - private final IPkgDesc mPkgDesc; - - /** - * Creates a new platform-tool package from the attributes and elements of the given XML node. - * This constructor should throw an exception if the package cannot be created. - * - * @param source The {@link SdkSource} where this is loaded from. - * @param packageNode The XML element being parsed. - * @param nsUri The namespace URI of the originating XML document, to be able to deal with - * parameters that vary according to the originating XML schema. - * @param licenses The licenses loaded from the XML originating document. - */ - public PlatformToolPackage(SdkSource source, Node packageNode, - String nsUri, Map licenses) { - super(source, packageNode, nsUri, licenses); - - mPkgDesc = setDescriptions(PkgDesc.Builder.newPlatformTool(getRevision())).create(); - } - - /** - * Manually create a new package with one archive and the given attributes or properties. - * This is used to create packages from local directories in which case there must be - * one archive which URL is the actual target location. - *

- * By design, this creates a package with one and only one archive. - */ - public static Package create( - SdkSource source, - Properties props, - int revision, - String license, - String description, - String descUrl, - String archiveOsPath) { - - PlatformToolPackage ptp = new PlatformToolPackage(source, props, revision, license, - description, descUrl, archiveOsPath); - - File platformToolsFolder = new File(archiveOsPath); - String error = null; - if (!platformToolsFolder.isDirectory()) { - error = "platform-tools folder is missing"; - } else { - File[] files = platformToolsFolder.listFiles(); - if (files == null || files.length == 0) { - error = "platform-tools folder is empty"; - } else { - Set names = new HashSet(); - for (File file : files) { - names.add(file.getName()); - } - - // Package-tools revision 17+ matches sdk-repository-8 and above - // and only requires adb (other tools moved to the build-tool packages.) - String[] expected = new String[] { SdkConstants.FN_ADB }; - if (ptp.getRevision().getMajor() < 17) { - // Platform-tools before revision 17 should have adb, aapt, aidl and dx. - expected = new String[] { SdkConstants.FN_ADB, - SdkConstants.FN_AAPT, - SdkConstants.FN_AIDL, - SdkConstants.FN_DX }; - } - - for (String name : expected) { - if (!names.contains(name)) { - if (error == null) { - error = "platform-tools folder is missing "; - } else { - error += ", "; - } - error += name; - } - } - } - } - - if (error != null) { - String shortDesc = ptp.getShortDescription() + " [*]"; //$NON-NLS-1$ - - String longDesc = String.format( - "Broken Platform-Tools Package: %1$s\n" + - "[*] Package cannot be used due to error: %2$s", - description, - error); - - BrokenPackage ba = new BrokenPackage(props, shortDesc, longDesc, - IMinApiLevelDependency.MIN_API_LEVEL_NOT_SPECIFIED, - IExactApiLevelDependency.API_LEVEL_INVALID, - archiveOsPath, - PkgDesc.Builder.newPlatformTool(ptp.getRevision()) - .setDescriptionShort(shortDesc) - .create()); - return ba; - } - - - return ptp; - } - - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected PlatformToolPackage( - SdkSource source, - Properties props, - int revision, - String license, - String description, - String descUrl, - String archiveOsPath) { - super(source, - props, - revision, - license, - description, - descUrl, - archiveOsPath); - - mPkgDesc = setDescriptions(PkgDesc.Builder.newPlatformTool(getRevision())).create(); - } - - @Override - @NonNull - public IPkgDesc getPkgDesc() { - return mPkgDesc; - } - - /** - * Returns a string identifier to install this package from the command line. - * For platform-tools, we use "platform-tools" or "platform-tools-preview" since - * this package type is unique. - *

- * {@inheritDoc} - */ - @Override - public String installId() { - if (getRevision().isPreview()) { - return INSTALL_ID_PREVIEW; - } else { - return INSTALL_ID; - } - } - - /** - * Returns a description of this package that is suitable for a list display. - *

- * {@inheritDoc} - */ - @Override - public String getListDescription() { - String ld = getListDisplay(); - if (!ld.isEmpty()) { - return String.format("%1$s%2$s", ld, isObsolete() ? " (Obsolete)" : ""); - } - - return String.format("Android SDK Platform-tools%1$s", - isObsolete() ? " (Obsolete)" : ""); - } - - /** - * Returns a short description for an {@link IDescription}. - */ - @Override - public String getShortDescription() { - String ld = getListDisplay(); - if (!ld.isEmpty()) { - return String.format("%1$s, revision %2$s%3$s", - ld, - getRevision().toShortString(), - isObsolete() ? " (Obsolete)" : ""); - } - - return String.format("Android SDK Platform-tools, revision %1$s%2$s", - getRevision().toShortString(), - isObsolete() ? " (Obsolete)" : ""); - } - - /** Returns a long description for an {@link IDescription}. */ - @Override - public String getLongDescription() { - String s = getDescription(); - if (s == null || s.isEmpty()) { - s = getShortDescription(); - } - - if (s.indexOf("revision") == -1) { - s += String.format("\nRevision %1$s%2$s", - getRevision().toShortString(), - isObsolete() ? " (Obsolete)" : ""); - } - - return s; - } - - /** - * Computes a potential installation folder if an archive of this package were - * to be installed right away in the given SDK root. - *

- * A "platform-tool" package should always be located in SDK/platform-tools. - * There can be only one installed at once. - * - * @param osSdkRoot The OS path of the SDK root folder. - * @param sdkManager An existing SDK manager to list current platforms and addons. - * @return A new {@link File} corresponding to the directory to use to install this package. - */ - @Override - public File getInstallFolder(String osSdkRoot, SdkManager sdkManager) { - return new File(osSdkRoot, SdkConstants.FD_PLATFORM_TOOLS); - } - - /** - * Check whether 2 platform-tool packages are the same and have the - * same preview bit. - */ - @Override - public boolean sameItemAs(Package pkg) { - return sameItemAs(pkg, PreviewComparison.COMPARE_TYPE); - } - - @Override - public boolean sameItemAs(Package pkg, PreviewComparison comparePreview) { - // only one platform-tool package so any platform-tool package is the same item. - if (pkg instanceof PlatformToolPackage) { - switch (comparePreview) { - case IGNORE: - return true; - - case COMPARE_NUMBER: - // Voluntary break-through. - case COMPARE_TYPE: - // There's only one platform-tools so the preview number doesn't matter; - // however previews can only match previews by default so both cases - // are treated the same. - return pkg.getRevision().isPreview() == getRevision().isPreview(); - } - } - return false; - } - - /** - * Hook called right before an archive is installed. - * This is used here to stop ADB before trying to replace the platform-tool package. - * - * @param archive The archive that will be installed - * @param monitor The {@link ITaskMonitor} to display errors. - * @param osSdkRoot The OS path of the SDK root folder. - * @param installFolder The folder where the archive will be installed. Note that this - * is not the folder where the archive was temporary - * unzipped. The installFolder, if it exists, contains the old - * archive that will soon be replaced by the new one. - * @return True if installing this archive shall continue, false if it should be skipped. - */ - @Override - public boolean preInstallHook(Archive archive, ITaskMonitor monitor, - String osSdkRoot, File installFolder) { - AdbWrapper aw = new AdbWrapper(osSdkRoot, monitor); - aw.stopAdb(); - return super.preInstallHook(archive, monitor, osSdkRoot, installFolder); - } - -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/SamplePackage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/SamplePackage.java deleted file mode 100644 index d31805ed..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/SamplePackage.java +++ /dev/null @@ -1,571 +0,0 @@ -/* - * 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.sdklib.internal.repository.packages; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.sdklib.AndroidVersion; -import com.android.sdklib.AndroidVersion.AndroidVersionException; -import com.android.sdklib.IAndroidTarget; -import com.android.sdklib.SdkManager; -import com.android.sdklib.repository.IDescription; -import com.android.sdklib.internal.repository.ITaskMonitor; -import com.android.sdklib.internal.repository.archives.Archive; -import com.android.sdklib.internal.repository.sources.SdkSource; -import com.android.sdklib.io.IFileOp; -import com.android.sdklib.repository.MajorRevision; -import com.android.sdklib.repository.PkgProps; -import com.android.sdklib.repository.SdkRepoConstants; -import com.android.sdklib.repository.descriptors.IPkgDesc; -import com.android.sdklib.repository.descriptors.PkgDesc; - -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; -import org.w3c.dom.Node; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Map; -import java.util.Properties; - -/** - * Represents a sample XML node in an SDK repository. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class SamplePackage extends MinToolsPackage - implements IAndroidVersionProvider, IMinApiLevelDependency { - - /** The matching platform version. */ - private final AndroidVersion mVersion; - - /** - * The minimal API level required by this extra package, if > 0, - * or {@link #MIN_API_LEVEL_NOT_SPECIFIED} if there is no such requirement. - */ - private final int mMinApiLevel; - - private final IPkgDesc mPkgDesc; - - /** - * Creates a new sample package from the attributes and elements of the given XML node. - * This constructor should throw an exception if the package cannot be created. - * - * @param source The {@link SdkSource} where this is loaded from. - * @param packageNode The XML element being parsed. - * @param nsUri The namespace URI of the originating XML document, to be able to deal with - * parameters that vary according to the originating XML schema. - * @param licenses The licenses loaded from the XML originating document. - */ - public SamplePackage(SdkSource source, - Node packageNode, - String nsUri, - Map licenses) { - super(source, packageNode, nsUri, licenses); - - int apiLevel = - PackageParserUtils.getXmlInt (packageNode, SdkRepoConstants.NODE_API_LEVEL, 0); - String codeName = - PackageParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_CODENAME); - if (codeName.isEmpty()) { - codeName = null; - } - mVersion = new AndroidVersion(apiLevel, codeName); - - mMinApiLevel = PackageParserUtils.getXmlInt(packageNode, - SdkRepoConstants.NODE_MIN_API_LEVEL, - MIN_API_LEVEL_NOT_SPECIFIED); - - mPkgDesc = setDescriptions(PkgDesc.Builder - .newSample(mVersion, (MajorRevision) getRevision(), getMinToolsRevision())) - .create(); - } - - /** - * Creates a new sample package based on an actual {@link IAndroidTarget} (which - * must have {@link IAndroidTarget#isPlatform()} true) from the {@link SdkManager}. - *

- * The target must have an existing sample directory that uses the /samples - * root form rather than the old form where the samples dir was located under the - * platform dir. - *

- * This is used to list local SDK folders in which case there is one archive which - * URL is the actual samples path location. - *

- * By design, this creates a package with one and only one archive. - */ - public static Package create(IAndroidTarget target, Properties props) { - return new SamplePackage(target, props); - } - - private SamplePackage(IAndroidTarget target, Properties props) { - super( null, //source - props, //properties - 0, //revision will be taken from props - null, //license - null, //description - null, //descUrl - target.getPath(IAndroidTarget.SAMPLES) //archiveOsPath - ); - - mVersion = target.getVersion(); - - mMinApiLevel = getPropertyInt(props, PkgProps.SAMPLE_MIN_API_LEVEL, - MIN_API_LEVEL_NOT_SPECIFIED); - - mPkgDesc = setDescriptions(PkgDesc.Builder - .newSample(mVersion, (MajorRevision) getRevision(), getMinToolsRevision())) - .create(); - } - - /** - * Creates a new sample package from an actual directory path and previously - * saved properties. - *

- * This is used to list local SDK folders in which case there is one archive which - * URL is the actual samples path location. - *

- * By design, this creates a package with one and only one archive. - * - * @throws AndroidVersionException if the {@link AndroidVersion} can't be restored - * from properties. - */ - public static Package create(String archiveOsPath, Properties props) - throws AndroidVersionException { - return new SamplePackage(archiveOsPath, props); - } - - private SamplePackage(String archiveOsPath, Properties props) throws AndroidVersionException { - super(null, //source - props, //properties - 0, //revision will be taken from props - null, //license - null, //description - null, //descUrl - archiveOsPath //archiveOsPath - ); - - mVersion = new AndroidVersion(props); - - mMinApiLevel = getPropertyInt(props, PkgProps.SAMPLE_MIN_API_LEVEL, - MIN_API_LEVEL_NOT_SPECIFIED); - - mPkgDesc = setDescriptions(PkgDesc.Builder - .newSample(mVersion, (MajorRevision) getRevision(), getMinToolsRevision())) - .create(); - } - - @Override - @NonNull - public IPkgDesc getPkgDesc() { - return mPkgDesc; - } - - /** - * Save the properties of the current packages in the given {@link Properties} object. - * These properties will later be given to a constructor that takes a {@link Properties} object. - */ - @Override - public void saveProperties(Properties props) { - super.saveProperties(props); - - mVersion.saveProperties(props); - - if (getMinApiLevel() != MIN_API_LEVEL_NOT_SPECIFIED) { - props.setProperty(PkgProps.SAMPLE_MIN_API_LEVEL, Integer.toString(getMinApiLevel())); - } - } - - /** - * Returns the minimal API level required by this extra package, if > 0, - * or {@link #MIN_API_LEVEL_NOT_SPECIFIED} if there is no such requirement. - */ - @Override - public int getMinApiLevel() { - return mMinApiLevel; - } - - /** Returns the matching platform version. */ - @Override @NonNull - public AndroidVersion getAndroidVersion() { - return mVersion; - } - - /** - * Returns a string identifier to install this package from the command line. - * For samples, we use "sample-N" where N is the API or the preview codename. - *

- * {@inheritDoc} - */ - @Override - public String installId() { - return "sample-" + mVersion.getApiString(); //$NON-NLS-1$ - } - - /** - * Returns a description of this package that is suitable for a list display. - *

- * {@inheritDoc} - */ - @Override - public String getListDescription() { - String ld = getListDisplay(); - if (!ld.isEmpty()) { - return String.format("%1$s%2$s", ld, isObsolete() ? " (Obsolete)" : ""); - } - - String s = String.format("Samples for SDK API %1$s%2$s%3$s", - mVersion.getApiString(), - mVersion.isPreview() ? " Preview" : "", - isObsolete() ? " (Obsolete)" : ""); - return s; - } - - /** - * Returns a short description for an {@link IDescription}. - */ - @Override - public String getShortDescription() { - String ld = getListDisplay(); - if (!ld.isEmpty()) { - return String.format("%1$s, revision %2$s%3$s", - ld, - getRevision().toShortString(), - isObsolete() ? " (Obsolete)" : ""); - } - - String s = String.format("Samples for SDK API %1$s%2$s, revision %3$s%4$s", - mVersion.getApiString(), - mVersion.isPreview() ? " Preview" : "", - getRevision().toShortString(), - isObsolete() ? " (Obsolete)" : ""); - return s; - } - - /** - * Returns a long description for an {@link IDescription}. - * - * The long description is whatever the XML contains for the <description> field, - * or the short description if the former is empty. - */ - @Override - public String getLongDescription() { - String s = getDescription(); - if (s == null || s.isEmpty()) { - s = getShortDescription(); - } - - if (s.indexOf("revision") == -1) { - s += String.format("\nRevision %1$s%2$s", - getRevision().toShortString(), - isObsolete() ? " (Obsolete)" : ""); - } - - return s; - } - - /** - * Computes a potential installation folder if an archive of this package were - * to be installed right away in the given SDK root. - *

- * A sample package is typically installed in SDK/samples/android-"version". - * However if we can find a different directory that already has this sample - * version installed, we'll use that one. - * - * @param osSdkRoot The OS path of the SDK root folder. - * @param sdkManager An existing SDK manager to list current platforms and addons. - * @return A new {@link File} corresponding to the directory to use to install this package. - */ - @Override - public File getInstallFolder(String osSdkRoot, SdkManager sdkManager) { - - // The /samples dir at the root of the SDK - File samplesRoot = new File(osSdkRoot, SdkConstants.FD_SAMPLES); - - // First find if this sample is already installed. If so, reuse the same directory. - for (IAndroidTarget target : sdkManager.getTargets()) { - if (target.isPlatform() && - target.getVersion().equals(mVersion)) { - String p = target.getPath(IAndroidTarget.SAMPLES); - File f = new File(p); - if (f.isDirectory()) { - // We *only* use this directory if it's using the "new" location - // under SDK/samples. We explicitly do not reuse the "old" location - // under SDK/platform/android-N/samples. - if (f.getParentFile().equals(samplesRoot)) { - return f; - } - } - } - } - - // Otherwise, get a suitable default - File folder = new File(samplesRoot, - String.format("android-%s", getAndroidVersion().getApiString())); //$NON-NLS-1$ - - for (int n = 1; folder.exists(); n++) { - // Keep trying till we find an unused directory. - folder = new File(samplesRoot, - String.format("android-%s_%d", getAndroidVersion().getApiString(), n)); //$NON-NLS-1$ - } - - return folder; - } - - @Override - public boolean sameItemAs(Package pkg) { - if (pkg instanceof SamplePackage) { - SamplePackage newPkg = (SamplePackage)pkg; - - // check they are the same version. - return newPkg.getAndroidVersion().equals(this.getAndroidVersion()); - } - - return false; - } - - /** - * Makes sure the base /samples folder exists before installing. - * - * {@inheritDoc} - */ - @Override - public boolean preInstallHook(Archive archive, - ITaskMonitor monitor, - String osSdkRoot, - File installFolder) { - - if (installFolder != null && installFolder.isDirectory()) { - // Get the hash computed during the last installation - String storedHash = readContentHash(installFolder); - if (storedHash != null && !storedHash.isEmpty()) { - - // Get the hash of the folder now - String currentHash = computeContentHash(installFolder); - - if (!storedHash.equals(currentHash)) { - // The hashes differ. The content was modified. - // Ask the user if we should still wipe the old samples. - - String pkgName = archive.getParentPackage().getShortDescription(); - - String msg = String.format( - "-= Warning ! =-\n" + - "You are about to replace the content of the folder:\n " + - " %1$s\n" + - "by the new package:\n" + - " %2$s.\n" + - "\n" + - "However it seems that the content of the existing samples " + - "has been modified since it was last installed. Are you sure " + - "you want to DELETE the existing samples? This cannot be undone.\n" + - "Please select YES to delete the existing sample and replace them " + - "by the new ones.\n" + - "Please select NO to skip this package. You can always install it later.", - installFolder.getAbsolutePath(), - pkgName); - - // Returns true if we can wipe & replace. - return monitor.displayPrompt("SDK Manager: overwrite samples?", msg); - } - } - } - - // The default is to allow installation - return super.preInstallHook(archive, monitor, osSdkRoot, installFolder); - } - - /** - * Computes a hash of the installed content (in case of successful install.) - * - * {@inheritDoc} - */ - @Override - public void postInstallHook(Archive archive, ITaskMonitor monitor, File installFolder) { - super.postInstallHook(archive, monitor, installFolder); - - if (installFolder != null) { - String h = computeContentHash(installFolder); - saveContentHash(installFolder, h); - } - } - - /** - * Set all the files from a sample package as read-only so that - * users don't end up modifying sources by mistake in Eclipse - * (samples are copied if using the NPW > Create from sample.) - */ - @Override - public void postUnzipFileHook( - Archive archive, - ITaskMonitor monitor, - IFileOp fileOp, - File unzippedFile, - ZipArchiveEntry zipEntry) { - super.postUnzipFileHook(archive, monitor, fileOp, unzippedFile, zipEntry); - - if (fileOp.isFile(unzippedFile) && - !SdkConstants.FN_SOURCE_PROP.equals(unzippedFile.getName())) { - fileOp.setReadOnly(unzippedFile); - } - } - - /** - * Reads the hash from the properties file, if it exists. - * Returns null if something goes wrong, e.g. there's no property file or - * it doesn't contain our hash. Returns an empty string if the hash wasn't - * correctly computed last time by {@link #saveContentHash(File, String)}. - */ - private String readContentHash(File folder) { - Properties props = new Properties(); - - FileInputStream fis = null; - try { - File f = new File(folder, SdkConstants.FN_CONTENT_HASH_PROP); - if (f.isFile()) { - fis = new FileInputStream(f); - props.load(fis); - return props.getProperty("content-hash", null); //$NON-NLS-1$ - } - } catch (Exception e) { - // ignore - } finally { - if (fis != null) { - try { - fis.close(); - } catch (IOException e) { - } - } - } - - return null; - } - - /** - * Saves the hash using a properties file - */ - private void saveContentHash(File folder, String hash) { - Properties props = new Properties(); - - props.setProperty("content-hash", hash == null ? "" : hash); //$NON-NLS-1$ //$NON-NLS-2$ - - FileOutputStream fos = null; - try { - File f = new File(folder, SdkConstants.FN_CONTENT_HASH_PROP); - fos = new FileOutputStream(f); - props.store( fos, "## Android - hash of this archive."); //$NON-NLS-1$ - } catch (IOException e) { - e.printStackTrace(); - } finally { - if (fos != null) { - try { - fos.close(); - } catch (IOException e) { - } - } - } - } - - /** - * Computes a hash of the files names and sizes installed in the folder - * using the SHA-1 digest. - * Returns null if the digest algorithm is not available. - */ - private String computeContentHash(File installFolder) { - MessageDigest md = null; - try { - // SHA-1 is a standard algorithm. - // http://java.sun.com/j2se/1.4.2/docs/guide/security/CryptoSpec.html#AppB - md = MessageDigest.getInstance("SHA-1"); //$NON-NLS-1$ - } catch (NoSuchAlgorithmException e) { - // We're unlikely to get there unless this JVM is not spec conforming - // in which case there won't be any hash available. - } - - if (md != null) { - hashDirectoryContent(installFolder, md); - return getDigestHexString(md); - } - - return null; - } - - /** - * Computes a hash of the *content* of this directory. The hash only uses - * the files names and the file sizes. - */ - private void hashDirectoryContent(File folder, MessageDigest md) { - if (folder == null || md == null || !folder.isDirectory()) { - return; - } - - for (File f : folder.listFiles()) { - if (f.isDirectory()) { - hashDirectoryContent(f, md); - - } else { - String name = f.getName(); - - // Skip the file we use to store the content hash - if (name == null || SdkConstants.FN_CONTENT_HASH_PROP.equals(name)) { - continue; - } - - try { - md.update(name.getBytes(SdkConstants.UTF_8)); - } catch (UnsupportedEncodingException e) { - // There is no valid reason for UTF-8 to be unsupported. Ignore. - } - try { - long len = f.length(); - md.update((byte) (len & 0x0FF)); - md.update((byte) ((len >> 8) & 0x0FF)); - md.update((byte) ((len >> 16) & 0x0FF)); - md.update((byte) ((len >> 24) & 0x0FF)); - - } catch (SecurityException e) { - // Might happen if file is not readable. Ignore. - } - } - } - } - - /** - * Returns a digest as an hex string. - */ - private String getDigestHexString(MessageDigest digester) { - // Create an hex string from the digest - byte[] digest = digester.digest(); - int n = digest.length; - String hex = "0123456789abcdef"; //$NON-NLS-1$ - char[] hexDigest = new char[n * 2]; - for (int i = 0; i < n; i++) { - int b = digest[i] & 0x0FF; - hexDigest[i*2 + 0] = hex.charAt(b >>> 4); - hexDigest[i*2 + 1] = hex.charAt(b & 0x0f); - } - - return new String(hexDigest); - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/SourcePackage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/SourcePackage.java deleted file mode 100644 index 6942431e..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/SourcePackage.java +++ /dev/null @@ -1,377 +0,0 @@ -/* - * Copyright (C) 2011 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.sdklib.internal.repository.packages; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.annotations.VisibleForTesting; -import com.android.annotations.VisibleForTesting.Visibility; -import com.android.sdklib.AndroidVersion; -import com.android.sdklib.AndroidVersion.AndroidVersionException; -import com.android.sdklib.SdkManager; -import com.android.sdklib.repository.IDescription; -import com.android.sdklib.internal.repository.ITaskMonitor; -import com.android.sdklib.internal.repository.archives.Archive; -import com.android.sdklib.internal.repository.sources.SdkSource; -import com.android.sdklib.io.IFileOp; -import com.android.sdklib.repository.MajorRevision; -import com.android.sdklib.repository.SdkRepoConstants; -import com.android.sdklib.repository.descriptors.IPkgDesc; -import com.android.sdklib.repository.descriptors.PkgDesc; - -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; -import org.w3c.dom.Node; - -import java.io.File; -import java.util.Map; -import java.util.Properties; - -/** - * Represents a source XML node in an SDK repository. - *

- * Note that a source package has a version and thus implements {@link IAndroidVersionProvider}. - * However there is no mandatory dependency that limits installation so this does not - * implement {@link IPlatformDependency}. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class SourcePackage extends MajorRevisionPackage implements IAndroidVersionProvider { - - /** The package version, for platform, add-on and doc packages. */ - private final AndroidVersion mVersion; - - private final IPkgDesc mPkgDesc; - - /** - * Creates a new source package from the attributes and elements of the given XML node. - * This constructor should throw an exception if the package cannot be created. - * - * @param source The {@link SdkSource} where this is loaded from. - * @param packageNode The XML element being parsed. - * @param nsUri The namespace URI of the originating XML document, to be able to deal with - * parameters that vary according to the originating XML schema. - * @param licenses The licenses loaded from the XML originating document. - */ - public SourcePackage( - SdkSource source, - Node packageNode, - String nsUri, - Map licenses) { - super(source, packageNode, nsUri, licenses); - - int apiLevel = - PackageParserUtils.getXmlInt(packageNode, SdkRepoConstants.NODE_API_LEVEL, 0); - String codeName = - PackageParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_CODENAME); - if (codeName.isEmpty()) { - codeName = null; - } - mVersion = new AndroidVersion(apiLevel, codeName); - - mPkgDesc = setDescriptions(PkgDesc.Builder.newSource(mVersion, (MajorRevision)getRevision())).create(); - } - - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected SourcePackage( - AndroidVersion platformVersion, - int revision, - Properties props, - String localOsPath) { - this(null /*source*/, platformVersion, revision, props, localOsPath); - } - - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected SourcePackage( - SdkSource source, - AndroidVersion platformVersion, - int revision, - Properties props, - String localOsPath) { - super( source, //source - props, //properties - revision, //revision - null, //license - null, //description - null, //descUrl - localOsPath //archiveOsPath - ); - mVersion = platformVersion; - - mPkgDesc = setDescriptions(PkgDesc.Builder.newSource(mVersion, (MajorRevision) getRevision())).create(); - } - - /** - * Creates either a valid {@link SourcePackage} or a {@link BrokenPackage}. - *

- * If the source directory contains valid properties, this creates a new {@link SourcePackage} - * with the android version listed in the properties. - * Otherwise returns a new {@link BrokenPackage} with some explanation on what failed. - * - * @param srcDir The SDK/sources/android-N folder - * @param props The properties located in {@code srcDir} or null if not found. - * @return A new {@link SourcePackage} or a new {@link BrokenPackage}. - */ - public static Package create(File srcDir, Properties props) { - AndroidVersion version = null; - String error = null; - - // Try to load the android version from the sources.props. - // If we don't find them, it would explain why this package is broken. - if (props == null) { - error = String.format("Missing file %1$s", SdkConstants.FN_SOURCE_PROP); - } else { - try { - version = new AndroidVersion(props); - // The constructor will extract the revision from the properties - // and it will not consider a missing revision as being fatal. - return new SourcePackage(version, 0 /*revision*/, props, srcDir.getAbsolutePath()); - } catch (AndroidVersionException e) { - error = String.format("Invalid file %1$s: %2$s", - SdkConstants.FN_SOURCE_PROP, - e.getMessage()); - } - } - - if (version == null) { - try { - // Try to parse the first number out of the platform folder name. - // This is just a wild guess in case we can create a broken package using that info. - String platform = srcDir.getParentFile().getName(); - platform = platform.replaceAll("[^0-9]+", " ").trim(); //$NON-NLS-1$ //$NON-NLS-2$ - int pos = platform.indexOf(' '); - if (pos >= 0) { - platform = platform.substring(0, pos); - } - int apiLevel = Integer.parseInt(platform); - version = new AndroidVersion(apiLevel, null /*codename*/); - } catch (Exception ignore) { - } - } - - StringBuilder sb = new StringBuilder("Broken Source Package"); - if (version != null) { - sb.append(String.format(", API %1$s", version.getApiString())); - } - - String shortDesc = sb.toString(); - - if (error != null) { - sb.append('\n').append(error); - } - - String longDesc = sb.toString(); - - IPkgDesc desc = PkgDesc.Builder - .newSource(version != null ? version : new AndroidVersion(0, null), - new MajorRevision(MajorRevision.MISSING_MAJOR_REV)) - .setDescriptionShort(shortDesc) - .create(); - - return new BrokenPackage(props, shortDesc, longDesc, - IMinApiLevelDependency.MIN_API_LEVEL_NOT_SPECIFIED, - version==null ? IExactApiLevelDependency.API_LEVEL_INVALID : version.getApiLevel(), - srcDir.getAbsolutePath(), - desc); - } - - @Override - @NonNull - public IPkgDesc getPkgDesc() { - return mPkgDesc; - } - - /** - * Save the properties of the current packages in the given {@link Properties} object. - * These properties will later be given to a constructor that takes a {@link Properties} object. - */ - @Override - public void saveProperties(Properties props) { - super.saveProperties(props); - mVersion.saveProperties(props); - } - - /** - * Returns the android version of this package. - */ - @Override @NonNull - public AndroidVersion getAndroidVersion() { - return mVersion; - } - - /** - * Returns a string identifier to install this package from the command line. - * For sources, we use "source-N" where N is the API or the preview codename. - *

- * {@inheritDoc} - */ - @Override - public String installId() { - return "source-" + mVersion.getApiString(); //$NON-NLS-1$ - } - - /** - * Returns a description of this package that is suitable for a list display. - *

- * {@inheritDoc} - */ - @Override - public String getListDescription() { - String ld = getListDisplay(); - if (!ld.isEmpty()) { - return String.format("%1$s%2$s", ld, isObsolete() ? " (Obsolete)" : ""); - } - - if (mVersion.isPreview()) { - return String.format("Sources for Android '%1$s' Preview SDK%2$s", - mVersion.getCodename(), - isObsolete() ? " (Obsolete)" : ""); - } else { - return String.format("Sources for Android SDK%2$s", - mVersion.getApiLevel(), - isObsolete() ? " (Obsolete)" : ""); - } - } - - /** - * Returns a short description for an {@link IDescription}. - */ - @Override - public String getShortDescription() { - String ld = getListDisplay(); - if (!ld.isEmpty()) { - return String.format("%1$s, revision %2$s%3$s", - ld, - getRevision().toShortString(), - isObsolete() ? " (Obsolete)" : ""); - } - - if (mVersion.isPreview()) { - return String.format("Sources for Android '%1$s' Preview SDK, revision %2$s%3$s", - mVersion.getCodename(), - getRevision().toShortString(), - isObsolete() ? " (Obsolete)" : ""); - } else { - return String.format("Sources for Android SDK, API %1$d, revision %2$s%3$s", - mVersion.getApiLevel(), - getRevision().toShortString(), - isObsolete() ? " (Obsolete)" : ""); - } - } - - /** - * Returns a long description for an {@link IDescription}. - * - * The long description is whatever the XML contains for the {@code description} field, - * or the short description if the former is empty. - */ - @Override - public String getLongDescription() { - String s = getDescription(); - if (s == null || s.isEmpty()) { - s = getShortDescription(); - } - - if (s.indexOf("revision") == -1) { - s += String.format("\nRevision %1$s%2$s", - getRevision().toShortString(), - isObsolete() ? " (Obsolete)" : ""); - } - - return s; - } - - /** - * Computes a potential installation folder if an archive of this package were - * to be installed right away in the given SDK root. - *

- * A sources package is typically installed in SDK/sources/platform. - * - * @param osSdkRoot The OS path of the SDK root folder. - * @param sdkManager An existing SDK manager to list current platforms and addons. - * @return A new {@link File} corresponding to the directory to use to install this package. - */ - @Override - public File getInstallFolder(String osSdkRoot, SdkManager sdkManager) { - File folder = new File(osSdkRoot, SdkConstants.FD_PKG_SOURCES); - folder = new File(folder, "android-" + mVersion.getApiString()); //$NON-NLS-1$ - return folder; - } - - /** - * Set all the files from a source package as read-only - * so that users don't end up modifying sources by mistake in Eclipse. - */ - @Override - public void postUnzipFileHook( - Archive archive, - ITaskMonitor monitor, - IFileOp fileOp, - File unzippedFile, - ZipArchiveEntry zipEntry) { - super.postUnzipFileHook(archive, monitor, fileOp, unzippedFile, zipEntry); - - if (fileOp.isFile(unzippedFile) && - !SdkConstants.FN_SOURCE_PROP.equals(unzippedFile.getName())) { - fileOp.setReadOnly(unzippedFile); - } - } - - @Override - public boolean sameItemAs(Package pkg) { - if (pkg instanceof SourcePackage) { - SourcePackage newPkg = (SourcePackage)pkg; - - // check they are the same version. - return getAndroidVersion().equals(newPkg.getAndroidVersion()); - } - - return false; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = super.hashCode(); - result = prime * result + ((mVersion == null) ? 0 : mVersion.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (!super.equals(obj)) { - return false; - } - if (!(obj instanceof SourcePackage)) { - return false; - } - SourcePackage other = (SourcePackage) obj; - if (mVersion == null) { - if (other.mVersion != null) { - return false; - } - } else if (!mVersion.equals(other.mVersion)) { - return false; - } - return true; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/SystemImagePackage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/SystemImagePackage.java deleted file mode 100644 index c5ce04a7..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/SystemImagePackage.java +++ /dev/null @@ -1,621 +0,0 @@ -/* - * Copyright (C) 2011 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.sdklib.internal.repository.packages; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.annotations.VisibleForTesting; -import com.android.annotations.VisibleForTesting.Visibility; -import com.android.sdklib.AndroidTargetHash; -import com.android.sdklib.AndroidVersion; -import com.android.sdklib.AndroidVersion.AndroidVersionException; -import com.android.sdklib.SdkManager; -import com.android.sdklib.SystemImage; -import com.android.sdklib.devices.Abi; -import com.android.sdklib.internal.repository.sources.SdkSource; -import com.android.sdklib.repository.*; -import com.android.sdklib.repository.descriptors.IPkgDesc; -import com.android.sdklib.repository.descriptors.IdDisplay; -import com.android.sdklib.repository.descriptors.PkgDesc; -import com.android.sdklib.repository.local.LocalSysImgPkgInfo; -import org.w3c.dom.Node; - -import java.io.File; -import java.util.Locale; -import java.util.Map; -import java.util.Properties; -import java.util.regex.Pattern; - -/** - * Represents a system-image XML node in an SDK repository. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class SystemImagePackage extends MajorRevisionPackage - implements IAndroidVersionProvider, IPlatformDependency { - - /** The package version, for platform, add-on and doc packages. */ - private final AndroidVersion mVersion; - - /** The ABI of the system-image. Must not be null nor empty. */ - private final String mAbi; - - private final IPkgDesc mPkgDesc; - - private final IdDisplay mTag; - private final IdDisplay mAddonVendor; - - /** - * Creates a new system-image package from the attributes and elements of the given XML node. - * This constructor should throw an exception if the package cannot be created. - * - * @param source The {@link SdkSource} where this is loaded from. - * @param packageNode The XML element being parsed. - * @param nsUri The namespace URI of the originating XML document, to be able to deal with - * parameters that vary according to the originating XML schema. - * @param licenses The licenses loaded from the XML originating document. - */ - public SystemImagePackage(SdkSource source, - Node packageNode, - String nsUri, - Map licenses) { - super(source, packageNode, nsUri, licenses); - - int apiLevel = - PackageParserUtils.getXmlInt(packageNode, SdkRepoConstants.NODE_API_LEVEL, 0); - String codeName = - PackageParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_CODENAME); - if (codeName.isEmpty()) { - codeName = null; - } - mVersion = new AndroidVersion(apiLevel, codeName); - - mAbi = PackageParserUtils.getXmlString(packageNode, SdkRepoConstants.NODE_ABI); - - // tag id - String tagId = PackageParserUtils.getXmlString(packageNode, - SdkSysImgConstants.ATTR_TAG_ID, - SystemImage.DEFAULT_TAG.getId()); - String tagDisp = PackageParserUtils.getOptionalXmlString(packageNode, - SdkSysImgConstants.ATTR_TAG_DISPLAY); - if (tagDisp == null || tagDisp.isEmpty()) { - tagDisp = LocalSysImgPkgInfo.tagIdToDisplay(tagId); - } - assert tagId != null; - assert tagDisp != null; - mTag = new IdDisplay(tagId, tagDisp); - - - Node addonNode = - PackageParserUtils.findChildElement(packageNode, SdkSysImgConstants.NODE_ADD_ON); - - IPkgDesc desc = null; - IdDisplay vendor = null; - - if (addonNode == null) { - // A platform system-image - desc = setDescriptions(PkgDesc.Builder - .newSysImg(mVersion, mTag, mAbi, (MajorRevision) getRevision())) - .create(); - } else { - // An add-on system-image - String vendorId = PackageParserUtils.getXmlString( - addonNode, - SdkAddonConstants.NODE_VENDOR_ID); - String vendorDisp = PackageParserUtils.getXmlString( - addonNode, - SdkAddonConstants.NODE_VENDOR_DISPLAY, - vendorId); - - assert !vendorId.isEmpty(); - assert !vendorDisp.isEmpty(); - - vendor = new IdDisplay(vendorId, vendorDisp); - - desc = setDescriptions(PkgDesc.Builder - .newAddonSysImg(mVersion, vendor, mTag, mAbi, (MajorRevision) getRevision())) - .create(); - } - - mPkgDesc = desc; - mAddonVendor = vendor; - } - - @VisibleForTesting(visibility=Visibility.PRIVATE) - public SystemImagePackage( - AndroidVersion platformVersion, - int revision, - String abi, - Properties props, - String localOsPath) { - this(null /*source*/, platformVersion, revision, abi, props, localOsPath); - } - - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected SystemImagePackage( - SdkSource source, - AndroidVersion platformVersion, - int revision, - String abi, - Properties props, - String localOsPath) { - super( source, //source - props, //properties - revision, //revision - null, //license - null, //description - null, //descUrl - localOsPath //archiveOsPath - ); - mVersion = platformVersion; - if (abi == null && props != null) { - abi = props.getProperty(PkgProps.SYS_IMG_ABI); - } - assert abi != null : "To use this SystemImagePackage constructor you must pass an ABI as a parameter or as a PROP_ABI property"; - mAbi = abi; - - mTag = LocalSysImgPkgInfo.extractTagFromProps(props); - - String vendorId = getProperty(props, PkgProps.ADDON_VENDOR_ID, null); - String vendorDisp = getProperty(props, PkgProps.ADDON_VENDOR_DISPLAY, vendorId); - - IPkgDesc desc = null; - IdDisplay vendor = null; - - if (vendorId == null) { - // A platform system-image - desc = setDescriptions(PkgDesc.Builder.newSysImg(mVersion, mTag, mAbi, (MajorRevision)getRevision())).create(); - } - else { - // An add-on system-image - assert !vendorId.isEmpty(); - assert !vendorDisp.isEmpty(); - - vendor = new IdDisplay(vendorId, vendorDisp); - - desc = setDescriptions(PkgDesc.Builder.newAddonSysImg(mVersion, vendor, mTag, mAbi, (MajorRevision)getRevision())).create(); - } - - mPkgDesc = desc; - mAddonVendor = vendor; - } - - /** - * Creates a {@link BrokenPackage} representing a system image that failed to load - * with the regular {@link SdkManager} workflow. - * - * @param abiDir The SDK/system-images/android-N/tag/abi folder - * @param props The properties located in {@code abiDir} or null if not found. - * @return A new {@link BrokenPackage} that represents this installed package. - */ - public static Package createBroken(File abiDir, Properties props) { - AndroidVersion version = null; - String abiType = abiDir.getName(); - String error = null; - IdDisplay tag = null; - - // Try to load the android version, tag & ABI from the sources.props. - // If we don't find them, it would explain why this package is broken. - if (props == null) { - error = String.format("Missing file %1$s", SdkConstants.FN_SOURCE_PROP); - } else { - try { - version = new AndroidVersion(props); - - tag = LocalSysImgPkgInfo.extractTagFromProps(props); - String abi = props.getProperty(PkgProps.SYS_IMG_ABI); - if (abi != null) { - abiType = abi; - } else { - error = String.format("Invalid file %1$s: Missing property %2$s", - SdkConstants.FN_SOURCE_PROP, - PkgProps.SYS_IMG_ABI); - } - } catch (AndroidVersionException e) { - error = String.format("Invalid file %1$s: %2$s", - SdkConstants.FN_SOURCE_PROP, - e.getMessage()); - } - } - - try { - // Try to parse the first number out of the platform folder name. - // Also try to parse the tag if not known yet. - // Folder structure should be: - // Tools < 22.6 / API < 20: sdk/system-images/android-N/abi/ - // Tools >=22.6 / API >=20: sdk/system-images/android-N/tag/abi/ - String[] segments = abiDir.getAbsolutePath().split(Pattern.quote(File.separator)); - int len = segments.length; - for (int i = len - 2; version == null && i >= 0; i--) { - if (SdkConstants.FD_SYSTEM_IMAGES.equals(segments[i])) { - if (version == null) { - String platform = segments[i+1]; - try { - platform = platform.replaceAll("[^0-9]+", " ").trim(); //$NON-NLS-1$ //$NON-NLS-2$ - int pos = platform.indexOf(' '); - if (pos >= 0) { - platform = platform.substring(0, pos); - } - int apiLevel = Integer.parseInt(platform); - version = new AndroidVersion(apiLevel, null /*codename*/); - } catch (Exception ignore) {} - } - if (tag == null && i+2 < len) { - // If we failed to find a tag id in the properties, check whether - // we can guess one from the system image folder path. It should - // match the limited tag id character set and not be one of the - // known ABIs. - String abiOrTag = segments[i+2].trim(); - if (abiOrTag.matches("[A-Za-z0-9_-]+")) { - if (Abi.getEnum(abiOrTag) == null) { - tag = new IdDisplay(abiOrTag, - LocalSysImgPkgInfo.tagIdToDisplay(abiOrTag)); - } - } - } - } - } - } catch (Exception ignore) {} - - String vendorId = getProperty(props, PkgProps.ADDON_VENDOR_ID, null); - String vendorDisp = getProperty(props, PkgProps.ADDON_VENDOR_DISPLAY, vendorId); - - StringBuilder sb = new StringBuilder("Broken "); - sb.append(getAbiDisplayNameInternal(abiType)).append(' '); - if (tag != null && !tag.getId().equals(SystemImage.DEFAULT_TAG.getId())) { - sb.append(tag).append(' '); - } - sb.append("System Image"); - if (vendorDisp != null) { - sb.append(", by ").append(vendorDisp); - } - if (version != null) { - sb.append(String.format(", API %1$s", version.getApiString())); - } - - String shortDesc = sb.toString(); - - if (error != null) { - sb.append('\n').append(error); - } - - String longDesc = sb.toString(); - - if (tag == null) { - // No tag? Use the default. - tag = SystemImage.DEFAULT_TAG; - } - assert tag != null; - - IPkgDesc desc = PkgDesc.Builder - .newSysImg(version != null ? version : new AndroidVersion(0, null), - tag, - abiType, - new MajorRevision(MajorRevision.MISSING_MAJOR_REV)) - .setDescriptionShort(shortDesc) - .create(); - - return new BrokenPackage(props, shortDesc, longDesc, - IMinApiLevelDependency.MIN_API_LEVEL_NOT_SPECIFIED, - version==null ? IExactApiLevelDependency.API_LEVEL_INVALID : version.getApiLevel(), - abiDir.getAbsolutePath(), - desc); - } - - @Override - @NonNull - public IPkgDesc getPkgDesc() { - return mPkgDesc; - } - - /** - * Save the properties of the current packages in the given {@link Properties} object. - * These properties will later be given to a constructor that takes a {@link Properties} object. - */ - @Override - public void saveProperties(Properties props) { - super.saveProperties(props); - - mVersion.saveProperties(props); - props.setProperty(PkgProps.SYS_IMG_ABI, mAbi); - props.setProperty(PkgProps.SYS_IMG_TAG_ID, mTag.getId()); - props.setProperty(PkgProps.SYS_IMG_TAG_DISPLAY, mTag.getDisplay()); - - if (mAddonVendor != null) { - props.setProperty(PkgProps.ADDON_VENDOR_ID, mAddonVendor.getId()); - props.setProperty(PkgProps.ADDON_VENDOR_DISPLAY, mAddonVendor.getDisplay()); - } - } - - /** Returns the tag of the system-image. */ - @NonNull - public IdDisplay getTag() { - return mTag; - } - - /** Returns the ABI of the system-image. Cannot be null nor empty. */ - public String getAbi() { - return mAbi; - } - - /** Returns a display-friendly name for the ABI of the system-image. */ - public String getAbiDisplayName() { - return getAbiDisplayNameInternal(mAbi); - } - - private static String getAbiDisplayNameInternal(String abi) { - return abi.replace("armeabi", "ARM EABI") //$NON-NLS-1$ //$NON-NLS-2$ - .replace("arm64", "ARM 64") //$NON-NLS-1$ //$NON-NLS-2$ - .replace("x86", "Intel x86 Atom") //$NON-NLS-1$ //$NON-NLS-2$ - .replace("x86_64", "Intel x86_64 Atom") //$NON-NLS-1$ //$NON-NLS-2$ - .replace("mips", "MIPS") //$NON-NLS-1$ //$NON-NLS-2$ - .replace("-", " "); //$NON-NLS-1$ //$NON-NLS-2$ - } - - /** - * Returns the version of the platform dependency of this package. - *

- * A system-image has the same {@link AndroidVersion} as the platform it depends on. - */ - @NonNull - @Override - public AndroidVersion getAndroidVersion() { - return mVersion; - } - - /** - * Returns true if the system-image belongs to a standard Android platform. - * In this case {@link #getAddonVendor()} returns null. - *

- * {@inheritDoc} - */ - @Override - public String installId() { - StringBuilder sb = new StringBuilder("sys-img-"); //$NON-NLS-1$ - sb.append(getAbi()).append('-'); - if (!isPlatform()) { - sb.append("addon-"); - } - sb.append(SystemImage.DEFAULT_TAG.equals(getTag()) ? "android" : getTag().getId()); - sb.append('-'); - if (!isPlatform()) { - sb.append(getAddonVendor().getId()).append('-'); - } - sb.append(getAndroidVersion().getApiString()); - - String s = sb.toString(); - s = s.toLowerCase(Locale.US).replaceAll("[^a-z0-9_.-]+", "_").replaceAll("_+", "_"); - return s; - - } - - /** - * Returns a description of this package that is suitable for a list display. - *

- * {@inheritDoc} - */ - @Override - public String getListDescription() { - String ld = getListDisplay(); - if (!ld.isEmpty()) { - return String.format("%1$s%2$s", ld, isObsolete() ? " (Obsolete)" : ""); - } - - boolean isDefaultTag = SystemImage.DEFAULT_TAG.equals(mTag); - return String.format("%1$s%2$s System Image%3$s", - isDefaultTag ? "" : (mTag.getDisplay() + " "), - getAbiDisplayName(), - isObsolete() ? " (Obsolete)" : ""); - } - - /** - * Returns a short description for an {@link IDescription}. - */ - @Override - public String getShortDescription() { - String ld = getListDisplay(); - if (!ld.isEmpty()) { - return String.format("%1$s, %2$s API %3$s, revision %4$s%5$s", - ld, - mAddonVendor == null ? "Android" : mAddonVendor.getDisplay(), - mVersion.getApiString(), - getRevision().toShortString(), - isObsolete() ? " (Obsolete)" : ""); - } - - boolean isDefaultTag = SystemImage.DEFAULT_TAG.equals(mTag); - return String.format("%1$s%2$s System Image, %3$s API %4$s, revision %5$s%6$s", - isDefaultTag ? "" : (mTag.getDisplay() + " "), - getAbiDisplayName(), - mAddonVendor == null ? "Android" : mAddonVendor.getDisplay(), - mVersion.getApiString(), - getRevision().toShortString(), - isObsolete() ? " (Obsolete)" : ""); - } - - /** - * Returns a long description for an {@link IDescription}. - * - * The long description is whatever the XML contains for the {@code description} field, - * or the short description if the former is empty. - */ - @Override - public String getLongDescription() { - String s = getDescription(); - if (s == null || s.isEmpty()) { - s = getShortDescription(); - } - - if (s.indexOf("revision") == -1) { - s += String.format("\nRevision %1$s%2$s", - getRevision().toShortString(), - isObsolete() ? " (Obsolete)" : ""); - } - - s += String.format("\nRequires SDK Platform Android API %1$s", - mVersion.getApiString()); - return s; - } - - /** - * Computes a potential installation folder if an archive of this package were - * to be installed right away in the given SDK root. - *

- * A system-image package is typically installed in SDK/systems/platform/tag/abi. - * The name needs to be sanitized to be acceptable as a directory name. - * - * @param osSdkRoot The OS path of the SDK root folder. - * @param sdkManager An existing SDK manager to list current platforms and addons. - * @return A new {@link File} corresponding to the directory to use to install this package. - */ - @Override - public File getInstallFolder(String osSdkRoot, SdkManager sdkManager) { - File folder = new File(osSdkRoot, SdkConstants.FD_SYSTEM_IMAGES); - folder = new File(folder, AndroidTargetHash.getPlatformHashString(mVersion)); - - // Computes a folder directory using the sanitized tag & abi strings. - String tag = mTag.getId(); - tag = tag.toLowerCase(Locale.US); - tag = tag.replaceAll("[^a-z0-9_-]+", "_"); //$NON-NLS-1$ //$NON-NLS-2$ - tag = tag.replaceAll("_+", "_"); //$NON-NLS-1$ //$NON-NLS-2$ - tag = tag.replaceAll("-+", "-"); //$NON-NLS-1$ //$NON-NLS-2$ - - folder = new File(folder, tag); - - String abi = mAbi; - abi = abi.toLowerCase(Locale.US); - abi = abi.replaceAll("[^a-z0-9_-]+", "_"); //$NON-NLS-1$ //$NON-NLS-2$ - abi = abi.replaceAll("_+", "_"); //$NON-NLS-1$ //$NON-NLS-2$ - abi = abi.replaceAll("-+", "-"); //$NON-NLS-1$ //$NON-NLS-2$ - - folder = new File(folder, abi); - return folder; - } - - @Override - public boolean sameItemAs(Package pkg) { - if (pkg instanceof SystemImagePackage) { - SystemImagePackage newPkg = (SystemImagePackage)pkg; - - // check they are the same tag, abi and version. - return getTag().equals(newPkg.getTag()) && - getAbi().equals(newPkg.getAbi()) && - getAndroidVersion().equals(newPkg.getAndroidVersion()) && - (mAddonVendor == newPkg.mAddonVendor || - (mAddonVendor != null && mAddonVendor.equals(newPkg.mAddonVendor))); - } - - return false; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = super.hashCode(); - result = prime * result + ((mAddonVendor == null) ? 0 : mAddonVendor.hashCode()); - result = prime * result + ((mTag == null) ? 0 : mTag.hashCode()); - result = prime * result + ((mAbi == null) ? 0 : mAbi.hashCode()); - result = prime * result + ((mVersion == null) ? 0 : mVersion.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (!super.equals(obj)) { - return false; - } - if (!(obj instanceof SystemImagePackage)) { - return false; - } - SystemImagePackage other = (SystemImagePackage) obj; - if (mAddonVendor == null) { - if (other.mAddonVendor != null) { - return false; - } - } else if (!mAddonVendor.equals(other.mAddonVendor)) { - return false; - } - if (mTag == null) { - if (other.mTag != null) { - return false; - } - } else if (!mTag.equals(other.mTag)) { - return false; - } - if (mAbi == null) { - if (other.mAbi != null) { - return false; - } - } else if (!mAbi.equals(other.mAbi)) { - return false; - } - if (mVersion == null) { - if (other.mVersion != null) { - return false; - } - } else if (!mVersion.equals(other.mVersion)) { - return false; - } - return true; - } - - /** - * For sys img packages, we want to add tag/abi to the sorting key - * before the revision number. - *

- * {@inheritDoc} - */ - @Override - protected String comparisonKey() { - String s = super.comparisonKey(); - int pos = s.indexOf("|r:"); //$NON-NLS-1$ - assert pos > 0; - s = s.substring(0, pos) + - "|vend:" + (mAddonVendor == null ? "" : mAddonVendor.getId()) + //$NON-NLS-1$ //$NON-NLS-2$ - "|tag:" + getTag().getId() + //$NON-NLS-1$ - "|abi:" + getAbiDisplayName() + //$NON-NLS-1$ - s.substring(pos); - return s; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/ToolPackage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/ToolPackage.java deleted file mode 100644 index 1b8b3b83..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/packages/ToolPackage.java +++ /dev/null @@ -1,384 +0,0 @@ -/* - * 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.sdklib.internal.repository.packages; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.annotations.VisibleForTesting; -import com.android.annotations.VisibleForTesting.Visibility; -import com.android.sdklib.SdkManager; -import com.android.sdklib.repository.IDescription; -import com.android.sdklib.internal.repository.ITaskMonitor; -import com.android.sdklib.internal.repository.archives.Archive; -import com.android.sdklib.internal.repository.sources.SdkSource; -import com.android.sdklib.repository.FullRevision; -import com.android.sdklib.repository.FullRevision.PreviewComparison; -import com.android.sdklib.repository.PkgProps; -import com.android.sdklib.repository.SdkRepoConstants; -import com.android.sdklib.repository.descriptors.IPkgDesc; -import com.android.sdklib.repository.descriptors.PkgDesc; -import com.android.utils.GrabProcessOutput; -import com.android.utils.GrabProcessOutput.IProcessOutput; -import com.android.utils.GrabProcessOutput.Wait; - -import org.w3c.dom.Node; - -import java.io.File; -import java.util.Map; -import java.util.Properties; - -/** - * Represents a tool XML node in an SDK repository. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class ToolPackage extends FullRevisionPackage implements IMinPlatformToolsDependency { - - /** The value returned by {@link ToolPackage#installId()}. */ - public static final String INSTALL_ID = "tools"; //$NON-NLS-1$ - /** The value returned by {@link ToolPackage#installId()}. */ - private static final String INSTALL_ID_PREVIEW = "tools-preview"; //$NON-NLS-1$ - - /** - * The minimal revision of the platform-tools package required by this package - * or {@link #MIN_PLATFORM_TOOLS_REV_INVALID} if the value was missing. - */ - private final FullRevision mMinPlatformToolsRevision; - - private final IPkgDesc mPkgDesc; - - /** - * Creates a new tool package from the attributes and elements of the given XML node. - * This constructor should throw an exception if the package cannot be created. - * - * @param source The {@link SdkSource} where this is loaded from. - * @param packageNode The XML element being parsed. - * @param nsUri The namespace URI of the originating XML document, to be able to deal with - * parameters that vary according to the originating XML schema. - * @param licenses The licenses loaded from the XML originating document. - */ - public ToolPackage(SdkSource source, - Node packageNode, - String nsUri, - Map licenses) { - super(source, packageNode, nsUri, licenses); - - mMinPlatformToolsRevision = PackageParserUtils.parseFullRevisionElement( - PackageParserUtils.findChildElement(packageNode, - SdkRepoConstants.NODE_MIN_PLATFORM_TOOLS_REV)); - - if (mMinPlatformToolsRevision.equals(MIN_PLATFORM_TOOLS_REV_INVALID)) { - // This revision number is mandatory starting with sdk-repository-3.xsd - // and did not exist before. Complain if the URI has level >= 3. - if (SdkRepoConstants.versionGreaterOrEqualThan(nsUri, 3)) { - throw new IllegalArgumentException( - String.format("Missing %1$s element in %2$s package", - SdkRepoConstants.NODE_MIN_PLATFORM_TOOLS_REV, - SdkRepoConstants.NODE_PLATFORM_TOOL)); - } - } - - mPkgDesc = setDescriptions(PkgDesc.Builder - .newTool(getRevision(), mMinPlatformToolsRevision)) - .create(); - } - - /** - * Manually create a new package with one archive and the given attributes or properties. - * This is used to create packages from local directories in which case there must be - * one archive which URL is the actual target location. - *

- * By design, this creates a package with one and only one archive. - */ - public static Package create( - SdkSource source, - Properties props, - int revision, - String license, - String description, - String descUrl, - String archiveOsPath) { - return new ToolPackage(source, props, revision, license, description, - descUrl, archiveOsPath); - } - - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected ToolPackage( - SdkSource source, - Properties props, - int revision, - String license, - String description, - String descUrl, - String archiveOsPath) { - super(source, - props, - revision, - license, - description, - descUrl, - archiveOsPath); - - // Setup min-platform-tool - String revStr = getProperty(props, PkgProps.MIN_PLATFORM_TOOLS_REV, null); - - FullRevision rev = MIN_PLATFORM_TOOLS_REV_INVALID; - if (revStr != null) { - try { - rev = FullRevision.parseRevision(revStr); - } catch (NumberFormatException ignore) {} - } - - mMinPlatformToolsRevision = rev; - - mPkgDesc = setDescriptions(PkgDesc.Builder - .newTool(getRevision(), mMinPlatformToolsRevision)) - .create(); - } - - @Override - @NonNull - public IPkgDesc getPkgDesc() { - return mPkgDesc; - } - - @Override - public FullRevision getMinPlatformToolsRevision() { - return mMinPlatformToolsRevision; - } - - /** - * Returns a string identifier to install this package from the command line. - * For tools, we use "tools" or "tools-preview" since this package is unique. - *

- * {@inheritDoc} - */ - @Override - public String installId() { - if (getRevision().isPreview()) { - return INSTALL_ID_PREVIEW; - } else { - return INSTALL_ID; - } - } - - /** - * Returns a description of this package that is suitable for a list display. - *

- * {@inheritDoc} - */ - @Override - public String getListDescription() { - String ld = getListDisplay(); - return String.format("%1$s%2$s", - ld.isEmpty() ? "Android SDK Tools" : ld, - isObsolete() ? " (Obsolete)" : ""); - } - - /** - * Returns a short description for an {@link IDescription}. - */ - @Override - public String getShortDescription() { - String ld = getListDisplay(); - if (!ld.isEmpty()) { - return String.format("%1$s, revision %2$s%3$s", - ld, - getRevision().toShortString(), - isObsolete() ? " (Obsolete)" : ""); - } - - return String.format("Android SDK Tools, revision %1$s%2$s", - getRevision().toShortString(), - isObsolete() ? " (Obsolete)" : ""); - } - - /** Returns a long description for an {@link IDescription}. */ - @Override - public String getLongDescription() { - String s = getDescription(); - if (s == null || s.isEmpty()) { - s = getShortDescription(); - } - - if (s.indexOf("revision") == -1) { - s += String.format("\nRevision %1$s%2$s", - getRevision().toShortString(), - isObsolete() ? " (Obsolete)" : ""); - } - - return s; - } - - /** - * Computes a potential installation folder if an archive of this package were - * to be installed right away in the given SDK root. - *

- * A "tool" package should always be located in SDK/tools. - * - * @param osSdkRoot The OS path of the SDK root folder. - * @param sdkManager An existing SDK manager to list current platforms and addons. - * @return A new {@link File} corresponding to the directory to use to install this package. - */ - @Override - public File getInstallFolder(String osSdkRoot, SdkManager sdkManager) { - return new File(osSdkRoot, SdkConstants.FD_TOOLS); - } - - /** - * Check whether 2 tool packages are the same and have the - * same preview bit. - */ - @Override - public boolean sameItemAs(Package pkg) { - return sameItemAs(pkg, PreviewComparison.COMPARE_TYPE); - } - - @Override - public boolean sameItemAs(Package pkg, PreviewComparison comparePreview) { - // only one tool package so any platform-tool package is the same item. - if (pkg instanceof ToolPackage) { - switch (comparePreview) { - case IGNORE: - return true; - - case COMPARE_NUMBER: - // Voluntary break-through. - case COMPARE_TYPE: - // There's only one tool so the preview number doesn't matter; - // however previews can only match previews by default so both cases - // are treated the same. - return pkg.getRevision().isPreview() == getRevision().isPreview(); - } - } - return false; - } - - @Override - public void saveProperties(Properties props) { - super.saveProperties(props); - - if (!getMinPlatformToolsRevision().equals(MIN_PLATFORM_TOOLS_REV_INVALID)) { - props.setProperty(PkgProps.MIN_PLATFORM_TOOLS_REV, - getMinPlatformToolsRevision().toShortString()); - } - } - - /** - * The tool package executes tools/lib/post_tools_install[.bat|.sh] - * {@inheritDoc} - */ - @Override - public void postInstallHook(Archive archive, final ITaskMonitor monitor, File installFolder) { - super.postInstallHook(archive, monitor, installFolder); - - if (installFolder == null) { - return; - } - - File libDir = new File(installFolder, SdkConstants.FD_LIB); - if (!libDir.isDirectory()) { - return; - } - - String scriptName = "post_tools_install"; //$NON-NLS-1$ - String shell = ""; //$NON-NLS-1$ - if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS) { - shell = "cmd.exe /c "; //$NON-NLS-1$ - scriptName += ".bat"; //$NON-NLS-1$ - } else { - scriptName += ".sh"; //$NON-NLS-1$ - } - - File scriptFile = new File(libDir, scriptName); - if (!scriptFile.isFile()) { - return; - } - - int status = -1; - - try { - Process proc = Runtime.getRuntime().exec( - shell + scriptName, // command - null, // environment - libDir); // working dir - - final String tag = scriptName; - status = GrabProcessOutput.grabProcessOutput( - proc, - Wait.WAIT_FOR_PROCESS, - new IProcessOutput() { - @Override - public void out(@Nullable String line) { - if (line != null) { - monitor.log("[%1$s] %2$s", tag, line); - } - } - - @Override - public void err(@Nullable String line) { - if (line != null) { - monitor.logError("[%1$s] Error: %2$s", tag, line); - } - } - }); - - } catch (Exception e) { - monitor.logError("Exception: %s", e.toString()); - } - - if (status != 0) { - monitor.logError("Failed to execute %s", scriptName); - return; - } - } - - @Override - public int hashCode() { - final int prime = 31; - int result = super.hashCode(); - result = prime * result - + ((mMinPlatformToolsRevision == null) ? 0 : mMinPlatformToolsRevision.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (!super.equals(obj)) { - return false; - } - if (!(obj instanceof ToolPackage)) { - return false; - } - ToolPackage other = (ToolPackage) obj; - if (mMinPlatformToolsRevision == null) { - if (other.mMinPlatformToolsRevision != null) { - return false; - } - } else if (!mMinPlatformToolsRevision.equals(other.mMinPlatformToolsRevision)) { - return false; - } - return true; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkAddonSource.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkAddonSource.java deleted file mode 100644 index a77b128e..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkAddonSource.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * 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.sdklib.internal.repository.sources; - -import com.android.annotations.Nullable; -import com.android.sdklib.internal.repository.packages.Package; -import com.android.sdklib.repository.SdkAddonConstants; - -import org.w3c.dom.Document; - -import java.io.InputStream; - - -/** - * An sdk-addon source, i.e. a download site for addons and extra packages. - * A repository describes one or more {@link Package}s available for download. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class SdkAddonSource extends SdkSource { - - /** - * Constructs a new source for the given repository URL. - * @param url The source URL. Cannot be null. If the URL ends with a /, the default - * addon.xml filename will be appended automatically. - * @param uiName The UI-visible name of the source. Can be null. - */ - public SdkAddonSource(String url, String uiName) { - super(url, uiName); - } - - /** - * Returns true if this is an addon source. - * We only load addons and extras from these sources. - */ - @Override - public boolean isAddonSource() { - return true; - } - - /** - * Returns true if this is a system-image source. - * We only load system-images from these sources. - */ - @Override - public boolean isSysImgSource() { - return false; - } - - @Override - protected String[] getDefaultXmlFileUrls() { - return new String[] { SdkAddonConstants.URL_DEFAULT_FILENAME }; - } - - @Override - protected int getNsLatestVersion() { - return SdkAddonConstants.NS_LATEST_VERSION; - } - - @Override - protected String getNsUri() { - return SdkAddonConstants.NS_URI; - } - - @Override - protected String getNsPattern() { - return SdkAddonConstants.NS_PATTERN; - } - - @Override - protected String getSchemaUri(int version) { - return SdkAddonConstants.getSchemaUri(version); - } - - @Override - protected String getRootElementName() { - return SdkAddonConstants.NODE_SDK_ADDON; - } - - @Override - protected InputStream getXsdStream(int version) { - return SdkAddonConstants.getXsdStream(version); - } - - /** - * This kind of schema does not support forward-evolution of the <tool> element. - * - * @param xml The input XML stream. Can be null. - * @return Always null. - * @null This implementation always return null. - */ - @Override - protected Document findAlternateToolsXml(@Nullable InputStream xml) { - return null; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkRepoSource.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkRepoSource.java deleted file mode 100644 index 3d2efb50..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkRepoSource.java +++ /dev/null @@ -1,528 +0,0 @@ -/* - * 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.sdklib.internal.repository.sources; - -import com.android.annotations.Nullable; -import com.android.sdklib.internal.repository.archives.ArchFilter; -import com.android.sdklib.internal.repository.packages.Package; -import com.android.sdklib.internal.repository.packages.PackageParserUtils; -import com.android.sdklib.repository.RepoConstants; -import com.android.sdklib.repository.SdkRepoConstants; - -import org.w3c.dom.Attr; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.NamedNodeMap; -import org.w3c.dom.Node; -import org.w3c.dom.Text; -import org.xml.sax.ErrorHandler; - -import java.io.IOException; -import java.io.InputStream; -import java.util.regex.Pattern; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; - - -/** - * An sdk-repository source, i.e. a download site. - * A repository describes one or more {@link Package}s available for download. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class SdkRepoSource extends SdkSource { - - /** - * Constructs a new source for the given repository URL. - * @param url The source URL. Cannot be null. If the URL ends with a /, the default - * repository.xml filename will be appended automatically. - * @param uiName The UI-visible name of the source. Can be null. - */ - public SdkRepoSource(String url, String uiName) { - super(url, uiName); - } - - /** - * Returns true if this is an addon source. - * We only load addons and extras from these sources. - */ - @Override - public boolean isAddonSource() { - return false; - } - - /** - * Returns true if this is a system-image source. - * We only load system-images from these sources. - */ - @Override - public boolean isSysImgSource() { - return false; - } - - private static String[] sDefaults = null; // lazily allocated in getDefaultXmlFileUrls - - @Override - protected String[] getDefaultXmlFileUrls() { - if (sDefaults == null) { - String[] values = new String[SdkRepoConstants.NS_LATEST_VERSION - - SdkRepoConstants.NS_SERVER_MIN_VERSION - + 2]; - int k = 0; - for (int i = SdkRepoConstants.NS_LATEST_VERSION; - i >= SdkRepoConstants.NS_SERVER_MIN_VERSION; - i--) { - values[k++] = String.format(SdkRepoConstants.URL_FILENAME_PATTERN, i); - } - values[k++] = SdkRepoConstants.URL_DEFAULT_FILENAME; - assert k == values.length; - sDefaults = values; - } - - return sDefaults; - } - - @Override - protected int getNsLatestVersion() { - return SdkRepoConstants.NS_LATEST_VERSION; - } - - @Override - protected String getNsUri() { - return SdkRepoConstants.NS_URI; - } - - @Override - protected String getNsPattern() { - return SdkRepoConstants.NS_PATTERN; - } - - @Override - protected String getSchemaUri(int version) { - return SdkRepoConstants.getSchemaUri(version); - } - - @Override - protected String getRootElementName() { - return SdkRepoConstants.NODE_SDK_REPOSITORY; - } - - @Override - protected InputStream getXsdStream(int version) { - return SdkRepoConstants.getXsdStream(version); - } - - /** - * The purpose of this method is to support forward evolution of our schema. - *

- * At this point, we know that xml does not point to any schema that this version of - * the tool knows how to process, so it's not one of the possible 1..N versions of our - * XSD schema. - *

- * We thus try to interpret the byte stream as a possible XML stream. It may not be - * one at all in the first place. If it looks anything line an XML schema, we try to - * find its <tool> and the <platform-tools> elements. If we find any, - * we recreate a suitable document that conforms to what we expect from our XSD schema - * with only those elements. - *

- * To be valid, the <tool> and the <platform-tools> elements must have at - * least one <archive> compatible with this platform. - *

- * Starting the sdk-repository schema v3, <tools> has a <min-platform-tools-rev> - * node, so technically the corresponding XML schema will be usable only if there's a - * <platform-tools> with the request revision number. We don't enforce that here, as - * this is done at install time. - *

- * If we don't find anything suitable, we drop the whole thing. - * - * @param xml The input XML stream. Can be null. - * @return Either a new XML document conforming to our schema with at least one <tool> - * and <platform-tools> element or null. - * @throws IOException if InputStream.reset() fails - * @null Can return null on failure. - */ - @Override - protected Document findAlternateToolsXml(@Nullable InputStream xml) throws IOException { - return findAlternateToolsXml(xml, null /*errorHandler*/); - } - - /** - * An alternate version of {@link #findAlternateToolsXml(InputStream)} that allows - * the caller to specify the XML error handler. The default from the underlying Java - * XML Xerces parser will dump to stdout/stderr, which is not convenient during unit tests. - * - * @param xml The input XML stream. Can be null. - * @param errorHandler An optional XML error handler. If null, the default will be used. - * @return Either a new XML document conforming to our schema with at least one <tool> - * and <platform-tools> element or null. - * @throws IOException if InputStream.reset() fails - * @null Can return null on failure. - * @see #findAlternateToolsXml(InputStream) findAlternateToolsXml() provides more details. - */ - protected Document findAlternateToolsXml( - @Nullable InputStream xml, - @Nullable ErrorHandler errorHandler) - throws IOException { - if (xml == null) { - return null; - } - - // Reset the stream if it supports that operation. - assert xml.markSupported(); - xml.reset(); - - // Get an XML document - - Document oldDoc = null; - Document newDoc = null; - try { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setIgnoringComments(false); - factory.setValidating(false); - - // Parse the old document using a non namespace aware builder - factory.setNamespaceAware(false); - DocumentBuilder builder = factory.newDocumentBuilder(); - - if (errorHandler != null) { - builder.setErrorHandler(errorHandler); - } - - oldDoc = builder.parse(xml); - - // Prepare a new document using a namespace aware builder - factory.setNamespaceAware(true); - builder = factory.newDocumentBuilder(); - newDoc = builder.newDocument(); - - } catch (Exception e) { - // Failed to get builder factor - // Failed to create XML document builder - // Failed to parse XML document - // Failed to read XML document - } - - if (oldDoc == null || newDoc == null) { - return null; - } - - - // Check the root element is an XML with at least the following properties: - // - // - // Note that we don't have namespace support enabled, we just do it manually. - - Pattern nsPattern = Pattern.compile(getNsPattern()); - - Node oldRoot = null; - String prefix = null; - for (Node child = oldDoc.getFirstChild(); child != null; child = child.getNextSibling()) { - if (child.getNodeType() == Node.ELEMENT_NODE) { - prefix = null; - String name = child.getNodeName(); - int pos = name.indexOf(':'); - if (pos > 0 && pos < name.length() - 1) { - prefix = name.substring(0, pos); - name = name.substring(pos + 1); - } - if (SdkRepoConstants.NODE_SDK_REPOSITORY.equals(name)) { - NamedNodeMap attrs = child.getAttributes(); - String xmlns = "xmlns"; //$NON-NLS-1$ - if (prefix != null) { - xmlns += ":" + prefix; //$NON-NLS-1$ - } - Node attr = attrs.getNamedItem(xmlns); - if (attr != null) { - String uri = attr.getNodeValue(); - if (uri != null && nsPattern.matcher(uri).matches()) { - oldRoot = child; - break; - } - } - } - } - } - - // we must have found the root node, and it must have an XML namespace prefix. - if (oldRoot == null || prefix == null || prefix.isEmpty()) { - return null; - } - - final String ns = getNsUri(); - Element newRoot = newDoc.createElementNS(ns, getRootElementName()); - newRoot.setPrefix(prefix); - newDoc.appendChild(newRoot); - int numTool = 0; - - // Find any inner or nodes and extract their required parameters - - String[] elementNames = { - SdkRepoConstants.NODE_TOOL, - SdkRepoConstants.NODE_PLATFORM_TOOL, - SdkRepoConstants.NODE_LICENSE - }; - - Element element = null; - while ((element = findChild(oldRoot, element, prefix, elementNames)) != null) { - boolean isElementValid = false; - - String name = element.getLocalName(); - if (name == null) { - name = element.getNodeName(); - - int pos = name.indexOf(':'); - if (pos > 0 && pos < name.length() - 1) { - name = name.substring(pos + 1); - } - } - - // To be valid, the tool or platform-tool element must have: - // - a element with a number - // - a element with a number for a element - // - an element with one or more elements inside - // - one of the elements must have an "os" and "arch" attributes - // compatible with the current platform. Only keep the first such element found. - // - the element must contain a , a and a . - // - none of the above for a license element - - if (SdkRepoConstants.NODE_LICENSE.equals(name)) { - isElementValid = true; - - } else { - try { - Node revision = findChild(element, null, prefix, RepoConstants.NODE_REVISION); - Node archives = findChild(element, null, prefix, RepoConstants.NODE_ARCHIVES); - - if (revision == null || archives == null) { - continue; - } - - // check revision contains a number - try { - String content = revision.getTextContent(); - content = content.trim(); - int rev = Integer.parseInt(content); - if (rev < 1) { - continue; - } - } catch (NumberFormatException ignore) { - continue; - } - - if (SdkRepoConstants.NODE_TOOL.equals(name)) { - Node minPTRev = findChild(element, null, prefix, - RepoConstants.NODE_MIN_PLATFORM_TOOLS_REV); - - if (minPTRev == null) { - continue; - } - - // check min-platform-tools-rev contains a number - try { - String content = minPTRev.getTextContent(); - content = content.trim(); - int rev = Integer.parseInt(content); - if (rev < 1) { - continue; - } - } catch (NumberFormatException ignore) { - continue; - } - } - - Node archive = null; - while ((archive = findChild(archives, - archive, - prefix, - RepoConstants.NODE_ARCHIVE)) != null) { - try { - ArchFilter af = PackageParserUtils.parseArchFilter(archive); - if (af == null || !af.isCompatibleWith(ArchFilter.getCurrent())) { - continue; - } - - Node node = findChild(archive, null, prefix, RepoConstants.NODE_URL); - String url = node == null ? null : node.getTextContent().trim(); - if (url == null || url.isEmpty()) { - continue; - } - - node = findChild(archive, null, prefix, RepoConstants.NODE_SIZE); - long size = 0; - try { - size = Long.parseLong(node.getTextContent()); - } catch (Exception e) { - // pass - } - if (size < 1) { - continue; - } - - node = findChild(archive, null, prefix, RepoConstants.NODE_CHECKSUM); - // double check that the checksum element contains a type=sha1 attribute - if (node == null) { - continue; - } - NamedNodeMap attrs = node.getAttributes(); - Node typeNode = attrs.getNamedItem(RepoConstants.ATTR_TYPE); - if (typeNode == null || - !RepoConstants.ATTR_TYPE.equals(typeNode.getNodeName()) || - !RepoConstants.SHA1_TYPE.equals(typeNode.getNodeValue())) { - continue; - } - String sha1 = node == null ? null : node.getTextContent().trim(); - if (sha1 == null || - sha1.length() != RepoConstants.SHA1_CHECKSUM_LEN) { - continue; - } - - isElementValid = true; - - } catch (Exception ignore1) { - // For debugging it is useful to re-throw the exception. - // For end-users, not so much. It would be nice to make it - // happen automatically during unit tests. - if (System.getenv("TESTING") != null || - System.getProperty("THROW_DEEP_EXCEPTION_DURING_TESTING") != null) { - throw new RuntimeException(ignore1); - } - } - } // while - } catch (Exception ignore2) { - // For debugging it is useful to re-throw the exception. - // For end-users, not so much. It would be nice to make it - // happen automatically during unit tests. - if (System.getenv("TESTING") != null || - System.getProperty("THROW_DEEP_EXCEPTION_DURING_TESTING") != null) { - throw new RuntimeException(ignore2); - } - } - } - - if (isElementValid) { - duplicateNode(newRoot, element, SdkRepoConstants.NS_URI, prefix); - numTool++; - } - } // while - - return numTool > 0 ? newDoc : null; - } - - /** - * Helper method used by {@link #findAlternateToolsXml(InputStream)} to find a given - * element child in a root XML node. - */ - private Element findChild(Node rootNode, Node after, String prefix, String[] nodeNames) { - for (int i = 0; i < nodeNames.length; i++) { - if (nodeNames[i].indexOf(':') < 0) { - nodeNames[i] = prefix + ":" + nodeNames[i]; - } - } - Node child = after == null ? rootNode.getFirstChild() : after.getNextSibling(); - for(; child != null; child = child.getNextSibling()) { - if (child.getNodeType() != Node.ELEMENT_NODE) { - continue; - } - for (String nodeName : nodeNames) { - if (nodeName.equals(child.getNodeName())) { - return (Element) child; - } - } - } - return null; - } - - /** - * Helper method used by {@link #findAlternateToolsXml(InputStream)} to find a given - * element child in a root XML node. - */ - private Node findChild(Node rootNode, Node after, String prefix, String nodeName) { - return findChild(rootNode, after, prefix, new String[] { nodeName }); - } - - /** - * Helper method used by {@link #findAlternateToolsXml(InputStream)} to duplicate a node - * and attach it to the given root in the new document. - */ - private Element duplicateNode(Element newRootNode, Element oldNode, - String namespaceUri, String prefix) { - // The implementation here is more or less equivalent to - // - // newRoot.appendChild(newDoc.importNode(oldNode, deep=true)) - // - // except we can't just use importNode() since we need to deal with the fact - // that the old document is not namespace-aware yet the new one is. - - Document newDoc = newRootNode.getOwnerDocument(); - Element newNode = null; - - String nodeName = oldNode.getNodeName(); - int pos = nodeName.indexOf(':'); - if (pos > 0 && pos < nodeName.length() - 1) { - nodeName = nodeName.substring(pos + 1); - newNode = newDoc.createElementNS(namespaceUri, nodeName); - newNode.setPrefix(prefix); - } else { - newNode = newDoc.createElement(nodeName); - } - - newRootNode.appendChild(newNode); - - // Merge in all the attributes - NamedNodeMap attrs = oldNode.getAttributes(); - for (int i = 0; i < attrs.getLength(); i++) { - Attr attr = (Attr) attrs.item(i); - Attr newAttr = null; - - String attrName = attr.getNodeName(); - pos = attrName.indexOf(':'); - if (pos > 0 && pos < attrName.length() - 1) { - attrName = attrName.substring(pos + 1); - newAttr = newDoc.createAttributeNS(namespaceUri, attrName); - newAttr.setPrefix(prefix); - } else { - newAttr = newDoc.createAttribute(attrName); - } - - newAttr.setNodeValue(attr.getNodeValue()); - - if (pos > 0) { - newNode.getAttributes().setNamedItemNS(newAttr); - } else { - newNode.getAttributes().setNamedItem(newAttr); - } - } - - // Merge all child elements and texts - for (Node child = oldNode.getFirstChild(); child != null; child = child.getNextSibling()) { - if (child.getNodeType() == Node.ELEMENT_NODE) { - duplicateNode(newNode, (Element) child, namespaceUri, prefix); - - } else if (child.getNodeType() == Node.TEXT_NODE) { - Text newText = newDoc.createTextNode(child.getNodeValue()); - newNode.appendChild(newText); - } - } - - return newNode; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSource.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSource.java deleted file mode 100644 index 54ebd340..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSource.java +++ /dev/null @@ -1,999 +0,0 @@ -/* - * 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.sdklib.internal.repository.sources; - -import com.android.annotations.Nullable; -import com.android.annotations.VisibleForTesting; -import com.android.annotations.VisibleForTesting.Visibility; -import com.android.io.NonClosingInputStream; -import com.android.io.NonClosingInputStream.CloseBehavior; -import com.android.sdklib.internal.repository.CanceledByUserException; -import com.android.sdklib.internal.repository.DownloadCache; -import com.android.sdklib.repository.IDescription; -import com.android.sdklib.internal.repository.ITaskMonitor; -import com.android.sdklib.internal.repository.packages.AddonPackage; -import com.android.sdklib.internal.repository.packages.BuildToolPackage; -import com.android.sdklib.internal.repository.packages.DocPackage; -import com.android.sdklib.internal.repository.packages.ExtraPackage; -import com.android.sdklib.internal.repository.packages.Package; -import com.android.sdklib.internal.repository.packages.PlatformPackage; -import com.android.sdklib.internal.repository.packages.PlatformToolPackage; -import com.android.sdklib.internal.repository.packages.SamplePackage; -import com.android.sdklib.internal.repository.packages.SourcePackage; -import com.android.sdklib.internal.repository.packages.SystemImagePackage; -import com.android.sdklib.internal.repository.packages.ToolPackage; -import com.android.sdklib.repository.RepoConstants; -import com.android.sdklib.repository.SdkAddonConstants; -import com.android.sdklib.repository.SdkRepoConstants; - -import org.w3c.dom.Document; -import org.w3c.dom.NamedNodeMap; -import org.w3c.dom.Node; -import org.xml.sax.ErrorHandler; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; -import org.xml.sax.SAXParseException; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import javax.net.ssl.SSLKeyException; -import javax.xml.XMLConstants; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.transform.stream.StreamSource; -import javax.xml.validation.Schema; -import javax.xml.validation.SchemaFactory; -import javax.xml.validation.Validator; - -/** - * An sdk-addon or sdk-repository source, i.e. a download site. - * It may be a full repository or an add-on only repository. - * A repository describes one or {@link Package}s available for download. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public abstract class SdkSource implements IDescription, Comparable { - - private String mUrl; - - private Package[] mPackages; - private String mDescription; - private String mFetchError; - private final String mUiName; - - private static final SdkSourceProperties sSourcesProps = new SdkSourceProperties(); - - /** - * Constructs a new source for the given repository URL. - * @param url The source URL. Cannot be null. If the URL ends with a /, the default - * repository.xml filename will be appended automatically. - * @param uiName The UI-visible name of the source. Can be null. - */ - public SdkSource(String url, String uiName) { - - // URLs should not be null and should not have whitespace. - if (url == null) { - url = ""; - } - url = url.trim(); - - // if the URL ends with a /, it must be "directory" resource, - // in which case we automatically add the default file that will - // looked for. This way it will be obvious to the user which - // resource we are actually trying to fetch. - if (url.endsWith("/")) { //$NON-NLS-1$ - String[] names = getDefaultXmlFileUrls(); - if (names.length > 0) { - url += names[0]; - } - } - - if (uiName == null) { - uiName = sSourcesProps.getProperty(SdkSourceProperties.KEY_NAME, url, null); - } else { - sSourcesProps.setProperty(SdkSourceProperties.KEY_NAME, url, uiName); - } - - mUrl = url; - mUiName = uiName; - setDefaultDescription(); - } - - /** - * Returns true if this is an addon source. - * We only load addons and extras from these sources. - */ - public abstract boolean isAddonSource(); - - /** - * Returns true if this is a system-image source. - * We only load system-images from these sources. - */ - public abstract boolean isSysImgSource(); - - - /** - * Returns the basename of the default URLs to try to download the - * XML manifest. - * E.g. this is typically SdkRepoConstants.URL_DEFAULT_XML_FILE - * or SdkAddonConstants.URL_DEFAULT_XML_FILE - */ - protected abstract String[] getDefaultXmlFileUrls(); - - /** Returns SdkRepoConstants.NS_LATEST_VERSION or SdkAddonConstants.NS_LATEST_VERSION. */ - protected abstract int getNsLatestVersion(); - - /** Returns SdkRepoConstants.NS_URI or SdkAddonConstants.NS_URI. */ - protected abstract String getNsUri(); - - /** Returns SdkRepoConstants.NS_PATTERN or SdkAddonConstants.NS_PATTERN. */ - protected abstract String getNsPattern(); - - /** Returns SdkRepoConstants.getSchemaUri() or SdkAddonConstants.getSchemaUri(). */ - protected abstract String getSchemaUri(int version); - - /* Returns SdkRepoConstants.NODE_SDK_REPOSITORY or SdkAddonConstants.NODE_SDK_ADDON. */ - protected abstract String getRootElementName(); - - /** Returns SdkRepoConstants.getXsdStream() or SdkAddonConstants.getXsdStream(). */ - protected abstract InputStream getXsdStream(int version); - - /** - * In case we fail to load an XML, examine the XML to see if it matches a future - * schema that as at least a tools node that we could load to update the - * SDK Manager. - * - * @param xml The input XML stream. Can be null. - * @return Null on failure, otherwise returns an XML DOM with just the tools we - * need to update this SDK Manager. - * @null Can return null on failure. - */ - protected abstract Document findAlternateToolsXml(@Nullable InputStream xml) - throws IOException; - - /** - * Two repo source are equal if they have the same URL. - */ - @Override - public boolean equals(Object obj) { - if (obj instanceof SdkSource) { - SdkSource rs = (SdkSource) obj; - return rs.getUrl().equals(this.getUrl()); - } - return false; - } - - @Override - public int hashCode() { - return mUrl.hashCode(); - } - - /** - * Implementation of the {@link Comparable} interface. - * Simply compares the URL using the string's default ordering. - */ - @Override - public int compareTo(SdkSource rhs) { - return this.getUrl().compareTo(rhs.getUrl()); - } - - /** - * Returns the UI-visible name of the source. Can be null. - */ - public String getUiName() { - return mUiName; - } - - /** Returns the URL of the XML file for this source. */ - public String getUrl() { - return mUrl; - } - - /** - * Returns the list of known packages found by the last call to load(). - * This is null when the source hasn't been loaded yet -- caller should - * then call {@link #load} to load the packages. - */ - public Package[] getPackages() { - return mPackages; - } - - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected void setPackages(Package[] packages) { - mPackages = packages; - - if (mPackages != null) { - // Order the packages. - Arrays.sort(mPackages, null); - } - } - - /** - * Clear the internal packages list. After this call, {@link #getPackages()} will return - * null till load() is called. - */ - public void clearPackages() { - setPackages(null); - } - - /** - * Indicates if the source is enabled. - *

- * A 3rd-party add-on source can be disabled by the user to prevent from loading it. - * - * @return True if the source is enabled (default is true). - */ - public boolean isEnabled() { - // A URL is enabled if it's not in the disabled list. - return sSourcesProps.getProperty(SdkSourceProperties.KEY_DISABLED, mUrl, null) == null; - } - - /** - * Changes whether the source is marked as enabled. - *

- * When changing the enable state, the current package list is purged - * and the next {@code load} will either return an empty list (if disabled) or - * the actual package list (if enabled.) - * - * @param enabled True for the source to be enabled (can be loaded), false otherwise. - */ - public void setEnabled(boolean enabled) { - if (enabled != isEnabled()) { - // First we clear the current package list, which will force the - // next load() to actually set the package list as desired. - clearPackages(); - - sSourcesProps.setProperty(SdkSourceProperties.KEY_DISABLED, mUrl, - enabled ? null /*remove*/ : "disabled"); //$NON-NLS-1$ - } - } - - /** - * Returns the short description of the source, if not null. - * Otherwise returns the default Object toString result. - *

- * This is mostly helpful for debugging. - * For UI display, use the {@link IDescription} interface. - */ - @Override - public String toString() { - String s = getShortDescription(); - if (s != null) { - return s; - } - return super.toString(); - } - - @Override - public String getShortDescription() { - - if (mUiName != null && !mUiName.isEmpty()) { - - String host = "malformed URL"; - - try { - URL u = new URL(mUrl); - host = u.getHost(); - } catch (MalformedURLException e) { - } - - return String.format("%1$s (%2$s)", mUiName, host); - - } - return mUrl; - } - - @Override - public String getLongDescription() { - // Note: in a normal workflow, mDescription is filled by setDefaultDescription(). - // However for packages made by unit tests or such, this can be null. - return mDescription == null ? "" : mDescription; //$NON-NLS-1$ - } - - /** - * Returns the last fetch error description. - * If there was no error, returns null. - */ - public String getFetchError() { - return mFetchError; - } - - /** - * Tries to fetch the repository index for the given URL and updates the package list. - * When a source is disabled, this create an empty non-null package list. - *

- * Callers can get the package list using {@link #getPackages()} after this. It will be - * null in case of error, in which case {@link #getFetchError()} can be used to an - * error message. - */ - public void load(DownloadCache cache, ITaskMonitor monitor, boolean forceHttp) { - - setDefaultDescription(); - monitor.setProgressMax(7); - - if (!isEnabled()) { - setPackages(new Package[0]); - mDescription += "\nSource is disabled."; - monitor.incProgress(7); - return; - } - - String url = mUrl; - if (forceHttp) { - url = url.replaceAll("https://", "http://"); //$NON-NLS-1$ //$NON-NLS-2$ - } - - monitor.setDescription("Fetching URL: %1$s", url); - monitor.incProgress(1); - - mFetchError = null; - Boolean[] validatorFound = new Boolean[] { Boolean.FALSE }; - String[] validationError = new String[] { null }; - Exception[] exception = new Exception[] { null }; - Document validatedDoc = null; - boolean usingAlternateXml = false; - boolean usingAlternateUrl = false; - String validatedUri = null; - - String[] defaultNames = getDefaultXmlFileUrls(); - String firstDefaultName = defaultNames.length > 0 ? defaultNames[0] : ""; - - InputStream xml = fetchXmlUrl(url, cache, monitor.createSubMonitor(1), exception); - if (xml != null) { - int version = getXmlSchemaVersion(xml); - if (version == 0) { - closeStream(xml); - xml = null; - } - } - - // FIXME: this is a quick fix to support an alternate upgrade path. - // The whole logic below needs to be updated. - if (xml == null && defaultNames.length > 0) { - ITaskMonitor subMonitor = monitor.createSubMonitor(1); - subMonitor.setProgressMax(defaultNames.length); - - String baseUrl = url; - if (!baseUrl.endsWith("/")) { - int pos = baseUrl.lastIndexOf('/'); - if (pos > 0) { - baseUrl = baseUrl.substring(0, pos + 1); - } - } - - for (String name : defaultNames) { - String newUrl = baseUrl + name; - if (newUrl.equals(url)) { - continue; - } - xml = fetchXmlUrl(newUrl, cache, subMonitor.createSubMonitor(1), exception); - if (xml != null) { - int version = getXmlSchemaVersion(xml); - if (version == 0) { - closeStream(xml); - xml = null; - } else { - url = newUrl; - subMonitor.incProgress( - subMonitor.getProgressMax() - subMonitor.getProgress()); - break; - } - } - } - } else { - monitor.incProgress(1); - } - - // If the original URL can't be fetched - // and the URL doesn't explicitly end with our filename - // and it wasn't an HTTP authentication operation canceled by the user - // then make another tentative after changing the URL. - if (xml == null - && !url.endsWith(firstDefaultName) - && !(exception[0] instanceof CanceledByUserException)) { - if (!url.endsWith("/")) { //$NON-NLS-1$ - url += "/"; //$NON-NLS-1$ - } - url += firstDefaultName; - - xml = fetchXmlUrl(url, cache, monitor.createSubMonitor(1), exception); - usingAlternateUrl = true; - } else { - monitor.incProgress(1); - } - - // FIXME this needs to revisited. - if (xml != null) { - monitor.setDescription("Validate XML: %1$s", url); - - ITaskMonitor subMonitor = monitor.createSubMonitor(2); - subMonitor.setProgressMax(2); - for (int tryOtherUrl = 0; tryOtherUrl < 2; tryOtherUrl++) { - // Explore the XML to find the potential XML schema version - int version = getXmlSchemaVersion(xml); - - if (version >= 1 && version <= getNsLatestVersion()) { - // This should be a version we can handle. Try to validate it - // and report any error as invalid XML syntax, - - String uri = validateXml(xml, url, version, validationError, validatorFound); - if (uri != null) { - // Validation was successful - validatedDoc = getDocument(xml, monitor); - validatedUri = uri; - - if (usingAlternateUrl && validatedDoc != null) { - // If the second tentative succeeded, indicate it in the console - // with the URL that worked. - monitor.log("Repository found at %1$s", url); - - // Keep the modified URL - mUrl = url; - } - } else if (validatorFound[0].equals(Boolean.FALSE)) { - // Validation failed because this JVM lacks a proper XML Validator - mFetchError = validationError[0]; - } else { - // We got a validator but validation failed. We know there's - // what looks like a suitable root element with a suitable XMLNS - // so it must be a genuine error of an XML not conforming to the schema. - } - } else if (version > getNsLatestVersion()) { - // The schema used is more recent than what is supported by this tool. - // Tell the user to upgrade, pointing him to the right version of the tool - // package. - - try { - validatedDoc = findAlternateToolsXml(xml); - } catch (IOException e) { - // Failed, will be handled below. - } - if (validatedDoc != null) { - validationError[0] = null; // remove error from XML validation - validatedUri = getNsUri(); - usingAlternateXml = true; - } - - } else if (version < 1 && tryOtherUrl == 0 && !usingAlternateUrl) { - // This is obviously not one of our documents. - mFetchError = String.format( - "Failed to validate the XML for the repository at URL '%1$s'", - url); - - // If we haven't already tried the alternate URL, let's do it now. - // We don't capture any fetch exception that happen during the second - // fetch in order to avoid hiding any previous fetch errors. - if (!url.endsWith(firstDefaultName)) { - if (!url.endsWith("/")) { //$NON-NLS-1$ - url += "/"; //$NON-NLS-1$ - } - url += firstDefaultName; - - closeStream(xml); - xml = fetchXmlUrl(url, cache, subMonitor.createSubMonitor(1), - null /* outException */); - subMonitor.incProgress(1); - // Loop to try the alternative document - if (xml != null) { - usingAlternateUrl = true; - continue; - } - } - } else if (version < 1 && usingAlternateUrl && mFetchError == null) { - // The alternate URL is obviously not a valid XML either. - // We only report the error if we failed to produce one earlier. - mFetchError = String.format( - "Failed to validate the XML for the repository at URL '%1$s'", - url); - } - - // If we get here either we succeeded or we ran out of alternatives. - break; - } - } - - // If any exception was handled during the URL fetch, display it now. - if (exception[0] != null) { - mFetchError = "Failed to fetch URL"; - - String reason = null; - if (exception[0] instanceof FileNotFoundException) { - // FNF has no useful getMessage, so we need to special handle it. - reason = "File not found"; - mFetchError += ": " + reason; - } else if (exception[0] instanceof SSLKeyException) { - // That's a common error and we have a pref for it. - reason = "HTTPS SSL error. You might want to force download through HTTP in the settings."; - mFetchError += ": HTTPS SSL error"; - } else if (exception[0].getMessage() != null) { - reason = - exception[0].getClass().getSimpleName().replace("Exception", "") //$NON-NLS-1$ //$NON-NLS-2$ - + ' ' - + exception[0].getMessage(); - } else { - reason = exception[0].toString(); - } - - monitor.logError("Failed to fetch URL %1$s, reason: %2$s", url, reason); - } - - if (validationError[0] != null) { - monitor.logError("%s", validationError[0]); //$NON-NLS-1$ - } - - // Stop here if we failed to validate the XML. We don't want to load it. - if (validatedDoc == null) { - return; - } - - if (usingAlternateXml) { - // We found something using the "alternate" XML schema (that is the one made up - // to support schema upgrades). That means the user can only install the tools - // and needs to upgrade them before it download more stuff. - - // Is the manager running from inside ADT? - // We check that com.android.ide.eclipse.adt.AdtPlugin exists using reflection. - - boolean isADT = false; - try { - Class adt = Class.forName("com.android.ide.eclipse.adt.AdtPlugin"); //$NON-NLS-1$ - isADT = (adt != null); - } catch (ClassNotFoundException e) { - // pass - } - - String info; - if (isADT) { - info = "This repository requires a more recent version of ADT. Please update the Eclipse Android plugin."; - mDescription = "This repository requires a more recent version of ADT, the Eclipse Android plugin.\nYou must update it before you can see other new packages."; - - } else { - info = "This repository requires a more recent version of the Tools. Please update."; - mDescription = "This repository requires a more recent version of the Tools.\nYou must update it before you can see other new packages."; - } - - mFetchError = mFetchError == null ? info : mFetchError + ". " + info; - } - - monitor.incProgress(1); - - if (xml != null) { - monitor.setDescription("Parse XML: %1$s", url); - monitor.incProgress(1); - parsePackages(validatedDoc, validatedUri, monitor); - if (mPackages == null || mPackages.length == 0) { - mDescription += "\nNo packages found."; - } else if (mPackages.length == 1) { - mDescription += "\nOne package found."; - } else { - mDescription += String.format("\n%1$d packages found.", mPackages.length); - } - } - - // done - monitor.incProgress(1); - closeStream(xml); - } - - private void setDefaultDescription() { - if (isAddonSource()) { - String desc = ""; - - if (mUiName != null) { - desc += "Add-on Provider: " + mUiName; - desc += "\n"; - } - desc += "Add-on URL: " + mUrl; - - mDescription = desc; - } else { - mDescription = String.format("SDK Source: %1$s", mUrl); - } - } - - /** - * Fetches the document at the given URL and returns it as a string. Returns - * null if anything wrong happens and write errors to the monitor. - * - * @param urlString The URL to load, as a string. - * @param monitor {@link ITaskMonitor} related to this URL. - * @param outException If non null, where to store any exception that - * happens during the fetch. - */ - private InputStream fetchXmlUrl(String urlString, - DownloadCache cache, - ITaskMonitor monitor, - Exception[] outException) { - try { - InputStream xml = cache.openCachedUrl(urlString, monitor); - if (xml != null) { - xml.mark(500000); - xml = new NonClosingInputStream(xml); - ((NonClosingInputStream) xml).setCloseBehavior(CloseBehavior.RESET); - } - return xml; - } catch (Exception e) { - if (outException != null) { - outException[0] = e; - } - } - - return null; - } - - /** - * Closes the stream, ignore any exception from InputStream.close(). - * If the stream is a NonClosingInputStream, sets it to CloseBehavior.CLOSE first. - */ - private void closeStream(InputStream is) { - if (is != null) { - if (is instanceof NonClosingInputStream) { - ((NonClosingInputStream) is).setCloseBehavior(CloseBehavior.CLOSE); - } - try { - is.close(); - } catch (IOException ignore) {} - } - } - - /** - * Validates this XML against one of the requested SDK Repository schemas. - * If the XML was correctly validated, returns the schema that worked. - * If it doesn't validate, returns null and stores the error in outError[0]. - * If we can't find a validator, returns null and set validatorFound[0] to false. - */ - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected String validateXml(InputStream xml, String url, int version, - String[] outError, Boolean[] validatorFound) { - - if (xml == null) { - return null; - } - - try { - Validator validator = getValidator(version); - - if (validator == null) { - validatorFound[0] = Boolean.FALSE; - outError[0] = String.format( - "XML verification failed for %1$s.\nNo suitable XML Schema Validator could be found in your Java environment. Please consider updating your version of Java.", - url); - return null; - } - - validatorFound[0] = Boolean.TRUE; - - // Reset the stream if it supports that operation. - assert xml.markSupported(); - xml.reset(); - - // Validation throws a bunch of possible Exceptions on failure. - validator.validate(new StreamSource(xml)); - return getSchemaUri(version); - - } catch (SAXParseException e) { - outError[0] = String.format( - "XML verification failed for %1$s.\nLine %2$d:%3$d, Error: %4$s", - url, - e.getLineNumber(), - e.getColumnNumber(), - e.toString()); - - } catch (Exception e) { - outError[0] = String.format( - "XML verification failed for %1$s.\nError: %2$s", - url, - e.toString()); - } - return null; - } - - /** - * Manually parses the root element of the XML to extract the schema version - * at the end of the xmlns:sdk="http://schemas.android.com/sdk/android/repository/$N" - * declaration. - * - * @return 1..{@link SdkRepoConstants#NS_LATEST_VERSION} for a valid schema version - * or 0 if no schema could be found. - */ - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected int getXmlSchemaVersion(InputStream xml) { - if (xml == null) { - return 0; - } - - // Get an XML document - Document doc = null; - try { - assert xml.markSupported(); - xml.reset(); - - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setIgnoringComments(false); - factory.setValidating(false); - - // Parse the old document using a non namespace aware builder - factory.setNamespaceAware(false); - DocumentBuilder builder = factory.newDocumentBuilder(); - - // We don't want the default handler which prints errors to stderr. - builder.setErrorHandler(new ErrorHandler() { - @Override - public void warning(SAXParseException e) throws SAXException { - // pass - } - @Override - public void fatalError(SAXParseException e) throws SAXException { - throw e; - } - @Override - public void error(SAXParseException e) throws SAXException { - throw e; - } - }); - - doc = builder.parse(xml); - - // Prepare a new document using a namespace aware builder - factory.setNamespaceAware(true); - builder = factory.newDocumentBuilder(); - - } catch (Exception e) { - // Failed to reset XML stream - // Failed to get builder factor - // Failed to create XML document builder - // Failed to parse XML document - // Failed to read XML document - } - - if (doc == null) { - return 0; - } - - // Check the root element is an XML with at least the following properties: - // - // - // Note that we don't have namespace support enabled, we just do it manually. - - Pattern nsPattern = Pattern.compile(getNsPattern()); - - String prefix = null; - for (Node child = doc.getFirstChild(); child != null; child = child.getNextSibling()) { - if (child.getNodeType() == Node.ELEMENT_NODE) { - prefix = null; - String name = child.getNodeName(); - int pos = name.indexOf(':'); - if (pos > 0 && pos < name.length() - 1) { - prefix = name.substring(0, pos); - name = name.substring(pos + 1); - } - if (getRootElementName().equals(name)) { - NamedNodeMap attrs = child.getAttributes(); - String xmlns = "xmlns"; //$NON-NLS-1$ - if (prefix != null) { - xmlns += ":" + prefix; //$NON-NLS-1$ - } - Node attr = attrs.getNamedItem(xmlns); - if (attr != null) { - String uri = attr.getNodeValue(); - if (uri != null) { - Matcher m = nsPattern.matcher(uri); - if (m.matches()) { - String version = m.group(1); - try { - return Integer.parseInt(version); - } catch (NumberFormatException e) { - return 0; - } - } - } - } - } - } - } - - return 0; - } - - /** - * Helper method that returns a validator for our XSD, or null if the current Java - * implementation can't process XSD schemas. - * - * @param version The version of the XML Schema. - * See {@link SdkRepoConstants#getXsdStream(int)} - */ - private Validator getValidator(int version) throws SAXException { - InputStream xsdStream = getXsdStream(version); - SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); - - if (factory == null) { - return null; - } - - // This may throw a SAX Exception if the schema itself is not a valid XSD - Schema schema = factory.newSchema(new StreamSource(xsdStream)); - - Validator validator = schema == null ? null : schema.newValidator(); - - // We don't want the default handler, which by default dumps errors to stderr. - validator.setErrorHandler(new ErrorHandler() { - @Override - public void warning(SAXParseException e) throws SAXException { - // pass - } - @Override - public void fatalError(SAXParseException e) throws SAXException { - throw e; - } - @Override - public void error(SAXParseException e) throws SAXException { - throw e; - } - }); - - return validator; - } - - /** - * Parse all packages defined in the SDK Repository XML and creates - * a new mPackages array with them. - */ - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected boolean parsePackages(Document doc, String nsUri, ITaskMonitor monitor) { - - Node root = getFirstChild(doc, nsUri, getRootElementName()); - if (root != null) { - - ArrayList packages = new ArrayList(); - - // Parse license definitions - HashMap licenses = new HashMap(); - for (Node child = root.getFirstChild(); - child != null; - child = child.getNextSibling()) { - if (child.getNodeType() == Node.ELEMENT_NODE && - nsUri.equals(child.getNamespaceURI()) && - child.getLocalName().equals(RepoConstants.NODE_LICENSE)) { - Node id = child.getAttributes().getNamedItem(RepoConstants.ATTR_ID); - if (id != null) { - licenses.put(id.getNodeValue(), child.getTextContent()); - } - } - } - - // Parse packages - for (Node child = root.getFirstChild(); - child != null; - child = child.getNextSibling()) { - if (child.getNodeType() == Node.ELEMENT_NODE && - nsUri.equals(child.getNamespaceURI())) { - String name = child.getLocalName(); - Package p = null; - - try { - // We can load add-on and extra packages from all sources, either - // internal or user sources. - if (SdkAddonConstants.NODE_ADD_ON.equals(name)) { - p = new AddonPackage(this, child, nsUri, licenses); - - } else if (SdkAddonConstants.NODE_EXTRA.equals(name)) { - p = new ExtraPackage(this, child, nsUri, licenses); - - } else if (!isAddonSource()) { - // We only load platform, doc and tool packages from internal - // sources, never from user sources. - if (SdkRepoConstants.NODE_PLATFORM.equals(name)) { - p = new PlatformPackage(this, child, nsUri, licenses); - } else if (SdkRepoConstants.NODE_DOC.equals(name)) { - p = new DocPackage(this, child, nsUri, licenses); - } else if (SdkRepoConstants.NODE_TOOL.equals(name)) { - p = new ToolPackage(this, child, nsUri, licenses); - } else if (SdkRepoConstants.NODE_PLATFORM_TOOL.equals(name)) { - p = new PlatformToolPackage(this, child, nsUri, licenses); - } else if (SdkRepoConstants.NODE_BUILD_TOOL.equals(name)) { - p = new BuildToolPackage(this, child, nsUri, licenses); - } else if (SdkRepoConstants.NODE_SAMPLE.equals(name)) { - p = new SamplePackage(this, child, nsUri, licenses); - } else if (SdkRepoConstants.NODE_SYSTEM_IMAGE.equals(name)) { - p = new SystemImagePackage(this, child, nsUri, licenses); - } else if (SdkRepoConstants.NODE_SOURCE.equals(name)) { - p = new SourcePackage(this, child, nsUri, licenses); - } - } - - if (p != null) { - packages.add(p); - monitor.logVerbose("Found %1$s", p.getShortDescription()); - } - } catch (Exception e) { - // Ignore invalid packages - monitor.logError("Ignoring invalid %1$s element: %2$s", name, e.toString()); - } - } - } - - setPackages(packages.toArray(new Package[packages.size()])); - - return true; - } - - return false; - } - - /** - * Returns the first child element with the given XML local name. - * If xmlLocalName is null, returns the very first child element. - */ - private Node getFirstChild(Node node, String nsUri, String xmlLocalName) { - - for(Node child = node.getFirstChild(); child != null; child = child.getNextSibling()) { - if (child.getNodeType() == Node.ELEMENT_NODE && - nsUri.equals(child.getNamespaceURI())) { - if (xmlLocalName == null || child.getLocalName().equals(xmlLocalName)) { - return child; - } - } - } - - return null; - } - - /** - * Takes an XML document as a string as parameter and returns a DOM for it. - * - * On error, returns null and prints a (hopefully) useful message on the monitor. - */ - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected Document getDocument(InputStream xml, ITaskMonitor monitor) { - try { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setIgnoringComments(true); - factory.setNamespaceAware(true); - - DocumentBuilder builder = factory.newDocumentBuilder(); - assert xml.markSupported(); - xml.reset(); - Document doc = builder.parse(new InputSource(xml)); - - return doc; - } catch (ParserConfigurationException e) { - monitor.logError("Failed to create XML document builder"); - - } catch (SAXException e) { - monitor.logError("Failed to parse XML document"); - - } catch (IOException e) { - monitor.logError("Failed to read XML document"); - } - - return null; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSourceCategory.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSourceCategory.java deleted file mode 100644 index 263e9d5f..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSourceCategory.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (C) 2010 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.sdklib.internal.repository.sources; - -import com.android.sdklib.repository.IDescription; - - -/** - * The category of a given {@link SdkSource} (which represents a download site). - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public enum SdkSourceCategory implements IDescription { - - /** - * The default canonical and official Android repository. - */ - ANDROID_REPO("Android Repository", true), - - /** - * Repositories contributed by the SDK_UPDATER_URLS env var, - * only used for local debugging. - */ - GETENV_REPOS("Custom Repositories", false), - - /** - * All third-party add-ons fetched from the Android repository. - */ - ADDONS_3RD_PARTY("Third party Add-ons", true), - - /** - * All add-ons contributed locally by the user via the "Add Add-on Site" button. - */ - USER_ADDONS("User Add-ons", false), - - /** - * Add-ons contributed by the SDK_UPDATER_USER_URLS env var, - * only used for local debugging. - */ - GETENV_ADDONS("Custom Add-ons", false); - - - private final String mUiName; - private final boolean mAlwaysDisplay; - - SdkSourceCategory(String uiName, boolean alwaysDisplay) { - mUiName = uiName; - mAlwaysDisplay = alwaysDisplay; - } - - /** - * Returns the UI-visible name of the category. Displayed in the available package tree. - * Cannot be null nor empty. - */ - public String getUiName() { - return mUiName; - } - - /** - * True if this category must always be displayed by the available package tree, even - * if empty. - * When false, the category must not be displayed when empty. - */ - public boolean getAlwaysDisplay() { - return mAlwaysDisplay; - } - - @Override - public String getLongDescription() { - return getUiName(); - } - - @Override - public String getShortDescription() { - return getUiName(); - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSourceProperties.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSourceProperties.java deleted file mode 100644 index fb8014ce..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSourceProperties.java +++ /dev/null @@ -1,254 +0,0 @@ -/* - * Copyright (C) 2012 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.sdklib.internal.repository.sources; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.annotations.VisibleForTesting; -import com.android.annotations.VisibleForTesting.Visibility; -import com.android.prefs.AndroidLocation; -import com.android.prefs.AndroidLocation.AndroidLocationException; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Properties; - -/** - * Properties for individual sources which are persisted by a local settings file. - *

- * All instances of {@link SdkSourceProperties} share the same singleton storage. - * The persisted setting file is loaded as necessary, however callers must persist - * it at some point by calling {@link #save()}. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class SdkSourceProperties { - - /** - * An internal file version number, in case we want to change the format later. - */ - private static final String KEY_VERSION = "@version@"; //$NON-NLS-1$ - /** - * The last known UI name of the source. - */ - public static final String KEY_NAME = "@name@"; //$NON-NLS-1$ - /** - * A non-null string if the source is disabled. Null if the source is enabled. - */ - public static final String KEY_DISABLED = "@disabled@"; //$NON-NLS-1$ - - private static final Properties sSourcesProperties = new Properties(); - private static final String SRC_FILENAME = "sites-settings.cfg"; //$NON-NLS-1$ - - private static boolean sModified = false; - - public SdkSourceProperties() { - } - - public void save() { - synchronized (sSourcesProperties) { - if (sModified && !sSourcesProperties.isEmpty()) { - saveLocked(); - sModified = false; - } - } - } - - /** - * Retrieves a property for the given source URL and the given key type. - *

- * Implementation detail: this loads the persistent settings file as needed. - * - * @param key The kind of property to retrieve for that source URL. - * @param sourceUrl The source URL. - * @param defaultValue The default value to return, if the property isn't found. Can be null. - * @return The non-null string property for the key/sourceUrl or the default value. - */ - @Nullable - public String getProperty(@NonNull String key, - @NonNull String sourceUrl, - @Nullable String defaultValue) { - String value = defaultValue; - - synchronized (sSourcesProperties) { - if (sSourcesProperties.isEmpty()) { - loadLocked(); - } - - value = sSourcesProperties.getProperty(key + sourceUrl, defaultValue); - } - - return value; - } - - /** - * Sets or remove a property for the given source URL and the given key type. - *

- * Implementation detail: this does not save the persistent settings file. - * Somehow the caller will need to call the {@link #save()} method later. - * - * @param key The kind of property to retrieve for that source URL. - * @param sourceUrl The source URL. - * @param value The new value to set (if non null) or null to remove an existing property. - */ - public void setProperty(String key, String sourceUrl, String value) { - synchronized (sSourcesProperties) { - if (sSourcesProperties.isEmpty()) { - loadLocked(); - } - - key += sourceUrl; - - String old = sSourcesProperties.getProperty(key); - if (value == null) { - if (old != null) { - sSourcesProperties.remove(key); - sModified = true; - } - } else if (old == null || !old.equals(value)) { - sSourcesProperties.setProperty(key, value); - sModified = true; - } - } - } - - /** - * Returns an internal string representation of the underlying Properties map, - * sorted by ascending keys. Useful for debugging and testing purposes only. - */ - @Override - public String toString() { - StringBuilder sb = new StringBuilder(" keys = Collections.list(sSourcesProperties.keys()); - Collections.sort(keys, new Comparator() { - @Override - public int compare(Object o1, Object o2) { - return o1.toString().compareTo(o2.toString()); - }}); - - for (Object key : keys) { - sb.append('\n').append(key) - .append(" = ").append(sSourcesProperties.get(key)); //$NON-NLS-1$ - } - } - sb.append('>'); - return sb.toString(); - } - - /** Load state from persistent file. Expects sSourcesProperties to be synchronized. */ - private void loadLocked() { - // Load state from persistent file - if (loadProperties()) { - // If it lacks our magic version key, don't use it - if (sSourcesProperties.getProperty(KEY_VERSION) == null) { - sSourcesProperties.clear(); - } - - sModified = false; - } - - if (sSourcesProperties.isEmpty()) { - // Nothing was loaded. Initialize the storage with a version - // identified. This isn't currently checked back, but we might - // want it later if we decide to change the way this works. - // The version key is chosen on purpose to not match any valid URL. - sSourcesProperties.setProperty(KEY_VERSION, "1"); //$NON-NLS-1$ //$NON-NLS-2$ - } - } - - /** - * Load properties from default file. Extracted so that it can be mocked in tests. - * - * @return True if actually loaded the file. False if there was an IO error or no - * file and nothing was loaded. - */ - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected boolean loadProperties() { - try { - String folder = AndroidLocation.getFolder(); - File f = new File(folder, SRC_FILENAME); - if (f.exists()) { - FileInputStream fis = null; - try { - fis = new FileInputStream(f); - sSourcesProperties.load(fis); - } catch (IOException ignore) { - // nop - } finally { - if (fis != null) { - try { - fis.close(); - } catch (IOException ignore) {} - } - } - - return true; - } - } catch (AndroidLocationException ignore) { - // nop - } - return false; - } - - /** - * Save file to disk. Expects sSourcesProperties to be synchronized. - * Made accessible for testing purposes. - * For public usage, please use {@link #save()} instead. - */ - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected void saveLocked() { - // Persist it to the file - FileOutputStream fos = null; - try { - String folder = AndroidLocation.getFolder(); - File f = new File(folder, SRC_FILENAME); - - fos = new FileOutputStream(f); - - sSourcesProperties.store(fos,"## Sites Settings for Android SDK Manager");//$NON-NLS-1$ - - } catch (AndroidLocationException ignore) { - // nop - } catch (IOException ignore) { - // nop - } finally { - if (fos != null) { - try { - fos.close(); - } catch (IOException ignore) {} - } - } - } - - /** Empty current property list. Made accessible for testing purposes. */ - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected void clear() { - synchronized (sSourcesProperties) { - sSourcesProperties.clear(); - sModified = false; - } - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSources.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSources.java deleted file mode 100644 index 7c663cad..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSources.java +++ /dev/null @@ -1,444 +0,0 @@ -/* - * 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.sdklib.internal.repository.sources; - -import com.android.prefs.AndroidLocation; -import com.android.prefs.AndroidLocation.AndroidLocationException; -import com.android.sdklib.repository.SdkSysImgConstants; -import com.android.utils.ILogger; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.EnumMap; -import java.util.Iterator; -import java.util.Properties; -import java.util.Map.Entry; - -/** - * A list of sdk-repository and sdk-addon sources, sorted by {@link SdkSourceCategory}. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class SdkSources { - - private static final String KEY_COUNT = "count"; - - private static final String KEY_SRC = "src"; - - private static final String SRC_FILENAME = "repositories.cfg"; //$NON-NLS-1$ - - private final EnumMap> mSources = - new EnumMap>(SdkSourceCategory.class); - - private ArrayList mChangeListeners; // lazily initialized - - - public SdkSources() { - } - - /** - * Adds a new source to the Sources list. - *

- * Implementation detail: {@link SdkSources} doesn't invoke {@link #notifyChangeListeners()} - * directly. Callers who use {@code add()} are responsible for notifying the listeners once - * they are done modifying the sources list. The intent is to notify the listeners only once - * at the end, not for every single addition. - */ - public void add(SdkSourceCategory category, SdkSource source) { - synchronized (mSources) { - ArrayList list = mSources.get(category); - if (list == null) { - list = new ArrayList(); - mSources.put(category, list); - } - - list.add(source); - } - } - - /** - * Removes a source from the Sources list. - *

- * Callers who remove entries are responsible for notifying the listeners using - * {@link #notifyChangeListeners()} once they are done modifying the sources list. - */ - public void remove(SdkSource source) { - synchronized (mSources) { - Iterator>> it = - mSources.entrySet().iterator(); - while (it.hasNext()) { - Entry> entry = it.next(); - ArrayList list = entry.getValue(); - - if (list.remove(source)) { - if (list.isEmpty()) { - // remove the entry since the source list became empty - it.remove(); - } - } - } - } - } - - /** - * Removes all the sources in the given category. - *

- * Callers who remove entries are responsible for notifying the listeners using - * {@link #notifyChangeListeners()} once they are done modifying the sources list. - */ - public void removeAll(SdkSourceCategory category) { - synchronized (mSources) { - mSources.remove(category); - } - } - - /** - * Returns a set of all categories that must be displayed. This includes all - * categories that are to be always displayed as well as all categories which - * have at least one source. - * Might return a empty array, but never returns null. - */ - public SdkSourceCategory[] getCategories() { - ArrayList cats = new ArrayList(); - - for (SdkSourceCategory cat : SdkSourceCategory.values()) { - if (cat.getAlwaysDisplay()) { - cats.add(cat); - } else { - synchronized (mSources) { - ArrayList list = mSources.get(cat); - if (list != null && !list.isEmpty()) { - cats.add(cat); - } - } - } - } - - return cats.toArray(new SdkSourceCategory[cats.size()]); - } - - /** - * Returns a new array of sources attached to the given category. - * Might return an empty array, but never returns null. - */ - public SdkSource[] getSources(SdkSourceCategory category) { - synchronized (mSources) { - ArrayList list = mSources.get(category); - if (list == null) { - return new SdkSource[0]; - } else { - return list.toArray(new SdkSource[list.size()]); - } - } - } - - /** - * Returns true if there are sources for the given category. - */ - public boolean hasSources(SdkSourceCategory category) { - synchronized (mSources) { - ArrayList list = mSources.get(category); - return list != null && !list.isEmpty(); - } - } - - /** - * Returns an array of the sources across all categories. This is never null. - */ - public SdkSource[] getAllSources() { - synchronized (mSources) { - int n = 0; - - for (ArrayList list : mSources.values()) { - n += list.size(); - } - - SdkSource[] sources = new SdkSource[n]; - - int i = 0; - for (ArrayList list : mSources.values()) { - for (SdkSource source : list) { - sources[i++] = source; - } - } - - return sources; - } - } - - /** - * Each source keeps a local cache of whatever it loaded recently. - * This calls {@link SdkSource#clearPackages()} on all the available sources, - * and the next call to {@link SdkSource#getPackages()} will actually reload - * the remote package list. - */ - public void clearAllPackages() { - synchronized (mSources) { - for (ArrayList list : mSources.values()) { - for (SdkSource source : list) { - source.clearPackages(); - } - } - } - } - - /** - * Returns the category of a given source, or null if the source is unknown. - *

- * Note that this method uses object identity to find a given source, and does - * not identify sources by their URL like {@link #hasSourceUrl(SdkSource)} does. - *

- * The search is O(N), which should be acceptable on the expectedly small source list. - */ - public SdkSourceCategory getCategory(SdkSource source) { - if (source != null) { - synchronized (mSources) { - for (Entry> entry : mSources.entrySet()) { - if (entry.getValue().contains(source)) { - return entry.getKey(); - } - } - } - } - return null; - } - - /** - * Returns true if there's already a similar source in the sources list - * under any category. - *

- * Important: The match is NOT done on object identity. - * Instead, this searches for a similar source, based on - * {@link SdkSource#equals(Object)} which compares the source URLs. - *

- * The search is O(N), which should be acceptable on the expectedly small source list. - */ - public boolean hasSourceUrl(SdkSource source) { - synchronized (mSources) { - for (ArrayList list : mSources.values()) { - for (SdkSource s : list) { - if (s.equals(source)) { - return true; - } - } - } - return false; - } - } - - /** - * Returns true if there's already a similar source in the sources list - * under the specified category. - *

- * Important: The match is NOT done on object identity. - * Instead, this searches for a similar source, based on - * {@link SdkSource#equals(Object)} which compares the source URLs. - *

- * The search is O(N), which should be acceptable on the expectedly small source list. - */ - public boolean hasSourceUrl(SdkSourceCategory category, SdkSource source) { - synchronized (mSources) { - ArrayList list = mSources.get(category); - if (list != null) { - for (SdkSource s : list) { - if (s.equals(source)) { - return true; - } - } - } - return false; - } - } - - /** - * Loads all user sources. This replaces all existing user sources - * by the ones from the property file. - *

- * This calls {@link #notifyChangeListeners()} at the end of the operation. - */ - public void loadUserAddons(ILogger log) { - // Implementation detail: synchronize on the sources list to make sure that - // a- the source list doesn't change while we load/save it, and most important - // b- to make sure it's not being saved while loaded or the reverse. - // In most cases we do these operation from the UI thread so it's not really - // that necessary. This is more a protection in case of someone calls this - // from a worker thread by mistake. - synchronized (mSources) { - // Remove all existing user sources - removeAll(SdkSourceCategory.USER_ADDONS); - - // Load new user sources from property file - FileInputStream fis = null; - try { - String folder = AndroidLocation.getFolder(); - File f = new File(folder, SRC_FILENAME); - if (f.exists()) { - fis = new FileInputStream(f); - - Properties props = new Properties(); - props.load(fis); - - int count = Integer.parseInt(props.getProperty(KEY_COUNT, "0")); - - for (int i = 0; i < count; i++) { - String url = props.getProperty(String.format("%s%02d", KEY_SRC, i)); //$NON-NLS-1$ - if (url != null) { - // FIXME: this code originally only dealt with add-on XML sources. - // Now we'd like it to deal with system-image sources too, but we - // don't know which kind of object it is (at least not without - // trying to fetch it.) As a temporary workaround, just take a - // guess based on the leaf URI name. However ideally what we can - // simply do is add a checkbox "is system-image XML" in the user - // dialog and pass this info down here. Another alternative is to - // make a "dynamic" source object that tries to guess its type once - // the URI has been fetched. - SdkSource s; - if (url.endsWith(SdkSysImgConstants.URL_DEFAULT_FILENAME)) { - s = new SdkSysImgSource(url, null/*uiName*/); - } else { - s = new SdkAddonSource(url, null/*uiName*/); - } - if (!hasSourceUrl(s)) { - add(SdkSourceCategory.USER_ADDONS, s); - } - } - } - } - - } catch (NumberFormatException e) { - log.error(e, null); - - } catch (AndroidLocationException e) { - log.error(e, null); - - } catch (IOException e) { - log.error(e, null); - - } finally { - if (fis != null) { - try { - fis.close(); - } catch (IOException e) { - } - } - } - } - notifyChangeListeners(); - } - - /** - * Saves all the user sources. - * @param log Logger. Cannot be null. - */ - public void saveUserAddons(ILogger log) { - // See the implementation detail note in loadUserAddons() about the synchronization. - synchronized (mSources) { - FileOutputStream fos = null; - try { - String folder = AndroidLocation.getFolder(); - File f = new File(folder, SRC_FILENAME); - - fos = new FileOutputStream(f); - - Properties props = new Properties(); - - int count = 0; - for (SdkSource s : getSources(SdkSourceCategory.USER_ADDONS)) { - props.setProperty(String.format("%s%02d", KEY_SRC, count), //$NON-NLS-1$ - s.getUrl()); - count++; - } - props.setProperty(KEY_COUNT, Integer.toString(count)); - - props.store( fos, "## User Sources for Android SDK Manager"); //$NON-NLS-1$ - - } catch (AndroidLocationException e) { - log.error(e, null); - - } catch (IOException e) { - log.error(e, null); - - } finally { - if (fos != null) { - try { - fos.close(); - } catch (IOException e) { - } - } - } - } - } - - /** - * Adds a listener that will be notified when the sources list has changed. - * - * @param changeListener A non-null listener to add. Ignored if already present. - * @see SdkSources#notifyChangeListeners() - */ - public void addChangeListener(Runnable changeListener) { - assert changeListener != null; - if (mChangeListeners == null) { - mChangeListeners = new ArrayList(); - } - synchronized (mChangeListeners) { - if (changeListener != null && !mChangeListeners.contains(changeListener)) { - mChangeListeners.add(changeListener); - } - } - } - - /** - * Removes a listener from the list of listeners to notify when the sources change. - * - * @param changeListener A listener to remove. Ignored if not previously added. - */ - public void removeChangeListener(Runnable changeListener) { - if (mChangeListeners != null && changeListener != null) { - synchronized (mChangeListeners) { - mChangeListeners.remove(changeListener); - } - } - } - - /** - * Invoke all the registered change listeners, if any. - *

- * This may be called from a worker thread, in which case the runnable - * should take care of only updating UI from a main thread. - */ - public void notifyChangeListeners() { - if (mChangeListeners == null) { - return; - } - synchronized (mChangeListeners) { - for (Runnable runnable : mChangeListeners) { - try { - runnable.run(); - } catch (Throwable ignore) { - assert ignore == null : - "A SdkSource.ChangeListener failed with an exception: " + ignore.toString(); - } - } - } - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSysImgSource.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSysImgSource.java deleted file mode 100644 index 1c90b4cd..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/sources/SdkSysImgSource.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (C) 2012 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.sdklib.internal.repository.sources; - -import com.android.annotations.Nullable; -import com.android.sdklib.internal.repository.packages.Package; -import com.android.sdklib.repository.SdkSysImgConstants; - -import org.w3c.dom.Document; - -import java.io.InputStream; - - -/** - * An sdk-sys-img source, i.e. a download site for system-image packages. - * A repository describes one or more {@link Package}s available for download. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class SdkSysImgSource extends SdkSource { - - /** - * Constructs a new source for the given repository URL. - * @param url The source URL. Cannot be null. If the URL ends with a /, the default - * sys-img.xml filename will be appended automatically. - * @param uiName The UI-visible name of the source. Can be null. - */ - public SdkSysImgSource(String url, String uiName) { - super(url, uiName); - } - - /** - * Returns true if this is an addon source. - * We only load addons and extras from these sources. - */ - @Override - public boolean isAddonSource() { - return false; - } - - /** - * Returns true if this is a system-image source. - * We only load system-images from these sources. - */ - @Override - public boolean isSysImgSource() { - return true; - } - - - @Override - protected String[] getDefaultXmlFileUrls() { - return new String[] { SdkSysImgConstants.URL_DEFAULT_FILENAME }; - } - - @Override - protected int getNsLatestVersion() { - return SdkSysImgConstants.NS_LATEST_VERSION; - } - - @Override - protected String getNsUri() { - return SdkSysImgConstants.NS_URI; - } - - @Override - protected String getNsPattern() { - return SdkSysImgConstants.NS_PATTERN; - } - - @Override - protected String getSchemaUri(int version) { - return SdkSysImgConstants.getSchemaUri(version); - } - - @Override - protected String getRootElementName() { - return SdkSysImgConstants.NODE_SDK_SYS_IMG; - } - - @Override - protected InputStream getXsdStream(int version) { - return SdkSysImgConstants.getXsdStream(version); - } - - /** - * This kind of schema does not support forward-evolution of the <tool> element. - * - * @param xml The input XML stream. Can be null. - * @return Always null. - * @null This implementation always return null. - */ - @Override - protected Document findAlternateToolsXml(@Nullable InputStream xml) { - return null; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/ArchiveInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/ArchiveInfo.java deleted file mode 100644 index c2098389..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/ArchiveInfo.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * 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.sdklib.internal.repository.updater; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.sdklib.internal.repository.archives.Archive; -import com.android.sdklib.internal.repository.archives.ArchiveReplacement; - -import java.util.ArrayList; -import java.util.Collection; - -/** - * Represents an archive that we want to install. - * Note that the installer deals with archives whereas the user mostly sees packages - * but as far as we are concerned for installation there's a 1-to-1 mapping. - *

- * A new archive is always a remote archive that needs to be downloaded and then - * installed. It can replace an existing local one. It can also depends on another - * (new or local) archive, which means the dependent archive needs to be successfully - * installed first. Finally this archive can also be a dependency for another one. - *

- * The accepted and rejected flags are used by {@code SdkUpdaterChooserDialog} to follow - * user choices. The installer should never install something that is not accepted. - *

- * Note: There is currently no logic to support more than one level of - * dependency, either here or in the {@code SdkUpdaterChooserDialog}, since we currently - * have no need for it. - * - * @see ArchiveInfo#ArchiveInfo(Archive, Archive, ArchiveInfo[]) - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class ArchiveInfo extends ArchiveReplacement implements Comparable { - - private final ArchiveInfo[] mDependsOn; - private final ArrayList mDependencyFor = new ArrayList(); - private boolean mAccepted; - private boolean mRejected; - - /** - * Creates a new replacement where the {@code newArchive} will replace the - * currently installed {@code replaced} archive. - * When {@code newArchive} is not intended to replace anything (e.g. because - * the user is installing a new package not present on her system yet), then - * {@code replace} shall be null. - * - * @param newArchive A "new archive" to be installed. This is always an archive - * that comes from a remote site. This may be null. - * @param replaced An optional local archive that the new one will replace. - * Can be null if this archive does not replace anything. - * @param dependsOn An optional new or local dependency, that is an archive that - * this archive depends upon. In other words, we can only install - * this archive if the dependency has been successfully installed. It also - * means we need to install the dependency first. Can be null or empty. - * However it cannot contain nulls. - */ - public ArchiveInfo( - @Nullable Archive newArchive, - @Nullable Archive replaced, - @Nullable ArchiveInfo[] dependsOn) { - super(newArchive, replaced); - mDependsOn = dependsOn; - } - - /** - * Returns an optional new or local dependency, that is an archive that this - * archive depends upon. In other words, we can only install this archive if the - * dependency has been successfully installed. It also means we need to install the - * dependency first. - *

- * This array can be null or empty. It can't contain nulls though. - */ - @Nullable - public ArchiveInfo[] getDependsOn() { - return mDependsOn; - } - - /** - * Returns true if this new archive is a dependency for another one that we - * want to install. - */ - public boolean isDependencyFor() { - return !mDependencyFor.isEmpty(); - } - - /** - * Adds an {@link ArchiveInfo} for which this package is a dependency. - * This means the package added here depends on this package. - */ - @NonNull - public ArchiveInfo addDependencyFor(ArchiveInfo dependencyFor) { - if (!mDependencyFor.contains(dependencyFor)) { - mDependencyFor.add(dependencyFor); - } - - return this; - } - - /** - * Returns the list of {@link ArchiveInfo} for which this package is a dependency. - * This means the packages listed here depend on this package. - *

- * Implementation detail: this is the internal mutable list. Callers should not modify it. - * This list can be empty but is never null. - */ - @NonNull - public Collection getDependenciesFor() { - return mDependencyFor; - } - - /** - * Sets whether this archive was accepted (either manually by the user or - * automatically if it doesn't have a license) for installation. - */ - public void setAccepted(boolean accepted) { - mAccepted = accepted; - } - - /** - * Returns whether this archive was accepted (either manually by the user or - * automatically if it doesn't have a license) for installation. - */ - public boolean isAccepted() { - return mAccepted; - } - - /** - * Sets whether this archive was rejected manually by the user. - * An archive can neither accepted nor rejected. - */ - public void setRejected(boolean rejected) { - mRejected = rejected; - } - - /** - * Returns whether this archive was rejected manually by the user. - * An archive can neither accepted nor rejected. - */ - public boolean isRejected() { - return mRejected; - } - - /** - * ArchiveInfos are compared using ther "new archive" ordering. - * - * @see Archive#compareTo(Archive) - */ - @Override - public int compareTo(ArchiveInfo rhs) { - if (getNewArchive() != null && rhs != null) { - return getNewArchive().compareTo(rhs.getNewArchive()); - } - return 0; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/ISettingsPage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/ISettingsPage.java deleted file mode 100644 index 1da1b01c..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/ISettingsPage.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * 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.sdklib.internal.repository.updater; - -import com.android.sdklib.internal.repository.DownloadCache; - -import java.net.URL; -import java.util.Properties; - -/** - * Interface that a settings page must implement. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public interface ISettingsPage { - - /** - * Java system setting picked up by {@link URL} for http proxy port. - * Type: String. - */ - String KEY_HTTP_PROXY_PORT = "http.proxyPort"; //$NON-NLS-1$ - - /** - * Java system setting picked up by {@link URL} for http proxy host. - * Type: String. - */ - String KEY_HTTP_PROXY_HOST = "http.proxyHost"; //$NON-NLS-1$ - - /** - * Setting to force using http:// instead of https:// connections. - * Type: Boolean. - * Default: False. - */ - String KEY_FORCE_HTTP = "sdkman.force.http"; //$NON-NLS-1$ - - /** - * Setting to display only packages that are new or updates. - * Type: Boolean. - * Default: True. - */ - String KEY_SHOW_UPDATE_ONLY = "sdkman.show.update.only"; //$NON-NLS-1$ - - /** - * Setting to ask for permission before restarting ADB. - * Type: Boolean. - * Default: False. - */ - String KEY_ASK_ADB_RESTART = "sdkman.ask.adb.restart"; //$NON-NLS-1$ - - /** - * Setting to use the {@link DownloadCache}, for small manifest XML files. - * Type: Boolean. - * Default: True. - */ - String KEY_USE_DOWNLOAD_CACHE = "sdkman.use.dl.cache"; //$NON-NLS-1$ - - /** - * Setting to enabling previews in the package list - * Type: Boolean. - * Default: True. - */ - String KEY_ENABLE_PREVIEWS = "sdkman.enable.previews2"; //$NON-NLS-1$ - - /** - * Setting to set the density of the monitor. - * Type: Integer. - * Default: -1 - */ - String KEY_MONITOR_DENSITY = "sdkman.monitor.density"; //$NON-NLS-1$ - - /** Loads settings from the given {@link Properties} container and update the page UI. */ - void loadSettings(Properties inSettings); - - /** Called by the application to retrieve settings from the UI and store them in - * the given {@link Properties} container. */ - void retrieveSettings(Properties outSettings); - - /** - * Called by the application to give a callback that the page should invoke when - * settings have changed. - */ - void setOnSettingsChanged(SettingsChangedCallback settingsChangedCallback); - - /** - * Callback used to notify the application that settings have changed and need to be - * applied. - */ - interface SettingsChangedCallback { - /** - * Invoked by the settings page when settings have changed and need to be - * applied. The application will call {@link ISettingsPage#retrieveSettings(Properties)} - * and apply the new settings. - */ - void onSettingsChanged(ISettingsPage page); - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/IUpdaterData.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/IUpdaterData.java deleted file mode 100644 index b3a74f2f..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/IUpdaterData.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2010 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.sdklib.internal.repository.updater; - -import com.android.sdklib.SdkManager; -import com.android.sdklib.internal.avd.AvdManager; -import com.android.sdklib.internal.repository.DownloadCache; -import com.android.sdklib.internal.repository.ITaskFactory; -import com.android.utils.ILogger; - - -/** - * Interface used to retrieve some parameters from an {@link UpdaterData} instance. - * Useful mostly for unit tests purposes. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public interface IUpdaterData { - - ITaskFactory getTaskFactory(); - - ILogger getSdkLog(); - - DownloadCache getDownloadCache(); - - SdkManager getSdkManager(); - - AvdManager getAvdManager(); - - SettingsController getSettingsController(); - -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/PackageLoader.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/PackageLoader.java deleted file mode 100644 index c0314d4b..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/PackageLoader.java +++ /dev/null @@ -1,502 +0,0 @@ -/* - * Copyright (C) 2011 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.sdklib.internal.repository.updater; - -import com.android.annotations.NonNull; -import com.android.sdklib.internal.repository.AddonsListFetcher; -import com.android.sdklib.internal.repository.AddonsListFetcher.Site; -import com.android.sdklib.internal.repository.DownloadCache; -import com.android.sdklib.internal.repository.ITask; -import com.android.sdklib.internal.repository.ITaskMonitor; -import com.android.sdklib.internal.repository.NullTaskMonitor; -import com.android.sdklib.internal.repository.archives.Archive; -import com.android.sdklib.internal.repository.packages.Package; -import com.android.sdklib.internal.repository.packages.Package.UpdateInfo; -import com.android.sdklib.internal.repository.sources.SdkAddonSource; -import com.android.sdklib.internal.repository.sources.SdkSource; -import com.android.sdklib.internal.repository.sources.SdkSourceCategory; -import com.android.sdklib.internal.repository.sources.SdkSources; -import com.android.sdklib.internal.repository.sources.SdkSysImgSource; -import com.android.sdklib.repository.SdkAddonsListConstants; -import com.android.sdklib.repository.SdkRepoConstants; - -import java.io.File; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Loads packages fetched from the remote SDK Repository and keeps track - * of their state compared with the current local SDK installation. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class PackageLoader { - - /** The update data context. Never null. */ - private final UpdaterData mUpdaterData; - - /** - * The {@link DownloadCache} override. Can be null, in which case the one from - * {@link UpdaterData} is used instead. - * @see #getDownloadCache() - */ - private final DownloadCache mOverrideCache; - - /** - * 0 = need to fetch remote addons list once.. - * 1 = fetch succeeded, don't need to do it any more. - * -1= fetch failed, do it again only if the user requests a refresh - * or changes the force-http setting. - */ - private int mStateFetchRemoteAddonsList; - - - /** - * Interface for the callback called by - * {@link PackageLoader#loadPackages(boolean, ISourceLoadedCallback)}. - *

- * After processing each source, the package loader calls {@link #onUpdateSource} - * with the list of packages found in that source. - * By returning true from {@link #onUpdateSource}, the client tells the loader to - * continue and process the next source. By returning false, it tells to stop loading. - *

- * The {@link #onLoadCompleted()} method is guaranteed to be called at the end, no - * matter how the loader stopped, so that the client can clean up or perform any - * final action. - */ - public interface ISourceLoadedCallback { - /** - * After processing each source, the package loader calls this method with the - * list of packages found in that source. - * By returning true from {@link #onUpdateSource}, the client tells - * the loader to continue and process the next source. - * By returning false, it tells to stop loading. - *

- * Important: This method is called from a sub-thread, so clients which - * try to access any UI widgets must wrap their calls into - * {@code Display.syncExec(Runnable)} or {@code Display.asyncExec(Runnable)}. - * - * @param packages All the packages loaded from the source. Never null. - * @return True if the load operation should continue, false if it should stop. - */ - boolean onUpdateSource(SdkSource source, Package[] packages); - - /** - * This method is guaranteed to be called at the end, no matter how the - * loader stopped, so that the client can clean up or perform any final action. - */ - void onLoadCompleted(); - } - - /** - * Interface describing the task of installing a specific package. - * For details on the operation, - * see {@link PackageLoader#loadPackagesWithInstallTask(int, IAutoInstallTask)}. - * - * @see PackageLoader#loadPackagesWithInstallTask(int, IAutoInstallTask) - */ - public interface IAutoInstallTask { - /** - * Invoked by the loader once a source has been loaded and its package - * definitions are known. The method should return the {@code packages} - * array and can modify it if necessary. - * The loader will call {@link #acceptPackage(Package)} on all the packages returned. - * - * @param source The source of the packages. Null for the locally installed packages. - * @param packages The packages found in the source. - */ - Package[] filterLoadedSource(SdkSource source, Package[] packages); - - /** - * Called by the install task for every package available (new ones, updates as well - * as existing ones that don't have a potential update.) - * The method should return true if this is a package that should be installed. - *

- * Important: This method is called from a sub-thread, so clients who try - * to access any UI widgets must wrap their calls into {@code Display.syncExec(Runnable)} - * or {@code Display.asyncExec(Runnable)}. - */ - boolean acceptPackage(Package pkg); - - /** - * Called when the accepted package has been installed, successfully or not. - * If an already installed (aka existing) package has been accepted, this will - * be called with a 'true' success and the actual install paths. - *

- * Important: This method is called from a sub-thread, so clients who try - * to access any UI widgets must wrap their calls into {@code Display.syncExec(Runnable)} - * or {@code Display.asyncExec(Runnable)}. - */ - void setResult(boolean success, Map installPaths); - - /** - * Called when the task is done iterating and completed. - */ - void taskCompleted(); - } - - /** - * Creates a new PackageManager associated with the given {@link UpdaterData} - * and using the {@link UpdaterData}'s default {@link DownloadCache}. - * - * @param updaterData The {@link UpdaterData}. Must not be null. - */ - public PackageLoader(UpdaterData updaterData) { - mUpdaterData = updaterData; - mOverrideCache = null; - } - - /** - * Creates a new PackageManager associated with the given {@link UpdaterData} - * but using the specified {@link DownloadCache} instead of the one from - * {@link UpdaterData}. - * - * @param updaterData The {@link UpdaterData}. Must not be null. - * @param cache The {@link DownloadCache} to use instead of the one from {@link UpdaterData}. - */ - public PackageLoader(UpdaterData updaterData, DownloadCache cache) { - mUpdaterData = updaterData; - mOverrideCache = cache; - } - - public UpdaterData getUpdaterData() { - return mUpdaterData; - } - - /** - * Runs a runnable on the UI thread. - * The base implementation just runs the runnable right away. - * - * @param r Non-null runnable. - */ - protected void runOnUiThread(@NonNull Runnable r) { - r.run(); - } - - /** - * Loads all packages from the remote repository. - * This runs in an {@link ITask}. The call is blocking. - *

- * The callback is called with each set of {@link PkgItem} found in each source. - * The caller is responsible to accumulate the packages given to the callback - * after each source is finished loaded. In return the callback tells the loader - * whether to continue loading sources. - *

- * Normally this method doesn't access the remote source if it's already - * been loaded in the in-memory source (e.g. don't fetch twice). - * - * @param overrideExisting Set this to true when the caller wants to - * check for updates and discard any existing source already - * loaded in memory. It should be false for normal use. - * @param sourceLoadedCallback The callback to invoke for each loaded source. - */ - public void loadPackages( - final boolean overrideExisting, - final ISourceLoadedCallback sourceLoadedCallback) { - try { - if (mUpdaterData == null) { - return; - } - - mUpdaterData.getTaskFactory().start("Loading Sources", new ITask() { - @Override - public void run(ITaskMonitor monitor) { - monitor.setProgressMax(10); - - // get local packages and offer them to the callback - Package[] localPkgs = - mUpdaterData.getInstalledPackages(monitor.createSubMonitor(1)); - if (localPkgs == null) { - localPkgs = new Package[0]; - } - if (!sourceLoadedCallback.onUpdateSource(null, localPkgs)) { - return; - } - - // get remote packages - boolean forceHttp = - mUpdaterData.getSettingsController().getSettings().getForceHttp(); - loadRemoteAddonsList(monitor.createSubMonitor(1)); - - SdkSource[] sources = mUpdaterData.getSources().getAllSources(); - try { - if (sources != null && sources.length > 0) { - ITaskMonitor subMonitor = monitor.createSubMonitor(8); - subMonitor.setProgressMax(sources.length); - for (SdkSource source : sources) { - Package[] pkgs = source.getPackages(); - if (pkgs == null || overrideExisting) { - source.load(getDownloadCache(), - subMonitor.createSubMonitor(1), - forceHttp); - pkgs = source.getPackages(); - } - if (pkgs == null) { - continue; - } - - // Notify the callback a new source has finished loading. - // If the callback requests so, stop right away. - if (!sourceLoadedCallback.onUpdateSource(source, pkgs)) { - return; - } - } - } - } catch(Exception e) { - monitor.logError("Loading source failed: %1$s", e.toString()); - } finally { - monitor.setDescription("Done loading packages."); - } - } - }); - } finally { - sourceLoadedCallback.onLoadCompleted(); - } - } - - /** - * Load packages, source by source using - * {@link #loadPackages(boolean, ISourceLoadedCallback)}, - * and executes the given {@link IAutoInstallTask} on the current package list. - * That is for each package known, the install task is queried to find if - * the package is the one to be installed or updated. - *

- * - If an already installed package is accepted by the task, it is returned.
- * - If a new package (remotely available but not installed locally) is accepted, - * the user will be prompted for permission to install it.
- * - If an existing package has updates, the install task will be accept if it - * accepts one of the updating packages, and if yes the the user will be - * prompted for permission to install it.
- *

- * Only one package can be accepted, after which the task is completed. - * There is no direct return value, {@link IAutoInstallTask#setResult} is called on the - * result of the accepted package. - * When the task is completed, {@link IAutoInstallTask#taskCompleted()} is called. - *

- * The call is blocking. Although the name says "Task", this is not an {@link ITask} - * running in its own thread but merely a synchronous call. - * - * @param installFlags Flags for installation such as - * {@link UpdaterData#TOOLS_MSG_UPDATED_FROM_ADT}. - * @param installTask The task to perform. - */ - public void loadPackagesWithInstallTask( - final int installFlags, - final IAutoInstallTask installTask) { - - loadPackages(false /*overrideExisting*/, new ISourceLoadedCallback() { - List mArchivesToInstall = new ArrayList(); - Map mInstallPaths = new HashMap(); - - @Override - public boolean onUpdateSource(SdkSource source, Package[] packages) { - packages = installTask.filterLoadedSource(source, packages); - if (packages == null || packages.length == 0) { - // Tell loadPackages() to process the next source. - return true; - } - - for (Package pkg : packages) { - if (pkg.isLocal()) { - // This is a local (aka installed) package - if (installTask.acceptPackage(pkg)) { - // If the caller is accepting an installed package, - // return a success and give the package's install path - Archive[] a = pkg.getArchives(); - // an installed package should have one local compatible archive - if (a.length == 1 && a[0].isCompatible()) { - mInstallPaths.put(pkg, new File(a[0].getLocalOsPath())); - } - } - - } else { - // This is a remote package - if (installTask.acceptPackage(pkg)) { - // The caller is accepting this remote package. We'll install it. - for (Archive archive : pkg.getArchives()) { - if (archive.isCompatible()) { - mArchivesToInstall.add(archive); - break; - } - } - } - } - } - - // Tell loadPackages() to process the next source. - return true; - } - - @Override - public void onLoadCompleted() { - if (!mArchivesToInstall.isEmpty()) { - installArchives(mArchivesToInstall); - } - if (mInstallPaths == null) { - installTask.setResult(false, null); - } else { - installTask.setResult(true, mInstallPaths); - } - - installTask.taskCompleted(); - } - - /** - * Shows the UI of the install selector. - * If the package is then actually installed, refresh the local list and - * notify the install task of the installation path. - * - * @param archivesToInstall The archives to install. - */ - private void installArchives(final List archivesToInstall) { - // Actually install the new archives that we just found. - // This will display some UI so we need a shell's sync exec. - - final List installedArchives = new ArrayList(); - - runOnUiThread(new Runnable() { - @Override - public void run() { - List archives = - mUpdaterData.updateOrInstallAll_WithGUI( - archivesToInstall, - true /* includeObsoletes */, - installFlags); - - if (archives != null) { - installedArchives.addAll(archives); - } - } - }); - - if (installedArchives.isEmpty()) { - // We failed to install anything. - mInstallPaths = null; - return; - } - - // The local package list has changed, make sure to refresh it - mUpdaterData.getSdkManager().reloadSdk(mUpdaterData.getSdkLog()); - mUpdaterData.getLocalSdkParser().clearPackages(); - final Package[] localPkgs = mUpdaterData.getInstalledPackages( - new NullTaskMonitor(mUpdaterData.getSdkLog())); - - // Scan the installed package list to find the install paths. - for (Archive installedArchive : installedArchives) { - Package pkg = installedArchive.getParentPackage(); - - for (Package localPkg : localPkgs) { - if (localPkg.canBeUpdatedBy(pkg) == UpdateInfo.NOT_UPDATE) { - Archive[] localArchive = localPkg.getArchives(); - if (localArchive.length == 1 && localArchive[0].isCompatible()) { - mInstallPaths.put( - localPkg, - new File(localArchive[0].getLocalOsPath())); - } - } - } - } - } - }); - } - - - /** - * Loads the remote add-ons list. - */ - public void loadRemoteAddonsList(ITaskMonitor monitor) { - - if (mStateFetchRemoteAddonsList != 0) { - return; - } - - mUpdaterData.getTaskFactory().start("Load Add-ons List", monitor, new ITask() { - @Override - public void run(ITaskMonitor subMonitor) { - loadRemoteAddonsListInTask(subMonitor); - } - }); - } - - private void loadRemoteAddonsListInTask(ITaskMonitor monitor) { - mStateFetchRemoteAddonsList = -1; - - String url = SdkAddonsListConstants.URL_ADDON_LIST; - - // We override SdkRepoConstants.URL_GOOGLE_SDK_SITE if this is defined - String baseUrl = System.getenv("SDK_TEST_BASE_URL"); //$NON-NLS-1$ - if (baseUrl != null) { - if (!baseUrl.isEmpty() && baseUrl.endsWith("/")) { //$NON-NLS-1$ - if (url.startsWith(SdkRepoConstants.URL_GOOGLE_SDK_SITE)) { - url = baseUrl + url.substring(SdkRepoConstants.URL_GOOGLE_SDK_SITE.length()); - } - } else { - monitor.logError("Ignoring invalid SDK_TEST_BASE_URL: %1$s", baseUrl); //$NON-NLS-1$ - } - } - - if (mUpdaterData.getSettingsController().getSettings().getForceHttp()) { - url = url.replaceAll("https://", "http://"); //$NON-NLS-1$ //$NON-NLS-2$ - } - - // Hook to bypass loading 3rd party addons lists. - boolean fetch3rdParties = System.getenv("SDK_SKIP_3RD_PARTIES") == null; - - AddonsListFetcher fetcher = new AddonsListFetcher(); - Site[] sites = fetcher.fetch(url, getDownloadCache(), monitor); - if (sites != null) { - SdkSources sources = mUpdaterData.getSources(); - sources.removeAll(SdkSourceCategory.ADDONS_3RD_PARTY); - - if (fetch3rdParties) { - for (Site s : sites) { - switch (s.getType()) { - case ADDON_SITE: - sources.add(SdkSourceCategory.ADDONS_3RD_PARTY, - new SdkAddonSource(s.getUrl(), s.getUiName())); - break; - case SYS_IMG_SITE: - sources.add(SdkSourceCategory.ADDONS_3RD_PARTY, - new SdkSysImgSource(s.getUrl(), s.getUiName())); - break; - } - } - } - - sources.notifyChangeListeners(); - - mStateFetchRemoteAddonsList = 1; - } - - monitor.setDescription("Fetched Add-ons List successfully"); - } - - /** - * Returns the {@link DownloadCache} to use. - * - * @return Returns {@link #mOverrideCache} if not null; otherwise returns the - * one from {@link UpdaterData} is used instead. - */ - private DownloadCache getDownloadCache() { - return mOverrideCache != null ? mOverrideCache : mUpdaterData.getDownloadCache(); - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/PkgItem.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/PkgItem.java deleted file mode 100644 index 60f7aa04..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/PkgItem.java +++ /dev/null @@ -1,290 +0,0 @@ -/* - * Copyright (C) 2011 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.sdklib.internal.repository.updater; - -import com.android.annotations.Nullable; -import com.android.sdklib.AndroidVersion; -import com.android.sdklib.repository.IDescription; -import com.android.sdklib.internal.repository.archives.Archive; -import com.android.sdklib.internal.repository.packages.IAndroidVersionProvider; -import com.android.sdklib.internal.repository.packages.Package; -import com.android.sdklib.internal.repository.packages.Package.UpdateInfo; -import com.android.sdklib.internal.repository.sources.SdkSource; -import com.android.sdklib.repository.FullRevision; - -/** - * A {@link PkgItem} represents one main {@link Package} combined with its state - * and an optional update package. - *

- * The main package is final and cannot change since it's what "defines" this PkgItem. - * The state or update package can change later. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class PkgItem implements Comparable { - private final PkgState mState; - private final Package mMainPkg; - private Package mUpdatePkg; - private boolean mChecked; - - /** - * The state of the a given {@link PkgItem}, that is the relationship between - * a given remote package and the local repository. - */ - public enum PkgState { - // Implementation detail: if this is changed then PackageDiffLogic#STATES - // and PackageDiffLogic#processSource() need to be changed accordingly. - - /** - * Package is locally installed and may or may not have an update. - */ - INSTALLED, - - /** - * There's a new package available on the remote site that isn't installed locally. - */ - NEW - } - - /** - * Create a new {@link PkgItem} for this main package. - * The main package is final and cannot change since it's what "defines" this PkgItem. - * The state or update package can change later. - */ - public PkgItem(Package mainPkg, PkgState state) { - mMainPkg = mainPkg; - mState = state; - assert mMainPkg != null; - } - - public boolean isObsolete() { - return mMainPkg.isObsolete(); - } - - public boolean isChecked() { - return mChecked; - } - - public void setChecked(boolean checked) { - mChecked = checked; - } - - public Package getUpdatePkg() { - return mUpdatePkg; - } - - public boolean hasUpdatePkg() { - return mUpdatePkg != null; - } - - public String getName() { - return mMainPkg.getListDescription(); - } - - public FullRevision getRevision() { - return mMainPkg.getRevision(); - } - - /** - * @deprecated Use {@link #getMainPackage()} with the {@link IDescription} interface instead. - */ - @Deprecated - public String getDescription() { - return mMainPkg.getLongDescription(); - } - - public Package getMainPackage() { - return mMainPkg; - } - - public PkgState getState() { - return mState; - } - - public SdkSource getSource() { - return mMainPkg.getParentSource(); - } - - @Nullable - public AndroidVersion getAndroidVersion() { - return mMainPkg instanceof IAndroidVersionProvider ? - ((IAndroidVersionProvider) mMainPkg).getAndroidVersion() : - null; - } - - public Archive[] getArchives() { - return mMainPkg.getArchives(); - } - - @Override - public int compareTo(PkgItem pkg) { - return getMainPackage().compareTo(pkg.getMainPackage()); - } - - /** - * Returns true if this package or its updating packages contains - * the exact given archive. - * Important: This compares object references, not object equality. - */ - public boolean hasArchive(Archive archive) { - if (mMainPkg.hasArchive(archive)) { - return true; - } - if (mUpdatePkg != null && mUpdatePkg.hasArchive(archive)) { - return true; - } - return false; - } - - /** - * Returns true if the main package has at least one archive - * compatible with the current platform. - */ - public boolean hasCompatibleArchive() { - return mMainPkg.hasCompatibleArchive(); - } - - /** - * Checks whether the main packages are of the same type and are - * not an update of each other and have the same revision number. - */ - public boolean isSameMainPackageAs(Package pkg) { - if (mMainPkg.canBeUpdatedBy(pkg) == UpdateInfo.NOT_UPDATE) { - // package revision numbers must match - return mMainPkg.getRevision().equals(pkg.getRevision()); - } - return false; - } - - /** - * Checks whether the update packages are of the same type and are - * not an update of each other and have the same revision numbers. - */ - public boolean isSameUpdatePackageAs(Package pkg) { - if (mUpdatePkg != null && mUpdatePkg.canBeUpdatedBy(pkg) == UpdateInfo.NOT_UPDATE) { - // package revision numbers must match - return mUpdatePkg.getRevision().equals(pkg.getRevision()); - } - return false; - } - - /** - * Checks whether too {@link PkgItem} are the same. - * This checks both items have the same state, both main package are similar - * and that they have the same updating packages. - */ - public boolean isSameItemAs(PkgItem item) { - if (this == item) { - return true; - } - boolean same = this.mState == item.mState; - if (same) { - same = isSameMainPackageAs(item.getMainPackage()); - } - - if (same) { - // check updating packages are the same - Package p1 = this.mUpdatePkg; - Package p2 = item.getUpdatePkg(); - same = (p1 == p2) || (p1 == null && p2 == null) || (p1 != null && p2 != null); - - if (same && p1 != null) { - same = p1.canBeUpdatedBy(p2) == UpdateInfo.NOT_UPDATE; - } - } - - return same; - } - - /** - * Equality is defined as {@link #isSameItemAs(PkgItem)}: state, main package - * and update package must be the similar. - */ - @Override - public boolean equals(Object obj) { - return (obj instanceof PkgItem) && this.isSameItemAs((PkgItem) obj); - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((mState == null) ? 0 : mState.hashCode()); - result = prime * result + ((mMainPkg == null) ? 0 : mMainPkg.hashCode()); - result = prime * result + ((mUpdatePkg == null) ? 0 : mUpdatePkg.hashCode()); - return result; - } - - /** - * Check whether the 'pkg' argument is an update for this package. - * If it is, record it as an updating package. - * If there's already an updating package, only keep the most recent update. - * Returns true if it is update (even if there was already an update and this - * ended up not being the most recent), false if incompatible or not an update. - * - * This should only be used for installed packages. - */ - public boolean mergeUpdate(Package pkg) { - if (mUpdatePkg == pkg) { - return true; - } - if (mMainPkg.canBeUpdatedBy(pkg) == UpdateInfo.UPDATE) { - if (mUpdatePkg == null) { - mUpdatePkg = pkg; - } else if (mUpdatePkg.canBeUpdatedBy(pkg) == UpdateInfo.UPDATE) { - // If we have more than one, keep only the most recent update - mUpdatePkg = pkg; - } - return true; - } - - return false; - } - - public void removeUpdate() { - mUpdatePkg = null; - } - - /** Returns a string representation of this item, useful when debugging. */ - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append('<'); - - if (mChecked) { - sb.append(" * "); //$NON-NLS-1$ - } - - sb.append(mState.toString()); - - if (mMainPkg != null) { - sb.append(", pkg:"); //$NON-NLS-1$ - sb.append(mMainPkg.toString()); - } - - if (mUpdatePkg != null) { - sb.append(", updated by:"); //$NON-NLS-1$ - sb.append(mUpdatePkg.toString()); - } - - sb.append('>'); - return sb.toString(); - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/SdkUpdaterLogic.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/SdkUpdaterLogic.java deleted file mode 100644 index ff2000fa..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/SdkUpdaterLogic.java +++ /dev/null @@ -1,1568 +0,0 @@ -/* - * 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.sdklib.internal.repository.updater; - -import com.android.sdklib.AndroidVersion; -import com.android.sdklib.internal.repository.ITask; -import com.android.sdklib.internal.repository.ITaskMonitor; -import com.android.sdklib.internal.repository.archives.Archive; -import com.android.sdklib.internal.repository.packages.AddonPackage; -import com.android.sdklib.internal.repository.packages.BuildToolPackage; -import com.android.sdklib.internal.repository.packages.DocPackage; -import com.android.sdklib.internal.repository.packages.ExtraPackage; -import com.android.sdklib.internal.repository.packages.IAndroidVersionProvider; -import com.android.sdklib.internal.repository.packages.IExactApiLevelDependency; -import com.android.sdklib.internal.repository.packages.IMinApiLevelDependency; -import com.android.sdklib.internal.repository.packages.IMinPlatformToolsDependency; -import com.android.sdklib.internal.repository.packages.IMinToolsDependency; -import com.android.sdklib.internal.repository.packages.IPlatformDependency; -import com.android.sdklib.internal.repository.packages.MinToolsPackage; -import com.android.sdklib.internal.repository.packages.Package; -import com.android.sdklib.internal.repository.packages.Package.UpdateInfo; -import com.android.sdklib.internal.repository.packages.PlatformPackage; -import com.android.sdklib.internal.repository.packages.PlatformToolPackage; -import com.android.sdklib.internal.repository.packages.SamplePackage; -import com.android.sdklib.internal.repository.packages.SystemImagePackage; -import com.android.sdklib.internal.repository.packages.ToolPackage; -import com.android.sdklib.internal.repository.sources.SdkSource; -import com.android.sdklib.internal.repository.sources.SdkSources; -import com.android.sdklib.repository.FullRevision; -import com.android.sdklib.repository.descriptors.IdDisplay; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** - * The logic to compute which packages to install, based on the choices - * made by the user. This adds required packages as needed. - *

- * When the user doesn't provide a selection, looks at local package to find - * those that can be updated and compute dependencies too. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class SdkUpdaterLogic { - - private final IUpdaterData mUpdaterData; - - public SdkUpdaterLogic(IUpdaterData updaterData) { - mUpdaterData = updaterData; - } - - /** - * Retrieves an unfiltered list of all remote archives. - * The archives are guaranteed to be compatible with the current platform. - */ - public List getAllRemoteArchives( - SdkSources sources, - Package[] localPkgs, - boolean includeAll) { - - List remotePkgs = new ArrayList(); - SdkSource[] remoteSources = sources.getAllSources(); - fetchRemotePackages(remotePkgs, remoteSources); - - ArrayList archives = new ArrayList(); - for (Package remotePkg : remotePkgs) { - // Only look for non-obsolete updates unless requested to include them - if (includeAll || !remotePkg.isObsolete()) { - // Found a suitable update. Only accept the remote package - // if it provides at least one compatible archive - - addArchives: - for (Archive a : remotePkg.getArchives()) { - if (a.isCompatible()) { - - // If we're trying to add a package for revision N, - // make sure we don't also have a package for revision N-1. - for (int i = archives.size() - 1; i >= 0; i--) { - Package pkgFound = archives.get(i).getParentPackage(); - if (pkgFound.canBeUpdatedBy(remotePkg) == UpdateInfo.UPDATE) { - // This package can update one we selected earlier. - // Remove the one that can be updated by this new one. - archives.remove(i); - } else if (remotePkg.canBeUpdatedBy(pkgFound) == UpdateInfo.UPDATE) { - // There is a package in the list that is already better - // than the one we want to add, so don't add it. - break addArchives; - } - } - - archives.add(a); - break; - } - } - } - } - - ArrayList result = new ArrayList(); - - ArchiveInfo[] localArchives = createLocalArchives(localPkgs); - - for (Archive a : archives) { - insertArchive(a, - result, - archives, - remotePkgs, - remoteSources, - localArchives, - false /*automated*/); - } - - return result; - } - - /** - * Compute which packages to install by taking the user selection - * and adding required packages as needed. - * - * When the user doesn't provide a selection, looks at local packages to find - * those that can be updated and compute dependencies too. - */ - public List computeUpdates( - Collection selectedArchives, - SdkSources sources, - Package[] localPkgs, - boolean includeAll) { - - List archives = new ArrayList(); - List remotePkgs = new ArrayList(); - SdkSource[] remoteSources = sources.getAllSources(); - - // Create ArchiveInfos out of local (installed) packages. - ArchiveInfo[] localArchives = createLocalArchives(localPkgs); - - // If we do not have a specific list of archives to install (that is the user - // selected "update all" rather than request specific packages), then we try to - // find updates based on the *existing* packages. - if (selectedArchives == null) { - selectedArchives = findUpdates( - localArchives, - remotePkgs, - remoteSources, - includeAll); - } - - // Once we have a list of packages to install, we try to solve all their - // dependencies by automatically adding them to the list of things to install. - // This works on the list provided either by the user directly or the list - // computed from potential updates. - for (Archive a : selectedArchives) { - insertArchive(a, - archives, - selectedArchives, - remotePkgs, - remoteSources, - localArchives, - false /*automated*/); - } - - // Finally we need to look at *existing* packages which are not being updated - // and check if they have any missing dependencies and suggest how to fix - // these dependencies. - fixMissingLocalDependencies( - archives, - selectedArchives, - remotePkgs, - remoteSources, - localArchives); - - return archives; - } - - private double getRevisionRank(FullRevision rev) { - int p = rev.isPreview() ? 999 : 999 - rev.getPreview(); - return rev.getMajor() + - rev.getMinor() / 1000.d + - rev.getMicro() / 1000000.d + - p / 1000000000.d; - } - - /** - * Finds new packages that the user does not have in his/her local SDK - * and adds them to the list of archives to install. - *

- * The default is to only find "new" platforms, that is anything more - * recent than the highest platform currently installed. - * A side effect is that for an empty SDK install this will list *all* - * platforms available (since there's no "highest" installed platform.) - *

- * This also adds "silent" dependencies. For example the user probably - * needs to have at least one version of the build-tools package although - * there is nothing that directly depends on it. So if the user doesn't have - * any installed or selected version, selected the most recent one as a - * candidate for install. - * - * @param archives The in-out list of archives to install. Typically the - * list is not empty at first as it should contain any archives that is - * already scheduled for install. This method will add to the list. - * @param sources The list of all sources, to fetch them as necessary. - * @param localPkgs The list of all currently installed packages. - * @param includeAll When true, this will list all platforms. - * (included these lower than the highest installed one) as well as - * all obsolete packages of these platforms. - */ - public void addNewPlatforms( - Collection archives, - SdkSources sources, - Package[] localPkgs, - boolean includeAll) { - - // Create ArchiveInfos out of local (installed) packages. - ArchiveInfo[] localArchives = createLocalArchives(localPkgs); - - // Find the highest platform installed - double currentBuildToolScore = 0; - double currentPlatformScore = 0; - double currentSampleScore = 0; - double currentAddonScore = 0; - double currentDocScore = 0; - HashMap currentExtraScore = new HashMap(); - if (!includeAll) { - if (localPkgs != null) { - for (Package p : localPkgs) { - double rev = getRevisionRank(p.getRevision()); - int api = 0; - boolean isPreview = false; - if (p instanceof IAndroidVersionProvider) { - AndroidVersion vers = ((IAndroidVersionProvider) p).getAndroidVersion(); - api = vers.getApiLevel(); - isPreview = vers.isPreview(); - } - - // The score is 1000*api + (999 if preview) + rev - // This allows previews to rank above a non-preview and - // allows revisions to rank appropriately. - double score = api * 1000 + (isPreview ? 999 : 0) + rev; - - if (p instanceof BuildToolPackage) { - currentBuildToolScore = Math.max(currentBuildToolScore, score); - } else if (p instanceof PlatformPackage) { - currentPlatformScore = Math.max(currentPlatformScore, score); - } else if (p instanceof SamplePackage) { - currentSampleScore = Math.max(currentSampleScore, score); - } else if (p instanceof AddonPackage) { - currentAddonScore = Math.max(currentAddonScore, score); - } else if (p instanceof ExtraPackage) { - currentExtraScore.put(((ExtraPackage) p).getPath(), score); - } else if (p instanceof DocPackage) { - currentDocScore = Math.max(currentDocScore, score); - } - } - } - } - - SdkSource[] remoteSources = sources.getAllSources(); - ArrayList remotePkgs = new ArrayList(); - fetchRemotePackages(remotePkgs, remoteSources); - - Package suggestedDoc = null; - Package suggestedBuildTool = null; - - for (Package p : remotePkgs) { - // Skip obsolete packages unless requested to include them. - if (p.isObsolete() && !includeAll) { - continue; - } - - double rev = getRevisionRank(p.getRevision()); - int api = 0; - boolean isPreview = false; - if (p instanceof IAndroidVersionProvider) { - AndroidVersion vers = ((IAndroidVersionProvider) p).getAndroidVersion(); - api = vers.getApiLevel(); - isPreview = vers.isPreview(); - } - - double score = api * 1000 + (isPreview ? 999 : 0) + rev; - - boolean shouldAdd = false; - if (p instanceof BuildToolPackage) { - // We don't want all the build-tool packages, only the most recent one - // if not is currently installed. - if (currentBuildToolScore == 0 && score > currentBuildToolScore) { - suggestedBuildTool = p; - currentBuildToolScore = score; - } - } else if (p instanceof PlatformPackage) { - shouldAdd = score > currentPlatformScore; - } else if (p instanceof SamplePackage) { - shouldAdd = score > currentSampleScore; - } else if (p instanceof AddonPackage) { - shouldAdd = score > currentAddonScore; - } else if (p instanceof ExtraPackage) { - String key = ((ExtraPackage) p).getPath(); - shouldAdd = !currentExtraScore.containsKey(key) || - score > currentExtraScore.get(key).doubleValue(); - } else if (p instanceof DocPackage) { - // We don't want all the doc, only the most recent one - if (score > currentDocScore) { - suggestedDoc = p; - currentDocScore = score; - } - } - - if (shouldAdd) { - // We should suggest this package for installation. - for (Archive a : p.getArchives()) { - if (a.isCompatible()) { - insertArchive(a, - archives, - null /*selectedArchives*/, - remotePkgs, - remoteSources, - localArchives, - true /*automated*/); - } - } - } - - if (p instanceof PlatformPackage && (score >= currentPlatformScore)) { - // We just added a new platform *or* we are visiting the highest currently - // installed platform. In either case we want to make sure it either has - // its own system image or that we provide one by default. - PlatformPackage pp = (PlatformPackage) p; - if (pp.getIncludedAbi() == null) { - for (Package p2 : remotePkgs) { - if (!(p2 instanceof SystemImagePackage) || - ((SystemImagePackage)p2).isPlatform() || - (p2.isObsolete() && !includeAll)) { - continue; - } - SystemImagePackage sip = (SystemImagePackage) p2; - if (sip.getAndroidVersion().equals(pp.getAndroidVersion())) { - for (Archive a : sip.getArchives()) { - if (a.isCompatible()) { - insertArchive(a, - archives, - null /*selectedArchives*/, - remotePkgs, - remoteSources, - localArchives, - true /*automated*/); - } - } - } - } - } - } - } - - if (suggestedDoc != null) { - // We should suggest this package for installation. - for (Archive a : suggestedDoc.getArchives()) { - if (a.isCompatible()) { - insertArchive(a, - archives, - null /*selectedArchives*/, - remotePkgs, - remoteSources, - localArchives, - true /*automated*/); - } - } - } - - if (suggestedBuildTool != null) { - // We should suggest this package for installation. - for (Archive a : suggestedBuildTool.getArchives()) { - if (a.isCompatible()) { - insertArchive(a, - archives, - null /*selectedArchives*/, - remotePkgs, - remoteSources, - localArchives, - true /*automated*/); - } - } - } - } - - /** - * Create a array of {@link ArchiveInfo} based on all local (already installed) - * packages. The array is always non-null but may be empty. - *

- * The local {@link ArchiveInfo} are guaranteed to have one non-null archive - * that you can retrieve using {@link ArchiveInfo#getNewArchive()}. - */ - public ArchiveInfo[] createLocalArchives(Package[] localPkgs) { - - if (localPkgs != null) { - ArrayList list = new ArrayList(); - for (Package p : localPkgs) { - // Only accept packages that have one compatible archive. - // Local package should have 1 and only 1 compatible archive anyway. - for (Archive a : p.getArchives()) { - if (a != null && a.isCompatible()) { - // We create an "installed" archive info to wrap the local package. - // Note that dependencies are not computed since right now we don't - // deal with more than one level of dependencies and installed archives - // are deemed implicitly accepted anyway. - list.add(new LocalArchiveInfo(a)); - } - } - } - - return list.toArray(new ArchiveInfo[list.size()]); - } - - return new ArchiveInfo[0]; - } - - /** - * Find suitable updates to all current local packages. - *

- * Returns a list of potential updates for *existing* packages. This does NOT solve - * dependencies for the new packages. - *

- * Always returns a non-null collection, which can be empty. - */ - private Collection findUpdates( - ArchiveInfo[] localArchives, - Collection remotePkgs, - SdkSource[] remoteSources, - boolean includeAll) { - ArrayList updates = new ArrayList(); - - fetchRemotePackages(remotePkgs, remoteSources); - - for (ArchiveInfo ai : localArchives) { - Archive na = ai.getNewArchive(); - if (na == null) { - continue; - } - Package localPkg = na.getParentPackage(); - - for (Package remotePkg : remotePkgs) { - // Only look for non-obsolete updates unless requested to include them - if ((includeAll || !remotePkg.isObsolete()) && - localPkg.canBeUpdatedBy(remotePkg) == UpdateInfo.UPDATE) { - // Found a suitable update. Only accept the remote package - // if it provides at least one compatible archive - - addArchives: - for (Archive a : remotePkg.getArchives()) { - if (a.isCompatible()) { - - // If we're trying to add a package for revision N, - // make sure we don't also have a package for revision N-1. - for (int i = updates.size() - 1; i >= 0; i--) { - Package pkgFound = updates.get(i).getParentPackage(); - if (pkgFound.canBeUpdatedBy(remotePkg) == UpdateInfo.UPDATE) { - // This package can update one we selected earlier. - // Remove the one that can be updated by this new one. - updates.remove(i); - } else if (remotePkg.canBeUpdatedBy(pkgFound) == - UpdateInfo.UPDATE) { - // There is a package in the list that is already better - // than the one we want to add, so don't add it. - break addArchives; - } - } - - updates.add(a); - break; - } - } - } - } - } - - return updates; - } - - /** - * Check all local archives which are NOT being updated and see if they - * miss any dependency. If they do, try to fix that dependency by selecting - * an appropriate package. - */ - private void fixMissingLocalDependencies( - Collection outArchives, - Collection selectedArchives, - Collection remotePkgs, - SdkSource[] remoteSources, - ArchiveInfo[] localArchives) { - - nextLocalArchive: for (ArchiveInfo ai : localArchives) { - Archive a = ai.getNewArchive(); - Package p = a == null ? null : a.getParentPackage(); - if (p == null) { - continue; - } - - // Is this local archive being updated? - for (ArchiveInfo ai2 : outArchives) { - if (ai2.getReplaced() == a) { - // this new archive will replace the current local one, - // so we don't have to care about fixing dependencies (since the - // new archive should already have had its dependencies resolved) - continue nextLocalArchive; - } - } - - // find dependencies for the local archive and add them as needed - // to the outArchives collection. - ArchiveInfo[] deps = findDependency(p, - outArchives, - selectedArchives, - remotePkgs, - remoteSources, - localArchives); - - if (deps != null) { - // The already installed archive has a missing dependency, which we - // just selected for install. Make sure we remember the dependency - // so that we can enforce it later in the UI. - for (ArchiveInfo aid : deps) { - aid.addDependencyFor(ai); - } - } - } - } - - private ArchiveInfo insertArchive(Archive archive, - Collection outArchives, - Collection selectedArchives, - Collection remotePkgs, - SdkSource[] remoteSources, - ArchiveInfo[] localArchives, - boolean automated) { - Package p = archive.getParentPackage(); - - // Is this an update? - Archive updatedArchive = null; - for (ArchiveInfo ai : localArchives) { - Archive a = ai.getNewArchive(); - if (a != null) { - Package lp = a.getParentPackage(); - - if (lp.canBeUpdatedBy(p) == UpdateInfo.UPDATE) { - updatedArchive = a; - } - } - } - - // Find dependencies and adds them as needed to outArchives - ArchiveInfo[] deps = findDependency(p, - outArchives, - selectedArchives, - remotePkgs, - remoteSources, - localArchives); - - // Make sure it's not a dup - ArchiveInfo ai = null; - - for (ArchiveInfo ai2 : outArchives) { - Archive a2 = ai2.getNewArchive(); - if (a2 != null && a2.getParentPackage().sameItemAs(archive.getParentPackage())) { - ai = ai2; - break; - } - } - - if (ai == null) { - ai = new ArchiveInfo( - archive, //newArchive - updatedArchive, //replaced - deps //dependsOn - ); - outArchives.add(ai); - } - - if (deps != null) { - for (ArchiveInfo d : deps) { - d.addDependencyFor(ai); - } - } - - return ai; - } - - /** - * Resolves dependencies for a given package. - * - * Returns null if no dependencies were found. - * Otherwise return an array of {@link ArchiveInfo}, which is guaranteed to have - * at least size 1 and contain no null elements. - */ - private ArchiveInfo[] findDependency(Package pkg, - Collection outArchives, - Collection selectedArchives, - Collection remotePkgs, - SdkSource[] remoteSources, - ArchiveInfo[] localArchives) { - - // Current dependencies can be: - // - addon: *always* depends on platform of same API level - // - platform: *might* depends on tools of rev >= min-tools-rev - // - extra: *might* depends on platform with api >= min-api-level - - Set aiFound = new HashSet(); - - if (pkg instanceof IPlatformDependency) { - ArchiveInfo ai = findPlatformDependency( - (IPlatformDependency) pkg, - outArchives, - selectedArchives, - remotePkgs, - remoteSources, - localArchives); - - if (ai != null) { - aiFound.add(ai); - } - } - - if (pkg instanceof IMinToolsDependency) { - - ArchiveInfo ai = findToolsDependency( - (IMinToolsDependency) pkg, - outArchives, - selectedArchives, - remotePkgs, - remoteSources, - localArchives); - - if (ai != null) { - aiFound.add(ai); - } - } - - if (pkg instanceof IMinPlatformToolsDependency) { - - ArchiveInfo ai = findPlatformToolsDependency( - (IMinPlatformToolsDependency) pkg, - outArchives, - selectedArchives, - remotePkgs, - remoteSources, - localArchives); - - if (ai != null) { - aiFound.add(ai); - } - } - - if (pkg instanceof IMinApiLevelDependency) { - - ArchiveInfo ai = findMinApiLevelDependency( - (IMinApiLevelDependency) pkg, - outArchives, - selectedArchives, - remotePkgs, - remoteSources, - localArchives); - - if (ai != null) { - aiFound.add(ai); - } - } - - if (pkg instanceof IExactApiLevelDependency) { - - ArchiveInfo ai = findExactApiLevelDependency( - (IExactApiLevelDependency) pkg, - outArchives, - selectedArchives, - remotePkgs, - remoteSources, - localArchives); - - if (ai != null) { - aiFound.add(ai); - } - } - - if (!aiFound.isEmpty()) { - ArchiveInfo[] result = aiFound.toArray(new ArchiveInfo[aiFound.size()]); - Arrays.sort(result); - return result; - } - - return null; - } - - /** - * Resolves dependencies on tools. - * - * A platform or an extra package can both have a min-tools-rev, in which case it - * depends on having a tools package of the requested revision. - * Finds the tools dependency. If found, add it to the list of things to install. - * Returns the archive info dependency, if any. - */ - public ArchiveInfo findToolsDependency( - IMinToolsDependency pkg, - Collection outArchives, - Collection selectedArchives, - Collection remotePkgs, - SdkSource[] remoteSources, - ArchiveInfo[] localArchives) { - // This is the requirement to match. - FullRevision rev = pkg.getMinToolsRevision(); - - if (rev.equals(MinToolsPackage.MIN_TOOLS_REV_NOT_SPECIFIED)) { - // Well actually there's no requirement. - return null; - } - - // First look in locally installed packages. - for (ArchiveInfo ai : localArchives) { - Archive a = ai.getNewArchive(); - if (a != null) { - Package p = a.getParentPackage(); - if (p instanceof ToolPackage) { - if (((ToolPackage) p).getRevision().compareTo(rev) >= 0) { - // We found one already installed. - return null; - } - } - } - } - - // Look in archives already scheduled for install - for (ArchiveInfo ai : outArchives) { - Archive a = ai.getNewArchive(); - if (a != null) { - Package p = a.getParentPackage(); - if (p instanceof ToolPackage) { - if (((ToolPackage) p).getRevision().compareTo(rev) >= 0) { - // The dependency is already scheduled for install, nothing else to do. - return ai; - } - } - } - } - - // Otherwise look in the selected archives. - if (selectedArchives != null) { - for (Archive a : selectedArchives) { - Package p = a.getParentPackage(); - if (p instanceof ToolPackage) { - if (((ToolPackage) p).getRevision().compareTo(rev) >= 0) { - // It's not already in the list of things to install, so add it now - return insertArchive(a, - outArchives, - selectedArchives, - remotePkgs, - remoteSources, - localArchives, - true /*automated*/); - } - } - } - } - - // Finally nothing matched, so let's look at all available remote packages - fetchRemotePackages(remotePkgs, remoteSources); - FullRevision localRev = rev; - Archive localArch = null; - for (Package p : remotePkgs) { - if (p instanceof ToolPackage) { - FullRevision r = ((ToolPackage) p).getRevision(); - if (r.compareTo(localRev) >= 0) { - // It's not already in the list of things to install, so add the - // first compatible archive we can find. - for (Archive a : p.getArchives()) { - if (a.isCompatible()) { - localRev = r; - localArch = a; - break; - } - } - } - } - } - if (localArch != null) { - return insertArchive(localArch, - outArchives, - selectedArchives, - remotePkgs, - remoteSources, - localArchives, - true /*automated*/); - - } - - // We end up here if nothing matches. We don't have a good platform to match. - // We need to indicate this extra depends on a missing platform archive - // so that it can be impossible to install later on. - return new MissingArchiveInfo(MissingArchiveInfo.TITLE_TOOL, rev); - } - - /** - * Resolves dependencies on platform-tools. - * - * A tool package can have a min-platform-tools-rev, in which case it depends on - * having a platform-tool package of the requested revision. - * Finds the platform-tool dependency. If found, add it to the list of things to install. - * Returns the archive info dependency, if any. - */ - public ArchiveInfo findPlatformToolsDependency( - IMinPlatformToolsDependency pkg, - Collection outArchives, - Collection selectedArchives, - Collection remotePkgs, - SdkSource[] remoteSources, - ArchiveInfo[] localArchives) { - // This is the requirement to match. - FullRevision rev = pkg.getMinPlatformToolsRevision(); - boolean findMax = false; - int compareThreshold = 0; - ArchiveInfo aiMax = null; - Archive aMax = null; - - if (rev.equals(IMinPlatformToolsDependency.MIN_PLATFORM_TOOLS_REV_INVALID)) { - // The requirement is invalid, which is not supposed to happen since this - // property is mandatory. However in a typical upgrade scenario we can end - // up with the previous updater managing a new package and not dealing - // correctly with the new unknown property. - // So instead we parse all the existing and remote packages and try to find - // the max available revision and we'll use it. - findMax = true; - // When findMax is false, we want r.compareTo(rev) >= 0. - // When findMax is true, we want r.compareTo(rev) > 0 (so >= 1). - compareThreshold = 1; - } - - // First look in locally installed packages. - for (ArchiveInfo ai : localArchives) { - Archive a = ai.getNewArchive(); - if (a != null) { - Package p = a.getParentPackage(); - if (p instanceof PlatformToolPackage) { - FullRevision r = ((PlatformToolPackage) p).getRevision(); - if (findMax && r.compareTo(rev) > compareThreshold) { - rev = r; - aiMax = ai; - } else if (!findMax && r.compareTo(rev) >= compareThreshold) { - // We found one already installed. - return null; - } - } - } - } - - // Because of previews, we can have more than 1 choice, so get the local max. - FullRevision localRev = rev; - ArchiveInfo localAiMax = null; - // Look in archives already scheduled for install - for (ArchiveInfo ai : outArchives) { - Archive a = ai.getNewArchive(); - if (a != null) { - Package p = a.getParentPackage(); - if (p instanceof PlatformToolPackage) { - FullRevision r = ((PlatformToolPackage) p).getRevision(); - // If computing dependencies for a non-preview package, don't offer preview dependencies - if (r.isPreview() && !rev.isPreview()) { - continue; - } - if (r.compareTo(localRev) >= compareThreshold) { - localRev = r; - localAiMax = ai; - } - } - } - } - if (localAiMax != null) { - if (findMax) { - rev = localRev; - aiMax = localAiMax; - } else { - // The dependency is already scheduled for install, nothing else to do. - return localAiMax; - } - } - - - // Otherwise look in the selected archives. - localRev = rev; - Archive localAMax = null; - if (selectedArchives != null) { - for (Archive a : selectedArchives) { - Package p = a.getParentPackage(); - if (p instanceof PlatformToolPackage) { - FullRevision r = ((PlatformToolPackage) p).getRevision(); - // If computing dependencies for a non-preview package, don't offer preview dependencies - if (r.isPreview() && !rev.isPreview()) { - continue; - } - - if (r.compareTo(localRev) >= compareThreshold) { - localRev = r; - localAiMax = null; - localAMax = a; - } - } - } - if (localAMax != null) { - if (findMax) { - rev = localRev; - aiMax = null; - aMax = localAMax; - } else { - // It's not already in the list of things to install, so add it now - return insertArchive(localAMax, - outArchives, - selectedArchives, - remotePkgs, - remoteSources, - localArchives, - true /*automated*/); - } - } - } - - // Finally nothing matched, so let's look at all available remote packages - fetchRemotePackages(remotePkgs, remoteSources); - localRev = rev; - localAMax = null; - for (Package p : remotePkgs) { - if (p instanceof PlatformToolPackage) { - FullRevision r = ((PlatformToolPackage) p).getRevision(); - // If computing dependencies for a non-preview package, don't offer preview dependencies - if (r.isPreview() && !rev.isPreview()) { - continue; - } - - if (r.compareTo(rev) >= 0) { - // Make sure there's at least one valid archive here - for (Archive a : p.getArchives()) { - if (a.isCompatible()) { - if (r.compareTo(localRev) >= compareThreshold) { - localRev = r; - localAiMax = null; - localAMax = a; - break; - } - } - } - } - } - } - if (localAMax != null) { - if (findMax) { - rev = localRev; - aiMax = null; - aMax = localAMax; - } else { - // It's not already in the list of things to install, so add the - // first compatible archive we can find. - return insertArchive(localAMax, - outArchives, - selectedArchives, - remotePkgs, - remoteSources, - localArchives, - true /*automated*/); - } - } - - if (findMax) { - if (aMax != null) { - return insertArchive(aMax, - outArchives, - selectedArchives, - remotePkgs, - remoteSources, - localArchives, - true /*automated*/); - } else if (aiMax != null) { - return aiMax; - } - } - - // We end up here if nothing matches. We don't have a good platform to match. - // We need to indicate this package depends on a missing platform archive - // so that it can be impossible to install later on. - return new MissingArchiveInfo(MissingArchiveInfo.TITLE_PLATFORM_TOOL, rev); - } - - /** - * Resolves dependencies on platform for an add-on. - * Resolves dependencies on a system-image on its base platform or add-on. - * - * An add-on depends on having a platform with the same API level. - * - * Finds the platform dependency. If found, add it to the list of things to install. - * Returns the archive info dependency, if any. - */ - public ArchiveInfo findPlatformDependency( - IPlatformDependency pkg, - Collection outArchives, - Collection selectedArchives, - Collection remotePkgs, - SdkSource[] remoteSources, - ArchiveInfo[] localArchives) { - // This is the requirement to match. - AndroidVersion v = pkg.getAndroidVersion(); - - - // The dependency package must be a PlatformPackage or an AddonPackage. - // For an add-on, we also need to have the same vendor and name-id. - Class expectedClass = PlatformPackage.class; - IdDisplay addonVendor = null; - IdDisplay addonTag = null; - if (pkg instanceof SystemImagePackage && !((SystemImagePackage) pkg).isPlatform()) { - expectedClass = AddonPackage.class; - addonVendor = ((SystemImagePackage) pkg).getAddonVendor(); - addonTag = ((SystemImagePackage) pkg).getTag(); - } - - // Find a platform or addon that would satisfy the requirement. - - // First look in locally installed packages. - for (ArchiveInfo ai : localArchives) { - Archive a = ai.getNewArchive(); - if (a != null) { - Package p = a.getParentPackage(); - if (expectedClass.isInstance(p)) { - if (v.equals(((IAndroidVersionProvider) p).getAndroidVersion())) { - if (addonVendor != null && addonTag != null && p instanceof AddonPackage) { - if (!((AddonPackage) p).getVendorId().equals(addonVendor.getId()) || - !((AddonPackage) p).getNameId().equals(addonTag.getId())) { - continue; - } - } - // We found one already installed. - return null; - } - } - } - } - - // Look in archives already scheduled for install - for (ArchiveInfo ai : outArchives) { - Archive a = ai.getNewArchive(); - if (a != null) { - Package p = a.getParentPackage(); - if (expectedClass.isInstance(p)) { - if (v.equals(((IAndroidVersionProvider) p).getAndroidVersion())) { - if (addonVendor != null && addonTag != null && p instanceof AddonPackage) { - if (!((AddonPackage) p).getVendorId().equals(addonVendor.getId()) || - !((AddonPackage) p).getNameId().equals(addonTag.getId())) { - continue; - } - } - // The dependency is already scheduled for install, nothing else to do. - return ai; - } - } - } - } - - // Otherwise look in the selected archives. - if (selectedArchives != null) { - for (Archive a : selectedArchives) { - Package p = a.getParentPackage(); - if (expectedClass.isInstance(p)) { - if (v.equals(((IAndroidVersionProvider) p).getAndroidVersion())) { - if (addonVendor != null && addonTag != null && p instanceof AddonPackage) { - if (!((AddonPackage) p).getVendorId().equals(addonVendor.getId()) || - !((AddonPackage) p).getNameId().equals(addonTag.getId())) { - continue; - } - } - // It's not already in the list of things to install, so add it now - return insertArchive(a, - outArchives, - selectedArchives, - remotePkgs, - remoteSources, - localArchives, - true /*automated*/); - } - } - } - } - - // Finally nothing matched, so let's look at all available remote packages - fetchRemotePackages(remotePkgs, remoteSources); - for (Package p : remotePkgs) { - if (expectedClass.isInstance(p)) { - if (v.equals(((IAndroidVersionProvider) p).getAndroidVersion())) { - if (addonVendor != null && addonTag != null && p instanceof AddonPackage) { - if (!((AddonPackage) p).getVendorId().equals(addonVendor.getId()) || - !((AddonPackage) p).getNameId().equals(addonTag.getId())) { - continue; - } - } - // It's not already in the list of things to install, so add the - // first compatible archive we can find. - for (Archive a : p.getArchives()) { - if (a.isCompatible()) { - return insertArchive(a, - outArchives, - selectedArchives, - remotePkgs, - remoteSources, - localArchives, - true /*automated*/); - } - } - } - } - } - - // We end up here if nothing matches. We don't have a good platform to match. - // We need to indicate this addon depends on a missing platform archive - // so that it can be impossible to install later on. - return new MissingPlatformArchiveInfo(pkg.getAndroidVersion()); - } - - /** - * Resolves platform dependencies for extras. - * An extra depends on having a platform with a minimun API level. - * - * We try to return the highest API level available above the specified minimum. - * Note that installed packages have priority so if one installed platform satisfies - * the dependency, we'll use it even if there's a higher API platform available but - * not installed yet. - * - * Finds the platform dependency. If found, add it to the list of things to install. - * Returns the archive info dependency, if any. - */ - protected ArchiveInfo findMinApiLevelDependency( - IMinApiLevelDependency pkg, - Collection outArchives, - Collection selectedArchives, - Collection remotePkgs, - SdkSource[] remoteSources, - ArchiveInfo[] localArchives) { - - int api = pkg.getMinApiLevel(); - - if (api == IMinApiLevelDependency.MIN_API_LEVEL_NOT_SPECIFIED) { - return null; - } - - // Find a platform that would satisfy the requirement. - - // First look in locally installed packages. - for (ArchiveInfo ai : localArchives) { - Archive a = ai.getNewArchive(); - if (a != null) { - Package p = a.getParentPackage(); - if (p instanceof PlatformPackage) { - if (((PlatformPackage) p).getAndroidVersion().isGreaterOrEqualThan(api)) { - // We found one already installed. - return null; - } - } - } - } - - // Look in archives already scheduled for install - int foundApi = 0; - ArchiveInfo foundAi = null; - - for (ArchiveInfo ai : outArchives) { - Archive a = ai.getNewArchive(); - if (a != null) { - Package p = a.getParentPackage(); - if (p instanceof PlatformPackage) { - if (((PlatformPackage) p).getAndroidVersion().isGreaterOrEqualThan(api)) { - if (api > foundApi) { - foundApi = api; - foundAi = ai; - } - } - } - } - } - - if (foundAi != null) { - // The dependency is already scheduled for install, nothing else to do. - return foundAi; - } - - // Otherwise look in the selected archives *or* available remote packages - // and takes the best out of the two sets. - foundApi = 0; - Archive foundArchive = null; - if (selectedArchives != null) { - for (Archive a : selectedArchives) { - Package p = a.getParentPackage(); - if (p instanceof PlatformPackage) { - if (((PlatformPackage) p).getAndroidVersion().isGreaterOrEqualThan(api)) { - if (api > foundApi) { - foundApi = api; - foundArchive = a; - } - } - } - } - } - - // Finally nothing matched, so let's look at all available remote packages - fetchRemotePackages(remotePkgs, remoteSources); - for (Package p : remotePkgs) { - if (p instanceof PlatformPackage) { - if (((PlatformPackage) p).getAndroidVersion().isGreaterOrEqualThan(api)) { - if (api > foundApi) { - // It's not already in the list of things to install, so add the - // first compatible archive we can find. - for (Archive a : p.getArchives()) { - if (a.isCompatible()) { - foundApi = api; - foundArchive = a; - } - } - } - } - } - } - - if (foundArchive != null) { - // It's not already in the list of things to install, so add it now - return insertArchive(foundArchive, - outArchives, - selectedArchives, - remotePkgs, - remoteSources, - localArchives, - true /*automated*/); - } - - // We end up here if nothing matches. We don't have a good platform to match. - // We need to indicate this extra depends on a missing platform archive - // so that it can be impossible to install later on. - return new MissingPlatformArchiveInfo(new AndroidVersion(api, null /*codename*/)); - } - - /** - * Resolves platform dependencies for add-ons. - * An add-ons depends on having a platform with an exact specific API level. - * - * Finds the platform dependency. If found, add it to the list of things to install. - * Returns the archive info dependency, if any. - */ - public ArchiveInfo findExactApiLevelDependency( - IExactApiLevelDependency pkg, - Collection outArchives, - Collection selectedArchives, - Collection remotePkgs, - SdkSource[] remoteSources, - ArchiveInfo[] localArchives) { - - int api = pkg.getExactApiLevel(); - - if (api == IExactApiLevelDependency.API_LEVEL_INVALID) { - return null; - } - - // Find a platform that would satisfy the requirement. - - // First look in locally installed packages. - for (ArchiveInfo ai : localArchives) { - Archive a = ai.getNewArchive(); - if (a != null) { - Package p = a.getParentPackage(); - if (p instanceof PlatformPackage) { - if (((PlatformPackage) p).getAndroidVersion().equals(api)) { - // We found one already installed. - return null; - } - } - } - } - - // Look in archives already scheduled for install - - for (ArchiveInfo ai : outArchives) { - Archive a = ai.getNewArchive(); - if (a != null) { - Package p = a.getParentPackage(); - if (p instanceof PlatformPackage) { - if (((PlatformPackage) p).getAndroidVersion().equals(api)) { - return ai; - } - } - } - } - - // Otherwise look in the selected archives. - if (selectedArchives != null) { - for (Archive a : selectedArchives) { - Package p = a.getParentPackage(); - if (p instanceof PlatformPackage) { - if (((PlatformPackage) p).getAndroidVersion().equals(api)) { - // It's not already in the list of things to install, so add it now - return insertArchive(a, - outArchives, - selectedArchives, - remotePkgs, - remoteSources, - localArchives, - true /*automated*/); - } - } - } - } - - // Finally nothing matched, so let's look at all available remote packages - fetchRemotePackages(remotePkgs, remoteSources); - for (Package p : remotePkgs) { - if (p instanceof PlatformPackage) { - if (((PlatformPackage) p).getAndroidVersion().equals(api)) { - // It's not already in the list of things to install, so add the - // first compatible archive we can find. - for (Archive a : p.getArchives()) { - if (a.isCompatible()) { - return insertArchive(a, - outArchives, - selectedArchives, - remotePkgs, - remoteSources, - localArchives, - true /*automated*/); - } - } - } - } - } - - // We end up here if nothing matches. We don't have a good platform to match. - // We need to indicate this extra depends on a missing platform archive - // so that it can be impossible to install later on. - return new MissingPlatformArchiveInfo(new AndroidVersion(api, null /*codename*/)); - } - - /** - * Fetch all remote packages only if really needed. - *

- * This method takes a list of sources. Each source is only fetched once -- that is each - * source keeps the list of packages that we fetched from the remote XML file. If the list - * is null, it means this source has never been fetched so we'll do it once here. Otherwise - * we rely on the cached list of packages from this source. - *

- * This method also takes a remote package list as input, which it will fill out. - * If a source has already been fetched, we'll add its packages to the remote package list - * if they are not already present. Otherwise, the source will be fetched and the packages - * added to the list. - * - * @param remotePkgs An in-out list of packages available from remote sources. - * This list must not be null. - * It can be empty or already contain some packages. - * @param remoteSources A list of available remote sources to fetch from. - */ - protected void fetchRemotePackages( - final Collection remotePkgs, - final SdkSource[] remoteSources) { - if (!remotePkgs.isEmpty()) { - return; - } - - // First check if there's any remote source we need to fetch. - // This will bring the task window, so we rather not display it unless - // necessary. - boolean needsFetch = false; - for (final SdkSource remoteSrc : remoteSources) { - Package[] pkgs = remoteSrc.getPackages(); - if (pkgs == null) { - // This source has never been fetched. We'll do it below. - needsFetch = true; - } else { - // This source has already been fetched and we know its package list. - // We still need to make sure all of its packages are present in the - // remotePkgs list. - - nextPackage: for (Package pkg : pkgs) { - for (Archive a : pkg.getArchives()) { - // Only add a package if it contains at least one compatible archive - // and is not already in the remote package list. - if (a.isCompatible()) { - if (!remotePkgs.contains(pkg)) { - remotePkgs.add(pkg); - continue nextPackage; - } - } - } - } - } - } - - if (!needsFetch) { - return; - } - - final boolean forceHttp = mUpdaterData.getSettingsController().getSettings().getForceHttp(); - - mUpdaterData.getTaskFactory().start("Refresh Sources", new ITask() { - @Override - public void run(ITaskMonitor monitor) { - for (SdkSource remoteSrc : remoteSources) { - Package[] pkgs = remoteSrc.getPackages(); - - if (pkgs == null) { - remoteSrc.load(mUpdaterData.getDownloadCache(), monitor, forceHttp); - pkgs = remoteSrc.getPackages(); - } - - if (pkgs != null) { - nextPackage: for (Package pkg : pkgs) { - for (Archive a : pkg.getArchives()) { - // Only add a package if it contains at least one compatible archive - // and is not already in the remote package list. - if (a.isCompatible()) { - if (!remotePkgs.contains(pkg)) { - remotePkgs.add(pkg); - continue nextPackage; - } - } - } - } - } - } - } - }); - } - - - /** - * A {@link LocalArchiveInfo} is an {@link ArchiveInfo} that wraps an already installed - * "local" package/archive. - *

- * In this case, the "new Archive" is still expected to be non null and the - * "replaced Archive" is null. Installed archives are always accepted and never - * rejected. - *

- * Dependencies are not set. - */ - private static class LocalArchiveInfo extends ArchiveInfo { - - public LocalArchiveInfo(Archive localArchive) { - super(localArchive, null /*replaced*/, null /*dependsOn*/); - } - - /** Installed archives are always accepted. */ - @Override - public boolean isAccepted() { - return true; - } - - /** Installed archives are never rejected. */ - @Override - public boolean isRejected() { - return false; - } - } - - /** - * A {@link MissingPlatformArchiveInfo} is an {@link ArchiveInfo} that represents a - * package/archive that we really need as a dependency but that we don't have. - *

- * This is currently used for addons and extras in case we can't find a matching base platform. - *

- * This kind of archive has specific properties: the new archive to install is null, - * there are no dependencies and no archive is being replaced. The info can never be - * accepted and is always rejected. - */ - private static class MissingPlatformArchiveInfo extends ArchiveInfo { - - private final AndroidVersion mVersion; - - /** - * Constructs a {@link MissingPlatformArchiveInfo} that will indicate the - * given platform version is missing. - */ - public MissingPlatformArchiveInfo(AndroidVersion version) { - super(null /*newArchive*/, null /*replaced*/, null /*dependsOn*/); - mVersion = version; - } - - /** Missing archives are never accepted. */ - @Override - public boolean isAccepted() { - return false; - } - - /** Missing archives are always rejected. */ - @Override - public boolean isRejected() { - return true; - } - - @Override - public String getShortDescription() { - return String.format("Missing SDK Platform Android%1$s, API %2$d", - mVersion.isPreview() ? " Preview" : "", - mVersion.getApiLevel()); - } - } - - /** - * A {@link MissingArchiveInfo} is an {@link ArchiveInfo} that represents a - * package/archive that we really need as a dependency but that we don't have. - *

- * This is currently used for extras in case we can't find a matching tool revision - * or when a platform-tool is missing. - *

- * This kind of archive has specific properties: the new archive to install is null, - * there are no dependencies and no archive is being replaced. The info can never be - * accepted and is always rejected. - */ - private static class MissingArchiveInfo extends ArchiveInfo { - - private final FullRevision mRevision; - private final String mTitle; - - public static final String TITLE_TOOL = "Tools"; - public static final String TITLE_PLATFORM_TOOL = "Platform-tools"; - - /** - * Constructs a {@link MissingPlatformArchiveInfo} that will indicate the - * given platform version is missing. - * - * @param title Typically "Tools" or "Platform-tools". - * @param revision The required revision. - */ - public MissingArchiveInfo(String title, FullRevision revision) { - super(null /*newArchive*/, null /*replaced*/, null /*dependsOn*/); - mTitle = title; - mRevision = revision; - } - - /** Missing archives are never accepted. */ - @Override - public boolean isAccepted() { - return false; - } - - /** Missing archives are always rejected. */ - @Override - public boolean isRejected() { - return true; - } - - @Override - public String getShortDescription() { - return String.format("Missing Android SDK %1$s, revision %2$s", - mTitle, - mRevision.toShortString()); - } - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/SdkUpdaterNoWindow.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/SdkUpdaterNoWindow.java deleted file mode 100644 index 8183e1bc..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/SdkUpdaterNoWindow.java +++ /dev/null @@ -1,715 +0,0 @@ -/* - * Copyright (C) 2010 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.sdklib.internal.repository.updater; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.sdklib.IAndroidTarget; -import com.android.sdklib.SdkManager; -import com.android.sdklib.internal.repository.ITask; -import com.android.sdklib.internal.repository.ITaskFactory; -import com.android.sdklib.internal.repository.ITaskMonitor; -import com.android.sdklib.internal.repository.NullTaskMonitor; -import com.android.sdklib.internal.repository.UserCredentials; -import com.android.sdklib.internal.repository.archives.Archive; -import com.android.sdklib.repository.SdkRepoConstants; -import com.android.utils.ILogger; -import com.android.utils.IReaderLogger; -import com.android.utils.NullLogger; -import com.android.utils.Pair; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -/** - * Performs an update using only a non-interactive console output with no GUI. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class SdkUpdaterNoWindow { - - /** The {@link UpdaterData} to use. */ - private final UpdaterData mUpdaterData; - /** The {@link ILogger} logger to use. */ - private final ILogger mSdkLog; - /** The reply to any question asked by the update process. Currently this will - * be yes/no for ability to replace modified samples or restart ADB. */ - private final boolean mForce; - - /** - * Creates an UpdateNoWindow object that will update using the given SDK root - * and outputs to the given SDK logger. - * - * @param osSdkRoot The OS path of the SDK folder to update. - * @param sdkManager An existing SDK manager to list current platforms and addons. - * @param sdkLog A logger object, that should ideally output to a write-only console. - * @param force The reply to any question asked by the update process. Currently this will - * be yes/no for ability to replace modified samples or restart ADB. - * @param useHttp True to force using HTTP instead of HTTPS for downloads. - * @param proxyPort An optional HTTP/HTTPS proxy port. Can be null. - * @param proxyHost An optional HTTP/HTTPS proxy host. Can be null. - */ - public SdkUpdaterNoWindow(String osSdkRoot, - SdkManager sdkManager, - ILogger sdkLog, - boolean force, - boolean useHttp, - String proxyHost, - String proxyPort) { - mSdkLog = sdkLog; - mForce = force; - mUpdaterData = new UpdaterData(osSdkRoot, sdkLog); - - // Read and apply settings from settings file, so that http/https proxy is set - // and let the command line args override them as necessary. - SettingsController settingsController = mUpdaterData.getSettingsController(); - settingsController.loadSettings(); - settingsController.applySettings(); - setupProxy(proxyHost, proxyPort); - - // Change the in-memory settings to force the http/https mode - settingsController.setSetting(ISettingsPage.KEY_FORCE_HTTP, useHttp); - - // Use a factory that only outputs to the given ILogger. - mUpdaterData.setTaskFactory(new ConsoleTaskFactory()); - - // Check that the AVD Manager has been correctly initialized. This is done separately - // from the constructor in the GUI-based UpdaterWindowImpl to give time to the UI to - // initialize before displaying a message box. Since we don't have any GUI here - // we can call it whenever we want. - if (mUpdaterData.checkIfInitFailed()) { - return; - } - - // Setup the default sources including the getenv overrides. - mUpdaterData.setupDefaultSources(); - - mUpdaterData.getLocalSdkParser().parseSdk( - osSdkRoot, - sdkManager, - new NullTaskMonitor(sdkLog)); - } - - /** - * Performs the actual update. - * - * @param pkgFilter A list of {@link SdkRepoConstants#NODES} to limit the type of packages - * we can update. A null or empty list means to update everything possible. - * @param includeAll True to list and install all packages, including obsolete ones. - * @param dryMode True to check what would be updated/installed but do not actually - * download or install anything. - * @param acceptLicense SDK licenses to automatically accept. - * @deprecated Use {@link #updateAll(java.util.ArrayList, boolean, boolean, String, boolean)} - * instead - */ - @Deprecated - public void updateAll( - ArrayList pkgFilter, - boolean includeAll, - boolean dryMode, - String acceptLicense) { - updateAll(pkgFilter, includeAll, dryMode, acceptLicense, false); - } - - /** - * Performs the actual update. - * - * @param pkgFilter A list of {@link SdkRepoConstants#NODES} to limit the type of packages - * we can update. A null or empty list means to update everything possible. - * @param includeAll True to list and install all packages, including obsolete ones. - * @param dryMode True to check what would be updated/installed but do not actually - * download or install anything. - * @param acceptLicense SDK licenses to automatically accept. - * @param includeDependencies If true, also include any required dependencies - */ - public void updateAll( - ArrayList pkgFilter, - boolean includeAll, - boolean dryMode, - String acceptLicense, - boolean includeDependencies) { - mUpdaterData.updateOrInstallAll_NoGUI(pkgFilter, includeAll, dryMode, acceptLicense, - includeDependencies); - } - - /** - * Lists remote packages available for install using 'android update sdk --no-ui'. - * - * @param includeAll True to list and install all packages, including obsolete ones. - * @param extendedOutput True to display more details on each package. - */ - public void listRemotePackages(boolean includeAll, boolean extendedOutput) { - mUpdaterData.listRemotePackages_NoGUI(includeAll, extendedOutput); - } - - /** - * Installs a platform package given its target hash string. - *

- * This does not work for add-ons right now, just basic platforms. - * - * @param hashString The hash string of the platform to install. - * @return A boolean indicating whether the installation was successful (meaning the package - * was either already present, or got installed or updated properly) and a {@link File} - * with the path to the root folder of the package. The file is null when the boolean - * is false, otherwise it should point to an existing valid folder. - */ - public Pair installPlatformPackage(String hashString) { - - // TODO right now we really need the caller to use a reader-logger to - // handle license confirmations. This isn't optimal and should be addressed - // when we provide a proper UI for this. - assert mSdkLog instanceof IReaderLogger; - - SdkManager sm = mUpdaterData.getSdkManager(); - IAndroidTarget target = sm.getTargetFromHashString(hashString); - - if (target == null) { - // Otherwise try to install it. - // This currently only works for platforms since the package's "install id" - // is an exactly match with the IAndroidTarget hash string. - ArrayList filter = new ArrayList(); - filter.add(hashString); - List installed = mUpdaterData.updateOrInstallAll_NoGUI( - filter, - true, //includeAll - false, //dryMode - null); //acceptLicense - - if (installed != null) { - sm.reloadSdk(new NullLogger()); - target = sm.getTargetFromHashString(hashString); - } - } - - if (target != null) { - // Return existing target - return Pair.of(Boolean.TRUE, new File(target.getLocation())); - } - - return null; - } - - // ----- - - /** - * Sets both the HTTP and HTTPS proxy system properties, overriding the ones - * from the settings with these values if they are defined. - */ - private void setupProxy(String proxyHost, String proxyPort) { - - // The system property constants can be found in the Java SE documentation at - // http://download.oracle.com/javase/6/docs/technotes/guides/net/proxies.html - final String JAVA_PROP_HTTP_PROXY_HOST = "http.proxyHost"; //$NON-NLS-1$ - final String JAVA_PROP_HTTP_PROXY_PORT = "http.proxyPort"; //$NON-NLS-1$ - final String JAVA_PROP_HTTPS_PROXY_HOST = "https.proxyHost"; //$NON-NLS-1$ - final String JAVA_PROP_HTTPS_PROXY_PORT = "https.proxyPort"; //$NON-NLS-1$ - - Properties props = System.getProperties(); - - if (proxyHost != null && !proxyHost.isEmpty()) { - props.setProperty(JAVA_PROP_HTTP_PROXY_HOST, proxyHost); - props.setProperty(JAVA_PROP_HTTPS_PROXY_HOST, proxyHost); - } - if (proxyPort != null && !proxyPort.isEmpty()) { - props.setProperty(JAVA_PROP_HTTP_PROXY_PORT, proxyPort); - props.setProperty(JAVA_PROP_HTTPS_PROXY_PORT, proxyPort); - } - } - - /** - * A custom implementation of {@link ITaskFactory} that - * provides {@link ConsoleTaskMonitor} objects. - */ - private class ConsoleTaskFactory implements ITaskFactory { - @Override - public void start(String title, ITask task) { - start(title, null /*parentMonitor*/, task); - } - - @Override - public void start(String title, ITaskMonitor parentMonitor, ITask task) { - if (parentMonitor == null) { - task.run(new ConsoleTaskMonitor(title, task)); - } else { - // Use all the reminder of the parent monitor. - if (parentMonitor.getProgressMax() == 0) { - parentMonitor.setProgressMax(1); - } - - ITaskMonitor sub = parentMonitor.createSubMonitor( - parentMonitor.getProgressMax() - parentMonitor.getProgress()); - try { - task.run(sub); - } finally { - int delta = - sub.getProgressMax() - sub.getProgress(); - if (delta > 0) { - sub.incProgress(delta); - } - } - } - } - } - - /** - * A custom implementation of {@link ITaskMonitor} that defers all output to the - * super {@link SdkUpdaterNoWindow#mSdkLog}. - */ - private class ConsoleTaskMonitor implements ITaskMonitor { - - private static final double MAX_COUNT = 10000.0; - private double mIncCoef = 0; - private double mValue = 0; - private String mLastDesc = null; - private String mLastProgressBase = null; - - /** - * Creates a new {@link ConsoleTaskMonitor} with the given title. - */ - public ConsoleTaskMonitor(String title, ITask task) { - mSdkLog.info("%s:\n", title); - } - - /** - * Sets the description in the current task dialog. - */ - @Override - public void setDescription(String format, Object...args) { - - String last = mLastDesc; - String line = String.format(" " + format, args); //$NON-NLS-1$ - - // If the description contains a %, it generally indicates a recurring - // progress so we want a \r at the end. - int pos = line.indexOf('%'); - if (pos > -1) { - String base = line.trim(); - if (mLastProgressBase != null && base.startsWith(mLastProgressBase)) { - line = " " + base.substring(mLastProgressBase.length()); //$NON-NLS-1$ - } - line += '\r'; - } else { - mLastProgressBase = line.trim(); - line += '\n'; - } - - // Skip line if it's the same as the last one. - if (last != null && last.equals(line.trim())) { - return; - } - mLastDesc = line.trim(); - - // If the last line terminated with a \r but the new one doesn't, we need to - // insert a \n to avoid erasing the previous line. - if (last != null && - last.endsWith("\r") && //$NON-NLS-1$ - !line.endsWith("\r")) { //$NON-NLS-1$ - line = '\n' + line; - } - - mSdkLog.info("%s", line); //$NON-NLS-1$ - } - - @Override - public void log(String format, Object...args) { - setDescription(" " + format, args); //$NON-NLS-1$ - } - - @Override - public void logError(String format, Object...args) { - setDescription(format, args); - } - - @Override - public void logVerbose(String format, Object...args) { - // The ConsoleTask does not display verbose log messages. - } - - // --- ILogger --- - - @Override - public void error(@Nullable Throwable t, @Nullable String errorFormat, Object... args) { - mSdkLog.error(t, errorFormat, args); - } - - @Override - public void warning(@NonNull String warningFormat, Object... args) { - mSdkLog.warning(warningFormat, args); - } - - @Override - public void info(@NonNull String msgFormat, Object... args) { - mSdkLog.info(msgFormat, args); - } - - @Override - public void verbose(@NonNull String msgFormat, Object... args) { - mSdkLog.verbose(msgFormat, args); - } - - /** - * Sets the max value of the progress bar. - * - * Weird things will happen if setProgressMax is called multiple times - * *after* {@link #incProgress(int)}: we don't try to adjust it on the - * fly. - */ - @Override - public void setProgressMax(int max) { - assert max > 0; - // Always set the dialog's progress max to 10k since it only handles - // integers and we want to have a better inner granularity. Instead - // we use the max to compute a coefficient for inc deltas. - mIncCoef = max > 0 ? MAX_COUNT / max : 0; - assert mIncCoef > 0; - } - - @Override - public int getProgressMax() { - return mIncCoef > 0 ? (int) (MAX_COUNT / mIncCoef) : 0; - } - - /** - * Increments the current value of the progress bar. - */ - @Override - public void incProgress(int delta) { - if (delta > 0 && mIncCoef > 0) { - internalIncProgress(delta * mIncCoef); - } - } - - private void internalIncProgress(double realDelta) { - mValue += realDelta; - // max value is 10k, so 10k/100 == 100%. - // Experimentation shows that it is not really useful to display this - // progression since during download the description line will change. - // mSdkLog.printf(" [%3d%%]\r", ((int)mValue) / 100); - } - - /** - * Returns the current value of the progress bar, - * between 0 and up to {@link #setProgressMax(int)} - 1. - */ - @Override - public int getProgress() { - assert mIncCoef > 0; - return mIncCoef > 0 ? (int)(mValue / mIncCoef) : 0; - } - - /** - * Returns true if the "Cancel" button was selected. - */ - @Override - public boolean isCancelRequested() { - return false; - } - - /** - * Display a yes/no question dialog box. - * - * This implementation allow this to be called from any thread, it - * makes sure the dialog is opened synchronously in the ui thread. - * - * @param title The title of the dialog box - * @param message The error message - * @return true if YES was clicked. - */ - @Override - public boolean displayPrompt(final String title, final String message) { - // TODO Make it interactive if mForce==false - mSdkLog.info("\n%1$s\n%2$s\n%3$s", //$NON-NLS-1$ - title, - message, - mForce ? "--force used, will reply yes\n" : - "Note: you can use --force to override to yes.\n"); - if (mForce) { - return true; - } - - while (true) { - mSdkLog.info("%1$s", "[y/n] =>"); //$NON-NLS-1$ - try { - byte[] readBuffer = new byte[2048]; - String reply = readLine(readBuffer).trim(); - mSdkLog.info("\n"); //$NON-NLS-1$ - if (!reply.isEmpty() && reply.length() <= 3) { - char c = reply.charAt(0); - if (c == 'y' || c == 'Y') { - return true; - } else if (c == 'n' || c == 'N') { - return false; - } - } - mSdkLog.info("Unknown reply '%s'. Please use y[es]/n[o].\n"); //$NON-NLS-1$ - - } catch (IOException e) { - // Exception. Be conservative and say no. - mSdkLog.info("\n"); //$NON-NLS-1$ - return false; - } - } - } - - /** - * Displays a prompt message to the user and read two values, - * login/password. - *

- * Asks user for login/password information. - *

- * This method shows a question in the standard output, asking for login - * and password.
- * Method Output:
- * Title
- * Message
- * Login: (Wait for user input)
- * Password: (Wait for user input)
- *

- * - * @param title The title of the iteration. - * @param message The message to be displayed. - * @return A {@link Pair} holding the entered login and password. The - * first element is always the Login, and the - * second element is always the Password. This - * method will never return null, in case of error the pair will - * be filled with empty strings. - * @see ITaskMonitor#displayLoginCredentialsPrompt(String, String) - */ - @Override - public UserCredentials displayLoginCredentialsPrompt(String title, String message) { - String login = ""; //$NON-NLS-1$ - String password = ""; //$NON-NLS-1$ - String workstation = ""; //$NON-NLS-1$ - String domain = ""; //$NON-NLS-1$ - - mSdkLog.info("\n%1$s\n%2$s", title, message); - byte[] readBuffer = new byte[2048]; - try { - mSdkLog.info("\nLogin: "); - login = readLine(readBuffer); - mSdkLog.info("\nPassword: "); - password = readLine(readBuffer); - mSdkLog.info("\nIf your proxy uses NTLM authentication, provide the following information. Leave blank otherwise."); - mSdkLog.info("\nWorkstation: "); - workstation = readLine(readBuffer); - mSdkLog.info("\nDomain: "); - domain = readLine(readBuffer); - - /* - * TODO: Implement a way to don't echo the typed password On - * Java 5 there's no simple way to do this. There's just a - * workaround which is output backspaces on each keystroke. - * A good alternative is to use Java 6 java.io.Console - */ - } catch (IOException e) { - // Reset login/pass to empty Strings. - login = ""; //$NON-NLS-1$ - password = ""; //$NON-NLS-1$ - workstation = ""; //$NON-NLS-1$ - domain = ""; //$NON-NLS-1$ - //Just print the error to console. - mSdkLog.info("\nError occurred during login/pass query: %s\n", e.getMessage()); - } - - return new UserCredentials(login, password, workstation, domain); - } - - /** - * Reads current console input in the given buffer. - * - * @param buffer Buffer to hold the user input. Must be larger than the largest - * expected input. Cannot be null. - * @return A new string. May be empty but not null. - * @throws IOException in case the buffer isn't long enough. - */ - private String readLine(byte[] buffer) throws IOException { - - int count; - if (mSdkLog instanceof IReaderLogger) { - count = ((IReaderLogger) mSdkLog).readLine(buffer); - } else { - count = System.in.read(buffer); - } - - // is the input longer than the buffer? - if (count == buffer.length && buffer[count-1] != 10) { - throw new IOException(String.format( - "Input is longer than the buffer size, (%1$s) bytes", buffer.length)); - } - - // ignore end whitespace - while (count > 0 && (buffer[count-1] == '\r' || buffer[count-1] == '\n')) { - count--; - } - - return new String(buffer, 0, count); - } - - /** - * Creates a sub-monitor that will use up to tickCount on the progress bar. - * tickCount must be 1 or more. - */ - @Override - public ITaskMonitor createSubMonitor(int tickCount) { - assert mIncCoef > 0; - assert tickCount > 0; - return new ConsoleSubTaskMonitor(this, null, mValue, tickCount * mIncCoef); - } - } - - private interface IConsoleSubTaskMonitor extends ITaskMonitor { - void subIncProgress(double realDelta); - } - - private static class ConsoleSubTaskMonitor implements IConsoleSubTaskMonitor { - - private final ConsoleTaskMonitor mRoot; - private final IConsoleSubTaskMonitor mParent; - private final double mStart; - private final double mSpan; - private double mSubValue; - private double mSubCoef; - - /** - * Creates a new sub task monitor which will work for the given range [start, start+span] - * in its parent. - * - * @param root The ProgressTask root - * @param parent The immediate parent. Can be the null or another sub task monitor. - * @param start The start value in the root's coordinates - * @param span The span value in the root's coordinates - */ - public ConsoleSubTaskMonitor(ConsoleTaskMonitor root, - IConsoleSubTaskMonitor parent, - double start, - double span) { - mRoot = root; - mParent = parent; - mStart = start; - mSpan = span; - mSubValue = start; - } - - @Override - public boolean isCancelRequested() { - return mRoot.isCancelRequested(); - } - - @Override - public void setDescription(String format, Object... args) { - mRoot.setDescription(format, args); - } - - @Override - public void log(String format, Object... args) { - mRoot.log(format, args); - } - - @Override - public void logError(String format, Object... args) { - mRoot.logError(format, args); - } - - @Override - public void logVerbose(String format, Object... args) { - mRoot.logVerbose(format, args); - } - - @Override - public void setProgressMax(int max) { - assert max > 0; - mSubCoef = max > 0 ? mSpan / max : 0; - assert mSubCoef > 0; - } - - @Override - public int getProgressMax() { - return mSubCoef > 0 ? (int) (mSpan / mSubCoef) : 0; - } - - @Override - public int getProgress() { - assert mSubCoef > 0; - return mSubCoef > 0 ? (int)((mSubValue - mStart) / mSubCoef) : 0; - } - - @Override - public void incProgress(int delta) { - if (delta > 0 && mSubCoef > 0) { - subIncProgress(delta * mSubCoef); - } - } - - @Override - public void subIncProgress(double realDelta) { - mSubValue += realDelta; - if (mParent != null) { - mParent.subIncProgress(realDelta); - } else { - mRoot.internalIncProgress(realDelta); - } - } - - @Override - public boolean displayPrompt(String title, String message) { - return mRoot.displayPrompt(title, message); - } - - @Override - public UserCredentials displayLoginCredentialsPrompt(String title, String message) { - return mRoot.displayLoginCredentialsPrompt(title, message); - } - - @Override - public ITaskMonitor createSubMonitor(int tickCount) { - assert mSubCoef > 0; - assert tickCount > 0; - return new ConsoleSubTaskMonitor(mRoot, - this, - mSubValue, - tickCount * mSubCoef); - } - - // --- ILogger --- - - @Override - public void error(@Nullable Throwable t, @Nullable String errorFormat, Object... args) { - mRoot.error(t, errorFormat, args); - } - - @Override - public void warning(@NonNull String warningFormat, Object... args) { - mRoot.warning(warningFormat, args); - } - - @Override - public void info(@NonNull String msgFormat, Object... args) { - mRoot.info(msgFormat, args); - } - - @Override - public void verbose(@NonNull String msgFormat, Object... args) { - mRoot.verbose(msgFormat, args); - } - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/SettingsController.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/SettingsController.java deleted file mode 100644 index f0204a0a..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/SettingsController.java +++ /dev/null @@ -1,392 +0,0 @@ -/* - * 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.sdklib.internal.repository.updater; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.annotations.VisibleForTesting; -import com.android.annotations.VisibleForTesting.Visibility; -import com.android.prefs.AndroidLocation; -import com.android.prefs.AndroidLocation.AndroidLocationException; -import com.android.sdklib.io.LegacyFileOp; -import com.android.sdklib.io.IFileOp; -import com.android.utils.ILogger; - -import java.io.File; -import java.io.FileNotFoundException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map.Entry; -import java.util.Properties; - -/** - * Controller class to get settings values. Settings are kept in-memory. - * Users of this class must first load the settings before changing them and save - * them when modified. - *

- * Settings are enumerated by constants in {@link ISettingsPage}. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class SettingsController { - - private static final String SETTINGS_FILENAME = "androidtool.cfg"; //$NON-NLS-1$ - - private final IFileOp mFileOp; - private final ILogger mSdkLog; - private final Settings mSettings; - - public interface OnChangedListener { - void onSettingsChanged(@NonNull SettingsController controller, - @NonNull Settings oldSettings); - } - private final List mChangedListeners = new ArrayList(1); - - /** The currently associated {@link ISettingsPage}. Can be null. */ - private ISettingsPage mSettingsPage; - - - /** - * Constructs a new default {@link SettingsController}. - * - * @param sdkLog A non-null logger to use. - */ - public SettingsController(@NonNull ILogger sdkLog) { - this(new LegacyFileOp(), sdkLog); - } - - /** - * Constructs a new default {@link SettingsController}. - * - * @param LegacyFileOp A non-null {@link LegacyFileOp} to perform file operations (to load/save settings.) - * @param sdkLog A non-null logger to use. - */ - public SettingsController(@NonNull IFileOp LegacyFileOp, @NonNull ILogger sdkLog) { - this(LegacyFileOp, sdkLog, new Settings()); - } - - /** - * Specialized constructor that wraps an existing {@link Settings} instance. - * This is mostly used in unit-tests to override settings that are being used. - * Normal usage should NOT need to call this constructor. - * - * @param LegacyFileOp A non-null {@link LegacyFileOp} to perform file operations (to load/save settings) - * @param sdkLog A non-null logger to use. - * @param settings A non-null {@link Settings} to use as-is. It is not duplicated. - */ - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected SettingsController(@NonNull IFileOp LegacyFileOp, - @NonNull ILogger sdkLog, - @NonNull Settings settings) { - mFileOp = LegacyFileOp; - mSdkLog = sdkLog; - mSettings = settings; - } - - @NonNull - public Settings getSettings() { - return mSettings; - } - - public void registerOnChangedListener(@Nullable OnChangedListener listener) { - if (listener != null && !mChangedListeners.contains(listener)) { - mChangedListeners.add(listener); - } - } - - public void unregisterOnChangedListener(@Nullable OnChangedListener listener) { - if (listener != null) { - mChangedListeners.remove(listener); - } - } - - //--- Access to settings ------------ - - - public static class Settings { - private final Properties mProperties; - - /** Initialize an empty set of settings. */ - public Settings() { - mProperties = new Properties(); - } - - /** Duplicates a set of settings. */ - public Settings(@NonNull Settings settings) { - this(); - for (Entry entry : settings.mProperties.entrySet()) { - mProperties.put(entry.getKey(), entry.getValue()); - } - } - - /** - * Specialized constructor for unit-tests that wraps an existing - * {@link Properties} instance. The properties instance is not duplicated, - * it's merely used as-is and changes will be reflected directly. - */ - protected Settings(@NonNull Properties properties) { - mProperties = properties; - } - - /** - * Returns the value of the {@link ISettingsPage#KEY_FORCE_HTTP} setting. - * - * @see ISettingsPage#KEY_FORCE_HTTP - */ - public boolean getForceHttp() { - return Boolean.parseBoolean(mProperties.getProperty(ISettingsPage.KEY_FORCE_HTTP)); - } - - /** - * Returns the value of the {@link ISettingsPage#KEY_ASK_ADB_RESTART} setting. - * - * @see ISettingsPage#KEY_ASK_ADB_RESTART - */ - public boolean getAskBeforeAdbRestart() { - return Boolean.parseBoolean(mProperties.getProperty(ISettingsPage.KEY_ASK_ADB_RESTART)); - } - - /** - * Returns the value of the {@link ISettingsPage#KEY_USE_DOWNLOAD_CACHE} setting. - * - * @see ISettingsPage#KEY_USE_DOWNLOAD_CACHE - */ - public boolean getUseDownloadCache() { - return Boolean.parseBoolean( - mProperties.getProperty( - ISettingsPage.KEY_USE_DOWNLOAD_CACHE, - Boolean.TRUE.toString())); - } - - /** - * Returns the value of the {@link ISettingsPage#KEY_SHOW_UPDATE_ONLY} setting. - * - * @see ISettingsPage#KEY_SHOW_UPDATE_ONLY - */ - public boolean getShowUpdateOnly() { - return Boolean.parseBoolean( - mProperties.getProperty( - ISettingsPage.KEY_SHOW_UPDATE_ONLY, - Boolean.TRUE.toString())); - } - - /** - * Returns the value of the {@link ISettingsPage#KEY_ENABLE_PREVIEWS} setting. - * - * @see ISettingsPage#KEY_ENABLE_PREVIEWS - */ - public boolean getEnablePreviews() { - return Boolean.parseBoolean( - mProperties.getProperty( - ISettingsPage.KEY_ENABLE_PREVIEWS, - Boolean.TRUE.toString())); - } - - /** - * Returns the value of the {@link ISettingsPage#KEY_MONITOR_DENSITY} setting - * @see ISettingsPage#KEY_MONITOR_DENSITY - */ - public int getMonitorDensity() { - String value = mProperties.getProperty(ISettingsPage.KEY_MONITOR_DENSITY, null); - if (value == null) { - return -1; - } - - try { - return Integer.parseInt(value); - } catch (NumberFormatException e) { - return -1; - } - } - } - - /** - * Sets the value of the {@link ISettingsPage#KEY_SHOW_UPDATE_ONLY} setting. - * - * @param enabled True if only compatible non-obsolete update items should be shown. - * @see ISettingsPage#KEY_SHOW_UPDATE_ONLY - */ - public void setShowUpdateOnly(boolean enabled) { - setSetting(ISettingsPage.KEY_SHOW_UPDATE_ONLY, enabled); - } - - /** - * Sets the value of the {@link ISettingsPage#KEY_MONITOR_DENSITY} setting. - * - * @param density the density of the monitor - * @see ISettingsPage#KEY_MONITOR_DENSITY - */ - public void setMonitorDensity(int density) { - mSettings.mProperties.setProperty( - ISettingsPage.KEY_MONITOR_DENSITY, Integer.toString(density)); - } - - /** - * Internal helper to set a boolean setting. - */ - void setSetting(@NonNull String key, boolean value) { - mSettings.mProperties.setProperty(key, Boolean.toString(value)); - } - - //--- Controller methods ------------- - - /** - * Associate the given {@link ISettingsPage} with this {@link SettingsController}. - *

- * This loads the current properties into the setting page UI. - * It then associates the SettingsChanged callback with this controller. - *

- * If the setting page given is null, it will be unlinked from controller. - * - * @param settingsPage An {@link ISettingsPage} to associate with the controller. - */ - public void setSettingsPage(@Nullable ISettingsPage settingsPage) { - mSettingsPage = settingsPage; - - if (settingsPage != null) { - settingsPage.loadSettings(mSettings.mProperties); - - settingsPage.setOnSettingsChanged(new ISettingsPage.SettingsChangedCallback() { - @Override - public void onSettingsChanged(ISettingsPage page) { - SettingsController.this.onSettingsChanged(); - } - }); - } - } - - /** - * Load settings from the settings file. - */ - public void loadSettings() { - - String path = null; - try { - String folder = AndroidLocation.getFolder(); - File f = new File(folder, SETTINGS_FILENAME); - path = f.getPath(); - - Properties props = mFileOp.loadProperties(f); - mSettings.mProperties.clear(); - mSettings.mProperties.putAll(props); - - // Properly reformat some settings to enforce their default value when missing. - setShowUpdateOnly(mSettings.getShowUpdateOnly()); - setSetting(ISettingsPage.KEY_ASK_ADB_RESTART, mSettings.getAskBeforeAdbRestart()); - setSetting(ISettingsPage.KEY_USE_DOWNLOAD_CACHE, mSettings.getUseDownloadCache()); - setSetting(ISettingsPage.KEY_ENABLE_PREVIEWS, mSettings.getEnablePreviews()); - - } catch (Exception e) { - if (mSdkLog != null) { - mSdkLog.error(e, - "Failed to load settings from .android folder. Path is '%1$s'.", - path); - } - } - } - - /** - * Saves settings to the settings file. - */ - public void saveSettings() { - - String path = null; - try { - String folder = AndroidLocation.getFolder(); - File f = new File(folder, SETTINGS_FILENAME); - path = f.getPath(); - mFileOp.saveProperties(f, mSettings.mProperties, "## Settings for Android Tool"); //$NON-NLS-1$ - - } catch (Exception e) { - if (mSdkLog != null) { - // This is important enough that we want to really nag the user about it - String reason = null; - - if (e instanceof FileNotFoundException) { - reason = "File not found"; - } else if (e instanceof AndroidLocationException) { - reason = ".android folder not found, please define ANDROID_SDK_HOME"; - } else if (e.getMessage() != null) { - reason = String.format("%1$s: %2$s", - e.getClass().getSimpleName(), - e.getMessage()); - } else { - reason = e.getClass().getName(); - } - - mSdkLog.error(e, "Failed to save settings file '%1$s': %2$s", path, reason); - } - } - } - - /** - * When settings have changed: retrieve the new settings, apply them, save them - * and notify on-settings-changed listeners. - */ - private void onSettingsChanged() { - if (mSettingsPage == null) { - return; - } - - Settings oldSettings = new Settings(mSettings); - mSettingsPage.retrieveSettings(mSettings.mProperties); - applySettings(); - saveSettings(); - - for (OnChangedListener listener : mChangedListeners) { - try { - listener.onSettingsChanged(this, oldSettings); - } catch (Throwable ignore) {} - } - } - - /** - * Applies the current settings. - */ - public void applySettings() { - Properties props = System.getProperties(); - - // Get the configured HTTP proxy settings - String proxyHost = mSettings.mProperties.getProperty(ISettingsPage.KEY_HTTP_PROXY_HOST, - ""); //$NON-NLS-1$ - String proxyPort = mSettings.mProperties.getProperty(ISettingsPage.KEY_HTTP_PROXY_PORT, - ""); //$NON-NLS-1$ - - // Set both the HTTP and HTTPS proxy system properties. - // The system property constants can be found in the Java SE documentation at - // http://download.oracle.com/javase/6/docs/technotes/guides/net/proxies.html - final String JAVA_PROP_HTTP_PROXY_HOST = "http.proxyHost"; //$NON-NLS-1$ - final String JAVA_PROP_HTTP_PROXY_PORT = "http.proxyPort"; //$NON-NLS-1$ - final String JAVA_PROP_HTTPS_PROXY_HOST = "https.proxyHost"; //$NON-NLS-1$ - final String JAVA_PROP_HTTPS_PROXY_PORT = "https.proxyPort"; //$NON-NLS-1$ - - // Only change the proxy if have something in the preferences. - // Do not erase the default settings by empty values. - if (proxyHost != null && !proxyHost.isEmpty()) { - props.setProperty(JAVA_PROP_HTTP_PROXY_HOST, proxyHost); - props.setProperty(JAVA_PROP_HTTPS_PROXY_HOST, proxyHost); - } - if (proxyPort != null && !proxyPort.isEmpty()) { - props.setProperty(JAVA_PROP_HTTP_PROXY_PORT, proxyPort); - props.setProperty(JAVA_PROP_HTTPS_PROXY_PORT, proxyPort); - } - } - -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/UpdaterData.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/UpdaterData.java deleted file mode 100644 index c1b691c6..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/updater/UpdaterData.java +++ /dev/null @@ -1,1336 +0,0 @@ -/* - * 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.sdklib.internal.repository.updater; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.annotations.VisibleForTesting; -import com.android.annotations.VisibleForTesting.Visibility; -import com.android.prefs.AndroidLocation.AndroidLocationException; -import com.android.sdklib.SdkManager; -import com.android.sdklib.internal.avd.AvdManager; -import com.android.sdklib.internal.repository.AdbWrapper; -import com.android.sdklib.internal.repository.DownloadCache; -import com.android.sdklib.internal.repository.ITask; -import com.android.sdklib.internal.repository.ITaskFactory; -import com.android.sdklib.internal.repository.ITaskMonitor; -import com.android.sdklib.internal.repository.LocalSdkParser; -import com.android.sdklib.internal.repository.NullTaskMonitor; -import com.android.sdklib.internal.repository.archives.Archive; -import com.android.sdklib.internal.repository.archives.ArchiveInstaller; -import com.android.sdklib.internal.repository.packages.AddonPackage; -import com.android.sdklib.repository.License; -import com.android.sdklib.internal.repository.packages.Package; -import com.android.sdklib.internal.repository.packages.PlatformToolPackage; -import com.android.sdklib.internal.repository.packages.ToolPackage; -import com.android.sdklib.internal.repository.sources.SdkRepoSource; -import com.android.sdklib.internal.repository.sources.SdkSource; -import com.android.sdklib.internal.repository.sources.SdkSourceCategory; -import com.android.sdklib.internal.repository.sources.SdkSources; -import com.android.sdklib.internal.repository.updater.SettingsController.OnChangedListener; -import com.android.sdklib.repository.ISdkChangeListener; -import com.android.sdklib.repository.SdkAddonConstants; -import com.android.sdklib.repository.SdkRepoConstants; -import com.android.sdklib.util.LineUtil; -import com.android.utils.ILogger; -import com.android.utils.IReaderLogger; -import com.android.utils.SparseIntArray; -import com.google.common.base.Charsets; -import com.google.common.base.Joiner; -import com.google.common.collect.Lists; -import com.google.common.collect.Sets; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.PrintStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.TreeMap; -import java.util.TreeSet; - -/** - * Data shared by the SDK Manager updaters. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class UpdaterData implements IUpdaterData { - - public static final int NO_TOOLS_MSG = 0; - public static final int TOOLS_MSG_UPDATED_FROM_ADT = 1; - public static final int TOOLS_MSG_UPDATED_FROM_SDKMAN = 2; - - private String mOsSdkRoot; - - private final LocalSdkParser mLocalSdkParser = new LocalSdkParser(); - /** Holds all sources. Do not use this directly. - * Instead use {@link #getSources()} so that unit tests can override this as needed. */ - private final SdkSources mSources = new SdkSources(); - /** Holds settings. Do not use this directly. - * Instead use {@link #getSettingsController()} so that unit tests can override this. */ - private final SettingsController mSettingsController; - private final ArrayList mListeners = new ArrayList(); - private final ILogger mSdkLog; - private ITaskFactory mTaskFactory; - - private SdkManager mSdkManager; - private AvdManager mAvdManager; - /** - * The current {@link PackageLoader} to use. - * Lazily created in {@link #getPackageLoader()}. - */ - private PackageLoader mPackageLoader; - /** - * The current {@link DownloadCache} to use. - * Lazily created in {@link #getDownloadCache()}. - */ - private DownloadCache mDownloadCache; - private AndroidLocationException mAvdManagerInitError; - - /** - * Creates a new updater data. - * - * @param sdkLog Logger. Cannot be null. - * @param osSdkRoot The OS path to the SDK root. - */ - public UpdaterData(String osSdkRoot, ILogger sdkLog) { - mOsSdkRoot = osSdkRoot; - mSdkLog = sdkLog; - - mSettingsController = initSettingsController(); - initSdk(); - } - - // ----- getters, setters ---- - - public String getOsSdkRoot() { - return mOsSdkRoot; - } - - @Override - public DownloadCache getDownloadCache() { - if (mDownloadCache == null) { - mDownloadCache = new DownloadCache( - getSettingsController().getSettings().getUseDownloadCache() ? - DownloadCache.Strategy.FRESH_CACHE : - DownloadCache.Strategy.DIRECT); - } - return mDownloadCache; - } - - public void setTaskFactory(ITaskFactory taskFactory) { - mTaskFactory = taskFactory; - } - - @Override - public ITaskFactory getTaskFactory() { - return mTaskFactory; - } - - public SdkSources getSources() { - return mSources; - } - - public LocalSdkParser getLocalSdkParser() { - return mLocalSdkParser; - } - - @Override - public ILogger getSdkLog() { - return mSdkLog; - } - - @Override - public SdkManager getSdkManager() { - return mSdkManager; - } - - @Override - public AvdManager getAvdManager() { - return mAvdManager; - } - - @Override - public SettingsController getSettingsController() { - return mSettingsController; - } - - /** Adds a listener ({@link ISdkChangeListener}) that is notified when the SDK is reloaded. */ - public void addListeners(ISdkChangeListener listener) { - if (mListeners.contains(listener) == false) { - mListeners.add(listener); - } - } - - /** Removes a listener ({@link ISdkChangeListener}) that is notified when the SDK is reloaded. */ - public void removeListener(ISdkChangeListener listener) { - mListeners.remove(listener); - } - - public PackageLoader getPackageLoader() { - // The package loader is lazily initialized here. - if (mPackageLoader == null) { - mPackageLoader = new PackageLoader(this); - } - return mPackageLoader; - } - - /** - * Check if any error occurred during initialization. - * If it did, display an error message. - * - * @return True if an error occurred, false if we should continue. - */ - public boolean checkIfInitFailed() { - if (mAvdManagerInitError != null) { - String example; - if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS) { - example = "%USERPROFILE%"; //$NON-NLS-1$ - } else { - example = "~"; //$NON-NLS-1$ - } - - String error = String.format( - "The AVD manager normally uses the user's profile directory to store " + - "AVD files. However it failed to find the default profile directory. " + - "\n" + - "To fix this, please set the environment variable ANDROID_SDK_HOME to " + - "a valid path such as \"%s\".", - example); - - displayInitError(error); - - return true; - } - return false; - } - - protected void displayInitError(String error) { - mSdkLog.error(null /* Throwable */, "%s", error); //$NON-NLS-1$ - } - - // ----- - - /** - * Runs a runnable on the UI thread. - * The base implementation just runs the runnable right away. - * - * @param r Non-null runnable. - */ - protected void runOnUiThread(@NonNull Runnable r) { - r.run(); - } - - /** - * Initializes the {@link SdkManager} and the {@link AvdManager}. - * Extracted so that we can override this in unit tests. - */ - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected void initSdk() { - setSdkManager(SdkManager.createManager(mOsSdkRoot, mSdkLog)); - try { - mAvdManager = null; - mAvdManager = AvdManager.getInstance(mSdkManager.getLocalSdk(), mSdkLog); - } catch (AndroidLocationException e) { - mSdkLog.error(e, "Unable to read AVDs: " + e.getMessage()); //$NON-NLS-1$ - - // Note: we used to continue here, but the thing is that - // mAvdManager==null so nothing is really going to work as - // expected. Let's just display an error later in checkIfInitFailed() - // and abort right there. This step is just too early in the SWT - // setup process to display a message box yet. - - mAvdManagerInitError = e; - } - - // notify listeners. - broadcastOnSdkReload(); - } - - /** - * Initializes the {@link SettingsController} - * Extracted so that we can override this in unit tests. - */ - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected SettingsController initSettingsController() { - SettingsController settingsController = new SettingsController(mSdkLog); - settingsController.registerOnChangedListener(new OnChangedListener() { - @Override - public void onSettingsChanged( - SettingsController controller, - SettingsController.Settings oldSettings) { - - // Reset the download cache if it doesn't match the right strategy. - // The cache instance gets lazily recreated later in getDownloadCache(). - if (mDownloadCache != null) { - if (controller.getSettings().getUseDownloadCache() && - mDownloadCache.getStrategy() != DownloadCache.Strategy.FRESH_CACHE) { - mDownloadCache = null; - } else if (!controller.getSettings().getUseDownloadCache() && - mDownloadCache.getStrategy() != DownloadCache.Strategy.DIRECT) { - mDownloadCache = null; - } - } - } - }); - return settingsController; - } - - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected void setSdkManager(SdkManager sdkManager) { - mSdkManager = sdkManager; - } - - /** - * Reloads the SDK content (targets). - *

- * This also reloads the AVDs in case their status changed. - *

- * This does not notify the listeners ({@link ISdkChangeListener}). - */ - public void reloadSdk() { - // reload SDK - mSdkManager.reloadSdk(mSdkLog); - - // reload AVDs - if (mAvdManager != null) { - try { - mAvdManager.reloadAvds(mSdkLog); - } catch (AndroidLocationException e) { - // FIXME - } - } - - mLocalSdkParser.clearPackages(); - - // notify listeners - broadcastOnSdkReload(); - } - - /** - * Reloads the AVDs. - *

- * This does not notify the listeners. - */ - public void reloadAvds() { - // reload AVDs - if (mAvdManager != null) { - try { - mAvdManager.reloadAvds(mSdkLog); - } catch (AndroidLocationException e) { - mSdkLog.error(e, null); - } - } - } - - /** - * Sets up the default sources:
- * - the default google SDK repository,
- * - the user sources from prefs
- * - the extra repo URLs from the environment,
- * - and finally the extra user repo URLs from the environment. - */ - public void setupDefaultSources() { - SdkSources sources = getSources(); - - // Load the conventional sources. - // For testing, the env var can be set to replace the default root download URL. - // It must end with a / and its the location where the updater will look for - // the repository.xml, addons_list.xml and such files. - - String baseUrl = System.getenv("SDK_TEST_BASE_URL"); //$NON-NLS-1$ - if (baseUrl == null || baseUrl.length() <= 0 || !baseUrl.endsWith("/")) { //$NON-NLS-1$ - baseUrl = SdkRepoConstants.URL_GOOGLE_SDK_SITE; - } - - sources.add(SdkSourceCategory.ANDROID_REPO, - new SdkRepoSource(baseUrl, - SdkSourceCategory.ANDROID_REPO.getUiName())); - - // Load user sources (this will also notify change listeners but this operation is - // done early enough that there shouldn't be any anyway.) - sources.loadUserAddons(getSdkLog()); - } - - /** - * Returns the list of installed packages, parsing them if this has not yet been done. - *

- * The package list is cached in the {@link LocalSdkParser} and will be reset when - * {@link #reloadSdk()} is invoked. - */ - public Package[] getInstalledPackages(ITaskMonitor monitor) { - LocalSdkParser parser = getLocalSdkParser(); - - Package[] packages = parser.getPackages(); - - if (packages == null) { - // load on demand the first time - packages = parser.parseSdk(getOsSdkRoot(), getSdkManager(), monitor); - } - - return packages; - } - /** - * Install the list of given {@link Archive}s. This is invoked by the user selecting some - * packages in the remote page and then clicking "install selected". - * - * @param archives The archives to install. Incompatible ones will be skipped. - * @param flags Optional flags for the installer, such as {@link #NO_TOOLS_MSG}. - * @return A list of archives that have been installed. Can be empty but not null. - */ - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected List installArchives(final List archives, final int flags) { - if (mTaskFactory == null) { - throw new IllegalArgumentException("Task Factory is null"); - } - - // this will accumulate all the packages installed. - final List newlyInstalledArchives = new ArrayList(); - - final boolean forceHttp = getSettingsController().getSettings().getForceHttp(); - - // sort all archives based on their dependency level. - Collections.sort(archives, new InstallOrderComparator()); - - mTaskFactory.start("Installing Archives", new ITask() { - @Override - public void run(ITaskMonitor monitor) { - - final int progressPerArchive = 2 * ArchiveInstaller.NUM_MONITOR_INC; - monitor.setProgressMax(1 + archives.size() * progressPerArchive); - monitor.setDescription("Preparing to install archives"); - - boolean installedAddon = false; - boolean installedTools = false; - boolean installedPlatformTools = false; - boolean preInstallHookInvoked = false; - - // Mark all current local archives as already installed. - HashSet installedArchives = new HashSet(); - for (Package p : getInstalledPackages(monitor.createSubMonitor(1))) { - for (Archive a : p.getArchives()) { - installedArchives.add(a); - } - } - - int numInstalled = 0; - nextArchive: for (ArchiveInfo ai : archives) { - Archive archive = ai.getNewArchive(); - if (archive == null) { - // This is not supposed to happen. - continue nextArchive; - } - - int nextProgress = monitor.getProgress() + progressPerArchive; - try { - if (monitor.isCancelRequested()) { - break; - } - - ArchiveInfo[] adeps = ai.getDependsOn(); - if (adeps != null) { - for (ArchiveInfo adep : adeps) { - Archive na = adep.getNewArchive(); - if (na == null) { - // This archive depends on a missing archive. - // We shouldn't get here. - // Skip it. - monitor.log("Skipping '%1$s'; it depends on a missing package.", - archive.getParentPackage().getShortDescription()); - continue nextArchive; - } else if (!installedArchives.contains(na)) { - // This archive depends on another one that was not installed. - // We shouldn't get here. - // Skip it. - monitor.logError("Skipping '%1$s'; it depends on '%2$s' which was not installed.", - archive.getParentPackage().getShortDescription(), - adep.getShortDescription()); - continue nextArchive; - } - } - } - - if (!preInstallHookInvoked) { - preInstallHookInvoked = true; - broadcastPreInstallHook(); - } - - ArchiveInstaller installer = createArchiveInstaler(); - if (installer.install(ai, - mOsSdkRoot, - forceHttp, - mSdkManager, - getDownloadCache(), - monitor)) { - // We installed this archive. - newlyInstalledArchives.add(archive); - installedArchives.add(archive); - numInstalled++; - - // If this package was replacing an existing one, the old one - // is no longer installed. - installedArchives.remove(ai.getReplaced()); - - // Check if we successfully installed a platform-tool or add-on package. - if (archive.getParentPackage() instanceof AddonPackage) { - installedAddon = true; - } else if (archive.getParentPackage() instanceof ToolPackage) { - installedTools = true; - } else if (archive.getParentPackage() instanceof PlatformToolPackage) { - installedPlatformTools = true; - } - } - - } catch (Throwable t) { - // Display anything unexpected in the monitor. - String msg = t.getMessage(); - if (msg != null) { - msg = String.format("Unexpected Error installing '%1$s': %2$s: %3$s", - archive.getParentPackage().getShortDescription(), - t.getClass().getCanonicalName(), msg); - } else { - // no error info? get the stack call to display it - // At least that'll give us a better bug report. - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - t.printStackTrace(new PrintStream(baos)); - - msg = String.format("Unexpected Error installing '%1$s'\n%2$s", - archive.getParentPackage().getShortDescription(), - baos.toString()); - } - - monitor.log( "%1$s", msg); //$NON-NLS-1$ - mSdkLog.error(t, "%1$s", msg); //$NON-NLS-1$ - } finally { - - // Always move the progress bar to the desired position. - // This allows internal methods to not have to care in case - // they abort early - monitor.incProgress(nextProgress - monitor.getProgress()); - } - } - - if (installedAddon) { - // Update the USB vendor ids for adb - try { - mSdkManager.updateAdb(); - monitor.log("Updated ADB to support the USB devices declared in the SDK add-ons."); - } catch (Exception e) { - mSdkLog.error(e, "Update ADB failed"); - monitor.logError("failed to update adb to support the USB devices declared in the SDK add-ons."); - } - } - - if (preInstallHookInvoked) { - broadcastPostInstallHook(); - } - - if (installedAddon || installedPlatformTools) { - // We need to restart ADB. Actually since we don't know if it's even - // running, maybe we should just kill it and not start it. - // Note: it turns out even under Windows we don't need to kill adb - // before updating the tools folder, as adb.exe is (surprisingly) not - // locked. - - askForAdbRestart(monitor); - } - - if (installedTools) { - notifyToolsNeedsToBeRestarted(flags); - } - - if (numInstalled == 0) { - monitor.setDescription("Done. Nothing was installed."); - } else { - monitor.setDescription("Done. %1$d %2$s installed.", - numInstalled, - numInstalled == 1 ? "package" : "packages"); - - //notify listeners something was installed, so that they can refresh - reloadSdk(); - } - } - }); - - return newlyInstalledArchives; - } - - /** - * A comparator to sort all the {@link ArchiveInfo} based on their - * dependency level. This forces the installer to install first all packages - * with no dependency, then those with one level of dependency, etc. - */ - private static class InstallOrderComparator implements Comparator { - - private final Map mOrders = new HashMap(); - - @Override - public int compare(ArchiveInfo o1, ArchiveInfo o2) { - int n1 = getDependencyOrder(o1); - int n2 = getDependencyOrder(o2); - - return n1 - n2; - } - - private int getDependencyOrder(ArchiveInfo ai) { - if (ai == null) { - return 0; - } - - // reuse cached value, if any - Integer cached = mOrders.get(ai); - if (cached != null) { - return cached.intValue(); - } - - ArchiveInfo[] deps = ai.getDependsOn(); - if (deps == null) { - return 0; - } - - // compute dependencies, recursively - int n = deps.length; - - for (ArchiveInfo dep : deps) { - n += getDependencyOrder(dep); - } - - // cache it - mOrders.put(ai, Integer.valueOf(n)); - - return n; - } - - } - - /** - * Attempts to restart ADB. - *

- * If the "ask before restart" setting is set (the default), prompt the user whether - * now is a good time to restart ADB. - */ - protected void askForAdbRestart(ITaskMonitor monitor) { - // Restart ADB if we don't need to ask. - if (!getSettingsController().getSettings().getAskBeforeAdbRestart()) { - AdbWrapper adb = new AdbWrapper(getOsSdkRoot(), monitor); - adb.stopAdb(); - adb.startAdb(); - } - } - - protected void notifyToolsNeedsToBeRestarted(int flags) { - - String msg = null; - if ((flags & TOOLS_MSG_UPDATED_FROM_ADT) == TOOLS_MSG_UPDATED_FROM_ADT) { - msg = - "The Android SDK and AVD Manager that you are currently using has been updated. " + - "Please also run Eclipse > Help > Check for Updates to see if the Android " + - "plug-in needs to be updated."; - - } else if ((flags & TOOLS_MSG_UPDATED_FROM_SDKMAN) == TOOLS_MSG_UPDATED_FROM_SDKMAN) { - msg = - "The Android SDK and AVD Manager that you are currently using has been updated. " + - "It is recommended that you now close the manager window and re-open it. " + - "If you use Eclipse, please run Help > Check for Updates to see if the Android " + - "plug-in needs to be updated."; - } else if ((flags & NO_TOOLS_MSG) == NO_TOOLS_MSG) { - return; - } - mSdkLog.info("%s", msg); //$NON-NLS-1$ - } - - /** - * Fetches all archives available on the known remote sources. - * - * Used by {@link UpdaterData#listRemotePackages_NoGUI} and - * {@link UpdaterData#updateOrInstallAll_NoGUI}. - * - * @param includeAll True to list and install all packages, including obsolete ones. - * @return A list of potential {@link ArchiveInfo} to install. - */ - private List getRemoteArchives_NoGUI(boolean includeAll) { - refreshSources(true); - getPackageLoader().loadRemoteAddonsList(new NullTaskMonitor(getSdkLog())); - - List archives; - SdkUpdaterLogic ul = new SdkUpdaterLogic(this); - - if (includeAll) { - archives = ul.getAllRemoteArchives( - getSources(), - getLocalSdkParser().getPackages(), - includeAll); - - } else { - archives = ul.computeUpdates( - null /*selectedArchives*/, - getSources(), - getLocalSdkParser().getPackages(), - includeAll); - - ul.addNewPlatforms( - archives, - getSources(), - getLocalSdkParser().getPackages(), - includeAll); - } - - Collections.sort(archives); - return archives; - } - - /** - * Lists remote packages available for install using - * {@link UpdaterData#updateOrInstallAll_NoGUI}. - * - * @param includeAll True to list and install all packages, including obsolete ones. - * @param extendedOutput True to display more details on each package. - */ - public void listRemotePackages_NoGUI(boolean includeAll, boolean extendedOutput) { - - List archives = getRemoteArchives_NoGUI(includeAll); - - mSdkLog.info("Packages available for installation or update: %1$d\n", archives.size()); - - int index = 1; - for (ArchiveInfo ai : archives) { - Archive a = ai.getNewArchive(); - if (a != null) { - Package p = a.getParentPackage(); - if (p != null) { - if (extendedOutput) { - mSdkLog.info("----------\n"); - mSdkLog.info("id: %1$d or \"%2$s\"\n", index, p.installId()); - mSdkLog.info(" Type: %1$s\n", - p.getClass().getSimpleName().replaceAll("Package", "")); //$NON-NLS-1$ //$NON-NLS-2$ - String desc = LineUtil.reformatLine(" Desc: %s\n", - p.getLongDescription()); - mSdkLog.info("%s", desc); //$NON-NLS-1$ - } else { - mSdkLog.info("%1$ 4d- %2$s\n", - index, - p.getShortDescription()); - } - index++; - } - } - } - } - - /** - * Tries to update all the *existing* local packages. - * This version *requires* to be run with a GUI. - *

- * There are two modes of operation: - *

    - *
  • If selectedArchives is null, refreshes all sources, compares the available remote - * packages with the current local ones and suggest updates to be done to the user (including - * new platforms that the users doesn't have yet). - *
  • If selectedArchives is not null, this represents a list of archives/packages that - * the user wants to install or update, so just process these. - *
- * - * @param selectedArchives The list of remote archives to consider for the update. - * This can be null, in which case a list of remote archive is fetched from all - * available sources. - * @param includeObsoletes True if obsolete packages should be used when resolving what - * to update. - * @param flags Optional flags for the installer, such as {@link #NO_TOOLS_MSG}. - * @return A list of archives that have been installed. Can be null if nothing was done. - */ - public List updateOrInstallAll_WithGUI( - Collection selectedArchives, - boolean includeObsoletes, - int flags) { - // FIXME revisit this logic. This is just an transitional implementation - // while I refactor the way the sdk manager works internally. - - SdkUpdaterLogic ul = new SdkUpdaterLogic(this); - List archives = ul.computeUpdates( - selectedArchives, - getSources(), - getLocalSdkParser().getPackages(), - includeObsoletes); - - if (selectedArchives == null) { - getPackageLoader().loadRemoteAddonsList(new NullTaskMonitor(getSdkLog())); - ul.addNewPlatforms( - archives, - getSources(), - getLocalSdkParser().getPackages(), - includeObsoletes); - } - - Collections.sort(archives); - - if (!archives.isEmpty()) { - return installArchives(archives, flags); - } - return null; - } - - /** - * Tries to update all the *existing* local packages. - * This version is intended to run without a GUI and - * only outputs to the current {@link ILogger}. - * - * @param pkgFilter A list of {@link SdkRepoConstants#NODES} or {@link Package#installId()} - * or package indexes to limit the packages we can update or install. - * A null or empty list means to update everything possible. - * @param includeAll True to list and install all packages, including obsolete ones. - * @param dryMode True to check what would be updated/installed but do not actually - * download or install anything. - * @param acceptLicense SDK licenses to automatically accept. - * @return A list of archives that have been installed. Can be null if nothing was done. - * @deprecated Use {@link #updateOrInstallAll_NoGUI(java.util.Collection, boolean, boolean, String, boolean)} - * instead - */ - @Deprecated - public List updateOrInstallAll_NoGUI( - Collection pkgFilter, - boolean includeAll, - boolean dryMode, - String acceptLicense) { - return updateOrInstallAll_NoGUI(pkgFilter, includeAll, dryMode, acceptLicense, false); - } - - /** - * Tries to update all the *existing* local packages. - * This version is intended to run without a GUI and - * only outputs to the current {@link ILogger}. - * - * @param pkgFilter A list of {@link SdkRepoConstants#NODES} or {@link Package#installId()} - * or package indexes to limit the packages we can update or install. - * A null or empty list means to update everything possible. - * @param includeAll True to list and install all packages, including obsolete ones. - * @param dryMode True to check what would be updated/installed but do not actually - * download or install anything. - * @param acceptLicense SDK licenses to automatically accept. - * @param includeDependencies If true, also include any required dependencies - * @return A list of archives that have been installed. Can be null if nothing was done. - */ - public List updateOrInstallAll_NoGUI( - Collection pkgFilter, - boolean includeAll, - boolean dryMode, - String acceptLicense, - boolean includeDependencies) { - - List archives = getRemoteArchives_NoGUI(includeAll); - - // Filter the selected archives to only keep the ones matching the filter - if (pkgFilter != null && !pkgFilter.isEmpty() && archives != null && !archives.isEmpty()) { - // Map filter types to an SdkRepository Package type, - // e.g. create a map "platform" => PlatformPackage.class - HashMap> pkgMap = - new HashMap>(); - - mapFilterToPackageClass(pkgMap, SdkRepoConstants.NODES); - mapFilterToPackageClass(pkgMap, SdkAddonConstants.NODES); - - // Prepare a map install-id => package instance - HashMap installIdMap = new HashMap(); - for (ArchiveInfo ai : archives) { - Archive a = ai.getNewArchive(); - if (a != null) { - Package p = a.getParentPackage(); - if (p != null) { - String iid = p.installId().toLowerCase(Locale.US); - if (iid != null && !iid.isEmpty() && !installIdMap.containsKey(iid)) { - installIdMap.put(iid, p); - } - } - } - } - - // Now intersect this with the pkgFilter requested by the user, in order to - // only keep the classes that the user wants to install. - // We also create a set with the package indices requested by the user - // and a set of install-ids requested by the user. - - HashSet> userFilteredClasses = - new HashSet>(); - SparseIntArray userFilteredIndices = new SparseIntArray(); - Set userFilteredInstallIds = new HashSet(); - - for (String iid : pkgFilter) { - // The install-id is not case-sensitive. - iid = iid.toLowerCase(Locale.US); - - if (installIdMap.containsKey(iid)) { - userFilteredInstallIds.add(iid); - - } else if (iid.replaceAll("[0-9]+", "").isEmpty()) {//$NON-NLS-1$ //$NON-NLS-2$ - // An all-digit number is a package index requested by the user. - int index = Integer.parseInt(iid); - userFilteredIndices.put(index, index); - - } else if (pkgMap.containsKey(iid)) { - userFilteredClasses.add(pkgMap.get(iid)); - - } else { - // This should not happen unless there's a mismatch in the package map. - mSdkLog.error(null, "Ignoring unknown package filter '%1$s'", iid); - } - } - - // we don't need the maps anymore - pkgMap = null; - installIdMap = null; - - // Now filter the remote archives list to keep: - // - any package which class matches userFilteredClasses - // - any package index which matches userFilteredIndices - // - any package install id which matches userFilteredInstallIds - - int index = 1; - for (Iterator it = archives.iterator(); it.hasNext(); ) { - boolean keep = false; - ArchiveInfo ai = it.next(); - Archive a = ai.getNewArchive(); - if (a != null) { - Package p = a.getParentPackage(); - if (p != null) { - if (userFilteredInstallIds.contains(p.installId().toLowerCase(Locale.US)) || - userFilteredClasses.contains(p.getClass()) || - userFilteredIndices.get(index) > 0) { - keep = true; - } - - index++; - } - } - - if (!keep) { - it.remove(); - } - } - - if (archives.isEmpty()) { - mSdkLog.info(LineUtil.reflowLine( - "Warning: The package filter removed all packages. There is nothing to install.\nPlease consider trying to update again without a package filter.\n")); - return null; - } - } - - if (archives != null && !archives.isEmpty()) { - if (includeDependencies) { - List dependencies = getDependencies(archives); - if (!dependencies.isEmpty()) { - List combined = Lists.newArrayList(); - combined.addAll(dependencies); - combined.addAll(archives); - archives = combined; - } - } - if (dryMode) { - mSdkLog.info("Packages selected for install:\n"); - for (ArchiveInfo ai : archives) { - Archive a = ai.getNewArchive(); - if (a != null) { - Package p = a.getParentPackage(); - if (p != null) { - mSdkLog.info("- %1$s\n", p.getShortDescription()); - } - } - } - mSdkLog.info("\nDry mode is on so nothing is actually being installed.\n"); - } else { - if (acceptLicense(archives, acceptLicense, 100 /* numRetries */)) { - return installArchives(archives, NO_TOOLS_MSG); - } - } - } else { - mSdkLog.info("There is nothing to install or update.\n"); - } - - return null; - } - - /** - * Computes the transitive dependencies of the given list of archives. This will only - * include dependencies that also need to be installed, not satisfied dependencies. - */ - private static List getDependencies(@NonNull List archives) { - List dependencies = Lists.newArrayList(); - for (ArchiveInfo archive : archives) { - addDependencies(dependencies, archive, Sets.newHashSet()); - } - return dependencies; - } - - private static void addDependencies(@NonNull List dependencies, - @NonNull ArchiveInfo archive, - @NonNull Set visited) { - if (visited.contains(archive)) { - return; - } - visited.add(archive); - - ArchiveInfo[] dependsOn = archive.getDependsOn(); - if (dependsOn != null) { - for (ArchiveInfo dependency : dependsOn) { - if (!dependencies.contains(dependency)) { - dependencies.add(dependency); - addDependencies(dependencies, dependency, visited); - } - } - } - } - - /** - * Validates that all archive licenses are accepted. - *

- * There are 2 cases:
- * - When {@code acceptLicenses} is given, the licenses specified are automatically - * accepted and all those not specified are automatically rejected.
- * - When {@code acceptLicenses} is empty or null, licenses are collected and there's - * an input prompt on StdOut to ask a yes/no question. To output, this uses the - * current {@link #mSdkLog} which should be configured to send - * {@link ILogger#info(String, Object...)} directly to {@link System#out}.
- * - * Finally only accepted licenses are kept in the archive list. - * - * @param archives The archives to validate. - * @param acceptLicenseIds A comma-separated list of licenses ids already approved. - * @param numRetries The number of times the command-line will ask to accept a given - * license when the input doesn't match the expected y/n/yes/no answer. - * Use 0 for infinite. Useful for unit-tests. Once the number of retries - * is reached, the license is assumed as rejected. - * @return True if there are any archives left to install. - */ - @VisibleForTesting(visibility=Visibility.PRIVATE) - boolean acceptLicense( - List archives, - String acceptLicenseIds, - final int numRetries) { - TreeSet acceptedLids = new TreeSet(); - if (acceptLicenseIds != null) { - acceptedLids.addAll(Arrays.asList(acceptLicenseIds.split(","))); //$NON-NLS-1$ - } - boolean automated = !acceptedLids.isEmpty(); - - TreeSet rejectedLids = new TreeSet(); - TreeMap lidToAccept = new TreeMap(); - TreeMap> lidPkgNames = new TreeMap>(); - - // Find the licenses needed. Include those already accepted. - for (ArchiveInfo ai : archives) { - License lic = getArchiveInfoLicense(ai); - if (lic == null) { - continue; - } - String lid = getLicenseId(lic); - if (!acceptedLids.contains(lid)) { - if (automated) { - // Automatically reject those not already accepted - rejectedLids.add(lid); - } else { - // Queue it to ask for it to be accepted - lidToAccept.put(lid, lic); - List list = lidPkgNames.get(lid); - if (list == null) { - list = new ArrayList(); - lidPkgNames.put(lid, list); - } - list.add(ai.getShortDescription()); - } - } - } - - // Ask for each license that needs to be asked manually for confirmation - nextEntry: for (Map.Entry entry : lidToAccept.entrySet()) { - String lid = entry.getKey(); - License lic = entry.getValue(); - mSdkLog.info("-------------------------------\n"); - mSdkLog.info("License id: %1$s\n", lid); - mSdkLog.info("Used by: \n - %1$s\n", - Joiner.on("\n - ").skipNulls().join(lidPkgNames.get(lid))); - mSdkLog.info("-------------------------------\n\n"); - mSdkLog.info("%1$s\n", lic.getLicense()); - - int retries = numRetries; - tryAgain: while(true) { - try { - mSdkLog.info("Do you accept the license '%1$s' [y/n]: ", lid); - - byte[] buffer = new byte[256]; - if (mSdkLog instanceof IReaderLogger) { - ((IReaderLogger) mSdkLog).readLine(buffer); - } else { - System.in.read(buffer); - } - mSdkLog.info("\n"); - - String reply = new String(buffer, Charsets.UTF_8); - reply = reply.trim().toLowerCase(Locale.US); - - if ("y".equals(reply) || "yes".equals(reply)) { - acceptedLids.add(lid); - continue nextEntry; - - } else if ("n".equals(reply) || "no".equals(reply)) { - break tryAgain; - - } else { - mSdkLog.info("Unknown response '%1$s'.\n", reply); - if (--retries == 0) { - mSdkLog.info("Max number of retries exceeded. Rejecting '%1$s'\n", lid); - break tryAgain; - } - continue tryAgain; - } - - } catch (IOException e) { - // Panic. Don't install anything. - e.printStackTrace(); - return false; - } - } - rejectedLids.add(lid); - } - - // Finally remove all archive which license is rejected or not accepted. - for (Iterator it = archives.iterator(); it.hasNext(); ) { - ArchiveInfo ai = it.next(); - License lic = getArchiveInfoLicense(ai); - if (lic == null) { - continue; - } - String lid = getLicenseId(lic); - if (rejectedLids.contains(lid) || !acceptedLids.contains(lid)) { - mSdkLog.info("Package %1$s not installed due to rejected license '%2$s'.\n", - ai.getShortDescription(), - lid); - it.remove(); - } - } - - - return !archives.isEmpty(); - } - - private License getArchiveInfoLicense(ArchiveInfo ai) { - Archive a = ai.getNewArchive(); - if (a != null) { - Package p = a.getParentPackage(); - if (p != null) { - License lic = p.getLicense(); - if (lic != null && - lic.getLicenseRef() != null && - !lic.getLicense().isEmpty() && - lic.getLicense() != null && - !lic.getLicense().isEmpty()) { - return lic; - } - } - } - - return null; - } - - private String getLicenseId(License lic) { - return String.format("%1$s-%2$08x", //$NON-NLS-1$ - lic.getLicenseRef(), - lic.getLicense().hashCode()); - } - - @SuppressWarnings("unchecked") - private void mapFilterToPackageClass( - HashMap> inOutPkgMap, - String[] nodes) { - - // Automatically find the classes matching the node names - ClassLoader classLoader = getClass().getClassLoader(); - String basePackage = Package.class.getPackage().getName(); - - for (String node : nodes) { - // Capitalize the name - String name = node.substring(0, 1).toUpperCase() + node.substring(1); - - // We can have one dash at most in a name. If it's present, we'll try - // with the dash or with the next letter capitalized. - int dash = name.indexOf('-'); - if (dash > 0) { - name = name.replaceFirst("-", ""); - } - - for (int alternatives = 0; alternatives < 2; alternatives++) { - - String fqcn = basePackage + '.' + name + "Package"; //$NON-NLS-1$ - try { - Class clazz = - (Class) classLoader.loadClass(fqcn); - if (clazz != null) { - inOutPkgMap.put(node, clazz); - continue; - } - } catch (Throwable ignore) { - } - - if (alternatives == 0 && dash > 0) { - // Try an alternative where the next letter after the dash - // is converted to an upper case. - name = name.substring(0, dash) + - name.substring(dash, dash + 1).toUpperCase() + - name.substring(dash + 1); - } else { - break; - } - } - } - } - - /** - * Refresh all sources. This is invoked either internally (reusing an existing monitor) - * or as a UI callback on the remote page "Refresh" button (in which case the monitor is - * null and a new task should be created.) - * - * @param forceFetching When true, load sources that haven't been loaded yet. - * When false, only refresh sources that have been loaded yet. - */ - public void refreshSources(final boolean forceFetching) { - assert mTaskFactory != null; - - final boolean forceHttp = getSettingsController().getSettings().getForceHttp(); - - mTaskFactory.start("Refresh Sources", new ITask() { - @Override - public void run(ITaskMonitor monitor) { - - getPackageLoader().loadRemoteAddonsList(monitor); - - SdkSource[] sources = getSources().getAllSources(); - monitor.setDescription("Refresh Sources"); - monitor.setProgressMax(monitor.getProgress() + sources.length); - for (SdkSource source : sources) { - if (forceFetching || - source.getPackages() != null || - source.getFetchError() != null) { - source.load(getDownloadCache(), monitor.createSubMonitor(1), forceHttp); - } - monitor.incProgress(1); - } - } - }); - } - - /** - * Safely invoke all the registered {@link ISdkChangeListener#onSdkLoaded()}. - * This can be called from any thread. - */ - public void broadcastOnSdkLoaded() { - if (!mListeners.isEmpty()) { - runOnUiThread(new Runnable() { - @Override - public void run() { - for (ISdkChangeListener listener : mListeners) { - try { - listener.onSdkLoaded(); - } catch (Throwable t) { - mSdkLog.error(t, null); - } - } - } - }); - } - } - - /** - * Safely invoke all the registered {@link ISdkChangeListener#onSdkReload()}. - * This can be called from any thread. - */ - private void broadcastOnSdkReload() { - if (!mListeners.isEmpty()) { - runOnUiThread(new Runnable() { - @Override - public void run() { - for (ISdkChangeListener listener : mListeners) { - try { - listener.onSdkReload(); - } catch (Throwable t) { - mSdkLog.error(t, null); - } - } - } - }); - } - } - - /** - * Safely invoke all the registered {@link ISdkChangeListener#preInstallHook()}. - * This can be called from any thread. - */ - private void broadcastPreInstallHook() { - if (!mListeners.isEmpty()) { - runOnUiThread(new Runnable() { - @Override - public void run() { - for (ISdkChangeListener listener : mListeners) { - try { - listener.preInstallHook(); - } catch (Throwable t) { - mSdkLog.error(t, null); - } - } - } - }); - } - } - - /** - * Safely invoke all the registered {@link ISdkChangeListener#postInstallHook()}. - * This can be called from any thread. - */ - private void broadcastPostInstallHook() { - if (!mListeners.isEmpty()) { - runOnUiThread(new Runnable() { - @Override - public void run() { - for (ISdkChangeListener listener : mListeners) { - try { - listener.postInstallHook(); - } catch (Throwable t) { - mSdkLog.error(t, null); - } - } - } - }); - } - } - - /** - * Internal helper to return a new {@link ArchiveInstaller}. - * This allows us to override the installer for unit-testing. - */ - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected ArchiveInstaller createArchiveInstaler() { - return new ArchiveInstaller(); - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/io/IFileOp.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/io/IFileOp.java deleted file mode 100644 index 4ed4ab1c..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/io/IFileOp.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright (C) 2011 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.sdklib.io; - -import com.android.annotations.NonNull; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.Properties; - - -/** - * Wraps some common {@link File} operations on files and folders. - *

- * This makes it possible to override/mock/stub some file operations in unit tests. - */ -public interface IFileOp { - - /** - * Helper to delete a file or a directory. - * For a directory, recursively deletes all of its content. - * Files that cannot be deleted right away are marked for deletion on exit. - * It's ok for the file or folder to not exist at all. - * The argument can be null. - */ - void deleteFileOrFolder(@NonNull File fileOrFolder); - - /** - * Sets the executable Unix permission (+x) on a file or folder. - *

- * This attempts to use File#setExecutable through reflection if - * it's available. - * If this is not available, this invokes a chmod exec instead, - * so there is no guarantee of it being fast. - *

- * Caller must make sure to not invoke this under Windows. - * - * @param file The file to set permissions on. - * @throws IOException If an I/O error occurs - */ - void setExecutablePermission(@NonNull File file) throws IOException; - - /** - * Sets the file or directory as read-only. - * - * @param file The file or directory to set permissions on. - */ - void setReadOnly(@NonNull File file); - - /** - * Copies a binary file. - * - * @param source the source file to copy. - * @param dest the destination file to write. - * @throws FileNotFoundException if the source file doesn't exist. - * @throws IOException if there's a problem reading or writing the file. - */ - void copyFile(@NonNull File source, @NonNull File dest) throws IOException; - - /** - * Checks whether 2 binary files are the same. - * - * @param file1 the source file to copy - * @param file2 the destination file to write - * @throws FileNotFoundException if the source files don't exist. - * @throws IOException if there's a problem reading the files. - */ - boolean isSameFile(@NonNull File file1, @NonNull File file2) - throws IOException; - - /** Invokes {@link File#exists()} on the given {@code file}. */ - boolean exists(@NonNull File file); - - /** Invokes {@link File#isFile()} on the given {@code file}. */ - boolean isFile(@NonNull File file); - - /** Invokes {@link File#isDirectory()} on the given {@code file}. */ - boolean isDirectory(@NonNull File file); - - /** Invokes {@link File#length()} on the given {@code file}. */ - long length(@NonNull File file); - - /** - * Invokes {@link File#delete()} on the given {@code file}. - * Note: for a recursive folder version, consider {@link #deleteFileOrFolder(File)}. - */ - boolean delete(@NonNull File file); - - /** Invokes {@link File#mkdirs()} on the given {@code file}. */ - boolean mkdirs(@NonNull File file); - - /** - * Invokes {@link File#listFiles()} on the given {@code file}. - * Contrary to the Java API, this returns an empty array instead of null when the - * directory does not exist. - */ - @NonNull - File[] listFiles(@NonNull File file); - - /** Invokes {@link File#renameTo(File)} on the given files. */ - boolean renameTo(@NonNull File oldDir, @NonNull File newDir); - - /** Creates a new {@link OutputStream} for the given {@code file}. */ - @NonNull - OutputStream newFileOutputStream(@NonNull File file) - throws FileNotFoundException; - - /** Creates a new {@link InputStream} for the given {@code file}. */ - @NonNull - InputStream newFileInputStream(@NonNull File file) - throws FileNotFoundException; - - /** - * Load {@link Properties} from a file. Returns an empty property set on error. - * - * @param file A non-null file to load from. File may not exist. - * @return A new {@link Properties} with the properties loaded from the file, - * or an empty property set in case of error. - */ - @NonNull - Properties loadProperties(@NonNull File file); - - /** - * Saves (write, store) the given {@link Properties} into the given {@link File}. - * - * @param file A non-null file to write to. - * @param props The properties to write. - * @param comments A non-null description of the properly list, written in the file. - * @throws IOException if the write operation failed. - */ - void saveProperties( - @NonNull File file, - @NonNull Properties props, - @NonNull String comments) throws IOException; - - /** - * Returns the lastModified attribute of the file. - * - * @see File#lastModified() - * @param file The non-null file of which to retrieve the lastModified attribute. - * @return The last-modified attribute of the file, in milliseconds since The Epoch. - */ - long lastModified(@NonNull File file); -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/io/LegacyFileOp.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/io/LegacyFileOp.java deleted file mode 100644 index 95b9ff72..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/io/LegacyFileOp.java +++ /dev/null @@ -1,488 +0,0 @@ -/* - * Copyright (C) 2010 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.sdklib.io; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.google.common.io.Closer; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.Properties; -import java.util.regex.Pattern; - - -/** - * Wraps some common {@link File} operations on files and folders. - *

- * This makes it possible to override/mock/stub some file operations in unit tests. - */ -public class LegacyFileOp implements IFileOp { - - public static final File[] EMPTY_FILE_ARRAY = new File[0]; - - /** - * Reflection method for File.setExecutable(boolean, boolean). Only present in Java 6. - */ - private static Method sFileSetExecutable = null; - - /** - * Parameters to call File.setExecutable through reflection. - */ - private static final Object[] sFileSetExecutableParams = new Object[] { - Boolean.TRUE, Boolean.FALSE }; - - // static initialization of sFileSetExecutable. - static { - try { - sFileSetExecutable = File.class.getMethod("setExecutable", //$NON-NLS-1$ - boolean.class, boolean.class); - - } catch (SecurityException e) { - // do nothing we'll use chmod instead - } catch (NoSuchMethodException e) { - // do nothing we'll use chmod instead - } - } - - /** - * Appends the given {@code segments} to the {@code base} file. - * - * @param base A base file, non-null. - * @param segments Individual folder or filename segments to append to the base file. - * @return A new file representing the concatenation of the base path with all the segments. - */ - public static File append(@NonNull File base, @NonNull String...segments) { - for (String segment : segments) { - base = new File(base, segment); - } - return base; - } - - /** - * Appends the given {@code segments} to the {@code base} file. - * - * @param base A base file path, non-empty and non-null. - * @param segments Individual folder or filename segments to append to the base path. - * @return A new file representing the concatenation of the base path with all the segments. - */ - public static File append(@NonNull String base, @NonNull String...segments) { - return append(new File(base), segments); - } - - /** - * Helper to delete a file or a directory. - * For a directory, recursively deletes all of its content. - * Files that cannot be deleted right away are marked for deletion on exit. - * It's ok for the file or folder to not exist at all. - * The argument can be null. - */ - @Override - public void deleteFileOrFolder(@NonNull File fileOrFolder) { - if (fileOrFolder != null) { - if (isDirectory(fileOrFolder)) { - // Must delete content recursively first - File[] files = fileOrFolder.listFiles(); - if (files != null) { - for (File item : files) { - deleteFileOrFolder(item); - } - } - } - - // Don't try to delete it if it doesn't exist. - if (!exists(fileOrFolder)) { - return; - } - - if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) { - // Trying to delete a resource on windows might fail if there's a file - // indexer locking the resource. Generally retrying will be enough to - // make it work. - // - // Try for half a second before giving up. - - for (int i = 0; i < 5; i++) { - if (fileOrFolder.delete()) { - return; - } - - try { - Thread.sleep(100 /*ms*/); - } catch (InterruptedException e) { - // Ignore. - } - } - - fileOrFolder.deleteOnExit(); - - } else { - // On Linux or Mac, just straight deleting it should just work. - - if (!fileOrFolder.delete()) { - fileOrFolder.deleteOnExit(); - } - } - } - } - - /** - * Sets the executable Unix permission (+x) on a file or folder. - *

- * This attempts to use File#setExecutable through reflection if - * it's available. - * If this is not available, this invokes a chmod exec instead, - * so there is no guarantee of it being fast. - *

- * Caller must make sure to not invoke this under Windows. - * - * @param file The file to set permissions on. - * @throws IOException If an I/O error occurs - */ - @Override - public void setExecutablePermission(@NonNull File file) throws IOException { - - if (sFileSetExecutable != null) { - try { - sFileSetExecutable.invoke(file, sFileSetExecutableParams); - return; - } catch (IllegalArgumentException e) { - // we'll run chmod below - } catch (IllegalAccessException e) { - // we'll run chmod below - } catch (InvocationTargetException e) { - // we'll run chmod below - } - } - - Runtime.getRuntime().exec(new String[] { - "chmod", "+x", file.getAbsolutePath() //$NON-NLS-1$ //$NON-NLS-2$ - }); - } - - @Override - public void setReadOnly(@NonNull File file) { - file.setReadOnly(); - } - - /** - * Copies a binary file. - * - * @param source the source file to copy. - * @param dest the destination file to write. - * @throws FileNotFoundException if the source file doesn't exist. - * @throws IOException if there's a problem reading or writing the file. - */ - @Override - public void copyFile(@NonNull File source, @NonNull File dest) throws IOException { - byte[] buffer = new byte[8192]; - - FileInputStream fis = null; - FileOutputStream fos = null; - try { - fis = new FileInputStream(source); - fos = new FileOutputStream(dest); - - int read; - while ((read = fis.read(buffer)) != -1) { - fos.write(buffer, 0, read); - } - - } finally { - if (fis != null) { - try { - fis.close(); - } catch (IOException e) { - // Ignore. - } - } - if (fos != null) { - try { - fos.close(); - } catch (IOException e) { - // Ignore. - } - } - } - } - - /** - * Checks whether 2 binary files are the same. - * - * @param file1 the source file to copy - * @param file2 the destination file to write - * @throws FileNotFoundException if the source files don't exist. - * @throws IOException if there's a problem reading the files. - */ - @Override - public boolean isSameFile(@NonNull File file1, @NonNull File file2) throws IOException { - - if (file1.length() != file2.length()) { - return false; - } - - FileInputStream fis1 = null; - FileInputStream fis2 = null; - - try { - fis1 = new FileInputStream(file1); - fis2 = new FileInputStream(file2); - - byte[] buffer1 = new byte[8192]; - byte[] buffer2 = new byte[8192]; - - int read1; - while ((read1 = fis1.read(buffer1)) != -1) { - int read2 = 0; - while (read2 < read1) { - int n = fis2.read(buffer2, read2, read1 - read2); - if (n == -1) { - break; - } - } - - if (read2 != read1) { - return false; - } - - if (!Arrays.equals(buffer1, buffer2)) { - return false; - } - } - } finally { - if (fis2 != null) { - try { - fis2.close(); - } catch (IOException e) { - // ignore - } - } - if (fis1 != null) { - try { - fis1.close(); - } catch (IOException e) { - // ignore - } - } - } - - return true; - } - - /** Invokes {@link File#isFile()} on the given {@code file}. */ - @Override - public boolean isFile(@NonNull File file) { - return file.isFile(); - } - - /** Invokes {@link File#isDirectory()} on the given {@code file}. */ - @Override - public boolean isDirectory(@NonNull File file) { - return file.isDirectory(); - } - - /** Invokes {@link File#exists()} on the given {@code file}. */ - @Override - public boolean exists(@NonNull File file) { - return file.exists(); - } - - /** Invokes {@link File#length()} on the given {@code file}. */ - @Override - public long length(@NonNull File file) { - return file.length(); - } - - /** - * Invokes {@link File#delete()} on the given {@code file}. - * Note: for a recursive folder version, consider {@link #deleteFileOrFolder(File)}. - */ - @Override - public boolean delete(@NonNull File file) { - return file.delete(); - } - - /** Invokes {@link File#mkdirs()} on the given {@code file}. */ - @Override - public boolean mkdirs(@NonNull File file) { - return file.mkdirs(); - } - - /** - * Invokes {@link File#listFiles()} on the given {@code file}. - * Contrary to the Java API, this returns an empty array instead of null when the - * directory does not exist. - */ - @Override - @NonNull - public File[] listFiles(@NonNull File file) { - File[] r = file.listFiles(); - if (r == null) { - return EMPTY_FILE_ARRAY; - } else { - return r; - } - } - - /** Invokes {@link File#renameTo(File)} on the given files. */ - @Override - public boolean renameTo(@NonNull File oldFile, @NonNull File newFile) { - return oldFile.renameTo(newFile); - } - - /** Creates a new {@link OutputStream} for the given {@code file}. */ - @Override - @NonNull - public OutputStream newFileOutputStream(@NonNull File file) throws FileNotFoundException { - return new FileOutputStream(file); - } - - /** Creates a new {@link InputStream} for the given {@code file}. */ - @Override - @NonNull - public InputStream newFileInputStream(@NonNull File file) throws FileNotFoundException { - return new FileInputStream(file); - } - - @Override - @NonNull - public Properties loadProperties(@NonNull File file) { - Properties props = new Properties(); - Closer closer = Closer.create(); - try { - FileInputStream fis = closer.register(new FileInputStream(file)); - props.load(fis); - } catch (IOException ignore) { - } finally { - try { - closer.close(); - } catch (IOException e) { - } - } - return props; - } - - @Override - public void saveProperties( - @NonNull File file, - @NonNull Properties props, - @NonNull String comments) throws IOException { - Closer closer = Closer.create(); - try { - OutputStream fos = closer.register(newFileOutputStream(file)); - props.store(fos, comments); - } catch (Throwable e) { - throw closer.rethrow(e); - } finally { - closer.close(); - } - } - - @Override - public long lastModified(@NonNull File file) { - return file.lastModified(); - } - - /** - * Computes a relative path from "toBeRelative" relative to "baseDir". - * - * Rule: - * - let relative2 = makeRelative(path1, path2) - * - then pathJoin(path1 + relative2) == path2 after canonicalization. - * - * Principle: - * - let base = /c1/c2.../cN/a1/a2../aN - * - let toBeRelative = /c1/c2.../cN/b1/b2../bN - * - result is removes the common paths, goes back from aN to cN then to bN: - * - result = ../..../../1/b2../bN - * - * @param baseDir The base directory to be relative to. - * @param toBeRelative The file or directory to make relative to the base. - * @return A path that makes toBeRelative relative to baseDir. - * @throws IOException If drive letters don't match on Windows or path canonicalization fails. - */ - @NonNull - public static String makeRelative(@NonNull File baseDir, @NonNull File toBeRelative) - throws IOException { - return makeRelativeImpl( - baseDir.getCanonicalPath(), - toBeRelative.getCanonicalPath(), - SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS, - File.separator); - } - - /** - * Implementation detail of makeRelative to make it testable - * Independently of the platform. - */ - @NonNull - static String makeRelativeImpl(@NonNull String path1, - @NonNull String path2, - boolean isWindows, - @NonNull String dirSeparator) - throws IOException { - if (isWindows) { - // Check whether both path are on the same drive letter, if any. - String p1 = path1; - String p2 = path2; - char drive1 = (p1.length() >= 2 && p1.charAt(1) == ':') ? p1.charAt(0) : 0; - char drive2 = (p2.length() >= 2 && p2.charAt(1) == ':') ? p2.charAt(0) : 0; - if (drive1 != drive2) { - // Either a mix of UNC vs drive or not the same drives. - throw new IOException("makeRelative: incompatible drive letters"); - } - } - - String[] segments1 = path1.split(Pattern.quote(dirSeparator)); - String[] segments2 = path2.split(Pattern.quote(dirSeparator)); - - int len1 = segments1.length; - int len2 = segments2.length; - int len = Math.min(len1, len2); - int start = 0; - for (; start < len; start++) { - // On Windows should compare in case-insensitive. - // Mac & Linux file systems can be both type, although their default - // is generally to have a case-sensitive file system. - if (( isWindows && !segments1[start].equalsIgnoreCase(segments2[start])) || - (!isWindows && !segments1[start].equals(segments2[start]))) { - break; - } - } - - StringBuilder result = new StringBuilder(); - for (int i = start; i < len1; i++) { - result.append("..").append(dirSeparator); - } - while (start < len2) { - result.append(segments2[start]); - if (++start < len2) { - result.append(dirSeparator); - } - } - - return result.toString(); - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/AddonManifestIniProps.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/AddonManifestIniProps.java deleted file mode 100644 index de82b2e8..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/AddonManifestIniProps.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (C) 2014 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.sdklib.repository; - -/** - * This class describes the properties that can appear in an add-on's manifest.ini file. - *

- * These constants are public and part of the SDK Manager public API. - * Once published we can't change them arbitrarily since various parts - * of our build process depend on them. - */ -public class AddonManifestIniProps { - - /** - * The display name of the add-on. Always present.
- * In source.properties, this matches {@link PkgProps#ADDON_NAME_DISPLAY}. - */ - public static final String ADDON_NAME = "name"; //$NON-NLS-1$ - - /** - * The optional "name id" of the add-on.
- * In source.properties, this matches {@link PkgProps#ADDON_NAME_ID}. - *

- * Historically the manifest used to have only a 'name' property for both internal unique id - * and display, in which case the internal id was synthesized using the display name and - * matching a {@code [a-zA-Z0-9_-]+} pattern (see {@code Addonpackage#sanitizeDisplayToNameId} - * for details.) - */ - public static final String ADDON_NAME_ID = "name-id"; //$NON-NLS-1$ - - /** - * The display vendor of the add-on. Always present.
- * In source.properties, this matches {@link PkgProps#ADDON_VENDOR_DISPLAY}. - */ - public static final String ADDON_VENDOR = "vendor"; //$NON-NLS-1$ - - /** - * The optional vendor id of the add-on.
- * In source.properties, this matches {@link PkgProps#ADDON_VENDOR_ID}. - *

- * Historically the manifest used to have only a 'vendor' property for both internal unique id - * and display, in which case the internal id was synthesized using the display name and - * matching a {@code [a-zA-Z0-9_-]+} pattern (see {@code Addonpackage#sanitizeDisplayToNameId} - * for details.) - */ - public static final String ADDON_VENDOR_ID = "vendor-id"; //$NON-NLS-1$ - - /** - * The free description string of the add-on.
- * Not saved in source.properties. - */ - public static final String ADDON_DESCRIPTION = "description"; //$NON-NLS-1$ - - /** - * The revision of the add-on.
- * In source.properties, this matches {@link PkgProps#PKG_REVISION}. - */ - public static final String ADDON_REVISION = "revision"; //$NON-NLS-1$ - - /** - * An older/obsolete attribute for the revision of the add-on.
- * The name was changed as it is ambiguous (platform version vs platform revision.) - */ - public static final String ADDON_REVISION_OLD = "version"; //$NON-NLS-1$ - - /** - * The API level of the add-on, always an integer.
- * Note: add-ons do not currently support API codenames.
- * In source.properties, this matches {@link PkgProps#VERSION_API_LEVEL}. - */ - public static final String ADDON_API = "api"; //$NON-NLS-1$ - - /** - * The list of libraries of the add-on.
- * This is a string in the format "java.package1;java.package2;...java.packageN". - * For each library's java package name, the manifest.ini contains a key with - * value "library.jar;Jar Description String". Example: - *

-     * libraries=com.example.foo;com.example.bar
-     * com.example.foo=foo.jar;Foo Library
-     * com.example.bar=bar.jar;Bar Library
-     * 
- * Not saved in source.properties. - */ - public static final String ADDON_LIBRARIES = "libraries"; //$NON-NLS-1$ - - /** - * An optional default skin string of the add-on.
- * Not saved in source.properties. - */ - public static final String ADDON_DEFAULT_SKIN = "skin"; //$NON-NLS-1$ - - /** - * An optional USB vendor string for the add-on.
- * Not saved in source.properties. - */ - public static final String ADDON_USB_VENDOR = "usb-vendor"; //$NON-NLS-1$ - -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/FullRevision.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/FullRevision.java deleted file mode 100644 index e71499f8..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/FullRevision.java +++ /dev/null @@ -1,426 +0,0 @@ -/* - * Copyright (C) 2012 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.sdklib.repository; - -import com.android.annotations.NonNull; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - - -/** - * Package multi-part revision number composed of a tuple - * (major.minor.micro) and an optional preview revision - * (the lack of a preview number indicates it's not a preview - * but a final package.) - * - * @see MajorRevision - */ -public class FullRevision implements Comparable { - - public static final int MISSING_MAJOR_REV = 0; - public static final int IMPLICIT_MINOR_REV = 0; - public static final int IMPLICIT_MICRO_REV = 0; - public static final int NOT_A_PREVIEW = 0; - - /** Only major revision specified: 1 term */ - protected static final int PRECISION_MAJOR = 1; - /** Only major and minor revisions specified: 2 terms (x.y) */ - protected static final int PRECISION_MINOR = 2; - /** Major, minor and micro revisions specified: 3 terms (x.y.z) */ - protected static final int PRECISION_MICRO = 3; - /** Major, minor, micro and preview revisions specified: 4 terms (x.y.z-rcN) */ - protected static final int PRECISION_PREVIEW = 4; - - public static final FullRevision NOT_SPECIFIED = new FullRevision(MISSING_MAJOR_REV); - - private static final Pattern FULL_REVISION_PATTERN = - // 1=major 2=minor 3=micro 4=separator 5=previewType 6=preview - Pattern.compile("\\s*([0-9]+)(?:\\.([0-9]+)(?:\\.([0-9]+))?)?([\\s-]*)?(?:(rc|alpha|beta)([0-9]+))?\\s*"); - - protected static final String DEFAULT_SEPARATOR = " "; - - private final int mMajor; - private final int mMinor; - private final int mMicro; - private final int mPreview; - private final String mPreviewSeparator; - private final PreviewType mPreviewType; - - public enum PreviewType { - ALPHA("alpha"), - BETA("beta"), - RC("rc") - ; - - final String name; - - PreviewType(String name) { - this.name = name; - } - } - - public FullRevision(int major) { - this(major, IMPLICIT_MINOR_REV, IMPLICIT_MICRO_REV); - } - - public FullRevision(int major, int minor, int micro) { - this(major, minor, micro, NOT_A_PREVIEW); - } - - public FullRevision(int major, int minor, int micro, int preview) { - this(major, minor, micro, PreviewType.RC, preview, DEFAULT_SEPARATOR); - } - - public FullRevision(int major, int minor, int micro, @NonNull PreviewType previewType, - int preview, @NonNull String previewSeparator) { - mMajor = major; - mMinor = minor; - mMicro = micro; - mPreview = preview; - mPreviewSeparator = previewSeparator; - mPreviewType = previewType; - } - - public int getMajor() { - return mMajor; - } - - public int getMinor() { - return mMinor; - } - - public int getMicro() { - return mMicro; - } - - @NonNull - protected String getSeparator() { - return mPreviewSeparator; - } - - public boolean isPreview() { - return mPreview > NOT_A_PREVIEW; - } - - public int getPreview() { - return mPreview; - } - - /** - * Parses a string of format "major.minor.micro rcPreview" and returns - * a new {@link FullRevision} for it. All the fields except major are - * optional. - *

- * The parsing is equivalent to the pseudo-BNF/regexp: - *

-     *   Major/Minor/Micro/Preview := [0-9]+
-     *   Revision := Major ('.' Minor ('.' Micro)? )? \s* ('rc'Preview)?
-     * 
- * - * @param revision A non-null revision to parse. - * @return A new non-null {@link FullRevision}. - * @throws NumberFormatException if the parsing failed. - */ - @NonNull - public static FullRevision parseRevision(@NonNull String revision) - throws NumberFormatException { - return parseRevisionImpl(revision, true /*supportMinorMicro*/, true /*supportPreview*/, - false /*keepPrevision*/); - } - - @NonNull - protected static FullRevision parseRevisionImpl(@NonNull String revision, - boolean supportMinorMicro, - boolean supportPreview, - boolean keepPrecision) - throws NumberFormatException { - if (revision == null) { - throw new NumberFormatException("revision is "); //$NON-NLS-1$ - } - - Throwable cause = null; - String error = null; - try { - Matcher m = FULL_REVISION_PATTERN.matcher(revision); - if (m != null && m.matches()) { - int major = Integer.parseInt(m.group(1)); - - int minor = IMPLICIT_MINOR_REV; - int micro = IMPLICIT_MICRO_REV; - int preview = NOT_A_PREVIEW; - int precision = PRECISION_MAJOR; - String previewSeparator = " "; - PreviewType previewType = PreviewType.RC; - - String s = m.group(2); - if (s != null) { - if (!supportMinorMicro) { - error = " -- Minor number not supported"; //$NON-NLS-1$ - } else { - minor = Integer.parseInt(s); - precision = PRECISION_MINOR; - } - } - - s = m.group(3); - if (s != null) { - if (!supportMinorMicro) { - error = " -- Micro number not supported"; //$NON-NLS-1$ - } else { - micro = Integer.parseInt(s); - precision = PRECISION_MICRO; - } - } - - s = m.group(6); - if (s != null) { - if (!supportPreview) { - error = " -- Preview number not supported"; //$NON-NLS-1$ - } else { - preview = Integer.parseInt(s); - previewSeparator = m.group(4); - precision = PRECISION_PREVIEW; - - String previewTypeName = m.group(5); - for (PreviewType pt : PreviewType.values()) { - if (pt.name.equals(previewTypeName)) { - previewType = pt; - break; - } - } - } - } - - if (error == null) { - if (keepPrecision) { - return new PreciseRevision(major, minor, micro, preview, precision, - previewSeparator); - } else { - return new FullRevision(major, minor, micro, previewType, preview, previewSeparator); - } - } - } - } catch (Throwable t) { - cause = t; - } - - NumberFormatException n = new NumberFormatException( - "Invalid revision: " //$NON-NLS-1$ - + revision - + (error == null ? "" : error)); - if (cause != null) { - n.initCause(cause); - } - throw n; - } - - /** - * Returns the version in a fixed format major.minor.micro - * with an optional "rc preview#". For example it would - * return "18.0.0", "18.1.0" or "18.1.2 rc5". - */ - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append(mMajor) - .append('.').append(mMinor) - .append('.').append(mMicro); - - if (mPreview != NOT_A_PREVIEW) { - sb.append(mPreviewSeparator).append(mPreviewType.name).append(mPreview); - } - - return sb.toString(); - } - - /** - * Returns the version in a dynamic format "major.minor.micro rc#". - * This is similar to {@link #toString()} except it omits minor, micro - * or preview versions when they are zero. - * For example it would return "18 rc1" instead of "18.0.0 rc1", - * or "18.1 rc2" instead of "18.1.0 rc2". - */ - public String toShortString() { - StringBuilder sb = new StringBuilder(); - sb.append(mMajor); - if (mMinor > 0 || mMicro > 0) { - sb.append('.').append(mMinor); - } - if (mMicro > 0) { - sb.append('.').append(mMicro); - } - if (mPreview != NOT_A_PREVIEW) { - sb.append(mPreviewSeparator).append(mPreviewType.name).append(mPreview); - } - - return sb.toString(); - } - - /** - * Returns the version number as an integer array, in the form - * [major, minor, micro] or [major, minor, micro, preview]. - * - * This is useful to initialize an instance of - * {@code org.apache.tools.ant.util.DeweyDecimal} using a - * {@link FullRevision}. - * - * @param includePreview If true the output will contain 4 fields - * to include the preview number (even if 0.) If false the output - * will contain only 3 fields (major, minor and micro.) - * @return A new int array, never null, with either 3 or 4 fields. - */ - public int[] toIntArray(boolean includePreview) { - int size = includePreview ? 4 : 3; - int[] result = new int[size]; - result[0] = mMajor; - result[1] = mMinor; - result[2] = mMicro; - if (result.length > 3) { - result[3] = mPreview; - } - return result; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + mMajor; - result = prime * result + mMinor; - result = prime * result + mMicro; - result = prime * result + mPreview; - result = prime * result + mPreviewType.hashCode(); - return result; - } - - @Override - public boolean equals(Object rhs) { - if (this == rhs) { - return true; - } - if (rhs == null) { - return false; - } - if (!(rhs instanceof FullRevision)) { - return false; - } - FullRevision other = (FullRevision) rhs; - if (mMajor != other.mMajor) { - return false; - } - if (mMinor != other.mMinor) { - return false; - } - if (mMicro != other.mMicro) { - return false; - } - if (mPreview != other.mPreview) { - return false; - } - if (mPreviewType != other.mPreviewType) { - return false; - } - return true; - } - - /** - * Trivial comparison of a version, e.g 17.1.2 < 18.0.0. - * - * Note that preview/release candidate are released before their final version, - * so "18.0.0 rc1" comes below "18.0.0". The best way to think of it as if the - * lack of preview number was "+inf": - * "18.1.2 rc5" => "18.1.2.5" so its less than "18.1.2.+INF" but more than "18.1.1.0" - * and more than "18.1.2.4" - * - * @param rhs The right-hand side {@link FullRevision} to compare with. - * @return <0 if lhs < rhs; 0 if lhs==rhs; >0 if lhs > rhs. - */ - @Override - public int compareTo(FullRevision rhs) { - return compareTo(rhs, PreviewComparison.COMPARE_NUMBER); - } - - /** - * Trivial comparison of a version, e.g 17.1.2 < 18.0.0. - * - * Note that preview/release candidate are released before their final version, - * so "18.0.0 rc1" comes below "18.0.0". The best way to think of it as if the - * lack of preview number was "+inf": - * "18.1.2 rc5" => "18.1.2.5" so its less than "18.1.2.+INF" but more than "18.1.1.0" - * and more than "18.1.2.4" - * - * @param rhs The right-hand side {@link FullRevision} to compare with. - * @param comparePreview How to compare the preview value. - * @return <0 if lhs < rhs; 0 if lhs==rhs; >0 if lhs > rhs. - */ - public int compareTo(FullRevision rhs, PreviewComparison comparePreview) { - int delta = mMajor - rhs.mMajor; - if (delta != 0) { - return delta; - } - - delta = mMinor - rhs.mMinor; - if (delta != 0) { - return delta; - } - - delta = mMicro - rhs.mMicro; - if (delta != 0) { - return delta; - } - - int p1, p2; - switch (comparePreview) { - case IGNORE: - // Nothing to compare. - break; - - case COMPARE_NUMBER: - if (!mPreviewType.equals(rhs.mPreviewType)) { - return mPreviewType.compareTo(rhs.mPreviewType); - } - - p1 = mPreview == NOT_A_PREVIEW ? Integer.MAX_VALUE : mPreview; - p2 = rhs.mPreview == NOT_A_PREVIEW ? Integer.MAX_VALUE : rhs.mPreview; - delta = p1 - p2; - break; - - case COMPARE_TYPE: - p1 = mPreview == NOT_A_PREVIEW ? 1 : 0; - p2 = rhs.mPreview == NOT_A_PREVIEW ? 1 : 0; - delta = p1 - p2; - break; - } - return delta; - } - - /** Indicates how to compare the preview field in - * {@link FullRevision#compareTo(FullRevision, PreviewComparison)} */ - public enum PreviewComparison { - /** Both revisions must have exactly the same preview number. */ - COMPARE_NUMBER, - /** Both revisions must have the same preview type (both must be previews - * or both must not be previews, but the actual number is irrelevant.) - * This is the most typical choice used to find updates of the same type. */ - COMPARE_TYPE, - /** The preview field is ignored and not used in the comparison. */ - IGNORE - } - - -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/IDescription.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/IDescription.java deleted file mode 100644 index 1e44964d..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/IDescription.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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.sdklib.repository; - -/** - * Interface for elements that can provide a description of themselves. - */ -public interface IDescription { - - /** - * Returns a description of the given element. Cannot be null. - *

- * A description is a multi-line of text, typically much more - * elaborate than what {@link Object#toString()} would provide. - */ - String getShortDescription(); - - /** - * Returns a description of the given element. Cannot be null. - *

- * A description is a multi-line of text, typically much more - * elaborate than what {@link Object#toString()} would provide. - */ - String getLongDescription(); - -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/IListDescription.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/IListDescription.java deleted file mode 100644 index c3ae3653..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/IListDescription.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (C) 2013 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.sdklib.repository; - -/** - * Interface for elements that can provide a description of themselves. - */ -public interface IListDescription { - - /** - * Returns a description of this package that is suitable for a list display. - * Should not be empty. Must never be null. - *

- * Note that this is the "base" name for the package - * with no specific revision nor API mentioned. - * In contrast, {@link IDescription#getShortDescription()} should be used if you - * want more details such as the package revision number or the API, if applicable. - */ - String getListDescription(); -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/License.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/License.java deleted file mode 100644 index 7040ec11..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/License.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * 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.sdklib.repository; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.sdklib.SdkManager; -import com.android.utils.FileUtils; -import com.google.common.base.Charsets; -import com.google.common.hash.Hashing; -import com.google.common.io.Files; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; - -/** - * License text, with an optional license XML reference. - */ -public class License { - private final String mLicense; - private final String mLicenseRef; - private final String mLicenseHash; - - private static final String LICENSE_DIR = "licenses"; - - public License(@NonNull String license, @Nullable String licenseRef) { - mLicense = license; - mLicenseRef = licenseRef; - mLicenseHash = Hashing.sha1().hashBytes(mLicense.getBytes()).toString(); - } - - /** Returns the license text. Never null. */ - @NonNull - public String getLicense() { - return mLicense; - } - - /** Returns the hash of the license text. Never null. */ - @NonNull - public String getLicenseHash() { - return mLicenseHash; - } - - /** - * Returns the license XML reference. - * Could be null, e.g. in tests or synthetic packages - * recreated from local source.properties. - */ - @Nullable - public String getLicenseRef() { - return mLicenseRef; - } - - /** - * Returns a string representation of the license, useful for debugging. - * This is not designed to be shown to the user. - */ - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append(""); - return sb.toString(); - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result - + ((mLicense == null) ? 0 : mLicense.hashCode()); - result = prime * result - + ((mLicenseRef == null) ? 0 : mLicenseRef.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (!(obj instanceof License)) { - return false; - } - License other = (License) obj; - if (mLicense == null) { - if (other.mLicense != null) { - return false; - } - } else if (!mLicense.equals(other.mLicense)) { - return false; - } - if (mLicenseRef == null) { - if (other.mLicenseRef != null) { - return false; - } - } else if (!mLicenseRef.equals(other.mLicenseRef)) { - return false; - } - return true; - } - - /** - * Checks whether this license has previously been accepted. - * @param sdkRoot The root directory of the Android SDK - * @return true if this license has already been accepted - */ - public boolean checkAccepted(@Nullable File sdkRoot) { - if (sdkRoot == null) { - return false; - } - File licenseDir = new File(sdkRoot, LICENSE_DIR); - File licenseFile = new File(licenseDir, mLicenseRef == null ? mLicenseHash : mLicenseRef); - if (!licenseFile.exists()) { - return false; - } - try { - String hash = Files.readFirstLine(licenseFile, Charsets.UTF_8); - return hash.equals(mLicenseHash); - } catch (IOException e) { - return false; - } - } - - /** - * Marks this license as accepted. - * - * @param sdkRoot The root directory of the Android SDK - * @return true if the acceptance was persisted successfully. - */ - public boolean setAccepted(@Nullable File sdkRoot) { - if (sdkRoot == null) { - return false; - } - if (checkAccepted(sdkRoot)) { - return true; - } - File licenseDir = new File(sdkRoot, LICENSE_DIR); - if (licenseDir.exists() && !licenseDir.isDirectory()) { - return false; - } - if (!licenseDir.exists()) { - licenseDir.mkdir(); - } - File licenseFile = new File(licenseDir, mLicenseRef == null ? mLicenseHash : mLicenseRef); - try { - Files.write(mLicenseHash, licenseFile, Charsets.UTF_8); - } - catch (IOException e) { - return false; - } - return true; - } -} - diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/MajorRevision.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/MajorRevision.java deleted file mode 100644 index b6d25573..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/MajorRevision.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2012 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.sdklib.repository; - -import com.android.annotations.NonNull; - - -/** - * Package revision number composed of a single major revision. - *

- * Contrary to a {@link FullRevision}, a {@link MajorRevision} does not - * provide minor, micro and preview revision numbers -- these are all - * set to zero. - */ -public class MajorRevision extends FullRevision { - - public MajorRevision(FullRevision fullRevision) { - super(fullRevision.getMajor(), IMPLICIT_MINOR_REV, IMPLICIT_MICRO_REV); - } - - public MajorRevision(int major) { - super(major, IMPLICIT_MINOR_REV, IMPLICIT_MICRO_REV); - } - - @Override - public String toString() { - return super.toShortString(); - } - - /** - * Parses a single-integer string and returns a new {@link MajorRevision} for it. - * - * @param revision A non-null revision to parse. - * @return A new non-null {@link MajorRevision}. - * @throws NumberFormatException if the parsing failed. - */ - @NonNull - public static MajorRevision parseRevision(@NonNull String revision) - throws NumberFormatException { - FullRevision r = parseRevisionImpl( - revision, false /*supportMinorMicro*/, false /*supportPreview*/, - false /*keepPrecision*/); - return new MajorRevision(r.getMajor()); - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/NoPreviewRevision.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/NoPreviewRevision.java deleted file mode 100644 index 0280cb28..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/NoPreviewRevision.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 2013 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.sdklib.repository; - -import com.android.annotations.NonNull; - - -/** - * Package multi-part revision number composed of a tuple - * (major.minor.micro) but without support for any optional preview number. - * - * @see FullRevision - */ -public class NoPreviewRevision extends FullRevision { - - public NoPreviewRevision(int major) { - this(major, IMPLICIT_MINOR_REV, IMPLICIT_MICRO_REV); - } - - public NoPreviewRevision(int major, int minor, int micro) { - super(major, minor, micro, NOT_A_PREVIEW); - } - - /** - * Parses a string of format "major.minor.micro" and returns - * a new {@link NoPreviewRevision} for it. All the fields except major are - * optional. - *

- * The parsing is equivalent to the pseudo-BNF/regexp: - *

-     *   Major/Minor/Micro/Preview := [0-9]+
-     *   Revision := Major ('.' Minor ('.' Micro)? )? \s*
-     * 
- * - * @param revision A non-null revision to parse. - * @return A new non-null {@link NoPreviewRevision}. - * @throws NumberFormatException if the parsing failed. - */ - @NonNull - public static NoPreviewRevision parseRevision(@NonNull String revision) - throws NumberFormatException { - FullRevision r = parseRevisionImpl( - revision, true /*supportMinorMicro*/, false /*supportPreview*/, - false /*keepPrecision*/); - return new NoPreviewRevision(r.getMajor(), r.getMinor(), r.getMicro()); - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/PkgProps.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/PkgProps.java deleted file mode 100644 index 3126984f..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/PkgProps.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (C) 2011 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.sdklib.repository; - - - -/** - * Public constants used by the repository when saving {@code source.properties} - * files in local packages. - *

- * These constants are public and part of the SDK Manager public API. - * Once published we can't change them arbitrarily since various parts - * of our build process depend on them. - */ -public class PkgProps { - - // Base Package - public static final String PKG_REVISION = "Pkg.Revision"; //$NON-NLS-1$ - public static final String PKG_LICENSE = "Pkg.License"; //$NON-NLS-1$ - public static final String PKG_LICENSE_REF = "Pkg.LicenseRef"; //$NON-NLS-1$ - public static final String PKG_DESC = "Pkg.Desc"; //$NON-NLS-1$ - public static final String PKG_DESC_URL = "Pkg.DescUrl"; //$NON-NLS-1$ - public static final String PKG_RELEASE_NOTE = "Pkg.RelNote"; //$NON-NLS-1$ - public static final String PKG_RELEASE_URL = "Pkg.RelNoteUrl"; //$NON-NLS-1$ - public static final String PKG_SOURCE_URL = "Pkg.SourceUrl"; //$NON-NLS-1$ - public static final String PKG_OBSOLETE = "Pkg.Obsolete"; //$NON-NLS-1$ - public static final String PKG_LIST_DISPLAY = "Pkg.ListDisplay"; //$NON-NLS-1$ - - // AndroidVersion - - public static final String VERSION_API_LEVEL = "AndroidVersion.ApiLevel";//$NON-NLS-1$ - /** Code name of the platform if the platform is not final */ - public static final String VERSION_CODENAME = "AndroidVersion.CodeName";//$NON-NLS-1$ - - - // AddonPackage - - public static final String ADDON_NAME = "Addon.Name"; //$NON-NLS-1$ - public static final String ADDON_NAME_ID = "Addon.NameId"; //$NON-NLS-1$ - public static final String ADDON_NAME_DISPLAY = "Addon.NameDisplay"; //$NON-NLS-1$ - - public static final String ADDON_VENDOR = "Addon.Vendor"; //$NON-NLS-1$ - public static final String ADDON_VENDOR_ID = "Addon.VendorId"; //$NON-NLS-1$ - public static final String ADDON_VENDOR_DISPLAY = "Addon.VendorDisplay"; //$NON-NLS-1$ - - // DocPackage - - // ExtraPackage - - public static final String EXTRA_PATH = "Extra.Path"; //$NON-NLS-1$ - public static final String EXTRA_OLD_PATHS = "Extra.OldPaths"; //$NON-NLS-1$ - public static final String EXTRA_MIN_API_LEVEL = "Extra.MinApiLevel"; //$NON-NLS-1$ - public static final String EXTRA_PROJECT_FILES = "Extra.ProjectFiles"; //$NON-NLS-1$ - public static final String EXTRA_VENDOR = "Extra.Vendor"; //$NON-NLS-1$ - public static final String EXTRA_VENDOR_ID = "Extra.VendorId"; //$NON-NLS-1$ - public static final String EXTRA_VENDOR_DISPLAY = "Extra.VendorDisplay"; //$NON-NLS-1$ - public static final String EXTRA_NAME_DISPLAY = "Extra.NameDisplay"; //$NON-NLS-1$ - - // ILayoutlibVersion - - public static final String LAYOUTLIB_API = "Layoutlib.Api"; //$NON-NLS-1$ - public static final String LAYOUTLIB_REV = "Layoutlib.Revision"; //$NON-NLS-1$ - - // MinToolsPackage - - public static final String MIN_TOOLS_REV = "Platform.MinToolsRev"; //$NON-NLS-1$ - - // PlatformPackage - - public static final String PLATFORM_VERSION = "Platform.Version"; //$NON-NLS-1$ - /** Code name of the platform. This has no bearing on the package being a preview or not. */ - public static final String PLATFORM_CODENAME = "Platform.CodeName"; //$NON-NLS-1$ - public static final String PLATFORM_INCLUDED_ABI = "Platform.Included.Abi"; //$NON-NLS-1$ - - // ToolPackage - - public static final String MIN_PLATFORM_TOOLS_REV = "Platform.MinPlatformToolsRev";//$NON-NLS-1$ - public static final String MIN_BUILD_TOOLS_REV = "Platform.MinBuildToolsRev"; //$NON-NLS-1$ - - - // SamplePackage - - public static final String SAMPLE_MIN_API_LEVEL = "Sample.MinApiLevel"; //$NON-NLS-1$ - - // SystemImagePackage - - public static final String SYS_IMG_ABI = "SystemImage.Abi"; //$NON-NLS-1$ - public static final String SYS_IMG_TAG_ID = "SystemImage.TagId"; //$NON-NLS-1$ - public static final String SYS_IMG_TAG_DISPLAY = "SystemImage.TagDisplay"; //$NON-NLS-1$ -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/PreciseRevision.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/PreciseRevision.java deleted file mode 100644 index 81e97443..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/PreciseRevision.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright (C) 2014 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.sdklib.repository; - -import com.android.annotations.NonNull; - -/** - * A {@link FullRevision} which distinguishes between x and x.0, x.0.0, x.y.0, etc; it basically - * keeps track of the precision of the revision string. - *

- * This is vital when referencing Gradle artifact numbers, - * since versions x.y.0 and version x.y are not the same. - */ -public class PreciseRevision extends FullRevision { - private final int mPrecision; - - /** - * Parses a string of format "major.minor.micro rcPreview" and returns - * a new {@link com.android.sdklib.repository.PreciseRevision} for it. - * - * All the fields except major are optional. - *

- * @param revision A non-null revision to parse. - * @return A new non-null {@link com.android.sdklib.repository.PreciseRevision}. - * @throws NumberFormatException if the parsing failed. - */ - @NonNull - public static PreciseRevision parseRevision(@NonNull String revision) - throws NumberFormatException { - return (PreciseRevision) parseRevisionImpl(revision, true /*supportMinorMicro*/, - true /*supportPreview*/, true /*keepPrevision*/); - } - - public PreciseRevision(int major) { - this(major, IMPLICIT_MINOR_REV, IMPLICIT_MICRO_REV, NOT_A_PREVIEW, PRECISION_MAJOR, - DEFAULT_SEPARATOR); - } - - public PreciseRevision(int major, int minor) { - this(major, minor, IMPLICIT_MICRO_REV, NOT_A_PREVIEW, PRECISION_MINOR, DEFAULT_SEPARATOR); - } - - public PreciseRevision(int major, int minor, int micro) { - this(major, minor, micro, NOT_A_PREVIEW, PRECISION_MICRO, DEFAULT_SEPARATOR); - } - - public PreciseRevision(int major, int minor, int micro, int preview) { - this(major, minor, micro, preview, PRECISION_PREVIEW, DEFAULT_SEPARATOR); - } - - PreciseRevision(int major, int minor, int micro, int preview, int precision, - String separator) { - this(major, minor, micro, preview, precision, separator, PreviewType.RC); - } - - PreciseRevision(int major, int minor, int micro, int preview, int precision, - String separator, FullRevision.PreviewType previewType) { - super(major, minor, micro, previewType, preview, separator); - mPrecision = precision; - } - - /** - * Returns the version in a fixed format major.minor.micro - * with an optional "rc preview#". For example it would - * return "18.0.0", "18.1.0" or "18.1.2 rc5". - */ - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append(getMajor()); - - if (mPrecision >= PRECISION_MINOR) { - sb.append('.').append(getMinor()); - if (mPrecision >= PRECISION_MICRO) { - sb.append('.').append(getMicro()); - if (mPrecision >= PRECISION_PREVIEW && isPreview()) { - sb.append(getSeparator()).append("rc").append(getPreview()); - } - } - } - - return sb.toString(); - } - - @Override - public String toShortString() { - return toString(); - } - - @Override - public int[] toIntArray(boolean includePreview) { - int[] result; - if (mPrecision >= PRECISION_PREVIEW) { - if (includePreview) { - result = new int[mPrecision]; - result[3] = getPreview(); - } else { - result = new int[mPrecision - 1]; - } - } else { - result = new int[mPrecision]; - } - result[0] = getMajor(); - if (mPrecision >= PRECISION_MINOR) { - result[1] = getMinor(); - if (mPrecision >= PRECISION_MICRO) { - result[2] = getMicro(); - } - } - - return result; - } - - @Override - public int hashCode() { - return 31 * super.hashCode() + mPrecision; - } - - @Override - public boolean equals(Object rhs) { - boolean equals = super.equals(rhs); - if (equals) { - if (!(rhs instanceof PreciseRevision)) { - return false; - } - PreciseRevision other = (PreciseRevision) rhs; - return mPrecision == other.mPrecision; - } - return false; - } - - public int compareTo(PreciseRevision rhs, PreviewComparison comparePreview) { - int delta = super.compareTo(rhs, comparePreview); - if (delta == 0) { - return mPrecision - rhs.mPrecision; - } - return delta; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/README.txt b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/README.txt deleted file mode 100644 index e6e0f633..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/README.txt +++ /dev/null @@ -1,25 +0,0 @@ -This directory contains the XML Schemas (XSD) used by the Android SDK Repository. - -The repository exports all the packages that compose the SDK as well as -various manifest that define what is available in the repository. -The XML schemas available here allows clients to validate the manifests. - -TODO: -- overview of schemas -- principles of design -- principles of evolution vs revision numbers -- naming convention -- using by "make sdk_repo" - - -Naming Convention ------------------ - -Repository schemas are named sdk-type-N.xsd where -- type is either addon, addons-list or repository. -- N is the schema revision number, starting at 1 and increment with each revision. - -Schemas can also be named -sdk-type-N.xsd. -The dash prefix means this schema is a *future* schema that is not yet -used in production. This allows the repository to test future schemas -before they are deployed. diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/RepoConstants.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/RepoConstants.java deleted file mode 100644 index 44f6f15c..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/RepoConstants.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * 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.sdklib.repository; - -import java.io.InputStream; - - - -/** - * Public constants common to the sdk-repository and sdk-addon XML Schemas. - * @deprecated moved to studio - */ -public class RepoConstants { - - /** The license definition. */ - public static final String NODE_LICENSE = "license"; //$NON-NLS-1$ - /** The optional uses-license for all packages or for a lib. */ - public static final String NODE_USES_LICENSE = "uses-license"; //$NON-NLS-1$ - /** The revision, an int > 0, for all packages. */ - public static final String NODE_REVISION = "revision"; //$NON-NLS-1$ - /** The optional description for all packages or for a lib. */ - public static final String NODE_DESCRIPTION = "description"; //$NON-NLS-1$ - /** The optional description URL for all packages. */ - public static final String NODE_DESC_URL = "desc-url"; //$NON-NLS-1$ - /** The optional release note for all packages. */ - public static final String NODE_RELEASE_NOTE = "release-note"; //$NON-NLS-1$ - /** The optional release note URL for all packages. */ - public static final String NODE_RELEASE_URL = "release-url"; //$NON-NLS-1$ - /** The optional obsolete qualifier for all packages. */ - public static final String NODE_OBSOLETE = "obsolete"; //$NON-NLS-1$ - /** The optional project-files provided by extra packages. */ - public static final String NODE_PROJECT_FILES = "project-files"; //$NON-NLS-1$ - - /** A system-image package. */ - public static final String NODE_SYSTEM_IMAGE = "system-image"; //$NON-NLS-1$ - - /* An included-ABI element for a system-image package. */ - public static final String NODE_ABI_INCLUDED = "included-abi"; //$NON-NLS-1$ - /* An ABI element for a system-image package. */ - public static final String NODE_ABI = "abi"; //$NON-NLS-1$ - - /** The optional minimal tools revision required by platform & extra packages. */ - public static final String NODE_MIN_TOOLS_REV = "min-tools-rev"; //$NON-NLS-1$ - /** The optional minimal platform-tools revision required by tool packages. */ - public static final String NODE_MIN_PLATFORM_TOOLS_REV = "min-platform-tools-rev"; //$NON-NLS-1$ - /** The optional minimal API level required by extra packages. */ - public static final String NODE_MIN_API_LEVEL = "min-api-level"; //$NON-NLS-1$ - - /** The version, a string, for platform packages. */ - public static final String NODE_VERSION = "version"; //$NON-NLS-1$ - /** The api-level, an int > 0, for platform, add-on and doc packages. */ - public static final String NODE_API_LEVEL = "api-level"; //$NON-NLS-1$ - /** The codename, a string, for platform packages. */ - public static final String NODE_CODENAME = "codename"; //$NON-NLS-1$ - /** The *old* vendor, a string, for add-on and extra packages. - * Replaced by {@link #NODE_VENDOR_DISPLAY} and {@link #NODE_VENDOR_ID} in addon-v4.xsd. */ - public static final String NODE_VENDOR = "vendor"; //$NON-NLS-1$ - /** The vendor display string, for add-on and extra packages. */ - public static final String NODE_VENDOR_DISPLAY = "vendor-display"; //$NON-NLS-1$ - /** The unique vendor id string, for add-on and extra packages. */ - public static final String NODE_VENDOR_ID = "vendor-id"; //$NON-NLS-1$ - /** The name, a string, for add-on packages or for libraries. - * Replaced by {@link #NODE_NAME_DISPLAY} and {@link #NODE_NAME_ID} in addon-v4.xsd. */ - public static final String NODE_NAME = "name"; //$NON-NLS-1$ - /** The name display string, for add-on packages or for libraries. */ - public static final String NODE_NAME_DISPLAY = "name-display"; //$NON-NLS-1$ - /** The unique name id string, for add-on packages or for libraries. */ - public static final String NODE_NAME_ID = "name-id"; //$NON-NLS-1$ - /** The optional string used to display a package in a list view. */ - public static final String NODE_LIST_DISPLAY = "list-display"; //$NON-NLS-1$ - - /** A layoutlib package. */ - public static final String NODE_LAYOUT_LIB = "layoutlib"; //$NON-NLS-1$ - /** The API integer for a layoutlib element. */ - public static final String NODE_API = "api"; //$NON-NLS-1$ - - /** The libs container, optional for an add-on. */ - public static final String NODE_LIBS = "libs"; //$NON-NLS-1$ - /** A lib element in a libs container. */ - public static final String NODE_LIB = "lib"; //$NON-NLS-1$ - - /** The path segment, a string, for extra packages. */ - public static final String NODE_PATH = "path"; //$NON-NLS-1$ - - /** The old_path segments, a string, for extra packages. */ - public static final String NODE_OLD_PATHS = "old-paths"; //$NON-NLS-1$ - - /** The archives container, for all packages. */ - public static final String NODE_ARCHIVES = "archives"; //$NON-NLS-1$ - /** An archive element, for the archives container. */ - public static final String NODE_ARCHIVE = "archive"; //$NON-NLS-1$ - - /** An archive size, an int > 0. */ - public static final String NODE_SIZE = "size"; //$NON-NLS-1$ - /** A sha1 archive checksum, as a 40-char hex. */ - public static final String NODE_CHECKSUM = "checksum"; //$NON-NLS-1$ - /** A download archive URL, either absolute or relative to the repository xml. */ - public static final String NODE_URL = "url"; //$NON-NLS-1$ - - /** - * Optional element to indicate an archive is only suitable for the specified OS.
- * Values: windows | macosx | linux. - * @since repo-10, addon-7 and sys-img-3. - * @replaces {@link #LEGACY_ATTR_OS} - */ - public static final String NODE_HOST_OS = "host-os"; //$NON-NLS-1$ - /** - * Optional element to indicate an archive is only suitable for the specified host bit size.
- * Values: 32 | 64. - * @since repo-10, addon-7 and sys-img-3. - */ - public static final String NODE_HOST_BITS = "host-bits"; //$NON-NLS-1$ - /** - * Optional element to indicate an archive is only suitable for the specified JVM bit size.
- * Values: 32 | 64. - * @since repo-10, addon-7 and sys-img-3. - * @replaces {@link #LEGACY_ATTR_ARCH} - */ - public static final String NODE_JVM_BITS = "jvm-bits"; //$NON-NLS-1$ - /** - * Optional element to indicate an archive is only suitable for a JVM equal or greater than - * the specified value.
- * Value format: [1-9](\.[0-9]{1,2}){0,2}, e.g. "1.6", "1.7.0", "1.10" or "2" - * @since repo-10, addon-7 and sys-img-3. - */ - public static final String NODE_MIN_JVM_VERSION = "min-jvm-version"; //$NON-NLS-1$ - - - /** An archive checksum type, mandatory. */ - public static final String ATTR_TYPE = "type"; //$NON-NLS-1$ - /** - * An archive OS attribute, mandatory.
- * Use {@link #NODE_HOST_OS} instead in repo-10, addon-7 and sys-img-3. - */ - public static final String LEGACY_ATTR_OS = "os"; //$NON-NLS-1$ - /** - * An optional archive Architecture attribute.
- * Use {@link #NODE_JVM_BITS} instead in repo-10, addon-7 and sys-img-3. - */ - public static final String LEGACY_ATTR_ARCH = "arch"; //$NON-NLS-1$ - - /** A license definition ID. */ - public static final String ATTR_ID = "id"; //$NON-NLS-1$ - /** A license reference. */ - public static final String ATTR_REF = "ref"; //$NON-NLS-1$ - - - /** Type of a sha1 checksum. */ - public static final String SHA1_TYPE = "sha1"; //$NON-NLS-1$ - - /** Length of a string representing a SHA1 checksum; always 40 characters long. */ - public static final int SHA1_CHECKSUM_LEN = 40; - - /** - * Temporary folder used to hold downloads and extract archives during installation. - * This folder will be located in the SDK. - */ - public static final String FD_TEMP = "temp"; //$NON-NLS-1$ - - /** - * Returns a stream to the requested XML Schema. - * This is an internal helper. Users of the library should call - * {@link SdkRepoConstants#getXsdStream(String, int)} or - * {@link SdkAddonConstants#getXsdStream(String, int)}. - * - * @param rootElement The root of the filename of the XML schema. - * This is by convention the same as the root element declared by the schema. - * @param version The XML schema revision number, an integer >= 1. - * @return An {@link InputStream} object for the local XSD file or - * null if there is no schema for the requested version. - * @see SdkRepoConstants#getXsdStream(int) - * @see SdkAddonConstants#getXsdStream(int) - */ - protected static InputStream getXsdStream(String rootElement, int version) { - String filename = String.format("%1$s-%2$02d.xsd", rootElement, version); //$NON-NLS-1$ - - InputStream stream = null; - try { - stream = RepoConstants.class.getResourceAsStream(filename); - } catch (Exception e) { - // Some implementations seem to return null on failure, - // others throw an exception. We want to return null. - } - if (stream == null) { - // Try the alternate schemas that are not published yet. - // This allows us to internally test with new schemas before the - // public repository uses it. - filename = String.format("-%1$s-%2$02d.xsd", rootElement, version); //$NON-NLS-1$ - try { - stream = RepoConstants.class.getResourceAsStream(filename); - } catch (Exception e) { - // Some implementations seem to return null on failure, - // others throw an exception. We want to return null. - } - } - - return stream; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/RepoXsdUtil.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/RepoXsdUtil.java deleted file mode 100644 index 805fa8a9..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/RepoXsdUtil.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (C) 2015 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.sdklib.repository; - -import com.google.common.collect.Lists; - -import org.xml.sax.Attributes; -import org.xml.sax.SAXException; -import org.xml.sax.helpers.DefaultHandler; - -import java.io.BufferedReader; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.List; -import java.util.logging.Logger; - -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.parsers.SAXParserFactory; -import javax.xml.transform.stream.StreamSource; - -/** - * Utilities related to the respository XSDs. - */ -public class RepoXsdUtil { - - public static final String NODE_IMPORT = "import"; - public static final String NODE_INCLUDE = "include"; - - public static final String ATTR_SCHEMA_LOCATION = "schemaLocation"; - - - /** - * Gets StreamSources for the given xsd (implied by the name and version), as well as any xsds imported or included by the main one. - * - * @param rootElement The root of the filename of the XML schema. This is by convention the same - * as the root element declared by the schema. - * @param version The XML schema revision number, an integer >= 1. - */ - public static StreamSource[] getXsdStream(final String rootElement, int version) { - String filename = String.format("%1$s-%2$02d.xsd", rootElement, version); //$NON-NLS-1$ - final List streams = Lists.newArrayList(); - InputStream stream = null; - try { - stream = RepoXsdUtil.class.getResourceAsStream(filename); - if (stream == null) { - filename = String.format("%1$s-%2$d.xsd", rootElement, version); //$NON-NLS-1$ - stream = RepoXsdUtil.class.getResourceAsStream(filename); - } - if (stream == null) { - // Try the alternate schemas that are not published yet. - // This allows us to internally test with new schemas before the - // public repository uses it. - filename = String.format("-%1$s-%2$02d.xsd", rootElement, version); //$NON-NLS-1$ - stream = RepoXsdUtil.class.getResourceAsStream(filename); - } - - // Parse the schema and find any imports or includes so we can return them as well. - // Currently transitive includes are not supported. - SAXParserFactory.newInstance().newSAXParser().parse(stream, new DefaultHandler() { - @Override - public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException { - name = name.substring(name.indexOf(':') + 1); - if (name.equals(NODE_IMPORT) || name.equals(NODE_INCLUDE)) { - String importFile = attributes.getValue(ATTR_SCHEMA_LOCATION); - streams.add(new StreamSource(RepoXsdUtil.class.getResourceAsStream(importFile))); - } - } - }); - // create and add the first stream again, since SaxParser closes the original one - streams.add(new StreamSource(RepoXsdUtil.class.getResourceAsStream(filename))); - } catch (Exception e) { - // Some implementations seem to return null on failure, - // others throw an exception. We want to return null. - return null; - } - return streams.toArray(new StreamSource[streams.size()]); - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkAddonConstants.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkAddonConstants.java deleted file mode 100644 index 805f85b2..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkAddonConstants.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * 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.sdklib.repository; - - -import java.io.InputStream; - -/** - * Public constants for the sdk-addon XML Schema. - * @deprecated moved to studio - */ -public class SdkAddonConstants extends RepoConstants { - - /** - * The latest version of the sdk-addon XML Schema. - * Valid version numbers are between 1 and this number, included. - */ - public static final int NS_LATEST_VERSION = 7; - - /** - * The default name looked for by SdkSource when trying to load an - * sdk-addon XML if the URL doesn't match an existing resource. - */ - public static final String URL_DEFAULT_FILENAME = "addon.xml"; //$NON-NLS-1$ - - /** The base of our sdk-addon XML namespace. */ - private static final String NS_BASE = - "http://schemas.android.com/sdk/android/addon/"; //$NON-NLS-1$ - - /** - * The pattern of our sdk-addon XML namespace. - * Matcher's group(1) is the schema version (integer). - */ - public static final String NS_PATTERN = NS_BASE + "([0-9]+)"; //$NON-NLS-1$ - - /** The XML namespace of the latest sdk-addon XML. */ - public static final String NS_URI = getSchemaUri(NS_LATEST_VERSION); - - /** The root sdk-addon element */ - public static final String NODE_SDK_ADDON = "sdk-addon"; //$NON-NLS-1$ - - /** An add-on package. */ - public static final String NODE_ADD_ON = "add-on"; //$NON-NLS-1$ - - /** An extra package. */ - public static final String NODE_EXTRA = "extra"; //$NON-NLS-1$ - - /** - * List of possible nodes in a repository XML. Used to populate options automatically - * in the no-GUI mode. - */ - public static final String[] NODES = { - NODE_ADD_ON, - NODE_EXTRA - }; - - /** - * Returns a stream to the requested {@code sdk-addon} XML Schema. - * - * @param version Between 1 and {@link #NS_LATEST_VERSION}, included. - * @return An {@link InputStream} object for the local XSD file or - * null if there is no schema for the requested version. - */ - public static InputStream getXsdStream(int version) { - return getXsdStream(NODE_SDK_ADDON, version); - } - - /** - * Returns the URI of the sdk-addon schema for the given version number. - * @param version Between 1 and {@link #NS_LATEST_VERSION} included. - */ - public static String getSchemaUri(int version) { - return String.format(NS_BASE + "%d", version); //$NON-NLS-1$ - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkAddonsListConstants.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkAddonsListConstants.java deleted file mode 100644 index 3468b778..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkAddonsListConstants.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * 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.sdklib.repository; - - -import java.io.InputStream; - -/** - * Public constants for the sdk-addons-list XML Schema. - * @deprecated moved to studio - */ -public class SdkAddonsListConstants { - - /** The base of our sdk-addons-list XML namespace. */ - private static final String NS_BASE = - "http://schemas.android.com/sdk/android/addons-list/"; //$NON-NLS-1$ - - /** - * The pattern of our sdk-addons-list XML namespace. - * Matcher's group(1) is the schema version (integer). - */ - public static final String NS_PATTERN = NS_BASE + "([1-9][0-9]*)"; //$NON-NLS-1$ - - /** The latest version of the sdk-addons-list XML Schema. - * Valid version numbers are between 1 and this number, included. */ - public static final int NS_LATEST_VERSION = 2; - - /** The XML namespace of the latest sdk-addons-list XML. */ - public static final String NS_URI = getSchemaUri(NS_LATEST_VERSION); - - - /** The canonical URL filename for addons-list XML files. */ - public static final String URL_DEFAULT_FILENAME = getDefaultName(NS_LATEST_VERSION); - - /** The URL where to find the official addons list fle. */ - public static final String URL_ADDON_LIST = - SdkRepoConstants.URL_GOOGLE_SDK_SITE + URL_DEFAULT_FILENAME; - - - - /** The root sdk-addons-list element */ - public static final String NODE_SDK_ADDONS_LIST = "sdk-addons-list"; //$NON-NLS-1$ - - /** An add-on site. */ - public static final String NODE_ADDON_SITE = "addon-site"; //$NON-NLS-1$ - - /** A system image site. */ - public static final String NODE_SYS_IMG_SITE = "sys-img-site"; //$NON-NLS-1$ - - /** The UI-visible name of the add-on site. */ - public static final String NODE_NAME = "name"; //$NON-NLS-1$ - - /** - * The URL of the site. - *

- * This can be either the exact URL of the an XML resource conforming - * to the latest sdk-addon-N.xsd schema, or it can be the URL of a - * 'directory', in which case the manager will look for a resource - * named 'addon.xml' at this location. - *

- * Examples: - *

-     *    http://www.example.com/android/my_addons.xml
-     *  or
-     *    http://www.example.com/android/
-     * 
- * In the second example, the manager will actually look for - * http://www.example.com/android/addon.xml - */ - public static final String NODE_URL = "url"; //$NON-NLS-1$ - - /** - * Returns a stream to the requested sdk-addon XML Schema. - * - * @param version Between 1 and {@link #NS_LATEST_VERSION}, included. - * @return An {@link InputStream} object for the local XSD file or - * null if there is no schema for the requested version. - */ - public static InputStream getXsdStream(int version) { - String filename = String.format("sdk-addons-list-%d.xsd", version); //$NON-NLS-1$ - return SdkAddonsListConstants.class.getResourceAsStream(filename); - } - - /** - * Returns the URI of the sdk-addon schema for the given version number. - * @param version Between 1 and {@link #NS_LATEST_VERSION} included. - */ - public static String getSchemaUri(int version) { - return String.format(NS_BASE + "%d", version); //$NON-NLS-1$ - } - - public static String getDefaultName(int version) { - return String.format("addons_list-%1$d.xml", version); //$NON-NLS-1$ - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkRepoConstants.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkRepoConstants.java deleted file mode 100644 index 7b325432..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkRepoConstants.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * 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.sdklib.repository; - - -import com.android.annotations.NonNull; - -import java.io.InputStream; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Public constants for the sdk-repository XML Schema. - * @deprecated moved to studio - */ -public class SdkRepoConstants extends RepoConstants { - - /** - * The latest version of the sdk-repository XML Schema. - * Valid version numbers are between 1 and this number, included. - */ - public static final int NS_LATEST_VERSION = 11; - - /** - * The min version of the sdk-repository XML Schema we'll try to load. - * When looking for a repository-N.xml on the server, we'll check from - * {@link #NS_LATEST_VERSION} down to this revision. - * We only introduced the "repository-N.xml" pattern start with revision - * 5, so we know that our server will never contain a repository - * XML with a schema version lower than this one. - */ - public static final int NS_SERVER_MIN_VERSION = 5; - - /** - * The URL of the official Google sdk-repository site. - * The URL ends with a /, allowing easy concatenation. - * */ - public static final String URL_GOOGLE_SDK_SITE = - "https://dl.google.com/android/repository/"; //$NON-NLS-1$ - - /** - * The default name looked for by SdkSource when trying to load an - * sdk-repository XML if the URL doesn't match an existing resource. - */ - public static final String URL_DEFAULT_FILENAME = "repository.xml"; //$NON-NLS-1$ - - /** - * The pattern name looked by {@link SdkSource} when trying to load - * an sdk-repository XML that is specific to a given XSD revision. - *

- * This must be used with {@link String#format(String, Object...)} with - * one integer parameter between 1 and {@link #NS_LATEST_VERSION}. - */ - public static final String URL_FILENAME_PATTERN = "repository-%1$d.xml"; //$NON-NLS-1$ - - /** The base of our sdk-repository XML namespace. */ - private static final String NS_BASE = - "http://schemas.android.com/sdk/android/repository/"; //$NON-NLS-1$ - - /** - * The pattern of our sdk-repository XML namespace. - * Matcher's group(1) is the schema version (integer). - */ - public static final String NS_PATTERN = NS_BASE + "([0-9]+)"; //$NON-NLS-1$ - - /** The XML namespace of the latest sdk-repository XML. */ - public static final String NS_URI = getSchemaUri(NS_LATEST_VERSION); - - /** The root sdk-repository element */ - public static final String NODE_SDK_REPOSITORY = "sdk-repository"; //$NON-NLS-1$ - - /* The major revision for tool and platform-tool package - * (the full revision number is revision.minor.micro + preview#.) - * Mandatory int > 0. 0 when missing, which should not happen in - * a valid document. */ - public static final String NODE_MAJOR_REV = "major"; //$NON-NLS-1$ - /* The minor revision for tool and platform-tool package - * (the full revision number is revision.minor.micro + preview#.) - * Optional int >= 0. Implied to be 0 when missing. */ - public static final String NODE_MINOR_REV = "minor"; //$NON-NLS-1$ - /* The micro revision for tool and platform-tool package - * (the full revision number is revision.minor.micro + preview#.) - * Optional int >= 0. Implied to be 0 when missing. */ - public static final String NODE_MICRO_REV = "micro"; //$NON-NLS-1$ - /* The preview revision for tool and platform-tool package. - * Int > 0, only present for "preview / release candidate" packages. */ - public static final String NODE_PREVIEW = "preview"; //$NON-NLS-1$ - - /** A platform package. */ - public static final String NODE_PLATFORM = "platform"; //$NON-NLS-1$ - /** A tool package. */ - public static final String NODE_TOOL = "tool"; //$NON-NLS-1$ - /** A platform-tool package. */ - public static final String NODE_PLATFORM_TOOL = "platform-tool"; //$NON-NLS-1$ - /** A build-tool package. */ - public static final String NODE_BUILD_TOOL = "build-tool"; //$NON-NLS-1$ - /** A doc package. */ - public static final String NODE_DOC = "doc"; //$NON-NLS-1$ - /** A sample package. */ - public static final String NODE_SAMPLE = "sample"; //$NON-NLS-1$ - /** A source package. */ - public static final String NODE_SOURCE = "source"; //$NON-NLS-1$ - - /** - * List of possible nodes in a repository XML. Used to populate options automatically - * in the no-GUI mode. - */ - public static final String[] NODES = { - NODE_PLATFORM, - NODE_SYSTEM_IMAGE, - NODE_TOOL, - NODE_PLATFORM_TOOL, - NODE_DOC, - NODE_SAMPLE, - NODE_SOURCE, - }; - - /** - * Returns a stream to the requested {@code sdk-repository} XML Schema. - * - * @param version Between 1 and {@link #NS_LATEST_VERSION}, included. - * @return An {@link InputStream} object for the local XSD file or - * null if there is no schema for the requested version. - */ - public static InputStream getXsdStream(int version) { - return getXsdStream(NODE_SDK_REPOSITORY, version); - } - - /** - * Returns the URI of the SDK Repository schema for the given version number. - * @param version Between 1 and {@link #NS_LATEST_VERSION} included. - */ - public static String getSchemaUri(int version) { - return String.format(NS_BASE + "%d", version); //$NON-NLS-1$ - } - - /** - * Checks whether the schema version is greater or equal to the specified one. - * - * @param nsUri A non-null sdk-repository schema URI. - * @param minVersion The minimum version accepted. - * @return True if the URI is valid and has at least the required version. False otherwise. - */ - public static boolean versionGreaterOrEqualThan(@NonNull String nsUri, int minVersion) { - Pattern nsPattern = Pattern.compile(SdkRepoConstants.NS_PATTERN); - Matcher m = nsPattern.matcher(nsUri); - if (m.matches()) { - String version = m.group(1); - try { - return Integer.parseInt(version) >= minVersion; - } catch (NumberFormatException e) { - } - } - return false; - } - -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkStatsConstants.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkStatsConstants.java deleted file mode 100644 index df3e5d6d..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkStatsConstants.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (C) 2012 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.sdklib.repository; - - -import java.io.InputStream; - -/** - * Public constants for the sdk-stats XML Schema. - * @deprecated moved to studio - */ -public class SdkStatsConstants { - - /** The canonical URL filename for addons-list XML files. */ - public static final String URL_DEFAULT_FILENAME = "stats-1.xml"; //$NON-NLS-1$ - - /** The URL where to find the official addons list fle. */ - public static final String URL_STATS = - SdkRepoConstants.URL_GOOGLE_SDK_SITE + URL_DEFAULT_FILENAME; - - /** The base of our sdk-addons-list XML namespace. */ - private static final String NS_BASE = - "http://schemas.android.com/sdk/android/stats/"; //$NON-NLS-1$ - - /** - * The pattern of our sdk-stats XML namespace. - * Matcher's group(1) is the schema version (integer). - */ - public static final String NS_PATTERN = NS_BASE + "([1-9][0-9]*)"; //$NON-NLS-1$ - - /** The latest version of the sdk-stats XML Schema. - * Valid version numbers are between 1 and this number, included. */ - public static final int NS_LATEST_VERSION = 1; - - /** The XML namespace of the latest sdk-stats XML. */ - public static final String NS_URI = getSchemaUri(NS_LATEST_VERSION); - - /** The root sdk-stats element */ - public static final String NODE_SDK_STATS = "sdk-stats"; //$NON-NLS-1$ - - /** A platform stat. */ - public static final String NODE_PLATFORM = "platform"; //$NON-NLS-1$ - - /** The Android API Level for the platform. An int > 0. */ - public static final String NODE_API_LEVEL = "api-level"; //$NON-NLS-1$ - - /** The official codename for this platform, for example "Cupcake". */ - public static final String NODE_CODENAME = "codename"; //$NON-NLS-1$ - - /** The official version name of this platform, for example "Android 1.5". */ - public static final String NODE_VERSION = "version"; //$NON-NLS-1$ - - /** - * The approximate share percentage of that platform. - * See the caveat in sdk-stats-1.xsd about value freshness and accuracy. - */ - public static final String NODE_SHARE = "share"; //$NON-NLS-1$ - - /** - * Returns a stream to the requested sdk-stats XML Schema. - * - * @param version Between 1 and {@link #NS_LATEST_VERSION}, included. - * @return An {@link InputStream} object for the local XSD file or - * null if there is no schema for the requested version. - */ - public static InputStream getXsdStream(int version) { - String filename = String.format("sdk-stats-%d.xsd", version); //$NON-NLS-1$ - return SdkStatsConstants.class.getResourceAsStream(filename); - } - - /** - * Returns the URI of the sdk-stats schema for the given version number. - * @param version Between 1 and {@link #NS_LATEST_VERSION} included. - */ - public static String getSchemaUri(int version) { - return String.format(NS_BASE + "%d", version); //$NON-NLS-1$ - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkSysImgConstants.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkSysImgConstants.java deleted file mode 100644 index d3e5752e..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/SdkSysImgConstants.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (C) 2012 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.sdklib.repository; - - -import java.io.InputStream; - -/** - * Public constants for the sdk-sys-img XML Schema. - * @deprecated moved to studio - */ -public class SdkSysImgConstants extends RepoConstants { - - /** - * The default name looked for by SdkSource when trying to load an - * sdk-sys-img XML if the URL doesn't match an existing resource. - */ - public static final String URL_DEFAULT_FILENAME = "sys-img.xml"; //$NON-NLS-1$ - - /** The base of our sdk-sys-img XML namespace. */ - private static final String NS_BASE = - "http://schemas.android.com/sdk/android/sys-img/"; //$NON-NLS-1$ - - /** - * The pattern of our sdk-sys-img XML namespace. - * Matcher's group(1) is the schema version (integer). - */ - public static final String NS_PATTERN = NS_BASE + "([0-9]+)"; //$NON-NLS-1$ - - /** - * The latest version of the sdk-sys-img XML Schema. - * Valid version numbers are between 1 and this number, included. - */ - public static final int NS_LATEST_VERSION = 3; - - /** The XML namespace of the latest sdk-sys-img XML. */ - public static final String NS_URI = getSchemaUri(NS_LATEST_VERSION); - - /** The root sdk-sys-img element */ - public static final String NODE_SDK_SYS_IMG = "sdk-sys-img"; //$NON-NLS-1$ - - /** A system-image tag id. */ - public static final String ATTR_TAG_ID = "tag-id"; //$NON-NLS-1$ - /** The user-visible display part of a system-image tag id. Optional. */ - public static final String ATTR_TAG_DISPLAY = "tag-display"; //$NON-NLS-1$ - - /** An add-on sub-element, indicating this is an add-on system image. */ - public static final String NODE_ADD_ON = SdkAddonConstants.NODE_ADD_ON; - - /** - * List of possible nodes in a repository XML. Used to populate options automatically - * in the no-GUI mode. - */ - public static final String[] NODES = { - NODE_SYSTEM_IMAGE, - }; - - /** - * Returns a stream to the requested {@code sdk-sys-img} XML Schema. - * - * @param version Between 1 and {@link #NS_LATEST_VERSION}, included. - * @return An {@link InputStream} object for the local XSD file or - * null if there is no schema for the requested version. - */ - public static InputStream getXsdStream(int version) { - return getXsdStream(NODE_SDK_SYS_IMG, version); - } - - /** - * Returns the URI of the sdk-sys-img schema for the given version number. - * @param version Between 1 and {@link #NS_LATEST_VERSION} included. - */ - public static String getSchemaUri(int version) { - return String.format(NS_BASE + "%d", version); //$NON-NLS-1$ - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IPkgCapabilities.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IPkgCapabilities.java deleted file mode 100644 index b481e6a6..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IPkgCapabilities.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (C) 2013 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.sdklib.repository.descriptors; - -import com.android.sdklib.AndroidVersion; -import com.android.sdklib.repository.FullRevision; -import com.android.sdklib.repository.MajorRevision; - -/** - * {@link IPkgCapabilities} describe which attributes are available for each kind of - * SDK Manager package type. - *

- * To query packages capabilities, rely on {@code PkgType.hasXxx()} or {@code PkgDesc.hasXxx()}. - * - * @see PkgType - * @see PkgDesc - */ -public interface IPkgCapabilities { - - /** - * Indicates whether this package type has a {@link FullRevision}. - * @return True if this package type has a {@link FullRevision}. - */ - boolean hasFullRevision(); - - /** - * Indicates whether this package type has a {@link MajorRevision}. - * @return True if this package type has a {@link MajorRevision}. - */ - boolean hasMajorRevision(); - - /** - * Indicates whether this package type has a {@link AndroidVersion}. - * @return True if this package type has a {@link AndroidVersion}. - */ - boolean hasAndroidVersion(); - - /** - * Indicates whether this package type has a path. - * @return True if this package type has a path. - */ - boolean hasPath(); - - /** - * Indicates whether this package type has a tag. - * @return True if this package type has a tag id-display tuple. - */ - boolean hasTag(); - - /** - * Indicates whether this package type has a vendor id. - * @return True if this package type has a vendor id. - */ - boolean hasVendor(); - - /** - * Indicates whether this package type has a {@code min-tools-rev} attribute. - * @return True if this package type has a {@code min-tools-rev} attribute. - */ - boolean hasMinToolsRev(); - - /** - * Indicates whether this package type has a {@code min-platform-tools-rev} attribute. - * @return True if this package type has a {@code min-platform-tools-rev} attribute. - */ - boolean hasMinPlatformToolsRev(); -} - diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IPkgDesc.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IPkgDesc.java deleted file mode 100644 index 5effda3a..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IPkgDesc.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright (C) 2013 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.sdklib.repository.descriptors; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.sdklib.AndroidVersion; -import com.android.sdklib.repository.IListDescription; -import com.android.sdklib.repository.License; -import com.android.sdklib.repository.FullRevision; -import com.android.sdklib.repository.MajorRevision; -import com.android.sdklib.repository.PreciseRevision; - -import java.io.File; - -/** - * {@link IPkgDesc} keeps information on individual SDK packages - * (both local or remote packages definitions.) - *
- * Packages have different attributes depending on their type. - *

- * To create a new {@link IPkgDesc}, use one of the package-specific constructors - * provided by {@code PkgDesc.Builder.newXxx()}. - *

- * To query packages capabilities, rely on {@link #getType()} and the {@code IPkgDesc.hasXxx()} - * methods provided by {@link IPkgDesc}. - */ -public interface IPkgDesc extends Comparable, IPkgCapabilities, IListDescription { - - /** - * Returns the type of the package. - * @return Returns one of the {@link PkgType} constants. - */ - @NonNull - PkgType getType(); - - /** - * Returns the list-display meta data of this package. - * @return The list-display data, if available, or null. - */ - @Nullable - String getListDisplay(); - - @Nullable - String getDescriptionShort(); - - @Nullable - String getDescriptionUrl(); - - @Nullable - License getLicense(); - - boolean isObsolete(); - - /** - * Returns the package's {@link FullRevision} or null. - * @return A non-null value if {@link #hasFullRevision()} is true; otherwise a null value. - */ - @Nullable - FullRevision getFullRevision(); - - /** - * Returns the package's {@link MajorRevision} or null. - * @return A non-null value if {@link #hasMajorRevision()} is true; otherwise a null value. - */ - @Nullable - MajorRevision getMajorRevision(); - - /** - * Returns the package's revision or null. This will come from the {@link FullRevision} or - * {@link MajorRevision}, with the precision set as appropriate. - * @return A representation of {@link #getMajorRevision()} or {@link #getFullRevision()}, - * depending on which one exists. - */ - @NonNull - PreciseRevision getPreciseRevision(); - - /** - * Returns the package's {@link AndroidVersion} or null. - * @return A non-null value if {@link #hasAndroidVersion()} is true; otherwise a null value. - */ - @Nullable - AndroidVersion getAndroidVersion(); - - /** - * Returns the package's path string or null. - *

- * For {@link PkgType#PKG_SYS_IMAGE}, the path is the system-image ABI.
- * For {@link PkgType#PKG_PLATFORM}, the path is the platform hash string.
- * For {@link PkgType#PKG_ADDON}, the path is the platform hash string.
- * For {@link PkgType#PKG_EXTRA}, the path is the extra-path string.
- * - * @return A non-null value if {@link #hasPath()} is true; otherwise a null value. - */ - @Nullable - String getPath(); - - /** - * Returns the package's tag id-display tuple or null. - * - * @return A non-null tag if {@link #hasTag()} is true; otherwise a null value. - */ - @Nullable - IdDisplay getTag(); - - /** - * Returns the package's vendor-id string or null. - * @return A non-null value if {@link #hasVendor()} is true; otherwise a null value. - */ - @Nullable - IdDisplay getVendor(); - - /** - * Returns the package's {@code min-tools-rev} or null. - * @return A non-null value if {@link #hasMinToolsRev()} is true; otherwise a null value. - */ - @Nullable - FullRevision getMinToolsRev(); - - /** - * Returns the package's {@code min-platform-tools-rev} or null. - * @return A non-null value if {@link #hasMinPlatformToolsRev()} is true; otherwise null. - */ - @Nullable - FullRevision getMinPlatformToolsRev(); - - /** - * Indicates whether this package descriptor is an update for the given - * existing descriptor. Preview versions are never considered updates for non- - * previews, and vice versa. - * - * @param existingDesc A non-null existing descriptor. - * @return True if this package is an update for the given one. - */ - boolean isUpdateFor(@NonNull IPkgDesc existingDesc); - - /** - * Indicates whether this package descriptor is an update for the given - * existing descriptor, using the given comparison method. - * - * @param existingDesc A non-null existing descriptor. - * @param previewComparison The {@link FullRevision.PreviewComparison} method to use - * when comparing the packages. - * @return True if this package is an update for the given one. - */ - boolean isUpdateFor(@NonNull IPkgDesc existingDesc, - @NonNull FullRevision.PreviewComparison previewComparison); - - /** - * Returns a stable string id that can be used to reference this package, including - * a suffix indicating that this package is a preview if it is. - */ - @NonNull - String getInstallId(); - - /** - * Returns a stable string id that can be used to reference this package, which - * excludes the preview suffix. - */ - String getBaseInstallId(); - - /** - * Returns the canonical location where such a package would be installed. - * @param sdkLocation The root of the SDK. - * @return the canonical location where such a package would be installed. - */ - @NonNull - File getCanonicalInstallFolder(@NonNull File sdkLocation); - - /** - * @return True if the revision of this package is a preview. - */ - boolean isPreview(); -} - diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IPkgDescAddon.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IPkgDescAddon.java deleted file mode 100644 index 02c0ad5a..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IPkgDescAddon.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (C) 2014 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.sdklib.repository.descriptors; - -import com.android.annotations.NonNull; - -/** - * {@link IPkgDescAddon} keeps information on individual add-on SDK packages - * (both local or remote packages definitions.) The base {@link IPkgDesc} tries - * to present a unified interface to package attributes and this interface - * adds methods specific to extras. - *

- * To create a new {@link IPkgDescAddon}, - * use {@link PkgDesc.Builder#newAddon(com.android.sdklib.AndroidVersion, com.android.sdklib.repository.MajorRevision, IdDisplay, IdDisplay)}. - *

- * To query generic packages capabilities, rely on {@link #getType()} and the - * {@code IPkgDesc.hasXxx()} methods provided by {@link IPkgDesc}. - */ -public interface IPkgDescAddon extends IPkgDesc { - - /** - * Returns the id/display name of the add-on. - * @return A non-null id/display name for the add-on - */ - @NonNull IdDisplay getName(); -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IPkgDescExtra.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IPkgDescExtra.java deleted file mode 100644 index 68c4bd72..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IPkgDescExtra.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (C) 2013 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.sdklib.repository.descriptors; - -import com.android.annotations.NonNull; -import com.android.sdklib.repository.NoPreviewRevision; - -/** - * {@link IPkgDescExtra} keeps information on individual extra SDK packages - * (both local or remote packages definitions.) The base {@link IPkgDesc} tries - * to present a unified interface to package attributes and this interface - * adds methods specific to extras. - *

- * To create a new {@link IPkgDescExtra}, - * use {@link PkgDesc.Builder#newExtra(IdDisplay, String, String, String[], NoPreviewRevision)}. - *

- * The extra's revision is a {@link NoPreviewRevision}; the attribute is however - * accessed via {@link IPkgDesc#getFullRevision()} instead of introducing a new - * custom method. - *

- * To query generic packages capabilities, rely on {@link #getType()} and the - * {@code IPkgDesc.hasXxx()} methods provided by {@link IPkgDesc}. - */ -public interface IPkgDescExtra extends IPkgDesc { - /** - * Returns an optional list of older paths for this extra package. - * @return A non-null, possibly empty, for old paths previously used for the same extra. - */ - @NonNull String[] getOldPaths(); - - /** - * Returns the display name of the Extra. - * @return A non-null name for the Extra, used for display purposes. - */ - @NonNull String getNameDisplay(); - -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IdDisplay.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IdDisplay.java deleted file mode 100644 index 0278ba5b..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/IdDisplay.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (C) 2014 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.sdklib.repository.descriptors; - -import com.android.annotations.NonNull; - -/** - * Immutable structure that represents a tuple (id-string + display-string.) - */ -public final class IdDisplay implements Comparable { - - private final String mId; - private final String mDisplay; - - /** - * Creates a new immutable tuple (id-string + display-string.) - * - * @param id The non-null id string. - * @param display The non-null display string. - */ - public IdDisplay(@NonNull String id, @NonNull String display) { - mId = id; - mDisplay = display; - } - - @NonNull - public String getId() { - return mId; - } - - @NonNull - public String getDisplay() { - return mDisplay; - } - - /** - * {@link IdDisplay} instances are the same if they have the same id. - * The display value is not used for comparison or ordering. - */ - @Override - public int compareTo(IdDisplay tag) { - return mId.compareTo(tag.mId); - } - - /** - * Hash code of {@link IdDisplay} instances only rely on the id hash code. - */ - @Override - public int hashCode() { - return mId.hashCode(); - } - - /** - * Equality of {@link IdDisplay} instances only rely on the id equality. - * The display value is not used for comparison or ordering. - */ - @Override - public boolean equals(Object obj) { - return (obj instanceof IdDisplay) && mId.equals(((IdDisplay)obj).mId); - } - - /** - * Returns a string representation for *debug* purposes only, not for UI display. - */ - @Override - public String toString() { - return String.format("%1$s [%2$s]", mId, mDisplay); - } - -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/PkgDesc.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/PkgDesc.java deleted file mode 100644 index eda9838d..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/PkgDesc.java +++ /dev/null @@ -1,1206 +0,0 @@ -/* - * Copyright (C) 2013 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.sdklib.repository.descriptors; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.annotations.VisibleForTesting; -import com.android.annotations.VisibleForTesting.Visibility; -import com.android.sdklib.AndroidTargetHash; -import com.android.sdklib.AndroidVersion; -import com.android.sdklib.SystemImage; -import com.android.sdklib.io.LegacyFileOp; -import com.android.sdklib.repository.FullRevision; -import com.android.sdklib.repository.FullRevision.PreviewComparison; -import com.android.sdklib.repository.License; -import com.android.sdklib.repository.MajorRevision; -import com.android.sdklib.repository.NoPreviewRevision; -import com.android.sdklib.repository.PreciseRevision; - -import java.io.File; -import java.util.Locale; - -/** - * {@link PkgDesc} keeps information on individual SDK packages - * (both local or remote packages definitions.) - *
- * Packages have different attributes depending on their type. - *

- * To create a new {@link PkgDesc}, use one of the package-specific constructors - * provided here. - *

- * To query packages capabilities, rely on {@link #getType()} and the {@code PkgDesc.hasXxx()} - * methods provided in the base {@link PkgDesc}. - */ -public class PkgDesc implements IPkgDesc { - public static final String PREVIEW_SUFFIX = "-preview"; - private final PkgType mType; - private final FullRevision mFullRevision; - private final MajorRevision mMajorRevision; - private final AndroidVersion mAndroidVersion; - private final String mPath; - private final IdDisplay mTag; - private final IdDisplay mVendor; - private final FullRevision mMinToolsRev; - private final FullRevision mMinPlatformToolsRev; - private final IIsUpdateFor mCustomIsUpdateFor; - private final IGetPath mCustomPath; - - private final License mLicense; - private final String mListDisplay; - private final String mDescriptionShort; - private final String mDescriptionUrl; - private final boolean mIsObsolete; - - protected PkgDesc(@NonNull PkgType type, - @Nullable License license, - @Nullable String listDisplay, - @Nullable String descriptionShort, - @Nullable String descriptionUrl, - boolean isObsolete, - @Nullable FullRevision fullRevision, - @Nullable MajorRevision majorRevision, - @Nullable AndroidVersion androidVersion, - @Nullable String path, - @Nullable IdDisplay tag, - @Nullable IdDisplay vendor, - @Nullable FullRevision minToolsRev, - @Nullable FullRevision minPlatformToolsRev, - @Nullable IIsUpdateFor customIsUpdateFor, - @Nullable IGetPath customPath) { - mType = type; - mIsObsolete = isObsolete; - mLicense = license; - mListDisplay = listDisplay; - mDescriptionShort = descriptionShort; - mDescriptionUrl = descriptionUrl; - mFullRevision = fullRevision; - mMajorRevision = majorRevision; - mAndroidVersion = androidVersion; - mPath = path; - mTag = tag; - mVendor = vendor; - mMinToolsRev = minToolsRev; - mMinPlatformToolsRev = minPlatformToolsRev; - mCustomIsUpdateFor = customIsUpdateFor; - mCustomPath = customPath; - } - - @NonNull - @Override - public PkgType getType() { - return mType; - } - - @Override - @Nullable - public String getListDisplay() { - return mListDisplay; - } - - @Override - @Nullable - public String getDescriptionShort() { - return mDescriptionShort; - } - - @Override - @Nullable - public String getDescriptionUrl() { - return mDescriptionUrl; - } - - @Override - @Nullable - public License getLicense() { - return mLicense; - } - - @Override - public boolean isObsolete() { - return mIsObsolete; - } - - @Override - public final boolean hasFullRevision() { - return getType().hasFullRevision(); - } - - @Override - public final boolean hasMajorRevision() { - return getType().hasMajorRevision(); - } - - @Override - public final boolean hasAndroidVersion() { - return getType().hasAndroidVersion(); - } - - @Override - public final boolean hasPath() { - return getType().hasPath(); - } - - @Override - public final boolean hasTag() { - return getType().hasTag(); - } - - @Override - public boolean hasVendor() { - return getType().hasVendor(); - } - - @Override - public final boolean hasMinToolsRev() { - return getType().hasMinToolsRev(); - } - - @Override - public final boolean hasMinPlatformToolsRev() { - return getType().hasMinPlatformToolsRev(); - } - - @Nullable - @Override - public FullRevision getFullRevision() { - return mFullRevision; - } - - @Nullable - @Override - public MajorRevision getMajorRevision() { - return mMajorRevision; - } - - @NonNull - @Override - public final PreciseRevision getPreciseRevision() { - if (mMajorRevision == null) { - return new PreciseRevision(mFullRevision.getMajor(), mFullRevision.getMinor(), - mFullRevision.getMicro(), mFullRevision.getPreview()); - } - return new PreciseRevision(mMajorRevision.getMajor()); - } - - @Nullable - @Override - public AndroidVersion getAndroidVersion() { - return mAndroidVersion; - } - - @Override - public boolean isPreview() { - return getPreciseRevision().isPreview(); - } - - @Nullable - @Override - public String getPath() { - if (mCustomPath != null) { - return mCustomPath.getPath(this); - } else { - return mPath; - } - } - - @Nullable - @Override - public IdDisplay getTag() { - return mTag; - } - - @Nullable - @Override - public IdDisplay getVendor() { - return mVendor; - } - - @Nullable - @Override - public FullRevision getMinToolsRev() { - return mMinToolsRev; - } - - @Nullable - @Override - public FullRevision getMinPlatformToolsRev() { - return mMinPlatformToolsRev; - } - - @Override - public String getInstallId() { - String id = getBaseInstallId(); - if (getPreciseRevision().isPreview()) { - return id + PREVIEW_SUFFIX; - } - return id; - } - - @Override - public String getBaseInstallId() { - StringBuilder sb = new StringBuilder(); - - /* iid patterns: - tools, platform-tools => FOLDER - build-tools => FOLDER-REV - doc, sample, source => ENUM-API - extra => ENUM-VENDOR.id-PATH - platform => android-API - add-on => addon-NAME.id-VENDOR.id-API - platform sys-img => sys-img-ABI-TAG|android-API - add-on sys-img => sys-img-ABI-addon-NAME.id-VENDOR.id-API - */ - - switch (mType) { - case PKG_TOOLS: - case PKG_PLATFORM_TOOLS: - sb.append(mType.getFolderName()); - break; - - case PKG_BUILD_TOOLS: - sb.append(mType.getFolderName()).append('-'); - // Add version number without the preview revision number. This is to make preview - // packages be updatable to the next revision. - int[] version = getPreciseRevision().toIntArray(false); - for (int i = 0; i < version.length; i++) { - sb.append(version[i]); - if (i != version.length - 1) { - sb.append('.'); - } - } - break; - - case PKG_DOC: - sb.append("doc"); - break; - - case PKG_SAMPLE: - case PKG_SOURCE: - sb.append(mType.toString().toLowerCase(Locale.US).replace("pkg_", "")); - sb.append('-').append(getAndroidVersion().getApiString()); - break; - - case PKG_EXTRA: - sb.append("extra-") - .append(getVendor().getId()) - .append('-') - .append(getPath()); - break; - - case PKG_PLATFORM: - sb.append(AndroidTargetHash.PLATFORM_HASH_PREFIX) - .append(getAndroidVersion().getApiString()); - break; - - case PKG_ADDON: - sb.append("addon-") - .append(((IPkgDescAddon)this).getName().getId()) - .append('-') - .append(getVendor().getId()) - .append('-') - .append(getAndroidVersion().getApiString()); - break; - - case PKG_SYS_IMAGE: - sb.append("sys-img-") - .append(getPath()) // path==ABI for sys-img - .append('-') - .append(SystemImage.DEFAULT_TAG.equals(getTag()) ? "android" : getTag().getId()) - .append('-') - .append(getAndroidVersion().getApiString()); - break; - - case PKG_ADDON_SYS_IMAGE: - sb.append("sys-img-") - .append(getPath()) // path==ABI for sys-img - .append("-addon-") - .append(SystemImage.DEFAULT_TAG.equals(getTag()) ? "android" : getTag().getId()) - .append('-') - .append(getVendor().getId()) - .append('-') - .append(getAndroidVersion().getApiString()); - break; - - case PKG_NDK: - sb.append("ndk"); - break; - - default: - throw new IllegalArgumentException("IID not defined for type " + mType.toString()); - } - - return sanitize(sb.toString()); - } - - @Override - public File getCanonicalInstallFolder(@NonNull File sdkLocation) { - File f = LegacyFileOp.append(sdkLocation, mType.getFolderName()); - - /* folder patterns: - tools, platform-tools, doc => FOLDER - build-tools, add-on => FOLDER/IID - platform, sample, source => FOLDER/android-API - platform sys-img => FOLDER/android-API/TAG/ABI - add-on sys-img => FOLDER/addon-NAME.id-VENDOR.id-API/ABI - extra => FOLDER/VENDOR.id/PATH - */ - - switch (mType) { - case PKG_TOOLS: - case PKG_PLATFORM_TOOLS: - case PKG_DOC: - // no-op, top-folder is all what is needed here - break; - - case PKG_BUILD_TOOLS: - case PKG_ADDON: - f = LegacyFileOp.append(f, getInstallId()); - break; - - case PKG_PLATFORM: - case PKG_SAMPLE: - case PKG_SOURCE: - f = LegacyFileOp.append(f, AndroidTargetHash.PLATFORM_HASH_PREFIX + sanitize( - getAndroidVersion().getApiString())); - break; - - case PKG_SYS_IMAGE: - f = LegacyFileOp.append(f, - AndroidTargetHash.PLATFORM_HASH_PREFIX + sanitize( - getAndroidVersion().getApiString()), - sanitize(SystemImage.DEFAULT_TAG.equals(getTag()) ? "android" - : getTag().getId()), - sanitize(getPath())); // path==abi - break; - - case PKG_ADDON_SYS_IMAGE: - String name = "addon-" - + (SystemImage.DEFAULT_TAG.equals(getTag()) ? "android" : getTag().getId()) - + '-' - + getVendor().getId() - + '-' - + getAndroidVersion().getApiString(); - f = LegacyFileOp.append(f, - sanitize(name), - sanitize(getPath())); // path==abi - break; - - case PKG_EXTRA: - f = LegacyFileOp.append(f, - sanitize(getVendor().getId()), - sanitize(getPath())); - break; - - default: - throw new IllegalArgumentException( - "CanonicalFolder not defined for type " + mType.toString()); - } - - return f; - } - - //---- Updating ---- - - /** - * Computes the most general case of {@link #isUpdateFor(IPkgDesc)}. - * Individual package types use this and complement with their own specific cases - * as needed. - * - * @param existingDesc A non-null package descriptor to compare with. - * @return True if this package descriptor would generally update the given one. - */ - @Override - public boolean isUpdateFor(@NonNull IPkgDesc existingDesc) { - return isUpdateFor(existingDesc, PreviewComparison.COMPARE_NUMBER); - } - - @Override - public boolean isUpdateFor(@NonNull IPkgDesc existingDesc, - @NonNull PreviewComparison previewComparison) { - if (mCustomIsUpdateFor != null) { - return mCustomIsUpdateFor.isUpdateFor(this, existingDesc); - } else { - return isGenericUpdateFor(existingDesc, previewComparison); - } - } - - /** - * Computes the most general case of {@link #isUpdateFor(IPkgDesc)}. - * Individual package types use this and complement with their own specific cases - * as needed. - * - * @param existingDesc A non-null package descriptor to compare with. - * @param previewComparison The type of preview comparison to do. - * @return True if this package descriptor would generally update the given one. - */ - private boolean isGenericUpdateFor(@NonNull IPkgDesc existingDesc, - PreviewComparison previewComparison) { - - if (existingDesc == null || !getType().equals(existingDesc.getType())) { - return false; - } - - // Packages that have an Android version can generally only be updated - // for the same Android version (otherwise it's a new artifact.) - if (hasAndroidVersion() && !getAndroidVersion().equals(existingDesc.getAndroidVersion())) { - return false; - } - - // Packages that have a vendor id need the same vendor id on both sides - if (hasVendor() && !getVendor().equals(existingDesc.getVendor())) { - return false; - } - - // Packages that have a tag id need the same tag id on both sides - if (hasTag() && !getTag().getId().equals(existingDesc.getTag().getId())) { - return false; - } - - // Packages that have a path can generally only be updated if both use the same path - if (hasPath()) { - if (this instanceof IPkgDescExtra) { - // Extra package handle paths differently, they need to use the old_path - // to allow upgrade compatibility. - if (!PkgDescExtra.compatibleVendorAndPath((IPkgDescExtra) this, - (IPkgDescExtra) existingDesc)) { - return false; - } - } else if (!getPath().equals(existingDesc.getPath())) { - return false; - } - } - - // Packages that have a major version are generally updates if it increases. - if (hasMajorRevision() && - getMajorRevision().compareTo(existingDesc.getMajorRevision()) > 0) { - return true; - } - - // Packages that have a full revision are generally updates if it increases - // but keeps the same kind of preview (e.g. previews are only updates by previews.) - if (hasFullRevision() && - (previewComparison == PreviewComparison.IGNORE - || existingDesc.isPreview() == isPreview())) { - // If both packages match in their preview type (both previews or both not previews) - // then is the RC/preview number an update? - return getFullRevision().compareTo(existingDesc.getFullRevision(), - PreviewComparison.COMPARE_NUMBER) > 0; - } - - return false; - } - - - //---- Ordering ---- - - /** - * Compares this descriptor to another one. - * All fields must match for equality. - *

- * This is must not be used an indication that a package is a suitable update for another one. - * The comparison order is however suitable for sorting packages for display purposes. - */ - @Override - public int compareTo(@NonNull IPkgDesc o) { - int t1 = getType().getIntValue(); - int t2 = o.getType().getIntValue(); - if (t1 != t2) { - return t1 - t2; - } - - if (hasAndroidVersion() && o.hasAndroidVersion()) { - t1 = getAndroidVersion().compareTo(o.getAndroidVersion()); - if (t1 != 0) { - return t1; - } - } - - if (hasVendor() && o.hasVendor()) { - t1 = getVendor().compareTo(o.getVendor()); - if (t1 != 0) { - return t1; - } - } - - if (hasTag() && o.hasTag()) { - t1 = getTag().compareTo(o.getTag()); - if (t1 != 0) { - return t1; - } - } - - if (hasPath() && o.hasPath()) { - t1 = getPath().compareTo(o.getPath()); - if (t1 != 0) { - return t1; - } - } - - if (hasFullRevision() && o.hasFullRevision()) { - t1 = getFullRevision().compareTo(o.getFullRevision()); - if (t1 != 0) { - return t1; - } - } - - if (hasMajorRevision() && o.hasMajorRevision()) { - t1 = getMajorRevision().compareTo(o.getMajorRevision()); - if (t1 != 0) { - return t1; - } - } - - if (hasMinToolsRev() && o.hasMinToolsRev()) { - t1 = getMinToolsRev().compareTo(o.getMinToolsRev()); - if (t1 != 0) { - return t1; - } - } - - if (hasMinPlatformToolsRev() && o.hasMinPlatformToolsRev()) { - t1 = getMinPlatformToolsRev().compareTo(o.getMinPlatformToolsRev()); - if (t1 != 0) { - return t1; - } - } - - return 0; - } - - // --- display description ---- - - @NonNull - @Override - public String getListDescription() { - if (mListDisplay != null && !mListDisplay.isEmpty()) { - return mListDisplay; - } - - return patternReplaceImpl(getType().getListDisplayPattern()); - } - - @VisibleForTesting(visibility=Visibility.PRIVATE) - protected String patternReplaceImpl(String result) { - // Flags for list description pattern string, used in PkgType: - // $MAJ $FULL $API $PATH $TAG $VEND $NAME (for extras) - - result = result - .replace("$MAJ", hasMajorRevision() ? getMajorRevision().toShortString() : ""); - result = result - .replace("$FULL", hasFullRevision() ? getFullRevision().toShortString() : ""); - result = result - .replace("$API", hasAndroidVersion() ? getAndroidVersion().getApiString() : ""); - result = result.replace("$PATH", hasPath() ? getPath() : ""); - result = result.replace("$TAG", hasTag() && !getTag().equals(SystemImage.DEFAULT_TAG) ? - getTag().getDisplay() : ""); - result = result.replace("$VEND", hasVendor() ? getVendor().getDisplay() : ""); - String name = ""; - if (this instanceof IPkgDescExtra) { - name = ((IPkgDescExtra) this).getNameDisplay(); - } else if (this instanceof IPkgDescAddon) { - name = ((IPkgDescAddon) this).getName().getDisplay(); - } - result = result.replace("$NAME", name); - - // Evaluate replacements. - // {|choice1|choice2|...|choiceN|} gets replaced by the first non-empty choice. - for (int start = result.indexOf("{|"); - start >= 0; - start = result.indexOf("{|")) { - int end = result.indexOf('}', start); - int last = start + 1; - for (int pipe = result.indexOf('|', last+1); - pipe > start; - last = pipe, pipe = result.indexOf('|', last+1)) { - if (pipe - last > 1) { - result = result.substring(0, start) + - result.substring(last+1, pipe) + - result.substring(end+1); - break; - } - } - } - - // Evaluate conditions. - // {?value>1:text to use} -- uses the text if value is greater than 1. - // Simplification: this is our only test right now so hard-code it instead of - // using a generic expression evaluation. - for (int start = result.indexOf("{?"); - start >= 0; - start = result.indexOf("{?")) { - int end = result.indexOf('}', start); - int op = result.indexOf(">1:"); - if (op > start) { - String value = ""; - try { - FullRevision i = FullRevision.parseRevision(result.substring(start+2, op)); - if (i.compareTo(new FullRevision(1)) > 0) { - value = result.substring(op+3, end); - } - } catch (NumberFormatException e) { - value = "ERROR " + e.getMessage() + " in " + result.substring(start, end+1); - } - result = result.substring(0, start) + - value + - result.substring(end+1); - } - } - - - return result; - } - - /** String representation for debugging purposes. */ - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append("'); - return builder.toString(); - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + (hasAndroidVersion() ? getAndroidVersion().hashCode() : 0); - result = prime * result + (hasVendor() ? getVendor() .hashCode() : 0); - result = prime * result + (hasTag() ? getTag() .hashCode() : 0); - result = prime * result + (hasPath() ? getPath() .hashCode() : 0); - result = prime * result + (hasFullRevision() ? getFullRevision() .hashCode() : 0); - result = prime * result + (hasMajorRevision() ? getMajorRevision() .hashCode() : 0); - result = prime * result + (hasMinToolsRev() ? getMinToolsRev() .hashCode() : 0); - result = prime * result + (hasMinPlatformToolsRev() ? - getMinPlatformToolsRev().hashCode() : 0); - - return result; - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof IPkgDesc)) return false; - IPkgDesc rhs = (IPkgDesc) obj; - - if (hasAndroidVersion() != rhs.hasAndroidVersion()) { - return false; - } - if (hasAndroidVersion() && !getAndroidVersion().equals(rhs.getAndroidVersion())) { - return false; - } - - if (hasTag() != rhs.hasTag()) { - return false; - } - if (hasTag() && !getTag().equals(rhs.getTag())) { - return false; - } - - if (hasPath() != rhs.hasPath()) { - return false; - } - if (hasPath() && !getPath().equals(rhs.getPath())) { - return false; - } - - if (hasFullRevision() != rhs.hasFullRevision()) { - return false; - } - if (hasFullRevision() && !getFullRevision().equals(rhs.getFullRevision())) { - return false; - } - - if (hasMajorRevision() != rhs.hasMajorRevision()) { - return false; - } - if (hasMajorRevision() && !getMajorRevision().equals(rhs.getMajorRevision())) { - return false; - } - - if (hasMinToolsRev() != rhs.hasMinToolsRev()) { - return false; - } - if (hasMinToolsRev() && !getMinToolsRev().equals(rhs.getMinToolsRev())) { - return false; - } - - if (hasMinPlatformToolsRev() != rhs.hasMinPlatformToolsRev()) { - return false; - } - if (hasMinPlatformToolsRev() && - !getMinPlatformToolsRev().equals(rhs.getMinPlatformToolsRev())) { - return false; - } - - return true; - } - - - // ---- Constructors ----- - - public interface IIsUpdateFor { - boolean isUpdateFor(@NonNull PkgDesc thisPkgDesc, @NonNull IPkgDesc existingDesc); - } - - public interface IGetPath { - String getPath(@NonNull PkgDesc thisPkgDesc); - } - - public static class Builder { - private final PkgType mType; - private FullRevision mFullRevision; - private MajorRevision mMajorRevision; - private AndroidVersion mAndroidVersion; - private String mPath; - private IdDisplay mTag; - private IdDisplay mVendor; - private FullRevision mMinToolsRev; - private FullRevision mMinPlatformToolsRev; - private IIsUpdateFor mCustomIsUpdateFor; - private IGetPath mCustomPath; - private String[] mOldPaths; - private String mNameDisplay; - private IdDisplay mNameIdDisplay; - - private License mLicense; - private String mListDisplay; - private String mDescriptionShort; - private String mDescriptionUrl; - private boolean mIsObsolete; - - - private Builder(PkgType type) { - mType = type; - } - - /** - * Creates a new tool package descriptor. - * - * @param revision The revision of the tool package. - * @param minPlatformToolsRev The {@code min-platform-tools-rev}. - * Use {@link FullRevision#NOT_SPECIFIED} to indicate there is no requirement. - * @return A {@link PkgDesc} describing this tool package. - */ - @NonNull - public static Builder newTool(@NonNull FullRevision revision, - @NonNull FullRevision minPlatformToolsRev) { - Builder p = new Builder(PkgType.PKG_TOOLS); - p.mFullRevision = revision; - p.mMinPlatformToolsRev = minPlatformToolsRev; - return p; - } - - /** - * Creates a new platform-tool package descriptor. - * - * @param revision The revision of the platform-tool package. - * @return A {@link PkgDesc} describing this platform-tool package. - */ - @NonNull - public static Builder newPlatformTool(@NonNull FullRevision revision) { - Builder p = new Builder(PkgType.PKG_PLATFORM_TOOLS); - p.mFullRevision = revision; - return p; - } - - /** - * Creates a new build-tool package descriptor. - * - * @param revision The revision of the build-tool package. - * @return A {@link PkgDesc} describing this build-tool package. - */ - @NonNull - public static Builder newBuildTool(@NonNull FullRevision revision) { - Builder p = new Builder(PkgType.PKG_BUILD_TOOLS); - p.mFullRevision = revision; - p.mCustomIsUpdateFor = new IIsUpdateFor() { - @Override - public boolean isUpdateFor(PkgDesc thisPkgDesc, IPkgDesc existingDesc) { - // Generic test checks that the preview type is the same (both previews or not). - // Build tool is different in that the full revision must be an exact match - // and not an increase. - return thisPkgDesc - .isGenericUpdateFor(existingDesc, PreviewComparison.COMPARE_NUMBER) && - thisPkgDesc.getFullRevision().compareTo( - existingDesc.getFullRevision(), - PreviewComparison.COMPARE_TYPE) == 0; - } - }; - return p; - } - - /** - * Creates a new doc package descriptor. - * - * @param revision The revision of the doc package. - * @return A {@link PkgDesc} describing this doc package. - */ - @NonNull - public static Builder newDoc(@NonNull AndroidVersion version, - @NonNull MajorRevision revision) { - Builder p = new Builder(PkgType.PKG_DOC); - p.mAndroidVersion = version; - p.mMajorRevision = revision; - p.mCustomIsUpdateFor = new IIsUpdateFor() { - @Override - public boolean isUpdateFor(PkgDesc thisPkgDesc, IPkgDesc existingDesc) { - if (existingDesc == null || - !thisPkgDesc.getType().equals(existingDesc.getType())) { - return false; - } - - // This package is unique in the SDK. It's an update if the API is newer - // or the revision is newer for the same API. - int diff = thisPkgDesc.getAndroidVersion().compareTo( - existingDesc.getAndroidVersion()); - return diff > 0 || - (diff == 0 && thisPkgDesc.getMajorRevision().compareTo( - existingDesc.getMajorRevision()) > 0); - } - }; - return p; - } - - /** - * Creates a new extra package descriptor. - * - * @param vendor The vendor id string of the extra package. - * @param path The path id string of the extra package. - * @param displayName The display name. If missing, caller should build one using the path. - * @param oldPaths An optional list of older paths for this extra package. - * @param revision The revision of the extra package. - * @return A {@link PkgDesc} describing this extra package. - */ - @NonNull - public static Builder newExtra(@NonNull IdDisplay vendor, - @NonNull String path, - @Nullable String displayName, - @Nullable String[] oldPaths, - @NonNull NoPreviewRevision revision) { - Builder p = new Builder(PkgType.PKG_EXTRA); - p.mFullRevision = revision; - p.mVendor = vendor; - p.mPath = path; - p.mNameDisplay = displayName; - p.mOldPaths = oldPaths; - return p; - } - - /** - * Creates a new platform package descriptor. - * - * @param version The android version of the platform package. - * @param revision The revision of the extra package. - * @param minToolsRev An optional {@code min-tools-rev}. - * Use {@link FullRevision#NOT_SPECIFIED} to indicate - * there is no requirement. - * @return A {@link PkgDesc} describing this platform package. - */ - @NonNull - public static Builder newPlatform(@NonNull AndroidVersion version, - @NonNull MajorRevision revision, - @NonNull FullRevision minToolsRev) { - Builder p = new Builder(PkgType.PKG_PLATFORM); - p.mAndroidVersion = version; - p.mMajorRevision = revision; - p.mMinToolsRev = minToolsRev; - p.mCustomPath = new IGetPath() { - @Override - public String getPath(PkgDesc thisPkgDesc) { - /** The "path" of a Platform is its Target Hash. */ - return AndroidTargetHash.getPlatformHashString(thisPkgDesc.getAndroidVersion()); - } - }; - return p; - } - - /** - * Create a new add-on package descriptor. - *

- * The vendor id and the name id provided are used to compute the add-on's - * target hash. - * - * @param version The android version of the add-on package. - * @param revision The revision of the add-on package. - * @param addonVendor The vendor id/display of the add-on package. - * @param addonName The name id/display of the add-on package. - * @return A {@link PkgDesc} describing this add-on package. - */ - @NonNull - public static Builder newAddon(@NonNull AndroidVersion version, - @NonNull MajorRevision revision, - @NonNull IdDisplay addonVendor, - @NonNull IdDisplay addonName) { - Builder p = new Builder(PkgType.PKG_ADDON); - p.mAndroidVersion = version; - p.mMajorRevision = revision; - p.mVendor = addonVendor; - p.mNameIdDisplay = addonName; - return p; - } - - /** - * Create a new platform system-image package descriptor. - *

- * For system-images, {@link PkgDesc#getPath()} returns the ABI. - * - * @param version The android version of the system-image package. - * @param tag The tag of the system-image package. - * @param abi The ABI of the system-image package. - * @param revision The revision of the system-image package. - * @return A {@link PkgDesc} describing this system-image package. - */ - @NonNull - public static Builder newSysImg(@NonNull AndroidVersion version, - @NonNull IdDisplay tag, - @NonNull String abi, - @NonNull MajorRevision revision) { - Builder p = new Builder(PkgType.PKG_SYS_IMAGE); - p.mAndroidVersion = version; - p.mMajorRevision = revision; - p.mTag = tag; - p.mPath = abi; - p.mVendor = null; - return p; - } - - /** - * Create a new add-on system-image package descriptor. - *

- * For system-images, {@link PkgDesc#getPath()} returns the ABI. - * - * @param version The android version of the system-image package. - * @param addonVendor The vendor id/display of an associated add-on. - * @param addonName The tag of the system-image package is the add-on name. - * @param abi The ABI of the system-image package. - * @param revision The revision of the system-image package. - * @return A {@link PkgDesc} describing this system-image package. - */ - @NonNull - public static Builder newAddonSysImg(@NonNull AndroidVersion version, - @NonNull IdDisplay addonVendor, - @NonNull IdDisplay addonName, - @NonNull String abi, - @NonNull MajorRevision revision) { - Builder p = new Builder(PkgType.PKG_ADDON_SYS_IMAGE); - p.mAndroidVersion = version; - p.mMajorRevision = revision; - p.mTag = addonName; - p.mPath = abi; - p.mVendor = addonVendor; - return p; - } - - /** - * Create a new source package descriptor. - * - * @param version The android version of the source package. - * @param revision The revision of the source package. - * @return A {@link PkgDesc} describing this source package. - */ - @NonNull - public static Builder newSource(@NonNull AndroidVersion version, - @NonNull MajorRevision revision) { - Builder p = new Builder(PkgType.PKG_SOURCE); - p.mAndroidVersion = version; - p.mMajorRevision = revision; - return p; - } - - /** - * Create a new sample package descriptor. - * - * @param version The android version of the sample package. - * @param revision The revision of the sample package. - * @param minToolsRev An optional {@code min-tools-rev}. - * Use {@link FullRevision#NOT_SPECIFIED} to indicate - * there is no requirement. - * @return A {@link PkgDesc} describing this sample package. - */ - @NonNull - public static Builder newSample(@NonNull AndroidVersion version, - @NonNull MajorRevision revision, - @NonNull FullRevision minToolsRev) { - Builder p = new Builder(PkgType.PKG_SAMPLE); - p.mAndroidVersion = version; - p.mMajorRevision = revision; - p.mMinToolsRev = minToolsRev; - return p; - } - - /** - * Creates a new NDK package descriptor. - * - * @param revision The revision of the NDK package. - * @return A {@link PkgDesc} describing this NDK package. - */ - @NonNull - public static Builder newNdk(@NonNull FullRevision revision) { - Builder p = new Builder(PkgType.PKG_NDK); - p.mFullRevision = revision; - return p; - } - - public Builder setLicense(@Nullable License license) { - mLicense = license; - return this; - } - - public Builder setListDisplay(@Nullable String text) { - mListDisplay = text; - return this; - } - - public Builder setDescriptionShort(@Nullable String text) { - mDescriptionShort = text; - return this; - } - - public Builder setDescriptionUrl(@Nullable String text) { - mDescriptionUrl = text; - return this; - } - - public Builder setIsObsolete(boolean isObsolete) { - mIsObsolete = isObsolete; - return this; - } - - public IPkgDesc create() { - if (mType == PkgType.PKG_ADDON) { - return new PkgDescAddon( - mType, - mLicense, - mListDisplay, - mDescriptionShort, - mDescriptionUrl, - mIsObsolete, - mMajorRevision, - mAndroidVersion, - mVendor, - mNameIdDisplay); - } - - if (mType == PkgType.PKG_EXTRA) { - return new PkgDescExtra( - mType, - mLicense, - mListDisplay, - mDescriptionShort, - mDescriptionUrl, - mIsObsolete, - mFullRevision, - mMajorRevision, - mAndroidVersion, - mPath, - mTag, - mVendor, - mMinToolsRev, - mMinPlatformToolsRev, - mNameDisplay, - mOldPaths); - } - - return new PkgDesc( - mType, - mLicense, - mListDisplay, - mDescriptionShort, - mDescriptionUrl, - mIsObsolete, - mFullRevision, - mMajorRevision, - mAndroidVersion, - mPath, - mTag, - mVendor, - mMinToolsRev, - mMinPlatformToolsRev, - mCustomIsUpdateFor, - mCustomPath); - } - } - - // ---- Helpers ----- - - @NonNull - private static String sanitize(@NonNull String str) { - str = str.toLowerCase(Locale.US).replaceAll("[^a-z0-9_.-]+", "_").replaceAll("_+", "_"); - return str; - } -} - diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/PkgDescAddon.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/PkgDescAddon.java deleted file mode 100644 index 9694d1f8..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/PkgDescAddon.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (C) 2013 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.sdklib.repository.descriptors; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.sdklib.AndroidTargetHash; -import com.android.sdklib.AndroidVersion; -import com.android.sdklib.repository.License; -import com.android.sdklib.repository.MajorRevision; - -/** - * Implementation detail of {@link PkgDesc} for add-ons. - * Do not use this class directly. - * To create an instance use {@link PkgDesc.Builder#newAddon} instead. - */ -final class PkgDescAddon extends PkgDesc implements IPkgDescAddon { - - private final IdDisplay mAddonName; - - /** - * Add-on descriptor. - * The following attributes are mandatory: - */ - PkgDescAddon(@NonNull PkgType type, - @Nullable License license, - @Nullable String listDisplay, - @Nullable String descriptionShort, - @Nullable String descriptionUrl, - boolean isObsolete, - @NonNull MajorRevision majorRevision, - @NonNull AndroidVersion androidVersion, - @NonNull IdDisplay addonVendor, - @NonNull IdDisplay addonName) { - super(type, - license, - listDisplay, - descriptionShort, - descriptionUrl, - isObsolete, - null, //fullRevision - majorRevision, - androidVersion, - AndroidTargetHash.getAddonHashString(addonVendor.getDisplay(), - addonName.getDisplay(), - androidVersion), - null, //tag - addonVendor, - null, //minToolsRev - null, //minPlatformToolsRev - null, //customIsUpdateFor - null); //customPath - - mAddonName = addonName; - } - - @NonNull - @Override - public IdDisplay getName() { - return mAddonName; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/PkgDescExtra.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/PkgDescExtra.java deleted file mode 100644 index b5f2efbf..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/PkgDescExtra.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright (C) 2013 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.sdklib.repository.descriptors; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.sdklib.AndroidVersion; -import com.android.sdklib.repository.License; -import com.android.sdklib.repository.FullRevision; -import com.android.sdklib.repository.MajorRevision; - -/** - * Implementation detail of {@link IPkgDescExtra} for extra packages. - */ -public final class PkgDescExtra extends PkgDesc implements IPkgDescExtra { - - private final String[] mOldPaths; - private final String mNameDisplay; - - PkgDescExtra(@NonNull PkgType type, - @Nullable License license, - @Nullable String listDisplay, - @Nullable String descriptionShort, - @Nullable String descriptionUrl, - boolean isObsolete, - @Nullable FullRevision fullRevision, - @Nullable MajorRevision majorRevision, - @Nullable AndroidVersion androidVersion, - @Nullable String path, - @Nullable IdDisplay tag, - @Nullable IdDisplay vendor, - @Nullable FullRevision minToolsRev, - @Nullable FullRevision minPlatformToolsRev, - @Nullable String nameDisplay, - @Nullable final String[] oldPaths) { - super(type, - license, - listDisplay, - descriptionShort, - descriptionUrl, - isObsolete, - fullRevision, - majorRevision, - androidVersion, - path, - tag, - vendor, - minToolsRev, - minPlatformToolsRev, - null, //customIsUpdateFor - null); //customPath - mNameDisplay = nameDisplay; - mOldPaths = oldPaths != null ? oldPaths : new String[0]; - } - - @NonNull - @Override - public String[] getOldPaths() { - return mOldPaths; - } - - @NonNull - @Override - public String getNameDisplay() { - return mNameDisplay == null ? String.format("Unknown (%s)", getInstallId()) : mNameDisplay; - } - - // ---- Helpers ---- - - /** - * Helper method that converts the old_paths property string into the - * an old paths array. - * - * @param oldPathsProperty A possibly-null old_path property string. - * @return A list of old paths split by their separator. Can be empty but not null. - */ - @NonNull - public static String[] convertOldPaths(@Nullable String oldPathsProperty) { - if (oldPathsProperty == null || oldPathsProperty.isEmpty()) { - return new String[0]; - } - return oldPathsProperty.split(";"); //$NON-NLS-1$ - } - - /** - * Helper to computhe whether the extra path of both {@link IPkgDescExtra}s - * are compatible with each other, which means they are either equal or are - * matched between existing path and the potential old paths list. - *

- * This also covers backward compatibility -- in earlier schemas the vendor id was - * merged into the path string when reloading installed extras. - * - * @param lhs A non-null {@link IPkgDescExtra}. - * @param rhs Another non-null {@link IPkgDescExtra}. - * @return true if the paths are compatible. - */ - public static boolean compatibleVendorAndPath( - @NonNull IPkgDescExtra lhs, - @NonNull IPkgDescExtra rhs) { - String[] epOldPaths = rhs.getOldPaths(); - int lenEpOldPaths = epOldPaths.length; - for (int indexEp = -1; indexEp < lenEpOldPaths; indexEp++) { - if (sameVendorAndPath( - lhs.getVendor().getId(), lhs.getPath(), - rhs.getVendor().getId(), indexEp < 0 ? rhs.getPath() : epOldPaths[indexEp])) { - return true; - } - } - - String[] thisOldPaths = lhs.getOldPaths(); - int lenThisOldPaths = thisOldPaths.length; - for (int indexThis = -1; indexThis < lenThisOldPaths; indexThis++) { - if (sameVendorAndPath( - lhs.getVendor().getId(), indexThis < 0 ? lhs.getPath() : thisOldPaths[indexThis], - rhs.getVendor().getId(), rhs.getPath())) { - return true; - } - } - - return false; - } - - private static boolean sameVendorAndPath( - @Nullable String thisVendor, @Nullable String thisPath, - @Nullable String otherVendor, @Nullable String otherPath) { - // To be backward compatible, we need to support the old vendor-path form - // in either the current or the remote package. - // - // The vendor test below needs to account for an old installed package - // (e.g. with an install path of vendor-name) that has then been updated - // in-place and thus when reloaded contains the vendor name in both the - // path and the vendor attributes. - if (otherPath != null && thisPath != null && thisVendor != null) { - if (otherPath.equals(thisVendor + '-' + thisPath) && - (otherVendor == null || - otherVendor.isEmpty() || - otherVendor.equals(thisVendor))) { - return true; - } - } - if (thisPath != null && otherPath != null && otherVendor != null) { - if (thisPath.equals(otherVendor + '-' + otherPath) && - (thisVendor == null || - thisVendor.isEmpty() || - thisVendor.equals(otherVendor))) { - return true; - } - } - - - if (thisPath != null && thisPath.equals(otherPath)) { - if ((thisVendor == null && otherVendor == null) || - (thisVendor != null && thisVendor.equals(otherVendor))) { - return true; - } - } - - return false; - } - -} - diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/PkgType.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/PkgType.java deleted file mode 100644 index 9f0ccc27..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/descriptors/PkgType.java +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Copyright (C) 2013 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.sdklib.repository.descriptors; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.sdklib.AndroidVersion; -import com.android.sdklib.internal.repository.LocalSdkParser; -import com.android.sdklib.repository.FullRevision; -import com.android.sdklib.repository.MajorRevision; -import com.android.sdklib.repository.local.LocalSdk; - -import java.util.EnumSet; - -/** - * Package types handled by the {@link LocalSdk}. - *

- * Integer bit values are provided via {@link #getIntValue()} for backward - * compatibility with the older {@link LocalSdkParser} class. - * The integer bit values also indicate the natural ordering of the packages. - */ -public enum PkgType implements IPkgCapabilities { - - // Boolean attributes below, in that order: - // maj-r, full-r, api, path, tag, vend, min-t-r, min-pt-r - // - // Corresponding flags for list description pattern string: - // $MAJ $FULL $API $PATH $TAG $VEND $NAME (for extras & add-ons) - // - // - - /** Filter the SDK/tools folder. - * Has {@link FullRevision}. */ - PKG_TOOLS(0x0001, SdkConstants.FD_TOOLS, - "Android SDK Tools $FULL", - false, true /*full-r*/, false, false, false, false, false, true /*min-pt-r*/), - - /** Filter the SDK/platform-tools folder. - * Has {@link FullRevision}. */ - PKG_PLATFORM_TOOLS(0x0002, SdkConstants.FD_PLATFORM_TOOLS, - "Android SDK Platform-Tools $FULL", - false, true /*full-r*/, false, false, false, false, false, false), - - /** Filter the SDK/build-tools folder. - * Has {@link FullRevision}. */ - PKG_BUILD_TOOLS(0x0004, SdkConstants.FD_BUILD_TOOLS, - "Android SDK Build-Tools $FULL", - false, true /*full-r*/, false, false, false, false, false, false), - - /** Filter the SDK/docs folder. - * Has {@link MajorRevision}. */ - PKG_DOC(0x0010, SdkConstants.FD_DOCS, - "Documentation for Android SDK", - true /*maj-r*/, false, true /*api*/, false, false, false, false, false), - - /** Filter the SDK/platforms. - * Has {@link AndroidVersion}. Has {@link MajorRevision}. - * Path returns the platform's target hash. */ - PKG_PLATFORM(0x0100, SdkConstants.FD_PLATFORMS, - "Android SDK Platform $API{?$MAJ>1:, rev $MAJ}", - true /*maj-r*/, false, true /*api*/, true /*path*/, false, false, true /*min-t-r*/, false), - - /** Filter the SDK/system-images/android. - * Has {@link AndroidVersion}. Has {@link MajorRevision}. Has tag. - * Path returns the system image ABI. */ - PKG_SYS_IMAGE(0x0200, SdkConstants.FD_SYSTEM_IMAGES, - "$PATH System Image, Android $API{?$MAJ>1:, rev $MAJ}", - true /*maj-r*/, false, true /*api*/, true /*path*/, true /*tag*/, false /*vend*/, false, false), - - /** Filter the SDK/addons. - * Has {@link AndroidVersion}. Has {@link MajorRevision}. - * Path returns the add-on's target hash. */ - PKG_ADDON(0x0400, SdkConstants.FD_ADDONS, - "{|$NAME|$VEND $PATH|}, Android $API{?$MAJ>1:, rev $MAJ}", - true /*maj-r*/, false, true /*api*/, true /*path*/, false, true /*vend*/, false, false), - - /** Filter the SDK/system-images/addons. - * Has {@link AndroidVersion}. Has {@link MajorRevision}. Has tag. - * Path returns the system image ABI. */ - PKG_ADDON_SYS_IMAGE(0x0800, SdkConstants.FD_SYSTEM_IMAGES, - "{|$NAME|$VEND $PATH|} System Image, Android $API{?$MAJ>1:, rev $MAJ}", - true /*maj-r*/, false, true /*api*/, true /*path*/, true /*tag*/, true /*vend*/, false, false), - - /** Filter the SDK/samples folder. - * Note: this will not detect samples located in the SDK/extras packages. - * Has {@link AndroidVersion}. Has {@link MajorRevision}. */ - PKG_SAMPLE(0x1000, SdkConstants.FD_SAMPLES, - "Samples for Android $API{?$MAJ>1:, rev $MAJ}", - true /*maj-r*/, false, true /*api*/, false, false, false, true /*min-t-r*/, false), - - /** Filter the SDK/sources folder. - * Has {@link AndroidVersion}. Has {@link MajorRevision}. */ - PKG_SOURCE(0x2000, SdkConstants.FD_ANDROID_SOURCES, - "Sources for Android $API{?$MAJ>1:, rev $MAJ}", - true /*maj-r*/, false, true /*api*/, false, false, false, false, false), - - /** Filter the SDK/extras folder. - * Has {@code Path}. Has {@link MajorRevision}. - * Path returns the combined vendor id + extra path. - * Cast the descriptor to {@link IPkgDescExtra} to get extra's specific attributes. */ - PKG_EXTRA(0x4000, SdkConstants.FD_EXTRAS, - "{|$NAME|$VEND $PATH|}{?$FULL>1:, rev $FULL}", - false, true /*full-r*/, false, true /*path*/, false, true /*vend*/, false, false), - - /** The SDK/ndk folder. */ - PKG_NDK(0x8000, SdkConstants.FD_NDK, "", - false, true, false, false, false, false, false, false); - - /** A collection of all the known PkgTypes. */ - public static final EnumSet PKG_ALL = EnumSet.allOf(PkgType.class); - - /** Integer value matching all available pkg types, for the old LocalSdkParer. */ - public static final int PKG_ALL_INT = 0xFFFF; - - private int mIntValue; - private String mFolderName; - - private final boolean mHasMajorRevision; - private final boolean mHasFullRevision; - private final boolean mHasAndroidVersion; - private final boolean mHasPath; - private final boolean mHasTag; - private final boolean mHasVendor; - private final boolean mHasMinToolsRev; - private final boolean mHasMinPlatformToolsRev; - private final String mListDisplayPattern; - - PkgType(int intValue, - @NonNull String folderName, - @NonNull String listDisplayPattern, - boolean hasMajorRevision, - boolean hasFullRevision, - boolean hasAndroidVersion, - boolean hasPath, - boolean hasTag, - boolean hasVendor, - boolean hasMinToolsRev, - boolean hasMinPlatformToolsRev) { - mIntValue = intValue; - mFolderName = folderName; - mListDisplayPattern = listDisplayPattern; - mHasMajorRevision = hasMajorRevision; - mHasFullRevision = hasFullRevision; - mHasAndroidVersion = hasAndroidVersion; - mHasPath = hasPath; - mHasTag = hasTag; - mHasVendor = hasVendor; - mHasMinToolsRev = hasMinToolsRev; - mHasMinPlatformToolsRev = hasMinPlatformToolsRev; - } - - /** Returns the integer value matching the type, compatible with the old LocalSdkParer. */ - public int getIntValue() { - return mIntValue; - } - - /** Returns the name of SDK top-folder where this type of package is stored. */ - @NonNull - public String getFolderName() { - return mFolderName; - } - - @Override - public boolean hasMajorRevision() { - return mHasMajorRevision; - } - - @Override - public boolean hasFullRevision() { - return mHasFullRevision; - } - - @Override - public boolean hasAndroidVersion() { - return mHasAndroidVersion; - } - - @Override - public boolean hasPath() { - return mHasPath; - } - - @Override - public boolean hasTag() { - return mHasTag; - } - - @Override - public boolean hasVendor() { - return mHasVendor; - } - - @Override - public boolean hasMinToolsRev() { - return mHasMinToolsRev; - } - - @Override - public boolean hasMinPlatformToolsRev() { - return mHasMinPlatformToolsRev; - } - - /* - * Returns a pattern string used by {@link PkgDesc#getListDescription()} to - * compute a default list-display representation string for this package. - */ - public String getListDisplayPattern() { - return mListDisplayPattern; - } -} - diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalAddonPkgInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalAddonPkgInfo.java deleted file mode 100644 index bce325e2..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalAddonPkgInfo.java +++ /dev/null @@ -1,503 +0,0 @@ -/* - * Copyright (C) 2013 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.sdklib.repository.local; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.sdklib.AndroidVersion; -import com.android.sdklib.IAndroidTarget; -import com.android.sdklib.ISystemImage; -import com.android.sdklib.ISystemImage.LocationType; -import com.android.sdklib.SystemImage; -import com.android.sdklib.internal.androidTarget.AddOnTarget; -import com.android.sdklib.internal.androidTarget.PlatformTarget; -import com.android.sdklib.internal.project.ProjectProperties; -import com.android.sdklib.io.LegacyFileOp; -import com.android.sdklib.io.IFileOp; -import com.android.sdklib.repository.AddonManifestIniProps; -import com.android.sdklib.repository.FullRevision; -import com.android.sdklib.repository.MajorRevision; -import com.android.sdklib.repository.descriptors.*; -import com.android.utils.Pair; -import com.google.common.base.Objects; -import com.google.common.collect.SetMultimap; -import com.google.common.collect.TreeMultimap; - -import java.io.File; -import java.io.FileNotFoundException; -import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -@SuppressWarnings("MethodMayBeStatic") -public class LocalAddonPkgInfo extends LocalPlatformPkgInfo { - - private static final Pattern PATTERN_LIB_DATA = Pattern.compile( - "^([a-zA-Z0-9._-]+\\.jar);(.*)$", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$ - - // usb ids are 16-bit hexadecimal values. - private static final Pattern PATTERN_USB_IDS = Pattern.compile( - "^0x[a-f0-9]{4}$", Pattern.CASE_INSENSITIVE); //$NON-NLS-1$ - - @NonNull - private final IPkgDescAddon mAddonDesc; - - public LocalAddonPkgInfo(@NonNull LocalSdk localSdk, - @NonNull File localDir, - @NonNull Properties sourceProps, - @NonNull AndroidVersion version, - @NonNull MajorRevision revision, - @NonNull IdDisplay vendor, - @NonNull IdDisplay name) { - super(localSdk, localDir, sourceProps, version, revision, FullRevision.NOT_SPECIFIED); - mAddonDesc = (IPkgDescAddon) PkgDesc.Builder.newAddon(version, revision, vendor, name) - .create(); - } - - @NonNull - @Override - public IPkgDesc getDesc() { - return mAddonDesc; - } - - /** The "path" of an add-on is its Target Hash. */ - @Override - @NonNull - public String getTargetHash() { - return getDesc().getPath(); - } - - //----- - - /** - * Computes a sanitized name-id based on an addon name-display. - * This is used to provide compatibility with older add-ons that lacks the new fields. - * - * @param displayName A name-display field or a old-style name field. - * @return A non-null sanitized name-id that fits in the {@code [a-zA-Z0-9_-]+} pattern. - */ - public static String sanitizeDisplayToNameId(@NonNull String displayName) { - String name = displayName.toLowerCase(Locale.US); - name = name.replaceAll("[^a-z0-9_-]+", "_"); //$NON-NLS-1$ //$NON-NLS-2$ - name = name.replaceAll("_+", "_"); //$NON-NLS-1$ //$NON-NLS-2$ - - // Trim leading and trailing underscores - if (name.length() > 1) { - name = name.replaceAll("^_+", ""); //$NON-NLS-1$ //$NON-NLS-2$ - } - if (name.length() > 1) { - name = name.replaceAll("_+$", ""); //$NON-NLS-1$ //$NON-NLS-2$ - } - return name; - } - - //----- - - /** - * Creates the AddOnTarget. Invoked by {@link #getAndroidTarget()}. - */ - @Override - @Nullable - protected IAndroidTarget createAndroidTarget() { - LocalSdk sdk = getLocalSdk(); - IFileOp fileOp = sdk.getFileOp(); - - // Parse the addon properties to ensure we can load it. - Pair, String> infos = parseAddonProperties(); - - Map propertyMap = infos.getFirst(); - String error = infos.getSecond(); - - if (error != null) { - appendLoadError("Ignoring add-on '%1$s': %2$s", getLocalDir().getName(), error); - return null; - } - - // Since error==null we're not supposed to encounter any issues loading this add-on. - try { - assert propertyMap != null; - - String api = propertyMap.get(AddonManifestIniProps.ADDON_API); - String name = propertyMap.get(AddonManifestIniProps.ADDON_NAME); - String vendor = propertyMap.get(AddonManifestIniProps.ADDON_VENDOR); - - assert api != null; - assert name != null; - assert vendor != null; - - PlatformTarget baseTarget = null; - - // Look for a platform that has a matching api level or codename. - LocalPkgInfo plat = sdk.getPkgInfo(PkgType.PKG_PLATFORM, - getDesc().getAndroidVersion()); - if (plat instanceof LocalPlatformPkgInfo) { - baseTarget = (PlatformTarget) ((LocalPlatformPkgInfo) plat).getAndroidTarget(); - } - assert baseTarget != null; - - // get the optional description - String description = propertyMap.get(AddonManifestIniProps.ADDON_DESCRIPTION); - - // get the add-on revision - int revisionValue = 1; - String revision = propertyMap.get(AddonManifestIniProps.ADDON_REVISION); - if (revision == null) { - revision = propertyMap.get(AddonManifestIniProps.ADDON_REVISION_OLD); - } - if (revision != null) { - revisionValue = Integer.parseInt(revision); - } - - // get the optional libraries - String librariesValue = propertyMap.get(AddonManifestIniProps.ADDON_LIBRARIES); - Map libMap = null; - - if (librariesValue != null) { - librariesValue = librariesValue.trim(); - if (!librariesValue.isEmpty()) { - // split in the string into the libraries name - String[] libraries = librariesValue.split(";"); //$NON-NLS-1$ - if (libraries.length > 0) { - libMap = new HashMap(); - for (String libName : libraries) { - libName = libName.trim(); - - // get the library data from the properties - String libData = propertyMap.get(libName); - - if (libData != null) { - // split the jar file from the description - Matcher m = PATTERN_LIB_DATA.matcher(libData); - if (m.matches()) { - libMap.put(libName, new String[] { - m.group(1), m.group(2) }); - } else { - appendLoadError( - "Ignoring library '%1$s', property value has wrong format\n\t%2$s", - libName, libData); - } - } else { - appendLoadError( - "Ignoring library '%1$s', missing property value", - libName, libData); - } - } - } - } - } - - // get the abi list. - ISystemImage[] systemImages = getAddonSystemImages(fileOp); - - // check whether the add-on provides its own rendering info/library. - boolean hasRenderingLibrary = false; - boolean hasRenderingResources = false; - - File dataFolder = new File(getLocalDir(), SdkConstants.FD_DATA); - if (fileOp.isDirectory(dataFolder)) { - hasRenderingLibrary = - fileOp.isFile(new File(dataFolder, SdkConstants.FN_LAYOUTLIB_JAR)); - hasRenderingResources = - fileOp.isDirectory(new File(dataFolder, SdkConstants.FD_RES)) && - fileOp.isDirectory(new File(dataFolder, SdkConstants.FD_FONTS)); - } - - AddOnTarget target = new AddOnTarget( - getLocalDir().getAbsolutePath(), - name, - vendor, - revisionValue, - description, - systemImages, - libMap, - hasRenderingLibrary, - hasRenderingResources, - baseTarget); - - // parse the legacy skins, located under SDK/addons/addon-name/skins/[skin-name] - // and merge with the system-image skins, if any, merging them by name. - File targetSkinFolder = target.getFile(IAndroidTarget.SKINS); - - Map skinsMap = new TreeMap(); - - for (File f : PackageParserUtils.parseSkinFolder(targetSkinFolder, fileOp)) { - skinsMap.put(f.getName().toLowerCase(Locale.US), f); - } - for (ISystemImage si : systemImages) { - for (File f : si.getSkins()) { - skinsMap.put(f.getName().toLowerCase(Locale.US), f); - } - } - - List skins = new ArrayList(skinsMap.values()); - Collections.sort(skins); - - // get the default skin - File defaultSkin = null; - String defaultSkinName = propertyMap.get(AddonManifestIniProps.ADDON_DEFAULT_SKIN); - if (defaultSkinName != null) { - defaultSkin = new File(targetSkinFolder, defaultSkinName); - } else { - // No default skin name specified, use the first one from the addon - // or the default from the platform. - if (skins.size() == 1) { - defaultSkin = skins.get(0); - } else { - defaultSkin = baseTarget.getDefaultSkin(); - } - } - - // get the USB ID (if available) - int usbVendorId = convertId(propertyMap.get(AddonManifestIniProps.ADDON_USB_VENDOR)); - if (usbVendorId != IAndroidTarget.NO_USB_ID) { - target.setUsbVendorId(usbVendorId); - } - - target.setSkins(skins.toArray(new File[skins.size()]), defaultSkin); - - return target; - - } catch (Exception e) { - appendLoadError("Ignoring add-on '%1$s': error %2$s.", - getLocalDir().getName(), e.toString()); - } - - return null; - - } - - /** - * Parses the add-on properties and decodes any error that occurs when loading an addon. - * - * @return A pair with the property map and an error string. Both can be null but not at the - * same time. If a non-null error is present then the property map must be ignored. The error - * should be translatable as it might show up in the SdkManager UI. - */ - @NonNull - private Pair, String> parseAddonProperties() { - Map propertyMap = null; - String error = null; - - IFileOp fileOp = getLocalSdk().getFileOp(); - File addOnManifest = new File(getLocalDir(), SdkConstants.FN_MANIFEST_INI); - - do { - if (!fileOp.isFile(addOnManifest)) { - error = String.format("File not found: %1$s", SdkConstants.FN_MANIFEST_INI); - break; - } - - try { - propertyMap = ProjectProperties.parsePropertyStream( - fileOp.newFileInputStream(addOnManifest), - addOnManifest.getPath(), - null /*log*/); - if (propertyMap == null) { - error = String.format("Failed to parse properties from %1$s", - SdkConstants.FN_MANIFEST_INI); - break; - } - } catch (FileNotFoundException e) { - // this can happen if the system fails to open the file because of too many - // open files. - error = String.format("Failed to parse properties from %1$s: %2$s", - SdkConstants.FN_MANIFEST_INI, e.getMessage()); - break; - } - - // look for some specific values in the map. - // we require name, vendor, and api - String name = propertyMap.get(AddonManifestIniProps.ADDON_NAME); - if (name == null) { - error = addonManifestWarning(AddonManifestIniProps.ADDON_NAME); - break; - } - - String vendor = propertyMap.get(AddonManifestIniProps.ADDON_VENDOR); - if (vendor == null) { - error = addonManifestWarning(AddonManifestIniProps.ADDON_VENDOR); - break; - } - - String api = propertyMap.get(AddonManifestIniProps.ADDON_API); - if (api == null) { - error = addonManifestWarning(AddonManifestIniProps.ADDON_API); - break; - } - - // Look for a platform that has a matching api level or codename. - IAndroidTarget baseTarget = null; - LocalPkgInfo plat = getLocalSdk().getPkgInfo(PkgType.PKG_PLATFORM, - getDesc().getAndroidVersion()); - if (plat instanceof LocalPlatformPkgInfo) { - baseTarget = ((LocalPlatformPkgInfo) plat).getAndroidTarget(); - } - - if (baseTarget == null) { - error = String.format("Unable to find base platform with API level '%1$s'", api); - break; - } - - // get the add-on revision - String revision = propertyMap.get(AddonManifestIniProps.ADDON_REVISION); - if (revision == null) { - revision = propertyMap.get(AddonManifestIniProps.ADDON_REVISION_OLD); - } - if (revision != null) { - try { - Integer.parseInt(revision); - } catch (NumberFormatException e) { - // looks like revision does not parse to a number. - error = String.format("%1$s is not a valid number in %2$s.", - AddonManifestIniProps.ADDON_REVISION, SdkConstants.FN_BUILD_PROP); - break; - } - } - - } while(false); - - return Pair.of(propertyMap, error); - } - - /** - * Prepares a warning about the addon being ignored due to a missing manifest value. - * This string will show up in the SdkManager UI. - * - * @param valueName The missing manifest value, for display. - */ - @NonNull - private static String addonManifestWarning(@NonNull String valueName) { - return String.format("'%1$s' is missing from %2$s.", - valueName, SdkConstants.FN_MANIFEST_INI); - } - - /** - * Converts a string representation of an hexadecimal ID into an int. - * @param value the string to convert. - * @return the int value, or {@link IAndroidTarget#NO_USB_ID} if the conversion failed. - */ - private int convertId(@Nullable String value) { - if (value != null && !value.isEmpty()) { - if (PATTERN_USB_IDS.matcher(value).matches()) { - String v = value.substring(2); - try { - return Integer.parseInt(v, 16); - } catch (NumberFormatException e) { - // this shouldn't happen since we check the pattern above, but this is safer. - // the method will return 0 below. - } - } - } - - return IAndroidTarget.NO_USB_ID; - } - - /** - * Get all the system images supported by an add-on target. - * For an add-on, we first look in the new sdk/system-images folders then we look - * for sub-folders in the addon/images directory. - * If none are found but the directory exists and is not empty, assume it's a legacy - * arm eabi system image. - * If any given API appears twice or more, the first occurrence wins. - *

- * Note that it's OK for an add-on to have no system-images at all, since it can always - * rely on the ones from its base platform. - * - * @param fileOp File operation wrapper. - * @return an array of ISystemImage containing all the system images for the target. - * The list can be empty but not null. - */ - @NonNull - private ISystemImage[] getAddonSystemImages(IFileOp fileOp) { - Set found = new TreeSet(); - SetMultimap tagToAbiFound = TreeMultimap.create(); - - - // Look in the system images folders: - // - SDK/system-image/platform/addon-id-tag/abi - // - SDK/system-image/addon-id-tag/abi (many abi possible) - // Optional: look for skins under - // - SDK/system-image/platform/addon-id-tag/abi/skins/skin-name - // - SDK/system-image/addon-id-tag/abi/skins/skin-name - // If we find multiple occurrences of the same platform/abi, the first one read wins. - - LocalPkgInfo[] sysImgInfos = getLocalSdk().getPkgsInfos(PkgType.PKG_ADDON_SYS_IMAGE); - for (LocalPkgInfo pkg : sysImgInfos) { - IPkgDesc d = pkg.getDesc(); - if (pkg instanceof LocalAddonSysImgPkgInfo && - d.hasVendor() && - mAddonDesc.getVendor().equals(d.getVendor()) && - mAddonDesc.getName().equals(d.getTag()) && - Objects.equal(mAddonDesc.getAndroidVersion(), pkg.getDesc().getAndroidVersion())) { - final IdDisplay tag = mAddonDesc.getName(); - final String abi = d.getPath(); - if (abi != null && !tagToAbiFound.containsEntry(tag, abi)) { - found.add(((LocalAddonSysImgPkgInfo)pkg).getSystemImage()); - tagToAbiFound.put(tag, abi); - } - } - } - - // Look for sub-directories: - // - SDK/addons/addon-name/images/abi (multiple abi possible) - // - SDK/addons/addon-name/armeabi (legacy support) - boolean useLegacy = true; - boolean hasImgFiles = false; - final IdDisplay defaultTag = SystemImage.DEFAULT_TAG; - - File imagesDir = new File(getLocalDir(), SdkConstants.OS_IMAGES_FOLDER); - File[] files = fileOp.listFiles(imagesDir); - for (File file : files) { - if (fileOp.isDirectory(file)) { - useLegacy = false; - String abi = file.getName(); - if (!tagToAbiFound.containsEntry(defaultTag, abi)) { - found.add(new SystemImage( - file, - LocationType.IN_IMAGES_SUBFOLDER, - SystemImage.DEFAULT_TAG, - mAddonDesc.getVendor(), - abi, - LegacyFileOp.EMPTY_FILE_ARRAY)); - tagToAbiFound.put(defaultTag, abi); - } - } else if (!hasImgFiles && fileOp.isFile(file)) { - if (file.getName().endsWith(".img")) { //$NON-NLS-1$ - // The legacy images folder is only valid if it contains some .img files - hasImgFiles = true; - } - } - } - - if (useLegacy && - hasImgFiles && - fileOp.isDirectory(imagesDir) && - !tagToAbiFound.containsEntry(defaultTag, SdkConstants.ABI_ARMEABI)) { - // We found no sub-folder system images but it looks like the top directory - // has some img files in it. It must be a legacy ARM EABI system image folder. - found.add(new SystemImage( - imagesDir, - LocationType.IN_LEGACY_FOLDER, - SystemImage.DEFAULT_TAG, - SdkConstants.ABI_ARMEABI, - LegacyFileOp.EMPTY_FILE_ARRAY)); - } - - return found.toArray(new ISystemImage[found.size()]); - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalAddonSysImgPkgInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalAddonSysImgPkgInfo.java deleted file mode 100644 index e02c8e95..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalAddonSysImgPkgInfo.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2014 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.sdklib.repository.local; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.sdklib.AndroidVersion; -import com.android.sdklib.ISystemImage; -import com.android.sdklib.repository.MajorRevision; -import com.android.sdklib.repository.descriptors.IPkgDesc; -import com.android.sdklib.repository.descriptors.IdDisplay; -import com.android.sdklib.repository.descriptors.PkgDesc; - -import java.io.File; -import java.util.Properties; - -/** - * Local add-on system-image package, for a given addon's {@link AndroidVersion} and given ABI. - * The system-image tag is the add-on name. - * The package itself has a major revision. - * There should be only one for a given android platform version & ABI. - */ -public class LocalAddonSysImgPkgInfo extends LocalPkgInfo { - - - @NonNull - private final IPkgDesc mDesc; - - public LocalAddonSysImgPkgInfo(@NonNull LocalSdk localSdk, - @NonNull File localDir, - @NonNull Properties sourceProps, - @NonNull AndroidVersion version, - @Nullable IdDisplay addonVendor, - @Nullable IdDisplay addonName, - @NonNull String abi, - @NonNull MajorRevision revision) { - super(localSdk, localDir, sourceProps); - mDesc = PkgDesc.Builder.newAddonSysImg(version, addonVendor, addonName, abi, revision) - .create(); - } - - @NonNull - @Override - public IPkgDesc getDesc() { - return mDesc; - } - - public ISystemImage getSystemImage() { - return LocalSysImgPkgInfo.getSystemImage(mDesc, getLocalDir(), getLocalSdk().getFileOp()); - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalBuildToolPkgInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalBuildToolPkgInfo.java deleted file mode 100644 index 0251cc3c..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalBuildToolPkgInfo.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (C) 2013 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.sdklib.repository.local; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.sdklib.BuildToolInfo; -import com.android.sdklib.repository.FullRevision; -import com.android.sdklib.repository.descriptors.IPkgDesc; -import com.android.sdklib.repository.descriptors.PkgDesc; - -import java.io.File; -import java.util.Properties; - -public class LocalBuildToolPkgInfo extends LocalPkgInfo { - - - @Nullable - private final BuildToolInfo mBuildToolInfo; - @NonNull - private final IPkgDesc mDesc; - - public LocalBuildToolPkgInfo(@NonNull LocalSdk localSdk, - @NonNull File localDir, - @NonNull Properties sourceProps, - @NonNull FullRevision revision, - @Nullable BuildToolInfo btInfo) { - super(localSdk, localDir, sourceProps); - mDesc = PkgDesc.Builder.newBuildTool(revision).create(); - mBuildToolInfo = btInfo; - } - - @NonNull - @Override - public IPkgDesc getDesc() { - return mDesc; - } - - @Nullable - public BuildToolInfo getBuildToolInfo() { - return mBuildToolInfo; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalDirInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalDirInfo.java deleted file mode 100644 index 4414fc6c..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalDirInfo.java +++ /dev/null @@ -1,275 +0,0 @@ -/* - * Copyright (C) 2014 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.sdklib.repository.local; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.sdklib.io.LegacyFileOp; -import com.android.sdklib.io.IFileOp; - -import java.io.File; -import java.io.InputStream; -import java.util.Arrays; -import java.util.Comparator; -import java.util.Map; -import java.util.zip.Adler32; - -/** - * Keeps information on a visited directory to quickly determine if it - * has changed later. A directory has changed if its timestamp has been - * modified, or if an underlying source.properties file has changed in - * timestamp or checksum. - *

- * Note that depending on the filesystem & OS, the content of the files in - * a directory can change without the directory's last-modified property - * changing. To have a consistent behavior between OSes, we compute a quick - * checksum using all the files & directories modified timestamps. - * The content of files is not included though, except for the checksum on - * the source.property file since this one is the most important for the SDK. - *

- * The {@link #hashCode()} and {@link #equals(Object)} methods directly - * defer to the underlying File object. This allows the DirInfo to be placed - * into a map and still call {@link Map#containsKey(Object)} with a File - * object to check whether there's a corresponding DirInfo in the map. - */ -class LocalDirInfo { - @NonNull - private final IFileOp mFileOp; - @NonNull - private final File mDir; - private final long mDirModifiedTS; - private final long mDirChecksum; - private final long mPropsModifiedTS; - private final long mPropsChecksum; - - /** - * Creates a new immutable {@link LocalDirInfo}. - * - * @param LegacyFileOp The {@link LegacyFileOp} to use for all file-based interactions. - * @param dir The platform/addon directory of the target. It should be a directory. - */ - public LocalDirInfo(@NonNull IFileOp LegacyFileOp, @NonNull File dir) { - mFileOp = LegacyFileOp; - mDir = dir; - mDirModifiedTS = mFileOp.lastModified(dir); - - // Capture some info about the source.properties file if it exists. - // We use propsModifiedTS == 0 to mean there is no props file. - long propsChecksum = 0; - long propsModifiedTS = 0; - File props = new File(dir, SdkConstants.FN_SOURCE_PROP); - if (mFileOp.isFile(props)) { - propsModifiedTS = mFileOp.lastModified(props); - propsChecksum = getFileChecksum(props); - } - mPropsModifiedTS = propsModifiedTS; - mPropsChecksum = propsChecksum; - mDirChecksum = getDirChecksum(mDir); - } - - /** - * Checks whether the directory/source.properties attributes have changed. - * - * @return True if the directory modified timestamp or - * its source.property files have changed. - */ - public boolean hasChanged() { - // Does platform directory still exist? - if (!mFileOp.isDirectory(mDir)) { - return true; - } - // Has platform directory modified-timestamp changed? - if (mDirModifiedTS != mFileOp.lastModified(mDir)) { - return true; - } - - File props = new File(mDir, SdkConstants.FN_SOURCE_PROP); - - // The directory did not have a props file if target was null or - // if mPropsModifiedTS is 0. - boolean hadProps = mPropsModifiedTS != 0; - - // Was there a props file and it vanished, or there wasn't and there's one now? - if (hadProps != mFileOp.isFile(props)) { - return true; - } - - if (hadProps) { - // Has source.props file modified-timestamp changed? - if (mPropsModifiedTS != mFileOp.lastModified(props)) { - return true; - } - // Had the content of source.props changed? - if (mPropsChecksum != getFileChecksum(props)) { - return true; - } - } - - // Has the deep directory checksum changed? - if (mDirChecksum != getDirChecksum(mDir)) { - return true; - } - - return false; - } - - /** - * Computes an adler32 checksum (source.props are small files, so this - * should be OK with an acceptable collision rate.) - */ - private long getFileChecksum(@NonNull File file) { - InputStream fis = null; - try { - fis = mFileOp.newFileInputStream(file); - Adler32 a = new Adler32(); - byte[] buf = new byte[1024]; - int n; - while ((n = fis.read(buf)) > 0) { - a.update(buf, 0, n); - } - return a.getValue(); - } catch (Exception ignore) { - } finally { - try { - if (fis != null) { - fis.close(); - } - } catch(Exception ignore) {} - } - return 0; - } - - /** - * Computes a checksum using the last-modified attributes of all - * the files and first-leveldirectories in this root directory. - *

- * Heuristic: the SDK Manager updates package by replacing whole directories - * so we don't need to do a recursive deep-first checksum of all files. Only - * the top-level of the package directory should be sufficient to detect - * SDK updates. - */ - private long getDirChecksum(@NonNull File dir) { - long checksum = mFileOp.lastModified(dir); - - // Get the file & directory list sorted by case-insensitive name - // to make the checksum more consistent. - File[] files = mFileOp.listFiles(dir); - Arrays.sort(files, new Comparator() { - @Override - public int compare(File o1, File o2) { - return o1.getName().compareToIgnoreCase(o2.getName()); - } - }); - for (File file : files) { - checksum = 31 * checksum | mFileOp.lastModified(file); - } - return checksum; - } - - /** Returns a visual representation of this object for debugging. */ - @Override - public String toString() { - String s = String.format(""; //$NON-NLS-1$ - } - - /** - * Returns the hashCode of the underlying File object. - *

- * When a {@link LocalDirInfo} is placed in a map, what matters is to use the underlying - * File object as the key so {@link #hashCode()} and {@link #equals(Object)} both - * return the properties of the underlying File object. - * - * @see File#hashCode() - */ - @Override - public int hashCode() { - return mDir.hashCode(); - } - - /** - * Checks equality of the underlying File object. - *

- * When a {@link LocalDirInfo} is placed in a map, what matters is to use the underlying - * File object as the key so {@link #hashCode()} and {@link #equals(Object)} both - * return the properties of the underlying File object. - * - * @see File#equals(Object) - */ - @Override - public boolean equals(Object obj) { - if (obj instanceof File) { - return mDir.equals(obj); - } else if (obj instanceof LocalDirInfo) { - return mDir.equals(((LocalDirInfo) obj).mDir); - } else if (obj instanceof MapComparator) { - return mDir.equals(((MapComparator) obj).mDir); - } - return false; - } - - /** - * Helper for Map.contains() to make sure we're comparing the inner directory File - * object and not the outer wrapper itself. - */ - public static class MapComparator { - private final File mDir; - - public MapComparator(File dir) { - mDir = dir; - } - - /** - * Returns the hashCode of the underlying File object. - *

- * When a {@link LocalDirInfo} is placed in a map, what matters is to use the underlying - * File object as the key so {@link #hashCode()} and {@link #equals(Object)} both - * return the properties of the underlying File object. - * - * @see File#hashCode() - */ - @Override - public int hashCode() { - return mDir.hashCode(); - } - - /** - * Checks equality of the underlying File object. - *

- * When a {@link LocalDirInfo} is placed in a map, what matters is to use the underlying - * File object as the key so {@link #hashCode()} and {@link #equals(Object)} both - * return the properties of the underlying File object. - * - * @see File#equals(Object) - */ - @Override - public boolean equals(Object obj) { - if (obj instanceof File) { - return mDir.equals(obj); - } else if (obj instanceof LocalDirInfo) { - return mDir.equals(((LocalDirInfo) obj).mDir); - } else if (obj instanceof MapComparator) { - return mDir.equals(((MapComparator) obj).mDir); - } - return false; - } - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalDocPkgInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalDocPkgInfo.java deleted file mode 100644 index 004f4503..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalDocPkgInfo.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2013 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.sdklib.repository.local; - -import com.android.annotations.NonNull; -import com.android.sdklib.AndroidVersion; -import com.android.sdklib.repository.MajorRevision; -import com.android.sdklib.repository.descriptors.IPkgDesc; -import com.android.sdklib.repository.descriptors.PkgDesc; - -import java.io.File; -import java.util.Properties; - -public class LocalDocPkgInfo extends LocalPkgInfo { - - @NonNull - private final IPkgDesc mDesc; - - public LocalDocPkgInfo(@NonNull LocalSdk localSdk, - @NonNull File localDir, - @NonNull Properties sourceProps, - @NonNull AndroidVersion version, - @NonNull MajorRevision revision) { - super(localSdk, localDir, sourceProps); - mDesc = PkgDesc.Builder.newDoc(version, revision).create(); - } - - @NonNull - @Override - public IPkgDesc getDesc() { - return mDesc; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalExtraPkgInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalExtraPkgInfo.java deleted file mode 100644 index 5ec811bd..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalExtraPkgInfo.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright (C) 2013 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.sdklib.repository.local; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.sdklib.repository.NoPreviewRevision; -import com.android.sdklib.repository.descriptors.IPkgDesc; -import com.android.sdklib.repository.descriptors.IPkgDescExtra; -import com.android.sdklib.repository.descriptors.IdDisplay; -import com.android.sdklib.repository.descriptors.PkgDesc; - -import java.io.File; -import java.util.Properties; - -public class LocalExtraPkgInfo extends LocalPkgInfo { - - @NonNull - private final IPkgDescExtra mDesc; - - public LocalExtraPkgInfo(@NonNull LocalSdk localSdk, - @NonNull File localDir, - @NonNull Properties sourceProps, - @NonNull IdDisplay vendor, - @NonNull String path, - @Nullable String displayName, - @NonNull String[] oldPaths, - @NonNull NoPreviewRevision revision) { - super(localSdk, localDir, sourceProps); - mDesc = (IPkgDescExtra) PkgDesc.Builder.newExtra( - vendor, - path, - displayName, - oldPaths, - revision).create(); - } - - @NonNull - @Override - public IPkgDesc getDesc() { - return mDesc; - } - - @NonNull - public String[] getOldPaths() { - return mDesc.getOldPaths(); - } - - // --- helpers --- - - /** - * Used to produce a suitable name-display based on the extra's path - * and vendor display string in addon-3 schemas. - * - * @param vendor The vendor id of the extra. - * @param extraPath The non-null path of the extra. - * @return A non-null display name based on the extra's path id. - */ - public static String getPrettyName(@Nullable IdDisplay vendor, @NonNull String extraPath) { - String name = extraPath; - - // In the past, we used to save the extras in a folder vendor-path, - // and that "vendor" would end up in the path when we reload the extra from - // disk. Detect this and compensate. - String disp = vendor == null ? null : vendor.getDisplay(); - if (disp != null && !disp.isEmpty()) { - if (name.startsWith(disp + "-")) { //$NON-NLS-1$ - name = name.substring(disp.length() + 1); - } - } - - // Uniformize all spaces in the name - if (name != null) { - name = name.replaceAll("[ _\t\f-]+", " ").trim(); //$NON-NLS-1$ //$NON-NLS-2$ - } - if (name == null || name.isEmpty()) { - name = "Unknown Extra"; - } - - if (disp != null && !disp.isEmpty()) { - name = disp + " " + name; //$NON-NLS-1$ - name = name.replaceAll("[ _\t\f-]+", " ").trim(); //$NON-NLS-1$ //$NON-NLS-2$ - } - - // Look at all lower case characters in range [1..n-1] and replace them by an upper - // case if they are preceded by a space. Also upper cases the first character of the - // string. - boolean changed = false; - char[] chars = name.toCharArray(); - for (int n = chars.length - 1, i = 0; i < n; i++) { - if (Character.isLowerCase(chars[i]) && (i == 0 || chars[i - 1] == ' ')) { - chars[i] = Character.toUpperCase(chars[i]); - changed = true; - } - } - if (changed) { - name = new String(chars); - } - - // Special case: reformat a few typical acronyms. - name = name.replaceAll(" Usb ", " USB "); //$NON-NLS-1$ - name = name.replaceAll(" Api ", " API "); //$NON-NLS-1$ - - return name; - } -} - diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalNdkPkgInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalNdkPkgInfo.java deleted file mode 100644 index 88583005..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalNdkPkgInfo.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2015 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.sdklib.repository.local; - -import com.android.annotations.NonNull; -import com.android.sdklib.repository.FullRevision; -import com.android.sdklib.repository.descriptors.IPkgDesc; -import com.android.sdklib.repository.descriptors.PkgDesc; - -import java.io.File; -import java.util.Properties; - -/** - * Local package representing the Android NDK - */ -public class LocalNdkPkgInfo extends LocalPkgInfo { - @NonNull - private final IPkgDesc mDesc; - - protected LocalNdkPkgInfo(@NonNull LocalSdk localSdk, - @NonNull File localDir, - @NonNull Properties sourceProps, - @NonNull FullRevision revision) { - super(localSdk, localDir, sourceProps); - mDesc = PkgDesc.Builder.newNdk(revision).setDescriptionShort("Android NDK").setListDisplay("Android NDK").create(); - } - - @NonNull - @Override - public IPkgDesc getDesc() { - return mDesc; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalPkgInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalPkgInfo.java deleted file mode 100644 index 4f8982e7..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalPkgInfo.java +++ /dev/null @@ -1,240 +0,0 @@ -/* - * Copyright (C) 2013 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.sdklib.repository.local; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.sdklib.repository.IDescription; -import com.android.sdklib.repository.IListDescription; -import com.android.sdklib.io.LegacyFileOp; -import com.android.sdklib.repository.descriptors.IPkgDesc; - -import java.io.File; -import java.util.Properties; - -/** - * Information about a locally installed package. - *

- * Local package information is retrieved via the {@link LocalSdk} object. - * Clients should not need to create instances of {@link LocalPkgInfo} directly. - * Instead please use the {@link LocalSdk} methods to parse and retrieve packages. - *

- */ -public abstract class LocalPkgInfo - implements IDescription, IListDescription, Comparable { - - private final LocalSdk mLocalSdk; - private final File mLocalDir; - private final Properties mSourceProperties; - - private String mLoadError; - - protected LocalPkgInfo(@NonNull LocalSdk localSdk, @NonNull File localDir, @NonNull Properties sourceProps) { - mLocalSdk = localSdk; - mLocalDir = localDir; - mSourceProperties = sourceProps; - } - - //---- Attributes ---- - - @NonNull - public LocalSdk getLocalSdk() { - return mLocalSdk; - } - - @NonNull - public File getLocalDir() { - return mLocalDir; - } - - @NonNull - public Properties getSourceProperties() { - return mSourceProperties; - } - - @Nullable - public String getLoadError() { - return mLoadError; - } - - // ---- - - /** - * Returns the {@link IPkgDesc} describing this package. - */ - @NonNull - public abstract IPkgDesc getDesc(); - - - //---- Ordering ---- - - /** - * Comparison is solely done based on the {@link IPkgDesc}. - *

- * Other local attributes (local directory, source properties) - * are not used in the comparison. Consequently {@link #compareTo(LocalPkgInfo)} - * does not match {@link #equals(Object)} and the {@link #hashCode()} properties. - */ - @Override - public int compareTo(@NonNull LocalPkgInfo o) { - return getDesc().compareTo(o.getDesc()); - } - - /** - * String representation for debugging purposes. - */ - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append('<').append(this.getClass().getSimpleName()).append(' '); - builder.append(getDesc().toString()); - builder.append('>'); - return builder.toString(); - } - - /** - * Computes a hash code specific to this instance based on the underlying - * {@link IPkgDesc} but also specific local properties such a local directory, - * and actual source properties. - */ - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((getDesc() == null) ? 0 : getDesc().hashCode()); - result = prime * result + ((mLocalDir == null) ? 0 : mLocalDir.hashCode()); - result = prime * result + ((mSourceProperties == null) ? 0 : mSourceProperties.hashCode()); - return result; - } - - /** - * Computes object equality to this instance based on the underlying - * {@link IPkgDesc} but also specific local properties such a local directory, - * update available and actual source properties. This is different from - * the behavior of {@link #compareTo(LocalPkgInfo)} which only uses the - * {@link IPkgDesc} for ordering. - */ - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (!(obj instanceof LocalPkgInfo)) { - return false; - } - LocalPkgInfo other = (LocalPkgInfo)obj; - - if (!getDesc().equals(other.getDesc())) { - return false; - } - if (mLocalDir == null) { - if (other.mLocalDir != null) { - return false; - } - } - else if (!mLocalDir.equals(other.mLocalDir)) { - return false; - } - if (mSourceProperties == null) { - if (other.mSourceProperties != null) { - return false; - } - } - else if (!mSourceProperties.equals(other.mSourceProperties)) { - return false; - } - return true; - } - - - //---- Package Management ---- - - /** - * A "broken" package is installed but is not fully operational. - *

- * For example an addon that lacks its underlying platform or a tool package - * that lacks some of its binaries or essentially files. - *

- * Operational code should generally ignore broken packages. - * Only the SDK Updater cares about displaying them so that they can be fixed. - */ - public boolean hasLoadError() { - return mLoadError != null; - } - - void appendLoadError(@NonNull String format, Object... params) { - String loadError = String.format(format, params); - if (mLoadError == null) { - mLoadError = loadError; - } - else { - mLoadError = mLoadError + '\n' + loadError; - } - } - - @NonNull - @Override - public String getListDescription() { - return getDesc().getListDescription(); - } - - @Override - public String getShortDescription() { - // TODO revisit to differentiate from list-description depending - // on how we'll use it in the sdkman UI. - return getListDescription(); - } - - @Override - public String getLongDescription() { - StringBuilder sb = new StringBuilder(); - IPkgDesc desc = getDesc(); - - sb.append(desc.getListDescription()).append('\n'); - - if (desc.hasVendor()) { - assert desc.getVendor() != null; - sb.append("By ").append(desc.getVendor().getDisplay()).append('\n'); - } - - if (desc.hasMinPlatformToolsRev()) { - assert desc.getMinPlatformToolsRev() != null; - sb.append("Requires Platform-Tools revision ").append(desc.getMinPlatformToolsRev().toShortString()).append('\n'); - } - - if (desc.hasMinToolsRev()) { - assert desc.getMinToolsRev() != null; - sb.append("Requires Tools revision ").append(desc.getMinToolsRev().toShortString()).append('\n'); - } - - sb.append("Location: ").append(mLocalDir.getAbsolutePath()); - - return sb.toString(); - } - - /** - * Deletes the files in the SDK corresponding to this package. - */ - public void delete() { - new LegacyFileOp().deleteFileOrFolder(getLocalDir()); - } - -} - diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalPlatformPkgInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalPlatformPkgInfo.java deleted file mode 100644 index 1ccb2be7..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalPlatformPkgInfo.java +++ /dev/null @@ -1,453 +0,0 @@ -/* - * Copyright (C) 2013 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.sdklib.repository.local; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.annotations.VisibleForTesting; -import com.android.sdklib.AndroidVersion; -import com.android.sdklib.IAndroidTarget; -import com.android.sdklib.IAndroidTarget.OptionalLibrary; -import com.android.sdklib.ISystemImage; -import com.android.sdklib.ISystemImage.LocationType; -import com.android.sdklib.SdkManager.LayoutlibVersion; -import com.android.sdklib.SystemImage; -import com.android.sdklib.internal.androidTarget.OptionalLibraryImpl; -import com.android.sdklib.internal.androidTarget.PlatformTarget; -import com.android.sdklib.internal.project.ProjectProperties; -import com.android.sdklib.io.LegacyFileOp; -import com.android.sdklib.io.IFileOp; -import com.android.sdklib.repository.FullRevision; -import com.android.sdklib.repository.MajorRevision; -import com.android.sdklib.repository.PkgProps; -import com.android.sdklib.repository.descriptors.IPkgDesc; -import com.android.sdklib.repository.descriptors.IdDisplay; -import com.android.sdklib.repository.descriptors.PkgDesc; -import com.android.sdklib.repository.descriptors.PkgType; -import com.google.common.base.Charsets; -import com.google.common.collect.Lists; -import com.google.common.collect.SetMultimap; -import com.google.common.collect.TreeMultimap; -import com.google.common.io.Files; -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; - -import java.io.File; -import java.io.FileNotFoundException; -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.Set; -import java.util.TreeSet; - -@SuppressWarnings("ConstantConditions") -public class LocalPlatformPkgInfo extends LocalPkgInfo { - - public static final String PROP_VERSION_SDK = "ro.build.version.sdk"; //$NON-NLS-1$ - public static final String PROP_VERSION_CODENAME = "ro.build.version.codename"; //$NON-NLS-1$ - public static final String PROP_VERSION_RELEASE = "ro.build.version.release"; //$NON-NLS-1$ - - @NonNull - private final IPkgDesc mDesc; - - /** Android target, lazyly loaded from #getAndroidTarget */ - private IAndroidTarget mTarget; - private boolean mLoaded; - - public LocalPlatformPkgInfo(@NonNull LocalSdk localSdk, - @NonNull File localDir, - @NonNull Properties sourceProps, - @NonNull AndroidVersion version, - @NonNull MajorRevision revision, - @NonNull FullRevision minToolsRev) { - super(localSdk, localDir, sourceProps); - mDesc = PkgDesc.Builder.newPlatform(version, revision, minToolsRev).create(); - } - - @NonNull - @Override - public IPkgDesc getDesc() { - return mDesc; - } - - /** The "path" of a Platform is its Target Hash. */ - @NonNull - public String getTargetHash() { - return getDesc().getPath(); - } - - @Nullable - public IAndroidTarget getAndroidTarget() { - if (!mLoaded) { - mTarget = createAndroidTarget(); - mLoaded = true; - } - return mTarget; - } - - public boolean isLoaded() { - return mLoaded; - } - - //----- - - /** - * Creates the PlatformTarget. Invoked by {@link #getAndroidTarget()}. - */ - @SuppressWarnings("ConstantConditions") - @Nullable - protected IAndroidTarget createAndroidTarget() { - LocalSdk sdk = getLocalSdk(); - IFileOp fileOp = sdk.getFileOp(); - File platformFolder = getLocalDir(); - File buildProp = new File(platformFolder, SdkConstants.FN_BUILD_PROP); - File sourcePropFile = new File(platformFolder, SdkConstants.FN_SOURCE_PROP); - - if (!fileOp.isFile(buildProp) || !fileOp.isFile(sourcePropFile)) { - appendLoadError("Ignoring platform '%1$s': %2$s is missing.", //$NON-NLS-1$ - platformFolder.getName(), - SdkConstants.FN_BUILD_PROP); - return null; - } - - Map platformProp = new HashMap(); - - // add all the property files - Map map = null; - - try { - map = ProjectProperties.parsePropertyStream( - fileOp.newFileInputStream(buildProp), - buildProp.getPath(), - null /*log*/); - if (map != null) { - platformProp.putAll(map); - } - } catch (FileNotFoundException ignore) {} - - try { - map = ProjectProperties.parsePropertyStream( - fileOp.newFileInputStream(sourcePropFile), - sourcePropFile.getPath(), - null /*log*/); - if (map != null) { - platformProp.putAll(map); - } - } catch (FileNotFoundException ignore) {} - - File sdkPropFile = new File(platformFolder, SdkConstants.FN_SDK_PROP); - if (fileOp.isFile(sdkPropFile)) { // obsolete platforms don't have this. - try { - map = ProjectProperties.parsePropertyStream( - fileOp.newFileInputStream(sdkPropFile), - sdkPropFile.getPath(), - null /*log*/); - if (map != null) { - platformProp.putAll(map); - } - } catch (FileNotFoundException ignore) {} - } - - // look for some specific values in the map. - - // api level - int apiNumber; - String stringValue = platformProp.get(PROP_VERSION_SDK); - if (stringValue == null) { - appendLoadError("Ignoring platform '%1$s': %2$s is missing from '%3$s'", - platformFolder.getName(), PROP_VERSION_SDK, - SdkConstants.FN_BUILD_PROP); - return null; - } else { - try { - apiNumber = Integer.parseInt(stringValue); - } catch (NumberFormatException e) { - // looks like apiNumber does not parse to a number. - // Ignore this platform. - appendLoadError( - "Ignoring platform '%1$s': %2$s is not a valid number in %3$s.", - platformFolder.getName(), PROP_VERSION_SDK, - SdkConstants.FN_BUILD_PROP); - return null; - } - } - - // Codename must be either null or a platform codename. - // REL means it's a release version and therefore the codename should be null. - AndroidVersion apiVersion = - new AndroidVersion(apiNumber, platformProp.get(PROP_VERSION_CODENAME)); - - // version string - String apiName = platformProp.get(PkgProps.PLATFORM_VERSION); - if (apiName == null) { - apiName = platformProp.get(PROP_VERSION_RELEASE); - } - if (apiName == null) { - appendLoadError( - "Ignoring platform '%1$s': %2$s is missing from '%3$s'", - platformFolder.getName(), PROP_VERSION_RELEASE, - SdkConstants.FN_BUILD_PROP); - return null; - } - - // platform rev number & layoutlib version are extracted from the source.properties - // saved by the SDK Manager when installing the package. - - int revision = 1; - LayoutlibVersion layoutlibVersion = null; - try { - revision = Integer.parseInt(platformProp.get(PkgProps.PKG_REVISION)); - } catch (NumberFormatException e) { - // do nothing, we'll keep the default value of 1. - } - - try { - String propApi = platformProp.get(PkgProps.LAYOUTLIB_API); - String propRev = platformProp.get(PkgProps.LAYOUTLIB_REV); - int llApi = propApi == null ? LayoutlibVersion.NOT_SPECIFIED : - Integer.parseInt(propApi); - int llRev = propRev == null ? LayoutlibVersion.NOT_SPECIFIED : - Integer.parseInt(propRev); - if (llApi > LayoutlibVersion.NOT_SPECIFIED && - llRev >= LayoutlibVersion.NOT_SPECIFIED) { - layoutlibVersion = new LayoutlibVersion(llApi, llRev); - } - } catch (NumberFormatException e) { - // do nothing, we'll ignore the layoutlib version if it's invalid - } - - // api number and name look valid, perform a few more checks - String err = checkPlatformContent(fileOp, platformFolder); - if (err != null) { - appendLoadError("%s", err); //$NLN-NLS-1$ - return null; - } - - ISystemImage[] systemImages = getPlatformSystemImages(fileOp, platformFolder, apiVersion); - - // create the target. - PlatformTarget pt = new PlatformTarget( - sdk.getLocation().getPath(), - platformFolder.getAbsolutePath(), - apiVersion, - apiName, - revision, - layoutlibVersion, - systemImages, - platformProp, - getOptionalLibraries(platformFolder), - sdk.getLatestBuildTool()); - - // add the skins from the platform. Make a copy to not modify the original collection. - List skins = new ArrayList(PackageParserUtils.parseSkinFolder(pt.getFile(IAndroidTarget.SKINS), fileOp)); - - // add the system-image specific skins, if any. - for (ISystemImage systemImage : systemImages) { - skins.addAll(Arrays.asList(systemImage.getSkins())); - } - - pt.setSkins(skins.toArray(new File[skins.size()])); - - // add path to the non-legacy samples package if it exists - LocalPkgInfo samples = sdk.getPkgInfo(PkgType.PKG_SAMPLE, getDesc().getAndroidVersion()); - if (samples != null) { - pt.setSamplesPath(samples.getLocalDir().getAbsolutePath()); - } - - // add path to the non-legacy sources package if it exists - LocalPkgInfo sources = sdk.getPkgInfo(PkgType.PKG_SOURCE, getDesc().getAndroidVersion()); - if (sources != null) { - pt.setSourcesPath(sources.getLocalDir().getAbsolutePath()); - } - - return pt; - } - - /** - * Get all the system images supported by a platform target. - * For a platform, we first look in the new sdk/system-images folders then we - * look for sub-folders in the platform/images directory and/or the one legacy - * folder. - * If any given API appears twice or more, the first occurrence wins. - * - * @param fileOp File operation wrapper. - * @param platformDir Root of the platform target being loaded. - * @param apiVersion API level + codename of platform being loaded. - * @return an array of ISystemImage containing all the system images for the target. - * The list can be empty but not null. - */ - @NonNull - private ISystemImage[] getPlatformSystemImages(IFileOp fileOp, - File platformDir, - AndroidVersion apiVersion) { - Set found = new TreeSet(); - SetMultimap tagToAbiFound = TreeMultimap.create(); - - - // Look in the SDK/system-image/platform-n/tag/abi folders. - // Look in the SDK/system-image/platform-n/abi folders. - // If we find multiple occurrences of the same platform/abi, the first one read wins. - - LocalPkgInfo[] sysImgInfos = getLocalSdk().getPkgsInfos(PkgType.PKG_SYS_IMAGE); - for (LocalPkgInfo pkg : sysImgInfos) { - IPkgDesc d = pkg.getDesc(); - if (pkg instanceof LocalSysImgPkgInfo && - !d.hasVendor() && - apiVersion.equals(d.getAndroidVersion())) { - IdDisplay tag = d.getTag(); - String abi = d.getPath(); - if (tag != null && abi != null && !tagToAbiFound.containsEntry(tag, abi)) { - found.add(((LocalSysImgPkgInfo)pkg).getSystemImage()); - tagToAbiFound.put(tag, abi); - } - } - } - - // Look in either the platform/images/abi or the legacy folder - File imgDir = new File(platformDir, SdkConstants.OS_IMAGES_FOLDER); - File[] files = fileOp.listFiles(imgDir); - boolean useLegacy = true; - boolean hasImgFiles = false; - final IdDisplay defaultTag = SystemImage.DEFAULT_TAG; - - // Look for sub-directories - for (File file : files) { - if (fileOp.isDirectory(file)) { - useLegacy = false; - String abi = file.getName(); - if (!tagToAbiFound.containsEntry(defaultTag, abi)) { - found.add(new SystemImage( - file, - LocationType.IN_IMAGES_SUBFOLDER, - defaultTag, - abi, - LegacyFileOp.EMPTY_FILE_ARRAY)); - tagToAbiFound.put(defaultTag, abi); - } - } else if (!hasImgFiles && fileOp.isFile(file)) { - if (file.getName().endsWith(".img")) { //$NON-NLS-1$ - hasImgFiles = true; - } - } - } - - if (useLegacy && - hasImgFiles && - fileOp.isDirectory(imgDir) && - !tagToAbiFound.containsEntry(defaultTag, SdkConstants.ABI_ARMEABI)) { - // We found no sub-folder system images but it looks like the top directory - // has some img files in it. It must be a legacy ARM EABI system image folder. - found.add(new SystemImage( - imgDir, - LocationType.IN_LEGACY_FOLDER, - defaultTag, - SdkConstants.ABI_ARMEABI, - LegacyFileOp.EMPTY_FILE_ARRAY)); - } - - return found.toArray(new ISystemImage[found.size()]); - } - - private List getOptionalLibraries(@NonNull File platformDir) { - File optionalDir = new File(platformDir, "optional"); - if (!optionalDir.isDirectory()) { - return Collections.emptyList(); - } - - File optionalJson = new File(optionalDir, "optional.json"); - if (!optionalJson.isFile()) { - return Collections.emptyList(); - } - - return getLibsFromJson(optionalJson); - } - - public static class Library { - String name; - String jar; - boolean manifest; - } - - - @VisibleForTesting - static List getLibsFromJson(@NonNull File jsonFile) { - - Gson gson = new Gson(); - - try { - Type collectionType = new TypeToken>() { - }.getType(); - Collection libs = gson - .fromJson(Files.newReader(jsonFile, Charsets.UTF_8), collectionType); - - // convert into the right format. - List optionalLibraries = Lists.newArrayListWithCapacity(libs.size()); - - File rootFolder = jsonFile.getParentFile(); - for (Library lib : libs) { - optionalLibraries.add(new OptionalLibraryImpl( - lib.name, - new File(rootFolder, lib.jar), - lib.name, - lib.manifest)); - } - - return optionalLibraries; - } catch (FileNotFoundException e) { - // shouldn't happen since we've checked the file is here, but can happen in - // some cases (too many files open). - return Collections.emptyList(); - } - } - - /** List of items in the platform to check when parsing it. These paths are relative to the - * platform root folder. */ - private static final String[] sPlatformContentList = new String[] { - SdkConstants.FN_FRAMEWORK_LIBRARY, - SdkConstants.FN_FRAMEWORK_AIDL, - }; - - /** - * Checks the given platform has all the required files, and returns true if they are all - * present. - *

This checks the presence of the following files: android.jar, framework.aidl, aapt(.exe), - * aidl(.exe), dx(.bat), and dx.jar - * - * @param fileOp File operation wrapper. - * @param platform The folder containing the platform. - * @return An error description if platform is rejected; null if no error is detected. - */ - @NonNull - private static String checkPlatformContent(IFileOp fileOp, @NonNull File platform) { - for (String relativePath : sPlatformContentList) { - File f = new File(platform, relativePath); - if (!fileOp.exists(f)) { - return String.format( - "Ignoring platform '%1$s': %2$s is missing.", //$NON-NLS-1$ - platform.getName(), relativePath); - } - } - return null; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalPlatformToolPkgInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalPlatformToolPkgInfo.java deleted file mode 100644 index 281c64e4..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalPlatformToolPkgInfo.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2013 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.sdklib.repository.local; - -import com.android.annotations.NonNull; -import com.android.sdklib.repository.FullRevision; -import com.android.sdklib.repository.descriptors.IPkgDesc; -import com.android.sdklib.repository.descriptors.PkgDesc; - -import java.io.File; -import java.util.Properties; - -public class LocalPlatformToolPkgInfo extends LocalPkgInfo { - - @NonNull - private final IPkgDesc mDesc; - - public LocalPlatformToolPkgInfo(@NonNull LocalSdk localSdk, - @NonNull File localDir, - @NonNull Properties sourceProps, - @NonNull FullRevision revision) { - super(localSdk, localDir, sourceProps); - mDesc = PkgDesc.Builder.newPlatformTool(revision).create(); - } - - @NonNull - @Override - public IPkgDesc getDesc() { - return mDesc; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalSamplePkgInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalSamplePkgInfo.java deleted file mode 100644 index d8159d61..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalSamplePkgInfo.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (C) 2013 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.sdklib.repository.local; - -import com.android.annotations.NonNull; -import com.android.sdklib.AndroidVersion; -import com.android.sdklib.repository.FullRevision; -import com.android.sdklib.repository.MajorRevision; -import com.android.sdklib.repository.descriptors.IPkgDesc; -import com.android.sdklib.repository.descriptors.PkgDesc; - -import java.io.File; -import java.util.Properties; - -/** - * Local sample package, for a given platform's {@link AndroidVersion}. - * The package itself has a major revision. - * There should be only one for a given android platform version. - */ -public class LocalSamplePkgInfo extends LocalPkgInfo { - - @NonNull - private final IPkgDesc mDesc; - - public LocalSamplePkgInfo(@NonNull LocalSdk localSdk, - @NonNull File localDir, - @NonNull Properties sourceProps, - @NonNull AndroidVersion version, - @NonNull MajorRevision revision, - @NonNull FullRevision minToolsRev) { - super(localSdk, localDir, sourceProps); - mDesc = PkgDesc.Builder.newSample(version, revision, minToolsRev).create(); - } - - @NonNull - @Override - public IPkgDesc getDesc() { - return mDesc; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalSdk.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalSdk.java deleted file mode 100644 index 72597cd3..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalSdk.java +++ /dev/null @@ -1,1239 +0,0 @@ -/* - * Copyright (C) 2013 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.sdklib.repository.local; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.annotations.VisibleForTesting; -import com.android.annotations.VisibleForTesting.Visibility; -import com.android.annotations.concurrency.GuardedBy; -import com.android.sdklib.*; -import com.android.sdklib.AndroidVersion.AndroidVersionException; -import com.android.sdklib.internal.androidTarget.MissingTarget; -import com.android.sdklib.io.LegacyFileOp; -import com.android.sdklib.io.IFileOp; -import com.android.sdklib.repository.FullRevision; -import com.android.sdklib.repository.MajorRevision; -import com.android.sdklib.repository.NoPreviewRevision; -import com.android.sdklib.repository.PkgProps; -import com.android.sdklib.repository.descriptors.IPkgDesc; -import com.android.sdklib.repository.descriptors.IdDisplay; -import com.android.sdklib.repository.descriptors.PkgDescExtra; -import com.android.sdklib.repository.descriptors.PkgType; -import com.google.common.collect.*; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.util.*; - -/** - * This class keeps information on the current locally installed SDK. - * It tries to lazily load information as much as possible. - *

- * Packages are accessed by their type and a main query attribute, depending on the - * package type. There are different versions of {@link #getPkgInfo} which depend on the - * query attribute. - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
TypeQuery parameterGetter
ToolsUnique instance{@code getPkgInfo(PkgType.PKG_TOOLS)} => {@link LocalPkgInfo}
Platform-ToolsUnique instance{@code getPkgInfo(PkgType.PKG_PLATFORM_TOOLS)} => {@link LocalPkgInfo}
DocsUnique instance{@code getPkgInfo(PkgType.PKG_DOCS)} => {@link LocalPkgInfo}
Build-Tools{@link FullRevision}{@code getLatestBuildTool()} => {@link BuildToolInfo},
- * or {@code getBuildTool(FullRevision)} => {@link BuildToolInfo},
- * or {@code getPkgInfo(PkgType.PKG_BUILD_TOOLS, FullRevision)} => {@link LocalPkgInfo},
- * or {@code getPkgsInfos(PkgType.PKG_BUILD_TOOLS)} => {@link LocalPkgInfo}[]
ExtrasString vendor/path{@code getExtra(String)} => {@link LocalExtraPkgInfo},
- * or {@code getPkgInfo(PkgType.PKG_EXTRAS, String)} => {@link LocalPkgInfo},
- * or {@code getPkgsInfos(PkgType.PKG_EXTRAS)} => {@link LocalPkgInfo}[]
Sources{@link AndroidVersion}{@code getPkgInfo(PkgType.PKG_SOURCES, AndroidVersion)} => {@link LocalPkgInfo},
- * or {@code getPkgsInfos(PkgType.PKG_SOURCES)} => {@link LocalPkgInfo}[]
Samples{@link AndroidVersion}{@code getPkgInfo(PkgType.PKG_SAMPLES, AndroidVersion)} => {@link LocalPkgInfo},
- * or {@code getPkgsInfos(PkgType.PKG_SAMPLES)} => {@link LocalPkgInfo}[]
Platforms{@link AndroidVersion}{@code getPkgInfo(PkgType.PKG_PLATFORMS, AndroidVersion)} => {@link LocalPkgInfo},
- * or {@code getPkgInfo(PkgType.PKG_ADDONS, String)} => {@link LocalPkgInfo},
- * or {@code getPkgsInfos(PkgType.PKG_PLATFORMS)} => {@link LocalPkgInfo}[],
- * or {@code getTargetFromHashString(String)} => {@link IAndroidTarget}
Add-ons{@link AndroidVersion} x String vendor/path{@code getPkgInfo(PkgType.PKG_ADDONS, String)} => {@link LocalPkgInfo},
- * or {@code getPkgsInfos(PkgType.PKG_ADDONS)} => {@link LocalPkgInfo}[],
- * or {@code getTargetFromHashString(String)} => {@link IAndroidTarget}
System images{@link AndroidVersion} x {@link String} ABI{@code getPkgsInfos(PkgType.PKG_SYS_IMAGES)} => {@link LocalPkgInfo}[]
- * - * Apps/libraries that use it are encouraged to keep an existing instance around - * (using a singleton or similar mechanism). - *

- * Threading: All accessor methods are synchronized on the same internal lock so - * it's safe to call them from any thread, even concurrently.
- * A method like {@code getPkgsInfos} returns a copy of its data array, which objects are - * not altered after creation, so its value is not influenced by the internal state after - * it returns. - *

- * - * Implementation Background: - *

    - *
  • The sdk manager has a set of "Package" classes that cover both local - * and remote SDK operations. - *
  • Goal was to split it in 2 cleanly separated parts: {@link LocalSdk} parses sdk on disk, - * and a separate class wraps the downloaded manifest (this is now handled within Studio only) - *
  • The local SDK should be a singleton accessible somewhere, so there will be one in ADT - * (via the Sdk instance), one in Studio, and one in the command line tool.
    - * Right now there's a bit of mess with some classes creating a temp LocalSdkParser, - * some others using an SdkManager instance, and that needs to be sorted out. - *
  • As a transition, the SdkManager instance wraps a LocalSdk and uses this. Eventually the - * SdkManager.java class will go away (its name is totally misleading, for starters.) - *
  • The current LocalSdkParser stays as-is for compatibility purposes and the goal is also - * to totally remove it when the SdkManager class goes away. - *
- * @version 2 of the {@code SdkManager} class, essentially. - */ -public class LocalSdk { - - /** Location of the SDK. Maybe null. Can be changed. */ - private File mSdkRoot; - /** File operation object. (Used for overriding in mock testing.) */ - private final IFileOp mFileOp; - /** List of package information loaded so far. Lazily populated. */ - @GuardedBy(value="mLocalPackages") - private final Multimap mLocalPackages = TreeMultimap.create(); - /** Directories already parsed into {@link #mLocalPackages}. */ - @GuardedBy(value="mLocalPackages") - private final Multimap mVisitedDirs = HashMultimap.create(); - /** A legacy build-tool for older platform-tools < 17. */ - private BuildToolInfo mLegacyBuildTools; - /** Cache of targets from local sdk. See {@link #getTargets()}. */ - @GuardedBy(value="mLocalPackages") - private List mCachedTargets = null; - private Set mCachedMissingTargets = null; - - /** - * Creates an initial LocalSdk instance with an unknown location. - */ - public LocalSdk() { - mFileOp = new LegacyFileOp(); - } - - /** - * Creates an initial LocalSdk instance for a known SDK location. - * - * @param sdkRoot The location of the SDK root folder. - */ - public LocalSdk(@NonNull File sdkRoot) { - this(); - setLocation(sdkRoot); - } - - /** - * Creates an initial LocalSdk instance with an unknown location. - * This is designed for unit tests to override the {@link LegacyFileOp} being used. - * - * @param LegacyFileOp The alternate {@link LegacyFileOp} to use for all file-based interactions. - */ - @VisibleForTesting(visibility=Visibility.PRIVATE) - public LocalSdk(@NonNull IFileOp LegacyFileOp) { - mFileOp = LegacyFileOp; - } - - /* - * Returns the current IFileOp being used. - */ - @NonNull - public IFileOp getFileOp() { - return mFileOp; - } - - /** - * Sets or changes the SDK root location. This also clears any cached information. - * - * @param sdkRoot The location of the SDK root folder. - */ - public void setLocation(@NonNull File sdkRoot) { - assert sdkRoot != null; - mSdkRoot = sdkRoot; - clearLocalPkg(PkgType.PKG_ALL); - } - - /** - * Location of the SDK. Maybe null. Can be changed. - * - * @return The location of the SDK. Null if not initialized yet. - */ - @Nullable - public File getLocation() { - return mSdkRoot; - } - - /** - * Location of the SDK. Maybe null. Can be changed. - * The getLocation() API replaces this function. - * @return The location of the SDK. Null if not initialized yet. - */ - @Deprecated - @Nullable - public String getPath() { - return mSdkRoot != null ? mSdkRoot.getPath() : null; - } - - /** - * Clear the tracked visited folders & the cached {@link LocalPkgInfo} for the - * given filter types. - * - * @param filters A set of PkgType constants or {@link PkgType#PKG_ALL} to clear everything. - */ - public void clearLocalPkg(@NonNull EnumSet filters) { - mLegacyBuildTools = null; - - synchronized (mLocalPackages) { - for (PkgType filter : filters) { - mVisitedDirs.removeAll(filter); - mLocalPackages.removeAll(filter); - } - - // Clear the targets if the platforms or addons are being cleared - if (filters.contains(PkgType.PKG_PLATFORM) || filters.contains(PkgType.PKG_ADDON)) { - mCachedMissingTargets = null; - mCachedTargets = null; - } - } - } - - /** - * Check the tracked visited folders to see if anything has changed for the - * requested filter types. - * This does not refresh or reload any package information. - * - * @param filters A set of PkgType constants or {@link PkgType#PKG_ALL} to clear everything. - */ - public boolean hasChanged(@NonNull EnumSet filters) { - for (PkgType filter : filters) { - Collection dirInfos; - synchronized (mLocalPackages) { - dirInfos = mVisitedDirs.get(filter); - for(LocalDirInfo dirInfo : dirInfos) { - if (dirInfo.hasChanged()) { - return true; - } - } - } - } - - return false; - } - - //--------- Generic querying --------- - - - /** - * Retrieves information on a package identified by an {@link IPkgDesc}. - * - * @param descriptor {@link IPkgDesc} describing a package. - * @return The first package found with the same descriptor or null. - */ - @Nullable - public LocalPkgInfo getPkgInfo(@NonNull IPkgDesc descriptor) { - - for (LocalPkgInfo pkg : getPkgsInfos(EnumSet.of(descriptor.getType()))) { - IPkgDesc d = pkg.getDesc(); - if (d.equals(descriptor)) { - return pkg; - } - } - - return null; - } - - /** - * Retrieves information on a package identified by an {@link AndroidVersion}. - * - * Note: don't use this for {@link PkgType#PKG_SYS_IMAGE} since there can be more than - * one ABI and this method only returns a single package per filter type. - * - * @param filter {@link PkgType#PKG_PLATFORM}, {@link PkgType#PKG_SAMPLE} - * or {@link PkgType#PKG_SOURCE}. - * @param version The {@link AndroidVersion} specific for this package type. - * @return An existing package information or null if not found. - */ - @Nullable - public LocalPkgInfo getPkgInfo(@NonNull PkgType filter, @NonNull AndroidVersion version) { - assert filter == PkgType.PKG_PLATFORM || - filter == PkgType.PKG_SAMPLE || - filter == PkgType.PKG_SOURCE; - - for (LocalPkgInfo pkg : getPkgsInfos(filter)) { - IPkgDesc d = pkg.getDesc(); - if (d.hasAndroidVersion() && d.getAndroidVersion().equals(version)) { - return pkg; - } - } - - return null; - } - - /** - * Retrieves information on a package identified by its {@link FullRevision}. - *

- * Note that {@link PkgType#PKG_TOOLS} and {@link PkgType#PKG_PLATFORM_TOOLS} - * are unique in a local SDK so you'll want to use {@link #getPkgInfo(PkgType)} - * to retrieve them instead. - * - * @param filter {@link PkgType#PKG_BUILD_TOOLS}. - * @param revision The {@link FullRevision} uniquely identifying this package. - * @return An existing package information or null if not found. - */ - @Nullable - public LocalPkgInfo getPkgInfo(@NonNull PkgType filter, @NonNull FullRevision revision) { - - assert filter == PkgType.PKG_BUILD_TOOLS; - - for (LocalPkgInfo pkg : getPkgsInfos(filter)) { - IPkgDesc d = pkg.getDesc(); - if (d.hasFullRevision() && d.getFullRevision().equals(revision)) { - return pkg; - } - } - return null; - } - - /** - * Retrieves information on a package identified by its {@link String} path. - *

- * For add-ons and platforms, the path is the target hash string - * (see {@link AndroidTargetHash} for helpers methods to generate this string.) - * - * @param filter {@link PkgType#PKG_ADDON}, {@link PkgType#PKG_PLATFORM}. - * @param path The vendor/path uniquely identifying this package. - * @return An existing package information or null if not found. - */ - @Nullable - public LocalPkgInfo getPkgInfo(@NonNull PkgType filter, @NonNull String path) { - - assert filter == PkgType.PKG_ADDON || - filter == PkgType.PKG_PLATFORM; - - for (LocalPkgInfo pkg : getPkgsInfos(filter)) { - IPkgDesc d = pkg.getDesc(); - if (d.hasPath() && path.equals(d.getPath())) { - return pkg; - } - } - return null; - } - - /** - * Retrieves information on a package identified by both vendor and path strings. - *

- * For add-ons the path is target hash string - * (see {@link AndroidTargetHash} for helpers methods to generate this string.) - * - * @param filter {@link PkgType#PKG_EXTRA}, {@link PkgType#PKG_ADDON}. - * @param vendor The vendor id of the extra package. - * @param path The path uniquely identifying this package for its vendor. - * @return An existing package information or null if not found. - */ - @Nullable - public LocalPkgInfo getPkgInfo(@NonNull PkgType filter, - @NonNull String vendor, - @NonNull String path) { - - assert filter == PkgType.PKG_EXTRA || - filter == PkgType.PKG_ADDON; - - for (LocalPkgInfo pkg : getPkgsInfos(filter)) { - IPkgDesc d = pkg.getDesc(); - if (d.hasVendor() && vendor.equals(d.getVendor().getId())) { - if (d.hasPath() && path.equals(d.getPath())) { - return pkg; - } - } - } - return null; - } - - /** - * Retrieves information on an extra package identified by its {@link String} vendor/path. - * - * @param vendor The vendor id of the extra package. - * @param path The path uniquely identifying this package for its vendor. - * @return An existing extra package information or null if not found. - */ - @Nullable - public LocalExtraPkgInfo getExtra(@NonNull String vendor, @NonNull String path) { - return (LocalExtraPkgInfo) getPkgInfo(PkgType.PKG_EXTRA, vendor, path); - } - - /** - * For unique local packages. - * Returns the cached LocalPkgInfo for the requested type. - * Loads it from disk if not cached. - * - * @param filter {@link PkgType#PKG_TOOLS} or {@link PkgType#PKG_PLATFORM_TOOLS} - * or {@link PkgType#PKG_DOC}. - * @return null if the package is not installed. - */ - @Nullable - public LocalPkgInfo getPkgInfo(@NonNull PkgType filter) { - if (filter != PkgType.PKG_TOOLS && - filter != PkgType.PKG_PLATFORM_TOOLS && - filter != PkgType.PKG_DOC && - filter != PkgType.PKG_NDK) { - assert false; - return null; - } - - LocalPkgInfo info = null; - synchronized (mLocalPackages) { - Collection existing = mLocalPackages.get(filter); - assert existing.size() <= 1; - if (!existing.isEmpty()) { - return existing.iterator().next(); - } - - File uniqueDir = new File(mSdkRoot, filter.getFolderName()); - - if (!mVisitedDirs.containsEntry(filter, new LocalDirInfo.MapComparator(uniqueDir))) { - switch(filter) { - case PKG_TOOLS: - info = scanTools(uniqueDir); - break; - case PKG_PLATFORM_TOOLS: - info = scanPlatformTools(uniqueDir); - break; - case PKG_DOC: - info = scanDoc(uniqueDir); - break; - case PKG_NDK: - info = scanNdk(uniqueDir); - default: - break; - } - } - - // Whether we have found a valid pkg or not, this directory has been visited. - mVisitedDirs.put(filter, new LocalDirInfo(mFileOp, uniqueDir)); - - if (info != null) { - mLocalPackages.put(filter, info); - } - } - - return info; - } - - /** - * Retrieve all the info about the requested package type. - * This is used for the package types that have one or more instances, each with different - * versions. - * The resulting array is sorted according to the PkgInfo's sort order. - *

- * Note: you can use this with {@link PkgType#PKG_TOOLS}, {@link PkgType#PKG_PLATFORM_TOOLS} and - * {@link PkgType#PKG_DOC} but since there can only be one package of these types, it is - * more efficient to use {@link #getPkgInfo(PkgType)} to query them. - * - * @param filter One of {@link PkgType} constants. - * @return A list (possibly empty) of matching installed packages. Never returns null. - */ - @NonNull - public LocalPkgInfo[] getPkgsInfos(@NonNull PkgType filter) { - return getPkgsInfos(EnumSet.of(filter)); - } - - /** - * Retrieve all the info about the requested package types. - * This is used for the package types that have one or more instances, each with different - * versions. - * The resulting array is sorted according to the PkgInfo's sort order. - *

- * To force the LocalSdk parser to load everything, simply call this method - * with the {@link PkgType#PKG_ALL} argument to load all the known package types. - *

- * Note: you can use this with {@link PkgType#PKG_TOOLS}, {@link PkgType#PKG_PLATFORM_TOOLS} and - * {@link PkgType#PKG_DOC} but since there can only be one package of these types, it is - * more efficient to use {@link #getPkgInfo(PkgType)} to query them. - * - * @param filters One or more of {@link PkgType#PKG_ADDON}, {@link PkgType#PKG_PLATFORM}, - * {@link PkgType#PKG_BUILD_TOOLS}, {@link PkgType#PKG_EXTRA}, - * {@link PkgType#PKG_SOURCE}, {@link PkgType#PKG_SYS_IMAGE} - * @return A list (possibly empty) of matching installed packages. Never returns null. - */ - @NonNull - public LocalPkgInfo[] getPkgsInfos(@NonNull EnumSet filters) { - List list = Lists.newArrayList(); - - for (PkgType filter : filters) { - if (filter == PkgType.PKG_TOOLS || - filter == PkgType.PKG_PLATFORM_TOOLS || - filter == PkgType.PKG_DOC || - filter == PkgType.PKG_NDK) { - LocalPkgInfo info = getPkgInfo(filter); - if (info != null) { - list.add(info); - } - } else { - synchronized (mLocalPackages) { - Collection existing = mLocalPackages.get(filter); - assert existing != null; // Multimap returns an empty set if not found - - if (!existing.isEmpty()) { - list.addAll(existing); - continue; - } - - File subDir = new File(mSdkRoot, filter.getFolderName()); - - if (!mVisitedDirs.containsEntry(filter, new LocalDirInfo.MapComparator(subDir))) { - switch(filter) { - case PKG_BUILD_TOOLS: - scanBuildTools(subDir, existing); - break; - - case PKG_PLATFORM: - scanPlatforms(subDir, existing); - break; - - case PKG_SYS_IMAGE: - scanSysImages(subDir, existing, false); - break; - - case PKG_ADDON_SYS_IMAGE: - scanSysImages(subDir, existing, true); - break; - - case PKG_ADDON: - scanAddons(subDir, existing); - break; - - case PKG_SAMPLE: - scanSamples(subDir, existing); - break; - - case PKG_SOURCE: - scanSources(subDir, existing); - break; - - case PKG_EXTRA: - scanExtras(subDir, existing); - break; - - case PKG_TOOLS: - case PKG_PLATFORM_TOOLS: - case PKG_DOC: - case PKG_NDK: - break; - default: - throw new IllegalArgumentException( - "Unsupported pkg type " + filter.toString()); - } - mVisitedDirs.put(filter, new LocalDirInfo(mFileOp, subDir)); - list.addAll(existing); - } - } - } - } - - Collections.sort(list); - return list.toArray(new LocalPkgInfo[list.size()]); - } - - //---------- Package-specific querying -------- - - /** - * Returns the {@link BuildToolInfo} for the given revision. - * - * @param revision The requested revision. - * @return A {@link BuildToolInfo}. Can be null if {@code revision} is null or is - * not part of the known set returned by {@code getPkgsInfos(PkgType.PKG_BUILD_TOOLS)}. - */ - @Nullable - public BuildToolInfo getBuildTool(@Nullable FullRevision revision) { - LocalPkgInfo pkg = getPkgInfo(PkgType.PKG_BUILD_TOOLS, revision); - if (pkg instanceof LocalBuildToolPkgInfo) { - return ((LocalBuildToolPkgInfo) pkg).getBuildToolInfo(); - } - return null; - } - - /** - * Returns the highest build-tool revision known, or null if there are are no build-tools. - *

- * If no specific build-tool package is installed but the platform-tools is lower than 17, - * then this creates and returns a "legacy" built-tool package using platform-tools. - * (We only split build-tools out of platform-tools starting with revision 17, - * before they were both the same thing.) - * - * @return The highest build-tool revision known, or null. - */ - @Nullable - public BuildToolInfo getLatestBuildTool() { - if (mLegacyBuildTools != null) { - return mLegacyBuildTools; - } - - LocalPkgInfo[] pkgs = getPkgsInfos(PkgType.PKG_BUILD_TOOLS); - - if (pkgs.length == 0) { - LocalPkgInfo ptPkg = getPkgInfo(PkgType.PKG_PLATFORM_TOOLS); - if (ptPkg instanceof LocalPlatformToolPkgInfo && - ptPkg.getDesc().getFullRevision().compareTo(new FullRevision(17)) < 0) { - // older SDK, create a compatible build-tools - mLegacyBuildTools = createLegacyBuildTools((LocalPlatformToolPkgInfo) ptPkg); - return mLegacyBuildTools; - } - return null; - } - - assert pkgs.length > 0; - - // Note: the pkgs come from a TreeMultimap so they should already be sorted. - // Just in case, sort them again. - Arrays.sort(pkgs); - - // LocalBuildToolPkgInfo's comparator sorts on its FullRevision so we just - // need to take the latest element. - LocalPkgInfo pkg = pkgs[pkgs.length - 1]; - if (pkg instanceof LocalBuildToolPkgInfo) { - return ((LocalBuildToolPkgInfo) pkg).getBuildToolInfo(); - } - - return null; - } - - @NonNull - private BuildToolInfo createLegacyBuildTools(@NonNull LocalPlatformToolPkgInfo ptInfo) { - File platformTools = new File(getLocation(), SdkConstants.FD_PLATFORM_TOOLS); - File platformToolsLib = ptInfo.getLocalDir(); - File platformToolsRs = new File(platformTools, SdkConstants.FN_FRAMEWORK_RENDERSCRIPT); - - return new BuildToolInfo( - ptInfo.getDesc().getFullRevision(), - platformTools, - new File(platformTools, SdkConstants.FN_AAPT), - new File(platformTools, SdkConstants.FN_AIDL), - new File(platformTools, SdkConstants.FN_DX), - new File(platformToolsLib, SdkConstants.FN_DX_JAR), - new File(platformTools, SdkConstants.FN_RENDERSCRIPT), - new File(platformToolsRs, SdkConstants.FN_FRAMEWORK_INCLUDE), - new File(platformToolsRs, SdkConstants.FN_FRAMEWORK_INCLUDE_CLANG), - null, - null, - null, - null, - new File(platformTools, SdkConstants.FN_ZIPALIGN)); - } - - /** - * Returns the targets (platforms & addons) that are available in the SDK. - * The target list is created on demand the first time then cached. - * It will not refreshed unless {@link #clearLocalPkg} is called to clear platforms - * and/or add-ons. - *

- * The array can be empty but not null. - */ - @NonNull - public IAndroidTarget[] getTargets() { - synchronized (mLocalPackages) { - if (mCachedTargets == null) { - List result = Lists.newArrayList(); - LocalPkgInfo[] pkgsInfos = getPkgsInfos(EnumSet.of(PkgType.PKG_PLATFORM, - PkgType.PKG_ADDON)); - for (LocalPkgInfo info : pkgsInfos) { - assert info instanceof LocalPlatformPkgInfo; - IAndroidTarget target = ((LocalPlatformPkgInfo) info).getAndroidTarget(); - if (target != null) { - result.add(target); - } - } - mCachedTargets = result; - } - return mCachedTargets.toArray(new IAndroidTarget[mCachedTargets.size()]); - } - } - - public IAndroidTarget[] getTargets(boolean includeMissing) { - IAndroidTarget[] result = getTargets(); - if (includeMissing) { - result = ObjectArrays.concat(result, getMissingTargets(), IAndroidTarget.class); - } - return result; - } - - @NonNull - public IAndroidTarget[] getMissingTargets() { - synchronized (mLocalPackages) { - if (mCachedMissingTargets == null) { - Map result = Maps.newHashMap(); - Set seen = Sets.newHashSet(); - for (IAndroidTarget target : getTargets()) { - Collections.addAll(seen, target.getSystemImages()); - } - for (LocalPkgInfo local : getPkgsInfos(PkgType.PKG_ADDON_SYS_IMAGE)) { - LocalAddonSysImgPkgInfo info = (LocalAddonSysImgPkgInfo)local; - ISystemImage image = info.getSystemImage(); - if (!seen.contains(image)) { - addOrphanedSystemImage(image, info.getDesc(), result); - } - } - for (LocalPkgInfo local : getPkgsInfos(PkgType.PKG_SYS_IMAGE)) { - LocalSysImgPkgInfo info = (LocalSysImgPkgInfo)local; - ISystemImage image = info.getSystemImage(); - if (!seen.contains(image)) { - addOrphanedSystemImage(image, info.getDesc(), result); - } - } - mCachedMissingTargets = result.keySet(); - } - return mCachedMissingTargets.toArray(new IAndroidTarget[mCachedMissingTargets.size()]); - } - } - - private static void addOrphanedSystemImage(ISystemImage image, IPkgDesc desc, Map targets) { - IdDisplay vendor = desc.getVendor(); - MissingTarget target = new MissingTarget(vendor == null ? null : vendor.getDisplay(), desc.getTag().getDisplay(), desc.getAndroidVersion()); - MissingTarget existing = targets.get(target); - if (existing == null) { - existing = target; - targets.put(target, target); - } - existing.addSystemImage(image); - } - - /** - * Returns a target from a hash that was generated by {@link IAndroidTarget#hashString()}. - * - * @param hash the {@link IAndroidTarget} hash string. - * @return The matching {@link IAndroidTarget} or null. - */ - @Nullable - public IAndroidTarget getTargetFromHashString(@Nullable String hash) { - if (hash != null) { - IAndroidTarget[] targets = getTargets(true); - for (IAndroidTarget target : targets) { - if (target != null && hash.equals(AndroidTargetHash.getTargetHashString(target))) { - return target; - } - } - } - return null; - } - - // ------------- - - /** - * Try to find a tools package at the given location. - * Returns null if not found. - */ - private LocalToolPkgInfo scanTools(File toolFolder) { - // Can we find some properties? - Properties props = parseProperties(new File(toolFolder, SdkConstants.FN_SOURCE_PROP)); - FullRevision rev = PackageParserUtils.getPropertyFull(props, PkgProps.PKG_REVISION); - if (rev == null) { - return null; - } - - FullRevision minPlatToolsRev = - PackageParserUtils.getPropertyFull(props, PkgProps.MIN_PLATFORM_TOOLS_REV); - if (minPlatToolsRev == null) { - minPlatToolsRev = FullRevision.NOT_SPECIFIED; - } - - LocalToolPkgInfo info = new LocalToolPkgInfo(this, toolFolder, props, rev, minPlatToolsRev); - - // We're not going to check that all tools are present. At the very least - // we should expect to find android and an emulator adapted to the current OS. - boolean hasEmulator = false; - boolean hasAndroid = false; - String android1 = SdkConstants.androidCmdName().replace(".bat", ".exe"); - String android2 = android1.indexOf('.') == -1 ? null : android1.replace(".exe", ".bat"); - File[] files = mFileOp.listFiles(toolFolder); - for (File file : files) { - String name = file.getName(); - if (SdkConstants.FN_EMULATOR.equals(name)) { - hasEmulator = true; - } - if (android1.equals(name) || (android2 != null && android2.equals(name))) { - hasAndroid = true; - } - } - if (!hasAndroid) { - info.appendLoadError("Missing %1$s", SdkConstants.androidCmdName()); - } - if (!hasEmulator) { - info.appendLoadError("Missing %1$s", SdkConstants.FN_EMULATOR); - } - - return info; - } - - /** - * Try to find a platform-tools package at the given location. - * Returns null if not found. - */ - private LocalPlatformToolPkgInfo scanPlatformTools(File ptFolder) { - // Can we find some properties? - Properties props = parseProperties(new File(ptFolder, SdkConstants.FN_SOURCE_PROP)); - FullRevision rev = PackageParserUtils.getPropertyFull(props, PkgProps.PKG_REVISION); - if (rev == null) { - return null; - } - - LocalPlatformToolPkgInfo info = new LocalPlatformToolPkgInfo(this, ptFolder, props, rev); - return info; - } - - /** - * Try to find a docs package at the given location. - * Returns null if not found. - */ - private LocalDocPkgInfo scanDoc(File docFolder) { - // Can we find some properties? - Properties props = parseProperties(new File(docFolder, SdkConstants.FN_SOURCE_PROP)); - MajorRevision rev = PackageParserUtils.getPropertyMajor(props, PkgProps.PKG_REVISION); - if (rev == null) { - return null; - } - - try { - AndroidVersion vers = new AndroidVersion(props); - LocalDocPkgInfo info = new LocalDocPkgInfo(this, docFolder, props, vers, rev); - - // To start with, a doc folder should have an "index.html" to be acceptable. - // We don't actually check the content of the file. - if (!mFileOp.isFile(new File(docFolder, "index.html"))) { - info.appendLoadError("Missing index.html"); - } - return info; - - } catch (AndroidVersionException e) { - return null; // skip invalid or missing android version. - } - } - - /** - * Try to find an NDK package at the given location. - * Returns null if not found. - */ - @Nullable - private LocalNdkPkgInfo scanNdk(@NonNull File ndkFolder) { - // Can we find some properties? - Properties props = parseProperties(new File(ndkFolder, SdkConstants.FN_SOURCE_PROP)); - FullRevision rev = PackageParserUtils.getPropertyFull(props, PkgProps.PKG_REVISION); - if (rev == null) { - return null; - } - - return new LocalNdkPkgInfo(this, ndkFolder, props, rev); - } - - - /** - * Helper used by scanXyz methods below to check whether a directory should be visited. - * It can be skipped if it's not a directory or if it's already marked as visited in - * mVisitedDirs for the given package type -- in which case the directory is added to - * the visited map. - * - * @param pkgType The package type being scanned. - * @param directory The file or directory to check. - * @return False if directory can/should be skipped. - * True if directory should be visited, in which case it's registered in mVisitedDirs. - */ - private boolean shouldVisitDir(@NonNull PkgType pkgType, @NonNull File directory) { - if (!mFileOp.isDirectory(directory)) { - return false; - } - synchronized (mLocalPackages) { - if (mVisitedDirs.containsEntry(pkgType, new LocalDirInfo.MapComparator(directory))) { - return false; - } - mVisitedDirs.put(pkgType, new LocalDirInfo(mFileOp, directory)); - } - return true; - } - - private void scanBuildTools(File collectionDir, Collection outCollection) { - // The build-tool root folder contains a list of per-revision folders. - for (File buildToolDir : mFileOp.listFiles(collectionDir)) { - if (!shouldVisitDir(PkgType.PKG_BUILD_TOOLS, buildToolDir)) { - continue; - } - - Properties props = parseProperties(new File(buildToolDir, SdkConstants.FN_SOURCE_PROP)); - FullRevision rev = PackageParserUtils.getPropertyFull(props, PkgProps.PKG_REVISION); - if (rev == null) { - continue; // skip, no revision - } - - BuildToolInfo btInfo = new BuildToolInfo(rev, buildToolDir); - LocalBuildToolPkgInfo pkgInfo = - new LocalBuildToolPkgInfo(this, buildToolDir, props, rev, btInfo); - outCollection.add(pkgInfo); - } - } - - private void scanPlatforms(File collectionDir, Collection outCollection) { - for (File platformDir : mFileOp.listFiles(collectionDir)) { - if (!shouldVisitDir(PkgType.PKG_PLATFORM, platformDir)) { - continue; - } - - Properties props = parseProperties(new File(platformDir, SdkConstants.FN_SOURCE_PROP)); - MajorRevision rev = PackageParserUtils.getPropertyMajor(props, PkgProps.PKG_REVISION); - if (rev == null) { - continue; // skip, no revision - } - - FullRevision minToolsRev = - PackageParserUtils.getPropertyFull(props, PkgProps.MIN_TOOLS_REV); - if (minToolsRev == null) { - minToolsRev = FullRevision.NOT_SPECIFIED; - } - - try { - AndroidVersion vers = new AndroidVersion(props); - - LocalPlatformPkgInfo pkgInfo = - new LocalPlatformPkgInfo(this, platformDir, props, vers, rev, minToolsRev); - outCollection.add(pkgInfo); - - } catch (AndroidVersionException e) { - continue; // skip invalid or missing android version. - } - } - } - - private void scanAddons(File collectionDir, Collection outCollection) { - for (File addonDir : mFileOp.listFiles(collectionDir)) { - if (!shouldVisitDir(PkgType.PKG_ADDON, addonDir)) { - continue; - } - - Properties props = parseProperties(new File(addonDir, SdkConstants.FN_SOURCE_PROP)); - MajorRevision rev = PackageParserUtils.getPropertyMajor(props, PkgProps.PKG_REVISION); - if (rev == null) { - continue; // skip, no revision - } - - try { - AndroidVersion vers = new AndroidVersion(props); - - // Starting with addon-4.xsd, we have vendor-id and name-id available - // in the add-on source properties so we'll use that directly. - - String nameId = props.getProperty(PkgProps.ADDON_NAME_ID); - String nameDisp = props.getProperty(PkgProps.ADDON_NAME_DISPLAY); - String vendorId = props.getProperty(PkgProps.ADDON_VENDOR_ID); - String vendorDisp = props.getProperty(PkgProps.ADDON_VENDOR_DISPLAY); - - if (nameId == null) { - // Support earlier add-ons that only had a name display attribute - nameDisp = props.getProperty(PkgProps.ADDON_NAME, "Unknown"); - nameId = LocalAddonPkgInfo.sanitizeDisplayToNameId(nameDisp); - } - - if (nameId != null && nameDisp == null) { - nameDisp = LocalExtraPkgInfo.getPrettyName(null, nameId); - } - - if (vendorId != null && vendorDisp == null) { - vendorDisp = LocalExtraPkgInfo.getPrettyName(null, nameId); - } - - if (vendorId == null) { - // Support earlier add-ons that only had a vendor display attribute - vendorDisp = props.getProperty(PkgProps.ADDON_VENDOR, "Unknown"); - vendorId = LocalAddonPkgInfo.sanitizeDisplayToNameId(vendorDisp); - } - - LocalAddonPkgInfo pkgInfo = new LocalAddonPkgInfo( - this, addonDir, props, vers, rev, - new IdDisplay(vendorId, vendorDisp), - new IdDisplay(nameId, nameDisp)); - outCollection.add(pkgInfo); - - } catch (AndroidVersionException e) { - continue; // skip invalid or missing android version. - } - } - } - - private void scanSysImages( - File collectionDir, - Collection outCollection, - boolean scanAddons) { - List propFiles = Lists.newArrayList(); - PkgType type = scanAddons ? PkgType.PKG_ADDON_SYS_IMAGE : PkgType.PKG_SYS_IMAGE; - - // Create a list of folders that contains a source.properties file matching these patterns: - // sys-img/target/tag/abi - // sys-img/target/abis - // sys-img/add-on-target/abi - // sys-img/target/add-on/abi - for (File platformDir : mFileOp.listFiles(collectionDir)) { - if (!shouldVisitDir(type, platformDir)) { - continue; - } - - for (File dir1 : mFileOp.listFiles(platformDir)) { - // dir1 might be either a tag or an abi folder. - if (!shouldVisitDir(type, dir1)) { - continue; - } - - File prop1 = new File(dir1, SdkConstants.FN_SOURCE_PROP); - if (mFileOp.isFile(prop1)) { - // dir1 was a legacy abi folder. - if (!propFiles.contains(prop1)) { - propFiles.add(prop1); - } - } else { - File[] dir1Files = mFileOp.listFiles(dir1); - for (File dir2 : dir1Files) { - // dir2 should be an abi folder in a tag folder. - if (!shouldVisitDir(type, dir2)) { - continue; - } - - File prop2 = new File(dir2, SdkConstants.FN_SOURCE_PROP); - if (mFileOp.isFile(prop2)) { - if (!propFiles.contains(prop2)) { - propFiles.add(prop2); - } - } - } - } - } - } - - for (File propFile : propFiles) { - Properties props = parseProperties(propFile); - MajorRevision rev = PackageParserUtils.getPropertyMajor(props, PkgProps.PKG_REVISION); - if (rev == null) { - continue; // skip, no revision - } - - try { - AndroidVersion vers = new AndroidVersion(props); - - IdDisplay tag = LocalSysImgPkgInfo.extractTagFromProps(props); - String vendorId = props.getProperty(PkgProps.ADDON_VENDOR_ID, null); - File abiDir = propFile.getParentFile(); - - if (vendorId == null && !scanAddons) { - LocalSysImgPkgInfo pkgInfo = - new LocalSysImgPkgInfo(this, abiDir, props, vers, tag, abiDir.getName(), rev); - outCollection.add(pkgInfo); - - } else if (vendorId != null && scanAddons) { - String vendorDisp = props.getProperty(PkgProps.ADDON_VENDOR_DISPLAY, vendorId); - IdDisplay vendor = new IdDisplay(vendorId, vendorDisp); - - LocalAddonSysImgPkgInfo pkgInfo = - new LocalAddonSysImgPkgInfo( - this, abiDir, props, vers, vendor, tag, abiDir.getName(), rev); - outCollection.add(pkgInfo); - } - - } catch (AndroidVersionException e) { - continue; // skip invalid or missing android version. - } - } - } - - private void scanSamples(File collectionDir, Collection outCollection) { - for (File platformDir : mFileOp.listFiles(collectionDir)) { - if (!shouldVisitDir(PkgType.PKG_SAMPLE, platformDir)) { - continue; - } - - Properties props = parseProperties(new File(platformDir, SdkConstants.FN_SOURCE_PROP)); - MajorRevision rev = PackageParserUtils.getPropertyMajor(props, PkgProps.PKG_REVISION); - if (rev == null) { - continue; // skip, no revision - } - - FullRevision minToolsRev = - PackageParserUtils.getPropertyFull(props, PkgProps.MIN_TOOLS_REV); - if (minToolsRev == null) { - minToolsRev = FullRevision.NOT_SPECIFIED; - } - - try { - AndroidVersion vers = new AndroidVersion(props); - - LocalSamplePkgInfo pkgInfo = - new LocalSamplePkgInfo(this, platformDir, props, vers, rev, minToolsRev); - outCollection.add(pkgInfo); - } catch (AndroidVersionException e) { - continue; // skip invalid or missing android version. - } - } - } - - private void scanSources(File collectionDir, Collection outCollection) { - // The build-tool root folder contains a list of per-revision folders. - for (File platformDir : mFileOp.listFiles(collectionDir)) { - if (!shouldVisitDir(PkgType.PKG_SOURCE, platformDir)) { - continue; - } - - Properties props = parseProperties(new File(platformDir, SdkConstants.FN_SOURCE_PROP)); - MajorRevision rev = PackageParserUtils.getPropertyMajor(props, PkgProps.PKG_REVISION); - if (rev == null) { - continue; // skip, no revision - } - - try { - AndroidVersion vers = new AndroidVersion(props); - - LocalSourcePkgInfo pkgInfo = - new LocalSourcePkgInfo(this, platformDir, props, vers, rev); - outCollection.add(pkgInfo); - } catch (AndroidVersionException e) { - continue; // skip invalid or missing android version. - } - } - } - - private void scanExtras(File collectionDir, Collection outCollection) { - for (File vendorDir : mFileOp.listFiles(collectionDir)) { - if (!shouldVisitDir(PkgType.PKG_EXTRA, vendorDir)) { - continue; - } - - for (File extraDir : mFileOp.listFiles(vendorDir)) { - if (!shouldVisitDir(PkgType.PKG_EXTRA, extraDir)) { - continue; - } - - Properties props = parseProperties(new File(extraDir, SdkConstants.FN_SOURCE_PROP)); - NoPreviewRevision rev = - PackageParserUtils.getPropertyNoPreview(props, PkgProps.PKG_REVISION); - if (rev == null) { - continue; // skip, no revision - } - - String oldPaths = - PackageParserUtils.getProperty(props, PkgProps.EXTRA_OLD_PATHS, null); - - String vendorId = vendorDir.getName(); - String vendorDisp = props.getProperty(PkgProps.EXTRA_VENDOR_DISPLAY); - if (vendorDisp == null || vendorDisp.isEmpty()) { - vendorDisp = vendorId; - } - - String displayName = props.getProperty(PkgProps.EXTRA_NAME_DISPLAY, null); - - LocalExtraPkgInfo pkgInfo = new LocalExtraPkgInfo( - this, - extraDir, - props, - new IdDisplay(vendorId, vendorDisp), - extraDir.getName(), - displayName, - PkgDescExtra.convertOldPaths(oldPaths), - rev); - outCollection.add(pkgInfo); - } - } - } - - /** - * Parses the given file as properties file if it exists. - * Returns null if the file does not exist, cannot be parsed or has no properties. - */ - private Properties parseProperties(File propsFile) { - InputStream fis = null; - try { - if (mFileOp.exists(propsFile)) { - fis = mFileOp.newFileInputStream(propsFile); - - Properties props = new Properties(); - props.load(fis); - - // To be valid, there must be at least one property in it. - if (!props.isEmpty()) { - return props; - } - } - } catch (IOException e) { - // Ignore - } finally { - if (fis != null) { - try { - fis.close(); - } catch (IOException e) {} - } - } - return null; - } - -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalSourcePkgInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalSourcePkgInfo.java deleted file mode 100644 index eb822de1..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalSourcePkgInfo.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) 2013 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.sdklib.repository.local; - -import com.android.annotations.NonNull; -import com.android.sdklib.AndroidVersion; -import com.android.sdklib.repository.MajorRevision; -import com.android.sdklib.repository.descriptors.IPkgDesc; -import com.android.sdklib.repository.descriptors.PkgDesc; - -import java.io.File; -import java.util.Properties; - -/** - * Local source package, for a given platform's {@link AndroidVersion}. - * The package itself has a major revision. - * There should be only one for a given android platform version. - */ -public class LocalSourcePkgInfo extends LocalPkgInfo { - - @NonNull - private final IPkgDesc mDesc; - - public LocalSourcePkgInfo(@NonNull LocalSdk localSdk, - @NonNull File localDir, - @NonNull Properties sourceProps, - @NonNull AndroidVersion version, - @NonNull MajorRevision revision) { - super(localSdk, localDir, sourceProps); - mDesc = PkgDesc.Builder.newSource(version, revision).create(); - } - - @NonNull - @Override - public IPkgDesc getDesc() { - return mDesc; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalSysImgPkgInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalSysImgPkgInfo.java deleted file mode 100644 index 0f6cdad5..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalSysImgPkgInfo.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright (C) 2013 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.sdklib.repository.local; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.sdklib.AndroidVersion; -import com.android.sdklib.ISystemImage; -import com.android.sdklib.SystemImage; -import com.android.sdklib.io.LegacyFileOp; -import com.android.sdklib.io.IFileOp; -import com.android.sdklib.repository.MajorRevision; -import com.android.sdklib.repository.PkgProps; -import com.android.sdklib.repository.descriptors.IPkgDesc; -import com.android.sdklib.repository.descriptors.IdDisplay; -import com.android.sdklib.repository.descriptors.PkgDesc; -import com.google.common.base.Objects; - -import java.io.File; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Locale; -import java.util.Properties; - -/** - * Local system-image package, for a given platform's {@link AndroidVersion} - * and given ABI. - * The package itself has a major revision. - * There should be only one for a given android platform version & ABI. - */ -public class LocalSysImgPkgInfo extends LocalPkgInfo { - - - @NonNull - private final IPkgDesc mDesc; - - public LocalSysImgPkgInfo(@NonNull LocalSdk localSdk, - @NonNull File localDir, - @NonNull Properties sourceProps, - @NonNull AndroidVersion version, - @Nullable IdDisplay tag, - @NonNull String abi, - @NonNull MajorRevision revision) { - super(localSdk, localDir, sourceProps); - mDesc = PkgDesc.Builder.newSysImg(version, tag, abi, revision).create(); - } - - @NonNull - @Override - public IPkgDesc getDesc() { - return mDesc; - } - - /** - * Extracts the tag id & display from the properties. - * If missing, uses the "default" tag id. - */ - @NonNull - public static IdDisplay extractTagFromProps(Properties props) { - if (props != null) { - String tagId = props.getProperty(PkgProps.SYS_IMG_TAG_ID, SystemImage.DEFAULT_TAG.getId()); - String tagDisp = props.getProperty(PkgProps.SYS_IMG_TAG_DISPLAY, ""); //$NON-NLS-1$ - if (tagDisp == null || tagDisp.isEmpty()) { - tagDisp = tagIdToDisplay(tagId); - } - assert tagId != null; - assert tagDisp != null; - return new IdDisplay(tagId, tagDisp); - } - return SystemImage.DEFAULT_TAG; - } - - /** - * Computes a display-friendly tag string based on the tag id. - * This is typically used when there's no tag-display attribute. - * - * @param tagId A non-null tag id to sanitize for display. - * @return The tag id with all non-alphanum symbols replaced by spaces and trimmed. - */ - @NonNull - public static String tagIdToDisplay(@NonNull String tagId) { - String name; - name = tagId.replaceAll("[^A-Za-z0-9]+", " "); //$NON-NLS-1$ //$NON-NLS-2$ - name = name.replaceAll(" +", " "); //$NON-NLS-1$ //$NON-NLS-2$ - name = name.trim(); - - if (!name.isEmpty()) { - char c = name.charAt(0); - if (!Character.isUpperCase(c)) { - StringBuilder sb = new StringBuilder(name); - sb.replace(0, 1, String.valueOf(c).toUpperCase(Locale.US)); - name = sb.toString(); - } - } - return name; - } - - public SystemImage getSystemImage() { - return getSystemImage(mDesc, getLocalDir(), getLocalSdk().getFileOp()); - } - - static SystemImage getSystemImage(IPkgDesc desc, File localDir, @NonNull IFileOp fileOp) { - final IdDisplay tag = desc.getTag(); - final String abi = desc.getPath(); - List parsedSkins = PackageParserUtils.parseSkinFolder(new File(localDir, SdkConstants.FD_SKINS), fileOp); - File[] skins = LegacyFileOp.EMPTY_FILE_ARRAY; - if (!parsedSkins.isEmpty()) { - skins = parsedSkins.toArray(new File[parsedSkins.size()]); - } - - return new SystemImage( - localDir, - ISystemImage.LocationType.IN_SYSTEM_IMAGE, - tag, - desc.getVendor(), - abi, - skins); - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalToolPkgInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalToolPkgInfo.java deleted file mode 100644 index 91306ea3..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/LocalToolPkgInfo.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2013 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.sdklib.repository.local; - -import com.android.annotations.NonNull; -import com.android.sdklib.repository.FullRevision; -import com.android.sdklib.repository.descriptors.IPkgDesc; -import com.android.sdklib.repository.descriptors.PkgDesc; - -import java.io.File; -import java.util.Properties; - -public class LocalToolPkgInfo extends LocalPkgInfo { - - @NonNull - private final IPkgDesc mDesc; - - public LocalToolPkgInfo(@NonNull LocalSdk localSdk, - @NonNull File localDir, - @NonNull Properties sourceProps, - @NonNull FullRevision revision, - @NonNull FullRevision minPlatformToolsRev) { - super(localSdk, localDir, sourceProps); - mDesc = PkgDesc.Builder.newTool(revision, minPlatformToolsRev).create(); - } - - @NonNull - @Override - public IPkgDesc getDesc() { - return mDesc; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/PackageParserUtils.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/PackageParserUtils.java deleted file mode 100644 index e8cc4b2e..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/local/PackageParserUtils.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright (C) 2015 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.sdklib.repository.local; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.sdklib.io.IFileOp; -import com.android.sdklib.repository.FullRevision; -import com.android.sdklib.repository.MajorRevision; -import com.android.sdklib.repository.NoPreviewRevision; -import com.android.sdklib.repository.PkgProps; - -import java.io.File; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Properties; - -/** - * Misc utilities to help extracting elements and attributes out of a repository XML document. - */ -class PackageParserUtils { - /** - * Utility method to parse the {@link PkgProps#PKG_REVISION} property as a full - * revision (major.minor.micro.preview). - * - * @param props The properties to parse. - * @return A {@link FullRevision} or null if there is no such property or it couldn't be parsed. - * @param propKey The name of the property. Must not be null. - */ - @Nullable - public static FullRevision getPropertyFull( - @Nullable Properties props, - @NonNull String propKey) { - String revStr = getProperty(props, propKey, null); - - FullRevision rev = null; - if (revStr != null) { - try { - rev = FullRevision.parseRevision(revStr); - } catch (NumberFormatException ignore) {} - } - - return rev; - } - - /** - * Utility method to parse the {@link PkgProps#PKG_REVISION} property as a major - * revision (major integer, no minor/micro/preview parts.) - * - * @param props The properties to parse. - * @return A {@link MajorRevision} or null if there is no such property or it couldn't be parsed. - * @param propKey The name of the property. Must not be null. - */ - @Nullable - public static MajorRevision getPropertyMajor( - @Nullable Properties props, - @NonNull String propKey) { - String revStr = getProperty(props, propKey, null); - - MajorRevision rev = null; - if (revStr != null) { - try { - rev = MajorRevision.parseRevision(revStr); - } catch (NumberFormatException ignore) {} - } - - return rev; - } - - /** - * Utility method to parse the {@link PkgProps#PKG_REVISION} property as a no-preview - * revision (major.minor.micro integers but no preview part.) - * - * @param props The properties to parse. - * @return A {@link NoPreviewRevision} or - * null if there is no such property or it couldn't be parsed. - * @param propKey The name of the property. Must not be null. - */ - @Nullable - public static NoPreviewRevision getPropertyNoPreview( - @Nullable Properties props, - @NonNull String propKey) { - String revStr = getProperty(props, propKey, null); - - NoPreviewRevision rev = null; - if (revStr != null) { - try { - rev = NoPreviewRevision.parseRevision(revStr); - } catch (NumberFormatException ignore) {} - } - - return rev; - } - - - /** - * Utility method that returns a property from a {@link Properties} object. - * Returns the default value if props is null or if the property is not defined. - * - * @param props The {@link Properties} to search into. - * If null, the default value is returned. - * @param propKey The name of the property. Must not be null. - * @param defaultValue The default value to return if {@code props} is null or if the - * key is not found. Can be null. - * @return The string value of the given key in the properties, or null if the key - * isn't found or if {@code props} is null. - */ - @Nullable - public static String getProperty( - @Nullable Properties props, - @NonNull String propKey, - @Nullable String defaultValue) { - if (props == null) { - return defaultValue; - } - return props.getProperty(propKey, defaultValue); - } - - - /** - * Parses the skin folder and builds the skin list. - * @param skinRootFolder The path to the skin root folder. - */ - @NonNull - public static List parseSkinFolder(@NonNull File skinRootFolder, @NonNull IFileOp fileOp) { - if (fileOp.isDirectory(skinRootFolder)) { - ArrayList skinList = new ArrayList(); - - File[] files = fileOp.listFiles(skinRootFolder); - - for (File skinFolder : files) { - if (fileOp.isDirectory(skinFolder)) { - // check for layout file - File layout = new File(skinFolder, SdkConstants.FN_SKIN_LAYOUT); - - if (fileOp.isFile(layout)) { - // for now we don't parse the content of the layout and - // simply add the directory to the list. - skinList.add(skinFolder); - } - } - } - - Collections.sort(skinList); - return skinList; - } - - return Collections.emptyList(); - } - -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-01.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-01.xsd deleted file mode 100644 index 1d533132..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-01.xsd +++ /dev/null @@ -1,295 +0,0 @@ - - - - - - - - - - - - The repository contains a collection of downloadable packages. - - - - - - - - - - - - - - An SDK add-on package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK extra package. This kind of package is for "free" content. - Such packages are installed in SDK/vendor/path. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - One path segment for the install path of an extra element. - It must be a single-segment path. It must not be empty. - - - - - - - - - - - - - - A license definition. Such a license must be used later as a reference - using a uses-license element in one of the package elements. - - - - - - - - - - - - - - - - - Describes the license used by a package. The license MUST be defined - using a license node and referenced using the ref attribute of the - license element inside a package. - - - - - - - - - - - - A collection of files that can be downloaded for a given architecture. - The <archives> node is mandatory in the repository packages and the - collection must have at least one <archive> declared. - Each archive is a zip file that will be unzipped in a location that depends - on its package type. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - A SHA1 checksum. - - - - - - - - - A file checksum, currently only SHA1. - - - - - - - - - diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-02.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-02.xsd deleted file mode 100644 index 27fae8b1..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-02.xsd +++ /dev/null @@ -1,361 +0,0 @@ - - - - - - - - - - - - The repository contains a collection of downloadable packages. - - - - - - - - - - - - - - An SDK add-on package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Version information for a layoutlib included in an addon. - . - - - - - - - - - - - - - - - - An SDK extra package. This kind of package is for "free" content. - Such packages are installed in SDK/vendor/path. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - One path segment for the install path of an extra element. - It must be a single-segment path. It must not be empty. - - - - - - - - - - - - - - A license definition. Such a license must be used later as a reference - using a uses-license element in one of the package elements. - - - - - - - - - - - - - - - - - Describes the license used by a package. The license MUST be defined - using a license node and referenced using the ref attribute of the - license element inside a package. - - - - - - - - - - - - A collection of files that can be downloaded for a given architecture. - The <archives> node is mandatory in the repository packages and the - collection must have at least one <archive> declared. - Each archive is a zip file that will be unzipped in a location that depends - on its package type. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - A collection of file paths available in an <extra> package - that can be installed in an Android project. - If present, the <project-files> collection must contain at least one path. - Each path is relative to the root directory of the package. - - - - - - - - - - - - - - A SHA1 checksum. - - - - - - - - - A file checksum, currently only SHA1. - - - - - - - - - diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-03.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-03.xsd deleted file mode 100644 index ccd00c2a..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-03.xsd +++ /dev/null @@ -1,381 +0,0 @@ - - - - - - - - - - - - The repository contains a collection of downloadable packages. - - - - - - - - - - - - - - An SDK add-on package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Version information for a layoutlib included in an addon. - . - - - - - - - - - - - - - - - - An SDK extra package. This kind of package is for "free" content. - Such packages are installed in SDK/vendor/path. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - One path segment for the install path of an extra element. - It must be a single-segment path. It must not be empty. - - - - - - - - - - - A semi-colon separated list of a segmentTypes. - - - - - - - - - - - - - - A license definition. Such a license must be used later as a reference - using a uses-license element in one of the package elements. - - - - - - - - - - - - - - - - - Describes the license used by a package. The license MUST be defined - using a license node and referenced using the ref attribute of the - license element inside a package. - - - - - - - - - - - - A collection of files that can be downloaded for a given architecture. - The <archives> node is mandatory in the repository packages and the - collection must have at least one <archive> declared. - Each archive is a zip file that will be unzipped in a location that depends - on its package type. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - A collection of file paths available in an <extra> package - that can be installed in an Android project. - If present, the <project-files> collection must contain at least one path. - Each path is relative to the root directory of the package. - - - - - - - - - - - - - - A SHA1 checksum. - - - - - - - - - A file checksum, currently only SHA1. - - - - - - - - - diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-04.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-04.xsd deleted file mode 100644 index c31efbf5..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-04.xsd +++ /dev/null @@ -1,417 +0,0 @@ - - - - - - - - - - - - The repository contains a collection of downloadable packages. - - - - - - - - - - - - - - An SDK add-on package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An ID string for an addon/extra name-id or vendor-id - can only be simple alphanumeric string. - - - - - - - - - - - - - - Version information for a layoutlib included in an addon. - . - - - - - - - - - - - - - - - - An SDK extra package. This kind of package is for "free" content. - Such packages are installed in SDK/vendor/path. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - One path segment for the install path of an extra element. - It must be a single-segment path. It must not be empty. - - - - - - - - - - - A semi-colon separated list of a segmentTypes. - - - - - - - - - - - - - - A license definition. Such a license must be used later as a reference - using a uses-license element in one of the package elements. - - - - - - - - - - - - - - - - - Describes the license used by a package. The license MUST be defined - using a license node and referenced using the ref attribute of the - license element inside a package. - - - - - - - - - - - - A collection of files that can be downloaded for a given architecture. - The <archives> node is mandatory in the repository packages and the - collection must have at least one <archive> declared. - Each archive is a zip file that will be unzipped in a location that depends - on its package type. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - A collection of file paths available in an <extra> package - that can be installed in an Android project. - If present, the <project-files> collection must contain at least one path. - Each path is relative to the root directory of the package. - - - - - - - - - - - - - - A SHA1 checksum. - - - - - - - - - A file checksum, currently only SHA1. - - - - - - - - - diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-05.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-05.xsd deleted file mode 100644 index 546b00d3..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-05.xsd +++ /dev/null @@ -1,442 +0,0 @@ - - - - - - - - - - - - The repository contains a collection of downloadable packages. - - - - - - - - - - - - - - - An SDK add-on package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An ID string for an addon/extra name-id or vendor-id - can only be simple alphanumeric string. - - - - - - - - - - - - - - Version information for a layoutlib included in an addon. - . - - - - - - - - - - - - - - - - An SDK extra package. This kind of package is for "free" content. - Such packages are installed in SDK/vendor/path. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - A full revision, with a major.minor.micro and an - optional preview number. The major number is mandatory. - - - - - - - - - - - - - - - - - - - - - One path segment for the install path of an extra element. - It must be a single-segment path. It must not be empty. - - - - - - - - - - - A semi-colon separated list of a segmentTypes. - - - - - - - - - - - - - - A license definition. Such a license must be used later as a reference - using a uses-license element in one of the package elements. - - - - - - - - - - - - - - - - - Describes the license used by a package. The license MUST be defined - using a license node and referenced using the ref attribute of the - license element inside a package. - - - - - - - - - - - - A collection of files that can be downloaded for a given architecture. - The <archives> node is mandatory in the repository packages and the - collection must have at least one <archive> declared. - Each archive is a zip file that will be unzipped in a location that depends - on its package type. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - A collection of file paths available in an <extra> package - that can be installed in an Android project. - If present, the <project-files> collection must contain at least one path. - Each path is relative to the root directory of the package. - - - - - - - - - - - - - - A SHA1 checksum. - - - - - - - - - A file checksum, currently only SHA1. - - - - - - - - - diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-06.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-06.xsd deleted file mode 100644 index 3457aada..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-06.xsd +++ /dev/null @@ -1,472 +0,0 @@ - - - - - - - - - - - - The repository contains a collection of downloadable packages. - - - - - - - - - - - - - - - An SDK add-on package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An ID string for an addon/extra name-id or vendor-id - can only be simple alphanumeric string. - - - - - - - - - - - - - - Version information for a layoutlib included in an addon. - . - - - - - - - - - - - - - - - - An SDK extra package. This kind of package is for "free" content. - Such packages are installed in SDK/vendor/path. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - A full revision, with a major.minor.micro and an - optional preview number. The major number is mandatory. - - - - - - - - - - - - - - - - - - - - - A full revision, with a major.minor.micro and no support for - the optional preview number. The major number is mandatory. - - - - - - - - - - - - - - - - - - - One path segment for the install path of an extra element. - It must be a single-segment path. It must not be empty. - - - - - - - - - - - A semi-colon separated list of a segmentTypes. - - - - - - - - - - - - - - A license definition. Such a license must be used later as a reference - using a uses-license element in one of the package elements. - - - - - - - - - - - - - - - - - Describes the license used by a package. The license MUST be defined - using a license node and referenced using the ref attribute of the - license element inside a package. - - - - - - - - - - - - A collection of files that can be downloaded for a given architecture. - The <archives> node is mandatory in the repository packages and the - collection must have at least one <archive> declared. - Each archive is a zip file that will be unzipped in a location that depends - on its package type. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - A collection of file paths available in an <extra> package - that can be installed in an Android project. - If present, the <project-files> collection must contain at least one path. - Each path is relative to the root directory of the package. - - - - - - - - - - - - - - A SHA1 checksum. - - - - - - - - - A file checksum, currently only SHA1. - - - - - - - - - diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-07.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-07.xsd deleted file mode 100644 index 3c2c13a4..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addon-07.xsd +++ /dev/null @@ -1,499 +0,0 @@ - - - - - - - - - - - - The repository contains a collection of downloadable packages. - - - - - - - - - - - - - - - An SDK add-on package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An ID string for an addon/extra name-id or vendor-id - can only be simple alphanumeric string. - - - - - - - - - - - - - - Version information for a layoutlib included in an addon. - . - - - - - - - - - - - - - - - - An SDK extra package. This kind of package is for "free" content. - Such packages are installed in SDK/vendor/path. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - A full revision, with a major.minor.micro and an - optional preview number. The major number is mandatory. - - - - - - - - - - - - - - - - - - - - - A full revision, with a major.minor.micro and no support for - the optional preview number. The major number is mandatory. - - - - - - - - - - - - - - - - - - - One path segment for the install path of an extra element. - It must be a single-segment path. It must not be empty. - - - - - - - - - - - A semi-colon separated list of a segmentTypes. - - - - - - - - - - - - - - A license definition. Such a license must be used later as a reference - using a uses-license element in one of the package elements. - - - - - - - - - - - - - - - - - Describes the license used by a package. The license MUST be defined - using a license node and referenced using the ref attribute of the - license element inside a package. - - - - - - - - - - - - A collection of files that can be downloaded for a given architecture. - The <archives> node is mandatory in the repository packages and the - collection must have at least one <archive> declared. - Each archive is a zip file that will be unzipped in a location that depends - on its package type. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - A collection of file paths available in an <extra> package - that can be installed in an Android project. - If present, the <project-files> collection must contain at least one path. - Each path is relative to the root directory of the package. - - - - - - - - - - - - - - A CPU bit size filter. - - - - - - - - - - A host OS filter. - - - - - - - - - - - A JVM version number, e.g. "1" or "1.6" or "1.14.15". - - - - - - - - - - - - A SHA1 checksum. - - - - - - - - - A file checksum, currently only SHA1. - - - - - - - - - diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addons-list-1.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addons-list-1.xsd deleted file mode 100644 index 176fb608..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addons-list-1.xsd +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - - - - - A simple list of add-ons site. - - - - - - - - - - - - An SDK add-on site. - - - - - - - - - - - diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addons-list-2.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addons-list-2.xsd deleted file mode 100644 index dde72148..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-addons-list-2.xsd +++ /dev/null @@ -1,106 +0,0 @@ - - - - - - - - - - - - A simple list of add-ons site. - - - - - - - - - - - - - An SDK add-on site. - - - - - - - - - - - - - - - - An SDK sys-img site. - - - - - - - - - - - - diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-01.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-01.xsd deleted file mode 100644 index 38ec3096..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-01.xsd +++ /dev/null @@ -1,381 +0,0 @@ - - - - - - - - - - The repository contains collections of downloadable packages. - - - - - - - - - - - An SDK platform package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK add-on package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK tool package. - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK doc package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK extra package. This kind of package is for "free" - content and specifies in which fixed root directory it must be - installed. - The paths "add-ons", "platforms", "tools" and "docs" are - reserved and cannot be used. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - A license definition. Such a license must be used later as a reference - using a uses-license element in one of the package elements. - - - - - - - - - - - - - - - - - - - - - - Describes the license used by a package. The license MUST be defined - using a license node and referenced using the ref attribute of the - license element inside a package. - - - - - - - - - - - - A collection of files that can be downloaded for a given architecture. - The <archives> node is mandatory in the repository packages and the - collection must have at least one <archive> declared. - Each archive is a zip file that will be unzipped in a location that depends - on its package type. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - A SHA1 checksum. - - - - - - - - - A file checksum, currently only SHA1. - - - - - - - - - - diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-02.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-02.xsd deleted file mode 100644 index ecadc3f9..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-02.xsd +++ /dev/null @@ -1,438 +0,0 @@ - - - - - - - - - - - - The repository contains a collection of downloadable packages. - - - - - - - - - - - - - - - - - - An SDK platform package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK add-on package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK tool package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK doc package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK sample package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK extra package. This kind of package is for "free" - content and specifies in which fixed root directory it must be - installed. - The paths "add-ons", "platforms", "tools" and "docs" are - reserved and cannot be used. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - A license definition. Such a license must be used later as a reference - using a uses-license element in one of the package elements. - - - - - - - - - - - - - - - - - Describes the license used by a package. The license MUST be defined - using a license node and referenced using the ref attribute of the - license element inside a package. - - - - - - - - - - - - A collection of files that can be downloaded for a given architecture. - The <archives> node is mandatory in the repository packages and the - collection must have at least one <archive> declared. - Each archive is a zip file that will be unzipped in a location that depends - on its package type. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - A SHA1 checksum. - - - - - - - - - A file checksum, currently only SHA1. - - - - - - - - - diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-03.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-03.xsd deleted file mode 100644 index 75d8541f..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-03.xsd +++ /dev/null @@ -1,436 +0,0 @@ - - - - - - - - - - - - The repository contains a collection of downloadable packages. - - - - - - - - - - - - - - - - - - An SDK platform package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK tool package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK platform-tool package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK doc package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK sample package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK extra package. This kind of package is for "free" content. - Such packages are installed in SDK/vendor/path. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - One path segment for the install path of an extra element. - It must be a single-segment path. - - - - - - - - - - - - - - A license definition. Such a license must be used later as a reference - using a uses-license element in one of the package elements. - - - - - - - - - - - - - - - - - Describes the license used by a package. The license MUST be defined - using a license node and referenced using the ref attribute of the - license element inside a package. - - - - - - - - - - - - A collection of files that can be downloaded for a given architecture. - The <archives> node is mandatory in the repository packages and the - collection must have at least one <archive> declared. - Each archive is a zip file that will be unzipped in a location that depends - on its package type. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - A SHA1 checksum. - - - - - - - - - A file checksum, currently only SHA1. - - - - - - - - - diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-04.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-04.xsd deleted file mode 100644 index 9b14772a..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-04.xsd +++ /dev/null @@ -1,500 +0,0 @@ - - - - - - - - - - - - The repository contains a collection of downloadable packages. - - - - - - - - - - - - - - - - - - An SDK platform package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Version information for a layoutlib included in a platform. - - - - - - - - - - - - - - - - An SDK tool package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK platform-tool package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK doc package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK sample package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK extra package. This kind of package is for "free" content. - Such packages are installed in SDK/vendor/path. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - One path segment for the install path of an extra element. - It must be a single-segment path. - - - - - - - - - - - - - - A license definition. Such a license must be used later as a reference - using a uses-license element in one of the package elements. - - - - - - - - - - - - - - - - - Describes the license used by a package. The license MUST be defined - using a license node and referenced using the ref attribute of the - license element inside a package. - - - - - - - - - - - - A collection of files that can be downloaded for a given architecture. - The <archives> node is mandatory in the repository packages and the - collection must have at least one <archive> declared. - Each archive is a zip file that will be unzipped in a location that depends - on its package type. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - A collection of file paths available in an <extra> package - that can be installed in an Android project. - If present, the <project-files> collection must contain at least one path. - Each path is relative to the root directory of the package. - - - - - - - - - - - - - - A SHA1 checksum. - - - - - - - - - A file checksum, currently only SHA1. - - - - - - - - - diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-05.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-05.xsd deleted file mode 100644 index ae8275f0..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-05.xsd +++ /dev/null @@ -1,624 +0,0 @@ - - - - - - - - - - - - The repository contains a collection of downloadable packages. - - - - - - - - - - - - - - - - - - - - An SDK platform package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Version information for a layoutlib included in a platform. - - - - - - - - - - - - - - - - - System Image for a platform. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - The ABI of a platform's system image. - - - - - - - - - - - - - - - Sources for a platform. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK tool package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK platform-tool package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK doc package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK sample package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK extra package. This kind of package is for "free" content. - Such packages are installed in SDK/vendor/path. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - One path segment for the install path of an extra element. - It must be a single-segment path. - - - - - - - - - - - A semi-colon separated list of a segmentTypes. - - - - - - - - - - - - - - A license definition. Such a license must be used later as a reference - using a uses-license element in one of the package elements. - - - - - - - - - - - - - - - - - Describes the license used by a package. The license MUST be defined - using a license node and referenced using the ref attribute of the - license element inside a package. - - - - - - - - - - - - A collection of files that can be downloaded for a given architecture. - The <archives> node is mandatory in the repository packages and the - collection must have at least one <archive> declared. - Each archive is a zip file that will be unzipped in a location that depends - on its package type. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - A collection of file paths available in an <extra> package - that can be installed in an Android project. - If present, the <project-files> collection must contain at least one path. - Each path is relative to the root directory of the package. - - - - - - - - - - - - - - A SHA1 checksum. - - - - - - - - - A file checksum, currently only SHA1. - - - - - - - - - diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-06.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-06.xsd deleted file mode 100644 index bccce69d..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-06.xsd +++ /dev/null @@ -1,608 +0,0 @@ - - - - - - - - - - - - The repository contains a collection of downloadable packages. - - - - - - - - - - - - - - - - - - - An SDK platform package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Version information for a layoutlib included in a platform. - - - - - - - - - - - - - - - - - System Image for a platform. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - The ABI of a platform's system image. - - - - - - - - - - - - - - - - Sources for a platform. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK tool package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK platform-tool package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK doc package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK sample package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - One path segment for the install path of an extra element. - It must be a single-segment path. - - - - - - - - - - - A semi-colon separated list of a segmentTypes. - - - - - - - - - - - - - - A license definition. Such a license must be used later as a reference - using a uses-license element in one of the package elements. - - - - - - - - - - - - - - - - - Describes the license used by a package. The license MUST be defined - using a license node and referenced using the ref attribute of the - license element inside a package. - - - - - - - - - - - - A collection of files that can be downloaded for a given architecture. - The <archives> node is mandatory in the repository packages and the - collection must have at least one <archive> declared. - Each archive is a zip file that will be unzipped in a location that depends - on its package type. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - A collection of file paths available in an <extra> package - that can be installed in an Android project. - If present, the <project-files> collection must contain at least one path. - Each path is relative to the root directory of the package. - - - - - - - - - - - - - - A SHA1 checksum. - - - - - - - - - A file checksum, currently only SHA1. - - - - - - - - - diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-07.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-07.xsd deleted file mode 100644 index ea180709..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-07.xsd +++ /dev/null @@ -1,612 +0,0 @@ - - - - - - - - - - - - The repository contains a collection of downloadable packages. - - - - - - - - - - - - - - - - - - - An SDK platform package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Version information for a layoutlib included in a platform. - - - - - - - - - - - - - - - - - System Image for a platform. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - The ABI of a platform's system image. - - - - - - - - - - - - - - - - Sources for a platform. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK tool package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK platform-tool package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK doc package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK sample package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - One path segment for the install path of an extra element. - It must be a single-segment path. - - - - - - - - - - - A semi-colon separated list of a segmentTypes. - - - - - - - - - - - - - - A license definition. Such a license must be used later as a reference - using a uses-license element in one of the package elements. - - - - - - - - - - - - - - - - - Describes the license used by a package. The license MUST be defined - using a license node and referenced using the ref attribute of the - license element inside a package. - - - - - - - - - - - - A collection of files that can be downloaded for a given architecture. - The <archives> node is mandatory in the repository packages and the - collection must have at least one <archive> declared. - Each archive is a zip file that will be unzipped in a location that depends - on its package type. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - A full revision, with a major.minor.micro and an - optional preview number. The major number is mandatory. - - - - - - - - - - - - - - - - - - - - - A collection of file paths available in an <extra> package - that can be installed in an Android project. - If present, the <project-files> collection must contain at least one path. - Each path is relative to the root directory of the package. - - - - - - - - - - - - - - A SHA1 checksum. - - - - - - - - - A file checksum, currently only SHA1. - - - - - - - - - diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-08.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-08.xsd deleted file mode 100644 index 0c4ca630..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-08.xsd +++ /dev/null @@ -1,652 +0,0 @@ - - - - - - - - - - - - The repository contains a collection of downloadable packages. - - - - - - - - - - - - - - - - - - - - An SDK platform package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Version information for a layoutlib included in a platform. - - - - - - - - - - - - - - - - - System Image for a platform. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - The ABI of a platform's system image. - - - - - - - - - - - - - - - - Sources for a platform. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK tool package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK platform-tool package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK build-tool package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK doc package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK sample package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - One path segment for the install path of an extra element. - It must be a single-segment path. - - - - - - - - - - - A semi-colon separated list of a segmentTypes. - - - - - - - - - - - - - - A license definition. Such a license must be used later as a reference - using a uses-license element in one of the package elements. - - - - - - - - - - - - - - - - - Describes the license used by a package. The license MUST be defined - using a license node and referenced using the ref attribute of the - license element inside a package. - - - - - - - - - - - - A collection of files that can be downloaded for a given architecture. - The <archives> node is mandatory in the repository packages and the - collection must have at least one <archive> declared. - Each archive is a zip file that will be unzipped in a location that depends - on its package type. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - A full revision, with a major.minor.micro and an - optional preview number. The major number is mandatory. - - - - - - - - - - - - - - - - - - - - - A collection of file paths available in an <extra> package - that can be installed in an Android project. - If present, the <project-files> collection must contain at least one path. - Each path is relative to the root directory of the package. - - - - - - - - - - - - - - A SHA1 checksum. - - - - - - - - - A file checksum, currently only SHA1. - - - - - - - - - diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-09.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-09.xsd deleted file mode 100644 index 1b9d68d4..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-09.xsd +++ /dev/null @@ -1,677 +0,0 @@ - - - - - - - - - - - - The repository contains a collection of downloadable packages. - - - - - - - - - - - - - - - - - - - - - An SDK platform package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Version information for a layoutlib included in a platform. - - - - - - - - - - - - - - - - - System Image for a platform. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - A tag string for a system image can only be a simple alphanumeric string. - - - - - - - - - - - - - The ABI of a platform's system image. - - - - - - - - - - - - - - - - Sources for a platform. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK tool package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK platform-tool package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK build-tool package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK doc package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK sample package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - One path segment for the install path of an extra element. - It must be a single-segment path. - - - - - - - - - - - A semi-colon separated list of a segmentTypes. - - - - - - - - - - - - - - A license definition. Such a license must be used later as a reference - using a uses-license element in one of the package elements. - - - - - - - - - - - - - - - - - Describes the license used by a package. The license MUST be defined - using a license node and referenced using the ref attribute of the - license element inside a package. - - - - - - - - - - - - A collection of files that can be downloaded for a given architecture. - The <archives> node is mandatory in the repository packages and the - collection must have at least one <archive> declared. - Each archive is a zip file that will be unzipped in a location that depends - on its package type. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - A full revision, with a major.minor.micro and an - optional preview number. The major number is mandatory. - - - - - - - - - - - - - - - - - - - - - A collection of file paths available in an <extra> package - that can be installed in an Android project. - If present, the <project-files> collection must contain at least one path. - Each path is relative to the root directory of the package. - - - - - - - - - - - - - - A SHA1 checksum. - - - - - - - - - A file checksum, currently only SHA1. - - - - - - - - - diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-10.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-10.xsd deleted file mode 100644 index 3fc1d929..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-10.xsd +++ /dev/null @@ -1,653 +0,0 @@ - - - - - - - - - - - - The repository contains a collection of downloadable packages. - - - - - - - - - - - - - - - - - - - - An SDK platform package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Version information for a layoutlib included in a platform. - - - - - - - - - - - - - - - - The ABI of a platform's system image. - - - - - - - - - - - - - - - - Sources for a platform. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK tool package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK platform-tool package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK build-tool package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK doc package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK sample package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - One path segment for the install path of an extra element. - It must be a single-segment path. - - - - - - - - - - - A semi-colon separated list of a segmentTypes. - - - - - - - - - - - - - - A license definition. Such a license must be used later as a reference - using a uses-license element in one of the package elements. - - - - - - - - - - - - - - - - - Describes the license used by a package. The license MUST be defined - using a license node and referenced using the ref attribute of the - license element inside a package. - - - - - - - - - - - - A collection of files that can be downloaded for a given architecture. - The <archives> node is mandatory in the repository packages and the - collection must have at least one <archive> declared. - Each archive is a zip file that will be unzipped in a location that depends - on its package type. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - A full revision, with a major.minor.micro and an - optional preview number. The major number is mandatory. - - - - - - - - - - - - - - - - - - - - - A collection of file paths available in an <extra> package - that can be installed in an Android project. - If present, the <project-files> collection must contain at least one path. - Each path is relative to the root directory of the package. - - - - - - - - - - - - - - A CPU bit size filter. - - - - - - - - - - A host OS filter. - - - - - - - - - - - A JVM version number, e.g. "1" or "1.6" or "1.14.15". - - - - - - - - - - - - A SHA1 checksum. - - - - - - - - - A file checksum, currently only SHA1. - - - - - - - - - diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-11.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-11.xsd deleted file mode 100644 index 8e9f736a..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-repository-11.xsd +++ /dev/null @@ -1,680 +0,0 @@ - - - - - - - - - - - - The repository contains a collection of downloadable packages. - - - - - - - - - - - - - - - - - - - - An NDK package. - - - - - - - - - - - - - - - - - - - - - - - - An SDK platform package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Version information for a layoutlib included in a platform. - - - - - - - - - - - - - - - - The ABI of a platform's system image. - - - - - - - - - - - - - - - - Sources for a platform. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK tool package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK platform-tool package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK build-tool package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK doc package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An SDK sample package. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - One path segment for the install path of an extra element. - It must be a single-segment path. - - - - - - - - - - - A semi-colon separated list of a segmentTypes. - - - - - - - - - - - - - - A license definition. Such a license must be used later as a reference - using a uses-license element in one of the package elements. - - - - - - - - - - - - - - - - - Describes the license used by a package. The license MUST be defined - using a license node and referenced using the ref attribute of the - license element inside a package. - - - - - - - - - - - - A collection of files that can be downloaded for a given architecture. - The <archives> node is mandatory in the repository packages and the - collection must have at least one <archive> declared. - Each archive is a zip file that will be unzipped in a location that depends - on its package type. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - A full revision, with a major.minor.micro and an - optional preview number. The major number is mandatory. - - - - - - - - - - - - - - - - - - - - - A collection of file paths available in an <extra> package - that can be installed in an Android project. - If present, the <project-files> collection must contain at least one path. - Each path is relative to the root directory of the package. - - - - - - - - - - - - - - A CPU bit size filter. - - - - - - - - - - A host OS filter. - - - - - - - - - - - A JVM version number, e.g. "1" or "1.6" or "1.14.15". - - - - - - - - - - - - A SHA1 checksum. - - - - - - - - - A file checksum, currently only SHA1. - - - - - - - - - diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-stats-1.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-stats-1.xsd deleted file mode 100644 index 2944b124..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-stats-1.xsd +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - - - - - - - A simple list of platform stats. - - - - - - - - - - - - - Stats information for a given Android platform. - The api-level acts as a key, and it is epxected there should only - be one platform listed with the same API-level. - - - - - - - - - - - - - - - - - - - - - - - A decimal percentage, between 0.0 and 100.0%. - - - - - - - - diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-sys-img-01.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-sys-img-01.xsd deleted file mode 100644 index 005b4316..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-sys-img-01.xsd +++ /dev/null @@ -1,229 +0,0 @@ - - - - - - - - - - - - The repository contains a collection of downloadable system images. - - - - - - - - - - - - - - - System Image for a platform. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - The ABI of a platform's system image. - - - - - - - - - - - - - - - - A license definition. Such a license must be used later as a reference - using a uses-license element in one of the package elements. - - - - - - - - - - - - - - - - - Describes the license used by a package. The license MUST be defined - using a license node and referenced using the ref attribute of the - license element inside a package. - - - - - - - - - - - - A collection of files that can be downloaded for a given architecture. - The <archives> node is mandatory in the repository packages and the - collection must have at least one <archive> declared. - Each archive is a zip file that will be unzipped in a location that depends - on its package type. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - A SHA1 checksum. - - - - - - - - - A file checksum, currently only SHA1. - - - - - - - - - diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-sys-img-02.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-sys-img-02.xsd deleted file mode 100644 index a1c16a32..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-sys-img-02.xsd +++ /dev/null @@ -1,249 +0,0 @@ - - - - - - - - - - - - The repository contains a collection of downloadable system images. - - - - - - - - - - - - - - - System Image for a platform. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - An tag string for a system image can only be simple alphanumeric string. - - - - - - - - - - - - - The ABI of a platform's system image. - - - - - - - - - - - - - - - - A license definition. Such a license must be used later as a reference - using a uses-license element in one of the package elements. - - - - - - - - - - - - - - - - - Describes the license used by a package. The license MUST be defined - using a license node and referenced using the ref attribute of the - license element inside a package. - - - - - - - - - - - - A collection of files that can be downloaded for a given architecture. - The <archives> node is mandatory in the repository packages and the - collection must have at least one <archive> declared. - Each archive is a zip file that will be unzipped in a location that depends - on its package type. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - A SHA1 checksum. - - - - - - - - - A file checksum, currently only SHA1. - - - - - - - - - diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-sys-img-03.xsd b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-sys-img-03.xsd deleted file mode 100644 index 9b63c010..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/sdk-sys-img-03.xsd +++ /dev/null @@ -1,309 +0,0 @@ - - - - - - - - - - - - The repository contains a collection of downloadable system images. - - - - - - - - - - - - - - - System Image for a platform. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Indicates a system-image is tied to an add-on and which one - (the combo tag-id + vendor-id uniquely identifies the add-on.) - Mandatory for add-on system-images. - Must not be present for platform system-images. - - - - - - - - - - - - - - - An tag string for a system image can only be simple alphanumeric string. - - - - - - - - - - - - - The ABI of a platform's system image. - - - - - - - - - - - - - - - - - - - A license definition. Such a license must be used later as a reference - using a uses-license element in one of the package elements. - - - - - - - - - - - - - - - - - Describes the license used by a package. The license MUST be defined - using a license node and referenced using the ref attribute of the - license element inside a package. - - - - - - - - - - - - A collection of files that can be downloaded for a given architecture. - The <archives> node is mandatory in the repository packages and the - collection must have at least one <archive> declared. - Each archive is a zip file that will be unzipped in a location that depends - on its package type. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - A CPU bit size filter. - - - - - - - - - - A host OS filter. - - - - - - - - - - - A JVM version number, e.g. "1" or "1.6" or "1.14.15". - - - - - - - - - - - - A SHA1 checksum. - - - - - - - - - A file checksum, currently only SHA1. - - - - - - - - - diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/util/CommandLineParser.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/util/CommandLineParser.java deleted file mode 100644 index 28918dac..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/util/CommandLineParser.java +++ /dev/null @@ -1,976 +0,0 @@ -/* - * 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.sdklib.util; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.utils.ILogger; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map.Entry; - -/** - * Parses the command-line and stores flags needed or requested. - *

- * This is a base class. To be useful you want to: - *

    - *
  • override it. - *
  • pass an action array to the constructor. - *
  • define flags for your actions. - *
- *

- * To use, call {@link #parseArgs(String[])} and then - * call {@link #getValue(String, String, String)}. - */ -public class CommandLineParser { - - /* - * Steps needed to add a new action: - * - Each action is defined as a "verb object" followed by parameters. - * - Either reuse a VERB_ constant or define a new one. - * - Either reuse an OBJECT_ constant or define a new one. - * - Add a new entry to mAction with a one-line help summary. - * - In the constructor, add a define() call for each parameter (either mandatory - * or optional) for the given action. - */ - - /** Internal verb name for internally hidden flags. */ - public static final String GLOBAL_FLAG_VERB = "@@internal@@"; //$NON-NLS-1$ - - /** String to use when the verb doesn't need any object. */ - public static final String NO_VERB_OBJECT = ""; //$NON-NLS-1$ - - /** The global help flag. */ - public static final String KEY_HELP = "help"; - /** The global verbose flag. */ - public static final String KEY_VERBOSE = "verbose"; - /** The global silent flag. */ - public static final String KEY_SILENT = "silent"; - - /** Verb requested by the user. Null if none specified, which will be an error. */ - private String mVerbRequested; - /** Direct object requested by the user. Can be null. */ - private String mDirectObjectRequested; - - /** - * Action definitions. - *

- * This list serves two purposes: first it is used to know which verb/object - * actions are acceptable on the command-line; second it provides a summary - * for each action that is printed in the help. - *

- * Each entry is a string array with: - *

    - *
  • the verb. - *
  • a direct object (use {@link #NO_VERB_OBJECT} if there's no object). - *
  • a description. - *
  • an alternate form for the object (e.g. plural). - *
- */ - private final String[][] mActions; - - private static final int ACTION_VERB_INDEX = 0; - private static final int ACTION_OBJECT_INDEX = 1; - private static final int ACTION_DESC_INDEX = 2; - private static final int ACTION_ALT_OBJECT_INDEX = 3; - - /** - * The map of all defined arguments. - *

- * The key is a string "verb/directObject/longName". - */ - private final HashMap mArguments = new HashMap(); - /** Logger */ - private final ILogger mLog; - - /** - * Constructs a new command-line processor. - * - * @param logger An SDK logger object. Must not be null. - * @param actions The list of actions recognized on the command-line. - * See the javadoc of {@link #mActions} for more details. - * - * @see #mActions - */ - public CommandLineParser(ILogger logger, String[][] actions) { - mLog = logger; - mActions = actions; - - /* - * usage should fit in 80 columns, including the space to print the options: - * " -v --verbose 7890123456789012345678901234567890123456789012345678901234567890" - */ - - define(Mode.BOOLEAN, false, GLOBAL_FLAG_VERB, NO_VERB_OBJECT, "v", KEY_VERBOSE, - "Verbose mode, shows errors, warnings and all messages.", - false); - define(Mode.BOOLEAN, false, GLOBAL_FLAG_VERB, NO_VERB_OBJECT, "s", KEY_SILENT, - "Silent mode, shows errors only.", - false); - define(Mode.BOOLEAN, false, GLOBAL_FLAG_VERB, NO_VERB_OBJECT, "h", KEY_HELP, - "Help on a specific command.", - false); - } - - /** - * Indicates if this command-line can work when no verb is specified. - * The default is false, which generates an error when no verb/object is specified. - * Derived implementations can set this to true if they can deal with a lack - * of verb/action. - */ - public boolean acceptLackOfVerb() { - return false; - } - - - //------------------ - // Helpers to get flags values - - /** Helper that returns true if --verbose was requested. */ - public boolean isVerbose() { - return ((Boolean) getValue(GLOBAL_FLAG_VERB, NO_VERB_OBJECT, KEY_VERBOSE)).booleanValue(); - } - - /** Helper that returns true if --silent was requested. */ - public boolean isSilent() { - return ((Boolean) getValue(GLOBAL_FLAG_VERB, NO_VERB_OBJECT, KEY_SILENT)).booleanValue(); - } - - /** Helper that returns true if --help was requested. */ - public boolean isHelpRequested() { - return ((Boolean) getValue(GLOBAL_FLAG_VERB, NO_VERB_OBJECT, KEY_HELP)).booleanValue(); - } - - /** Returns the verb name from the command-line. Can be null. */ - public String getVerb() { - return mVerbRequested; - } - - /** Returns the direct object name from the command-line. Can be null. */ - public String getDirectObject() { - return mDirectObjectRequested; - } - - //------------------ - - /** - * Raw access to parsed parameter values. - *

- * The default is to scan all parameters. Parameters that have been explicitly set on the - * command line are returned first. Otherwise one with a non-null value is returned. - *

- * Both a verb and a direct object filter can be specified. When they are non-null they limit - * the scope of the search. - *

- * If nothing has been found, return the last default value seen matching the filter. - * - * @param verb The verb name, including {@link #GLOBAL_FLAG_VERB}. If null, all possible - * verbs that match the direct object condition will be examined and the first - * value set will be used. - * @param directObject The direct object name, including {@link #NO_VERB_OBJECT}. If null, - * all possible direct objects that match the verb condition will be examined and - * the first value set will be used. - * @param longFlagName The long flag name for the given action. Mandatory. Cannot be null. - * @return The current value object stored in the parameter, which depends on the argument mode. - */ - public Object getValue(String verb, String directObject, String longFlagName) { - - if (verb != null && directObject != null) { - String key = verb + '/' + directObject + '/' + longFlagName; - Arg arg = mArguments.get(key); - return arg.getCurrentValue(); - } - - Object lastDefault = null; - for (Arg arg : mArguments.values()) { - if (arg.getLongArg().equals(longFlagName)) { - if (verb == null || arg.getVerb().equals(verb)) { - if (directObject == null || arg.getDirectObject().equals(directObject)) { - if (arg.isInCommandLine()) { - return arg.getCurrentValue(); - } - if (arg.getCurrentValue() != null) { - lastDefault = arg.getCurrentValue(); - } - } - } - } - } - - return lastDefault; - } - - /** - * Internal setter for raw parameter value. - * @param verb The verb name, including {@link #GLOBAL_FLAG_VERB}. - * @param directObject The direct object name, including {@link #NO_VERB_OBJECT}. - * @param longFlagName The long flag name for the given action. - * @param value The new current value object stored in the parameter, which depends on the - * argument mode. - */ - protected void setValue(String verb, String directObject, String longFlagName, Object value) { - String key = verb + '/' + directObject + '/' + longFlagName; - Arg arg = mArguments.get(key); - arg.setCurrentValue(value); - } - - /** - * Parses the command-line arguments. - *

- * This method will exit and not return if a parsing error arise. - * - * @param args The arguments typically received by a main method. - */ - public void parseArgs(String[] args) { - String errorMsg = null; - String verb = null; - String directObject = null; - - try { - int n = args.length; - for (int i = 0; i < n; i++) { - Arg arg = null; - String a = args[i]; - if (a.startsWith("--")) { //$NON-NLS-1$ - arg = findLongArg(verb, directObject, a.substring(2)); - } else if (a.startsWith("-")) { //$NON-NLS-1$ - arg = findShortArg(verb, directObject, a.substring(1)); - } - - // No matching argument name found - if (arg == null) { - // Does it looks like a dashed parameter? - if (a.startsWith("-")) { //$NON-NLS-1$ - if (verb == null || directObject == null) { - // It looks like a dashed parameter and we don't have a a verb/object - // set yet, the parameter was just given too early. - - errorMsg = String.format( - "Flag '%1$s' is not a valid global flag. Did you mean to specify it after the verb/object name?", - a); - return; - } else { - // It looks like a dashed parameter but it is unknown by this - // verb-object combination - - errorMsg = String.format( - "Flag '%1$s' is not valid for '%2$s %3$s'.", - a, verb, directObject); - return; - } - } - - if (verb == null) { - // Fill verb first. Find it. - for (String[] actionDesc : mActions) { - if (actionDesc[ACTION_VERB_INDEX].equals(a)) { - verb = a; - break; - } - } - - // Error if it was not a valid verb - if (verb == null) { - errorMsg = String.format( - "Expected verb after global parameters but found '%1$s' instead.", - a); - return; - } - - } else if (directObject == null) { - // Then fill the direct object. Find it. - for (String[] actionDesc : mActions) { - if (actionDesc[ACTION_VERB_INDEX].equals(verb)) { - if (actionDesc[ACTION_OBJECT_INDEX].equals(a)) { - directObject = a; - break; - } else if (actionDesc.length > ACTION_ALT_OBJECT_INDEX && - actionDesc[ACTION_ALT_OBJECT_INDEX].equals(a)) { - // if the alternate form exist and is used, we internally - // only memorize the default direct object form. - directObject = actionDesc[ACTION_OBJECT_INDEX]; - break; - } - } - } - - // Error if it was not a valid object for that verb - if (directObject == null) { - errorMsg = String.format( - "Expected verb after global parameters but found '%1$s' instead.", - a); - return; - - } - } else { - // The argument is not a dashed parameter and we already - // have a verb/object. Must be some extra unknown argument. - errorMsg = String.format( - "Argument '%1$s' is not recognized.", - a); - } - } else if (arg != null) { - // This argument was present on the command line - arg.setInCommandLine(true); - - // Process keyword - Object error = null; - if (arg.getMode().needsExtra()) { - if (i+1 >= n) { - errorMsg = String.format("Missing argument for flag %1$s.", a); - return; - } - - while (i+1 < n) { - String b = args[i+1]; - - if (arg.getMode() != Mode.STRING_ARRAY) { - // We never accept something that looks like a valid argument - // unless we see -- first - Arg dummyArg = null; - if (b.startsWith("--")) { //$NON-NLS-1$ - dummyArg = findLongArg(verb, directObject, b.substring(2)); - } else if (b.startsWith("-")) { //$NON-NLS-1$ - dummyArg = findShortArg(verb, directObject, b.substring(1)); - } - if (dummyArg != null) { - errorMsg = String.format( - "Oops, it looks like you didn't provide an argument for '%1$s'.\n'%2$s' was found instead.", - a, b); - return; - } - } - - error = arg.getMode().process(arg, b); - if (error == Accept.CONTINUE) { - i++; - } else if (error == Accept.ACCEPT_AND_STOP) { - i++; - break; - } else if (error == Accept.REJECT_AND_STOP) { - break; - } else if (error instanceof String) { - // We stop because of an error - break; - } - } - } else { - error = arg.getMode().process(arg, null); - - if (isHelpRequested()) { - // The --help flag was requested. We'll continue the usual processing - // so that we can find the optional verb/object words. Those will be - // used to print specific help. - // Setting a non-null error message triggers printing the help, however - // there is no specific error to print. - errorMsg = ""; //$NON-NLS-1$ - } - } - - if (error instanceof String) { - errorMsg = String.format("Invalid usage for flag %1$s: %2$s.", a, error); - return; - } - } - } - - if (errorMsg == null) { - if (verb == null && !acceptLackOfVerb()) { - errorMsg = "Missing verb name."; - } else if (verb != null) { - if (directObject == null) { - // Make sure this verb has an optional direct object - for (String[] actionDesc : mActions) { - if (actionDesc[ACTION_VERB_INDEX].equals(verb) && - actionDesc[ACTION_OBJECT_INDEX].equals(NO_VERB_OBJECT)) { - directObject = NO_VERB_OBJECT; - break; - } - } - - if (directObject == null) { - errorMsg = String.format("Missing object name for verb '%1$s'.", verb); - return; - } - } - - // Validate that all mandatory arguments are non-null for this action - String missing = null; - boolean plural = false; - for (Entry entry : mArguments.entrySet()) { - Arg arg = entry.getValue(); - if (arg.getVerb().equals(verb) && - arg.getDirectObject().equals(directObject)) { - if (arg.isMandatory() && arg.getCurrentValue() == null) { - if (missing == null) { - missing = "--" + arg.getLongArg(); //$NON-NLS-1$ - } else { - missing += ", --" + arg.getLongArg(); //$NON-NLS-1$ - plural = true; - } - } - } - } - - if (missing != null) { - errorMsg = String.format( - "The %1$s %2$s must be defined for action '%3$s %4$s'", - plural ? "parameters" : "parameter", - missing, - verb, - directObject); - } - - mVerbRequested = verb; - mDirectObjectRequested = directObject; - } - } - } finally { - if (errorMsg != null) { - printHelpAndExitForAction(verb, directObject, errorMsg); - } - } - } - - /** - * Finds an {@link Arg} given an action name and a long flag name. - * @return The {@link Arg} found or null. - */ - protected Arg findLongArg(String verb, String directObject, String longName) { - if (verb == null) { - verb = GLOBAL_FLAG_VERB; - } - if (directObject == null) { - directObject = NO_VERB_OBJECT; - } - String key = verb + '/' + directObject + '/' + longName; //$NON-NLS-1$ - return mArguments.get(key); - } - - /** - * Finds an {@link Arg} given an action name and a short flag name. - * @return The {@link Arg} found or null. - */ - protected Arg findShortArg(String verb, String directObject, String shortName) { - if (verb == null) { - verb = GLOBAL_FLAG_VERB; - } - if (directObject == null) { - directObject = NO_VERB_OBJECT; - } - - for (Entry entry : mArguments.entrySet()) { - Arg arg = entry.getValue(); - if (arg.getVerb().equals(verb) && arg.getDirectObject().equals(directObject)) { - if (shortName.equals(arg.getShortArg())) { - return arg; - } - } - } - - return null; - } - - /** - * Prints the help/usage and exits. - * - * @param errorFormat Optional error message to print prior to usage using String.format - * @param args Arguments for String.format - */ - public void printHelpAndExit(String errorFormat, Object... args) { - printHelpAndExitForAction(null /*verb*/, null /*directObject*/, errorFormat, args); - } - - /** - * Prints the help/usage and exits. - * - * @param verb If null, displays help for all verbs. If not null, display help only - * for that specific verb. In all cases also displays general usage and action list. - * @param directObject If null, displays help for all verb objects. - * If not null, displays help only for that specific action - * In all cases also display general usage and action list. - * @param errorFormat Optional error message to print prior to usage using String.format - * @param args Arguments for String.format - */ - public void printHelpAndExitForAction(String verb, String directObject, - String errorFormat, Object... args) { - if (errorFormat != null && !errorFormat.isEmpty()) { - stderr(errorFormat, args); - } - - /* - * usage should fit in 80 columns - * 12345678901234567890123456789012345678901234567890123456789012345678901234567890 - */ - stdout("\n" + - "Usage:\n" + - " android [global options] %s [action options]\n" + - "\n" + - "Global options:", - verb == null ? "action" : - verb + (directObject == null ? "" : " " + directObject)); //$NON-NLS-1$ - listOptions(GLOBAL_FLAG_VERB, NO_VERB_OBJECT); - - if (verb == null || directObject == null) { - stdout("\nValid actions are composed of a verb and an optional direct object:"); - for (String[] action : mActions) { - if (verb == null || verb.equals(action[ACTION_VERB_INDEX])) { - stdout("- %1$6s %2$-13s: %3$s", - action[ACTION_VERB_INDEX], - action[ACTION_OBJECT_INDEX], - action[ACTION_DESC_INDEX]); - } - } - } - - // Only print details if a verb/object is requested - if (verb != null) { - for (String[] action : mActions) { - if (verb == null || verb.equals(action[ACTION_VERB_INDEX])) { - if (directObject == null || directObject.equals(action[ACTION_OBJECT_INDEX])) { - stdout("\nAction \"%1$s %2$s\":", - action[ACTION_VERB_INDEX], - action[ACTION_OBJECT_INDEX]); - stdout(" %1$s", action[ACTION_DESC_INDEX]); - stdout("Options:"); - listOptions(action[ACTION_VERB_INDEX], action[ACTION_OBJECT_INDEX]); - } - } - } - } - - exit(); - } - - /** - * Internal helper to print all the option flags for a given action name. - */ - protected void listOptions(String verb, String directObject) { - int numOptions = 0; - int longArgLen = 8; - - for (Entry entry : mArguments.entrySet()) { - Arg arg = entry.getValue(); - if (arg.getVerb().equals(verb) && arg.getDirectObject().equals(directObject)) { - int n = arg.getLongArg().length(); - if (n > longArgLen) { - longArgLen = n; - } - } - } - - for (Entry entry : mArguments.entrySet()) { - Arg arg = entry.getValue(); - if (arg.getVerb().equals(verb) && arg.getDirectObject().equals(directObject)) { - - String value = ""; //$NON-NLS-1$ - String required = ""; //$NON-NLS-1$ - if (arg.isMandatory()) { - required = " [required]"; - - } else { - if (arg.getDefaultValue() instanceof String[]) { - for (String v : (String[]) arg.getDefaultValue()) { - if (!value.isEmpty()) { - value += ", "; - } - value += v; - } - } else if (arg.getDefaultValue() != null) { - Object v = arg.getDefaultValue(); - if (arg.getMode() != Mode.BOOLEAN || v.equals(Boolean.TRUE)) { - value = v.toString(); - } - } - if (!value.isEmpty()) { - value = " [Default: " + value + "]"; - } - } - - // Java doesn't support * for printf variable width, so we'll insert the long arg - // width "manually" in the printf format string. - String longArgWidth = Integer.toString(longArgLen + 2); - - // Print a line in the form " -1_letter_arg --long_arg description" - // where either the 1-letter arg or the long arg are optional. - String output = String.format( - " %1$-2s %2$-" + longArgWidth + "s: %3$s%4$s%5$s", //$NON-NLS-1$ //$NON-NLS-2$ - !arg.getShortArg().isEmpty() ? - "-" + arg.getShortArg() : //$NON-NLS-1$ - "", //$NON-NLS-1$ - !arg.getLongArg().isEmpty() ? - "--" + arg.getLongArg() : //$NON-NLS-1$ - "", //$NON-NLS-1$ - arg.getDescription(), - value, - required); - stdout(output); - numOptions++; - } - } - - if (numOptions == 0) { - stdout(" No options"); - } - } - - //---- - - protected enum Accept { - CONTINUE, - ACCEPT_AND_STOP, - REJECT_AND_STOP, - } - - /** - * The mode of an argument specifies the type of variable it represents, - * whether an extra parameter is required after the flag and how to parse it. - */ - protected enum Mode { - /** Argument value is a Boolean. Default value is a Boolean. */ - BOOLEAN { - @Override - public boolean needsExtra() { - return false; - } - @Override - public Object process(Arg arg, String extra) { - // Toggle the current value - arg.setCurrentValue(! ((Boolean) arg.getCurrentValue()).booleanValue()); - return Accept.ACCEPT_AND_STOP; - } - }, - - /** Argument value is an Integer. Default value is an Integer. */ - INTEGER { - @Override - public boolean needsExtra() { - return true; - } - @Override - public Object process(Arg arg, String extra) { - try { - arg.setCurrentValue(Integer.parseInt(extra)); - return null; - } catch (NumberFormatException e) { - return String.format("Failed to parse '%1$s' as an integer: %2$s", extra, - e.getMessage()); - } - } - }, - - /** Argument value is a String. Default value is a String[]. */ - ENUM { - @Override - public boolean needsExtra() { - return true; - } - @Override - public Object process(Arg arg, String extra) { - StringBuilder desc = new StringBuilder(); - String[] values = (String[]) arg.getDefaultValue(); - for (String value : values) { - if (value.equals(extra)) { - arg.setCurrentValue(extra); - return Accept.ACCEPT_AND_STOP; - } - - if (desc.length() != 0) { - desc.append(", "); - } - desc.append(value); - } - - return String.format("'%1$s' is not one of %2$s", extra, desc.toString()); - } - }, - - /** Argument value is a String. Default value is a null. */ - STRING { - @Override - public boolean needsExtra() { - return true; - } - @Override - public Object process(Arg arg, String extra) { - arg.setCurrentValue(extra); - return Accept.ACCEPT_AND_STOP; - } - }, - - /** Argument value is a {@link List}<String>. Default value is an empty list. */ - STRING_ARRAY { - @Override - public boolean needsExtra() { - return true; - } - @Override - public Object process(Arg arg, String extra) { - // For simplification, a string array doesn't accept something that - // starts with a dash unless a pure -- was seen before. - if (extra != null) { - Object v = arg.getCurrentValue(); - if (v == null) { - ArrayList a = new ArrayList(); - arg.setCurrentValue(a); - v = a; - } - if (v instanceof List) { - @SuppressWarnings("unchecked") List a = (List) v; - - if (extra.equals("--") || - !extra.startsWith("-") || - (extra.startsWith("-") && a.contains("--"))) { - a.add(extra); - return Accept.CONTINUE; - } else if (a.isEmpty()) { - return "No values provided"; - } - } - } - return Accept.REJECT_AND_STOP; - } - }; - - /** - * Returns true if this mode requires an extra parameter. - */ - public abstract boolean needsExtra(); - - /** - * Processes the flag for this argument. - * - * @param arg The argument being processed. - * @param extra The extra parameter. Null if {@link #needsExtra()} returned false. - * @return {@link Accept#CONTINUE} if this argument can use multiple values and - * wishes to receive more. - * Or {@link Accept#ACCEPT_AND_STOP} if this was the last value accepted by the argument. - * Or {@link Accept#REJECT_AND_STOP} if this was value was reject and the argument - * stops accepting new values with no error. - * Or a string in case of error. - * Never returns null. - */ - public abstract Object process(Arg arg, String extra); - } - - /** - * An argument accepted by the command-line, also called "a flag". - * Arguments must have a short version (one letter), a long version name and a description. - * They can have a default value, or it can be null. - * Depending on the {@link Mode}, the default value can be a Boolean, an Integer, a String - * or a String array (in which case the first item is the current by default.) - */ - protected static class Arg { - /** Verb for that argument. Never null. */ - private final String mVerb; - /** Direct Object for that argument. Never null, but can be empty string. */ - private final String mDirectObject; - /** The 1-letter short name of the argument, e.g. -v. */ - private final String mShortName; - /** The long name of the argument, e.g. --verbose. */ - private final String mLongName; - /** A description. Never null. */ - private final String mDescription; - /** A default value. Can be null. */ - private final Object mDefaultValue; - /** The argument mode (type + process method). Never null. */ - private final Mode mMode; - /** True if this argument is mandatory for this verb/directobject. */ - private final boolean mMandatory; - /** Current value. Initially set to the default value. */ - private Object mCurrentValue; - /** True if the argument has been used on the command line. */ - private boolean mInCommandLine; - - /** - * Creates a new argument flag description. - * - * @param mode The {@link Mode} for the argument. - * @param mandatory True if this argument is mandatory for this action. - * @param verb The verb name. Never null. Can be {@link CommandLineParser#GLOBAL_FLAG_VERB}. - * @param directObject The action name. Can be {@link CommandLineParser#NO_VERB_OBJECT}. - * @param shortName The one-letter short argument name. Can be empty but not null. - * @param longName The long argument name. Can be empty but not null. - * @param description The description. Cannot be null. - * @param defaultValue The default value (or values), which depends on the selected - * {@link Mode}. Can be null. - */ - public Arg(Mode mode, - boolean mandatory, - @NonNull String verb, - @NonNull String directObject, - @NonNull String shortName, - @NonNull String longName, - @NonNull String description, - @Nullable Object defaultValue) { - mMode = mode; - mMandatory = mandatory; - mVerb = verb; - mDirectObject = directObject; - mShortName = shortName; - mLongName = longName; - mDescription = description; - mDefaultValue = defaultValue; - mInCommandLine = false; - if (defaultValue instanceof String[]) { - mCurrentValue = ((String[])defaultValue)[0]; - } else { - mCurrentValue = mDefaultValue; - } - } - - /** Return true if this argument is mandatory for this verb/directobject. */ - public boolean isMandatory() { - return mMandatory; - } - - /** Returns the 1-letter short name of the argument, e.g. -v. */ - public String getShortArg() { - return mShortName; - } - - /** Returns the long name of the argument, e.g. --verbose. */ - public String getLongArg() { - return mLongName; - } - - /** Returns the description. Never null. */ - public String getDescription() { - return mDescription; - } - - /** Returns the verb for that argument. Never null. */ - public String getVerb() { - return mVerb; - } - - /** Returns the direct Object for that argument. Never null, but can be empty string. */ - public String getDirectObject() { - return mDirectObject; - } - - /** Returns the default value. Can be null. */ - public Object getDefaultValue() { - return mDefaultValue; - } - - /** Returns the current value. Initially set to the default value. Can be null. */ - public Object getCurrentValue() { - return mCurrentValue; - } - - /** Sets the current value. Can be null. */ - public void setCurrentValue(Object currentValue) { - mCurrentValue = currentValue; - } - - /** Returns the argument mode (type + process method). Never null. */ - public Mode getMode() { - return mMode; - } - - /** Returns true if the argument has been used on the command line. */ - public boolean isInCommandLine() { - return mInCommandLine; - } - - /** Sets if the argument has been used on the command line. */ - public void setInCommandLine(boolean inCommandLine) { - mInCommandLine = inCommandLine; - } - } - - /** - * Internal helper to define a new argument for a give action. - * - * @param mode The {@link Mode} for the argument. - * @param mandatory The argument is required (never if {@link Mode#BOOLEAN}) - * @param verb The verb name. Never null. Can be {@link CommandLineParser#GLOBAL_FLAG_VERB}. - * @param directObject The action name. Can be {@link CommandLineParser#NO_VERB_OBJECT}. - * @param shortName The one-letter short argument name. Can be empty but not null. - * @param longName The long argument name. Can be empty but not null. - * @param description The description. Cannot be null. - * @param defaultValue The default value (or values), which depends on the selected - * {@link Mode}. - */ - protected void define(Mode mode, - boolean mandatory, - @NonNull String verb, - @NonNull String directObject, - @NonNull String shortName, - @NonNull String longName, - @NonNull String description, - @Nullable Object defaultValue) { - assert verb != null; - assert(!(mandatory && mode == Mode.BOOLEAN)); // a boolean mode cannot be mandatory - - // We should always have at least a short or long name, ideally both but never none. - assert shortName != null; - assert longName != null; - assert !shortName.isEmpty() || !longName.isEmpty(); - - if (directObject == null) { - directObject = NO_VERB_OBJECT; - } - - String key = verb + '/' + directObject + '/' + longName; - mArguments.put(key, new Arg(mode, mandatory, - verb, directObject, shortName, longName, description, defaultValue)); - } - - /** - * Exits in case of error. - * This is protected so that it can be overridden in unit tests. - */ - protected void exit() { - System.exit(1); - } - - /** - * Prints a line to stdout. - * This is protected so that it can be overridden in unit tests. - * - * @param format The string to be formatted. Cannot be null. - * @param args Format arguments. - */ - protected void stdout(String format, Object...args) { - String output = String.format(format, args); - output = LineUtil.reflowLine(output); - mLog.info("%s\n", output); //$NON-NLS-1$ - } - - /** - * Prints a line to stderr. - * This is protected so that it can be overridden in unit tests. - * - * @param format The string to be formatted. Cannot be null. - * @param args Format arguments. - */ - protected void stderr(String format, Object...args) { - mLog.error(null, format, args); - } - - /** - * Returns the logger object. - * @return the logger object. - */ - protected ILogger getLog() { - return mLog; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/util/FormatUtils.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/util/FormatUtils.java deleted file mode 100644 index 0ff5e693..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/util/FormatUtils.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2012 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.sdklib.util; - -import com.android.annotations.NonNull; - -/** - * Helper methods to do some format conversions. - */ -public abstract class FormatUtils { - - /** - * Converts a byte size to a human readable string, - * for example "3 MiB", "1020 Bytes" or "1.2 GiB". - * - * @param size The byte size to convert. - * @return A new non-null string, with the size expressed in either Bytes - * or KiB or MiB or GiB. - */ - @NonNull - public static String byteSizeToString(long size) { - String sizeStr; - - if (size < 1024) { - sizeStr = String.format("%d Bytes", size); - } else if (size < 1024 * 1024) { - sizeStr = String.format("%d KiB", Math.round(size / 1024.0)); - } else if (size < 1024 * 1024 * 1024) { - sizeStr = String.format("%.1f MiB", - Math.round(10.0 * size / (1024 * 1024.0))/ 10.0); - } else { - sizeStr = String.format("%.1f GiB", - Math.round(10.0 * size / (1024 * 1024 * 1024.0))/ 10.0); - } - - return sizeStr; - } - -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/util/LineUtil.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/util/LineUtil.java deleted file mode 100644 index c42bd0dd..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/util/LineUtil.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright (C) 2011 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.sdklib.util; - - -public abstract class LineUtil { - - /** - * Reformats a line so that it fits in 78 characters max. - *

- * When wrapping the second line and following, prefix the string with a number of - * spaces. This will use the first colon (:) to determine the prefix size - * or use 4 as a minimum if there are no colons in the string. - * - * @param line The line to reflow. Must be non-null. - * @return A new line to print as-is, that contains \n as needed. - */ - public static String reflowLine(String line) { - final int maxLen = 78; - - // Most of time the line will fit in the given length and this will be a no-op - int n = line.length(); - int cr = line.indexOf('\n'); - if (n <= maxLen && (cr == -1 || cr == n - 1)) { - return line; - } - - int prefixSize = line.indexOf(':') + 1; - // If there' some spacing after the colon, use the same when wrapping - if (prefixSize > 0 && prefixSize < maxLen) { - while(prefixSize < n && line.charAt(prefixSize) == ' ') { - prefixSize++; - } - } else { - prefixSize = 4; - } - String prefix = String.format( - "%-" + Integer.toString(prefixSize) + "s", //$NON-NLS-1$ //$NON-NLS-2$ - " "); //$NON-NLS-1$ - - StringBuilder output = new StringBuilder(n + prefixSize); - - while (n > 0) { - cr = line.indexOf('\n'); - if (n <= maxLen && (cr == -1 || cr == n - 1)) { - output.append(line); - break; - } - - // Line is longer than the max length, find the first character before and after - // the whitespace where we want to break the line. - int posNext = maxLen; - if (cr != -1 && cr != n - 1 && cr <= posNext) { - posNext = cr + 1; - while (posNext < n && line.charAt(posNext) == '\n') { - posNext++; - } - } - while (posNext < n && line.charAt(posNext) == ' ') { - posNext++; - } - while (posNext > 0) { - char c = line.charAt(posNext - 1); - if (c != ' ' && c != '\n') { - posNext--; - } else { - break; - } - } - - if (posNext == 0 || (posNext >= n && maxLen < n)) { - // We found no whitespace separator. This should generally not occur. - posNext = maxLen; - } - int posPrev = posNext; - while (posPrev > 0) { - char c = line.charAt(posPrev - 1); - if (c == ' ' || c == '\n') { - posPrev--; - } else { - break; - } - } - - output.append(line.substring(0, posPrev)).append('\n'); - line = prefix + line.substring(posNext); - n = line.length(); - } - - return output.toString(); - } - - /** - * Formats the string using {@link String#format(String, Object...)} - * and then returns the result of {@link #reflowLine(String)}. - * - * @param format The string format. - * @param params The parameters for the string format. - * @return The result of {@link #reflowLine(String)} on the formatted string. - */ - public static String reformatLine(String format, Object...params) { - return reflowLine(String.format(format, params)); - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/AboutDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/AboutDialog.java index 9220305a..f32d6437 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/AboutDialog.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/AboutDialog.java @@ -17,31 +17,33 @@ package com.android.sdkuilib.internal.repository; -import com.android.SdkConstants; -import com.android.sdklib.io.LegacyFileOp; -import com.android.sdklib.repository.PkgProps; -import com.android.sdklib.repository.SdkAddonConstants; -import com.android.sdklib.repository.SdkRepoConstants; -import com.android.sdkuilib.internal.repository.icons.ImageFactory; -import com.android.sdkuilib.ui.GridDataBuilder; -import com.android.sdkuilib.ui.GridLayoutBuilder; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.andmore.sdktool.SdkContext; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Shell; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.util.Properties; +import com.android.SdkConstants; +import com.android.repository.api.LocalPackage; +import com.android.repository.io.FileOp; +import com.android.repository.io.FileOpUtils; +import com.android.sdklib.repository.PkgProps; +import com.android.sdkuilib.ui.GridDataBuilder; +import com.android.sdkuilib.ui.GridLayoutBuilder; public class AboutDialog extends UpdaterBaseDialog { - public AboutDialog(Shell parentShell, SwtUpdaterData swtUpdaterData) { - super(parentShell, swtUpdaterData, "About" /*title*/); - assert swtUpdaterData != null; + private static final String COPYRIGHT = "Copyright (C) 2009-2017"; + + public AboutDialog(Shell parentShell, SdkContext sdkContext) { + super(parentShell, sdkContext, "About" /*title*/); } @Override @@ -54,23 +56,21 @@ protected void createContents() { GridLayoutBuilder.create(shell).columns(3); Label logo = new Label(shell, SWT.NONE); - ImageFactory imgf = getSwtUpdaterData() == null ? null - : getSwtUpdaterData().getImageFactory(); + ImageFactory imgf = mSdkContext.getSdkHelper().getImageFactory(); Image image = imgf == null ? null : imgf.getImageByName("sdkman_logo_128.png"); if (image != null) logo.setImage(image); Label label = new Label(shell, SWT.NONE); - GridDataBuilder.create(label).hFill().hGrab().hSpan(2);; + GridDataBuilder.create(label).hFill().hGrab().hSpan(2); + String version = getVersion(); + if (!version.isEmpty()) + version = String.format("Revision %1$s", version); label.setText(String.format( "Android SDK Manager.\n" + - "Revision %1$s\n" + - "Add-on XML Schema #%2$d\n" + - "Repository XML Schema #%3$d\n" + - // TODO: update with new year date (search this to find other occurrences to update) - "Copyright (C) 2009-2012 The Android Open Source Project.", - getRevision(), - SdkAddonConstants.NS_LATEST_VERSION, - SdkRepoConstants.NS_LATEST_VERSION)); + "%1$s\n" + + "%2$s The Android Open Source Project.", + version, + COPYRIGHT)); Label filler = new Label(shell, SWT.NONE); GridDataBuilder.create(filler).fill().grab().hSpan(2); @@ -90,32 +90,30 @@ protected void checkSubclass() { // End of hiding from SWT Designer //$hide<<$ - private String getRevision() { + private String getVersion() { + LocalPackage platformPackage = mSdkContext.getHandler().getLatestLocalPackageForPrefix(SdkConstants.FD_TOOLS, false, mSdkContext.getProgressIndicator()); + if (platformPackage != null) + return platformPackage.getVersion().toShortString(); + return getToolsVersion(); + } + + private String getToolsVersion() { Properties p = new Properties(); - try{ - File sourceProp = LegacyFileOp.append(getSwtUpdaterData().getOsSdkRoot(), - SdkConstants.FD_TOOLS, - SdkConstants.FN_SOURCE_PROP); - FileInputStream fis = null; - try { - fis = new FileInputStream(sourceProp); - p.load(fis); - } finally { - if (fis != null) { - try { - fis.close(); - } catch (IOException ignore) { - } - } - } - - String revision = p.getProperty(PkgProps.PKG_REVISION); - if (revision != null) { - return revision; + File suffix = new File(SdkConstants.FD_TOOLS, SdkConstants.FN_SOURCE_PROP); + FileOp fileOp = FileOpUtils.create(); + InputStream fis = null; + try { + fis = fileOp.newFileInputStream(new File(mSdkContext.getLocation().toString(), suffix.toString())); + p.load(fis); + } catch (IOException ignore) { + } finally { + if (fis != null) + try { + fis.close(); + } catch (IOException ignore) { } - } catch (IOException e) { } - - return "?"; + return p.getProperty(PkgProps.PKG_REVISION, ""); } + } diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ISdkUpdaterWindow.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ISdkUpdaterWindow.java index 88a9ac13..2e442f8b 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ISdkUpdaterWindow.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ISdkUpdaterWindow.java @@ -16,7 +16,7 @@ package com.android.sdkuilib.internal.repository; -import com.android.sdklib.repository.ISdkChangeListener; +import com.android.sdkuilib.repository.ISdkChangeListener; /** * Interface for the actual implementation of the Update Window. diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ISwtUpdaterData.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ISwtUpdaterData.java deleted file mode 100644 index 916423e0..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ISwtUpdaterData.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2013 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.sdkuilib.internal.repository; - -import com.android.sdklib.internal.repository.updater.IUpdaterData; -import com.android.sdklib.internal.repository.updater.UpdaterData; -import com.android.sdkuilib.internal.repository.icons.ImageFactory; - -import org.eclipse.swt.widgets.Shell; - - -/** - * Interface used to retrieve some parameters from an {@link UpdaterData} instance. - * Useful mostly for unit tests purposes. - */ -interface ISwtUpdaterData extends IUpdaterData { - - public abstract ImageFactory getImageFactory(); - - public abstract Shell getWindowShell(); - -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/ITask.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ITask.java similarity index 80% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/ITask.java rename to andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ITask.java index 58e76f9c..b97bd1b3 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/ITask.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ITask.java @@ -1,31 +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. - */ - -package com.android.sdklib.internal.repository; - - -/** - * A task that executes and can update a monitor to display its status. - * The task will generally be run in a separate thread. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public interface ITask { - void run(ITaskMonitor monitor); -} +/* + * 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.sdkuilib.internal.repository; + +/** + * A task that executes and can update a monitor to display its status. + * The task will generally be run in a separate thread. + */ +public interface ITask { + void run(ITaskMonitor monitor); +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/ITaskFactory.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ITaskFactory.java similarity index 92% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/ITaskFactory.java rename to andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ITaskFactory.java index ce7133aa..afe16852 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/ITaskFactory.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ITaskFactory.java @@ -1,64 +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.sdklib.internal.repository; - - -/** - * A factory that can start and run new {@link ITask}s. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public interface ITaskFactory { - - /** - * Starts a new task with a new {@link ITaskMonitor}. - *

- * The task will execute in a thread and runs it own UI loop. - * This means the task can perform UI operations using - * {@code Display#asyncExec(Runnable)}. - *

- * In either case, the method only returns when the task has finished. - * - * @param title The title of the task, displayed in the monitor if any. - * @param task The task to run. - */ - void start(String title, ITask task); - - /** - * Starts a new task contributing to an already existing {@link ITaskMonitor}. - *

- * To use this properly, you should use {@link ITaskMonitor#createSubMonitor(int)} - * and give the sub-monitor to the new task with the number of work units you want - * it to fill. The {@link #start} method will make sure to fill the progress - * when the task is completed, in case the actual task did not. - *

- * When a task is started from within a monitor, it reuses the thread - * from the parent. Otherwise it starts a new thread and runs it own - * UI loop. This means the task can perform UI operations using - * {@code Display#asyncExec(Runnable)}. - *

- * In either case, the method only returns when the task has finished. - * - * @param title The title of the task, displayed in the monitor if any. - * @param parentMonitor The parent monitor. Can be null. - * @param task The task to run and have it display on the monitor. - */ - void start(String title, ITaskMonitor parentMonitor, ITask task); -} +/* + * 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.sdkuilib.internal.repository; + + +/** + * A factory that can start and run new {@link ITask}s. + */ +public interface ITaskFactory { + + /** + * Starts a new task with a new {@link ITaskMonitor}. + *

+ * The task will execute in a thread and runs it own UI loop. + * This means the task can perform UI operations using + * {@code Display#asyncExec(Runnable)}. + *

+ * In either case, the method only returns when the task has finished. + * + * @param title The title of the task, displayed in the monitor if any. + * @param task The task to run. + */ + void start(String title, ITask task); + + /** + * Starts a new task contributing to an already existing {@link ITaskMonitor}. + *

+ * To use this properly, you should use {@link ITaskMonitor#createSubMonitor(int)} + * and give the sub-monitor to the new task with the number of work units you want + * it to fill. The {@link #start} method will make sure to fill the progress + * when the task is completed, in case the actual task did not. + *

+ * When a task is started from within a monitor, it reuses the thread + * from the parent. Otherwise it starts a new thread and runs it own + * UI loop. This means the task can perform UI operations using + * {@code Display#asyncExec(Runnable)}. + *

+ * In either case, the method only returns when the task has finished. + * + * @param title The title of the task, displayed in the monitor if any. + * @param parentMonitor The parent monitor. Can be null. + * @param task The task to run and have it display on the monitor. + */ + void start(String title, ITaskMonitor parentMonitor, ITask task); +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/ITaskMonitor.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ITaskMonitor.java similarity index 90% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/ITaskMonitor.java rename to andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ITaskMonitor.java index 89aac97f..966fc58c 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/ITaskMonitor.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ITaskMonitor.java @@ -1,152 +1,158 @@ -/* - * 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.sdklib.internal.repository; - -import com.android.utils.ILogger; - - -/** - * A monitor interface for a {@link ITask}. - *

- * Depending on the task factory that created the task, there might not be any UI - * or it might not implement all the methods, in which case calling them would be - * a no-op but is guaranteed not to crash. - *

- * If the task runs in a non-UI worker thread, the task factory implementation - * will take care of the update the UI in the correct thread. The task itself - * must not have to deal with it. - *

- * A monitor typically has 3 levels of text displayed:
- * - A title may be present on a task dialog, typically when a task - * dialog is created. This is not covered by this monitor interface.
- * - A description displays prominent information on what the task - * is currently doing. This is expected to vary over time, typically changing - * with each sub-monitor, and typically only the last description is visible. - * For example an updater would typically have descriptions such as "downloading", - * "installing" and finally "done". This is set using {@link #setDescription}.
- * - A verbose optional log that can provide more information than the summary - * description and is typically displayed in some kind of scrollable multi-line - * text field so that the user can keep track of what happened. 3 levels are - * provided: error, normal and verbose. An UI may hide the log till an error is - * logged and/or might hide the verbose text unless a flag is checked by the user. - * This is set using {@link #log}, {@link #logError} and {@link #logVerbose}. - *

- * A monitor is also an {@link ILogger} implementation. - * - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public interface ITaskMonitor extends ILogger { - - /** - * Sets the description in the current task dialog. - * This method can be invoked from a non-UI thread. - */ - void setDescription(String format, Object...args); - - /** - * Logs a "normal" information line. - * This method can be invoked from a non-UI thread. - */ - void log(String format, Object...args); - - /** - * Logs an "error" information line. - * This method can be invoked from a non-UI thread. - */ - void logError(String format, Object...args); - - /** - * Logs a "verbose" information line, that is extra details which are typically - * not that useful for the end-user and might be hidden until explicitly shown. - * This method can be invoked from a non-UI thread. - */ - void logVerbose(String format, Object...args); - - /** - * Sets the max value of the progress bar. - * This method can be invoked from a non-UI thread. - * - * This method MUST be invoked once before using {@link #incProgress(int)} or - * {@link #getProgress()} or {@link #createSubMonitor(int)}. Callers are - * discouraged from using more than once -- implementations can either discard - * the next calls or behave incoherently. - */ - void setProgressMax(int max); - - /** - * Returns the max value of the progress bar, as last set by {@link #setProgressMax(int)}. - * Returns 0 if the max has never been set yet. - */ - int getProgressMax(); - - /** - * Increments the current value of the progress bar. - * This method can be invoked from a non-UI thread. - * - * Callers MUST use setProgressMax before using this method. - */ - void incProgress(int delta); - - /** - * Returns the current value of the progress bar, - * between 0 and up to {@link #setProgressMax(int)} - 1. - * - * Callers MUST use setProgressMax before using this method. - */ - int getProgress(); - - /** - * Returns true if the user requested to cancel the operation. - * It is up to the task thread to pool this and exit as soon - * as possible. - */ - boolean isCancelRequested(); - - /** - * Creates a sub-monitor that will use up to tickCount on the progress bar. - * tickCount must be 1 or more. - */ - ITaskMonitor createSubMonitor(int tickCount); - - /** - * Display a yes/no question dialog box. - * - * Implementations MUST allow this to be called from any thread, e.g. by - * making sure the dialog is opened synchronously in the ui thread. - * - * @param title The title of the dialog box - * @param message The error message - * @return true if YES was clicked. - */ - boolean displayPrompt(final String title, final String message); - - /** - * Launch an interface which asks for user credentials. Implementations - * MUST allow this to be called from any thread, e.g. by making sure the - * dialog is opened synchronously in the UI thread. - * - * @param title The title of the dialog box. - * @param message The message to be displayed as an instruction. - * @return Returns the user provided credentials. Some fields may be blank if the user - * did not provide any input. - If operation is canceled by user the return value must be null. - */ - UserCredentials displayLoginCredentialsPrompt(String title, String message); -} +/* + * 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.sdkuilib.internal.repository; + +import com.android.utils.ILogger; + + +/** + * A monitor interface for a {@link ITask}. + *

+ * Depending on the task factory that created the task, there might not be any UI + * or it might not implement all the methods, in which case calling them would be + * a no-op but is guaranteed not to crash. + *

+ * If the task runs in a non-UI worker thread, the task factory implementation + * will take care of the update the UI in the correct thread. The task itself + * must not have to deal with it. + *

+ * A monitor typically has 3 levels of text displayed:
+ * - A title may be present on a task dialog, typically when a task + * dialog is created. This is not covered by this monitor interface.
+ * - A description displays prominent information on what the task + * is currently doing. This is expected to vary over time, typically changing + * with each sub-monitor, and typically only the last description is visible. + * For example an updater would typically have descriptions such as "downloading", + * "installing" and finally "done". This is set using {@link #setDescription}.
+ * - A verbose optional log that can provide more information than the summary + * description and is typically displayed in some kind of scrollable multi-line + * text field so that the user can keep track of what happened. 3 levels are + * provided: error, normal and verbose. An UI may hide the log till an error is + * logged and/or might hide the verbose text unless a flag is checked by the user. + * This is set using {@link #log}, {@link #logError} and {@link #logVerbose}. + *

+ * A monitor is also an {@link ILogger} implementation. + */ +public interface ITaskMonitor extends ILogger { + + /** + * Sets the description in the current task dialog. + * This method can be invoked from a non-UI thread. + */ + void setDescription(String format, Object...args); + + /** + * Logs a "normal" information line. + * This method can be invoked from a non-UI thread. + */ + void log(String format, Object...args); + + /** + * Logs an "error" information line. + * This method can be invoked from a non-UI thread. + */ + void logError(String format, Object...args); + + /** + * Logs a "verbose" information line, that is extra details which are typically + * not that useful for the end-user and might be hidden until explicitly shown. + * This method can be invoked from a non-UI thread. + */ + void logVerbose(String format, Object...args); + + /** + * Sets the max value of the progress bar. + * This method can be invoked from a non-UI thread. + * + * This method MUST be invoked once before using {@link #incProgress(int)} or + * {@link #getProgress()} or {@link #createSubMonitor(int)}. Callers are + * discouraged from using more than once -- implementations can either discard + * the next calls or behave incoherently. + */ + void setProgressMax(int max); + + /** + * Returns the max value of the progress bar, as last set by {@link #setProgressMax(int)}. + * Returns 0 if the max has never been set yet. + */ + int getProgressMax(); + + /** + * Increments the current value of the progress bar. + * This method can be invoked from a non-UI thread. + * + * Callers MUST use setProgressMax before using this method. + */ + void incProgress(int delta); + + /** + * Returns the current value of the progress bar, + * between 0 and up to {@link #setProgressMax(int)} - 1. + * + * Callers MUST use setProgressMax before using this method. + */ + int getProgress(); + + /** + * Returns true if the user requested to cancel the operation. + * It is up to the task thread to pool this and exit as soon + * as possible. + */ + boolean isCancelRequested(); + + /** + * Creates a sub-monitor that will use up to tickCount on the progress bar. + * tickCount must be 1 or more. + */ + ITaskMonitor createSubMonitor(int tickCount); + + /** + * Display a yes/no question dialog box. + * + * Implementations MUST allow this to be called from any thread, e.g. by + * making sure the dialog is opened synchronously in the ui thread. + * + * @param title The title of the dialog box + * @param message The error message + * @return true if YES was clicked. + */ + boolean displayPrompt(final String title, final String message); + + /** + * Display info dialog box. + * + * Implementations MUST allow this to be called from any thread, e.g. by + * making sure the dialog is opened synchronously in the ui thread. + * + * @param title The title of the dialog box + * @param message The error message + */ + void displayInfo(final String title, final String message); + + /** + * Launch an interface which asks for user credentials. Implementations + * MUST allow this to be called from any thread, e.g. by making sure the + * dialog is opened synchronously in the UI thread. + * + * @param title The title of the dialog box. + * @param message The message to be displayed as an instruction. + * @return Returns the user provided credentials. Some fields may be blank if the user + * did not provide any input. + If operation is canceled by user the return value must be null. + */ + UserCredentials displayLoginCredentialsPrompt(String title, String message); +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/LoadPackagesRequest.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/LoadPackagesRequest.java new file mode 100644 index 00000000..9813cbb5 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/LoadPackagesRequest.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2017 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.sdkuilib.internal.repository; + +import java.util.List; + +import com.android.repository.api.ProgressRunner; +import com.android.repository.api.RepoManager.RepoLoadedCallback; + +/** + * Packet containing parameters to call RepoManager load() + * + * + *

public abstract void load(long cacheExpirationMs,
+            @Nullable List onLocalComplete,
+            @Nullable List onSuccess,
+            @Nullable List onError,
+            @NonNull ProgressRunner runner,
+            @Nullable Downloader downloader,
+            @Nullable SettingsController settings,
+            boolean sync);
+        
+ * @author Andrew Bowley + * + * 12-11-2017 + */ +public class LoadPackagesRequest { + + private ProgressRunner runner; + private List onLocalComplete; + private List onSuccess; + private List onError; + + /** + * + */ + public LoadPackagesRequest(ProgressRunner runner) { + this.runner = runner; + } + + public List getOnLocalComplete() { + return onLocalComplete; + } + + public void setOnLocalComplete(List onLocalComplete) { + this.onLocalComplete = onLocalComplete; + } + + public List getOnSuccess() { + return onSuccess; + } + + public void setOnSuccess(List onSuccess) { + this.onSuccess = onSuccess; + } + + public List getOnError() { + return onError; + } + + public void setOnError(List onError) { + this.onError = onError; + } + + public ProgressRunner getRunner() { + return runner; + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/MenuBarWrapper.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/MenuBarWrapper.java index feea181f..d7edfd13 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/MenuBarWrapper.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/MenuBarWrapper.java @@ -19,7 +19,6 @@ import com.android.menubar.IMenuBarCallback; import com.android.menubar.MenuBarEnhancer; -import com.android.sdkuilib.internal.repository.ui.SdkUpdaterWindowImpl2; import org.eclipse.swt.widgets.Menu; diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/PackageInfo.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/PackageInfo.java new file mode 100644 index 00000000..a06effc3 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/PackageInfo.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2009-2017 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.sdkuilib.internal.repository; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.repository.api.LocalPackage; +import com.android.repository.api.RemotePackage; + +/** + * Represents package that we want to install. + *

+ * A new package is a remote package that needs to be downloaded and then + * installed. It can replace an existing local one. It can also depend on another + * (new or local) package, which means the dependent package needs to be successfully + * installed first. Finally this package can also be a dependency for another one. + *

+ * The accepted and rejected flags are used by {@code SdkUpdaterChooserDialog} to follow + * user choices. The installer should never install something that is not accepted. + *

+ * Note: There is currently no logic to support more than one level of + * dependency, either here or in the {@code SdkUpdaterChooserDialog}, since we currently + * have no need for it. + */ +public class PackageInfo implements Comparable { + + private final RemotePackage mNewPackage; + private LocalPackage mReplaced; + private boolean mAccepted; + private boolean mRejected; + + /** + * Creates a new replacement where the {@code newPackage} will replace the + * currently installed {@code replaced} package. + * When {@code newPackage} is not intended to replace anything (e.g. because + * the user is installing a new package not present on her system yet), then + * {@code replace} shall be null. + * + * @param newPackage A "new package" to be installed. This is always an package + * that comes from a remote site. + */ + public PackageInfo(@NonNull RemotePackage newPackage) { + this(newPackage, null); + } + + public PackageInfo(@NonNull RemotePackage newPackage, @Nullable LocalPackage replaced) { + mNewPackage = newPackage; + mReplaced = replaced; + } + + /** + * Returns the "new archive" to be installed. + * This may be null for missing archives. + */ + public RemotePackage getNewPackage() { + return mNewPackage; + } + + /** + * Returns an optional local archive that the new one will replace. + * Can be null if this archive does not replace anything. + */ + public LocalPackage getReplaced() { + return mReplaced; + } + + /** + * Sets whether this package was accepted (either manually by the user or + * automatically if it doesn't have a license) for installation. + */ + public void setAccepted(boolean accepted) { + mAccepted = accepted; + } + + /** + * Returns whether this package was accepted (either manually by the user or + * automatically if it doesn't have a license) for installation. + */ + public boolean isAccepted() { + return mAccepted; + } + + /** + * Sets whether this package was rejected manually by the user. + * An package can neither accepted nor rejected. + */ + public void setRejected(boolean rejected) { + mRejected = rejected; + } + + /** + * Returns whether this package was rejected manually by the user. + * An package can neither accepted nor rejected. + */ + public boolean isRejected() { + return mRejected; + } + + /** + * PackageInfos are compared using ther "new package" ordering. + * + * @see Package#compareTo(Package) + */ + @Override + public int compareTo(PackageInfo rhs) { + if (mNewPackage != null && rhs != null) { + return mNewPackage.compareTo(rhs.mNewPackage); + } + return 0; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/PackageManager.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/PackageManager.java new file mode 100644 index 00000000..8b4ad535 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/PackageManager.java @@ -0,0 +1,282 @@ +package com.android.sdkuilib.internal.repository; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.eclipse.andmore.sdktool.SdkContext; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.annotations.VisibleForTesting; +import com.android.annotations.VisibleForTesting.Visibility; +import com.android.repository.api.Downloader; +import com.android.repository.api.Installer; +import com.android.repository.api.LocalPackage; +import com.android.repository.api.PackageOperation; +import com.android.repository.api.ProgressIndicator; +import com.android.repository.api.ProgressRunner; +import com.android.repository.api.RemotePackage; +import com.android.repository.api.RepoManager; +import com.android.repository.api.RepoManager.RepoLoadedCallback; +import com.android.repository.api.UpdatablePackage; +import com.android.repository.impl.meta.RepositoryPackages; +import com.android.repository.io.FileOpUtils; +import com.android.repository.io.impl.FileSystemFileOp; +import com.android.sdklib.repository.installer.SdkInstallerUtil; +import com.android.sdklib.repository.legacy.LegacyDownloader; +import com.google.common.collect.Maps; + +public class PackageManager { + + public interface LocalPackageHandler + { + void onPackageLoaded(LocalPackage localPackage); + } + + public interface RemotePackageHandler + { + void onPackageLoaded(RemotePackage remotePackage); + } + + public interface UpdatablePackageHandler + { + void onPackageLoaded(UpdatablePackage updatablePackage); + } + + /** + * Map from {@code path} (the unique ID of a package) to {@link UpdatablePackage}, including all + * packages installed or available. + */ + private final Map consolidatedPkgs = Maps.newTreeMap(); + private final SdkContext sdkContext; + private final Downloader downloader; + private RepositoryPackages packages; + + public PackageManager(SdkContext sdkContext) + { + this.sdkContext = sdkContext; + downloader = downloaderInstance(); + } + + public Downloader getDownloader() + { + return downloader; + } + + public UpdatablePackage getPackageById(String id) + { + return consolidatedPkgs.get(id); + } + + public boolean loadLocalPackages(LocalPackageHandler handler) + { + sdkContext.getRepoManager().loadSynchronously(0, sdkContext.getProgressIndicator(), downloader, sdkContext.getSettings()); + if (sdkContext.hasError()) + return false; + + for (LocalPackage local : getRepositoryPackages().getLocalPackages().values()) + handler.onPackageLoaded(local); + return true; + } + + public boolean loadRemotePackages(RemotePackageHandler handler) + { + sdkContext.getRepoManager().loadSynchronously(0, sdkContext.getProgressIndicator(), downloader, sdkContext.getSettings()); + if (sdkContext.hasError()) + return false; + + for (RemotePackage remote : getRepositoryPackages().getRemotePackages().values()) + handler.onPackageLoaded(remote); + return true; + } + + public boolean loadUpdatablePackages(UpdatablePackageHandler handler) + { + if (sdkContext.hasError()) + return false; + for (UpdatablePackage updatable : getRepositoryPackages().getUpdatedPkgs()) + handler.onPackageLoaded(updatable); + return true; + } + + public boolean loadConsolidatedPackages(UpdatablePackageHandler handler) + { + if (sdkContext.hasError()) + return false; + consolidatedPkgs.clear(); + consolidatedPkgs.putAll(getRepositoryPackages().getConsolidatedPkgs()); + for (UpdatablePackage updatable : consolidatedPkgs.values()) + handler.onPackageLoaded(updatable); + return true; + } + + public RepositoryPackages getRepositoryPackages() + { + if (packages == null) + { + RepoManager repoManager = sdkContext.getRepoManager(); + repoManager.loadSynchronously(0, sdkContext.getProgressIndicator(), downloader, sdkContext.getSettings()); + packages = repoManager.getPackages(); + } + return packages; + } +/* + public abstract void load(long cacheExpirationMs, + @Nullable List onLocalComplete, + @Nullable List onSuccess, + @Nullable List onError, + @NonNull ProgressRunner runner, + @Nullable Downloader downloader, + @Nullable SettingsController settings, + boolean sync); + */ + + public void requestRepositoryPackages(LoadPackagesRequest loadPackagesRequest) + { + RepoManager repoManager = sdkContext.getRepoManager(); + repoManager.load(0, + loadPackagesRequest.getOnLocalComplete(), + loadPackagesRequest.getOnSuccess(), + loadPackagesRequest.getOnError(), + loadPackagesRequest.getRunner(), + downloader, + sdkContext.getSettings(), + false); + } + + public RepositoryPackages getRepositoryPackages( + @Nullable List onLocalComplete, + @Nullable List onSuccess, + @Nullable List onError, + @NonNull ProgressRunner runner) + { + if (packages == null) + { + RepoManager repoManager = sdkContext.getRepoManager(); + repoManager.load(0, onLocalComplete, onSuccess, onError, runner, downloader, sdkContext.getSettings(), true); + packages = repoManager.getPackages(); + } + return packages; + } + + public void setPackages(RepositoryPackages packages) { + this.packages = packages; + } + + /** + * Install the list of given {@link Package}s. This is invoked by the user selecting some + * packages in the remote page and then clicking "install selected". + * + * @param packages The packages to install. + * @param taskFactory Task factory to show progress during installation + * @param flags Optional flags for the installer, such as {@link #NO_TOOLS_MSG}. + * @return A list of packages that have been installed. Can be empty but not null. + */ + @VisibleForTesting(visibility=Visibility.PRIVATE) + public int installPackages(final List packages, List acceptedRemotes, ITaskFactory taskFactory, final int flags) { + // List to accumulate all the packages installed. + int[] numInstalled = new int[]{0}; + taskFactory.start("Installing Packages", new ITask() { + @Override + public void run(ITaskMonitor monitor) { + List rejectedRemotes = new ArrayList<>(); + Iterator iterator = packages.iterator(); + while (iterator.hasNext()) { + RemotePackage remote = iterator.next(); + if (!acceptedRemotes.contains(remote)) + rejectedRemotes.add(remote); + } + List remotes = new ArrayList<>(); + remotes.addAll(packages); + if (!rejectedRemotes.isEmpty()) { + String title = "Package licences not accepted"; + StringBuilder builder = new StringBuilder(); + builder.append("The following packages can not be installed since their " + + "licenses or those of the packages they depend on were not accepted:"); + Iterator iterator2 = rejectedRemotes.iterator(); + while(iterator2.hasNext()) + builder.append('\n').append(iterator2.next().getPath()); + if (!acceptedRemotes.isEmpty()) { + builder.append("\n\nContinue installing the remaining packages?"); + if (!monitor.displayPrompt(title, + builder.toString())) + return; + else { + monitor.displayInfo(title, builder.toString()); + return; + } + } + remotes = acceptedRemotes; + } + final int progressPerPackage = 2 * 100; + monitor.setProgressMax(1 + remotes.size() * progressPerPackage); + monitor.setDescription("Preparing to install packages"); + for (RemotePackage remotePackage : remotes) { + int nextProgress = monitor.getProgress() + progressPerPackage; + Installer installer = SdkInstallerUtil.findBestInstallerFactory(remotePackage, sdkContext.getHandler()) + .createInstaller(remotePackage, sdkContext.getRepoManager(), downloader, sdkContext.getFileOp()); + if (applyPackageOperation(installer)) { + ++numInstalled[0]; + } else { + // there was an error, abort. + monitor.error(null, "Install of package failed due to an error"); + monitor.setProgressMax(0); + break; + } + if (monitor.isCancelRequested()) { + break; + } + monitor.incProgress(nextProgress - monitor.getProgress()); + } +/* TODO - Complete post package install operations + if (installedAddon) { + // Update the USB vendor ids for adb + try { + mSdkManager.updateAdb(); + monitor.log("Updated ADB to support the USB devices declared in the SDK add-ons."); + } catch (Exception e) { + mSdkLog.error(e, "Update ADB failed"); + monitor.logError("failed to update adb to support the USB devices declared in the SDK add-ons."); + } + } + + if (preInstallHookInvoked) { + broadcastPostInstallHook(); + } + + if (installedAddon || installedPlatformTools) { + // We need to restart ADB. Actually since we don't know if it's even + // running, maybe we should just kill it and not start it. + // Note: it turns out even under Windows we don't need to kill adb + // before updating the tools folder, as adb.exe is (surprisingly) not + // locked. + + askForAdbRestart(monitor); + } + + if (installedTools) { + notifyToolsNeedsToBeRestarted(flags); + } +*/ + } + }); + + return numInstalled[0]; + } + + private Downloader downloaderInstance() + { + FileSystemFileOp fop = (FileSystemFileOp)FileOpUtils.create(); + return new LegacyDownloader(fop, sdkContext.getSettings()); + } + + private boolean applyPackageOperation( + @NonNull PackageOperation operation) { + ProgressIndicator progressIndicator = sdkContext.getProgressIndicator(); + return operation.prepare(progressIndicator) && operation.complete(progressIndicator); + } + + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/Settings.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/Settings.java new file mode 100644 index 00000000..d080f229 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/Settings.java @@ -0,0 +1,88 @@ +package com.android.sdkuilib.internal.repository; + +import java.io.File; +import java.io.IOException; +import java.util.Properties; + +import com.android.annotations.Nullable; +import com.android.prefs.AndroidLocation; +import com.android.prefs.AndroidLocation.AndroidLocationException; +import com.android.repository.api.Channel; +import com.android.repository.api.SettingsController; +import com.android.repository.io.FileOp; +import com.android.repository.io.FileOpUtils; +import com.android.repository.io.impl.FileSystemFileOp; +import com.android.utils.ILogger; + +public class Settings implements SettingsController { + + private static final String SETTINGS_FILENAME = "androidtool.cfg"; + public static final String FORCE_HTTP_KEY = "sdkman.force.http"; + + private int mChannel = 0; + private final FileOp mFileOp; + private final Properties mProperties; + + public Settings() + { + mProperties = new Properties(); + mFileOp = (FileSystemFileOp)FileOpUtils.create(); + } + + public boolean getEnablePreviews() { + return true; + } + + @Override + public boolean getForceHttp() { + return Boolean.parseBoolean(this.mProperties.getProperty(FORCE_HTTP_KEY)); + } + + @Override + public void setForceHttp(boolean force) { + mProperties.setProperty(FORCE_HTTP_KEY, Boolean.toString(force)); + } + + @Nullable + @Override + public Channel getChannel() { + return Channel.create(mChannel); + } + + public boolean initialize(ILogger logger) { + try { + loadSettings(); + } catch (AndroidLocationException | IOException e) { + logger.error(e, "Error initializing SDK settings"); + return false; + } + return true; + } + + + private void loadSettings() throws AndroidLocationException, IOException + { + String folder = AndroidLocation.getFolder(); + File file = new File(folder, SETTINGS_FILENAME); + + mProperties.clear(); + mProperties.load(mFileOp.newFileInputStream(file)); + + //setShowUpdateOnly(getShowUpdateOnly()); + //setSetting("sdkman.ask.adb.restart", getAskBeforeAdbRestart()); + //setSetting("sdkman.use.dl.cache", getUseDownloadCache()); + //setSetting("sdkman.enable.previews2", getEnablePreviews()); + } + + public Settings copy() { + Settings copy = new Settings(); + // Load is not expected to fail. Copy will contain defaults if it does. + try { + copy.loadSettings(); + } catch (AndroidLocationException e) { + } catch (IOException e) { + } + return copy; + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SettingsDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SettingsDialog.java index 06a9df9f..029fd936 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SettingsDialog.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SettingsDialog.java @@ -16,14 +16,15 @@ package com.android.sdkuilib.internal.repository; -import com.android.sdklib.internal.repository.DownloadCache; -import com.android.sdklib.internal.repository.DownloadCache.Strategy; -import com.android.sdklib.internal.repository.updater.ISettingsPage; -import com.android.sdklib.internal.repository.updater.SettingsController; -import com.android.sdklib.util.FormatUtils; +//import com.android.sdklib.repository.legacy.remote.internal.DownloadCache; +//import com.android.sdklib.repository.legacy.remote.internal.DownloadCache.Strategy; +//import com.android.sdklib.internal.repository.updater.ISettingsPage; +//import com.android.sdklib.internal.repository.updater.SettingsController; +//import com.android.sdklib.util.FormatUtils; import com.android.sdkuilib.ui.GridDataBuilder; import com.android.sdkuilib.ui.GridLayoutBuilder; +import org.eclipse.andmore.sdktool.SdkContext; import org.eclipse.swt.SWT; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; @@ -39,22 +40,23 @@ import java.util.Properties; -public class SettingsDialog extends UpdaterBaseDialog implements ISettingsPage { +public class SettingsDialog extends UpdaterBaseDialog { // data members - private final DownloadCache mDownloadCache = new DownloadCache(Strategy.SERVE_CACHE); - private final SettingsController mSettingsController; - private SettingsChangedCallback mSettingsChangedCallback; +// private final DownloadCache mDownloadCache = new DownloadCache(Strategy.SERVE_CACHE); +// private final SettingsController mSettingsController; +// private SettingsChangedCallback mSettingsChangedCallback; // UI widgets - private Text mTextProxyServer; - private Text mTextProxyPort; - private Text mTextCacheSize; - private Button mCheckUseCache; +// private Text mTextProxyServer; +// private Text mTextProxyPort; + // TODO - Add channel configuration + private Text mTextChannel; +// private Button mCheckUseCache; private Button mCheckForceHttp; - private Button mCheckAskAdbRestart; - private Button mCheckEnablePreviews; +// private Button mCheckAskAdbRestart; +// private Button mCheckEnablePreviews; private SelectionAdapter mApplyOnSelected = new SelectionAdapter() { @Override @@ -70,10 +72,8 @@ public void modifyText(ModifyEvent e) { } }; - public SettingsDialog(Shell parentShell, SwtUpdaterData swtUpdaterData) { - super(parentShell, swtUpdaterData, "Settings" /*title*/); - assert swtUpdaterData != null; - mSettingsController = swtUpdaterData.getSettingsController(); + public SettingsDialog(Shell parentShell, SdkContext sdkContext) { + super(parentShell, sdkContext, "Settings" /*title*/); } @Override @@ -88,7 +88,7 @@ protected void createShell() { protected void createContents() { super.createContents(); Shell shell = getShell(); - + /* Group group = new Group(shell, SWT.NONE); group.setText("Proxy Settings"); GridDataBuilder.create(group).fill().grab().hSpan(2); @@ -100,7 +100,6 @@ protected void createContents() { String tooltip = "The hostname or IP of the HTTP & HTTPS proxy server to use (e.g. proxy.example.com).\n" + "When empty, the default Java proxy setting is used."; label.setToolTipText(tooltip); - mTextProxyServer = new Text(group, SWT.BORDER); GridDataBuilder.create(mTextProxyServer).hFill().hGrab().vCenter(); mTextProxyServer.addModifyListener(mApplyOnModified); @@ -163,10 +162,10 @@ public void widgetSelected(SelectionEvent arg0) { updateDownloadCacheSize(); } }); - +*/ // ---- - group = new Group(shell, SWT.NONE); - group.setText("Others"); + Group group = new Group(shell, SWT.NONE); + group.setText("Options"); GridDataBuilder.create(group).fill().grab().hSpan(2); GridLayoutBuilder.create(group).columns(2); @@ -177,7 +176,7 @@ public void widgetSelected(SelectionEvent arg0) { "If you are not able to connect to the official Android repository using HTTPS,\n" + "enable this setting to force accessing it via HTTP."); mCheckForceHttp.addSelectionListener(mApplyOnSelected); - +/* mCheckAskAdbRestart = new Button(group, SWT.CHECK); GridDataBuilder.create(mCheckAskAdbRestart).hFill().hGrab().vCenter().hSpan(2); mCheckAskAdbRestart.setText("Ask before restarting ADB"); @@ -185,7 +184,6 @@ public void widgetSelected(SelectionEvent arg0) { "When checked, the user will be asked for permission to restart ADB\n" + "after updating an addon-on package or a tool package."); mCheckAskAdbRestart.addSelectionListener(mApplyOnSelected); - mCheckEnablePreviews = new Button(group, SWT.CHECK); GridDataBuilder.create(mCheckEnablePreviews).hFill().hGrab().vCenter().hSpan(2); mCheckEnablePreviews.setText("Enable Preview Tools"); @@ -194,6 +192,7 @@ public void widgetSelected(SelectionEvent arg0) { "These are optional future release candidates that the Android tools team\n" + "publishes from time to time for early feedback."); mCheckEnablePreviews.addSelectionListener(mApplyOnSelected); +*/ Label filler = new Label(shell, SWT.NONE); GridDataBuilder.create(filler).hFill().hGrab(); @@ -204,14 +203,13 @@ public void widgetSelected(SelectionEvent arg0) { @Override protected void postCreate() { super.postCreate(); - // This tells the controller to load the settings into the page UI. - mSettingsController.setSettingsPage(this); + loadSettings(); } @Override protected void close() { // Dissociate this page from the controller - mSettingsController.setSettingsPage(null); + //mSettingsController.setSettingsPage(null); super.close(); } @@ -221,35 +219,32 @@ protected void close() { //$hide>>$ /** Loads settings from the given {@link Properties} container and update the page UI. */ - @Override - public void loadSettings(Properties inSettings) { - mTextProxyServer.setText(inSettings.getProperty(KEY_HTTP_PROXY_HOST, "")); //$NON-NLS-1$ - mTextProxyPort.setText( inSettings.getProperty(KEY_HTTP_PROXY_PORT, "")); //$NON-NLS-1$ - mCheckForceHttp.setSelection( - Boolean.parseBoolean(inSettings.getProperty(KEY_FORCE_HTTP))); - mCheckAskAdbRestart.setSelection( - Boolean.parseBoolean(inSettings.getProperty(KEY_ASK_ADB_RESTART))); - mCheckUseCache.setSelection( - Boolean.parseBoolean(inSettings.getProperty(KEY_USE_DOWNLOAD_CACHE))); - mCheckEnablePreviews.setSelection( - Boolean.parseBoolean(inSettings.getProperty(KEY_ENABLE_PREVIEWS))); + public void loadSettings() { + Settings settings = mSdkContext.getSettings(); + //mTextProxyServer.setText(inSettings.getProperty(KEY_HTTP_PROXY_HOST, "")); //$NON-NLS-1$ + //mTextProxyPort.setText( inSettings.getProperty(KEY_HTTP_PROXY_PORT, "")); //$NON-NLS-1$ + mCheckForceHttp.setSelection(settings.getForceHttp()); + //mCheckAskAdbRestart.setSelection( + // Boolean.parseBoolean(inSettings.getProperty(KEY_ASK_ADB_RESTART))); + //mCheckUseCache.setSelection( + // Boolean.parseBoolean(inSettings.getProperty(KEY_USE_DOWNLOAD_CACHE))); + //mCheckEnablePreviews.setSelection(false); } /** Called by the application to retrieve settings from the UI and store them in * the given {@link Properties} container. */ - @Override - public void retrieveSettings(Properties outSettings) { - outSettings.setProperty(KEY_HTTP_PROXY_HOST, mTextProxyServer.getText()); - outSettings.setProperty(KEY_HTTP_PROXY_PORT, mTextProxyPort.getText()); - outSettings.setProperty(KEY_FORCE_HTTP, - Boolean.toString(mCheckForceHttp.getSelection())); - outSettings.setProperty(KEY_ASK_ADB_RESTART, - Boolean.toString(mCheckAskAdbRestart.getSelection())); - outSettings.setProperty(KEY_USE_DOWNLOAD_CACHE, - Boolean.toString(mCheckUseCache.getSelection())); - outSettings.setProperty(KEY_ENABLE_PREVIEWS, - Boolean.toString(mCheckEnablePreviews.getSelection())); + public void retrieveSettings() { + Settings settings = mSdkContext.getSettings(); + //outSettings.setProperty(KEY_HTTP_PROXY_HOST, mTextProxyServer.getText()); + //outSettings.setProperty(KEY_HTTP_PROXY_PORT, mTextProxyPort.getText()); + settings.setForceHttp(mCheckForceHttp.getSelection()); + //outSettings.setProperty(KEY_ASK_ADB_RESTART, + // Boolean.toString(mCheckAskAdbRestart.getSelection())); + //outSettings.setProperty(KEY_USE_DOWNLOAD_CACHE, + // Boolean.toString(mCheckUseCache.getSelection())); + //outSettings.setProperty(KEY_ENABLE_PREVIEWS, + // Boolean.toString(mCheckEnablePreviews.getSelection())); } @@ -258,28 +253,26 @@ public void retrieveSettings(Properties outSettings) { * settings must be applied. The page does not apply the settings itself, instead * it notifies the application. */ - @Override + /* public void setOnSettingsChanged(SettingsChangedCallback settingsChangedCallback) { mSettingsChangedCallback = settingsChangedCallback; } - +*/ /** * Callback invoked when user touches one of the settings. * There is no "Apply" button, settings are applied immediately as they are changed. * Notify the application that settings have changed. */ private void applyNewSettings() { - if (mSettingsChangedCallback != null) { - mSettingsChangedCallback.onSettingsChanged(this); - } + retrieveSettings(); } - +/* private void updateDownloadCacheSize() { long size = mDownloadCache.getCurrentSize(); String str = FormatUtils.byteSizeToString(size); mTextCacheSize.setText(str); } - +*/ // End of hiding from SWT Designer //$hide<<$ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SwtUpdaterData.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SwtUpdaterData.java deleted file mode 100644 index 0d26a3f8..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SwtUpdaterData.java +++ /dev/null @@ -1,240 +0,0 @@ -/* - * Copyright (C) 2013 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.sdkuilib.internal.repository; - -import com.android.annotations.NonNull; -import com.android.sdklib.internal.repository.AdbWrapper; -import com.android.sdklib.internal.repository.ITaskMonitor; -import com.android.sdklib.internal.repository.NullTaskMonitor; -import com.android.sdklib.internal.repository.archives.Archive; -import com.android.sdklib.internal.repository.updater.ArchiveInfo; -import com.android.sdklib.internal.repository.updater.SdkUpdaterLogic; -import com.android.sdklib.internal.repository.updater.UpdaterData; -import com.android.sdkuilib.internal.repository.icons.ImageFactory; -import com.android.sdkuilib.internal.repository.ui.SdkUpdaterWindowImpl2; -import com.android.utils.ILogger; - -import org.eclipse.jface.dialogs.MessageDialog; -import org.eclipse.swt.widgets.Display; -import org.eclipse.swt.widgets.Shell; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; - -/** - * Data shared between {@link SdkUpdaterWindowImpl2} and its pages. - */ -public class SwtUpdaterData extends UpdaterData { - - private Shell mWindowShell; - - /** - * The current {@link ImageFactory}. - * Set via {@link #setImageFactory(ImageFactory)} by the window implementation. - * It is null when invoked using the command-line interface. - */ - private ImageFactory mImageFactory; - - /** - * Creates a new updater data. - * - * @param sdkLog Logger. Cannot be null. - * @param osSdkRoot The OS path to the SDK root. - */ - public SwtUpdaterData(String osSdkRoot, ILogger sdkLog) { - super(osSdkRoot, sdkLog); - } - - // ----- getters, setters ---- - - public void setImageFactory(ImageFactory imageFactory) { - mImageFactory = imageFactory; - } - - public ImageFactory getImageFactory() { - return mImageFactory; - } - - public void setWindowShell(Shell windowShell) { - mWindowShell = windowShell; - } - - public Shell getWindowShell() { - return mWindowShell; - } - - @Override - protected void displayInitError(String error) { - // We may not have any UI. Only display a dialog if there's a window shell available. - if (mWindowShell != null && !mWindowShell.isDisposed()) { - MessageDialog.openError(mWindowShell, - "Android Virtual Devices Manager", - error); - } else { - super.displayInitError(error); - } - } - - // ----- - - /** - * Runs the runnable on the UI thread using {@link Display#syncExec(Runnable)}. - * - * @param r Non-null runnable. - */ - @Override - protected void runOnUiThread(@NonNull Runnable r) { - if (mWindowShell != null && !mWindowShell.isDisposed()) { - mWindowShell.getDisplay().syncExec(r); - } - } - - /** - * Attempts to restart ADB. - *

- * If the "ask before restart" setting is set (the default), prompt the user whether - * now is a good time to restart ADB. - */ - @Override - protected void askForAdbRestart(ITaskMonitor monitor) { - final boolean[] canRestart = new boolean[] { true }; - - if (getWindowShell() != null && - getSettingsController().getSettings().getAskBeforeAdbRestart()) { - // need to ask for permission first - final Shell shell = getWindowShell(); - if (shell != null && !shell.isDisposed()) { - shell.getDisplay().syncExec(new Runnable() { - @Override - public void run() { - if (!shell.isDisposed()) { - canRestart[0] = MessageDialog.openQuestion(shell, - "ADB Restart", - "A package that depends on ADB has been updated. \n" + - "Do you want to restart ADB now?"); - } - } - }); - } - } - - if (canRestart[0]) { - AdbWrapper adb = new AdbWrapper(getOsSdkRoot(), monitor); - adb.stopAdb(); - adb.startAdb(); - } - } - - @Override - protected void notifyToolsNeedsToBeRestarted(int flags) { - String msg = null; - if ((flags & TOOLS_MSG_UPDATED_FROM_ADT) != 0) { - msg = - "The Android SDK and AVD Manager that you are currently using has been updated. " + - "Please also run Eclipse > Help > Check for Updates to see if the Android " + - "plug-in needs to be updated."; - - } else if ((flags & TOOLS_MSG_UPDATED_FROM_SDKMAN) != 0) { - msg = - "The Android SDK and AVD Manager that you are currently using has been updated. " + - "It is recommended that you now close the manager window and re-open it. " + - "If you use Eclipse, please run Help > Check for Updates to see if the Android " + - "plug-in needs to be updated."; - } - - final String msg2 = msg; - - final Shell shell = getWindowShell(); - if (msg2 != null && shell != null && !shell.isDisposed()) { - shell.getDisplay().syncExec(new Runnable() { - @Override - public void run() { - if (!shell.isDisposed()) { - MessageDialog.openInformation(shell, - "Android Tools Updated", - msg2); - } - } - }); - } - } - - /** - * Tries to update all the *existing* local packages. - * This version *requires* to be run with a GUI. - *

- * There are two modes of operation: - *

    - *
  • If selectedArchives is null, refreshes all sources, compares the available remote - * packages with the current local ones and suggest updates to be done to the user (including - * new platforms that the users doesn't have yet). - *
  • If selectedArchives is not null, this represents a list of archives/packages that - * the user wants to install or update, so just process these. - *
- * - * @param selectedArchives The list of remote archives to consider for the update. - * This can be null, in which case a list of remote archive is fetched from all - * available sources. - * @param includeObsoletes True if obsolete packages should be used when resolving what - * to update. - * @param flags Optional flags for the installer, such as {@link #NO_TOOLS_MSG}. - * @return A list of archives that have been installed. Can be null if nothing was done. - */ - @Override - public List updateOrInstallAll_WithGUI( - Collection selectedArchives, - boolean includeObsoletes, - int flags) { - - // Note: we no longer call refreshSources(true) here. This will be done - // automatically by computeUpdates() iif it needs to access sources to - // resolve missing dependencies. - - SdkUpdaterLogic ul = new SdkUpdaterLogic(this); - List archives = ul.computeUpdates( - selectedArchives, - getSources(), - getLocalSdkParser().getPackages(), - includeObsoletes); - - if (selectedArchives == null) { - getPackageLoader().loadRemoteAddonsList(new NullTaskMonitor(getSdkLog())); - ul.addNewPlatforms( - archives, - getSources(), - getLocalSdkParser().getPackages(), - includeObsoletes); - } - - // TODO if selectedArchives is null and archives.len==0, find if there are - // any new platform we can suggest to install instead. - - Collections.sort(archives); - - SdkUpdaterChooserDialog dialog = - new SdkUpdaterChooserDialog(getWindowShell(), this, archives); - dialog.open(); - - ArrayList result = dialog.getResult(); - if (result != null && result.size() > 0) { - return installArchives(result, flags); - } - return null; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/UpdaterBaseDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/UpdaterBaseDialog.java index bc8d9d51..f1123f1f 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/UpdaterBaseDialog.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/UpdaterBaseDialog.java @@ -17,12 +17,13 @@ package com.android.sdkuilib.internal.repository; import com.android.SdkConstants; -import com.android.sdkuilib.internal.repository.icons.ImageFactory; -import com.android.sdkuilib.internal.repository.ui.SdkUpdaterWindowImpl2; +import org.eclipse.andmore.base.resources.ImageFactory; +//import com.android.sdkuilib.internal.repository.ui.SdkUpdaterWindowImpl2; import com.android.sdkuilib.ui.GridDataBuilder; import com.android.sdkuilib.ui.GridLayoutBuilder; import com.android.sdkuilib.ui.SwtBaseDialog; +import org.eclipse.andmore.sdktool.SdkContext; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; @@ -36,18 +37,19 @@ * about box or add-on site.) */ public abstract class UpdaterBaseDialog extends SwtBaseDialog { + public static final String APP_NAME = "Android SDK Manager"; - private final SwtUpdaterData mSwtUpdaterData; + protected final SdkContext mSdkContext; - protected UpdaterBaseDialog(Shell parentShell, SwtUpdaterData swtUpdaterData, String title) { + protected UpdaterBaseDialog(Shell parentShell, SdkContext sdkContext, String title) { super(parentShell, SWT.APPLICATION_MODAL, - String.format("%1$s - %2$s", SdkUpdaterWindowImpl2.APP_NAME, title)); //$NON-NLS-1$ - mSwtUpdaterData = swtUpdaterData; + String.format("%1$s - %2$s", APP_NAME, title)); //$NON-NLS-1$ + mSdkContext = sdkContext; } - public SwtUpdaterData getSwtUpdaterData() { - return mSwtUpdaterData; + public SdkContext getSdkContext() { + return mSdkContext; } /** @@ -96,11 +98,9 @@ private void setWindowImage(Shell shell) { imageName = "android_icon_128.png"; //$NON-NLS-1$ } - if (mSwtUpdaterData != null) { - ImageFactory imgFactory = mSwtUpdaterData.getImageFactory(); - if (imgFactory != null) { - shell.setImage(imgFactory.getImageByName(imageName)); - } + ImageFactory imgFactory = mSdkContext.getSdkHelper().getImageFactory(); + if (imgFactory != null) { + shell.setImage(imgFactory.getImageByName(imageName)); } } } diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/UserCredentials.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/UserCredentials.java similarity index 87% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/UserCredentials.java rename to andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/UserCredentials.java index 62949564..c3e55d1c 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/repository/UserCredentials.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/UserCredentials.java @@ -1,53 +1,50 @@ -/* - * Copyright (C) 2011 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.sdklib.internal.repository; - -/** - * @deprecated - * com.android.sdklib.internal.repository has moved into Studio as - * com.android.tools.idea.sdk.remote.internal. - */ -@Deprecated -public class UserCredentials { - private final String mUserName; - private final String mPassword; - private final String mWorkstation; - private final String mDomain; - - public UserCredentials(String userName, String password, String workstation, String domain) { - mUserName = userName; - mPassword = password; - mWorkstation = workstation; - mDomain = domain; - } - - public String getUserName() { - return mUserName; - } - - public String getPassword() { - return mPassword; - } - - public String getWorkstation() { - return mWorkstation; - } - - public String getDomain() { - return mDomain; - } -} +/* + * Copyright (C) 2011 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.sdkuilib.internal.repository; + +/** + * User authentication details + */ +public class UserCredentials { + private final String mUserName; + private final String mPassword; + private final String mWorkstation; + private final String mDomain; + + public UserCredentials(String userName, String password, String workstation, String domain) { + mUserName = userName; + mPassword = password; + mWorkstation = workstation; + mDomain = domain; + } + + public String getUserName() { + return mUserName; + } + + public String getPassword() { + return mPassword; + } + + public String getWorkstation() { + return mWorkstation; + } + + public String getDomain() { + return mDomain; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/avd/AvdAgent.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/avd/AvdAgent.java new file mode 100644 index 00000000..f335f369 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/avd/AvdAgent.java @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2017 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.sdkuilib.internal.repository.avd; + +import java.util.Map; + +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.SdkVersionInfo; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.internal.avd.AvdInfo; +import com.android.sdklib.internal.avd.AvdManager; +import com.android.sdklib.repository.IdDisplay; +import com.android.sdklib.repository.meta.DetailsTypes; +import com.android.sdklib.repository.targets.SystemImage; +import com.android.sdkuilib.internal.repository.content.PackageType; + +/** + * @author Andrew Bowley + * + * 15-11-2017 + */ +public class AvdAgent { + + private static final String BLANK = ""; + private final AvdInfo avd; + private final IAndroidTarget target; + private String deviceName; + private String deviceMfctr; + private String path; + private SystemImage systemImage; + private String platformVersion; + private String versionWithCodename; + private AndroidVersion androidVersion; + private PackageType packageType; + private String vendor; + private String targetDisplayName; + private String skin; + private String sdcard; + private String snapshot; + + /** + * + */ + public AvdAgent(IAndroidTarget target, AvdInfo avd) { + this.target = target; + this.avd = avd; + path = avd.getDataFolderPath(); + systemImage = (SystemImage) avd.getSystemImage(); + vendor = BLANK; + targetDisplayName = BLANK; + init(); + } + + public AvdInfo getAvd() { + return avd; + } + + public IAndroidTarget getTarget() + { + return target; + } + + public String getDeviceName() { + return deviceName; + } + + public String getDeviceMfctr() { + return deviceMfctr; + } + + public String getPath() { + return path; + } + + public SystemImage getSystemImage() { + return systemImage; + } + + public String getPlatformVersion() { + return platformVersion; + } + + public String getVersionWithCodename() { + return versionWithCodename; + } + + public AndroidVersion getAndroidVersion() { + return androidVersion; + } + + public PackageType getPackageType() { + return packageType; + } + + public String getVendor() { + return vendor; + } + + public String getTargetFullName() { + return target.getFullName(); + } + + public String getTargetVersionName() { + return target.getVersionName(); + } + + public String getTargetDisplayName() { + return targetDisplayName; + } + + public String getSkin() { + return skin; + } + + public String getSdcard() { + return sdcard; + } + + public String getSnapshot() { + return snapshot; + } + + public String getPrettyAbiType() { + return AvdInfo.getPrettyAbiType(avd); + } + + private void init() + { + deviceName = avd.getProperties().get(AvdManager.AVD_INI_DEVICE_NAME); + deviceMfctr = avd.getProperties().get(AvdManager.AVD_INI_DEVICE_MANUFACTURER); + if (deviceName == null) { + deviceName = BLANK; + deviceMfctr = BLANK; + } + else if (deviceMfctr == null) + deviceMfctr = BLANK; + DetailsTypes.ApiDetailsType details = + (DetailsTypes.ApiDetailsType) systemImage.getPackage().getTypeDetails(); + androidVersion = details.getAndroidVersion(); + versionWithCodename = SdkVersionInfo + .getVersionWithCodename(androidVersion); + platformVersion = SdkVersionInfo.getVersionString(androidVersion.getApiLevel()); + if (details instanceof DetailsTypes.PlatformDetailsType) { + packageType = PackageType.platforms; + } else if (details instanceof DetailsTypes.SysImgDetailsType) { + packageType = PackageType.system_images; + IdDisplay vendorId = ((DetailsTypes.SysImgDetailsType) details).getVendor(); + if (vendorId != null) { + vendor = vendorId.getDisplay(); + } + } else if (details instanceof DetailsTypes.AddonDetailsType) { + packageType = PackageType.add_ons; + vendor = ((DetailsTypes.AddonDetailsType) details).getVendor().getDisplay(); + } + if (target.isPlatform()) { + targetDisplayName = String.format(" API: %s", versionWithCodename); + } else { + targetDisplayName = + String.format("Target: %s\n" + + " Based on %s)", + target.getFullName(), + target.getParent().getFullName()); + } + // Some extra values. + Map properties = avd.getProperties(); + skin = properties.get(AvdManager.AVD_INI_SKIN_NAME); + sdcard = properties.get(AvdManager.AVD_INI_SDCARD_SIZE); + if (sdcard == null) + sdcard = properties.get(AvdManager.AVD_INI_SDCARD_PATH); + if (sdcard == null) + sdcard = BLANK; + snapshot = properties.get(AvdManager.AVD_INI_SNAPSHOT_PRESENT); + if (snapshot == null) + snapshot = BLANK; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/avd/SdkTargets.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/avd/SdkTargets.java new file mode 100644 index 00000000..0df8b847 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/avd/SdkTargets.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2017 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.sdkuilib.internal.repository.avd; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; + +import org.eclipse.andmore.sdktool.SdkContext; + +import com.android.repository.api.ProgressIndicator; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.internal.avd.AvdInfo; +import com.android.sdklib.repository.IdDisplay; +import com.android.sdklib.repository.meta.DetailsTypes; +import com.android.sdklib.repository.targets.AddonTarget; +import com.android.sdklib.repository.targets.PlatformTarget; +import com.android.sdklib.repository.targets.SystemImage; +import com.android.sdkuilib.internal.repository.content.PackageType; + +/** + * @author Andrew Bowley + * + * 16-11-2017 + */ +public class SdkTargets { + + private final List targets = new ArrayList<>(); + private final Collection sysImages; + private final Map targetMap = new HashMap<>(); + + /** + * + */ + public SdkTargets(SdkContext sdkContext) { + ProgressIndicator progress = sdkContext.getProgressIndicator(); + targets.addAll( + sdkContext.getHandler().getAndroidTargetManager(progress) + .getTargets(progress)); + Collections.sort(targets, new Comparator(){ + + @Override + public int compare(IAndroidTarget target1, IAndroidTarget target2) { + return getApiLevel(target1) - getApiLevel(target2); + }}); + + sysImages = + sdkContext.getHandler().getSystemImageManager(progress).getImages(); + Iterator iterator = sysImages.iterator(); + while(iterator.hasNext()) { + SystemImage systemImage = iterator.next(); + IAndroidTarget target = mapTarget(systemImage); + if (target != null) + targetMap.put(systemImage, target); + } + + } + + public List getSystemImages(IAndroidTarget target) { + List systemImages = new ArrayList<>(); + Set> targetEntries = targetMap.entrySet(); + Iterator> iterator = targetEntries.iterator(); + while(iterator.hasNext()) { + Entry entry = iterator.next(); + if (entry.getValue().canRunOn(target)) + systemImages.add(entry.getKey()); + } + // Sort + Collections.sort(systemImages, new Comparator(){ + + @Override + public int compare(SystemImage sysImage1, SystemImage sysImage2) { + return sysImage1.compareTo(sysImage2); + }}); + // Remove duplicates which contain same abitype details + // Prior sort ensures predictable results + Set abiTypeSet = new HashSet<>(); + Iterator imagesIterator = systemImages.iterator(); + while (imagesIterator.hasNext()) { + SystemImage systemImage = imagesIterator.next(); + String key = AvdInfo.getPrettyAbiType(systemImage); + if (abiTypeSet.contains(key)) + imagesIterator.remove(); + else + abiTypeSet.add(key); + } + return systemImages; + } + + public Map getTargetMap() + { + return Collections.unmodifiableMap(targetMap); + } + + public Collection getTargets() { + return targets; + } + + public Collection getSysImages() { + return sysImages; + } + + public IAndroidTarget getTargetForSysImage(SystemImage systemImage) { + return targetMap.get(systemImage); + } + + IAndroidTarget mapTarget(SystemImage systemImage) { + PackageType packageType = null; + IdDisplay vendorId = IdDisplay.create("", ""); + DetailsTypes.ApiDetailsType details = + (DetailsTypes.ApiDetailsType) systemImage.getPackage().getTypeDetails(); + if (details instanceof DetailsTypes.PlatformDetailsType) { + packageType = PackageType.platforms; + } else if (details instanceof DetailsTypes.SysImgDetailsType) { + packageType = PackageType.system_images; + vendorId = ((DetailsTypes.SysImgDetailsType) details).getVendor(); + } else if (details instanceof DetailsTypes.AddonDetailsType) { + packageType = PackageType.add_ons; + vendorId = ((DetailsTypes.AddonDetailsType) details).getVendor(); + } + for (IAndroidTarget target: targets) + { + if (filterOnApi(systemImage, target)) + { + if ((packageType == PackageType.add_ons) && + !target.isPlatform() && + target.getVendor().equals(vendorId.getId())) + return target; + else if (target.isPlatform()) + return target; + } + } + return null; + } + + private boolean filterOnApi(SystemImage systemImage, IAndroidTarget target) + { + int imageApi = systemImage.getAndroidVersion().getApiLevel(); + int targetApi = getApiLevel(target); + return imageApi <= targetApi; + } + + private int getApiLevel(IAndroidTarget target) { + if (target.isPlatform()) { + PlatformTarget plaformTarget = (PlatformTarget)target; + return plaformTarget.getVersion().getApiLevel(); + } + AddonTarget addonTarget = (AddonTarget)target; + return addonTarget.getParent().getVersion().getApiLevel(); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/CategoryKeyType.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/CategoryKeyType.java new file mode 100644 index 00000000..b8d703f3 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/CategoryKeyType.java @@ -0,0 +1,10 @@ +package com.android.sdkuilib.internal.repository.content; + +public enum CategoryKeyType { + TOOLS, + TOOLS_PREVIEW, + API, + EXTRA, + LOCAL, + REMOTE +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/INode.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/INode.java new file mode 100644 index 00000000..dea81e15 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/INode.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2017 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.sdkuilib.internal.repository.content; + +import java.util.Collections; +import java.util.List; + +import com.android.sdkuilib.internal.repository.content.PackageAnalyser.PkgState; + +/** + * Tree node which provides the text, font and image for the label of a given tree element + * @author Andrew Bowley + * + */ +public class INode { + /** Font can be normal or italic */ + static enum LabelFont { normal, italic } + + static final List EMPTY_NODE_LIST; + static final String VOID = ""; + + static + { + EMPTY_NODE_LIST = Collections.emptyList(); + } + + /** Flag to mirror node checkbox state */ + protected boolean isChecked; + + /** + * Returns the image resource value for the label of the given element. + * @param element Target + * @param columnIndex The index of the column being displayed + * @return the resource value of image used to label the element, or VOID if there is no image for the given object + */ + public String getImage(Object element, int columnIndex) { + return VOID; + } + + /** + * Returns the text for the label of the given element. + * @param element Target + * @param columnIndex The index of the column being displayed + * @return the text string used to label the element, or VOID if there is no text label for the given object + */ + public String getText(Object element, int columnIndex) { + return VOID; + } + + /** + * Provides a font for the given element at index columnIndex. + * @param element Target + * @param columnIndex The index of the column being displayed + * @return LabelFont.normal or LabelFont.italic + */ + public LabelFont getFont(Object element, int columnIndex) { + return LabelFont.normal; + } + + /** + * Get the text displayed in the tool tip for given element + * @param element Target + * @return the tooltop text, or VOID for no text to display + */ + public String getToolTipText(Object element) { + return VOID; + } + + /** + * Returns list of descendents + * @return INode list + */ + public List getChildren() { + return EMPTY_NODE_LIST; + } + + /** + * Returns checkbox state + * @return boolean + */ + public boolean isChecked() { + return isChecked; + } + + /** + * Sets checkbox state + * @param isChecked Value to set checkbox on next refresh + */ + public void setChecked(boolean isChecked) { + this.isChecked = isChecked; + } + + /** + * Mark item as checked according to given criteria. Force uncheck if no criteria specified. + * @param selectUpdates If true, select all update packages + * @param topApiLevel If greater than 0, select platform packages of this api level + */ + public void checkSelections( + boolean selectUpdates, + int topApiLevel) + { + } + + public void markDeleted() + { + } + + public boolean isDeleted() + { + return false; + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/MetaPackage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/MetaPackage.java new file mode 100644 index 00000000..5af460dd --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/MetaPackage.java @@ -0,0 +1,28 @@ +package com.android.sdkuilib.internal.repository.content; + +/** + * Information about a package type + */ +public class MetaPackage { + + private final PackageType mPackageType; + private final String mIconResource; + + public MetaPackage(PackageType packageType, String iconResource) { + this.mPackageType = packageType; + this.mIconResource = iconResource; + } + + public PackageType getPackageType() { + return mPackageType; + } + + public String getIconResource() { + return mIconResource; + } + + public String getName() + { + return mPackageType.toString().replaceAll("_", "-"); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PackageAnalyser.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PackageAnalyser.java new file mode 100644 index 00000000..bab62caf --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PackageAnalyser.java @@ -0,0 +1,304 @@ +package com.android.sdkuilib.internal.repository.content; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.andmore.sdktool.SdkContext; + +import com.android.SdkConstants; +import com.android.repository.api.LocalPackage; +import com.android.repository.api.RemotePackage; +import com.android.repository.api.RepoPackage; +import com.android.repository.api.UpdatablePackage; +import com.android.repository.impl.meta.TypeDetails; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.repository.meta.DetailsTypes; +import com.android.sdkuilib.internal.repository.PackageManager.UpdatablePackageHandler; +import com.android.sdkuilib.internal.repository.content.UpdateOp.SdkSource; + +public class PackageAnalyser { + public static final String GENERIC = "generic"; + + private final SdkContext mSdkContext; + private final UpdateOpApi mOpApi; + private final Map mMetaPackageMap = new HashMap<>(); + private boolean mFirstLoadComplete = true; + + /** + * {@link PkgState}s to check in {@link #processSource(UpdateOp, SdkSource, Package[])}. + */ + public static enum PkgState { INSTALLED, NEW, DELETED }; + + public PackageAnalyser(SdkContext sdkContext) + { + this.mSdkContext = sdkContext; + mOpApi = new UpdateOpApi(); + initMetaPackages(); + } + + public MetaPackage getMetaPackage(String name) + { + return mMetaPackageMap.get(name); + } + + /** + * Removes all the internal state and resets the object. + * Useful for testing. + */ + public void clear() { + mFirstLoadComplete = true; + mOpApi.clear(); + } + + /** Return mFirstLoadComplete and resets it to false. + * All following calls will returns false. */ + public boolean isFirstLoadComplete() { + boolean b = mFirstLoadComplete; + mFirstLoadComplete = false; + return b; + } + + + /** + * Mark all new and update PkgItems as checked. + * + * @param selectNew If true, select all new packages (except the rc/preview ones). + * @param selectUpdates If true, select all update packages. + * @param selectTop If true, select the top platform. All new packages are selected, excluding system images and + * rc/preview. Packages to update are selected regardless. + * @param currentPlatform The {@link SdkConstants#currentPlatform()} value. + */ + public void checkNewUpdateItems( + boolean selectUpdates, + boolean selectTop) { + int apiLevel = 0; + if (selectTop) { + for (PkgCategory cat: mOpApi.getCategories()) + { + // Find first API category to get top API + if (cat.getKeyType() == CategoryKeyType.API) { + PkgCategoryApi pkgCategoryApi = (PkgCategoryApi)cat; + apiLevel = pkgCategoryApi.getKeyValue().getApiLevel(); + break; + } + } + } + for (PkgCategory cat: mOpApi.getCategories()) + checkNode(cat, selectUpdates, apiLevel, 0); + } + + private void checkNode(INode node, boolean selectUpdates, int topApiLevel, int level) + { + node.checkSelections(selectUpdates, topApiLevel); + for (INode child: node.getChildren()) + checkNode(child, selectUpdates, topApiLevel, level+ 1); + } + + /** + * Mark all PkgItems as not checked. + */ + public void uncheckAllItems() { + for (PkgCategory cat: mOpApi.getCategories()) + checkNode(cat, false, 0, 0); + } + + public List> getApiCategories() { + return mOpApi.getCategories(); + } + + public List getAllPkgItems() { + List items = new ArrayList(); + + List> cats = getApiCategories(); + synchronized (cats) { + for (PkgCategory cat : cats) { + items.addAll(cat.getItems()); + } + } + return items; + } + + public void updateStart() { + mOpApi.updateStart(); + } + + public void removeDeletedNodes() { + for (PkgCategory cat: mOpApi.getCategories()) + remoteDeleted(cat, 0); + } + + private void remoteDeleted(INode node, int level) { + List removeList = null; + for (INode childNode: node.getChildren()) { + if (childNode.isDeleted()) { + if (removeList == null) + removeList = new ArrayList<>(); + removeList.add(childNode); + } + remoteDeleted(childNode, level + 1); + } + if (removeList != null) { + for (INode removeNode: removeList) { + node.getChildren().remove(removeNode); + } + } + } + + public boolean updateSourcePackages(SdkSource source, RepoPackage[] newPackages) { + + return mOpApi.updateSourcePackages(source, newPackages); + } + + public boolean updateEnd() { + return mOpApi.updateEnd(); + } + + public static String getNameFromPath(String path) + { + int pos = path.indexOf(RepoPackage.PATH_SEPARATOR); + return pos == -1 ? path : path.substring(0, pos); + } + + public static AndroidVersion getAndroidVersion(RepoPackage pkg) { + TypeDetails details = pkg.getTypeDetails(); + if (details instanceof DetailsTypes.ApiDetailsType) { + return ((DetailsTypes.ApiDetailsType)details).getAndroidVersion(); + } + return null; + } + + private void initMetaPackages() { + MetaPackage metaPackage = new MetaPackage(PackageType.tools, "tool_pkg_16.png"); + mMetaPackageMap.put(metaPackage.getName(), metaPackage); + + metaPackage = new MetaPackage(PackageType.platform_tools, "platformtool_pkg_16.png"); + mMetaPackageMap.put(metaPackage.getName(), metaPackage); + + metaPackage = new MetaPackage(PackageType.build_tools, "buildtool_pkg_16.png"); + mMetaPackageMap.put(metaPackage.getName(), metaPackage); + + metaPackage = new MetaPackage(PackageType.platforms, "platform_pkg_16.png"); + mMetaPackageMap.put(metaPackage.getName(), metaPackage); + + metaPackage = new MetaPackage(PackageType.add_ons, "addon_pkg_16.png"); + mMetaPackageMap.put(metaPackage.getName(), metaPackage); + + metaPackage = new MetaPackage(PackageType.system_images, "sysimg_pkg_16.png"); + mMetaPackageMap.put(metaPackage.getName(), metaPackage); + + metaPackage = new MetaPackage(PackageType.sources, "source_pkg_16.png"); + mMetaPackageMap.put(metaPackage.getName(), metaPackage); + + metaPackage = new MetaPackage(PackageType.docs, "doc_pkg_16.png"); + mMetaPackageMap.put(metaPackage.getName(), metaPackage); + + metaPackage = new MetaPackage(PackageType.extras, "extra_pkg_16.png"); + mMetaPackageMap.put(metaPackage.getName(), metaPackage); + + metaPackage = new MetaPackage(PackageType.emulator, "tool_pkg_16.png"); + mMetaPackageMap.put(metaPackage.getName(), metaPackage); + + metaPackage = new MetaPackage(PackageType.cmake, "tool_pkg_16.png"); + mMetaPackageMap.put(metaPackage.getName(), metaPackage); + + metaPackage = new MetaPackage(PackageType.lldb, "tag_default_16.png"); + mMetaPackageMap.put(metaPackage.getName(), metaPackage); + + metaPackage = new MetaPackage(PackageType.ndk_bundle, "tag_default_16.png"); + mMetaPackageMap.put(metaPackage.getName(), metaPackage); + + metaPackage = new MetaPackage(PackageType.patcher, "tool_pkg_16.png"); + mMetaPackageMap.put(metaPackage.getName(), metaPackage); + + metaPackage = new MetaPackage(PackageType.generic, "tag_default_16.png"); + mMetaPackageMap.put(metaPackage.getName(), metaPackage); + } + + public void loadPackages() { + UpdatablePackageHandler updateHandler = new UpdatablePackageHandler(){ + + @Override + public void onPackageLoaded(UpdatablePackage updatePackage) { + updateApiItem(updatePackage); + }}; + // All previous entries must be deleted or duplicates will occur + mOpApi.getCategories().clear(); + mSdkContext.getPackageManager().loadConsolidatedPackages(updateHandler); + mOpApi.sortCategoryList(); + } + + private void updateApiItem(UpdatablePackage updatePackage) { + LocalPackage local = updatePackage.getLocal(); + RemotePackage remote = updatePackage.getRemote(); + PkgCategory cat = null; + PkgItem item = null; + if (local != null) { + cat = getPkgCategory(local); + item = new PkgItem(local, metaPackageFromPackage(local), PkgState.INSTALLED); + if ((remote != null) && !local.getVersion().equals(remote.getVersion())) + item.setUpdatePkg(updatePackage); + } + else { + cat = getPkgCategory(remote); + item = new PkgItem(remote, metaPackageFromPackage(remote), PkgState.NEW); + } + cat.getItems().add(item); + } + + private PkgCategory getPkgCategory(RepoPackage pkg) + { + List> cats = mOpApi.getCategories(); + CategoryKeyType catKeyType = mOpApi.getCategoryKeyType(pkg); + PkgCategory cat = null; + AndroidVersion catKeyValue = null; + switch (catKeyType) + { + case API: + case REMOTE: + catKeyValue = mOpApi.getCategoryKeyValue(pkg); + cat = findCurrentCategory(cats, catKeyType, catKeyValue); + break; + default: + cat = findCurrentCategory(cats, catKeyType); + } + if (cat == null) { + // This is a new category. Create it and add it to the list. + cat = mOpApi.createCategory(catKeyType, catKeyValue); + synchronized (cats) { + cats.add(cat); + } + } + return cat; + } + + private PkgCategory findCurrentCategory( + List> currentCategories, + CategoryKeyType catKeyType) { + for (PkgCategory cat : currentCategories) { + if (cat.getKeyType() == catKeyType) { + return cat; + } + } + return null; + } + + private PkgCategory findCurrentCategory( + List> currentCategories, + CategoryKeyType catKeyType, Object categoryKeyValue) { + for (PkgCategory cat : currentCategories) { + if ((cat.getKeyType() == catKeyType) && (cat.getKeyValue().equals(categoryKeyValue))) + return cat; + } + return null; + } + + private MetaPackage metaPackageFromPackage(RepoPackage repoPackage) + { + String name = PackageAnalyser.getNameFromPath(repoPackage.getPath()); + MetaPackage metaPackage = getMetaPackage(name); + return metaPackage != null ? metaPackage : getMetaPackage(PackageAnalyser.GENERIC); + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PackageContentProvider.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PackageContentProvider.java new file mode 100644 index 00000000..9442c62c --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PackageContentProvider.java @@ -0,0 +1,130 @@ +package com.android.sdkuilib.internal.repository.content; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.andmore.sdktool.SdkContext; +import org.eclipse.jface.viewers.IInputProvider; +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.Viewer; + +import com.android.repository.api.RemotePackage; +import com.android.repository.api.RepoPackage; +import com.android.repository.api.UpdatablePackage; +import com.android.repository.impl.meta.Archive; +import com.android.sdkuilib.internal.repository.PackageManager; + +public class PackageContentProvider implements ITreeContentProvider { + + private final IInputProvider mViewer; + private final SdkContext mSdkContext; + private final PackageManager mPackageManager; + private boolean mDisplayArchives; + + public PackageContentProvider(SdkContext sdkContext, IInputProvider viewer) + { + this.mSdkContext = sdkContext; + this.mViewer = viewer; + this.mPackageManager = sdkContext.getPackageManager(); + } + + public void setDisplayArchives(boolean displayArchives) { + mDisplayArchives = displayArchives; + } + + + @Override + public Object[] getChildren(Object parentElement) { + if (parentElement instanceof ArrayList) { + return ((ArrayList) parentElement).toArray(); + + } else if (parentElement instanceof PkgCategory) { + return ((PkgCategory) parentElement).getItems().toArray(); + + } else if (parentElement instanceof PkgItem) { + if (mDisplayArchives) { + + UpdatablePackage pkg = ((PkgItem) parentElement).getUpdatePkg(); + + // Display update packages as sub-items if the details mode is activated. + if (pkg != null) { + return new Object[] { pkg }; + } + + return ((PkgItem) parentElement).getArchives(); + } + + } else if (parentElement instanceof RemotePackage) { + if (mDisplayArchives) { + return new Archive[]{((RemotePackage) parentElement).getArchive()}; + } + + } + + return new Object[0]; + } + + @Override + public Object[] getElements(Object inputElement) { + return getChildren(inputElement); + } + + @Override + @SuppressWarnings("unchecked") + public Object getParent(Object element) { + // This operation is expensive, so we do the minimum + // and don't try to cover all cases. + + if (element instanceof PkgItem) { + Object input = mViewer.getInput(); + if (input != null) { + for (PkgCategory cat : (List) input) { + if (cat.getItems().contains(element)) { + return cat; + } + } + } + } + + return null; + } + + + @Override + public boolean hasChildren(Object parentElement) { + if (parentElement instanceof ArrayList) { + return true; + + } else if (parentElement instanceof PkgCategory) { + return true; + + } else if (parentElement instanceof PkgItem) { + if (mDisplayArchives) { + UpdatablePackage pkg = ((PkgItem) parentElement).getUpdatePkg(); + + // Display update packages as sub-items if the details mode is activated. + if (pkg != null) { + return true; + } + + Archive[] archives = ((PkgItem) parentElement).getArchives(); + return archives.length > 0; + } + } else if (parentElement instanceof RemotePackage) { + if (mDisplayArchives) { + return ((RemotePackage) parentElement).getArchive() != null; + } + } + + return false; + } + + @Override + public void dispose() { + } + + @Override + public void inputChanged(Viewer arg0, Object arg1, Object arg2) { + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PackageType.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PackageType.java new file mode 100644 index 00000000..fd9dc2f3 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PackageType.java @@ -0,0 +1,19 @@ +package com.android.sdkuilib.internal.repository.content; + +public enum PackageType { + tools, + platform_tools, + build_tools, + platforms, + add_ons, + system_images, + sources, + docs, + extras, + emulator, + cmake, + lldb, + ndk_bundle, + patcher, + generic +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgCategory.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgCategory.java new file mode 100644 index 00000000..9ff07680 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgCategory.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2011 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.sdkuilib.internal.repository.content; + + +import java.util.ArrayList; +import java.util.List; + +import com.android.sdkuilib.internal.repository.ui.PackagesPage; + +/** + * PkgCategory represents the first level of the package tree + * @author Andrew Bowley + * + * 10-11-2017 + */ +public abstract class PkgCategory extends INode { + protected final CategoryKeyType keyType; + protected final K keyValue; + protected final String imageReference; + protected final List packageList = new ArrayList(); + protected String label; + + public PkgCategory(CategoryKeyType keyType, String label, String imageReference) { + this(keyType, null, label, imageReference); + } + + public PkgCategory(CategoryKeyType keyType, K keyValue, String label, String imageReference) { + super(); + this.keyType = keyType; + this.keyValue = keyValue; + this.label = label; + this.imageReference = imageReference; + } + + public CategoryKeyType getKeyType() + { + return keyType; + } + + public K getKeyValue() { + return keyValue; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public List getItems() { + return packageList; + } + + /** + * Returns the text for the label of the given element. + * @param element Target + * @param columnIndex The index of the column being displayed + * @return the text string used to label the element + */ + @Override + public String getText(Object element, int columnIndex) { + return columnIndex == PackagesPage.NAME ? getLabel() : VOID; + } + + /** + * Returns the image resource value for the label of the given element. + * @param element Target + * @param columnIndex The index of the column being displayed + * @return the resource value of image used to label the element + */ + @Override + public String getImage(Object element, int columnIndex) { + if (columnIndex == PackagesPage.NAME) + return imageReference; + return VOID; + } + + /** + * Returns list of descendents + * @return INode list + */ + @Override + public List getChildren() { + return packageList; + } + + @Override + public String toString() { + return String.format("%s ", + this.getClass().getSimpleName(), + keyValue == null ? keyType.toString() : keyValue.toString(), + label, + packageList.size()); + } + + /** {@link PkgCategory}s are equal if their internal keys are equal. */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((keyValue == null) ? keyType.hashCode() : keyValue.hashCode()); + return result; + } + + /** {@link PkgCategory}s are equal if their internal keys are equal. */ + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + PkgCategory other = (PkgCategory) obj; + if (keyType != other.keyType) + return false; + if (keyValue == null) { + if (other.keyValue != null) return false; + } else if (!keyValue.equals(other.keyValue)) return false; + return true; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgCategoryApi.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgCategoryApi.java similarity index 65% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgCategoryApi.java rename to andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgCategoryApi.java index 89a4914a..1353e01e 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgCategoryApi.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgCategoryApi.java @@ -14,12 +14,13 @@ * limitations under the License. */ -package com.android.sdkuilib.internal.repository.core; +package com.android.sdkuilib.internal.repository.content; import com.android.sdklib.AndroidVersion; +import com.android.sdklib.SdkVersionInfo; -public class PkgCategoryApi extends PkgCategory { +public class PkgCategoryApi extends PkgCategory { /** Platform name, in the form "Android 1.2". Can be null if we don't have the name. */ private String mPlatformName; @@ -29,16 +30,11 @@ public class PkgCategoryApi extends PkgCategory { // We always want categories in order tools..platforms..extras; to achieve that tools // and extras have the special values so they get "naturally" sorted the way we want // them. - // (Note: don't use integer.max to avoid integers wrapping in comparisons. We can - // revisit the day we get 2^30 platforms.) - public final static AndroidVersion KEY_TOOLS = new AndroidVersion(Integer.MAX_VALUE / 2, null); - public final static AndroidVersion KEY_TOOLS_PREVIEW = - new AndroidVersion(Integer.MAX_VALUE / 2 - 1, null); - public final static AndroidVersion KEY_EXTRA = new AndroidVersion(-1, null); - public PkgCategoryApi(AndroidVersion version, String platformName, Object iconRef) { - super(version, null /*label*/, iconRef); - setPlatformName(platformName); + public PkgCategoryApi(CategoryKeyType keyType, AndroidVersion version, String iconRef) { + super(keyType, version, null /*label*/, iconRef); + if (version != null) + setPlatformName(SdkVersionInfo.getVersionWithCodename(version)); } public String getPlatformName() { @@ -54,29 +50,28 @@ public void setPlatformName(String platformName) { } public String getApiLabel() { - AndroidVersion key = (AndroidVersion) getKey(); - if (key.equals(KEY_TOOLS)) { + if (getKeyType() == CategoryKeyType.TOOLS) { return "TOOLS"; //$NON-NLS-1$ // for internal debug use only - } else if (key.equals(KEY_TOOLS_PREVIEW)) { - return "TOOLS-PREVIEW"; //$NON-NLS-1$ // for internal debug use only - } else if (key.equals(KEY_EXTRA)) { + } else if (getKeyType() == CategoryKeyType.TOOLS_PREVIEW){ + return "TOOLS-PREVIEW"; //$NON-NLS-1$ // for internal debug use only + } else if (getKeyType() == CategoryKeyType.EXTRA) { return "EXTRAS"; //$NON-NLS-1$ // for internal debug use only - } else { - return key.toString(); - } - } + } + AndroidVersion key = getKeyValue(); + return key.getApiString(); + } @Override public String getLabel() { String label = super.getLabel(); if (label == null) { - AndroidVersion key = (AndroidVersion) getKey(); + CategoryKeyType key = getKeyType(); - if (key.equals(KEY_TOOLS)) { + if (key == CategoryKeyType.TOOLS) { label = "Tools"; - } else if (key.equals(KEY_TOOLS_PREVIEW)) { + } else if (key == CategoryKeyType.TOOLS_PREVIEW) { label = "Tools (Preview Channel)"; - } else if (key.equals(KEY_EXTRA)) { + } else if (key == CategoryKeyType.EXTRA) { label = "Extras"; } else { if (mPlatformName != null) { diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgCellAgent.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgCellAgent.java new file mode 100644 index 00000000..1fadf298 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgCellAgent.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2017 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.sdkuilib.internal.repository.content; + +import java.util.List; + +import org.eclipse.andmore.sdktool.SdkContext; +import org.eclipse.swt.graphics.Font; + +import com.android.sdklib.AndroidVersion; +import org.eclipse.andmore.base.resources.ImageFactory; + +public class PkgCellAgent { + + /** Column identities */ + public static final int NAME = 1; + public static final int API = 2; + public static final int REVISION = 3; + public static final int STATUS = 4; + + private final Font mTreeFontItalic; + private final ImageFactory mImgFactory; + private final List> mCategoryList; + + public PkgCellAgent(SdkContext sdkContext, PackageAnalyser packageAnalyser, Font treeFontItalic) { + this.mTreeFontItalic = treeFontItalic; + this.mImgFactory = sdkContext.getSdkHelper().getImageFactory(); + this.mCategoryList = packageAnalyser.getApiCategories(); + } + + public Font getTreeFontItalic() + { + return mTreeFontItalic; + } + + public ImageFactory getImgFactory() { + return mImgFactory; + } + + public List> getCategoryList() { + return mCategoryList; + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgCellLabelProvider.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgCellLabelProvider.java new file mode 100644 index 00000000..dfe38289 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgCellLabelProvider.java @@ -0,0 +1,79 @@ +package com.android.sdkuilib.internal.repository.content; + +import org.eclipse.jface.viewers.ColumnLabelProvider; +import org.eclipse.jface.viewers.ITableFontProvider; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; + +import com.android.sdkuilib.internal.repository.content.INode.LabelFont; +import org.eclipse.andmore.base.resources.ImageFactory; + +public class PkgCellLabelProvider extends ColumnLabelProvider implements ITableFontProvider { + + public static final int NAME = 1; + public static final int API = 2; + public static final int REVISION = 3; + public static final int STATUS = 4; + private final int columnIndex; + private final PkgCellAgent agent; + + public PkgCellLabelProvider(PkgCellAgent agent, int columnIndex) { + super(); + this.columnIndex = columnIndex; + this.agent = agent; + } + + @Override + public String getText(Object element) { + INode node = (INode)element; + String text = node.getText(element, columnIndex); + return text != INode.VOID ? text : null; + } + + /** + * The image is owned by the label provider and must not be disposed directly. + */ + @Override + public Image getImage(Object element) { + ImageFactory imgFactory = agent.getImgFactory(); + if (imgFactory != null) { + INode node = (INode)element; + String reference = node.getImage(element, columnIndex); + if (reference != INode.VOID) + return imgFactory.getImageByName(reference); + } + return super.getImage(element); + } + + // -- ITableFontProvider + + @Override + public Font getFont(Object element, int columnIndex) { + INode node = (INode)element; + LabelFont fontType = node.getFont(element, columnIndex); + if (fontType == LabelFont.italic) + return agent.getTreeFontItalic(); + return super.getFont(element); + } + + // -- Tooltip support + @Override + public String getToolTipText(Object element) { + INode node = (INode)element; + String text = node.getToolTipText(element); + if (text != INode.VOID) + return text; + return super.getToolTipText(element); + } + + @Override + public Point getToolTipShift(Object object) { + return new Point(15, 5); + } + + @Override + public int getToolTipDisplayDelayTime(Object object) { + return 500; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgItem.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgItem.java new file mode 100644 index 00000000..6b2535eb --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgItem.java @@ -0,0 +1,342 @@ +/* + * Copyright (C) 2011 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.sdkuilib.internal.repository.content; + +import java.util.regex.Pattern; + +import org.eclipse.andmore.sdktool.Utilities; + +import com.android.SdkConstants; +import com.android.annotations.Nullable; +import com.android.repository.Revision; +import com.android.repository.api.RemotePackage; +import com.android.repository.api.RepoPackage; +import com.android.repository.api.UpdatablePackage; +import com.android.repository.impl.meta.Archive; +import com.android.repository.impl.meta.TypeDetails; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.repository.meta.DetailsTypes; +import com.android.sdkuilib.internal.repository.content.PackageAnalyser.PkgState; +import com.android.sdkuilib.internal.repository.ui.PackagesPage; +import com.android.sdkuilib.internal.repository.ui.PackagesPageIcons; + +/** + * A {@link PkgItem} represents one main {@link Package} combined with its state + * and an optional update package. + *

+ * The main package is final and cannot change since it's what "defines" this PkgItem. + * The state or update package can change later. + */ +public class PkgItem extends INode implements Comparable { + private final MetaPackage metaPackage; + private PkgState state; + private RepoPackage mainPackage; + private UpdatablePackage updatePackage; + + /** + * Create a new {@link PkgItem} for this main package. + * The main package is final and cannot change since it's what "defines" this PkgItem. + * The state or update package can change later. + */ + public PkgItem(RepoPackage mainPkg, MetaPackage metaPackage, PkgState state) { + super(); + this.mainPackage = mainPkg; + this.metaPackage = metaPackage; + this.state = state; + } + + public boolean isObsolete() { + return mainPackage.obsolete(); + } + + public UpdatablePackage getUpdatePkg() { + return updatePackage; + } + + public void setUpdatePkg(UpdatablePackage updatePkg) { + updatePackage = updatePkg; + } + + public boolean hasUpdatePkg() { + return updatePackage != null; + } + + public String getName() { + return mainPackage.getDisplayName(); + } + + public Revision getRevision() { + return mainPackage.getVersion(); + } + + public MetaPackage getMetaPackage() + { + return metaPackage; + } + + public RepoPackage getMainPackage() { + return mainPackage; + } + + public PkgState getState() { + return state; + } + + @Nullable + public AndroidVersion getAndroidVersion() { + return getAndroidVersion(mainPackage); + } + + public Archive[] getArchives() { + if (state == PkgState.NEW) + return new Archive[]{((RemotePackage)mainPackage).getArchive()}; + return new Archive[0]; + } + + /** + * Returns the image resource value for the label of the given element. + * @param element Target + * @param columnIndex The index of the column being displayed + * @return the resource value of image used to label the element + */ + @Override + public String getImage(Object element, int columnIndex) { + if (columnIndex == PackagesPage.NAME) + return metaPackage.getIconResource(); + else if (columnIndex == PackagesPage.STATUS) { + switch(state) { + case INSTALLED: + if (updatePackage != null) { + return PackagesPageIcons.ICON_PKG_UPDATE; + } else { + return PackagesPageIcons.ICON_PKG_INSTALLED; + } + case NEW: + return PackagesPageIcons.ICON_PKG_NEW; + case DELETED: + return PackagesPageIcons.ICON_PKG_INCOMPAT; + } + } + return VOID; + } + + + /** + * Returns the text for the label of the given element. + * @param element Target + * @param columnIndex The index of the column being displayed + * @return the text string used to label the element, or VOID if there is no text label for the given object + */ + @Override + public String getText(Object element, int columnIndex) { + switch (columnIndex) + { + case PackagesPage.NAME: + return getPkgItemName(); + case PackagesPage.API: + { + AndroidVersion version = getAndroidVersion(); + return version == null ? VOID : getAndroidVersion().getApiString(); + } + case PackagesPage.REVISION: + // Do not repeat version if included in name + if (Pattern.matches(".*\\d+\\.\\d+\\.\\d+.*", getPkgItemName())) + return VOID; + return mainPackage.getVersion().toString(); + case PackagesPage.STATUS: + return getStatusText(); + default: + } + return VOID; + } + + /** + * Get the text displayed in the tool tip for given element + * @param element Target + * @return the tooltop text, or VOID for no text to display + */ + @Override + public String getToolTipText(Object element) { + String s = getTooltipDescription(mainPackage); + + if ((updatePackage != null) && updatePackage.isUpdate()) { + s += "\n-----------------" + //$NON-NLS-1$ + "\nUpdate Available:\n" + //$NON-NLS-1$ + getTooltipDescription(updatePackage.getRemote()); + } + return s; + } + + /** + * Mark item as checked according to given criteria. Force uncheck if no criteria specified. + * @param selectUpdates If true, select all update packages + * @param topApiLevel If greater than 0, select platform packages of this api level + */ + @Override + public void checkSelections( + boolean selectUpdates, + int topApiLevel) + { + boolean hasUpdate = (state == PkgState.INSTALLED) && (updatePackage != null); + if (selectUpdates && hasUpdate) { + setChecked(true); + return; + } + if (topApiLevel > 0) { + if (hasUpdate || // or new packages excluding system images and previews + ((state == PkgState.NEW) && (metaPackage.getPackageType() != PackageType.system_images))) { + AndroidVersion version = getAndroidVersion(); + if ((version != null) && (version.getApiLevel() == topApiLevel) && !version.isPreview()) { + setChecked(true); + return; + } + } + } else { + setChecked(false); + } + } + + @Override + public void markDeleted() + { + state = PkgState.DELETED; + setChecked(false); + updatePackage = null; + } + + @Override + public boolean isDeleted() + { + return state == PkgState.DELETED; + } + + /** Returns a string representation of this item, useful when debugging. */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append('<'); + + if (isChecked) { + sb.append(" * "); //$NON-NLS-1$ + } + + sb.append(state.toString()); + + if (mainPackage != null) { + sb.append(", pkg:"); //$NON-NLS-1$ + sb.append(mainPackage.toString()); + } + + if (updatePackage != null) { + sb.append(", updated by:"); //$NON-NLS-1$ + sb.append(updatePackage.toString()); + } + + sb.append('>'); + return sb.toString(); + } + + @Override + public int compareTo(PkgItem other) { + if (other == null) + return Integer.MIN_VALUE; + int comparison1 = state.ordinal() - other.getState().ordinal(); + if (comparison1 != 0) + return comparison1; + if (hasUpdatePkg() && other.hasUpdatePkg()) + return updatePackage.compareTo(other.getUpdatePkg()); + return mainPackage.compareTo(other.getMainPackage()); + } + + /** + * Equality is defined as {@link #isSameItemAs(PkgItem)}: state, main package + * and update package must be the similar. + */ + @Override + public boolean equals(Object obj) { + return (obj instanceof PkgItem) && (compareTo((PkgItem) obj) == 0); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + state.hashCode(); + result = prime * result + mainPackage.hashCode(); + result = prime * result + ((updatePackage == null) ? 0 : updatePackage.hashCode()); + return result; + } + + @Nullable + public static AndroidVersion getAndroidVersion(RepoPackage repoPackage) { + TypeDetails details = repoPackage.getTypeDetails(); + if (details instanceof DetailsTypes.ApiDetailsType) { + return ((DetailsTypes.ApiDetailsType)details).getAndroidVersion(); + } + return null; + } + + public static boolean isPreview(RepoPackage repoPackage) { + TypeDetails details = repoPackage.getTypeDetails(); + if (details instanceof DetailsTypes.ApiDetailsType) { + return ((DetailsTypes.ApiDetailsType)details).getCodename() != null; + } + return false; + } + + private String getStatusText() { + switch(state) { + case INSTALLED: + if (updatePackage != null) { + return String.format( + "Update available: rev. %1$s", + updatePackage.getRemote().getVersion().toString()); + } + return "Installed"; + + case NEW: + if (((RemotePackage)mainPackage).getArchive().isCompatible()) { + return "Not installed"; + } else { + return String.format("Not compatible with %1$s", + SdkConstants.currentPlatformName()); + } + case DELETED: + return "Deleted"; + } + return state.toString(); + } + + private String getTooltipDescription(RepoPackage repoPackage) { + String s = repoPackage.getDisplayName(); + if (repoPackage instanceof RemotePackage) { + RemotePackage remote = (RemotePackage) repoPackage; + // For non-installed item get download size + long fileSize = remote.getArchive().getComplete().getSize(); + s += '\n' + Utilities.formatFileSize(fileSize); + s += String.format("\nProvided by %1$s", remote.getSource().getUrl()); + } + return s; + } + + private String getPkgItemName() { + if (metaPackage.getPackageType() == PackageType.platforms) + return "Platform SDK"; + return mainPackage.getDisplayName(); + } + + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PkgTreeColumnViewerLabelProvider.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgTreeColumnViewerLabelProvider.java similarity index 94% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PkgTreeColumnViewerLabelProvider.java rename to andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgTreeColumnViewerLabelProvider.java index 575b8a3e..30791d88 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PkgTreeColumnViewerLabelProvider.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/PkgTreeColumnViewerLabelProvider.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.sdkuilib.internal.repository.ui; +package com.android.sdkuilib.internal.repository.content; import org.eclipse.jface.viewers.CellLabelProvider; import org.eclipse.jface.viewers.ColumnLabelProvider; @@ -39,8 +39,7 @@ * delegate all the tooltip calls for completeness and avoid surprises later * if we ever decide to override more things in the label provider. */ -class PkgTreeColumnViewerLabelProvider extends TreeColumnViewerLabelProvider { - +public class PkgTreeColumnViewerLabelProvider extends TreeColumnViewerLabelProvider { private CellLabelProvider mTooltipProvider; public PkgTreeColumnViewerLabelProvider(ColumnLabelProvider columnLabelProvider) { @@ -134,4 +133,5 @@ public int getToolTipStyle(Object object) { } return super.getToolTipStyle(object); } + } diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/UpdateOp.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/UpdateOp.java new file mode 100644 index 00000000..034f3e63 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/UpdateOp.java @@ -0,0 +1,84 @@ +package com.android.sdkuilib.internal.repository.content; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.android.repository.api.RepoPackage; + +/** + * An update operation, customized to either sort by API or sort by source. + */ +public abstract class UpdateOp { + static public class SdkSource{} + + private final List> mCategories = new ArrayList<>(); + private final Set> mCatsToRemove = new HashSet<>(); + private final Set mItemsToRemove = new HashSet<>(); + private final Map mUpdatesToRemove = new HashMap<>(); + + /** Removes all internal state. */ + public void clear() { + } + + /** Retrieve the sorted category list. */ + public List> getCategories() { + return mCategories; + } + + /** Retrieve the category key type for the given package */ + public abstract CategoryKeyType getCategoryKeyType(RepoPackage pkg); + + /** Retrieve the category key value for the given package. May be null. */ + public abstract K getCategoryKeyValue(RepoPackage pkg); + + /** Modified {@code currentCategories} to add default categories. */ + public abstract void addDefaultCategories(); + + /** Creates the category for the given key and returns it. */ + public abstract PkgCategory createCategory(CategoryKeyType catKeyType, K catKeyValue); + + /** Sorts the category list (but not the items within the categories.) */ + public abstract void sortCategoryList(); + + /** Called after items of a given category have changed. Used to sort the + * items and/or adjust the category name. */ + public abstract void postCategoryItemsChanged(); + + public void updateStart() { + } + + public boolean updateSourcePackages(SdkSource source, RepoPackage[] newPackages) { + return false; + } + + public boolean updateEnd() { + return false; + } + public boolean isKeep(PkgItem item) { + return !mItemsToRemove.contains(item); + } + + public void keep(Package pkg) { + mUpdatesToRemove.remove(pkg); + } + + public void keep(PkgItem item) { + mItemsToRemove.remove(item); + } + + public void keep(PkgCategory cat) { + mCatsToRemove.remove(cat); + } + + public void dontKeep(PkgItem item) { + mItemsToRemove.add(item); + } + + public void dontKeep(PkgCategory cat) { + mCatsToRemove.add(cat); + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/UpdateOpApi.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/UpdateOpApi.java new file mode 100644 index 00000000..7ae38daf --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/content/UpdateOpApi.java @@ -0,0 +1,110 @@ +package com.android.sdkuilib.internal.repository.content; + +import java.util.Collections; +import java.util.Comparator; + +import com.android.repository.api.RepoPackage; +import com.android.sdkuilib.internal.repository.ui.PackagesPageIcons; +import com.android.sdklib.AndroidVersion; + +public class UpdateOpApi extends UpdateOp { + + @Override + public CategoryKeyType getCategoryKeyType(RepoPackage pkg) { + // Sort by API + AndroidVersion androidVersion = PkgItem.getAndroidVersion(pkg); + if (androidVersion != null) { + return CategoryKeyType.API; + + } else if (pkg.getPath().indexOf("tools") != -1) { + if (PkgItem.isPreview(pkg)) { + return CategoryKeyType.TOOLS_PREVIEW; + } else { + return CategoryKeyType.TOOLS; + } + } else { + return CategoryKeyType.EXTRA; + } + } + + @Override + public AndroidVersion getCategoryKeyValue(RepoPackage pkg) { + // Sort by API + return PkgItem.getAndroidVersion(pkg); + } + + @Override + public void addDefaultCategories() { + // TODO Auto-generated method stub + + } + + @Override + public PkgCategory createCategory(CategoryKeyType catKeyType, AndroidVersion catKeyValue) { + // Create API category. + PkgCategory cat = null; + + // We should not be trying to recreate the tools or extra categories. + //assert (catKeyType != CategoryKeyType.TOOLS) && (catKeyType != CategoryKeyType.EXTRA); + cat = new PkgCategoryApi( + catKeyType, + catKeyValue, + PackagesPageIcons.ICON_CAT_PLATFORM); + + return cat; + } + + @Override + public void sortCategoryList() { + // Sort the categories list. + // We always want categories in order tools..platforms..extras. + // For platform, we compare in descending order (o2-o1). + // This order is achieved by having the category keys ordered as + // needed for the sort to just do what we expect. + + synchronized (getCategories()) { + Collections.sort(getCategories(), new Comparator>() { + @Override + public int compare(PkgCategory cat1, PkgCategory cat2) { + int comparison1 = cat1.getKeyType().ordinal() - cat2.getKeyType().ordinal(); + if ((cat1.getKeyType() == CategoryKeyType.API) && (cat2.getKeyType() == CategoryKeyType.API)) + return cat2.getKeyValue().compareTo(cat1.getKeyValue()); + else + return comparison1; + } + }); + for (PkgCategory cat: getCategories()) + sortPackages(cat); + } + } + + @Override + public void postCategoryItemsChanged() { + // TODO Auto-generated method stub + + } + + private void sortPackages(PkgCategory cat) + { + synchronized (cat) + { + Collections.sort(cat.getItems(), new Comparator() { + + @Override + public int compare(PkgItem item1, PkgItem item2) { + int ordinal1 = item1.getMetaPackage().getPackageType().ordinal(); + int ordinal2 = item2.getMetaPackage().getPackageType().ordinal(); + int comparison1 = ordinal1 - ordinal2; + if (comparison1 != 0) + return comparison1; + String name1 = PackageAnalyser.getNameFromPath(item1.getMainPackage().getPath()); + String name2 = PackageAnalyser.getNameFromPath(item2.getMainPackage().getPath()); + int comparison2 = name1.compareTo(name2); + // Use reverse lexical order of paths for same package types to get top down version ordering + return comparison2 != 0 ? comparison2 : item2.getMainPackage().getPath().compareTo(item1.getMainPackage().getPath()); + } + + }); + } + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PackagesDiffLogic.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PackagesDiffLogic.java deleted file mode 100644 index 5617898e..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PackagesDiffLogic.java +++ /dev/null @@ -1,859 +0,0 @@ -/* - * Copyright (C) 2011 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.sdkuilib.internal.repository.core; - -import com.android.SdkConstants; -import com.android.sdklib.AndroidVersion; -import com.android.sdklib.IAndroidTarget; -import com.android.sdklib.internal.repository.packages.BuildToolPackage; -import com.android.sdklib.internal.repository.packages.ExtraPackage; -import com.android.sdklib.internal.repository.packages.IAndroidVersionProvider; -import com.android.sdklib.internal.repository.packages.IFullRevisionProvider; -import com.android.sdklib.internal.repository.packages.Package; -import com.android.sdklib.internal.repository.packages.Package.UpdateInfo; -import com.android.sdklib.internal.repository.packages.PlatformPackage; -import com.android.sdklib.internal.repository.packages.PlatformToolPackage; -import com.android.sdklib.internal.repository.packages.SystemImagePackage; -import com.android.sdklib.internal.repository.packages.ToolPackage; -import com.android.sdklib.internal.repository.sources.SdkSource; -import com.android.sdklib.internal.repository.updater.PkgItem; -import com.android.sdklib.internal.repository.updater.PkgItem.PkgState; -import com.android.sdklib.repository.FullRevision; -import com.android.sdklib.repository.FullRevision.PreviewComparison; -import com.android.sdkuilib.internal.repository.SwtUpdaterData; -import com.android.sdkuilib.internal.repository.ui.PackagesPageIcons; -import com.android.utils.Pair; -import com.android.utils.SparseArray; -import com.google.common.collect.Maps; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Helper class that separates the logic of package management from the UI - * so that we can test it using head-less unit tests. - */ -public class PackagesDiffLogic { - private final SwtUpdaterData mUpdaterData; - private boolean mFirstLoadComplete = true; - - public PackagesDiffLogic(SwtUpdaterData swtUpdaterData) { - mUpdaterData = swtUpdaterData; - } - - /** - * Removes all the internal state and resets the object. - * Useful for testing. - */ - public void clear() { - mFirstLoadComplete = true; - mOpApi.clear(); - } - - /** Return mFirstLoadComplete and resets it to false. - * All following calls will returns false. */ - public boolean isFirstLoadComplete() { - boolean b = mFirstLoadComplete; - mFirstLoadComplete = false; - return b; - } - - /** - * Mark all new and update PkgItems as checked. - * - * @param selectNew If true, select all new packages (except the rc/preview ones). - * @param selectUpdates If true, select all update packages. - * @param selectTop If true, select the top platform. - * If the top platform has nothing installed, select all items in it (except the rc/preview); - * If it is partially installed, at least select the platform and system images if none of - * the system images are installed. - * @param currentPlatform The {@link SdkConstants#currentPlatform()} value. - */ - public void checkNewUpdateItems( - boolean selectNew, - boolean selectUpdates, - boolean selectTop, - int currentPlatform) { - int maxApi = 0; - Set installedPlatforms = new HashSet(); - SparseArray> platformItems = new SparseArray>(); - - boolean hasTools = false; - Map, Pair> toolsCandidates = Maps.newHashMap(); - toolsCandidates.put(PlatformToolPackage.class, Pair.of((PkgItem)null, (FullRevision)null)); - toolsCandidates.put(BuildToolPackage.class, Pair.of((PkgItem)null, (FullRevision)null)); - - // sort items in platforms... directly deal with new/update items - List allItems = getAllPkgItems(); - for (PkgItem item : allItems) { - if (!item.hasCompatibleArchive()) { - // Ignore items that have no archive compatible with the current platform. - continue; - } - - // Get the main package's API level. We don't need to look at the updates - // since by definition they should target the same API level. - int api = 0; - Package p = item.getMainPackage(); - if (p instanceof IAndroidVersionProvider) { - api = ((IAndroidVersionProvider) p).getAndroidVersion().getApiLevel(); - } - - if (selectTop && api > 0) { - // Keep track of the max api seen - maxApi = Math.max(maxApi, api); - - // keep track of what platform is currently installed (that is, has at least - // one thing installed.) - if (item.getState() == PkgState.INSTALLED) { - installedPlatforms.add(api); - } - - // for each platform, collect all its related item for later use below. - List items = platformItems.get(api); - if (items == null) { - platformItems.put(api, items = new ArrayList()); - } - items.add(item); - } - - if ((selectUpdates || selectNew) && - item.getState() == PkgState.NEW && - !item.getRevision().isPreview()) { - boolean sameFound = false; - Package newPkg = item.getMainPackage(); - if (newPkg instanceof IFullRevisionProvider) { - // We have a potential new non-preview package; but this kind of package - // supports having previews, which means we want to make sure we're not - // offering an older "new" non-preview if there's a newer preview installed. - // - // We should get into this odd situation only when updating an RC/preview - // by a final release pkg. - - IFullRevisionProvider newPkg2 = (IFullRevisionProvider) newPkg; - for (PkgItem item2 : allItems) { - if (item2.getState() == PkgState.INSTALLED) { - Package installed = item2.getMainPackage(); - - if (installed.getRevision().isPreview() && - newPkg2.sameItemAs(installed, PreviewComparison.IGNORE)) { - sameFound = true; - - if (installed.canBeUpdatedBy(newPkg) == UpdateInfo.UPDATE) { - item.setChecked(true); - break; - } - } - } - } - } - - if (selectNew && !sameFound) { - item.setChecked(true); - } - - } else if (selectUpdates && item.hasUpdatePkg()) { - item.setChecked(true); - } - - // Keep track of the tools and offer to auto-select platform-tools/build-tools. - if (selectTop) { - if (p instanceof ToolPackage && p.isLocal()) { - hasTools = true; // main tool package is installed. - } else if (p instanceof PlatformToolPackage || p instanceof BuildToolPackage) { - for (Class clazz : toolsCandidates.keySet()) { - if (clazz.isInstance(p)) { // allow p to be a mock-derived class - if (p.isLocal()) { - // There's one such package installed, we don't need candidates. - toolsCandidates.remove(clazz); - } else if (toolsCandidates.containsKey(clazz)) { - Pair val = toolsCandidates.get(clazz); - FullRevision rev = p.getRevision(); - if (!rev.isPreview()) { - // Don't auto-select previews. - if (val.getSecond() == null || - rev.compareTo(val.getSecond()) > 0) { - // No revision: set the first candidate. - // Or we found a new higher revision - toolsCandidates.put(clazz, Pair.of(item, rev)); - } - } - } - break; - } - } - } - } - } - - // Select the top platform/build-tool found above if needed. - if (selectTop && hasTools) { - for (Pair candidate : toolsCandidates.values()) { - PkgItem item = candidate.getFirst(); - if (item != null) { - item.setChecked(true); - } - } - } - - - // Select top platform items. - - List items = platformItems.get(maxApi); - if (selectTop && maxApi > 0 && items != null) { - if (!installedPlatforms.contains(maxApi)) { - // If the top platform has nothing installed at all, select everything in it - for (PkgItem item : items) { - if ((item.getState() == PkgState.NEW && !item.getRevision().isPreview()) || - item.hasUpdatePkg()) { - item.setChecked(true); - } - } - - } else { - // The top platform has at least one thing installed. - - // First make sure the platform package itself is installed, or select it. - for (PkgItem item : items) { - Package p = item.getMainPackage(); - if (p instanceof PlatformPackage && - item.getState() == PkgState.NEW && !item.getRevision().isPreview()) { - item.setChecked(true); - break; - } - } - - // Check we have at least one system image installed, otherwise select them - boolean hasSysImg = false; - for (PkgItem item : items) { - Package p = item.getMainPackage(); - if (p instanceof PlatformPackage && item.getState() == PkgState.INSTALLED) { - if (item.hasUpdatePkg() && item.isChecked()) { - // If the installed platform is scheduled for update, look for the - // system image in the update package, not the current one. - p = item.getUpdatePkg(); - if (p instanceof PlatformPackage) { - hasSysImg = ((PlatformPackage) p).getIncludedAbi() != null; - } - } else { - // Otherwise look into the currently installed platform - hasSysImg = ((PlatformPackage) p).getIncludedAbi() != null; - } - if (hasSysImg) { - break; - } - } - if (p instanceof SystemImagePackage && - ((SystemImagePackage) p).isPlatform() && - item.getState() == PkgState.INSTALLED) { - hasSysImg = true; - break; - } - } - if (!hasSysImg) { - // No system image installed. - // Try whether the current platform or its update would bring one. - - for (PkgItem item : items) { - Package p = item.getMainPackage(); - if (p instanceof PlatformPackage) { - if (item.getState() == PkgState.NEW && - !item.getRevision().isPreview() && - ((PlatformPackage) p).getIncludedAbi() != null) { - item.setChecked(true); - hasSysImg = true; - } else if (item.hasUpdatePkg()) { - p = item.getUpdatePkg(); - if (p instanceof PlatformPackage && - ((PlatformPackage) p).getIncludedAbi() != null) { - item.setChecked(true); - hasSysImg = true; - } - } - } - } - } - if (!hasSysImg) { - // No system image in the platform, try a system image package - for (PkgItem item : items) { - Package p = item.getMainPackage(); - if (p instanceof SystemImagePackage && - ((SystemImagePackage) p).isPlatform() && - item.getState() == PkgState.NEW) { - item.setChecked(true); - } - } - } - } - } - - if (selectTop) { - for (PkgItem item : getAllPkgItems()) { - Package p = item.getMainPackage(); - if (p instanceof ExtraPackage && - item.getState() == PkgState.NEW && - !item.getRevision().isPreview()) { - ExtraPackage ep = (ExtraPackage) p; - - // On Windows, we'll also auto-select the USB driver - if (currentPlatform == SdkConstants.PLATFORM_WINDOWS) { - if (ep.getVendorId().equals("google") && //$NON-NLS-1$ - ep.getPath().equals("usb_driver")) { //$NON-NLS-1$ - item.setChecked(true); - continue; - } - } - - // On all platforms, we'll auto-select the support library. - if (ep.getVendorId().equals("android") && //$NON-NLS-1$ - ep.getPath().equals("support")) { //$NON-NLS-1$ - item.setChecked(true); - continue; - } - - } - } - } - } - - /** - * Mark all PkgItems as not checked. - */ - public void uncheckAllItems() { - for (PkgItem item : getAllPkgItems()) { - item.setChecked(false); - } - } - - /** - * An update operation, customized to either sort by API or sort by source. - */ - abstract class UpdateOp { - private final Set mVisitedSources = new HashSet(); - private final List mCategories = new ArrayList(); - private final Set mCatsToRemove = new HashSet(); - private final Set mItemsToRemove = new HashSet(); - private final Map mUpdatesToRemove = new HashMap(); - - /** Removes all internal state. */ - public void clear() { - mVisitedSources.clear(); - mCategories.clear(); - } - - /** Retrieve the sorted category list. */ - public List getCategories() { - return mCategories; - } - - /** Retrieve the category key for the given package, either local or remote. */ - public abstract Object getCategoryKey(Package pkg); - - /** Modified {@code currentCategories} to add default categories. */ - public abstract void addDefaultCategories(); - - /** Creates the category for the given key and returns it. */ - public abstract PkgCategory createCategory(Object catKey); - /** Adjust attributes of an existing category. */ - public abstract void adjustCategory(PkgCategory cat, Object catKey); - - /** Sorts the category list (but not the items within the categories.) */ - public abstract void sortCategoryList(); - - /** Called after items of a given category have changed. Used to sort the - * items and/or adjust the category name. */ - public abstract void postCategoryItemsChanged(); - - public void updateStart() { - mVisitedSources.clear(); - - // Note that default categories are created after the unused ones so that - // the callback can decide whether they should be marked as unused or not. - mCatsToRemove.clear(); - mItemsToRemove.clear(); - mUpdatesToRemove.clear(); - for (PkgCategory cat : mCategories) { - mCatsToRemove.add(cat); - List items = cat.getItems(); - mItemsToRemove.addAll(items); - for (PkgItem item : items) { - if (item.hasUpdatePkg()) { - mUpdatesToRemove.put(item.getUpdatePkg(), item); - } - } - } - - addDefaultCategories(); - } - - public boolean updateSourcePackages(SdkSource source, Package[] newPackages) { - mVisitedSources.add(source); - if (source == null) { - return processLocals(this, newPackages); - } else { - return processSource(this, source, newPackages); - } - } - - public boolean updateEnd() { - boolean hasChanged = false; - - // Remove unused categories & items at the end of the update - synchronized (mCategories) { - for (PkgCategory unusedCat : mCatsToRemove) { - if (mCategories.remove(unusedCat)) { - hasChanged = true; - } - } - } - - for (PkgCategory cat : mCategories) { - for (Iterator itemIt = cat.getItems().iterator(); itemIt.hasNext(); ) { - PkgItem item = itemIt.next(); - if (mItemsToRemove.contains(item)) { - itemIt.remove(); - hasChanged = true; - } else if (item.hasUpdatePkg() && - mUpdatesToRemove.containsKey(item.getUpdatePkg())) { - item.removeUpdate(); - hasChanged = true; - } - } - } - - mCatsToRemove.clear(); - mItemsToRemove.clear(); - mUpdatesToRemove.clear(); - - return hasChanged; - } - - public boolean isKeep(PkgItem item) { - return !mItemsToRemove.contains(item); - } - - public void keep(Package pkg) { - mUpdatesToRemove.remove(pkg); - } - - public void keep(PkgItem item) { - mItemsToRemove.remove(item); - } - - public void keep(PkgCategory cat) { - mCatsToRemove.remove(cat); - } - - public void dontKeep(PkgItem item) { - mItemsToRemove.add(item); - } - - public void dontKeep(PkgCategory cat) { - mCatsToRemove.add(cat); - } - } - - private final UpdateOpApi mOpApi = new UpdateOpApi(); - - public List getCategories() { - return mOpApi.getCategories(); - } - - public List getAllPkgItems() { - List items = new ArrayList(); - - List cats = getCategories(); - synchronized (cats) { - for (PkgCategory cat : cats) { - items.addAll(cat.getItems()); - } - } - - return items; - } - - public void updateStart() { - mOpApi.updateStart(); - } - - public boolean updateSourcePackages(SdkSource source, Package[] newPackages) { - - return mOpApi.updateSourcePackages(source, newPackages); - } - - public boolean updateEnd() { - return mOpApi.updateEnd(); - } - - - /** Process all local packages. Returns true if something changed. */ - private boolean processLocals(UpdateOp op, Package[] packages) { - boolean hasChanged = false; - List cats = op.getCategories(); - Set keep = new HashSet(); - - // For all locally installed packages, check they are either listed - // as installed or create new installed items for them. - - nextPkg: for (Package localPkg : packages) { - // Check to see if we already have the exact same package - // (type & revision) marked as installed. - for (PkgCategory cat : cats) { - for (PkgItem currItem : cat.getItems()) { - if (currItem.getState() == PkgState.INSTALLED && - currItem.isSameMainPackageAs(localPkg)) { - // This package is already listed as installed. - op.keep(currItem); - op.keep(cat); - keep.add(currItem); - continue nextPkg; - } - } - } - - // If not found, create a new installed package item - keep.add(addNewItem(op, localPkg, PkgState.INSTALLED)); - hasChanged = true; - } - - // Remove installed items that we don't want to keep anymore. They would normally be - // cleanup up in UpdateOp.updateEnd(); however it's easier to remove them before we - // run processSource() to avoid merging updates in items that would be removed later. - - for (PkgCategory cat : cats) { - for (Iterator itemIt = cat.getItems().iterator(); itemIt.hasNext(); ) { - PkgItem item = itemIt.next(); - if (item.getState() == PkgState.INSTALLED && !keep.contains(item)) { - itemIt.remove(); - hasChanged = true; - } - } - } - - if (hasChanged) { - op.postCategoryItemsChanged(); - } - - return hasChanged; - } - - /** - * {@link PkgState}s to check in {@link #processSource(UpdateOp, SdkSource, Package[])}. - * The order matters. - * When installing the diff will have both the new and the installed item and we - * need to merge with the installed one before the new one. - */ - private final static PkgState[] PKG_STATES = { PkgState.INSTALLED, PkgState.NEW }; - - /** Process all remote packages. Returns true if something changed. */ - private boolean processSource(UpdateOp op, SdkSource source, Package[] packages) { - boolean hasChanged = false; - List cats = op.getCategories(); - - boolean enablePreviews = - mUpdaterData.getSettingsController().getSettings().getEnablePreviews(); - - nextPkg: for (Package newPkg : packages) { - - if (!enablePreviews && newPkg.getRevision().isPreview()) { - // This is a preview and previews are not enabled. Ignore the package. - // Starting with Tools 23, we explicitly allows Build-Tools RC packages to - // always be visible so only RCs for Tools & Platform-Tools will be hidden. - if (!(newPkg instanceof BuildToolPackage)) { - continue nextPkg; - } - } - - for (PkgCategory cat : cats) { - for (PkgState state : PKG_STATES) { - for (Iterator currItemIt = cat.getItems().iterator(); - currItemIt.hasNext(); ) { - PkgItem currItem = currItemIt.next(); - // We need to merge with installed items first. When installing - // the diff will have both the new and the installed item and we - // need to merge with the installed one before the new one. - if (currItem.getState() != state) { - continue; - } - // Only process current items if they represent the same item (but - // with a different revision number) than the new package. - Package mainPkg = currItem.getMainPackage(); - if (!mainPkg.sameItemAs(newPkg)) { - continue; - } - - // Check to see if we already have the exact same package - // (type & revision) marked as main or update package. - if (currItem.isSameMainPackageAs(newPkg)) { - op.keep(currItem); - op.keep(cat); - continue nextPkg; - } else if (currItem.hasUpdatePkg() && - currItem.isSameUpdatePackageAs(newPkg)) { - op.keep(currItem.getUpdatePkg()); - op.keep(cat); - continue nextPkg; - } - - switch (currItem.getState()) { - case NEW: - if (newPkg.getRevision().compareTo(mainPkg.getRevision()) < 0) { - if (!op.isKeep(currItem)) { - // The new item has a lower revision than the current one, - // but the current one hasn't been marked as being kept so - // it's ok to downgrade it. - currItemIt.remove(); - addNewItem(op, newPkg, PkgState.NEW); - hasChanged = true; - } - } else if (newPkg.getRevision().compareTo(mainPkg.getRevision()) > 0) { - // We have a more recent new version, remove the current one - // and replace by a new one - currItemIt.remove(); - addNewItem(op, newPkg, PkgState.NEW); - hasChanged = true; - } - break; - case INSTALLED: - // if newPkg.revision<=mainPkg.revision: it's already installed, ignore. - if (newPkg.getRevision().compareTo(mainPkg.getRevision()) > 0) { - // This is a new update for the main package. - if (currItem.mergeUpdate(newPkg)) { - op.keep(currItem.getUpdatePkg()); - op.keep(cat); - hasChanged = true; - } - } - break; - } - continue nextPkg; - } - } - } - // If not found, create a new package item - addNewItem(op, newPkg, PkgState.NEW); - hasChanged = true; - } - - if (hasChanged) { - op.postCategoryItemsChanged(); - } - - return hasChanged; - } - - private PkgItem addNewItem(UpdateOp op, Package pkg, PkgState state) { - List cats = op.getCategories(); - Object catKey = op.getCategoryKey(pkg); - PkgCategory cat = findCurrentCategory(cats, catKey); - - if (cat == null) { - // This is a new category. Create it and add it to the list. - cat = op.createCategory(catKey); - synchronized (cats) { - cats.add(cat); - } - op.sortCategoryList(); - } else { - // Not a new category. Give op a chance to adjust the category attributes - op.adjustCategory(cat, catKey); - } - - PkgItem item = new PkgItem(pkg, state); - op.keep(item); - cat.getItems().add(item); - op.keep(cat); - return item; - } - - private PkgCategory findCurrentCategory( - List currentCategories, - Object categoryKey) { - for (PkgCategory cat : currentCategories) { - if (cat.getKey().equals(categoryKey)) { - return cat; - } - } - return null; - } - - /** - * {@link UpdateOp} describing the Sort-by-API operation. - */ - private class UpdateOpApi extends UpdateOp { - @Override - public Object getCategoryKey(Package pkg) { - // Sort by API - - if (pkg instanceof IAndroidVersionProvider) { - return ((IAndroidVersionProvider) pkg).getAndroidVersion(); - - } else if (pkg instanceof ToolPackage || - pkg instanceof PlatformToolPackage || - pkg instanceof BuildToolPackage) { - if (pkg.getRevision().isPreview()) { - return PkgCategoryApi.KEY_TOOLS_PREVIEW; - } else { - return PkgCategoryApi.KEY_TOOLS; - } - } else { - return PkgCategoryApi.KEY_EXTRA; - } - } - - @Override - public void addDefaultCategories() { - boolean needTools = true; - boolean needExtras = true; - - List cats = getCategories(); - for (PkgCategory cat : cats) { - if (cat.getKey().equals(PkgCategoryApi.KEY_TOOLS)) { - // Mark them as not unused to prevent their removal in updateEnd(). - keep(cat); - needTools = false; - } else if (cat.getKey().equals(PkgCategoryApi.KEY_EXTRA)) { - keep(cat); - needExtras = false; - } - } - - // Always add the tools & extras categories, even if empty (unlikely anyway) - if (needTools) { - PkgCategoryApi acat = new PkgCategoryApi( - PkgCategoryApi.KEY_TOOLS, - null, - mUpdaterData.getImageFactory().getImageByName(PackagesPageIcons.ICON_CAT_OTHER)); - synchronized (cats) { - cats.add(acat); - } - } - - if (needExtras) { - PkgCategoryApi acat = new PkgCategoryApi( - PkgCategoryApi.KEY_EXTRA, - null, - mUpdaterData.getImageFactory().getImageByName(PackagesPageIcons.ICON_CAT_OTHER)); - synchronized (cats) { - cats.add(acat); - } - } - } - - @Override - public PkgCategory createCategory(Object catKey) { - // Create API category. - PkgCategory cat = null; - - assert catKey instanceof AndroidVersion; - AndroidVersion key = (AndroidVersion) catKey; - - // We should not be trying to recreate the tools or extra categories. - assert !key.equals(PkgCategoryApi.KEY_TOOLS) && !key.equals(PkgCategoryApi.KEY_EXTRA); - - // We need a label for the category. - // If we have an API level, try to get the info from the SDK Manager. - // If we don't (e.g. when installing a new platform that isn't yet available - // locally in the SDK Manager), it's OK we'll try to find the first platform - // package available. - String platformName = null; - for (IAndroidTarget target : - mUpdaterData.getSdkManager().getTargets()) { - if (target.isPlatform() && key.equals(target.getVersion())) { - platformName = target.getVersionName(); - break; - } - } - - cat = new PkgCategoryApi( - key, - platformName, - mUpdaterData.getImageFactory().getImageByName(PackagesPageIcons.ICON_CAT_PLATFORM)); - - return cat; - } - - @Override - public void adjustCategory(PkgCategory cat, Object catKey) { - // Pass. Nothing to do for API-sorted categories - } - - @Override - public void sortCategoryList() { - // Sort the categories list. - // We always want categories in order tools..platforms..extras. - // For platform, we compare in descending order (o2-o1). - // This order is achieved by having the category keys ordered as - // needed for the sort to just do what we expect. - - synchronized (getCategories()) { - Collections.sort(getCategories(), new Comparator() { - @Override - public int compare(PkgCategory cat1, PkgCategory cat2) { - assert cat1 instanceof PkgCategoryApi; - assert cat2 instanceof PkgCategoryApi; - assert cat1.getKey() instanceof AndroidVersion; - assert cat2.getKey() instanceof AndroidVersion; - AndroidVersion v1 = (AndroidVersion) cat1.getKey(); - AndroidVersion v2 = (AndroidVersion) cat2.getKey(); - return v2.compareTo(v1); - } - }); - } - } - - @Override - public void postCategoryItemsChanged() { - // Sort the items - for (PkgCategory cat : getCategories()) { - Collections.sort(cat.getItems()); - - // When sorting by API, we can't always get the platform name - // from the package manager. In this case at the very end we - // look for a potential platform package we can use to extract - // the platform version name (e.g. '1.5') from the first suitable - // platform package we can find. - - assert cat instanceof PkgCategoryApi; - PkgCategoryApi pac = (PkgCategoryApi) cat; - if (pac.getPlatformName() == null) { - // Check whether we can get the actual platform version name (e.g. "1.5") - // from the first Platform package we find in this category. - - for (PkgItem item : cat.getItems()) { - Package p = item.getMainPackage(); - if (p instanceof PlatformPackage) { - String platformName = ((PlatformPackage) p).getVersionName(); - if (platformName != null) { - pac.setPlatformName(platformName); - break; - } - } - } - } - } - - } - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgCategory.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgCategory.java deleted file mode 100644 index 7eb57e67..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgCategory.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (C) 2011 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.sdkuilib.internal.repository.core; - - -import com.android.sdklib.internal.repository.updater.PkgItem; - -import java.util.ArrayList; -import java.util.List; - -public abstract class PkgCategory { - private final Object mKey; - private final Object mIconRef; - private final List mItems = new ArrayList(); - private String mLabel; - - public PkgCategory(Object key, String label, Object iconRef) { - mKey = key; - mLabel = label; - mIconRef = iconRef; - } - - public Object getKey() { - return mKey; - } - - public String getLabel() { - return mLabel; - } - - public void setLabel(String label) { - mLabel = label; - } - - public Object getIconRef() { - return mIconRef; - } - - public List getItems() { - return mItems; - } - - @Override - public String toString() { - return String.format("%s ", - this.getClass().getSimpleName(), - mKey == null ? "null" : mKey.toString(), - mLabel, - mItems.size()); - } - - /** {@link PkgCategory}s are equal if their internal keys are equal. */ - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((mKey == null) ? 0 : mKey.hashCode()); - return result; - } - - /** {@link PkgCategory}s are equal if their internal keys are equal. */ - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null) return false; - if (getClass() != obj.getClass()) return false; - PkgCategory other = (PkgCategory) obj; - if (mKey == null) { - if (other.mKey != null) return false; - } else if (!mKey.equals(other.mKey)) return false; - return true; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgCategorySource.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgCategorySource.java deleted file mode 100644 index 463049ce..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgCategorySource.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2011 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.sdkuilib.internal.repository.core; - -import com.android.sdklib.internal.repository.sources.SdkRepoSource; -import com.android.sdklib.internal.repository.sources.SdkSource; -import com.android.sdkuilib.internal.repository.SwtUpdaterData; -import com.android.sdkuilib.internal.repository.ui.PackagesPageIcons; - - -public class PkgCategorySource extends PkgCategory { - - /** - * A special {@link SdkSource} object that represents the locally installed - * items, or more exactly a lack of remote source. - */ - public final static SdkSource UNKNOWN_SOURCE = - new SdkRepoSource("http://no.source", "Local Packages"); - private final SdkSource mSource; - - /** - * Creates a new {@link PkgCategorySource}. - * This uses {@link SdkSource#toString()} to get the source's description. - * Note that if the name of the source isn't known, the description will use its URL. - */ - public PkgCategorySource(SdkSource source, SwtUpdaterData swtUpdaterData) { - super( - source, // the source is the key and it can be null - source == UNKNOWN_SOURCE ? "Local Packages" : source.toString(), - source == UNKNOWN_SOURCE ? - swtUpdaterData.getImageFactory() - .getImageByName(PackagesPageIcons.ICON_PKG_INSTALLED) : - source); - mSource = source; - } - - @Override - public String toString() { - return String.format("%s ", - this.getClass().getSimpleName(), - mSource.toString(), - getItems().size()); - } - - public SdkSource getSource() { - return mSource; - } - - /** Sets the label to match the source's UI name if the label wasn't already set. */ - public void adjustLabel(SdkSource source) { - if (getLabel() == null || getLabel().startsWith("http")) { //$NON-NLS-1$ - setLabel(source == UNKNOWN_SOURCE ? "Local Packages" : source.toString()); - } - } - -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgContentProvider.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgContentProvider.java deleted file mode 100644 index 34bf17de..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/PkgContentProvider.java +++ /dev/null @@ -1,237 +0,0 @@ -/* - * Copyright (C) 2012 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.sdkuilib.internal.repository.core; - -import com.android.sdklib.internal.repository.archives.Archive; -import com.android.sdklib.internal.repository.packages.Package; -import com.android.sdklib.internal.repository.sources.SdkSource; -import com.android.sdklib.internal.repository.updater.PkgItem; -import com.android.sdklib.repository.IDescription; -import com.android.sdkuilib.internal.repository.ui.PackagesPage; - -import org.eclipse.jface.viewers.IInputProvider; -import org.eclipse.jface.viewers.ITreeContentProvider; -import org.eclipse.jface.viewers.Viewer; - -import java.util.ArrayList; -import java.util.List; - -/** - * Content provider for the main tree view in {@link PackagesPage}. - */ -public class PkgContentProvider implements ITreeContentProvider { - - private final IInputProvider mViewer; - private boolean mDisplayArchives; - - public PkgContentProvider(IInputProvider viewer) { - mViewer = viewer; - } - - public void setDisplayArchives(boolean displayArchives) { - mDisplayArchives = displayArchives; - } - - @Override - public Object[] getChildren(Object parentElement) { - if (parentElement instanceof ArrayList) { - return ((ArrayList) parentElement).toArray(); - - } else if (parentElement instanceof PkgCategorySource) { - return getSourceChildren((PkgCategorySource) parentElement); - - } else if (parentElement instanceof PkgCategory) { - return ((PkgCategory) parentElement).getItems().toArray(); - - } else if (parentElement instanceof PkgItem) { - if (mDisplayArchives) { - - Package pkg = ((PkgItem) parentElement).getUpdatePkg(); - - // Display update packages as sub-items if the details mode is activated. - if (pkg != null) { - return new Object[] { pkg }; - } - - return ((PkgItem) parentElement).getArchives(); - } - - } else if (parentElement instanceof Package) { - if (mDisplayArchives) { - return ((Package) parentElement).getArchives(); - } - - } - - return new Object[0]; - } - - @Override - @SuppressWarnings("unchecked") - public Object getParent(Object element) { - // This operation is expensive, so we do the minimum - // and don't try to cover all cases. - - if (element instanceof PkgItem) { - Object input = mViewer.getInput(); - if (input != null) { - for (PkgCategory cat : (List) input) { - if (cat.getItems().contains(element)) { - return cat; - } - } - } - } - - return null; - } - - @Override - public boolean hasChildren(Object parentElement) { - if (parentElement instanceof ArrayList) { - return true; - - } else if (parentElement instanceof PkgCategory) { - return true; - - } else if (parentElement instanceof PkgItem) { - if (mDisplayArchives) { - Package pkg = ((PkgItem) parentElement).getUpdatePkg(); - - // Display update packages as sub-items if the details mode is activated. - if (pkg != null) { - return true; - } - - Archive[] archives = ((PkgItem) parentElement).getArchives(); - return archives.length > 0; - } - } else if (parentElement instanceof Package) { - if (mDisplayArchives) { - return ((Package) parentElement).getArchives().length > 0; - } - } - - return false; - } - - @Override - public Object[] getElements(Object inputElement) { - return getChildren(inputElement); - } - - @Override - public void dispose() { - // unused - - } - - @Override - public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { - // unused - } - - - private Object[] getSourceChildren(PkgCategorySource parentElement) { - List children = parentElement.getItems(); - - SdkSource source = parentElement.getSource(); - IDescription error = null; - IDescription empty = null; - - String errStr = source.getFetchError(); - if (errStr != null) { - error = new RepoSourceError(source); - } - if (!source.isEnabled() || children.isEmpty()) { - empty = new RepoSourceNotification(source); - } - - if (error != null || empty != null) { - ArrayList children2 = new ArrayList(); - if (error != null) { - children2.add(error); - } - if (empty != null) { - children2.add(empty); - } - children2.addAll(children); - children = children2; - } - - return children.toArray(); - } - - - /** - * A dummy entry returned for sources which had load errors. - * It displays a summary of the error as its short description or - * it displays the source's long description. - */ - public static class RepoSourceError implements IDescription { - - private final SdkSource mSource; - - public RepoSourceError(SdkSource source) { - mSource = source; - } - - @Override - public String getLongDescription() { - return mSource.getLongDescription(); - } - - @Override - public String getShortDescription() { - return mSource.getFetchError(); - } - } - - /** - * A dummy entry returned for sources with no packages. - * We need that to force the SWT tree to display an open/close triangle - * even for empty sources. - */ - public static class RepoSourceNotification implements IDescription { - - private final SdkSource mSource; - - public RepoSourceNotification(SdkSource source) { - mSource = source; - } - - @Override - public String getLongDescription() { - if (mSource.isEnabled()) { - return mSource.getLongDescription(); - } else { - return "Loading from this site has been disabled. " + - "To enable it, use Tools > Manage Add-ons Sites."; - } - } - - @Override - public String getShortDescription() { - if (mSource.isEnabled()) { - return "No packages found."; - } else { - return "This site is disabled. "; - } - } - } - -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/SdkLogAdapter.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/SdkLogAdapter.java deleted file mode 100644 index cbc33950..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/SdkLogAdapter.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright (C) 2011 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.sdkuilib.internal.repository.core; - -import com.android.sdkuilib.internal.tasks.ILogUiProvider; -import com.android.utils.ILogger; - - -/** - * Adapter that transform log from an {@link ILogUiProvider} to an {@link ILogger}. - */ -public final class SdkLogAdapter implements ILogUiProvider { - - private ILogger mSdkLog; - private String mLastLogMsg; - - /** - * Creates a new adapter to output log on the given {@code sdkLog}. - * - * @param sdkLog The logger to output to. Must not be null. - */ - public SdkLogAdapter(ILogger sdkLog) { - mSdkLog = sdkLog; - } - - /** - * Sets the description in the current task dialog. - * This method can be invoked from a non-UI thread. - */ - @Override - public void setDescription(final String description) { - if (acceptLog(description)) { - mSdkLog.info("%1$s", description); //$NON-NLS-1$ - } - } - - /** - * Logs a "normal" information line. - * This method can be invoked from a non-UI thread. - */ - @Override - public void log(String log) { - if (acceptLog(log)) { - mSdkLog.info(" %1$s", log); //$NON-NLS-1$ - } - } - - /** - * Logs an "error" information line. - * This method can be invoked from a non-UI thread. - */ - @Override - public void logError(String log) { - if (acceptLog(log)) { - mSdkLog.error(null, " %1$s", log); //$NON-NLS-1$ - } - } - - /** - * Logs a "verbose" information line, that is extra details which are typically - * not that useful for the end-user and might be hidden until explicitly shown. - * This method can be invoked from a non-UI thread. - */ - @Override - public void logVerbose(String log) { - if (acceptLog(log)) { - mSdkLog.verbose(" %1$s", log); //$NON-NLS-1$ - } - } - - // ---- - - /** - * Filter messages displayed in the log:
- * - Messages with a % are typical part of a progress update and shouldn't be in the log.
- * - Messages that are the same as the same output message should be output a second time. - * - * @param msg The potential log line to print. - * @return True if the log line should be printed, false otherwise. - */ - private boolean acceptLog(String msg) { - if (msg == null) { - return false; - } - - msg = msg.trim(); - if (msg.indexOf('%') != -1) { - return false; - } - - if (msg.equals(mLastLogMsg)) { - return false; - } - - mLastLogMsg = msg; - return true; - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/SwtPackageLoader.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/SwtPackageLoader.java deleted file mode 100644 index d5e7982c..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/core/SwtPackageLoader.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 2013 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.sdkuilib.internal.repository.core; - -import com.android.annotations.NonNull; -import com.android.sdklib.internal.repository.DownloadCache; -import com.android.sdklib.internal.repository.updater.PackageLoader; -import com.android.sdkuilib.internal.repository.SwtUpdaterData; - -import org.eclipse.swt.widgets.Display; -import org.eclipse.swt.widgets.Shell; - -/** - * Loads packages fetched from the remote SDK Repository and keeps track - * of their state compared with the current local SDK installation. - */ -public class SwtPackageLoader extends PackageLoader { - - /** - * Creates a new PackageManager associated with the given {@link SwtUpdaterData} - * and using the {@link SwtUpdaterData}'s default {@link DownloadCache}. - * - * @param swtUpdaterData The {@link SwtUpdaterData}. Must not be null. - */ - public SwtPackageLoader(SwtUpdaterData swtUpdaterData) { - super(swtUpdaterData); - } - - /** - * Creates a new PackageManager associated with the given {@link SwtUpdaterData} - * but using the specified {@link DownloadCache} instead of the one from - * {@link SwtUpdaterData}. - * - * @param swtUpdaterData The {@link SwtUpdaterData}. Must not be null. - * @param cache The {@link DownloadCache} to use instead of the one from {@link SwtUpdaterData}. - */ - public SwtPackageLoader(SwtUpdaterData swtUpdaterData, DownloadCache cache) { - super(swtUpdaterData, cache); - } - - /** - * Runs the runnable on the UI thread using {@link Display#syncExec(Runnable)}. - * - * @param r Non-null runnable. - */ - @Override - protected void runOnUiThread(@NonNull Runnable r) { - SwtUpdaterData swtUpdaterData = (SwtUpdaterData) getUpdaterData(); - Shell shell = swtUpdaterData.getWindowShell(); - - if (shell != null && !shell.isDisposed()) { - shell.getDisplay().syncExec(r); - } - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/ImageFactory.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/ImageFactory.java deleted file mode 100644 index d985162f..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/icons/ImageFactory.java +++ /dev/null @@ -1,205 +0,0 @@ -/* - * 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.sdkuilib.internal.repository.icons; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.sdklib.internal.repository.archives.Archive; -import com.android.sdklib.internal.repository.packages.Package; -import com.android.sdklib.internal.repository.sources.SdkSource; -import com.android.sdkuilib.internal.repository.core.PkgContentProvider; - -import org.eclipse.swt.SWTException; -import org.eclipse.swt.graphics.Image; -import org.eclipse.swt.widgets.Display; - -import java.io.InputStream; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Locale; -import java.util.Map; - - -/** - * An utility class to serve {@link Image} correspond to the various icons - * present in this package and dispose of them correctly at the end. - */ -public class ImageFactory { - - private final Display mDisplay; - private final Map mImages = new HashMap(); - - /** - * Filter an image when it's loaded by - * {@link ImageFactory#getImageByName(String, String, Filter)}. - */ - public interface Filter { - /** - * Invoked by {@link ImageFactory#getImageByName(String, String, Filter)} when - * a non-null {@link Image} object has been loaded. The filter should create a - * new image, modifying it as needed.
- * If no modification is necessary, the filter can simply return the source image.
- * The result will be cached and returned by {@link ImageFactory}. - *

- * - * @param source A non-null source image. - * @return Either the source or a new, potentially modified, image. - */ - @NonNull public Image filter(@NonNull Image source); - } - - public ImageFactory(@NonNull Display display) { - mDisplay = display; - } - - /** - * Loads an image given its filename (with its extension). - * Might return null if the image cannot be loaded.
- * The image is cached. Successive calls will return the same object.
- * The image is automatically disposed when {@link ImageFactory} is disposed. - * - * @param imageName The filename (with extension) of the image to load. - * @return A new or existing {@link Image}. The caller must NOT dispose the image (the - * image will disposed by {@link #dispose()}). The returned image can be null if the - * expected file is missing. - */ - @Nullable - public Image getImageByName(@NonNull String imageName) { - return getImageByName(imageName, imageName, null); - } - - - /** - * Loads an image given its filename (with its extension), caches it using the given - * {@code KeyName} name and optionally applies a filter to it. - * Might return null if the image cannot be loaded. - * The image is cached. Successive calls using {@code KeyName} will return the same - * object directly (the filter is not re-applied in this case.)
- * The image is automatically disposed when {@link ImageFactory} is disposed. - *

- * - * @param imageName The filename (with extension) of the image to load. - * @return A new or existing {@link Image}. The caller must NOT dispose the image (the - * image will disposed by {@link #dispose()}). The returned image is null if the - * expected file is missing. - */ - @Nullable - public Image getImageByName(@NonNull String imageName, - @NonNull String keyName, - @Nullable Filter filter) { - - Image image = mImages.get(keyName); - if (image != null) { - return image; - } - - InputStream stream = getClass().getResourceAsStream(imageName); - if (stream != null) { - try { - image = new Image(mDisplay, stream); - if (image != null && filter != null) { - Image image2 = filter.filter(image); - if (image2 != image && !image.isDisposed()) { - image.dispose(); - } - image = image2; - } - } catch (SWTException e) { - // ignore - } catch (IllegalArgumentException e) { - // ignore - } - } - - // Store the image in the hash, even if this failed. If it fails now, it will fail later. - mImages.put(keyName, image); - - return image; - } - - /** - * Loads and returns the appropriate image for a given package, archive or source object. - * The image is cached. Successive calls will return the same object. - * - * @param object A {@link SdkSource} or {@link Package} or {@link Archive}. - * @return A new or existing {@link Image}. The caller must NOT dispose the image (the - * image will disposed by {@link #dispose()}). The returned image can be null if the - * object is of an unknown type. - */ - @Nullable - public Image getImageForObject(@Nullable Object object) { - - if (object == null) { - return null; - } - - if (object instanceof Image) { - return (Image) object; - } - - String clz = object.getClass().getSimpleName(); - if (clz.endsWith(Package.class.getSimpleName())) { - String name = clz.replaceFirst(Package.class.getSimpleName(), "") //$NON-NLS-1$ - .replace("SystemImage", "sysimg") //$NON-NLS-1$ //$NON-NLS-2$ - .toLowerCase(Locale.US); - name += "_pkg_16.png"; //$NON-NLS-1$ - return getImageByName(name); - } - - if (object instanceof PkgContentProvider.RepoSourceError) { - return getImageByName("error_icon_16.png"); //$NON-NLS-1$ - - } else if (object instanceof PkgContentProvider.RepoSourceNotification) { - return getImageByName("nopkg_icon_16.png"); //$NON-NLS-1$ - } - - if (object instanceof Archive) { - if (((Archive) object).isCompatible()) { - return getImageByName("archive_icon16.png"); //$NON-NLS-1$ - } else { - return getImageByName("incompat_icon16.png"); //$NON-NLS-1$ - } - } - - if (object instanceof String) { - return getImageByName((String) object); - } - - - if (object != null) { - // For debugging - // System.out.println("No image for object " + object.getClass().getSimpleName()); - } - - return null; - } - - /** - * Dispose all the images created by this factory so far. - */ - public void dispose() { - Iterator it = mImages.values().iterator(); - while(it.hasNext()) { - Image img = it.next(); - if (img != null && img.isDisposed() == false) { - img.dispose(); - } - it.remove(); - } - } - -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AddonSitesDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AddonSitesDialog.java deleted file mode 100644 index 01539496..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AddonSitesDialog.java +++ /dev/null @@ -1,574 +0,0 @@ -/* - * Copyright (C) 2011 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.sdkuilib.internal.repository.ui; - -import com.android.sdklib.internal.repository.sources.SdkAddonSource; -import com.android.sdklib.internal.repository.sources.SdkSource; -import com.android.sdklib.internal.repository.sources.SdkSourceCategory; -import com.android.sdklib.internal.repository.sources.SdkSourceProperties; -import com.android.sdklib.internal.repository.sources.SdkSources; -import com.android.sdklib.internal.repository.sources.SdkSysImgSource; -import com.android.sdklib.repository.SdkSysImgConstants; -import com.android.sdkuilib.internal.repository.UpdaterBaseDialog; -import com.android.sdkuilib.internal.repository.SwtUpdaterData; -import com.android.sdkuilib.ui.GridDataBuilder; -import com.android.sdkuilib.ui.GridLayoutBuilder; - -import org.eclipse.jface.dialogs.IInputValidator; -import org.eclipse.jface.dialogs.InputDialog; -import org.eclipse.jface.viewers.CheckStateChangedEvent; -import org.eclipse.jface.viewers.CheckboxTableViewer; -import org.eclipse.jface.viewers.ColumnLabelProvider; -import org.eclipse.jface.viewers.ICheckStateListener; -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.LabelProvider; -import org.eclipse.jface.viewers.SelectionChangedEvent; -import org.eclipse.jface.viewers.StructuredSelection; -import org.eclipse.jface.viewers.TableViewer; -import org.eclipse.jface.viewers.TableViewerColumn; -import org.eclipse.jface.viewers.Viewer; -import org.eclipse.jface.window.Window; -import org.eclipse.swt.SWT; -import org.eclipse.swt.events.ControlAdapter; -import org.eclipse.swt.events.ControlEvent; -import org.eclipse.swt.events.MouseAdapter; -import org.eclipse.swt.events.MouseEvent; -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.Button; -import org.eclipse.swt.widgets.Composite; -import org.eclipse.swt.widgets.Display; -import org.eclipse.swt.widgets.Label; -import org.eclipse.swt.widgets.MessageBox; -import org.eclipse.swt.widgets.Shell; -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 java.util.ArrayList; -import java.util.Arrays; - -/** - * Dialog that displays 2 tabs:
- * - one tab with the list of extra add-ons sites defined by the user.
- * - one tab with the list of 3rd-party add-ons currently available, which the user can - * deactivate to prevent from loading them. - */ -public class AddonSitesDialog extends UpdaterBaseDialog { - - private final SdkSources mSources; - private Table mUserTable; - private TableViewer mUserTableViewer; - private CheckboxTableViewer mSitesTableViewer; - private Button mUserButtonNew; - private Button mUserButtonDelete; - private Button mUserButtonEdit; - private Runnable mSourcesChangeListener; - - /** - * Create the dialog. - * - * @param parent The parent's shell - * @wbp.parser.entryPoint - */ - public AddonSitesDialog(Shell parent, SwtUpdaterData updaterData) { - super(parent, updaterData, "Add-on Sites"); - mSources = updaterData.getSources(); - assert mSources != null; - } - - /** - * Create contents of the dialog. - * @wbp.parser.entryPoint - */ - @Override - protected void createContents() { - super.createContents(); - Shell shell = getShell(); - shell.setMinimumSize(new Point(300, 300)); - shell.setSize(600, 400); - - TabFolder tabFolder = new TabFolder(shell, SWT.NONE); - GridDataBuilder.create(tabFolder).fill().grab().hSpan(2); - - TabItem sitesTabItem = new TabItem(tabFolder, SWT.NONE); - sitesTabItem.setText("Official Add-on Sites"); - createTabOfficialSites(tabFolder, sitesTabItem); - - TabItem userTabItem = new TabItem(tabFolder, SWT.NONE); - userTabItem.setText("User Defined Sites"); - createTabUserSites(tabFolder, userTabItem); - - // placeholder for aligning close button - Label label = new Label(shell, SWT.NONE); - GridDataBuilder.create(label).hFill().hGrab(); - - createCloseButton(); - } - - void createTabOfficialSites(TabFolder tabFolder, TabItem sitesTabItem) { - Composite root = new Composite(tabFolder, SWT.NONE); - sitesTabItem.setControl(root); - GridLayoutBuilder.create(root).columns(3); - - Label label = new Label(root, SWT.NONE); - GridDataBuilder.create(label).hGrab().vCenter().hSpan(3); - label.setText( - "This lets select which official 3rd-party sites you want to load.\n" + - "\n" + - "These sites are managed by non-Android vendors to provide add-ons and extra packages.\n" + - "They are by default all enabled. When you disable one, the SDK Manager will not check the site for new packages." - ); - - mSitesTableViewer = CheckboxTableViewer.newCheckList(root, SWT.BORDER | SWT.FULL_SELECTION); - mSitesTableViewer.setContentProvider(new SourcesContentProvider()); - - Table sitesTable = mSitesTableViewer.getTable(); - sitesTable.setToolTipText("Enable 3rd-Party Site"); - sitesTable.setLinesVisible(true); - sitesTable.setHeaderVisible(true); - GridDataBuilder.create(sitesTable).fill().grab().hSpan(3); - - TableViewerColumn columnViewer = new TableViewerColumn(mSitesTableViewer, SWT.NONE); - TableColumn column = columnViewer.getColumn(); - column.setResizable(true); - column.setWidth(150); - column.setText("Name"); - columnViewer.setLabelProvider(new ColumnLabelProvider() { - @Override - public String getText(Object element) { - if (element instanceof SdkSource) { - String name = ((SdkSource) element).getUiName(); - if (name != null) { - return name; - } - return ((SdkSource) element).getShortDescription(); - } - return super.getText(element); - } - }); - - columnViewer = new TableViewerColumn(mSitesTableViewer, SWT.NONE); - column = columnViewer.getColumn(); - column.setResizable(true); - column.setWidth(400); - column.setText("URL"); - columnViewer.setLabelProvider(new ColumnLabelProvider() { - @Override - public String getText(Object element) { - if (element instanceof SdkSource) { - return ((SdkSource) element).getUrl(); - } - return super.getText(element); - } - }); - - mSitesTableViewer.addCheckStateListener(new ICheckStateListener() { - @Override - public void checkStateChanged(CheckStateChangedEvent event) { - on_SitesTableViewer_checkStateChanged(event); - } - }); - - // "enable all" and "disable all" buttons under the table - Button selectAll = new Button(root, SWT.NONE); - selectAll.setText("Enable All"); - GridDataBuilder.create(selectAll).hLeft(); - selectAll.addSelectionListener(new SelectionAdapter() { - @Override - public void widgetSelected(SelectionEvent event) { - on_SitesTableViewer_selectAll(); - } - }); - - // placeholder between both buttons - label = new Label(root, SWT.NONE); - GridDataBuilder.create(label).hFill().hGrab(); - - Button deselectAll = new Button(root, SWT.NONE); - deselectAll.setText("Disable All"); - GridDataBuilder.create(deselectAll).hRight(); - deselectAll.addSelectionListener(new SelectionAdapter() { - @Override - public void widgetSelected(SelectionEvent event) { - on_SitesTableViewer_deselectAll(); - } - }); - } - - void createTabUserSites(TabFolder tabFolder, TabItem userTabItem) { - Composite root = new Composite(tabFolder, SWT.NONE); - userTabItem.setControl(root); - GridLayoutBuilder.create(root).columns(2); - - Label label = new Label(root, SWT.NONE); - GridDataBuilder.create(label).hLeft().vCenter().hSpan(2); - label.setText( - "This lets you manage a list of user-contributed external add-on sites URLs.\n" + - "\n" + - "Add-on sites can provide new add-ons and extra packages.\n" + - "They cannot provide standard Android platforms, system images or docs.\n" + - "Adding a URL here will not allow you to clone an official Android repository." - ); - - mUserTableViewer = new TableViewer(root, SWT.BORDER | SWT.FULL_SELECTION); - mUserTableViewer.setContentProvider(new SourcesContentProvider()); - - mUserTableViewer.addPostSelectionChangedListener(new ISelectionChangedListener() { - @Override - public void selectionChanged(SelectionChangedEvent event) { - on_UserTableViewer_selectionChanged(event); - } - }); - mUserTable = mUserTableViewer.getTable(); - mUserTable.setLinesVisible(true); - mUserTable.addMouseListener(new MouseAdapter() { - @Override - public void mouseUp(MouseEvent event) { - on_UserTable_mouseUp(event); - } - }); - GridDataBuilder.create(mUserTable).fill().grab().vSpan(5); - - TableViewerColumn tableViewerColumn = new TableViewerColumn(mUserTableViewer, SWT.NONE); - TableColumn userColumnUrl = tableViewerColumn.getColumn(); - userColumnUrl.setWidth(100); - - // Implementation detail: set the label provider on the table viewer *after* associating - // a column. This will set the label provider on the column for us. - mUserTableViewer.setLabelProvider(new LabelProvider()); - - - mUserButtonNew = new Button(root, SWT.NONE); - mUserButtonNew.addSelectionListener(new SelectionAdapter() { - @Override - public void widgetSelected(SelectionEvent e) { - userNewOrEdit(false /*isEdit*/); - } - }); - GridDataBuilder.create(mUserButtonNew).hFill().vCenter(); - mUserButtonNew.setText("New..."); - - mUserButtonEdit = new Button(root, SWT.NONE); - mUserButtonEdit.addSelectionListener(new SelectionAdapter() { - @Override - public void widgetSelected(SelectionEvent e) { - userNewOrEdit(true /*isEdit*/); - } - }); - GridDataBuilder.create(mUserButtonEdit).hFill().vCenter(); - mUserButtonEdit.setText("Edit..."); - - mUserButtonDelete = new Button(root, SWT.NONE); - mUserButtonDelete.addSelectionListener(new SelectionAdapter() { - @Override - public void widgetSelected(SelectionEvent e) { - on_UserButtonDelete_widgetSelected(e); - } - }); - GridDataBuilder.create(mUserButtonDelete).hFill().vCenter(); - mUserButtonDelete.setText("Delete..."); - - adjustColumnsWidth(mUserTable, userColumnUrl); - } - - @Override - protected void close() { - if (mSources != null && mSourcesChangeListener != null) { - mSources.removeChangeListener(mSourcesChangeListener); - } - SdkSourceProperties p = new SdkSourceProperties(); - p.save(); - super.close(); - } - - /** - * Adds a listener to adjust the column width when the parent is resized. - */ - private void adjustColumnsWidth(final Table table, final TableColumn column0) { - // Add a listener to resize the column to the full width of the table - table.addControlListener(new ControlAdapter() { - @Override - public void controlResized(ControlEvent e) { - Rectangle r = table.getClientArea(); - column0.setWidth(r.width * 100 / 100); // 100% - } - }); - } - - private void userNewOrEdit(final boolean isEdit) { - final SdkSource[] knownSources = mSources.getAllSources(); - String title = isEdit ? "Edit Add-on Site URL" : "Add Add-on Site URL"; - String msg = "Please enter the URL of the addon.xml:"; - IStructuredSelection sel = (IStructuredSelection) mUserTableViewer.getSelection(); - final String initialValue = !isEdit || sel.isEmpty() ? null : - sel.getFirstElement().toString(); - - if (isEdit && initialValue == null) { - // Edit with no actual value is not supposed to happen. Ignore this case. - return; - } - - InputDialog dlg = new InputDialog( - getShell(), - title, - msg, - initialValue, - new IInputValidator() { - @Override - public String isValid(String newText) { - - newText = newText == null ? null : newText.trim(); - - if (newText == null || newText.length() == 0) { - return "Error: URL field is empty. Please enter a URL."; - } - - // A URL should have one of the following prefixes - if (!newText.startsWith("file://") && //$NON-NLS-1$ - !newText.startsWith("ftp://") && //$NON-NLS-1$ - !newText.startsWith("http://") && //$NON-NLS-1$ - !newText.startsWith("https://")) { //$NON-NLS-1$ - return "Error: The URL must start by one of file://, ftp://, http:// or https://"; - } - - if (isEdit && newText.equals(initialValue)) { - // Edited value hasn't changed. This isn't an error. - return null; - } - - // Reject URLs that are already in the source list. - // URLs are generally case-insensitive (except for file:// where it all depends - // on the current OS so we'll ignore this case.) - for (SdkSource s : knownSources) { - if (newText.equalsIgnoreCase(s.getUrl())) { - return "Error: This site is already listed."; - } - } - - return null; - } - }); - - if (dlg.open() == Window.OK) { - String url = dlg.getValue().trim(); - - if (!url.equals(initialValue)) { - if (isEdit && initialValue != null) { - // Remove the old value before we add the new one, which is we just - // asserted will be different. - for (SdkSource source : mSources.getSources(SdkSourceCategory.USER_ADDONS)) { - if (initialValue.equals(source.getUrl())) { - mSources.remove(source); - break; - } - } - - } - - // create the source, store it and update the list - SdkSource newSource; - // use url suffix to decide whether this is a SysImg or Addon; - // see SdkSources.loadUserAddons() for another check like this - if (url.endsWith(SdkSysImgConstants.URL_DEFAULT_FILENAME)) { - newSource = new SdkSysImgSource(url, null/*uiName*/); - } else { - newSource = new SdkAddonSource(url, null/*uiName*/); - } - mSources.add(SdkSourceCategory.USER_ADDONS, newSource); - setReturnValue(true); - // notify sources change listeners. This will invoke our own loadUserUrlsList(). - mSources.notifyChangeListeners(); - - // select the new source - IStructuredSelection newSel = new StructuredSelection(newSource); - mUserTableViewer.setSelection(newSel, true /*reveal*/); - } - } - } - - private void on_UserButtonDelete_widgetSelected(SelectionEvent e) { - IStructuredSelection sel = (IStructuredSelection) mUserTableViewer.getSelection(); - String selectedUrl = sel.isEmpty() ? null : sel.getFirstElement().toString(); - - if (selectedUrl == null) { - return; - } - - MessageBox mb = new MessageBox(getShell(), - SWT.YES | SWT.NO | SWT.ICON_QUESTION | SWT.APPLICATION_MODAL); - mb.setText("Delete add-on site"); - mb.setMessage(String.format("Do you want to delete the URL %1$s?", selectedUrl)); - if (mb.open() == SWT.YES) { - for (SdkSource source : mSources.getSources(SdkSourceCategory.USER_ADDONS)) { - if (selectedUrl.equals(source.getUrl())) { - mSources.remove(source); - setReturnValue(true); - mSources.notifyChangeListeners(); - break; - } - } - } - } - - private void on_UserTable_mouseUp(MouseEvent event) { - Point p = new Point(event.x, event.y); - if (mUserTable.getItem(p) == null) { - mUserTable.deselectAll(); - on_UserTableViewer_selectionChanged(null /*event*/); - } - } - - private void on_UserTableViewer_selectionChanged(SelectionChangedEvent event) { - ISelection sel = mUserTableViewer.getSelection(); - mUserButtonDelete.setEnabled(!sel.isEmpty()); - mUserButtonEdit.setEnabled(!sel.isEmpty()); - } - - private void on_SitesTableViewer_checkStateChanged(CheckStateChangedEvent event) { - Object element = event.getElement(); - if (element instanceof SdkSource) { - SdkSource source = (SdkSource) element; - boolean isChecked = event.getChecked(); - if (source.isEnabled() != isChecked) { - setReturnValue(true); - source.setEnabled(isChecked); - mSources.notifyChangeListeners(); - } - } - } - - private void on_SitesTableViewer_selectAll() { - for (Object item : (Object[]) mSitesTableViewer.getInput()) { - if (!mSitesTableViewer.getChecked(item)) { - mSitesTableViewer.setChecked(item, true); - on_SitesTableViewer_checkStateChanged( - new CheckStateChangedEvent(mSitesTableViewer, item, true)); - } - } - } - - private void on_SitesTableViewer_deselectAll() { - for (Object item : (Object[]) mSitesTableViewer.getInput()) { - if (mSitesTableViewer.getChecked(item)) { - mSitesTableViewer.setChecked(item, false); - on_SitesTableViewer_checkStateChanged( - new CheckStateChangedEvent(mSitesTableViewer, item, false)); - } - } - } - - - @Override - protected void postCreate() { - // A runnable to initially load and then update the user urls & sites lists. - final Runnable updateInUiThread = new Runnable() { - @Override - public void run() { - loadUserUrlsList(); - loadSiteUrlsList(); - } - }; - - // A listener that runs when the sources have changed. - // This is most likely called on a worker thread. - mSourcesChangeListener = new Runnable() { - @Override - public void run() { - Shell shell = getShell(); - if (shell != null) { - Display display = shell.getDisplay(); - if (display != null) { - display.syncExec(updateInUiThread); - } - } - } - }; - - mSources.addChangeListener(mSourcesChangeListener); - - // initialize the list - updateInUiThread.run(); - } - - private void loadUserUrlsList() { - SdkSource[] knownSources = mSources.getSources(SdkSourceCategory.USER_ADDONS); - Arrays.sort(knownSources); - - ISelection oldSelection = mUserTableViewer.getSelection(); - - mUserTableViewer.setInput(knownSources); - mUserTableViewer.refresh(); - // initialize buttons' state that depend on the list - on_UserTableViewer_selectionChanged(null /*event*/); - - if (oldSelection != null && !oldSelection.isEmpty()) { - mUserTableViewer.setSelection(oldSelection, true /*reveal*/); - } - } - - private void loadSiteUrlsList() { - SdkSource[] knownSources = mSources.getSources(SdkSourceCategory.ADDONS_3RD_PARTY); - Arrays.sort(knownSources); - - ISelection oldSelection = mSitesTableViewer.getSelection(); - - mSitesTableViewer.setInput(knownSources); - mSitesTableViewer.refresh(); - - if (oldSelection != null && !oldSelection.isEmpty()) { - mSitesTableViewer.setSelection(oldSelection, true /*reveal*/); - } - - // Check the sources which are currently enabled. - ArrayList disabled = new ArrayList(knownSources.length); - for (SdkSource source : knownSources) { - if (source.isEnabled()) { - disabled.add(source); - } - } - mSitesTableViewer.setCheckedElements(disabled.toArray()); - } - - - private static class SourcesContentProvider implements IStructuredContentProvider { - @Override - public void dispose() { - // pass - } - - @Override - public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { - // pass - } - - @Override - public Object[] getElements(Object inputElement) { - if (inputElement instanceof SdkSource[]) { - return (Object[]) inputElement; - } else { - return new Object[0]; - } - } - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AvdManagerPage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AvdManagerPage.java index ed0b933a..691284d6 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AvdManagerPage.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AvdManagerPage.java @@ -21,11 +21,11 @@ import com.android.sdklib.devices.DeviceManager.DevicesChangedListener; import com.android.sdklib.internal.avd.AvdInfo; import com.android.sdklib.internal.avd.AvdManager; -import com.android.sdklib.repository.ISdkChangeListener; -import com.android.sdkuilib.internal.repository.SwtUpdaterData; +import com.android.sdkuilib.repository.ISdkChangeListener; +import com.android.sdkuilib.ui.AvdDisplayMode; import com.android.sdkuilib.internal.widgets.AvdSelector; -import com.android.sdkuilib.internal.widgets.AvdSelector.DisplayMode; +import org.eclipse.andmore.sdktool.SdkContext; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; @@ -48,23 +48,22 @@ public class AvdManagerPage extends Composite private AvdSelector mAvdSelector; - private final SwtUpdaterData mSwtUpdaterData; + private final SdkContext mSdkContext; private final DeviceManager mDeviceManager; /** * Create the composite. * @param parent The parent of the composite. - * @param swtUpdaterData An instance of {@link SwtUpdaterData}. + * @param sdkContext SDK handler and repo manager */ public AvdManagerPage(Composite parent, int swtStyle, - SwtUpdaterData swtUpdaterData, - DeviceManager deviceManager) { + SdkContext sdkContext) { super(parent, swtStyle); - mSwtUpdaterData = swtUpdaterData; - mSwtUpdaterData.addListeners(this); + mSdkContext = sdkContext; + mSdkContext.getSdkHelper().addListeners(this); - mDeviceManager = deviceManager; + mDeviceManager = mSdkContext.getDeviceManager(); mDeviceManager.registerListener(this); createContents(this); @@ -78,23 +77,16 @@ private void createContents(Composite parent) { label.setLayoutData(new GridData()); try { - if (mSwtUpdaterData != null && mSwtUpdaterData.getAvdManager() != null) { - label.setText(String.format( + label.setText(String.format( "List of existing Android Virtual Devices located at %s", - mSwtUpdaterData.getAvdManager().getBaseAvdFolder())); - } else { - label.setText("Error: cannot find the AVD folder location.\r\n Please set the 'ANDROID_SDK_HOME' env variable."); - } + mSdkContext.getAvdManager().getBaseAvdFolder())); } catch (AndroidLocationException e) { label.setText(e.getMessage()); } mAvdSelector = new AvdSelector(parent, - mSwtUpdaterData.getOsSdkRoot(), - mSwtUpdaterData.getAvdManager(), - DisplayMode.MANAGER, - mSwtUpdaterData.getSdkLog()); - mAvdSelector.setSettingsController(mSwtUpdaterData.getSettingsController()); + mSdkContext, + AvdDisplayMode.MANAGER); } @Override @@ -104,7 +96,7 @@ public void widgetDisposed(DisposeEvent e) { @Override public void dispose() { - mSwtUpdaterData.removeListener(this); + mSdkContext.getSdkHelper().removeListener(this); mDeviceManager.unregisterListener(this); super.dispose(); } @@ -114,16 +106,16 @@ protected void checkSubclass() { // Disable the check that prevents subclassing of SWT components } - public void selectAvd(AvdInfo avdInfo, boolean reloadAvdList) { + public void selectAvd(AvdInfo avd, boolean reloadAvdList) { if (reloadAvdList) { mAvdSelector.refresh(true /*reload*/); // Reloading the AVDs created new objects, so the reference to avdInfo // will never be selected. Instead reselect it based on its unique name. - AvdManager am = mSwtUpdaterData.getAvdManager(); - avdInfo = am.getAvd(avdInfo.getName(), false /*validAvdOnly*/); + AvdManager avdManager = mSdkContext.getAvdManager(); + avd = avdManager.getAvd(avd.getName(), false /*validAvdOnly*/); } - mAvdSelector.setSelection(avdInfo); + mAvdSelector.setSelection(avd); } // -- Start of internal part ---------- @@ -166,7 +158,6 @@ public void onDevicesChanged() { mAvdSelector.refresh(false /*reload*/); } - // End of hiding from SWT Designer //$hide<<$ } diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AvdManagerWindowImpl1.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AvdManagerWindowImpl1.java index 1f14d7f7..74b15402 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AvdManagerWindowImpl1.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AvdManagerWindowImpl1.java @@ -18,23 +18,23 @@ import com.android.SdkConstants; -import com.android.sdklib.devices.DeviceManager; import com.android.sdklib.internal.avd.AvdInfo; -import com.android.sdklib.internal.repository.ITaskFactory; -import com.android.sdklib.internal.repository.updater.SettingsController; -import com.android.sdklib.repository.ISdkChangeListener; + +import com.android.sdkuilib.repository.ISdkChangeListener; import com.android.sdkuilib.internal.repository.AboutDialog; import com.android.sdkuilib.internal.repository.MenuBarWrapper; import com.android.sdkuilib.internal.repository.SettingsDialog; -import com.android.sdkuilib.internal.repository.SwtUpdaterData; -import com.android.sdkuilib.internal.repository.icons.ImageFactory; +import com.android.sdkuilib.internal.repository.avd.SdkTargets; import com.android.sdkuilib.internal.repository.ui.DeviceManagerPage.IAvdCreatedListener; -import com.android.sdkuilib.repository.AvdManagerWindow.AvdInvocationContext; import com.android.sdkuilib.repository.SdkUpdaterWindow; + +import com.android.sdkuilib.repository.AvdManagerWindow.AvdInvocationContext; import com.android.sdkuilib.ui.GridDataBuilder; import com.android.sdkuilib.ui.GridLayoutBuilder; import com.android.utils.ILogger; +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.andmore.sdktool.SdkContext; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; @@ -50,8 +50,6 @@ import org.eclipse.swt.widgets.TabFolder; import org.eclipse.swt.widgets.TabItem; -import java.io.File; - /** * This is an intermediate version of the {@link AvdManagerPage} * wrapped in its own standalone window for use from the SDK Manager 2. @@ -64,18 +62,16 @@ public class AvdManagerWindowImpl1 { private final Shell mParentShell; private final AvdInvocationContext mContext; + private final SdkContext mSdkContext; /** Internal data shared between the window and its pages. */ - private final SwtUpdaterData mSwtUpdaterData; /** True if this window created the UpdaterData, in which case it needs to dispose it. */ private final boolean mOwnUpdaterData; - private final DeviceManager mDeviceManager; // --- UI members --- protected Shell mShell; private AvdManagerPage mAvdPage; - private SettingsController mSettingsController; private TabFolder mTabFolder; /** @@ -83,20 +79,19 @@ public class AvdManagerWindowImpl1 { * * @param parentShell Parent shell. * @param sdkLog Logger. Cannot be null. - * @param osSdkRoot The OS path to the SDK root. + * @param sdkContext SDK handler and repo manager * @param context The {@link AvdInvocationContext} to change the behavior depending on who's * opening the SDK Manager. */ public AvdManagerWindowImpl1( Shell parentShell, ILogger sdkLog, - String osSdkRoot, + SdkContext sdkContext, AvdInvocationContext context) { mParentShell = parentShell; + mSdkContext = sdkContext; mContext = context; - mSwtUpdaterData = new SwtUpdaterData(osSdkRoot, sdkLog); mOwnUpdaterData = true; - mDeviceManager = DeviceManager.createInstance(new File(osSdkRoot), sdkLog); } /** @@ -106,20 +101,18 @@ public AvdManagerWindowImpl1( * to share the same {@link SwtUpdaterData} structure. * * @param parentShell Parent shell. - * @param swtUpdaterData The parent's updater data. + * @param sdkContext SDK handler and repo manager * @param context The {@link AvdInvocationContext} to change the behavior depending on who's * opening the SDK Manager. */ public AvdManagerWindowImpl1( Shell parentShell, - SwtUpdaterData swtUpdaterData, + SdkContext sdkContext, AvdInvocationContext context) { mParentShell = parentShell; mContext = context; - mSwtUpdaterData = swtUpdaterData; + mSdkContext = sdkContext; mOwnUpdaterData = false; - mDeviceManager = DeviceManager.createInstance(new File(mSwtUpdaterData.getOsSdkRoot()), - mSwtUpdaterData.getSdkLog()); } /** @@ -147,7 +140,6 @@ public void open() { display.sleep(); } } - dispose(); //$hide$ } } @@ -202,7 +194,7 @@ private void createAvdTab(TabFolder tabFolder, TabItem avdTabItem) { avdTabItem.setControl(root); GridLayoutBuilder.create(root).columns(1); - mAvdPage = new AvdManagerPage(root, SWT.NONE, mSwtUpdaterData, mDeviceManager); + mAvdPage = new AvdManagerPage(root, SWT.NONE, mSdkContext); GridDataBuilder.create(mAvdPage).fill().grab(); } @@ -212,7 +204,7 @@ private void createDeviceTab(TabFolder tabFolder, TabItem devTabItem) { GridLayoutBuilder.create(root).columns(1); DeviceManagerPage devicePage = - new DeviceManagerPage(root, SWT.NONE, mSwtUpdaterData, mDeviceManager); + new DeviceManagerPage(root, SWT.NONE, mSdkContext, new SdkTargets(mSdkContext)); GridDataBuilder.create(devicePage).fill().grab(); devicePage.setAvdCreatedListener(new IAvdCreatedListener() { @@ -226,7 +218,6 @@ public void onAvdCreated(AvdInfo avdInfo) { }); } - @SuppressWarnings("unused") // MenuBarWrapper works using side effects private void createMenuBar() { Menu menuBar = new Menu(mShell, SWT.BAR); @@ -258,25 +249,25 @@ public void widgetSelected(SelectionEvent event) { new MenuBarWrapper(APP_NAME_MAC_MENU, menuTools) { @Override public void onPreferencesMenuSelected() { - SettingsDialog sd = new SettingsDialog(mShell, mSwtUpdaterData); + SettingsDialog sd = new SettingsDialog(mShell, mSdkContext); sd.open(); } @Override public void onAboutMenuSelected() { - AboutDialog ad = new AboutDialog(mShell, mSwtUpdaterData); + AboutDialog ad = new AboutDialog(mShell, mSdkContext); ad.open(); } @Override public void printError(String format, Object... args) { - if (mSwtUpdaterData != null) { - mSwtUpdaterData.getSdkLog().error(null, format, args); + if (mSdkContext != null) { + mSdkContext.getSdkLog().error(null, format, args); } } }; } catch (Throwable e) { - mSwtUpdaterData.getSdkLog().error(e, "Failed to setup menu bar"); + mSdkContext.getSdkLog().error(e, "Failed to setup menu bar"); e.printStackTrace(); } } @@ -293,7 +284,7 @@ public void printError(String format, Object... args) { * Adds a new listener to be notified when a change is made to the content of the SDK. */ public void addListener(ISdkChangeListener listener) { - mSwtUpdaterData.addListeners(listener); + mSdkContext.getSdkHelper().addListeners(listener); } /** @@ -301,7 +292,7 @@ public void addListener(ISdkChangeListener listener) { * the SDK. */ public void removeListener(ISdkChangeListener listener) { - mSwtUpdaterData.removeListener(listener); + mSdkContext.getSdkHelper().removeListener(listener); } // --- Internals & UI Callbacks ----------- @@ -310,9 +301,7 @@ public void removeListener(ISdkChangeListener listener) { * Called before the UI is created. */ private void preCreateContent() { - mSwtUpdaterData.setWindowShell(mShell); - // We need the UI factory to create the UI - mSwtUpdaterData.setImageFactory(new ImageFactory(mShell.getDisplay())); + mSdkContext.getSdkHelper().setWindowShell(mShell); // Note: we can't create the TaskFactory yet because we need the UI // to be created first, so this is done in postCreateContent(). } @@ -326,15 +315,11 @@ private void preCreateContent() { private boolean postCreateContent() { setWindowImage(mShell); - setupSources(); - initializeSettings(); - - if (mSwtUpdaterData.checkIfInitFailed()) { + if (!initializeSettings()) { return false; } - - mSwtUpdaterData.broadcastOnSdkLoaded(); - + // TODO - Consider how to signal SDK loaded + //mSdkContext.getSdkHelper().broadcastOnSdkLoaded(mSdkContext.getSdkLog()); return true; } @@ -349,8 +334,8 @@ private void setWindowImage(Shell shell) { imageName = "android_icon_128.png"; } - if (mSwtUpdaterData != null) { - ImageFactory imgFactory = mSwtUpdaterData.getImageFactory(); + if (mSdkContext != null) { + ImageFactory imgFactory = mSdkContext.getSdkHelper().getImageFactory(); if (imgFactory != null) { shell.setImage(imgFactory.getImageByName(imageName)); } @@ -361,54 +346,41 @@ private void setWindowImage(Shell shell) { * Called by the main loop when the window has been disposed. */ private void dispose() { - mSwtUpdaterData.getSources().saveUserAddons(mSwtUpdaterData.getSdkLog()); + //mSdkContext.getSources().saveUserAddons(mSdkContext.getSdkLog()); } /** * Callback called when the window shell is disposed. */ private void onAndroidSdkUpdaterDispose() { - if (mOwnUpdaterData && mSwtUpdaterData != null) { - ImageFactory imgFactory = mSwtUpdaterData.getImageFactory(); + if (mOwnUpdaterData && mSdkContext != null) { + ImageFactory imgFactory = mSdkContext.getSdkHelper().getImageFactory(); if (imgFactory != null) { imgFactory.dispose(); } } } - /** - * Used to initialize the sources. - */ - private void setupSources() { - mSwtUpdaterData.setupDefaultSources(); - } - /** * Initializes settings. * This must be called after addExtraPages(), which created a settings page. * Iterate through all the pages to find the first (and supposedly unique) setting page, * and use it to load and apply these settings. */ - private void initializeSettings() { - mSettingsController = mSwtUpdaterData.getSettingsController(); - mSettingsController.loadSettings(); - mSettingsController.applySettings(); + private boolean initializeSettings() { + return mSdkContext.getSettings().initialize(mSdkContext.getSdkLog()); } private void onSdkManager() { - ITaskFactory oldFactory = mSwtUpdaterData.getTaskFactory(); - try { SdkUpdaterWindowImpl2 win = new SdkUpdaterWindowImpl2( mShell, - mSwtUpdaterData, + mSdkContext, SdkUpdaterWindow.SdkInvocationContext.AVD_MANAGER); win.open(); } catch (Exception e) { - mSwtUpdaterData.getSdkLog().error(e, "SDK Manager window error"); - } finally { - mSwtUpdaterData.setTaskFactory(oldFactory); + mSdkContext.getSdkLog().error(e, "SDK Manager window error"); } } } diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/DeviceManagerPage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/DeviceManagerPage.java index bd20ef82..83300337 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/DeviceManagerPage.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/DeviceManagerPage.java @@ -18,7 +18,6 @@ import com.android.annotations.NonNull; import com.android.annotations.Nullable; -import com.android.sdklib.SystemImage; import com.android.sdklib.devices.Device; import com.android.sdklib.devices.DeviceManager; import com.android.sdklib.devices.DeviceManager.DeviceFilter; @@ -28,16 +27,18 @@ import com.android.sdklib.devices.Storage; import com.android.sdklib.devices.Storage.Unit; import com.android.sdklib.internal.avd.AvdInfo; -import com.android.sdklib.repository.ISdkChangeListener; -import com.android.sdkuilib.internal.repository.SwtUpdaterData; -import com.android.sdkuilib.internal.repository.icons.ImageFactory; -import com.android.sdkuilib.internal.repository.icons.ImageFactory.Filter; +import com.android.sdklib.repository.targets.SystemImage; +import com.android.sdkuilib.internal.repository.avd.SdkTargets; +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.andmore.base.resources.ImageFactory.ImageEditor; import com.android.sdkuilib.internal.widgets.AvdCreationDialog; import com.android.sdkuilib.internal.widgets.AvdSelector; import com.android.sdkuilib.internal.widgets.DeviceCreationDialog; +import com.android.sdkuilib.repository.ISdkChangeListener; import com.android.sdkuilib.ui.GridDataBuilder; import com.android.sdkuilib.ui.GridLayoutBuilder; +import org.eclipse.andmore.sdktool.SdkContext; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.window.Window; import org.eclipse.swt.SWT; @@ -102,7 +103,8 @@ public interface IAvdCreatedListener { public void onAvdCreated(AvdInfo createdAvdInfo); } - private final SwtUpdaterData mSwtUpdaterData; + private final SdkContext mSdkContext; + private final SdkTargets mSdkTargets; private final DeviceManager mDeviceManager; private Table mTable; private Button mNewButton; @@ -116,9 +118,9 @@ public interface IAvdCreatedListener { private int mImageWidth; private boolean mDisableRefresh; private IAvdCreatedListener mAvdCreatedListener; - private final Filter mUserColorFilter = new Filter() { + private final ImageEditor mUserColorFilter = new ImageEditor() { @Override - public Image filter(Image source) { + public ImageData edit(Image source) { ImageData srcData = source.getImageData(); // swap green and blue @@ -131,25 +133,26 @@ public Image filter(Image source) { p.blueShift = p.greenShift; p.greenShift = b; - return new Image(source.getDevice(), srcData); + return srcData; } }; /** * Create the composite. * @param parent The parent of the composite. - * @param swtUpdaterData An instance of {@link SwtUpdaterData}. + * @param SdkContext An instance of {@link SdkContext}. */ public DeviceManagerPage(Composite parent, int swtStyle, - SwtUpdaterData swtUpdaterData, - DeviceManager deviceManager) { + SdkContext sdkContext, + SdkTargets sdkTargets) { super(parent, swtStyle); - mSwtUpdaterData = swtUpdaterData; - mSwtUpdaterData.addListeners(this); + mSdkContext = sdkContext; + mSdkTargets = sdkTargets; + mSdkContext.getSdkHelper().addListeners(this); - mDeviceManager = deviceManager; + mDeviceManager = mSdkContext.getDeviceManager(); mDeviceManager.registerListener(this); createContents(this); @@ -163,7 +166,7 @@ public void setAvdCreatedListener(IAvdCreatedListener avdCreatedListener) { private void createContents(Composite parent) { // get some bitmaps. - mImageFactory = new ImageFactory(parent.getDisplay()); + mImageFactory = mSdkContext.getSdkHelper().getImageFactory(); mUserImage = getTagImage(null /*tag*/, true /*isUser*/); mDeviceImage = getTagImage(null /*tag*/, false /*isUser*/); mImageWidth = Math.max(mDeviceImage.getImageData().width, @@ -316,7 +319,7 @@ public void widgetDisposed(DisposeEvent e) { @Override public void dispose() { - mSwtUpdaterData.removeListener(this); + mSdkContext.getSdkHelper().removeListener(this); mDeviceManager.unregisterListener(this); super.dispose(); } @@ -484,14 +487,14 @@ private List fillDevices( // Generate a list of the AVD names using these devices Map> device2avdMap = new HashMap>(); - for (AvdInfo avd : mSwtUpdaterData.getAvdManager().getAllAvds()) { + for (AvdInfo avd : mSdkContext.getAvdManager().getAllAvds()) { String n = avd.getDeviceName(); String m = avd.getDeviceManufacturer(); if (n == null || m == null || n.isEmpty() || m.isEmpty()) { continue; } for (Device device : devices) { - if (m.equals(device.getManufacturer()) && n.equals(device.getName())) { + if (m.equals(device.getManufacturer()) && n.equals(device.getDisplayName())) { List list = device2avdMap.get(device); if (list == null) { list = new LinkedList(); @@ -598,7 +601,7 @@ private static String getPrettyName(Device d, boolean leadZeroes) { if (d == null) { return ""; } - String name = d.getName(); + String name = d.getDisplayName(); if (name.equals("3.7 FWVGA slider")) { //$NON-NLS-1$ // Fix metadata: this one entry doesn't have "in" like the rest of them name = "3.7in FWVGA slider"; //$NON-NLS-1$ @@ -654,7 +657,7 @@ private void onNewDevice() { DeviceCreationDialog dlg = new DeviceCreationDialog( getShell(), mDeviceManager, - mSwtUpdaterData.getImageFactory(), + mSdkContext.getSdkHelper().getImageFactory(), null /*device*/); if (dlg.open() == Window.OK) { onRefresh(); @@ -674,7 +677,7 @@ private void onEditDevice() { DeviceCreationDialog dlg = new DeviceCreationDialog( getShell(), mDeviceManager, - mSwtUpdaterData.getImageFactory(), + mSdkContext.getSdkHelper().getImageFactory(), ci.mDevice); if (dlg.open() == Window.OK) { onRefresh(); @@ -720,9 +723,8 @@ private void onCreateAvd() { } final AvdCreationDialog dlg = new AvdCreationDialog(mTable.getShell(), - mSwtUpdaterData.getAvdManager(), - mImageFactory, - mSwtUpdaterData.getSdkLog(), + mSdkContext, + mSdkTargets, null); dlg.selectInitialDevice(ci.mDevice); @@ -760,13 +762,13 @@ private boolean selectCellByName(CellInfo selected) { if (mTable.isDisposed() || selected == null || selected.mDevice == null) { return false; } - String name = selected.mDevice.getName(); + String name = selected.mDevice.getDisplayName(); for (int n = mTable.getItemCount() - 1; n >= 0; n--) { TableItem item = mTable.getItem(n); Object data = item.getData(); if (data instanceof CellInfo) { CellInfo ci = (CellInfo) data; - if (ci != null && ci.mDevice != null && name.equals(ci.mDevice.getName())) { + if (ci != null && ci.mDevice != null && name.equals(ci.mDevice.getDisplayName())) { // Same cell object. Select it. mTable.select(n); return true; diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/LogWindow.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/LogWindow.java index 5977f315..2e79601c 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/LogWindow.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/LogWindow.java @@ -48,7 +48,7 @@ * the floating window is hidden but not closed. This way it can easily accumulate * all the log. */ -class LogWindow implements ILogUiProvider { +public class LogWindow implements ILogUiProvider { private Shell mParentShell; private Shell mShell; diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPage.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPage.java index bb72ea18..933f8a6c 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPage.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPage.java @@ -16,27 +16,19 @@ package com.android.sdkuilib.internal.repository.ui; -import com.android.sdklib.internal.repository.ITask; -import com.android.sdklib.internal.repository.ITaskMonitor; -import com.android.sdklib.internal.repository.archives.Archive; -import com.android.sdklib.internal.repository.archives.ArchiveInstaller; -import com.android.sdklib.internal.repository.packages.Package; -import com.android.sdklib.internal.repository.updater.PkgItem; -import com.android.sdklib.internal.repository.updater.PkgItem.PkgState; -import com.android.sdklib.repository.ISdkChangeListener; -import com.android.sdkuilib.internal.repository.SwtUpdaterData; -import com.android.sdkuilib.internal.repository.core.PkgCategory; -import com.android.sdkuilib.internal.repository.core.PkgCategoryApi; -import com.android.sdkuilib.internal.repository.core.PkgContentProvider; -import com.android.sdkuilib.internal.repository.icons.ImageFactory; -import com.android.sdkuilib.repository.SdkUpdaterWindow.SdkInvocationContext; -import com.android.sdkuilib.ui.GridDataBuilder; -import com.android.sdkuilib.ui.GridLayoutBuilder; +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import org.eclipse.andmore.sdktool.SdkContext; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.viewers.CheckStateChangedEvent; import org.eclipse.jface.viewers.CheckboxTreeViewer; -import org.eclipse.jface.viewers.ColumnLabelProvider; import org.eclipse.jface.viewers.ColumnViewerToolTipSupport; import org.eclipse.jface.viewers.DoubleClickEvent; import org.eclipse.jface.viewers.ICheckStateListener; @@ -68,27 +60,52 @@ import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeColumn; -import java.io.File; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; +import com.android.annotations.NonNull; +import com.android.repository.api.LocalPackage; +import com.android.repository.api.PackageOperation; +import com.android.repository.api.ProgressIndicator; +import com.android.repository.api.ProgressRunner; +import com.android.repository.api.RemotePackage; +import com.android.repository.api.RepoManager.RepoLoadedCallback; +import com.android.repository.api.Uninstaller; +import com.android.repository.api.UpdatablePackage; +import com.android.repository.impl.meta.Archive; +import com.android.repository.impl.meta.RepositoryPackages; +import com.android.repository.util.InstallerUtil; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.repository.installer.SdkInstallerUtil; +import com.android.sdkuilib.internal.repository.ITask; +import com.android.sdkuilib.internal.repository.ITaskFactory; +import com.android.sdkuilib.internal.repository.ITaskMonitor; +import com.android.sdkuilib.internal.repository.LoadPackagesRequest; +import com.android.sdkuilib.internal.repository.PackageManager; +import com.android.sdkuilib.internal.repository.content.CategoryKeyType; +import com.android.sdkuilib.internal.repository.content.PackageAnalyser; +import com.android.sdkuilib.internal.repository.content.PackageAnalyser.PkgState; +import com.android.sdkuilib.internal.repository.content.PackageContentProvider; +import com.android.sdkuilib.internal.repository.content.PkgCategory; +import com.android.sdkuilib.internal.repository.content.PkgCellAgent; +import com.android.sdkuilib.internal.repository.content.PkgCellLabelProvider; +import com.android.sdkuilib.internal.repository.content.PkgItem; +import com.android.sdkuilib.internal.repository.content.PkgTreeColumnViewerLabelProvider; +import org.eclipse.andmore.base.resources.ImageFactory; +import com.android.sdkuilib.internal.tasks.ILogUiProvider; +import com.android.sdkuilib.repository.SdkUpdaterWindow.SdkInvocationContext; +import com.android.sdkuilib.ui.GridDataBuilder; +import com.android.sdkuilib.ui.GridLayoutBuilder; /** * Page that displays both locally installed packages as well as all known * remote available packages. This gives an overview of what is installed * vs what is available and allows the user to update or install packages. */ -public final class PackagesPage extends Composite implements ISdkChangeListener { +public final class PackagesPage extends Composite { enum MenuAction { - RELOAD (SWT.NONE, "Reload"), - SHOW_ADDON_SITES (SWT.NONE, "Manage Add-on Sites..."), - TOGGLE_SHOW_ARCHIVES (SWT.CHECK, "Show Archives Details"), - TOGGLE_SHOW_INSTALLED_PKG (SWT.CHECK, "Show Installed Packages"), + RELOAD (SWT.NONE, "Reload"), + //TOGGLE_SHOW_INSTALLED_PKG (SWT.CHECK, "Show Installed Packages"), TOGGLE_SHOW_OBSOLETE_PKG (SWT.CHECK, "Show Obsolete Packages"), - TOGGLE_SHOW_UPDATE_NEW_PKG (SWT.CHECK, "Show Updates/New Packages") + TOGGLE_SHOW_NEW_PKG (SWT.CHECK, "Show New Packages") ; private final int mMenuStyle; @@ -108,18 +125,26 @@ public String getMenuTitle() { } }; - private final Map mMenuActions = new HashMap(); + // Column ids + public static final int NAME = 1; + public static final int API = 2; + public static final int REVISION = 3; + public static final int STATUS = 4; - private final PackagesPageImpl mImpl; - private final SdkInvocationContext mContext; + private final Map mMenuActions = new HashMap(); + + private final SdkContext mSdkContext; + //private final SdkInvocationContext mContext; + private final PackageAnalyser mPackageAnalyser; private boolean mDisplayArchives = false; private boolean mOperationPending; + private ProgressRunner mProgressRunner; private Composite mGroupPackages; private Text mTextSdkOsPath; private Button mCheckFilterObsolete; - private Button mCheckFilterInstalled; + //private Button mCheckFilterInstalled; private Button mCheckFilterNew; private Composite mGroupOptions; private Composite mGroupSdk; @@ -128,58 +153,26 @@ public String getMenuTitle() { private Font mTreeFontItalic; private TreeColumn mTreeColumnName; private CheckboxTreeViewer mTreeViewer; + private ILogUiProvider mSdkProgressControl; + private ITaskFactory mTaskFactory; public PackagesPage( Composite parent, int swtStyle, - SwtUpdaterData swtUpdaterData, - SdkInvocationContext context) { + SdkContext sdkContext, + SdkInvocationContext context) + { super(parent, swtStyle); - mImpl = new PackagesPageImpl(swtUpdaterData) { - @Override - protected boolean isUiDisposed() { - return mGroupPackages == null || mGroupPackages.isDisposed(); - }; - @Override - protected void syncExec(Runnable runnable) { - if (!isUiDisposed()) { - mGroupPackages.getDisplay().syncExec(runnable); - } - }; - - @Override - protected void syncViewerSelection() { - PackagesPage.this.syncViewerSelection(); - } - - @Override - protected void refreshViewerInput() { - PackagesPage.this.refreshViewerInput(); - } - - @Override - protected Font getTreeFontItalic() { - return mTreeFontItalic; - } - - @Override - protected void loadPackages(boolean useLocalCache, boolean overrideExisting) { - PackagesPage.this.loadPackages(useLocalCache, overrideExisting); - } - }; - mContext = context; - + mSdkContext = sdkContext; + mPackageAnalyser = new PackageAnalyser(sdkContext); + //mContext = context; createContents(this); postCreate(); //$hide$ } - public void performFirstLoad() { - mImpl.performFirstLoad(); - } - - @SuppressWarnings("unused") - private void createContents(Composite parent) { - GridLayoutBuilder.create(parent).noMargins().columns(2); + private void createContents(Composite parent) + { + GridLayoutBuilder.create(parent).noMargins().columns(2); mGroupSdk = new Composite(parent, SWT.NONE); GridDataBuilder.create(mGroupSdk).hFill().vCenter().hGrab().hSpan(2); @@ -199,47 +192,24 @@ private void createContents(Composite parent) { GridLayoutBuilder.create(groupPackages).columns(1); mTreeViewer = new CheckboxTreeViewer(groupPackages, SWT.BORDER); - mImpl.setITreeViewer(new PackagesPageImpl.ICheckboxTreeViewer() { - @Override - public Object getInput() { - return mTreeViewer.getInput(); - } - - @Override - public void setInput(List cats) { - mTreeViewer.setInput(cats); - } - - @Override - public void setContentProvider(PkgContentProvider pkgContentProvider) { - mTreeViewer.setContentProvider(pkgContentProvider); - } - - @Override - public void refresh() { - mTreeViewer.refresh(); - } - - @Override - public Object[] getCheckedElements() { - return mTreeViewer.getCheckedElements(); - } - }); - mTreeViewer.addFilter(new ViewerFilter() { + mTreeViewer.addFilter(new ViewerFilter() + { @Override public boolean select(Viewer viewer, Object parentElement, Object element) { return filterViewerItem(element); } }); - mTreeViewer.addCheckStateListener(new ICheckStateListener() { + mTreeViewer.addCheckStateListener(new ICheckStateListener() + { @Override public void checkStateChanged(CheckStateChangedEvent event) { onTreeCheckStateChanged(event); //$hide$ } }); - mTreeViewer.addDoubleClickListener(new IDoubleClickListener() { + mTreeViewer.addDoubleClickListener(new IDoubleClickListener() + { @Override public void doubleClick(DoubleClickEvent event) { onTreeDoubleClick(event); //$hide$ @@ -278,35 +248,31 @@ public void doubleClick(DoubleClickEvent event) { treeColumn4.setAlignment(SWT.LEAD); treeColumn4.setWidth(190); - mImpl.setIColumns( - wrapColumn(columnName), - wrapColumn(columnApi), - wrapColumn(columnRevision), - wrapColumn(columnStatus)); - mGroupOptions = new Composite(groupPackages, SWT.NONE); GridDataBuilder.create(mGroupOptions).hFill().vCenter().hGrab(); - GridLayoutBuilder.create(mGroupOptions).columns(7).noMargins(); + GridLayoutBuilder.create(mGroupOptions).columns(4).noMargins(); - // Options line 1, 7 columns + // Options line 1, 4 columns Label label3 = new Label(mGroupOptions, SWT.NONE); label3.setText("Show:"); - + GridDataBuilder.create(label3).vSpan(2).vTop(); mCheckFilterNew = new Button(mGroupOptions, SWT.CHECK); - mCheckFilterNew.setText("Updates/New"); - mCheckFilterNew.setToolTipText("Show Updates and New"); - mCheckFilterNew.addSelectionListener(new SelectionAdapter() { + mCheckFilterNew.setText("New"); + mCheckFilterNew.setToolTipText("Show New"); + mCheckFilterNew.addSelectionListener(new SelectionAdapter() + { @Override public void widgetSelected(SelectionEvent e) { refreshViewerInput(); } }); mCheckFilterNew.setSelection(true); - +/* mCheckFilterInstalled = new Button(mGroupOptions, SWT.CHECK); mCheckFilterInstalled.setToolTipText("Show Installed"); - mCheckFilterInstalled.addSelectionListener(new SelectionAdapter() { + mCheckFilterInstalled.addSelectionListener(new SelectionAdapter() + { @Override public void widgetSelected(SelectionEvent e) { refreshViewerInput(); @@ -314,50 +280,48 @@ public void widgetSelected(SelectionEvent e) { }); mCheckFilterInstalled.setSelection(true); mCheckFilterInstalled.setText("Installed"); - - new Label(mGroupOptions, SWT.NONE); - - Link linkSelectNew = new Link(mGroupOptions, SWT.NONE); - // Note for i18n: we need to identify which link is used, and this is done by using the - // text itself so for translation purposes we want to keep the link strings separate. - final String strLinkNew = "New"; - final String strLinkUpdates = "Updates"; - linkSelectNew.setText( - String.format("Select %1$s or %2$s", strLinkNew, strLinkUpdates)); - linkSelectNew.setToolTipText("Selects all items that are either new or updates."); - GridDataBuilder.create(linkSelectNew).hFill(); - linkSelectNew.addSelectionListener(new SelectionAdapter() { +*/ + //new Label(mGroupOptions, SWT.NONE); + + Link linkSelectUpdates = new Link(mGroupOptions, SWT.NONE); + linkSelectUpdates.setText("Select Updates"); + linkSelectUpdates.setToolTipText("Selects all items that are updates."); + //GridDataBuilder.create(linkSelectUpdates).hFill(); + linkSelectUpdates.addSelectionListener(new SelectionAdapter() + { @Override public void widgetSelected(SelectionEvent e) { super.widgetSelected(e); - boolean selectNew = e.text == null || e.text.equals(strLinkNew); - onSelectNewUpdates(selectNew, !selectNew, false/*selectTop*/); + onSelectPackages(true, false); // selectTop } }); // placeholder between "select all" and "install" - Label placeholder = new Label(mGroupOptions, SWT.NONE); - GridDataBuilder.create(placeholder).hFill().hGrab(); + //Label placeholder = new Label(mGroupOptions, SWT.NONE); + //GridDataBuilder.create(placeholder).hFill().hGrab(); mButtonInstall = new Button(mGroupOptions, SWT.NONE); mButtonInstall.setText(""); //$NON-NLS-1$ placeholder, filled in updateButtonsState() mButtonInstall.setToolTipText("Install one or more packages"); - GridDataBuilder.create(mButtonInstall).vCenter().wHint(150); - mButtonInstall.addSelectionListener(new SelectionAdapter() { + GridDataBuilder.create(mButtonInstall).vCenter().wHint(150).hFill().hGrab().hRight(); + mButtonInstall.addSelectionListener(new SelectionAdapter() + { @Override public void widgetSelected(SelectionEvent e) { onButtonInstall(); //$hide$ } }); - // Options line 2, 7 columns + // Options line 2, 4 columns - new Label(mGroupOptions, SWT.NONE); + //Label placeholder2 = new Label(mGroupOptions, SWT.NONE); + //GridDataBuilder.create(placeholder2).hFill().hGrab(); mCheckFilterObsolete = new Button(mGroupOptions, SWT.CHECK); mCheckFilterObsolete.setText("Obsolete"); mCheckFilterObsolete.setToolTipText("Also show obsolete packages"); - mCheckFilterObsolete.addSelectionListener(new SelectionAdapter() { + mCheckFilterObsolete.addSelectionListener(new SelectionAdapter() + { @Override public void widgetSelected(SelectionEvent e) { refreshViewerInput(); @@ -366,14 +330,15 @@ public void widgetSelected(SelectionEvent e) { mCheckFilterObsolete.setSelection(false); // placeholder before "deselect" - new Label(mGroupOptions, SWT.NONE); - new Label(mGroupOptions, SWT.NONE); + //new Label(mGroupOptions, SWT.NONE); + //new Label(mGroupOptions, SWT.NONE); Link linkDeselect = new Link(mGroupOptions, SWT.NONE); linkDeselect.setText("Deselect All"); linkDeselect.setToolTipText("Deselects all the currently selected items"); - GridDataBuilder.create(linkDeselect).hFill(); - linkDeselect.addSelectionListener(new SelectionAdapter() { + //GridDataBuilder.create(linkDeselect).hFill(); + linkDeselect.addSelectionListener(new SelectionAdapter() + { @Override public void widgetSelected(SelectionEvent e) { super.widgetSelected(e); @@ -382,37 +347,47 @@ public void widgetSelected(SelectionEvent e) { }); // placeholder between "deselect" and "delete" - placeholder = new Label(mGroupOptions, SWT.NONE); - GridDataBuilder.create(placeholder).hFill().hGrab(); + //placeholder = new Label(mGroupOptions, SWT.NONE); + //GridDataBuilder.create(placeholder).hFill().hGrab(); mButtonDelete = new Button(mGroupOptions, SWT.NONE); mButtonDelete.setText(""); //$NON-NLS-1$ placeholder, filled in updateButtonsState() mButtonDelete.setToolTipText("Delete one ore more installed packages"); - GridDataBuilder.create(mButtonDelete).vCenter().wHint(150); - mButtonDelete.addSelectionListener(new SelectionAdapter() { + GridDataBuilder.create(mButtonDelete).vCenter().wHint(150).hFill().hGrab().hRight(); + mButtonDelete.addSelectionListener(new SelectionAdapter() + { @Override public void widgetSelected(SelectionEvent e) { onButtonDelete(); //$hide$ } }); - } - - private PackagesPageImpl.ITreeViewerColumn wrapColumn(final TreeViewerColumn column) { - return new PackagesPageImpl.ITreeViewerColumn() { + FontData fontData = tree.getFont().getFontData()[0]; + fontData.setStyle(SWT.ITALIC); + mTreeFontItalic = new Font(tree.getDisplay(), fontData); + tree.addDisposeListener(new DisposeListener() { @Override - public void setLabelProvider(ColumnLabelProvider labelProvider) { - column.setLabelProvider(labelProvider); + public void widgetDisposed(DisposeEvent e) { + mTreeFontItalic.dispose(); + mTreeFontItalic = null; } - }; + }); + mTreeViewer.setContentProvider(new PackageContentProvider(mSdkContext, mTreeViewer)); + PkgCellAgent pkgCellAgent = new PkgCellAgent(mSdkContext, mPackageAnalyser, mTreeFontItalic); + columnApi.setLabelProvider( + new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(pkgCellAgent, PkgCellLabelProvider.API))); + columnName.setLabelProvider( + new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(pkgCellAgent, PkgCellLabelProvider.NAME))); + columnStatus.setLabelProvider( + new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(pkgCellAgent, PkgCellLabelProvider.STATUS))); + columnRevision.setLabelProvider( + new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(pkgCellAgent, PkgCellLabelProvider.REVISION))); } private Image getImage(String filename) { - if (mImpl.mSwtUpdaterData != null) { - ImageFactory imgFactory = mImpl.mSwtUpdaterData.getImageFactory(); + ImageFactory imgFactory = mSdkContext.getSdkHelper().getImageFactory(); if (imgFactory != null) { return imgFactory.getImageByName(filename); } - } return null; } @@ -425,37 +400,22 @@ private Image getImage(String filename) { // --- menu interactions --- public void registerMenuAction(final MenuAction action, MenuItem item) { - item.addSelectionListener(new SelectionAdapter() { + item.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { - Button button = null; + Button button = null; switch (action) { case RELOAD: - mImpl.fullReload(); - break; - case SHOW_ADDON_SITES: - AddonSitesDialog d = new AddonSitesDialog(getShell(), mImpl.mSwtUpdaterData); - if (d.open()) { - mImpl.loadPackages(); - } - break; - case TOGGLE_SHOW_ARCHIVES: - mDisplayArchives = !mDisplayArchives; - // Force the viewer to be refreshed - ((PkgContentProvider) mTreeViewer.getContentProvider()). - setDisplayArchives(mDisplayArchives); - mTreeViewer.setInput(null); - refreshViewerInput(); - syncViewerSelection(); - break; - case TOGGLE_SHOW_INSTALLED_PKG: - button = mCheckFilterInstalled; + startLoadPackages(); break; +// case TOGGLE_SHOW_INSTALLED_PKG: +// button = mCheckFilterInstalled; +// break; case TOGGLE_SHOW_OBSOLETE_PKG: button = mCheckFilterObsolete; break; - case TOGGLE_SHOW_UPDATE_NEW_PKG: + case TOGGLE_SHOW_NEW_PKG: button = mCheckFilterNew; break; } @@ -487,7 +447,7 @@ public void widgetSelected(SelectionEvent e) { } updateMenuCheckmarks(); - } + } }); mMenuActions.put(action, item); @@ -496,7 +456,6 @@ public void widgetSelected(SelectionEvent e) { // --- internal methods --- private void updateMenuCheckmarks() { - for (Entry entry : mMenuActions.entrySet()) { MenuAction action = entry.getKey(); MenuItem item = entry.getValue(); @@ -509,20 +468,16 @@ private void updateMenuCheckmarks() { Button button = null; switch (action) { - case TOGGLE_SHOW_ARCHIVES: - value = mDisplayArchives; - break; - case TOGGLE_SHOW_INSTALLED_PKG: - button = mCheckFilterInstalled; - break; + //case TOGGLE_SHOW_INSTALLED_PKG: + // button = mCheckFilterInstalled; + // break; case TOGGLE_SHOW_OBSOLETE_PKG: button = mCheckFilterObsolete; break; - case TOGGLE_SHOW_UPDATE_NEW_PKG: + case TOGGLE_SHOW_NEW_PKG: button = mCheckFilterNew; break; case RELOAD: - case SHOW_ADDON_SITES: // No checkmark to update break; } @@ -538,60 +493,71 @@ private void updateMenuCheckmarks() { } private void postCreate() { - mImpl.postCreate(); - - if (mImpl.mSwtUpdaterData != null) { - mTextSdkOsPath.setText(mImpl.mSwtUpdaterData.getOsSdkRoot()); - } + mTextSdkOsPath.setText(mSdkContext.getLocation().toString()); - ((PkgContentProvider) mTreeViewer.getContentProvider()).setDisplayArchives( + ((PackageContentProvider) mTreeViewer.getContentProvider()).setDisplayArchives( mDisplayArchives); ColumnViewerToolTipSupport.enableFor(mTreeViewer, ToolTip.NO_RECREATE); - Tree tree = mTreeViewer.getTree(); - FontData fontData = tree.getFont().getFontData()[0]; - fontData.setStyle(SWT.ITALIC); - mTreeFontItalic = new Font(tree.getDisplay(), fontData); - - tree.addDisposeListener(new DisposeListener() { - @Override - public void widgetDisposed(DisposeEvent e) { - mTreeFontItalic.dispose(); - mTreeFontItalic = null; - } - }); } - private void loadPackages(boolean useLocalCache, boolean overrideExisting) { - if (mImpl.mSwtUpdaterData == null) { - return; - } - - // LoadPackage is synchronous but does not block the UI. - // Consequently it's entirely possible for the user - // to request the app to close whilst the packages are loading. Any - // action done after loadPackages must check the UI hasn't been - // disposed yet. Otherwise hilarity ensues. - + private void startLoadPackages() { + if (mTreeColumnName.isDisposed()) { // If the UI got disposed, don't try to load anything since we won't be // able to display it anyway. return; } - + // Packages will be loaded when onReady() is called + if (mProgressRunner == null) + return; mTreeColumnName.setImage(getImage(PackagesPageIcons.ICON_SORT_BY_API)); - mImpl.loadPackagesImpl(useLocalCache, overrideExisting); + PackageManager packageManager = mSdkContext.getPackageManager(); + LoadPackagesRequest loadPackagesRequest = new LoadPackagesRequest(mProgressRunner); + //RepoLoadedCallback onLocalComplete = new RepoLoadedCallback(){ + + // @Override + // public void doRun(RepositoryPackages packages) { + // }}; + RepoLoadedCallback onSuccess = new RepoLoadedCallback(){ + @Override + public void doRun(RepositoryPackages packages) { + if (!(mGroupPackages == null || mGroupPackages.isDisposed())) + { + packageManager.setPackages(packages); + mPackageAnalyser.loadPackages(); + mGroupPackages.getDisplay().syncExec(new Runnable(){ + + @Override + public void run() { + // automatically select all new and update packages. + Object[] checked = mTreeViewer.getCheckedElements(); + if (checked == null || checked.length == 0) + onSelectPackages( + true, //selectUpdates, + true); //selectTop + refreshViewerInput(); + }}); + mSdkProgressControl.setDescription("Done loading packages."); + } + }}; + Runnable onError = new Runnable(){ + @Override + public void run() { + mSdkProgressControl.setDescription("Package operation did not complete due to error or cancellation"); + }}; + //loadPackagesRequest.setOnLocalComplete(Collections.singletonList(onLocalComplete)); + loadPackagesRequest.setOnSuccess(Collections.singletonList(onSuccess)); + loadPackagesRequest.setOnError(Collections.singletonList(onError)); + packageManager.requestRepositoryPackages(loadPackagesRequest); } private void refreshViewerInput() { - // Dynamically update the table while we load after each source. - // Since the official Android source gets loaded first, it makes the - // window look non-empty a lot sooner. if (!mGroupPackages.isDisposed()) { try { - mImpl.setViewerInput(); + setViewerInput(); } catch (Exception ignore) {} // set the initial expanded state @@ -601,13 +567,30 @@ private void refreshViewerInput() { updateMenuCheckmarks(); } } + + /** + * Invoked from {@link #refreshViewerInput()} to actually either set the + * input of the tree viewer or refresh it if it's the same input + * object. + */ + protected void setViewerInput() { + List> cats = mPackageAnalyser.getApiCategories(); + if (mTreeViewer.getInput() != cats) { + // set initial input + mTreeViewer.setInput(cats); + } else { + // refresh existing, which preserves the expanded state, the selection + // and the checked state. + mTreeViewer.refresh(); + } + } /** * Decide whether to keep an item in the current tree based on user-chosen filter options. */ private boolean filterViewerItem(Object treeElement) { if (treeElement instanceof PkgCategory) { - PkgCategory cat = (PkgCategory) treeElement; + PkgCategory cat = (PkgCategory) treeElement; if (!cat.getItems().isEmpty()) { // A category is hidden if all of its content is hidden. @@ -630,15 +613,15 @@ private boolean filterViewerItem(Object treeElement) { return false; } } - +/* if (!mCheckFilterInstalled.getSelection()) { if (item.getState() == PkgState.INSTALLED) { return false; } } - +*/ if (!mCheckFilterNew.getSelection()) { - if (item.getState() == PkgState.NEW || item.hasUpdatePkg()) { + if (item.getState() == PkgState.NEW ) { //|| item.hasUpdatePkg() return false; } } @@ -662,24 +645,21 @@ private void expandInitial(Object elem) { if (mTreeViewer != null && !mTreeViewer.getTree().isDisposed()) { boolean enablePreviews = - mImpl.mSwtUpdaterData.getSettingsController().getSettings().getEnablePreviews(); + mSdkContext.getSettings().getEnablePreviews(); mTreeViewer.setExpandedState(elem, true); nextCategory: for (Object pkg : ((ITreeContentProvider) mTreeViewer.getContentProvider()). getChildren(elem)) { if (pkg instanceof PkgCategory) { - PkgCategory cat = (PkgCategory) pkg; - + PkgCategory cat = (PkgCategory) pkg; // Always expand the Tools category (and the preview one, if enabled) - if (cat.getKey().equals(PkgCategoryApi.KEY_TOOLS) || + if ((cat.getKeyType() == CategoryKeyType.TOOLS) || (enablePreviews && - cat.getKey().equals(PkgCategoryApi.KEY_TOOLS_PREVIEW))) { + (cat.getKeyType() == CategoryKeyType.TOOLS_PREVIEW))) { expandInitial(pkg); continue nextCategory; } - - for (PkgItem item : cat.getItems()) { if (item.getState() == PkgState.INSTALLED) { expandInitial(pkg); @@ -831,21 +811,23 @@ private void selectCompatibleArchives(Object pkg, ITreeContentProvider provider) } /** - * Checks all PkgItems that are either new or have updates or select top platform - * for initial run. + * Mark packages as checked according to selection criteria. */ - private void onSelectNewUpdates(boolean selectNew, boolean selectUpdates, boolean selectTop) { + private void onSelectPackages(boolean selectUpdates, boolean selectTop) { // This will update the tree's "selected" state and then invoke syncViewerSelection() // which will in turn update tree. - mImpl.onSelectNewUpdates(selectNew, selectUpdates, selectTop); + mPackageAnalyser.checkNewUpdateItems( + selectUpdates, + selectTop); + mTreeViewer.setInput(mPackageAnalyser.getApiCategories()); + syncViewerSelection(); } /** * Deselect all checked PkgItems. */ private void onDeselectAll() { - // This does not update the tree itself, syncViewerSelection does it below. - mImpl.onDeselectAll(); + mPackageAnalyser.uncheckAllItems(); syncViewerSelection(); } @@ -910,7 +892,7 @@ private void endOperationPending() { */ private void updateButtonsState() { if (!mButtonInstall.isDisposed()) { - int numPackages = getArchivesForInstall(null /*archives*/); + int numPackages = getPackagesForInstall(null /*archives*/); mButtonInstall.setEnabled((numPackages > 0) && !mOperationPending); mButtonInstall.setText( @@ -936,115 +918,101 @@ private void updateButtonsState() { * Collects the packages to be installed and shows the installation window. */ private void onButtonInstall() { - ArrayList archives = new ArrayList(); - getArchivesForInstall(archives); + List outPackages = new ArrayList<>(); + getPackagesForInstall(outPackages); + List remotes = new ArrayList<>(); + Map updateMap = new HashMap<>(); + for (PkgItem item: outPackages) { + RemotePackage remotePackage = (RemotePackage)item.getMainPackage(); + remotes.add(remotePackage); + if (item.hasUpdatePkg()) + updateMap.put(remotePackage, item.getUpdatePkg()); + } + ProgressIndicator progress = mSdkContext.getProgressIndicator(); + remotes = InstallerUtil.computeRequiredPackages( + remotes, mSdkContext.getPackages(), progress); + if (remotes == null) { + progress.logWarning("Unable to compute a complete list of dependencies."); + return; + } + Iterator iterator = remotes.iterator(); + while (iterator.hasNext()) + { + if (updateMap.keySet().contains(iterator.next())) + iterator.remove(); + } + boolean needsRefresh = false; + try { + beginOperationPending(); + List acceptedRemotes = null; + SdkUpdaterChooserDialog dialog = + new SdkUpdaterChooserDialog(getShell(), mSdkContext, updateMap.values(), remotes); + dialog.open(); + + acceptedRemotes = dialog.getResult(); + needsRefresh = (acceptedRemotes != null) && (acceptedRemotes.size() > 0); + if (needsRefresh) { + int count = mSdkContext.getPackageManager().installPackages(remotes, acceptedRemotes, mTaskFactory, 0); + if (count == 0) { + needsRefresh = false; + mSdkProgressControl.setDescription("Done. Nothing was installed."); + } + else { + mSdkProgressControl.setDescription(String.format("Done. %1$d %2$s installed.", + count, + count == 1 ? "package" : "packages")); - if (mImpl.mSwtUpdaterData != null) { - boolean needsRefresh = false; - try { - beginOperationPending(); - - List installed = mImpl.mSwtUpdaterData.updateOrInstallAll_WithGUI( - archives, - mCheckFilterObsolete.getSelection() /* includeObsoletes */, - mContext == SdkInvocationContext.IDE ? - SwtUpdaterData.TOOLS_MSG_UPDATED_FROM_ADT : - SwtUpdaterData.TOOLS_MSG_UPDATED_FROM_SDKMAN); - needsRefresh = installed != null && !installed.isEmpty(); - } finally { - endOperationPending(); - - if (needsRefresh) { - // The local package list has changed, make sure to refresh it - mImpl.localReload(); + //notify listeners something was installed, so that they can refresh + //reloadSdk(); } } + } finally { + endOperationPending(); + + if (needsRefresh) { + // The local package list has changed, make sure to refresh it + startLoadPackages(); + } } } /** - * Selects the archives that can be installed. - * This can be used with a null {@code outArchives} just to count the number of - * installable archives. + * Selects the packages that can be installed. + * This can be used with a null {@code outPackageItems} just to count the number of + * installable packages. * - * @param outArchives An archive list where to add the archives that can be installed. + * @param outPackageItems A package item list to return remote packages. * This can be null. * @return The number of archives that can be installed. */ - private int getArchivesForInstall(List outArchives) { + private int getPackagesForInstall(List outPackageItems) { if (mTreeViewer == null || - mTreeViewer.getTree() == null || - mTreeViewer.getTree().isDisposed()) { + mTreeViewer.getTree() == null || + mTreeViewer.getTree().isDisposed()) { return 0; } Object[] checked = mTreeViewer.getCheckedElements(); if (checked == null) { return 0; } - int count = 0; - - // Give us a way to force install of incompatible archives. - boolean checkIsCompatible = - System.getenv(ArchiveInstaller.ENV_VAR_IGNORE_COMPAT) == null; - - if (mDisplayArchives) { - // In detail mode, we display archives so we can install only the - // archives that are actually selected. - - for (Object c : checked) { - if (c instanceof Archive) { - Archive a = (Archive) c; - if (a != null) { - if (checkIsCompatible && !a.isCompatible()) { - continue; - } - count++; - if (outArchives != null) { - outArchives.add((Archive) c); - } - } - } - } - } else { - // In non-detail mode, we install all the compatible archives - // found in the selected pkg items. We also automatically - // select update packages rather than the root package if any. - - for (Object c : checked) { - Package p = null; - if (c instanceof Package) { - // This is an update package - p = (Package) c; - } else if (c instanceof PkgItem) { - p = ((PkgItem) c).getMainPackage(); - - PkgItem pi = (PkgItem) c; - if (pi.getState() == PkgState.INSTALLED) { - // We don't allow installing items that are already installed - // unless they have a pending update. - p = pi.getUpdatePkg(); - - } else if (pi.getState() == PkgState.NEW) { - p = pi.getMainPackage(); - } + for (Object c : checked) { + if (c instanceof PkgItem) { + PkgItem packageItem = (PkgItem)c; + RemotePackage remotePackage = null; + if (packageItem.hasUpdatePkg()) { + remotePackage = packageItem.getUpdatePkg().getRemote(); + } else if (packageItem.getState() == PkgState.NEW) { + remotePackage = (RemotePackage) packageItem.getMainPackage(); } - if (p != null) { - for (Archive a : p.getArchives()) { - if (a != null) { - if (checkIsCompatible && !a.isCompatible()) { - continue; - } - count++; - if (outArchives != null) { - outArchives.add(a); - } - } + if (remotePackage != null) { + count++; + if (outPackageItems != null) { + outPackageItems.add(packageItem); } } } } - return count; } @@ -1057,29 +1025,38 @@ private void onButtonDelete() { final String title = "Delete SDK Package"; StringBuilder msg = new StringBuilder("Are you sure you want to delete:"); - // A list of archives to delete - final ArrayList archives = new ArrayList(); + // A list of package items to delete + final ArrayList outPackageItems = new ArrayList(); - getArchivesToDelete(msg, archives); + getArchivesToDelete(msg, outPackageItems); - if (!archives.isEmpty()) { + if (!outPackageItems.isEmpty()) { msg.append("\n").append("This cannot be undone."); //$NON-NLS-1$ if (MessageDialog.openQuestion(getShell(), title, msg.toString())) { try { beginOperationPending(); - mImpl.mSwtUpdaterData.getTaskFactory().start("Delete Package", new ITask() { + mTaskFactory.start("Delete Package", new ITask() { @Override public void run(ITaskMonitor monitor) { - monitor.setProgressMax(archives.size() + 1); - for (Archive a : archives) { + monitor.setProgressMax(outPackageItems.size() + 1); + for (PkgItem packageItem : outPackageItems) { + LocalPackage localPackage = (LocalPackage)packageItem.getMainPackage(); monitor.setDescription("Deleting '%1$s' (%2$s)", - a.getParentPackage().getShortDescription(), - a.getLocalOsPath()); + localPackage.getDisplayName(), + localPackage.getPath()); // Delete the actual package - a.deleteLocal(); - + Uninstaller uninstaller = SdkInstallerUtil.findBestInstallerFactory(localPackage, mSdkContext.getHandler()) + .createUninstaller(localPackage, mSdkContext.getRepoManager(), mSdkContext.getFileOp()); + if (applyPackageOperation(uninstaller)) { + packageItem.markDeleted(); + } else { + // there was an error, abort. + monitor.error(null, "Uninstall of package failed due to an error"); + monitor.setProgressMax(0); + break; + } monitor.incProgress(1); if (monitor.isCancelRequested()) { break; @@ -1088,18 +1065,26 @@ public void run(ITaskMonitor monitor) { monitor.incProgress(1); monitor.setDescription("Done"); + mPackageAnalyser.removeDeletedNodes(); } }); } finally { endOperationPending(); // The local package list has changed, make sure to refresh it - mImpl.localReload(); + startLoadPackages(); } - } + } } } + + private boolean applyPackageOperation( + @NonNull PackageOperation operation) { + ProgressIndicator progressIndicator = mSdkContext.getProgressIndicator(); + return operation.prepare(progressIndicator) && operation.complete(progressIndicator); + } + /** * Selects the archives that can be deleted and collect their names. * This can be used with a null {@code outArchives} and a null {@code outMsg} @@ -1107,11 +1092,11 @@ public void run(ITaskMonitor monitor) { * * @param outMsg A StringBuilder where the names of the packages to be deleted is * accumulated. This is used to confirm deletion with the user. - * @param outArchives An archive list where to add the archives that can be installed. + * @param outPackageItems A package item list to return local packages * This can be null. * @return The number of archives that can be deleted. */ - private int getArchivesToDelete(StringBuilder outMsg, List outArchives) { + private int getArchivesToDelete(StringBuilder outMsg, List outPackageItems) { if (mTreeViewer == null || mTreeViewer.getTree() == null || mTreeViewer.getTree().isDisposed()) { @@ -1124,92 +1109,43 @@ private int getArchivesToDelete(StringBuilder outMsg, List outArchives) } int count = 0; - - if (mDisplayArchives) { - // In detail mode, select archives that can be deleted - - for (Object c : checked) { - if (c instanceof Archive) { - Archive a = (Archive) c; - if (a != null && a.isLocal()) { - count++; - if (outMsg != null) { - String osPath = a.getLocalOsPath(); - File dir = new File(osPath); - Package p = a.getParentPackage(); - if (p != null && dir.isDirectory()) { - outMsg.append("\n - ") //$NON-NLS-1$ - .append(p.getShortDescription()); - } - } - if (outArchives != null) { - outArchives.add(a); + for (Object c : checked) { + if (c instanceof PkgItem) { + PkgItem packageItem = (PkgItem) c; + PkgState state = packageItem.getState(); + if (state == PkgState.INSTALLED) { + LocalPackage localPackage = (LocalPackage)packageItem.getMainPackage(); + count++; + if (outMsg != null) { + File dir = new File(localPackage.getPath()); + if (dir.isDirectory()) { + outMsg.append("\n - ") //$NON-NLS-1$ + .append(localPackage.getDisplayName()); } } - } - } - } else { - // In non-detail mode, select archives of selected packages that can be deleted. - - for (Object c : checked) { - if (c instanceof PkgItem) { - PkgItem pi = (PkgItem) c; - PkgState state = pi.getState(); - if (state == PkgState.INSTALLED) { - Package p = pi.getMainPackage(); - - for (Archive a : p.getArchives()) { - if (a != null && a.isLocal()) { - count++; - if (outMsg != null) { - String osPath = a.getLocalOsPath(); - File dir = new File(osPath); - if (dir.isDirectory()) { - outMsg.append("\n - ") //$NON-NLS-1$ - .append(p.getShortDescription()); - } - } - if (outArchives != null) { - outArchives.add(a); - } - } - } + if (outPackageItems != null) { + outPackageItems.add(packageItem); } } } } - return count; } // ---------------------- - - // --- Implementation of ISdkChangeListener --- - - @Override - public void onSdkLoaded() { - onSdkReload(); - } - - @Override - public void onSdkReload() { - // The sdkmanager finished reloading its data. We must not call localReload() from here - // since we don't want to alter the sdkmanager's data that just finished loading. - mImpl.loadPackages(); + public void onReady(ProgressRunner progressRunner, ILogUiProvider sdkProgressControl, ITaskFactory taskFactory) { + mProgressRunner = progressRunner; + mSdkProgressControl = sdkProgressControl; + mTaskFactory = taskFactory; + startLoadPackages(); } - @Override - public void preInstallHook() { - // nothing to be done for now. - } - @Override - public void postInstallHook() { - // nothing to be done for now. + public void onSdkReload() { + startLoadPackages(); } - // --- End of hiding from SWT Designer --- //$hide<<$ } diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPageImpl.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPageImpl.java deleted file mode 100644 index 71f990e6..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/PackagesPageImpl.java +++ /dev/null @@ -1,564 +0,0 @@ -/* - * Copyright (C) 2012 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.sdkuilib.internal.repository.ui; - -import com.android.SdkConstants; -import com.android.sdklib.AndroidVersion; -import com.android.sdklib.internal.repository.DownloadCache; -import com.android.sdklib.internal.repository.DownloadCache.Strategy; -import com.android.sdklib.internal.repository.archives.Archive; -import com.android.sdklib.internal.repository.packages.Package; -import com.android.sdklib.internal.repository.sources.SdkSource; -import com.android.sdklib.internal.repository.updater.PackageLoader; -import com.android.sdklib.internal.repository.updater.PackageLoader.ISourceLoadedCallback; -import com.android.sdklib.internal.repository.updater.PkgItem; -import com.android.sdklib.internal.repository.updater.PkgItem.PkgState; -import com.android.sdklib.repository.IDescription; -import com.android.sdkuilib.internal.repository.SwtUpdaterData; -import com.android.sdkuilib.internal.repository.core.PackagesDiffLogic; -import com.android.sdkuilib.internal.repository.core.PkgCategory; -import com.android.sdkuilib.internal.repository.core.PkgCategoryApi; -import com.android.sdkuilib.internal.repository.core.PkgContentProvider; -import com.android.sdkuilib.internal.repository.icons.ImageFactory; - -import org.eclipse.jface.viewers.ColumnLabelProvider; -import org.eclipse.jface.viewers.IInputProvider; -import org.eclipse.jface.viewers.ITableFontProvider; -import org.eclipse.swt.graphics.Font; -import org.eclipse.swt.graphics.Image; -import org.eclipse.swt.graphics.Point; - -import java.net.MalformedURLException; -import java.net.URL; -import java.util.List; - -/** - * Base class for {@link PackagesPage} that holds most of the logic to display - * the tree/list of packages. This class holds most of the logic and {@link PackagesPage} - * holds most of the UI (creating the UI, dealing with menus and buttons and tree - * selection.) This makes it easier to test the functionality by mocking only a - * subset of the UI. - */ -abstract class PackagesPageImpl { - - final SwtUpdaterData mSwtUpdaterData; - final PackagesDiffLogic mDiffLogic; - - private ICheckboxTreeViewer mITreeViewer; - private ITreeViewerColumn mIColumnName; - private ITreeViewerColumn mIColumnApi; - private ITreeViewerColumn mIColumnRevision; - private ITreeViewerColumn mIColumnStatus; - - PackagesPageImpl(SwtUpdaterData swtUpdaterData) { - mSwtUpdaterData = swtUpdaterData; - mDiffLogic = new PackagesDiffLogic(swtUpdaterData); - } - - /** - * Utility method that derived classes can override to check whether the UI is disposed. - * When the UI is disposed, most operations that affect the UI will be bypassed. - * @return True if UI is not available and should not be touched. - */ - abstract protected boolean isUiDisposed(); - - /** - * Utility method to execute a runnable on the main UI thread. - * Will do nothing if {@link #isUiDisposed()} returns false. - * @param runnable The runnable to execute on the main UI thread. - */ - abstract protected void syncExec(Runnable runnable); - - /** - * Synchronizes the 'checked' state of PkgItems in the tree with their internal isChecked state. - */ - abstract protected void syncViewerSelection(); - - void performFirstLoad() { - // First a package loader is created that only checks - // the local cache xml files. It populates the package - // list based on what the client got last, essentially. - loadPackages(true /*useLocalCache*/, false /*overrideExisting*/); - - // Next a regular package loader is created that will - // respect the expiration and refresh parameters of the - // download cache. - loadPackages(false /*useLocalCache*/, true /*overrideExisting*/); - } - - public void setITreeViewer(ICheckboxTreeViewer iTreeViewer) { - mITreeViewer = iTreeViewer; - } - - public void setIColumns( - ITreeViewerColumn columnName, - ITreeViewerColumn columnApi, - ITreeViewerColumn columnRevision, - ITreeViewerColumn columnStatus) { - mIColumnName = columnName; - mIColumnApi = columnApi; - mIColumnRevision = columnRevision; - mIColumnStatus = columnStatus; - } - - void postCreate() { - // Caller needs to call setITreeViewer before this. - assert mITreeViewer != null; - // Caller needs to call setIColumns before this. - assert mIColumnApi != null; - assert mIColumnName != null; - assert mIColumnStatus != null; - assert mIColumnRevision != null; - - mITreeViewer.setContentProvider(new PkgContentProvider(mITreeViewer)); - - mIColumnApi.setLabelProvider( - new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(mIColumnApi))); - mIColumnName.setLabelProvider( - new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(mIColumnName))); - mIColumnStatus.setLabelProvider( - new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(mIColumnStatus))); - mIColumnRevision.setLabelProvider( - new PkgTreeColumnViewerLabelProvider(new PkgCellLabelProvider(mIColumnRevision))); - } - - /** - * Performs a full reload by removing all cached packages data, including the platforms - * and addons from the sdkmanager instance. This will perform a full local parsing - * as well as a full reload of the remote data (by fetching all sources again.) - */ - void fullReload() { - // Clear all source information, forcing them to be refreshed. - mSwtUpdaterData.getSources().clearAllPackages(); - // Clear and reload all local data too. - localReload(); - } - - /** - * Performs a full reload of all the local package information, including the platforms - * and addons from the sdkmanager instance. This will perform a full local parsing. - *

- * This method does NOT force a new fetch of the remote sources. - * - * @see #fullReload() - */ - void localReload() { - // Clear all source caches, otherwise loading will use the cached data - mSwtUpdaterData.getLocalSdkParser().clearPackages(); - mSwtUpdaterData.getSdkManager().reloadSdk(mSwtUpdaterData.getSdkLog()); - loadPackages(); - } - - /** - * Performs a "normal" reload of the package information, use the default download - * cache and refreshing strategy as needed. - */ - void loadPackages() { - loadPackages(false /*useLocalCache*/, false /*overrideExisting*/); - } - - /** - * Performs a reload of the package information. - * - * @param useLocalCache When true, the {@link PackageLoader} is switched to use - * a specific {@link DownloadCache} using the {@link Strategy#ONLY_CACHE}, meaning - * it will only use data from the local cache. It will not try to fetch or refresh - * manifests. This is used once the very first time the sdk manager window opens - * and is typically followed by a regular load with refresh. - */ - abstract protected void loadPackages(boolean useLocalCache, boolean overrideExisting); - - /** - * Actual implementation of {@link #loadPackages(boolean, boolean)}. - * Derived implementations must call this to do the actual work after setting up the UI. - */ - void loadPackagesImpl(final boolean useLocalCache, final boolean overrideExisting) { - if (mSwtUpdaterData == null) { - return; - } - - PackageLoader packageLoader = getPackageLoader(useLocalCache); - assert packageLoader != null; - - mDiffLogic.updateStart(); - packageLoader.loadPackages(overrideExisting, new ISourceLoadedCallback() { - @Override - public boolean onUpdateSource(SdkSource source, Package[] newPackages) { - // This runs in a thread and must not access UI directly. - final boolean changed = mDiffLogic.updateSourcePackages(source, newPackages); - - syncExec(new Runnable() { - @Override - public void run() { - if (changed || - mITreeViewer.getInput() != mDiffLogic.getCategories()) { - refreshViewerInput(); - } - } - }); - - // Return true to tell the loader to continue with the next source. - // Return false to stop the loader if any UI has been disposed, which can - // happen if the user is trying to close the window during the load operation. - return !isUiDisposed(); - } - - @Override - public void onLoadCompleted() { - // This runs in a thread and must not access UI directly. - final boolean changed = mDiffLogic.updateEnd(); - - syncExec(new Runnable() { - @Override - public void run() { - if (changed || - mITreeViewer.getInput() != mDiffLogic.getCategories()) { - try { - refreshViewerInput(); - } catch (Exception ignore) {} - } - - if (!useLocalCache && - mDiffLogic.isFirstLoadComplete() && - !isUiDisposed()) { - // At the end of the first load, if nothing is selected then - // automatically select all new and update packages. - Object[] checked = mITreeViewer.getCheckedElements(); - if (checked == null || checked.length == 0) { - onSelectNewUpdates( - false, //selectNew - true, //selectUpdates, - true); //selectTop - } - } - } - }); - } - }); - } - - /** - * Used by {@link #loadPackagesImpl(boolean, boolean)} to get the package - * loader for the first or second pass update. When starting the manager - * starts with a first pass that reads only from the local cache, with no - * extra network access. That's {@code useLocalCache} being true. - *

- * Leter it does a second pass with {@code useLocalCache} set to false - * and actually uses the download cache specified in {@link SwtUpdaterData}. - * - * This is extracted so that we can control this cache via unit tests. - */ - protected PackageLoader getPackageLoader(boolean useLocalCache) { - if (useLocalCache) { - return new PackageLoader(mSwtUpdaterData, new DownloadCache(Strategy.ONLY_CACHE)); - } else { - return mSwtUpdaterData.getPackageLoader(); - } - } - - /** - * Overridden by the UI to respond to a request to refresh the tree viewer - * when the input has changed. - * The implementation must call {@link #setViewerInput()} somehow and will - * also need to adjust the expand state of the tree items and/or update - * some buttons or other state. - */ - abstract protected void refreshViewerInput(); - - /** - * Invoked from {@link #refreshViewerInput()} to actually either set the - * input of the tree viewer or refresh it if it's the same input - * object. - */ - protected void setViewerInput() { - List cats = mDiffLogic.getCategories(); - if (mITreeViewer.getInput() != cats) { - // set initial input - mITreeViewer.setInput(cats); - } else { - // refresh existing, which preserves the expanded state, the selection - // and the checked state. - mITreeViewer.refresh(); - } - } - - /** - * Checks all PkgItems that are either new or have updates or select top platform - * for initial run. - */ - void onSelectNewUpdates(boolean selectNew, boolean selectUpdates, boolean selectTop) { - // This does not update the tree itself, syncViewerSelection does it in the caller. - mDiffLogic.checkNewUpdateItems( - selectNew, - selectUpdates, - selectTop, - SdkConstants.CURRENT_PLATFORM); - syncViewerSelection(); - } - - /** - * Deselect all checked PkgItems. - */ - void onDeselectAll() { - // This does not update the tree itself, syncViewerSelection does it in the caller. - mDiffLogic.uncheckAllItems(); - } - - // ---------------------- - - abstract protected Font getTreeFontItalic(); - - class PkgCellLabelProvider extends ColumnLabelProvider implements ITableFontProvider { - - private final ITreeViewerColumn mColumn; - - public PkgCellLabelProvider(ITreeViewerColumn column) { - super(); - mColumn = column; - } - - @Override - public String getText(Object element) { - - if (mColumn == mIColumnName) { - if (element instanceof PkgCategory) { - return ((PkgCategory) element).getLabel(); - } else if (element instanceof PkgItem) { - return getPkgItemName((PkgItem) element); - } else if (element instanceof IDescription) { - return ((IDescription) element).getShortDescription(); - } - - } else if (mColumn == mIColumnApi) { - AndroidVersion version = null; - if (element instanceof PkgItem) { - version = ((PkgItem) element).getAndroidVersion(); - } - if (version != null) { - return version.getApiString(); - } - - } else if (mColumn == mIColumnRevision) { - if (element instanceof PkgItem) { - PkgItem pkg = (PkgItem) element; - return pkg.getRevision().toShortString(); - } - - } else if (mColumn == mIColumnStatus) { - if (element instanceof PkgItem) { - PkgItem pkg = (PkgItem) element; - - switch(pkg.getState()) { - case INSTALLED: - Package update = pkg.getUpdatePkg(); - if (update != null) { - return String.format( - "Update available: rev. %1$s", - update.getRevision().toShortString()); - } - return "Installed"; - - case NEW: - Package p = pkg.getMainPackage(); - if (p != null && p.hasCompatibleArchive()) { - return "Not installed"; - } else { - return String.format("Not compatible with %1$s", - SdkConstants.currentPlatformName()); - } - } - return pkg.getState().toString(); - - } else if (element instanceof Package) { - // This is an update package. - return "New revision " + ((Package) element).getRevision().toShortString(); - } - } - - return ""; //$NON-NLS-1$ - } - - private String getPkgItemName(PkgItem item) { - String name = item.getName().trim(); - - // When sorting by API, the package name might contains the API number - // or the platform name at the end. If we find it, cut it out since it's - // redundant. - - PkgCategoryApi cat = (PkgCategoryApi) findCategoryForItem(item); - String apiLabel = cat.getApiLabel(); - String platLabel = cat.getPlatformName(); - - if (platLabel != null && name.endsWith(platLabel)) { - return name.substring(0, name.length() - platLabel.length()); - - } else if (apiLabel != null && name.endsWith(apiLabel)) { - return name.substring(0, name.length() - apiLabel.length()); - - } else if (platLabel != null && item.isObsolete() && name.indexOf(platLabel) > 0) { - // For obsolete items, the format is " (Obsolete)" - // so in this case only accept removing a platform name that is not at - // the end. - name = name.replace(platLabel, ""); //$NON-NLS-1$ - } - - // Collapse potential duplicated spacing - name = name.replaceAll(" +", " "); //$NON-NLS-1$ //$NON-NLS-2$ - - return name; - } - - private PkgCategory findCategoryForItem(PkgItem item) { - List cats = mDiffLogic.getCategories(); - for (PkgCategory cat : cats) { - for (PkgItem i : cat.getItems()) { - if (i == item) { - return cat; - } - } - } - - return null; - } - - @Override - public Image getImage(Object element) { - ImageFactory imgFactory = mSwtUpdaterData.getImageFactory(); - - if (imgFactory != null) { - if (mColumn == mIColumnName) { - if (element instanceof PkgCategory) { - return imgFactory.getImageForObject(((PkgCategory) element).getIconRef()); - } else if (element instanceof PkgItem) { - return imgFactory.getImageForObject(((PkgItem) element).getMainPackage()); - } - return imgFactory.getImageForObject(element); - - } else if (mColumn == mIColumnStatus && element instanceof PkgItem) { - PkgItem pi = (PkgItem) element; - switch(pi.getState()) { - case INSTALLED: - if (pi.hasUpdatePkg()) { - return imgFactory.getImageByName(PackagesPageIcons.ICON_PKG_UPDATE); - } else { - return imgFactory.getImageByName(PackagesPageIcons.ICON_PKG_INSTALLED); - } - case NEW: - Package p = pi.getMainPackage(); - if (p != null && p.hasCompatibleArchive()) { - return imgFactory.getImageByName(PackagesPageIcons.ICON_PKG_NEW); - } else { - return imgFactory.getImageByName(PackagesPageIcons.ICON_PKG_INCOMPAT); - } - } - } - } - return super.getImage(element); - } - - // -- ITableFontProvider - - @Override - public Font getFont(Object element, int columnIndex) { - if (element instanceof PkgItem) { - if (((PkgItem) element).getState() == PkgState.NEW) { - return getTreeFontItalic(); - } - } else if (element instanceof Package) { - // update package - return getTreeFontItalic(); - } - return super.getFont(element); - } - - // -- Tooltip support - - @Override - public String getToolTipText(Object element) { - PkgItem pi = element instanceof PkgItem ? (PkgItem) element : null; - if (pi != null) { - element = pi.getMainPackage(); - } - if (element instanceof IDescription) { - String s = getTooltipDescription((IDescription) element); - - if (pi != null && pi.hasUpdatePkg()) { - s += "\n-----------------" + //$NON-NLS-1$ - "\nUpdate Available:\n" + //$NON-NLS-1$ - getTooltipDescription(pi.getUpdatePkg()); - } - - return s; - } - return super.getToolTipText(element); - } - - private String getTooltipDescription(IDescription element) { - String s = element.getLongDescription(); - if (element instanceof Package) { - Package p = (Package) element; - - if (!p.isLocal()) { - // For non-installed item, try to find a download size - for (Archive a : p.getArchives()) { - if (!a.isLocal() && a.isCompatible()) { - s += '\n' + a.getSizeDescription(); - break; - } - } - } - - // Display info about where this package comes/came from - SdkSource src = p.getParentSource(); - if (src != null) { - try { - URL url = new URL(src.getUrl()); - String host = url.getHost(); - if (p.isLocal()) { - s += String.format("\nInstalled from %1$s", host); - } else { - s += String.format("\nProvided by %1$s", host); - } - } catch (MalformedURLException ignore) { - } - } - } - return s; - } - - @Override - public Point getToolTipShift(Object object) { - return new Point(15, 5); - } - - @Override - public int getToolTipDisplayDelayTime(Object object) { - return 500; - } - } - - interface ICheckboxTreeViewer extends IInputProvider { - void setContentProvider(PkgContentProvider pkgContentProvider); - void refresh(); - void setInput(List cats); - Object[] getCheckedElements(); - } - - interface ITreeViewerColumn { - void setLabelProvider(ColumnLabelProvider labelProvider); - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/SdkProgressFactory.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/SdkProgressFactory.java new file mode 100644 index 00000000..52394419 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/SdkProgressFactory.java @@ -0,0 +1,288 @@ +/* + * Copyright (C) 2017 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.sdkuilib.internal.repository.ui; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.ProgressBar; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.repository.api.ProgressIndicator; +import com.android.repository.api.ProgressIndicatorAdapter; +import com.android.repository.api.ProgressRunner; +import com.android.sdkuilib.internal.repository.ITask; +import com.android.sdkuilib.internal.repository.ITaskFactory; +import com.android.sdkuilib.internal.repository.ITaskMonitor; +import com.android.sdkuilib.internal.tasks.ILogUiProvider; +import com.android.sdkuilib.internal.tasks.ProgressTask; +import com.android.sdkuilib.internal.tasks.ProgressView; +import com.android.sdkuilib.internal.tasks.SdkProgressIndicator; +import com.android.utils.ILogger; + +/** + * An {@link ITaskFactory} that creates a new {@link ProgressTask} dialog + * for each new task. + */ +public class SdkProgressFactory extends ProgressIndicatorAdapter implements ITaskFactory, ILogger, ProgressRunner { + + public interface ISdkLogWindow + { + void log(String log); + void logVerbose(String log); + void logError(String log); + void setDescription(String description); + void show(); + } + + private final ProgressView progressView; + private final ISdkLogWindow logWindow; + private final ProgressBar progressBar; + private int referenceCount; + + /** + * Creates a new {@link ProgressView} object, a simple "holder" for the various + * widgets used to display and update a progress + status bar. This object is + * provided to the factory. + * + * @param statusText The label to display titles of status updates (e.g. task titles and + * calls to {@link #setDescription(String)}.) Must not be null. + * @param progressBar The progress bar to update during a task. Must not be null. + * @param stopButton The stop button. It will be disabled when there's no task that can + * be interrupted. A selection listener will be attached to it. Optional. Can be null. + * @param logWindow Log adapter which can be requested to become visible when errors are logged + */ + public SdkProgressFactory( + Label statusText, + ProgressBar progressBar, + Control stopButton, + ISdkLogWindow logWindow) { + this.logWindow = logWindow; + this.progressBar = progressBar; + this.progressView = new ProgressView(statusText, progressBar, stopButton, getLogUiProvider(logWindow)); + } + + public ILogUiProvider getProgressControl() + { + return progressView; + } + + @Override + public void start(String title, ITask task) { + start(title, null /*monitor*/, task); + } + + @Override + public void start(String title, ITaskMonitor parentMonitor, ITask task) { + progressView.startTask(title, parentMonitor, task, true); + } + + // Returns object which delegates all logging to the logWindow window + // and filters errors to make sure the window is visible when + // an error is logged. + private ILogUiProvider getLogUiProvider(ISdkLogWindow logWindow) + { + return new ILogUiProvider() { + @Override + public void setDescription(String description) { + logWindow.setDescription(description); + } + + @Override + public void log(String log) { + logWindow.log(log); + } + + @Override + public void logVerbose(String log) { + logWindow.logVerbose(log); + } + + @Override + public void logError(String log) { + logWindow.logError(log); + logWindow.show(); + }}; + } + + // --- ILogger interface ---- // + @Override + public void error(Throwable throwable, String errorFormat, Object... arg) { + StringWriter builder = new StringWriter(); + if (errorFormat != null) + builder.append(String.format("Error: " + errorFormat, arg)); + + if (throwable != null) { + if (errorFormat == null) + builder.append("Error: ").append(throwable.getMessage()); + builder.append("\n"); + PrintWriter writer = new PrintWriter(builder); + throwable.printStackTrace(writer); + } + logWindow.logError(builder.toString()); + logWindow.show(); + } + + + @Override + public void info(String errorFormat, Object... arg) { + logWindow.log(String.format(errorFormat, arg)); + } + + + @Override + public void verbose(String errorFormat, Object... arg) { + logWindow.logVerbose(String.format(errorFormat, arg)); + } + + + @Override + public void warning(String errorFormat, Object... arg) { + // TODO - Add warning level + logWindow.log(String.format(errorFormat, arg)); + } + + // --- Logger component of IProgressIndicator interface ---- // + /** + * Logs a warning. + */ + @Override + public void logWarning(@NonNull String s) { + logWindow.log(s); + } + + /** + * Logs a warning, including a stacktrace. + */ + @Override + public void logWarning(@NonNull String s, @Nullable Throwable throwable) { + StringWriter builder = new StringWriter(); + builder.append(s); + if (throwable != null) { + builder.append("\n"); + PrintWriter writer = new PrintWriter(builder); + throwable.printStackTrace(writer); + } + logWindow.log(builder.toString()); + } + + /** + * Logs an error. + */ + @Override + public void logError(@NonNull String s) { + error(null, s); + } + + /** + * Logs an error, including a stacktrace. + */ + @Override + public void logError(@NonNull String s, @Nullable Throwable throwable) { + error(throwable, s); + } + + /** + * Logs an info message. + */ + @Override + public void logInfo(@NonNull String s) { + logWindow.log(s); + } + + @Override + public void logVerbose(@NonNull String s) { + logWindow.logVerbose(s); + } + + + @Override + public void runAsyncWithProgress(final ProgressRunnable progressRunnable) { + String title = this.getClass().getSimpleName(); + ProgressRunnable monitor = new ProgressRunnable(){ + + @Override + public void run(ProgressIndicator indicator, ProgressRunner runner) { + try + { + progressRunnable.run(indicator, runner); + } + finally + { + progressView.endTask(); + } + }}; + + Runnable runnable = new Runnable(){ + + @Override + public void run() { + progressView.startTask(title, null, taskInstance(monitor), false); + }}; + final Thread t = new Thread(runnable , title + "_async_thread_ " + referenceCount++); + t.start(); + } + + + @Override + public void runSyncWithProgress(ProgressRunnable progressRunnable) { + start("", taskInstance(progressRunnable)); + } + + + @Override + public void runSyncWithoutProgress(Runnable runnable) { + runnable.run(); + } + + private ITask taskInstance(ProgressRunnable progressRunnable) + { + return new ITask(){ + + @Override + public void run(ITaskMonitor monitor) { + ProgressIndicator progressIndicator = new SdkProgressIndicator(monitor); + ProgressRunner progressRunner = new ProgressRunner(){ + + @Override + public void runAsyncWithProgress(ProgressRunnable r) { + if (!progressIndicator.isCanceled()) + SdkProgressFactory.this.runAsyncWithProgress(r); + } + + @Override + public void runSyncWithProgress(ProgressRunnable r) { + if (!progressIndicator.isCanceled()) + SdkProgressFactory.this.runSyncWithProgress(r); + } + + @Override + public void runSyncWithoutProgress(Runnable r) { + if (!progressIndicator.isCanceled()) + SdkProgressFactory.this.runSyncWithoutProgress(r); + }}; + progressRunnable.run(progressIndicator, progressRunner); + } + }; + + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SdkUpdaterChooserDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/SdkUpdaterChooserDialog.java similarity index 67% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SdkUpdaterChooserDialog.java rename to andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/SdkUpdaterChooserDialog.java index 002011ba..60fc8905 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/SdkUpdaterChooserDialog.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/SdkUpdaterChooserDialog.java @@ -14,23 +14,22 @@ * limitations under the License. */ -package com.android.sdkuilib.internal.repository; +package com.android.sdkuilib.internal.repository.ui; -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.sdklib.AndroidVersion; -import com.android.sdklib.internal.repository.archives.Archive; -import com.android.sdklib.internal.repository.packages.IAndroidVersionProvider; -import com.android.sdklib.internal.repository.packages.Package; -import com.android.sdklib.internal.repository.sources.SdkSource; -import com.android.sdklib.internal.repository.updater.ArchiveInfo; -import com.android.sdklib.internal.repository.updater.SdkUpdaterLogic; -import com.android.sdklib.repository.FullRevision; -import com.android.sdklib.repository.License; -import com.android.sdkuilib.internal.repository.icons.ImageFactory; -import com.android.sdkuilib.ui.GridDialog; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import org.eclipse.andmore.sdktool.SdkContext; +import org.eclipse.andmore.sdktool.Utilities; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.IStructuredSelection; @@ -62,13 +61,19 @@ import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeColumn; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.TreeMap; +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.repository.api.License; +import com.android.repository.api.LocalPackage; +import com.android.repository.api.RemotePackage; +import com.android.repository.api.RepositorySource; +import com.android.repository.api.UpdatablePackage; +import com.android.repository.util.InstallerUtil; +import com.android.sdklib.AndroidVersion; +import com.android.sdkuilib.internal.repository.PackageInfo; +import com.android.sdkuilib.internal.repository.content.PackageAnalyser; +import org.eclipse.andmore.base.resources.ImageFactory; +import com.android.sdkuilib.ui.GridDialog; /** @@ -93,10 +98,13 @@ final class SdkUpdaterChooserDialog extends GridDialog { private Button mLicenseRadioReject; private Button mLicenseRadioAcceptLicense; private Group mPackageTextGroup; - private final SwtUpdaterData mSwtUpdaterData; private Group mTableGroup; private Label mErrorLabel; + private Set unacceptedLicenses = new HashSet<>(); + //private Multimap unacceptedLicenses = HashMultimap.create(); + //private List acceptedPackages = new ArrayList<>(); + private final SdkContext mSdkContext; /** * List of all archives to be installed with dependency information. *

@@ -105,9 +113,9 @@ final class SdkUpdaterChooserDialog extends GridDialog { * limited number of archives to deal with (e.g. < 10 now). We might want to revisit * this later if it becomes an issue. Right now just do the simple thing. *

- * Typically we could add a map Archive=>ArchiveInfo later. + * Typically we could add a map Package=>PackageInfo later. */ - private final Collection mArchives; + private final List mPackages = new ArrayList<>(); @@ -115,41 +123,40 @@ final class SdkUpdaterChooserDialog extends GridDialog { * Create the dialog. * * @param parentShell The shell to use, typically updaterData.getWindowShell() - * @param swtUpdaterData The updater data - * @param archives The archives to be installed + * @param SdkContext The updater data + * @param packages The packages to be installed */ public SdkUpdaterChooserDialog(Shell parentShell, - SwtUpdaterData swtUpdaterData, - Collection archives) { + SdkContext SdkContext, + Collection updates, + List newPackages) { super(parentShell, 3, false/*makeColumnsEqual*/); - mSwtUpdaterData = swtUpdaterData; - mArchives = archives; + mSdkContext = SdkContext; + init(updates); + init(newPackages); } - @Override + @Override protected boolean isResizable() { return true; } /** - * Returns the results, i.e. the list of selected new archives to install. - * This is similar to the {@link ArchiveInfo} list instance given to the constructor - * except only accepted archives are present. + * Returns the results, i.e. the list of selected new packages to install. *

* An empty list is returned if cancel was chosen. */ - public ArrayList getResult() { - ArrayList ais = new ArrayList(); + public ArrayList getResult() { + ArrayList packageList = new ArrayList(); if (getReturnCode() == Window.OK) { - for (ArchiveInfo ai : mArchives) { - if (ai.isAccepted()) { - ais.add(ai); + for (PackageInfo packageInfo : mPackages) { + if (packageInfo.isAccepted()) { + packageList.add(packageInfo.getNewPackage()); } } } - - return ais; + return packageList; } /** @@ -314,32 +321,29 @@ protected Control createButtonBar(Composite parent) { @Override public void create() { super.create(); - // set window title getShell().setText("Choose Packages to Install"); - setWindowImage(); - - // Automatically accept those with an empty license or no license - for (ArchiveInfo ai : mArchives) { - Archive a = ai.getNewArchive(); - if (a != null) { - License license = a.getParentPackage().getLicense(); - boolean hasLicense = license != null && - license.getLicense() != null && - license.getLicense().length() > 0; - ai.setAccepted(!hasLicense); + // Automatically accept those with no license + for (PackageInfo packageInfo : mPackages) { + RemotePackage remote = packageInfo.getNewPackage(); + License license = remote.getLicense(); + if (license == null) + packageInfo.setAccepted(true); + else { + boolean hasLicense = license.checkAccepted(mSdkContext.getLocation(), mSdkContext.getFileOp()); + if (hasLicense) + packageInfo.setAccepted(true); + else + unacceptedLicenses.add(remote); } } - // Fill the list with the replacement packages - mTreeViewPackage.setLabelProvider(new NewArchivesLabelProvider()); - mTreeViewPackage.setContentProvider(new NewArchivesContentProvider()); - mTreeViewPackage.setInput(createTreeInput(mArchives)); + mTreeViewPackage.setLabelProvider(new NewPackagesLabelProvider()); + mTreeViewPackage.setContentProvider(new NewPackagesContentProvider()); + mTreeViewPackage.setInput(createTreeInput(mPackages)); mTreeViewPackage.expandAll(); - adjustColumnsWidth(); - // select first item onPackageSelected(); } @@ -353,8 +357,8 @@ private void setWindowImage() { imageName = "android_icon_128.png"; //$NON-NLS-1$ } - if (mSwtUpdaterData != null) { - ImageFactory imgFactory = mSwtUpdaterData.getImageFactory(); + if (mSdkContext != null) { + ImageFactory imgFactory = mSdkContext.getSdkHelper().getImageFactory(); if (imgFactory != null) { getShell().setImage(imgFactory.getImageByName(imageName)); } @@ -414,21 +418,21 @@ private void onPackageSelected() { Object item = getSelectedItem(); // Update mAcceptSameAllLicense : true if all items under the same license are accepted. - ArchiveInfo ai = null; - List list = null; - if (item instanceof ArchiveInfo) { - ai = (ArchiveInfo) item; + PackageInfo packageInfo = null; + List list = null; + if (item instanceof PackageInfo) { + packageInfo = (PackageInfo) item; Object p = - ((NewArchivesContentProvider) mTreeViewPackage.getContentProvider()).getParent(ai); + ((NewPackagesContentProvider) mTreeViewPackage.getContentProvider()).getParent(packageInfo); if (p instanceof LicenseEntry) { - list = ((LicenseEntry) p).getArchives(); + list = ((LicenseEntry) p).getPackageInfoList(); } - displayPackageInformation(ai); + displayPackageInformation(packageInfo); } else if (item instanceof LicenseEntry) { LicenseEntry entry = (LicenseEntry) item; - list = entry.getArchives(); + list = entry.getPackageInfoList(); displayLicenseInformation(entry); } else { @@ -441,10 +445,10 @@ private void onPackageSelected() { mAcceptSameAllLicense = list != null && list.size() > 0; if (mAcceptSameAllLicense) { assert list != null; - License lic0 = getLicense(list.get(0)); - for (ArchiveInfo ai2 : list) { - License lic2 = getLicense(ai2); - if (ai2.isAccepted() && (lic0 == lic2 || lic0.equals(lic2))) { + License lic0 = list.get(0).getNewPackage().getLicense(); + for (PackageInfo packageInfo2 : list) { + License lic2 = packageInfo2.getNewPackage().getLicense(); + if (packageInfo2.isAccepted() && (lic0 == lic2 || lic0.equals(lic2))) { continue; } else { mAcceptSameAllLicense = false; @@ -453,17 +457,17 @@ private void onPackageSelected() { } } - displayMissingDependency(ai); - updateLicenceRadios(ai); + displayMissingDependency(packageInfo); + updateLicenceRadios(packageInfo); } /** Returns the currently selected tree item. - * @return Either {@link ArchiveInfo} or {@link LicenseEntry} or null. */ + * @return Either {@link PackageInfo} or {@link LicenseEntry} or null. */ private Object getSelectedItem() { ISelection sel = mTreeViewPackage.getSelection(); if (sel instanceof IStructuredSelection) { Object elem = ((IStructuredSelection) sel).getFirstElement(); - if (elem instanceof ArchiveInfo || elem instanceof LicenseEntry) { + if (elem instanceof PackageInfo || elem instanceof LicenseEntry) { return elem; } } @@ -483,94 +487,74 @@ private void displayEmptyInformation() { * Note that right now there is no logic to support more than one level of dependencies * (e.g. A <- B <- C and A is disabled so C should be disabled; currently C's state depends * solely on B's state). We currently don't need this. It would be straightforward to add - * if we had a need for it, though. This would require changes to {@link ArchiveInfo} and + * if we had a need for it, though. This would require changes to {@link PackageInfo} and * {@link SdkUpdaterLogic}. */ - private void displayPackageInformation(ArchiveInfo ai) { - Archive aNew = ai == null ? null : ai.getNewArchive(); - Package pNew = aNew == null ? null : aNew.getParentPackage(); - - if (pNew == null) { - displayEmptyInformation(); - return; - } - assert ai != null; // make Eclipse null detector happy - assert aNew != null; - + private void displayPackageInformation(PackageInfo packageInfo) { mPackageText.setText(""); //$NON-NLS-1$ - addSectionTitle("Package Description\n"); - addText(pNew.getLongDescription(), "\n\n"); //$NON-NLS-1$ - - Archive aOld = ai.getReplaced(); - if (aOld != null) { - Package pOld = aOld.getParentPackage(); - - FullRevision rOld = pOld.getRevision(); - FullRevision rNew = pNew.getRevision(); - - boolean showRev = true; - - if (pNew instanceof IAndroidVersionProvider && - pOld instanceof IAndroidVersionProvider) { - AndroidVersion vOld = ((IAndroidVersionProvider) pOld).getAndroidVersion(); - AndroidVersion vNew = ((IAndroidVersionProvider) pNew).getAndroidVersion(); - - if (!vOld.equals(vNew)) { - // Versions are different, so indicate more than just the revision. - addText(String.format("This update will replace API %1$s revision %2$s with API %3$s revision %4$s.\n\n", - vOld.getApiString(), rOld.toShortString(), - vNew.getApiString(), rNew.toShortString())); - showRev = false; - } + RemotePackage remotePackage = packageInfo.getNewPackage(); + addText(remotePackage.getDisplayName(), "\n\n"); //$NON-NLS-1$ + + LocalPackage localPackage = packageInfo.getReplaced(); + if (localPackage != null) { + AndroidVersion vOld = PackageAnalyser.getAndroidVersion(localPackage); + AndroidVersion vNew = PackageAnalyser.getAndroidVersion(remotePackage); + boolean showRev = (vOld != null) && (vNew != null); + + if (showRev && !vOld.equals(vNew)) { + // Versions are different, so indicate more than just the revision. + addText(String.format("This update will replace API %1$s revision %2$s with API %3$s revision %4$s.\n\n", + vOld.getApiString(), localPackage.getVersion(), + vNew.getApiString(), remotePackage.getVersion())); + showRev = false; } - if (showRev) { addText(String.format("This update will replace revision %1$s with revision %2$s.\n\n", - rOld.toShortString(), - rNew.toShortString())); + localPackage.getVersion(), + remotePackage.getVersion())); } } - - ArchiveInfo[] aDeps = ai.getDependsOn(); - if ((aDeps != null && aDeps.length > 0) || ai.isDependencyFor()) { + List required = InstallerUtil.computeRequiredPackages( + Collections.singletonList(remotePackage), mSdkContext.getPackages(), + mSdkContext.getProgressIndicator()); + if ((required != null && required.size() > 0)) { + // The returned required dependencies includes principal + required.remove(remotePackage); + if ((required.size() > 0)) { addSectionTitle("Dependencies\n"); - - if (aDeps != null && aDeps.length > 0) { - addText("Installing this package also requires installing:"); - for (ArchiveInfo aDep : aDeps) { - addText(String.format("\n- %1$s", - aDep.getShortDescription())); - } - addText("\n\n"); - } - - if (ai.isDependencyFor()) { - addText("This package is a dependency for:"); - for (ArchiveInfo ai2 : ai.getDependenciesFor()) { - addText(String.format("\n- %1$s", - ai2.getShortDescription())); - } - addText("\n\n"); - } + addText("Installing this package also requires installing:"); + for (RemotePackage dependency : required) { + addText(String.format("\n- %1$s", dependency.getDisplayName())); + } + addText("\n\n"); + /* + if (ai.isDependencyFor()) { + addText("This package is a dependency for:"); + for (PackageInfo ai2 : ai.getDependenciesFor()) { + addText(String.format("\n- %1$s", + ai2.getShortDescription())); + } + addText("\n\n"); + } + */ + } } - addSectionTitle("Archive Description\n"); - addText(aNew.getLongDescription(), "\n\n"); //$NON-NLS-1$ + addSectionTitle("Archive Size\n"); + long fileSize = remotePackage.getArchive().getComplete().getSize(); + addText(Utilities.formatFileSize(fileSize), "\n\n"); //$NON-NLS-1$ - License license = pNew.getLicense(); + License license = remotePackage.getLicense(); if (license != null) { - String text = license.getLicense(); - if (text != null) { - addSectionTitle("License\n"); - addText(text.trim(), "\n\n"); //$NON-NLS-1$ - } + addSectionTitle(String.format("License %s:%n", license.getId())); + addText(license.getValue().trim(), "\n\n"); //$NON-NLS-1$ } addSectionTitle("Site\n"); - SdkSource source = pNew.getParentSource(); + RepositorySource source = remotePackage.getSource(); if (source != null) { - addText(source.getShortDescription()); + addText(source.getDisplayName()); } } @@ -578,8 +562,8 @@ private void displayPackageInformation(ArchiveInfo ai) { * Updates the description for a license entry. */ private void displayLicenseInformation(LicenseEntry entry) { - List archives = entry == null ? null : entry.getArchives(); - if (archives == null) { + List packageInfoList = entry == null ? null : entry.getPackageInfoList(); + if (packageInfoList == null) { // There should not be a license entry without any package in it. displayEmptyInformation(); return; @@ -590,105 +574,16 @@ private void displayLicenseInformation(LicenseEntry entry) { License license = null; addSectionTitle("Packages\n"); - for (ArchiveInfo ai : entry.getArchives()) { - Archive aNew = ai.getNewArchive(); - if (aNew != null) { - Package pNew = aNew.getParentPackage(); - if (pNew != null) { - if (license == null) { - license = pNew.getLicense(); - } else { - assert license.equals(pNew.getLicense()); // all items have the same license - } - addText("- ", pNew.getShortDescription(), "\n"); //$NON-NLS-1$ //$NON-NLS-2$ - } - } + for (PackageInfo packageInfo : entry.getPackageInfoList()) { + RemotePackage remote = packageInfo.getNewPackage(); + if (license == null) + license = remote.getLicense(); + addText("- ", remote.getDisplayName(), "\n"); //$NON-NLS-1$ //$NON-NLS-2$ } if (license != null) { - String text = license.getLicense(); - if (text != null) { - addSectionTitle("\nLicense\n"); - addText(text.trim(), "\n\n"); //$NON-NLS-1$ - } - } - } - - /** - * Computes and displays missing dependencies. - * - * If there's a selected package, check the dependency for that one. - * Otherwise display the first missing dependency of any other package. - */ - private void displayMissingDependency(ArchiveInfo ai) { - String error = null; - - try { - if (ai != null) { - if (ai.isAccepted()) { - // Case where this package is accepted but blocked by another non-accepted one - ArchiveInfo[] adeps = ai.getDependsOn(); - if (adeps != null) { - for (ArchiveInfo adep : adeps) { - if (!adep.isAccepted()) { - error = String.format("This package depends on '%1$s'.", - adep.getShortDescription()); - return; - } - } - } - } else { - // Case where this package blocks another one when not accepted - for (ArchiveInfo adep : ai.getDependenciesFor()) { - // It only matters if the blocked one is accepted - if (adep.isAccepted()) { - error = String.format("Package '%1$s' depends on this one.", - adep.getShortDescription()); - return; - } - } - } - } - - // If there is no missing dependency on the current selection, - // just find the first missing dependency of any other package. - for (ArchiveInfo ai2 : mArchives) { - if (ai2 == ai) { - // We already processed that one above. - continue; - } - if (ai2.isAccepted()) { - // The user requested to install this package. - // Check if all its dependencies are met. - ArchiveInfo[] adeps = ai2.getDependsOn(); - if (adeps != null) { - for (ArchiveInfo adep : adeps) { - if (!adep.isAccepted()) { - error = String.format("Package '%1$s' depends on '%2$s'", - ai2.getShortDescription(), - adep.getShortDescription()); - return; - } - } - } - } else { - // The user did not request to install this package. - // Check whether this package blocks another one when not accepted. - for (ArchiveInfo adep : ai2.getDependenciesFor()) { - // It only matters if the blocked one is accepted - // or if it's a local archive that is already installed (these - // are marked as implicitly accepted, so it's the same test.) - if (adep.isAccepted()) { - error = String.format("Package '%1$s' depends on '%2$s'", - adep.getShortDescription(), - ai2.getShortDescription()); - return; - } - } - } - } - } finally { - mErrorLabel.setText(error == null ? "" : error); //$NON-NLS-1$ + addSectionTitle(String.format("License %s:%n", license.getId())); + addText(license.getValue().trim(), "\n\n"); //$NON-NLS-1$ } } @@ -711,7 +606,7 @@ private void addSectionTitle(String string) { mPackageText.setStyleRange(sr); } - private void updateLicenceRadios(ArchiveInfo ai) { + private void updateLicenceRadios(PackageInfo ai) { if (mInternalLicenseRadioUpdate) { return; } @@ -730,16 +625,14 @@ private void updateLicenceRadios(ArchiveInfo ai) { // If the current one isn't, look for another one. boolean missing = mErrorLabel.getText() != null && mErrorLabel.getText().length() > 0; if (!missing && !oneAccepted) { - for(ArchiveInfo ai2 : mArchives) { + for(PackageInfo ai2 : mPackages) { if (ai2.isAccepted()) { oneAccepted = true; break; } } } - getButton(IDialogConstants.OK_ID).setEnabled(!missing && oneAccepted); - mInternalLicenseRadioUpdate = false; } @@ -756,43 +649,44 @@ private void onLicenseRadioSelected() { mInternalLicenseRadioUpdate = true; Object item = getSelectedItem(); - ArchiveInfo ai = (item instanceof ArchiveInfo) ? (ArchiveInfo) item : null; + PackageInfo packageInfo = (item instanceof PackageInfo) ? (PackageInfo) item : null; boolean needUpdate = true; if (!mAcceptSameAllLicense && mLicenseRadioAcceptLicense.getSelection()) { // Accept all has been switched on. Mark all packages as accepted - List list = null; + List list = null; if (item instanceof LicenseEntry) { - list = ((LicenseEntry) item).getArchives(); - } else if (ai != null) { - Object p = ((NewArchivesContentProvider) mTreeViewPackage.getContentProvider()) - .getParent(ai); + list = ((LicenseEntry) item).getPackageInfoList(); + } else if (packageInfo != null) { + Object p = ((NewPackagesContentProvider) mTreeViewPackage.getContentProvider()) + .getParent(packageInfo); if (p instanceof LicenseEntry) { - list = ((LicenseEntry) p).getArchives(); + list = ((LicenseEntry) p).getPackageInfoList(); } } if (list != null && list.size() > 0) { mAcceptSameAllLicense = true; - for(ArchiveInfo ai2 : list) { - ai2.setAccepted(true); - ai2.setRejected(false); + for(PackageInfo packageInfo2 : list) { + packageInfo2.setAccepted(true); + packageInfo2.setRejected(false); + unacceptedLicenses.remove(packageInfo2.getNewPackage()); } } - } else if (ai != null && mLicenseRadioAccept.getSelection()) { + } else if (packageInfo != null && mLicenseRadioAccept.getSelection()) { // Accept only this one mAcceptSameAllLicense = false; - ai.setAccepted(true); - ai.setRejected(false); - - } else if (ai != null && mLicenseRadioReject.getSelection()) { + packageInfo.setAccepted(true); + packageInfo.setRejected(false); + unacceptedLicenses.remove(packageInfo.getNewPackage()); + } else if (packageInfo != null && mLicenseRadioReject.getSelection()) { // Reject only this one mAcceptSameAllLicense = false; - ai.setAccepted(false); - ai.setRejected(true); - + packageInfo.setAccepted(false); + packageInfo.setRejected(true); + unacceptedLicenses.add(packageInfo.getNewPackage()); } else { needUpdate = false; } @@ -803,13 +697,13 @@ private void onLicenseRadioSelected() { if (mAcceptSameAllLicense) { mTreeViewPackage.refresh(); } else { - mTreeViewPackage.refresh(ai); + mTreeViewPackage.refresh(packageInfo); mTreeViewPackage.refresh( - ((NewArchivesContentProvider) mTreeViewPackage.getContentProvider()). - getParent(ai)); + ((NewPackagesContentProvider) mTreeViewPackage.getContentProvider()). + getParent(packageInfo)); } - displayMissingDependency(ai); - updateLicenceRadios(ai); + displayMissingDependency(packageInfo); + updateLicenceRadios(packageInfo); } } @@ -819,45 +713,129 @@ private void onLicenseRadioSelected() { private void onPackageDoubleClick() { Object item = getSelectedItem(); - if (item instanceof ArchiveInfo) { - ArchiveInfo ai = (ArchiveInfo) item; - boolean wasAccepted = ai.isAccepted(); - ai.setAccepted(!wasAccepted); - ai.setRejected(wasAccepted); + if (item instanceof PackageInfo) { + PackageInfo packageInfo = (PackageInfo) item; + boolean wasAccepted = packageInfo.isAccepted(); + packageInfo.setAccepted(!wasAccepted); + packageInfo.setRejected(wasAccepted); // update state mAcceptSameAllLicense = false; - mTreeViewPackage.refresh(ai); + mTreeViewPackage.refresh(packageInfo); // refresh parent since its icon might have changed. mTreeViewPackage.refresh( - ((NewArchivesContentProvider) mTreeViewPackage.getContentProvider()). - getParent(ai)); + ((NewPackagesContentProvider) mTreeViewPackage.getContentProvider()). + getParent(packageInfo)); - displayMissingDependency(ai); - updateLicenceRadios(ai); + displayMissingDependency(packageInfo); + updateLicenceRadios(packageInfo); } else if (item instanceof LicenseEntry) { mTreeViewPackage.setExpandedState(item, !mTreeViewPackage.getExpandedState(item)); } } + /** + * Computes and displays missing dependencies. + * + * If there's a selected package, check the dependency for that one. + * Otherwise display the first missing dependency of any other package. + */ + private void displayMissingDependency(PackageInfo packageInfo) { + String error = null; + + try { + if (packageInfo != null) { + if (packageInfo.isAccepted()) { + // Case where this package is accepted but blocked by another non-accepted one + List required = InstallerUtil.computeRequiredPackages( + Collections.singletonList(packageInfo.getNewPackage()), mSdkContext.getPackages(), + mSdkContext.getProgressIndicator()); + if ((required != null && required.size() > 0)) { + for (RemotePackage dependency : required) { + if (unacceptedLicenses.contains(dependency)) { + error = String.format("This package depends on '%1$s'.", + dependency.getDisplayName()); + return; + } + } + } + } else { + /* + // Case where this package blocks another one when not accepted + for (PackageInfo adep : packageInfo.getDependenciesFor()) { + // It only matters if the blocked one is accepted + if (adep.isAccepted()) { + error = String.format("Package '%1$s' depends on this one.", + adep.getShortDescription()); + return; + } + } + */ + } + } +/* + // If there is no missing dependency on the current selection, + // just find the first missing dependency of any other package. + for (PackageInfo packageInfo2 : mArchives) { + if (packageInfo2 == packageInfo) { + // We already processed that one above. + continue; + } + if (packageInfo2.isAccepted()) { + // The user requested to install this package. + // Check if all its dependencies are met. + PackageInfo[] adeps = packageInfo2.getDependsOn(); + if (adeps != null) { + for (PackageInfo adep : adeps) { + if (!adep.isAccepted()) { + error = String.format("Package '%1$s' depends on '%2$s'", + packageInfo2.getShortDescription(), + adep.getShortDescription()); + return; + } + } + } + } else { + // The user did not request to install this package. + // Check whether this package blocks another one when not accepted. + for (PackageInfo adep : packageInfo2.getDependenciesFor()) { + // It only matters if the blocked one is accepted + // or if it's a local archive that is already installed (these + // are marked as implicitly accepted, so it's the same test.) + if (adep.isAccepted()) { + error = String.format("Package '%1$s' depends on '%2$s'", + adep.getShortDescription(), + packageInfo2.getShortDescription()); + return; + } + } + } + } +*/ + } finally { + mErrorLabel.setText(error == null ? "" : error); //$NON-NLS-1$ + } + } + + /** * Provides the labels for the tree view. * Root branches are {@link LicenseEntry} elements. - * Leave nodes are {@link ArchiveInfo} which all have the same license. + * Leave nodes are {@link PackageInfo} which all have the same license. */ - private class NewArchivesLabelProvider extends LabelProvider { + private class NewPackagesLabelProvider extends LabelProvider { @Override public Image getImage(Object element) { - if (element instanceof ArchiveInfo) { - // Archive icon: accepted (green), rejected (red), not set yet (question mark) - ArchiveInfo ai = (ArchiveInfo) element; + if (element instanceof PackageInfo) { + // Package icon: accepted (green), rejected (red), not set yet (question mark) + PackageInfo packageInfo = (PackageInfo) element; - ImageFactory imgFactory = mSwtUpdaterData.getImageFactory(); + ImageFactory imgFactory = mSdkContext.getSdkHelper().getImageFactory(); if (imgFactory != null) { - if (ai.isAccepted()) { + if (packageInfo.isAccepted()) { return imgFactory.getImageByName("accept_icon16.png"); - } else if (ai.isRejected()) { + } else if (packageInfo.isRejected()) { return imgFactory.getImageByName("reject_icon16.png"); } return imgFactory.getImageByName("unknown_icon16.png"); @@ -867,13 +845,13 @@ public Image getImage(Object element) { } else if (element instanceof LicenseEntry) { // License icon: green if all below are accepted, red if all rejected, otherwise // no icon. - ImageFactory imgFactory = mSwtUpdaterData.getImageFactory(); + ImageFactory imgFactory = mSdkContext.getSdkHelper().getImageFactory(); if (imgFactory != null) { boolean allAccepted = true; boolean allRejected = true; - for (ArchiveInfo ai : ((LicenseEntry) element).getArchives()) { - allAccepted = allAccepted && ai.isAccepted(); - allRejected = allRejected && ai.isRejected(); + for (PackageInfo packageInfo : ((LicenseEntry) element).getPackageInfoList()) { + allAccepted = allAccepted && packageInfo.isAccepted(); + allRejected = allRejected && packageInfo.isRejected(); } if (allAccepted && !allRejected) { return imgFactory.getImageByName("accept_icon16.png"); @@ -882,7 +860,6 @@ public Image getImage(Object element) { } } } - return null; } @@ -891,20 +868,17 @@ public String getText(Object element) { if (element instanceof LicenseEntry) { return ((LicenseEntry) element).getLicenseRef(); - } else if (element instanceof ArchiveInfo) { - ArchiveInfo ai = (ArchiveInfo) element; + } else if (element instanceof PackageInfo) { + PackageInfo packageInfo = (PackageInfo) element; - String desc = ai.getShortDescription(); - - if (ai.isDependencyFor()) { - desc += " [*]"; - } + String desc = packageInfo.getNewPackage().getDisplayName(); + // if (packageInfo.isDependencyFor()) { + // desc += " [*]"; + // } return desc; - } - - assert element instanceof String || element instanceof ArchiveInfo; + assert element instanceof String || element instanceof PackageInfo; return null; } } @@ -912,9 +886,9 @@ public String getText(Object element) { /** * Provides the content for the tree view. * Root branches are {@link LicenseEntry} elements. - * Leave nodes are {@link ArchiveInfo} which all have the same license. + * Leave nodes are {@link PackageInfo} which all have the same license. */ - private class NewArchivesContentProvider implements ITreeContentProvider { + private class NewPackagesContentProvider implements ITreeContentProvider { private List mInput; @Override @@ -942,7 +916,7 @@ public boolean hasChildren(Object parent) { return true; } else if (parent instanceof LicenseEntry) { - return ((LicenseEntry) parent).getArchives().size() > 0; + return ((LicenseEntry) parent).getPackageInfoList().size() > 0; } return false; @@ -959,7 +933,7 @@ public Object[] getChildren(Object parent) { return ((List) parent).toArray(); } else if (parent instanceof LicenseEntry) { - return ((LicenseEntry) parent).getArchives().toArray(); + return ((LicenseEntry) parent).getPackageInfoList().toArray(); } return new Object[0]; @@ -970,9 +944,9 @@ public Object getParent(Object child) { if (child instanceof LicenseEntry) { return ((LicenseEntry) child).getRoot(); - } else if (child instanceof ArchiveInfo && mInput != null) { + } else if (child instanceof PackageInfo && mInput != null) { for (LicenseEntry entry : mInput) { - if (entry.getArchives().contains(child)) { + if (entry.getPackageInfoList().contains(child)) { return entry; } } @@ -989,15 +963,15 @@ public Object getParent(Object child) { private static class LicenseEntry { private final List mRoot; private final String mLicenseRef; - private final List mArchives; + private final List mPackageInfoList; public LicenseEntry( @NonNull List root, @NonNull String licenseRef, - @NonNull List archives) { + @NonNull List packageInfoList) { mRoot = root; mLicenseRef = licenseRef; - mArchives = archives; + mPackageInfoList = packageInfoList; } @NonNull @@ -1011,8 +985,8 @@ public String getLicenseRef() { } @NonNull - public List getArchives() { - return mArchives; + public List getPackageInfoList() { + return mPackageInfoList; } } @@ -1023,10 +997,10 @@ public List getArchives() { * Elements with no license are left at the root. * * @param archives The non-null collection of archive info to display. Ideally non-empty. - * @return A list of {@link LicenseEntry}, each containing a list of {@link ArchiveInfo}. + * @return A list of {@link LicenseEntry}, each containing a list of {@link PackageInfo}. */ @NonNull - private List createTreeInput(@NonNull Collection archives) { + private List createTreeInput(@NonNull List packageInfoList) { // Build an ordered map with all the licenses, ordered by license ref name. final String noLicense = "No license"; //NLS @@ -1046,51 +1020,43 @@ public int compare(String s1, String s2) { } }; - Map> map = new TreeMap>(comp); + Map> packageInfoMap = new TreeMap>(comp); - for (ArchiveInfo info : archives) { + for (PackageInfo info : packageInfoList) { String ref = noLicense; - License license = getLicense(info); - if (license != null && license.getLicenseRef() != null) { - ref = prettyLicenseRef(license.getLicenseRef()); + License license = info.getNewPackage().getLicense(); + if (license != null) { + ref = license.getId(); //prettyLicenseRef(license.getLicenseRef()); } - List list = map.get(ref); + List list = packageInfoMap.get(ref); if (list == null) { - map.put(ref, list = new ArrayList()); + packageInfoMap.put(ref, list = new ArrayList()); } list.add(info); } // Transform result into a list List licensesList = new ArrayList(); - for (Map.Entry> entry : map.entrySet()) { + for (Map.Entry> entry : packageInfoMap.entrySet()) { licensesList.add(new LicenseEntry(licensesList, entry.getKey(), entry.getValue())); } - return licensesList; } - /** - * Helper method to retrieve the {@link License} for a given {@link ArchiveInfo}. - * - * @param ai The archive info. Can be null. - * @return The license for the package owning the archive. Can be null. - */ - @Nullable - private License getLicense(@Nullable ArchiveInfo ai) { - if (ai != null) { - Archive aNew = ai.getNewArchive(); - if (aNew != null) { - Package pNew = aNew.getParentPackage(); - if (pNew != null) { - return pNew.getLicense(); - } - } + private void init(List newPackages) { + Iterator iterator = newPackages.iterator(); + while(iterator.hasNext()) + mPackages.add(new PackageInfo(iterator.next())); + } + + private void init(Collection updates) { + Iterator iterator = updates.iterator(); + while(iterator.hasNext()) { + UpdatablePackage updatable = iterator.next(); + mPackages.add(new PackageInfo(updatable.getRemote(), updatable.getLocal())); } - return null; - } - + } /** * Reformats the licenseRef to be more human-readable. * It's an XML ref and in practice it looks like [oem-]android-[type]-license. diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/SdkUpdaterWindowImpl2.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/SdkUpdaterWindowImpl2.java index 99c5eb22..65d71937 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/SdkUpdaterWindowImpl2.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/SdkUpdaterWindowImpl2.java @@ -18,27 +18,23 @@ import com.android.SdkConstants; -import com.android.sdklib.internal.repository.ITaskFactory; -import com.android.sdklib.internal.repository.sources.SdkSourceProperties; -import com.android.sdklib.internal.repository.updater.SettingsController; -import com.android.sdklib.internal.repository.updater.SettingsController.Settings; -import com.android.sdklib.repository.ISdkChangeListener; import com.android.sdkuilib.internal.repository.AboutDialog; import com.android.sdkuilib.internal.repository.ISdkUpdaterWindow; import com.android.sdkuilib.internal.repository.MenuBarWrapper; +import com.android.sdkuilib.internal.repository.Settings; import com.android.sdkuilib.internal.repository.SettingsDialog; -import com.android.sdkuilib.internal.repository.SwtUpdaterData; -import com.android.sdkuilib.internal.repository.icons.ImageFactory; +import org.eclipse.andmore.base.resources.ImageFactory; import com.android.sdkuilib.internal.repository.ui.PackagesPage.MenuAction; -import com.android.sdkuilib.internal.tasks.ILogUiProvider; -import com.android.sdkuilib.internal.tasks.ProgressView; -import com.android.sdkuilib.internal.tasks.ProgressViewFactory; import com.android.sdkuilib.internal.widgets.ImgDisabledButton; import com.android.sdkuilib.internal.widgets.ToggleButton; import com.android.sdkuilib.repository.AvdManagerWindow.AvdInvocationContext; +import com.android.sdkuilib.repository.ISdkChangeListener; import com.android.sdkuilib.repository.SdkUpdaterWindow.SdkInvocationContext; import com.android.utils.ILogger; +import java.util.Iterator; + +import org.eclipse.andmore.sdktool.SdkContext; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; @@ -71,8 +67,7 @@ public class SdkUpdaterWindowImpl2 implements ISdkUpdaterWindow { private final Shell mParentShell; private final SdkInvocationContext mContext; - /** Internal data shared between the window and its pages. */ - private final SwtUpdaterData mSwtUpdaterData; + private final SdkContext mSdkContext; // --- UI members --- @@ -82,7 +77,6 @@ public class SdkUpdaterWindowImpl2 implements ISdkUpdaterWindow { private Label mStatusText; private ImgDisabledButton mButtonStop; private ToggleButton mButtonShowLog; - private SettingsController mSettingsController; private LogWindow mLogWindow; /** @@ -90,38 +84,17 @@ public class SdkUpdaterWindowImpl2 implements ISdkUpdaterWindow { * * @param parentShell Parent shell. * @param sdkLog Logger. Cannot be null. - * @param osSdkRoot The OS path to the SDK root. + * @param sdkContext SDK handler and repo manager * @param context The {@link SdkInvocationContext} to change the behavior depending on who's * opening the SDK Manager. */ public SdkUpdaterWindowImpl2( Shell parentShell, - ILogger sdkLog, - String osSdkRoot, + SdkContext sdkContext, SdkInvocationContext context) { mParentShell = parentShell; mContext = context; - mSwtUpdaterData = new SwtUpdaterData(osSdkRoot, sdkLog); - } - - /** - * Creates a new window. Caller must call open(), which will block. - *

- * This is to be used when the window is opened from {@link AvdManagerWindowImpl1} - * to share the same {@link SwtUpdaterData} structure. - * - * @param parentShell Parent shell. - * @param swtUpdaterData The parent's updater data. - * @param context The {@link SdkInvocationContext} to change the behavior depending on who's - * opening the SDK Manager. - */ - public SdkUpdaterWindowImpl2( - Shell parentShell, - SwtUpdaterData swtUpdaterData, - SdkInvocationContext context) { - mParentShell = parentShell; - mContext = context; - mSwtUpdaterData = swtUpdaterData; + mSdkContext = sdkContext; } /** @@ -150,10 +123,6 @@ public void open() { } } } - - SdkSourceProperties p = new SdkSourceProperties(); - p.save(); - dispose(); //$hide$ } @@ -189,7 +158,7 @@ public void widgetDisposed(DisposeEvent e) { } private void createContents() { - mPkgPage = new PackagesPage(mShell, SWT.NONE, mSwtUpdaterData, mContext); + mPkgPage = new PackagesPage(mShell, SWT.NONE, mSdkContext, mContext); mPkgPage.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1)); Composite composite1 = new Composite(mShell, SWT.NONE); @@ -231,7 +200,6 @@ public void handleEvent(Event event) { }); } - @SuppressWarnings("unused") // MenuItem works using side effects private void createMenuBar() { Menu menuBar = new Menu(mShell, SWT.BAR); @@ -242,20 +210,19 @@ private void createMenuBar() { Menu menuPkgs = new Menu(menuBarPackages); menuBarPackages.setMenu(menuPkgs); - MenuItem showUpdatesNew = new MenuItem(menuPkgs, - MenuAction.TOGGLE_SHOW_UPDATE_NEW_PKG.getMenuStyle()); + MenuAction.TOGGLE_SHOW_NEW_PKG.getMenuStyle()); showUpdatesNew.setText( - MenuAction.TOGGLE_SHOW_UPDATE_NEW_PKG.getMenuTitle()); + MenuAction.TOGGLE_SHOW_NEW_PKG.getMenuTitle()); mPkgPage.registerMenuAction( - MenuAction.TOGGLE_SHOW_UPDATE_NEW_PKG, showUpdatesNew); + MenuAction.TOGGLE_SHOW_NEW_PKG, showUpdatesNew); - MenuItem showInstalled = new MenuItem(menuPkgs, - MenuAction.TOGGLE_SHOW_INSTALLED_PKG.getMenuStyle()); - showInstalled.setText( - MenuAction.TOGGLE_SHOW_INSTALLED_PKG.getMenuTitle()); - mPkgPage.registerMenuAction( - MenuAction.TOGGLE_SHOW_INSTALLED_PKG, showInstalled); + //MenuItem showInstalled = new MenuItem(menuPkgs, + // MenuAction.TOGGLE_SHOW_INSTALLED_PKG.getMenuStyle()); + //showInstalled.setText( + // MenuAction.TOGGLE_SHOW_INSTALLED_PKG.getMenuTitle()); + //mPkgPage.registerMenuAction( + // MenuAction.TOGGLE_SHOW_INSTALLED_PKG, showInstalled); MenuItem showObsoletePackages = new MenuItem(menuPkgs, MenuAction.TOGGLE_SHOW_OBSOLETE_PKG.getMenuStyle()); @@ -264,13 +231,6 @@ private void createMenuBar() { mPkgPage.registerMenuAction( MenuAction.TOGGLE_SHOW_OBSOLETE_PKG, showObsoletePackages); - MenuItem showArchives = new MenuItem(menuPkgs, - MenuAction.TOGGLE_SHOW_ARCHIVES.getMenuStyle()); - showArchives.setText( - MenuAction.TOGGLE_SHOW_ARCHIVES.getMenuTitle()); - mPkgPage.registerMenuAction( - MenuAction.TOGGLE_SHOW_ARCHIVES, showArchives); - new MenuItem(menuPkgs, SWT.SEPARATOR); MenuItem reload = new MenuItem(menuPkgs, @@ -279,7 +239,6 @@ private void createMenuBar() { MenuAction.RELOAD.getMenuTitle()); mPkgPage.registerMenuAction( MenuAction.RELOAD, reload); - MenuItem menuBarTools = new MenuItem(menuBar, SWT.CASCADE); menuBarTools.setText("Tools"); @@ -296,14 +255,6 @@ public void widgetSelected(SelectionEvent event) { } }); } - - MenuItem manageSources = new MenuItem(menuTools, - MenuAction.SHOW_ADDON_SITES.getMenuStyle()); - manageSources.setText( - MenuAction.SHOW_ADDON_SITES.getMenuTitle()); - mPkgPage.registerMenuAction( - MenuAction.SHOW_ADDON_SITES, manageSources); - if (mContext == SdkInvocationContext.STANDALONE || mContext == SdkInvocationContext.IDE) { try { new MenuBarWrapper(APP_NAME, menuTools) { @@ -311,14 +262,14 @@ public void widgetSelected(SelectionEvent event) { public void onPreferencesMenuSelected() { // capture a copy of the initial settings - Settings settings1 = new Settings(mSettingsController.getSettings()); + Settings settings1 = mSdkContext.getSettings().copy(); // open the dialog and wait for it to close - SettingsDialog sd = new SettingsDialog(mShell, mSwtUpdaterData); + SettingsDialog sd = new SettingsDialog(mShell, mSdkContext); sd.open(); // get the new settings - Settings settings2 = mSettingsController.getSettings(); + Settings settings2 = mSdkContext.getSettings(); // We need to reload the package list if the http mode or the preview // modes have changed. @@ -330,27 +281,27 @@ public void onPreferencesMenuSelected() { @Override public void onAboutMenuSelected() { - AboutDialog ad = new AboutDialog(mShell, mSwtUpdaterData); + AboutDialog ad = new AboutDialog(mShell, mSdkContext); ad.open(); } @Override public void printError(String format, Object... args) { - if (mSwtUpdaterData != null) { - mSwtUpdaterData.getSdkLog().error(null, format, args); + if (mSdkContext != null) { + mSdkContext.getSdkLog().error(null, format, args); } } }; } catch (Throwable e) { - mSwtUpdaterData.getSdkLog().error(e, "Failed to setup menu bar"); + mSdkContext.getSdkLog().error(e, "Failed to setup menu bar"); e.printStackTrace(); } } } private Image getImage(String filename) { - if (mSwtUpdaterData != null) { - ImageFactory imgFactory = mSwtUpdaterData.getImageFactory(); + if (mSdkContext != null) { + ImageFactory imgFactory = mSdkContext.getSdkHelper().getImageFactory(); if (imgFactory != null) { return imgFactory.getImageByName(filename); } @@ -367,7 +318,7 @@ private Image getImage(String filename) { */ private void createLogWindow() { mLogWindow = new LogWindow(mShell, - mContext == SdkInvocationContext.IDE ? mSwtUpdaterData.getSdkLog() : null); + mContext == SdkInvocationContext.IDE ? mSdkContext.getSdkLog() : null); mLogWindow.open(); } @@ -383,7 +334,7 @@ private void createLogWindow() { */ @Override public void addListener(ISdkChangeListener listener) { - mSwtUpdaterData.addListeners(listener); + mSdkContext.getSdkHelper().addListeners(listener); } /** @@ -392,7 +343,7 @@ public void addListener(ISdkChangeListener listener) { */ @Override public void removeListener(ISdkChangeListener listener) { - mSwtUpdaterData.removeListener(listener); + mSdkContext.getSdkHelper().removeListener(listener); } // --- Internals & UI Callbacks ----------- @@ -401,9 +352,7 @@ public void removeListener(ISdkChangeListener listener) { * Called before the UI is created. */ private void preCreateContent() { - mSwtUpdaterData.setWindowShell(mShell); - // We need the UI factory to create the UI - mSwtUpdaterData.setImageFactory(new ImageFactory(mShell.getDisplay())); + mSdkContext.getSdkHelper().setWindowShell(mShell); // Note: we can't create the TaskFactory yet because we need the UI // to be created first, so this is done in postCreateContent(). } @@ -415,12 +364,11 @@ private void preCreateContent() { * Returns true if we should show the window. */ private boolean postCreateContent() { - ProgressViewFactory factory = new ProgressViewFactory(); // This class delegates all logging to the mLogWindow window // and filters errors to make sure the window is visible when // an error is logged. - ILogUiProvider logAdapter = new ILogUiProvider() { + SdkProgressFactory.ISdkLogWindow logAdapter = new SdkProgressFactory.ISdkLogWindow() { @Override public void setDescription(String description) { mLogWindow.setDescription(description); @@ -439,7 +387,10 @@ public void logVerbose(String log) { @Override public void logError(String log) { mLogWindow.logError(log); - + } + @Override + public void show() + { // Run the window visibility check/toggle on the UI thread. // Note: at least on Windows, it seems ok to check for the window visibility // on a sub-thread but that doesn't seem cross-platform safe. We shouldn't @@ -460,25 +411,26 @@ public void run() { } } }; - - factory.setProgressView( - new ProgressView(mStatusText, mProgressBar, mButtonStop, logAdapter)); - mSwtUpdaterData.setTaskFactory(factory); - + SdkProgressFactory factory = new SdkProgressFactory(mStatusText, mProgressBar, mButtonStop, logAdapter); setWindowImage(mShell); - - setupSources(); - initializeSettings(); - - if (mSwtUpdaterData.checkIfInitFailed()) { + if (!initializeSettings()) return false; + if (mSdkContext.hasError()) + { + ILogger logger = (ILogger)factory; + Iterator iterator = mSdkContext.getLogMessages().iterator(); + while(iterator.hasNext()) + logger.error(null, iterator.next()); + return false; + } + mSdkContext.setSdkLogger(factory); + mSdkContext.setSdkProgressIndicator(factory); + // TODO - Consider how to signal SDK loaded + //mSdkContext.getSdkHelper().broadcastOnSdkLoaded(mSdkContext.getSdkLog()); - mSwtUpdaterData.broadcastOnSdkLoaded(); - - // Tell the one page its the selected one - mPkgPage.performFirstLoad(); - + // Display packages + mPkgPage.onReady(factory, factory.getProgressControl(), factory); return true; } @@ -493,8 +445,8 @@ private void setWindowImage(Shell shell) { imageName = "android_icon_128.png"; //$NON-NLS-1$ } - if (mSwtUpdaterData != null) { - ImageFactory imgFactory = mSwtUpdaterData.getImageFactory(); + if (mSdkContext != null) { + ImageFactory imgFactory = mSdkContext.getSdkHelper().getImageFactory(); if (imgFactory != null) { shell.setImage(imgFactory.getImageByName(imageName)); } @@ -506,38 +458,34 @@ private void setWindowImage(Shell shell) { */ private void dispose() { mLogWindow.close(); - mSwtUpdaterData.getSources().saveUserAddons(mSwtUpdaterData.getSdkLog()); } /** * Callback called when the window shell is disposed. */ private void onAndroidSdkUpdaterDispose() { - if (mSwtUpdaterData != null) { - ImageFactory imgFactory = mSwtUpdaterData.getImageFactory(); + if (mSdkContext != null) { + ImageFactory imgFactory = mSdkContext.getSdkHelper().getImageFactory(); if (imgFactory != null) { imgFactory.dispose(); } } } - /** - * Used to initialize the sources. - */ - private void setupSources() { - mSwtUpdaterData.setupDefaultSources(); - } - /** * Initializes settings. * This must be called after addExtraPages(), which created a settings page. * Iterate through all the pages to find the first (and supposedly unique) setting page, * and use it to load and apply these settings. */ - private void initializeSettings() { - mSettingsController = mSwtUpdaterData.getSettingsController(); - mSettingsController.loadSettings(); - mSettingsController.applySettings(); + private boolean initializeSettings() { + Settings settings = new Settings(); + if (settings.initialize(mSdkContext.getSdkLog())) + { + mSdkContext.setSettings(settings); + return true; + } + return false; } private void onToggleLogWindow() { @@ -549,24 +497,20 @@ private void onToggleLogWindow() { } private void onStopSelected() { - // TODO + mSdkContext.getProgressIndicator().cancel(); } private void onAvdManager() { - ITaskFactory oldFactory = mSwtUpdaterData.getTaskFactory(); - try { AvdManagerWindowImpl1 win = new AvdManagerWindowImpl1( mShell, - mSwtUpdaterData, + mSdkContext, AvdInvocationContext.DIALOG); win.open(); } catch (Exception e) { - mSwtUpdaterData.getSdkLog().error(e, "AVD Manager window error"); - } finally { - mSwtUpdaterData.setTaskFactory(oldFactory); - } + mSdkContext.getSdkLog().error(e, "AVD Manager window error"); + } } // End of hiding from SWT Designer diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/IProgressUiProvider.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/IProgressUiProvider.java index e10a0c24..23d4a416 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/IProgressUiProvider.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/IProgressUiProvider.java @@ -16,8 +16,8 @@ package com.android.sdkuilib.internal.tasks; -import com.android.sdklib.internal.repository.ITaskMonitor; -import com.android.sdklib.internal.repository.UserCredentials; +import com.android.sdkuilib.internal.repository.ITaskMonitor; +import com.android.sdkuilib.internal.repository.UserCredentials; import org.eclipse.swt.widgets.ProgressBar; @@ -73,6 +73,17 @@ interface IProgressUiProvider extends ILogUiProvider { */ public abstract boolean displayPrompt(String title, String message); + /** + * Display info dialog box. + * + * This implementation allow this to be called from any thread, it + * makes sure the dialog is opened synchronously in the ui thread. + * + * @param title The title of the dialog box + * @param message The error message + */ + public abstract void displayInfo(String title, String message); + /** * Launch an interface which asks for login credentials. Implementations * MUST allow this to be called from any thread, e.g. by making sure the diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTask.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTask.java index 2563a5ff..00f6c6fa 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTask.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTask.java @@ -16,8 +16,8 @@ package com.android.sdkuilib.internal.tasks; -import com.android.sdklib.internal.repository.ITask; -import com.android.sdklib.internal.repository.ITaskMonitor; +import com.android.sdkuilib.internal.repository.ITask; +import com.android.sdkuilib.internal.repository.ITaskMonitor; import org.eclipse.swt.widgets.Shell; diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTaskDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTaskDialog.java index 1a16799f..5b4b1048 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTaskDialog.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTaskDialog.java @@ -17,8 +17,8 @@ package com.android.sdkuilib.internal.tasks; import com.android.SdkConstants; -import com.android.sdklib.internal.repository.ITaskMonitor; -import com.android.sdklib.internal.repository.UserCredentials; +import com.android.sdkuilib.internal.repository.ITaskMonitor; +import com.android.sdkuilib.internal.repository.UserCredentials; import com.android.sdkuilib.ui.AuthenticationDialog; import com.android.sdkuilib.ui.GridDialog; import com.android.utils.Pair; @@ -414,6 +414,28 @@ public void run() { return result[0]; } + + /** + * Display info dialog box. + * + * This implementation allow this to be called from any thread, it + * makes sure the dialog is opened synchronously in the ui thread. + * + * @param title The title of the dialog box + * @param message The error message + */ + @Override + public void displayInfo(String title, String message) { + Display display = mDialogShell.getDisplay(); + + display.syncExec(new Runnable() { + @Override + public void run() { + MessageDialog.openInformation(mDialogShell, title, message); + } + }); + } + /** * This method opens a pop-up window which requests for User Login and * password. diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTaskFactory.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTaskFactory.java index bd2cc14f..da4e94d5 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTaskFactory.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressTaskFactory.java @@ -16,9 +16,9 @@ package com.android.sdkuilib.internal.tasks; -import com.android.sdklib.internal.repository.ITask; -import com.android.sdklib.internal.repository.ITaskFactory; -import com.android.sdklib.internal.repository.ITaskMonitor; +import com.android.sdkuilib.internal.repository.ITask; +import com.android.sdkuilib.internal.repository.ITaskFactory; +import com.android.sdkuilib.internal.repository.ITaskMonitor; import org.eclipse.swt.widgets.Shell; diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressView.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressView.java index fe7d6c51..f01164f5 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressView.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressView.java @@ -16,9 +16,9 @@ package com.android.sdkuilib.internal.tasks; -import com.android.sdklib.internal.repository.ITask; -import com.android.sdklib.internal.repository.ITaskMonitor; -import com.android.sdklib.internal.repository.UserCredentials; +import com.android.sdkuilib.internal.repository.ITask; +import com.android.sdkuilib.internal.repository.ITaskMonitor; +import com.android.sdkuilib.internal.repository.UserCredentials; import com.android.sdkuilib.ui.AuthenticationDialog; import com.android.sdkuilib.ui.GridDialog; @@ -33,6 +33,10 @@ import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Widget; +import java.io.ByteArrayOutputStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; import java.util.concurrent.atomic.AtomicReference; @@ -113,13 +117,19 @@ public void handleEvent(Event event) { public void startTask( final String title, final ITaskMonitor parentMonitor, - final ITask task) { + final ITask task, + boolean sync) { if (task != null) { try { if (parentMonitor == null && !mProgressBar.isDisposed()) { - mLabel.setText(title); - mProgressBar.setSelection(0); - mProgressBar.setEnabled(true); + syncExec(mProgressBar, new Runnable() { + @Override + public void run() { + mLabel.setText(title); + mProgressBar.setSelection(0); + mProgressBar.setEnabled(true); + } + }); changeState(ProgressView.State.ACTIVE); } @@ -167,29 +177,42 @@ public void run() { final Thread t = new Thread(r, title); t.start(); - - // Process the app's event loop whilst we wait for the thread to finish - while (!mProgressBar.isDisposed() && t.isAlive()) { - Display display = mProgressBar.getDisplay(); - if (!mProgressBar.isDisposed() && !display.readAndDispatch()) { - display.sleep(); - } - } + if (sync) + // Process the app's event loop whilst we wait for the thread to finish + while (!mProgressBar.isDisposed() && t.isAlive()) { + Display display = mProgressBar.getDisplay(); + if (!mProgressBar.isDisposed() && !display.readAndDispatch()) { + display.sleep(); + } + } } } catch (Exception e) { - // TODO log - + StringWriter builder = new StringWriter(); + builder.append(e.getMessage()).append("\n"); + PrintWriter writer = new PrintWriter(builder); + e.printStackTrace(writer); + mLog.logError(builder.toString()); } finally { - if (parentMonitor == null && !mProgressBar.isDisposed()) { - changeState(ProgressView.State.IDLE); - mProgressBar.setSelection(0); - mProgressBar.setEnabled(false); + if (parentMonitor == null && sync && !mProgressBar.isDisposed()) { + endTask(); } } } } - private void syncExec(final Widget widget, final Runnable runnable) { + public void endTask() + { + changeState(ProgressView.State.IDLE); + syncExec(mProgressBar, new Runnable() { + @Override + public void run() { + mProgressBar.setSelection(0); + mProgressBar.setEnabled(false); + } + }); + } + + public void syncExec(final Widget widget, final Runnable runnable) { if (widget != null && !widget.isDisposed()) { widget.getDisplay().syncExec(new Runnable() { @Override @@ -222,7 +245,10 @@ public void run() { @Override public boolean isCancelRequested() { - return mState != State.ACTIVE; + boolean cancelStatus = mState != State.ACTIVE; + if (cancelStatus) + log("Stop button pressed"); + return cancelStatus; } /** @@ -337,6 +363,27 @@ public void run() { return result[0]; } + /** + * Display info dialog box. + * + * This implementation allow this to be called from any thread, it + * makes sure the dialog is opened synchronously in the ui thread. + * + * @param title The title of the dialog box + * @param message The error message + */ + @Override + public void displayInfo(String title, String message) { + + syncExec(mProgressBar, new Runnable() { + @Override + public void run() { + Shell shell = mProgressBar.getShell(); + MessageDialog.openInformation(shell, title, message); + } + }); + } + /** * This method opens a pop-up window which requests for User Credentials. * diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressViewFactory.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressViewFactory.java deleted file mode 100644 index 1d39c597..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/ProgressViewFactory.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.sdkuilib.internal.tasks; - -import com.android.sdklib.internal.repository.ITask; -import com.android.sdklib.internal.repository.ITaskFactory; -import com.android.sdklib.internal.repository.ITaskMonitor; - -/** - * An {@link ITaskFactory} that creates a new {@link ProgressTask} dialog - * for each new task. - */ -public final class ProgressViewFactory implements ITaskFactory { - - private ProgressView mProgressView; - - public ProgressViewFactory() { - } - - public void setProgressView(ProgressView progressView) { - mProgressView = progressView; - } - - @Override - public void start(String title, ITask task) { - start(title, null /*monitor*/, task); - } - - @Override - public void start(String title, ITaskMonitor parentMonitor, ITask task) { - assert mProgressView != null; - mProgressView.startTask(title, parentMonitor, task); - } -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/SdkProgressIndicator.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/SdkProgressIndicator.java new file mode 100644 index 00000000..6d27d15a --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/SdkProgressIndicator.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2017 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.sdkuilib.internal.tasks; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import com.android.repository.api.ProgressIndicator; +import com.android.sdkuilib.internal.repository.ITaskMonitor; + +/** + * @author Andrew Bowley + * + * 12-11-2017 + */ +public class SdkProgressIndicator implements ProgressIndicator { + // Use Monitor max count value convert fractions to integer values + private static final int MAX_COUNT = 10000; + + private final ITaskMonitor monitor; + private volatile boolean isCancelled = false; + private volatile boolean cancellable = true; + // TODO - implement indeterminate cursor + private volatile boolean indeterminate = false; + + /** + * + */ + public SdkProgressIndicator(ITaskMonitor monitor) { + this.monitor = monitor; + monitor.setProgressMax(MAX_COUNT); + } + + /* (non-Javadoc) + * @see com.android.repository.api.ProgressIndicator#cancel() + */ + @Override + public void cancel() { + if (cancellable) + // TODO - Consider informing user request denied or disable stop button when not cancellable + isCancelled = true; + } + + /* (non-Javadoc) + * @see com.android.repository.api.ProgressIndicator#getFraction() + */ + @Override + public double getFraction() { + return MAX_COUNT / monitor.getProgress(); + } + + /* (non-Javadoc) + * @see com.android.repository.api.ProgressIndicator#isCanceled() + */ + @Override + public boolean isCanceled() { + return isCancelled || monitor.isCancelRequested(); + } + + /* (non-Javadoc) + * @see com.android.repository.api.ProgressIndicator#isCancellable() + */ + @Override + public boolean isCancellable() { + return cancellable; + } + + /* (non-Javadoc) + * @see com.android.repository.api.ProgressIndicator#isIndeterminate() + */ + @Override + public boolean isIndeterminate() { + return indeterminate; + } + + /* (non-Javadoc) + * @see com.android.repository.api.ProgressIndicator#logError(java.lang.String) + */ + @Override + public void logError(String message) { + monitor.logError(message); + } + + /* (non-Javadoc) + * @see com.android.repository.api.ProgressIndicator#logError(java.lang.String, java.lang.Throwable) + */ + @Override + public void logError(String message, Throwable throwable) { + monitor.error(throwable, message); + } + + /* (non-Javadoc) + * @see com.android.repository.api.ProgressIndicator#logInfo(java.lang.String) + */ + @Override + public void logInfo(String message) { + monitor.info(message); + } + + /* (non-Javadoc) + * @see com.android.repository.api.ProgressIndicator#logWarning(java.lang.String) + */ + @Override + public void logWarning(String message) { + monitor.warning(message); + } + + /* (non-Javadoc) + * @see com.android.repository.api.ProgressIndicator#logWarning(java.lang.String, java.lang.Throwable) + */ + @Override + public void logWarning(String message, Throwable throwable) { + StringWriter builder = new StringWriter(); + builder.append(message); + if (throwable != null) { + builder.append("\n"); + PrintWriter writer = new PrintWriter(builder); + throwable.printStackTrace(writer); + } + monitor.warning(builder.toString()); + } + + /* (non-Javadoc) + * @see com.android.repository.api.ProgressIndicator#setCancellable(boolean) + */ + @Override + public void setCancellable(boolean cancellable) { + this.cancellable = cancellable; + } + + /* (non-Javadoc) + * @see com.android.repository.api.ProgressIndicator#setFraction(double) + */ + @Override + public void setFraction(double fraction) { + int progress = fraction == 1.0 ? MAX_COUNT : (int)((double)MAX_COUNT * fraction); + monitor.incProgress(progress - monitor.getProgress()); + } + + /* (non-Javadoc) + * @see com.android.repository.api.ProgressIndicator#setIndeterminate(boolean) + */ + @Override + public void setIndeterminate(boolean indeterminate) { + this.indeterminate = indeterminate; + } + + /* (non-Javadoc) + * @see com.android.repository.api.ProgressIndicator#setSecondaryText(java.lang.String) + */ + @Override + public void setSecondaryText(String text) { + // TODO - implement secondary text + monitor.logVerbose(text); + } + + /* (non-Javadoc) + * @see com.android.repository.api.ProgressIndicator#setText(java.lang.String) + */ + @Override + public void setText(String text) { + monitor.setDescription(text); + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/TaskMonitorImpl.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/TaskMonitorImpl.java index eaeb7522..072a8629 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/TaskMonitorImpl.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/tasks/TaskMonitorImpl.java @@ -17,8 +17,8 @@ package com.android.sdkuilib.internal.tasks; import com.android.annotations.NonNull; -import com.android.sdklib.internal.repository.ITaskMonitor; -import com.android.sdklib.internal.repository.UserCredentials; +import com.android.sdkuilib.internal.repository.ITaskMonitor; +import com.android.sdkuilib.internal.repository.UserCredentials; /** * Internal class that implements the logic of an {@link ITaskMonitor}. @@ -177,6 +177,21 @@ public boolean displayPrompt(final String title, final String message) { return mUi.displayPrompt(title, message); } + /** + * Display info dialog box. + * + * Implementations MUST allow this to be called from any thread, e.g. by + * making sure the dialog is opened synchronously in the ui thread. + * + * @param title The title of the dialog box + * @param message The error message + */ + @Override + public void displayInfo(final String title, final String message) + { + mUi.displayInfo(title, message); + }; + /** * Displays a Login/Password dialog. This implementation allows this method to be * called from any thread, it makes sure the dialog is opened synchronously @@ -329,6 +344,11 @@ public boolean displayPrompt(String title, String message) { return mRoot.displayPrompt(title, message); } + @Override + public void displayInfo(String title, String message) { + mRoot.displayInfo(title, message); + } + @Override public UserCredentials displayLoginCredentialsPrompt(String title, String message) { return mRoot.displayLoginCredentialsPrompt(title, message); @@ -365,5 +385,6 @@ public void info(@NonNull String msgFormat, Object... arg) { public void verbose(@NonNull String msgFormat, Object... arg) { mRoot.verbose(msgFormat, arg); } + } } diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationDialog.java index 643732fb..4c633cc2 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationDialog.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationDialog.java @@ -19,11 +19,11 @@ import com.android.annotations.NonNull; import com.android.sdklib.devices.Device; import com.android.sdklib.internal.avd.AvdInfo; -import com.android.sdklib.internal.avd.AvdManager; -import com.android.sdkuilib.internal.repository.icons.ImageFactory; +import com.android.sdkuilib.internal.repository.avd.AvdAgent; +import com.android.sdkuilib.internal.repository.avd.SdkTargets; import com.android.sdkuilib.internal.widgets.AvdCreationPresenter.IWidgetAdapter; -import com.android.utils.ILogger; +import org.eclipse.andmore.sdktool.SdkContext; import org.eclipse.swt.widgets.Shell; /** @@ -42,11 +42,10 @@ public class AvdCreationDialog extends AvdCreationSwtView { public AvdCreationDialog(Shell shell, - AvdManager avdManager, - ImageFactory imageFactory, - ILogger log, - AvdInfo editAvdInfo) { - super(shell, imageFactory, new AvdCreationPresenter(avdManager, log, editAvdInfo)); + @NonNull SdkContext sdkContext, + @NonNull SdkTargets sdkTargets, + AvdAgent editAvdAgent) { + super(shell, sdkContext.getSdkHelper().getImageFactory(), new AvdCreationPresenter(sdkContext, sdkTargets, editAvdAgent)); } diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationPresenter.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationPresenter.java index 338690f6..5926a03a 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationPresenter.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationPresenter.java @@ -20,11 +20,14 @@ import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.prefs.AndroidLocation.AndroidLocationException; +import com.android.repository.Revision; +import com.android.repository.api.ProgressIndicator; import com.android.resources.Density; import com.android.resources.ScreenSize; +import com.android.sdklib.AndroidVersion; import com.android.sdklib.IAndroidTarget; import com.android.sdklib.ISystemImage; -import com.android.sdklib.SystemImage; +import com.android.sdklib.SdkVersionInfo; import com.android.sdklib.devices.Camera; import com.android.sdklib.devices.CameraLocation; import com.android.sdklib.devices.Device; @@ -35,10 +38,15 @@ import com.android.sdklib.devices.Storage; import com.android.sdklib.internal.avd.AvdInfo; import com.android.sdklib.internal.avd.AvdManager; -import com.android.sdklib.internal.avd.AvdManager.AvdConflict; import com.android.sdklib.internal.avd.HardwareProperties; -import com.android.sdklib.repository.descriptors.IdDisplay; -import com.android.sdklib.repository.local.LocalSdk; +import com.android.sdklib.repository.IdDisplay; +import com.android.sdklib.repository.meta.DetailsTypes; +import com.android.sdklib.repository.meta.DetailsTypes.AddonDetailsType.Libraries; +import com.android.sdklib.repository.meta.Library; +import com.android.sdklib.repository.targets.SystemImage; +import com.android.sdkuilib.internal.repository.avd.AvdAgent; +import com.android.sdkuilib.internal.repository.avd.SdkTargets; +import com.android.sdkuilib.widgets.MessageBoxLog; import com.android.utils.ILogger; import com.android.utils.Pair; import com.google.common.base.Joiner; @@ -56,9 +64,10 @@ import java.util.Map; import java.util.Set; import java.util.TreeMap; -import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.eclipse.andmore.sdktool.SdkContext; + /** * Implements all the logic of the {@link AvdCreationDialog}. *

@@ -71,12 +80,23 @@ * constructor. */ class AvdCreationPresenter { - + public enum AvdConflict + { + NO_CONFLICT, CONFLICT_EXISTING_AVD, CONFLICT_INVALID_AVD, CONFLICT_EXISTING_PATH; + + private AvdConflict() {} + } + public static final Pattern RE_AVD_NAME = Pattern.compile("[a-zA-Z0-9._-]+"); + public static final String CHARS_AVD_NAME = "a-z A-Z 0-9 . _ -"; + @NonNull private IWidgetAdapter mWidgets; private AvdManager mAvdManager; private ILogger mSdkLog; private AvdInfo mAvdInfo; + private AvdAgent mAvdAgent; + private final SdkContext mSdkContext; + private final SdkTargets mSdkTargets; private final TreeMap mCurrentTargets = new TreeMap(); @@ -86,8 +106,8 @@ class AvdCreationPresenter { private static final AvdSkinChoice SKIN_NONE = new AvdSkinChoice(SkinType.NONE, "No skin"); - private final List mComboDevices = new ArrayList(); - private final List mComboSkins = new ArrayList(); + private final List mComboDevices = new ArrayList<>(); + private final List mComboSkins = new ArrayList<>(); private final List mComboSystemImages = new ArrayList(); private final List mComboTargets = new ArrayList(); @@ -155,12 +175,16 @@ public interface IWidgetAdapter { } - public AvdCreationPresenter(@NonNull AvdManager avdManager, - @NonNull ILogger log, - @Nullable AvdInfo editAvdInfo) { - mAvdManager = avdManager; - mSdkLog = log; - mAvdInfo = editAvdInfo; + public AvdCreationPresenter(@NonNull SdkContext sdkContext, + @NonNull SdkTargets sdkTargets, + @Nullable AvdAgent editAvdAgent) { + mSdkContext = sdkContext; + mSdkTargets = sdkTargets; + mAvdManager = sdkContext.getAvdManager(); + mSdkLog = mSdkContext.getSdkLog(); + mAvdAgent = editAvdAgent; + if (editAvdAgent != null) + mAvdInfo = editAvdAgent.getAvd(); } /** Returns the AVD Created, if successful. */ @@ -204,7 +228,7 @@ public void onViewInit() { if (mAvdInfo != null) { - fillExistingAvdInfo(mAvdInfo); + fillExistingAvdInfo(mAvdAgent); } else if (mInitWithDevice != null) { fillInitialDeviceInfo(mInitWithDevice); } @@ -217,47 +241,22 @@ public void onViewInit() { private void initializeDevices() { - LocalSdk localSdk = mAvdManager.getLocalSdk(); - File location = localSdk.getLocation(); - if (location != null) { - DeviceManager deviceManager = DeviceManager.createInstance(location, mSdkLog); - Collection deviceList = deviceManager.getDevices(DeviceManager.ALL_DEVICES); - - // Sort - List nexus = new ArrayList(deviceList.size()); - List other = new ArrayList(deviceList.size()); - for (Device device : deviceList) { - if (isNexus(device) && !isGeneric(device)) { - nexus.add(device); - } else { - other.add(device); - } - } - Collections.reverse(other); - Collections.sort(nexus, new Comparator() { - @Override - public int compare(Device device1, Device device2) { - // Descending order of age - return nexusRank(device2) - nexusRank(device1); - } - }); - - mComboDevices.clear(); - mComboDevices.addAll(nexus); - mComboDevices.addAll(other); - - String[] labels = new String[mComboDevices.size()]; - for (int i = 0, n = mComboDevices.size(); i < n; i++) { - Device device = mComboDevices.get(i); - if (isNexus(device) && !isGeneric(device)) { - labels[i] = getNexusLabel(device); - } else { - labels[i] = getGenericLabel(device); - } - } + DeviceManager deviceManager = mSdkContext.getDeviceManager(); + List deviceList = new ArrayList<>(deviceManager.getDevices(DeviceManager.ALL_DEVICES)); + + // Sort + Collections.sort(deviceList, Device.getDisplayComparator()); - mWidgets.setComboItems(Ctrl.COMBO_DEVICE, labels); + mComboDevices.clear(); + mComboDevices.addAll(deviceList); + + String[] labels = new String[mComboDevices.size()]; + for (int i = 0, n = mComboDevices.size(); i < n; i++) { + Device device = mComboDevices.get(i); + labels[i] = getGenericLabel(device); } + + mWidgets.setComboItems(Ctrl.COMBO_DEVICE, labels); } @Nullable @@ -309,8 +308,8 @@ void onAvdNameModified() { // Case where we're creating a new AVD or editing an existing one // and the AVD name has been changed... check for name uniqueness. - Pair conflict = mAvdManager.isAvdNameConflicting(name); - if (conflict.getFirst() != AvdManager.AvdConflict.NO_CONFLICT) { + Pair conflict = isAvdNameConflicting(name); + if (conflict.getFirst() != AvdConflict.NO_CONFLICT) { // If we're changing the state from disabled to enabled, make sure // to uncheck the button, to force the user to voluntarily re-enforce it. // This happens when editing an existing AVD and changing the name from @@ -362,6 +361,26 @@ private void fillDeviceProperties(Device device) { break; case NODPI: break; + case ANYDPI: + break; + case DPI_260: + break; + case DPI_280: + break; + case DPI_300: + break; + case DPI_340: + break; + case DPI_360: + break; + case DPI_400: + break; + case DPI_420: + break; + case DPI_560: + break; + default: + break; } } else { switch (density) { @@ -380,6 +399,26 @@ private void fillDeviceProperties(Device device) { break; case NODPI: break; + case ANYDPI: + break; + case DPI_260: + break; + case DPI_280: + break; + case DPI_300: + break; + case DPI_340: + break; + case DPI_360: + break; + case DPI_400: + break; + case DPI_420: + break; + case DPI_560: + break; + default: + break; } } mWidgets.setText(Ctrl.TEXT_VM_HEAP, Integer.toString(vmHeapSize)); @@ -462,30 +501,31 @@ private void preloadTargetCombo() { index = -1; mComboTargets.clear(); - LocalSdk localSdk = mAvdManager.getLocalSdk(); - if (localSdk != null) { - for (IAndroidTarget target : localSdk.getTargets()) { - String name; - if (target.isPlatform()) { - name = String.format("%s - API Level %s", - target.getName(), - target.getVersion().getApiString()); - } else { - name = String.format("%s (%s) - API Level %s", - target.getName(), - target.getVendor(), - target.getVersion().getApiString()); - } - mCurrentTargets.put(name, target); - mWidgets.addComboItem(Ctrl.COMBO_TARGET, name); - mComboTargets.add(target); - if (!found) { - index++; - found = name.equals(selected); - } + for (IAndroidTarget target : mSdkTargets.getTargets()) { + String name; + if (target.isPlatform()) { + name = target.getFullName(); + } else { + name = target.getName(); + if (!name.equals(target.getVendor())) { + name = String.format("%s (%s) - API Level %s", + name, + target.getVendor(), + target.getVersion().getApiString()); + } else { + name = String.format("%s - API Level %s", + name, + target.getVersion().getApiString()); + } + } + mCurrentTargets.put(name, target); + mWidgets.addComboItem(Ctrl.COMBO_TARGET, name); + mComboTargets.add(target); + if (!found) { + index++; + found = name.equals(selected); } } - mWidgets.setEnabled(Ctrl.COMBO_TARGET, mCurrentTargets.size() > 0); if (found) { @@ -524,8 +564,7 @@ void reloadTagAbiCombo() { String targetName = mWidgets.getComboItem(Ctrl.COMBO_TARGET, index); IAndroidTarget target = mCurrentTargets.get(targetName); - ISystemImage[] systemImages = getSystemImages(target); - + Collection systemImages = getSystemImages(target); // If user explicitly selected an ABI before, preserve that option // If user did not explicitly select before (only one option before) // force them to select @@ -548,15 +587,16 @@ void reloadTagAbiCombo() { mWidgets.setComboItems(Ctrl.COMBO_TAG_ABI, null); mComboSystemImages.clear(); - int i; + int i = 0; boolean found = false; - for (i = 0; i < systemImages.length; i++) { - ISystemImage systemImage = systemImages[i]; + Iterator iterator = systemImages.iterator(); + while(iterator.hasNext()) { + SystemImage systemImage = iterator.next(); if (deviceTagId != null && !deviceTagId.equals(systemImage.getTag().getId())) { continue; } - mComboSystemImages.add(systemImage); String prettyAbiType = AvdInfo.getPrettyAbiType(systemImage); + mComboSystemImages.add(systemImage); mWidgets.addComboItem(Ctrl.COMBO_TAG_ABI, prettyAbiType); if (!found) { found = prettyAbiType.equals(selected); @@ -564,6 +604,7 @@ void reloadTagAbiCombo() { mWidgets.selectComboIndex(Ctrl.COMBO_TAG_ABI, i); } } + ++i; } mWidgets.setEnabled(Ctrl.COMBO_TAG_ABI, !mComboSystemImages.isEmpty()); @@ -575,7 +616,6 @@ void reloadTagAbiCombo() { mWidgets.selectComboIndex(Ctrl.COMBO_TAG_ABI, 0); } } - reloadSkinCombo(); } @@ -599,7 +639,7 @@ void reloadSkinCombo() { } // path of sdk/system-images - String sdkSysImgPath = new File(mAvdManager.getLocalSdk().getLocation(), + String sdkSysImgPath = new File(mSdkContext.getLocation(), SdkConstants.FD_SYSTEM_IMAGES).getAbsolutePath(); for (File skin : target.getSkins()) { @@ -678,10 +718,10 @@ void validatePage() { return; } - if (!AvdManager.RE_AVD_NAME.matcher(avdName).matches()) { + if (!RE_AVD_NAME.matcher(avdName).matches()) { error = String.format( "AVD name '%1$s' contains invalid characters.\nAllowed characters are: %2$s", - avdName, AvdManager.CHARS_AVD_NAME); + avdName, CHARS_AVD_NAME); setPageValid(false, error, null); return; } @@ -713,8 +753,8 @@ void validatePage() { IAndroidTarget target = mCurrentTargets.get(targetName); if (target != null && !target.isPlatform()) { - ISystemImage[] sis = target.getSystemImages(); - if (sis != null && sis.length > 0) { + Collection sis = mSdkTargets.getSystemImages(target); + if (sis != null && sis.size() > 0) { // Note: if an addon has no system-images of its own, it depends on its parent // platform and it wouldn't have been loaded properly if the platform were // missing so we don't need to double-check that part here. @@ -724,7 +764,7 @@ void validatePage() { String abiType = tagAbi.getSecond(); if (abiType != null && !abiType.isEmpty() && - target.getParent().getSystemImage(tag, abiType) == null) { + getSystemImage(target.getParent(), tag, abiType) == null) { // We have a system-image requirement but there is no such system image // loaded in the parent platform. This AVD won't run properly. warnings.add( @@ -875,7 +915,16 @@ boolean createAvd() { } IdDisplay tag = tagAbi.getFirst(); String abiType = tagAbi.getSecond(); - + ISystemImage systemImage = null; + Collection sysImgs = mSdkContext.getHandler().getSystemImageManager(mSdkContext.getProgressIndicator()).getImages(); + for (SystemImage img : sysImgs) { + if (img.getAbiType().equals(abiType)) { + systemImage = img; + break; + } + } + if (systemImage == null) + return false; // get the SD card data from the UI. String sdName = null; if (mWidgets.isChecked(Ctrl.RADIO_SDCARD_SIZE)) { @@ -939,7 +988,8 @@ boolean createAvd() { File avdFolder = null; try { - avdFolder = AvdInfo.getDefaultAvdFolder(mAvdManager, avdName); + AvdInfo.getDefaultAvdFolder( + mAvdManager, avdName, mSdkContext.getHandler().getFileOp(), false); } catch (AndroidLocationException e) { return false; } @@ -985,16 +1035,30 @@ boolean createAvd() { hwProps.put(HardwareProperties.HW_SDCARD, HardwareProperties.BOOLEAN_YES); } + /* + @NonNull File avdFolder, + @NonNull String avdName, + @NonNull ISystemImage systemImage, + @Nullable File skinFolder, + @Nullable String skinName, + @Nullable String sdcard, + @Nullable Map hardwareConfig, + @Nullable Map bootProps, + boolean deviceHasPlayStore, + boolean createSnapshot, + boolean removePrevious, + boolean editExisting, + @NonNull ILogger log) { + */ AvdInfo avdInfo = mAvdManager.createAvd(avdFolder, avdName, - target, - tag, - abiType, + systemImage, skinFolder, skinName, sdName, hwProps, device.getBootProps(), + false, // deviceHasPlayStore mWidgets.isChecked(Ctrl.CHECK_SNAPSHOT), mWidgets.isChecked(Ctrl.CHECK_FORCE_CREATION), mAvdInfo != null, // edit existing @@ -1038,12 +1102,12 @@ private ISystemImage getSelectedSysImg() { return null; } - private void fillExistingAvdInfo(AvdInfo avd) { - mWidgets.setText(Ctrl.TEXT_AVD_NAME, avd.getName()); - selectDevice(avd.getDeviceManufacturer(), avd.getDeviceName()); + private void fillExistingAvdInfo(AvdAgent avdAgent) { + mWidgets.setText(Ctrl.TEXT_AVD_NAME, avdAgent.getAvd().getName()); + selectDevice(avdAgent.getDeviceMfctr(), avdAgent.getDeviceName()); toggleCameras(); - IAndroidTarget target = avd.getTarget(); + IAndroidTarget target = avdAgent.getTarget(); if (target != null && !mCurrentTargets.isEmpty()) { // Try to select the target in the target combo. @@ -1061,13 +1125,12 @@ private void fillExistingAvdInfo(AvdInfo avd) { } } } - - ISystemImage[] systemImages = getSystemImages(target); - if (target != null && systemImages.length > 0) { - mWidgets.setEnabled(Ctrl.COMBO_TAG_ABI, systemImages.length > 1); - String abiType = AvdInfo.getPrettyAbiType(avd.getTag(), avd.getAbiType()); - int n = mWidgets.getComboSize(Ctrl.COMBO_TAG_ABI); - for (int i = 0; i < n; i++) { + Collection systemImages = getSystemImages(target); + if (target != null && systemImages.size() > 0) { + mWidgets.setEnabled(Ctrl.COMBO_TAG_ABI, systemImages.size() > 1); + String abiType = mAvdAgent.getPrettyAbiType(); + int count = mWidgets.getComboSize(Ctrl.COMBO_TAG_ABI); + for (int i = 0; i < count; i++) { if (abiType.equals(mWidgets.getComboItem(Ctrl.COMBO_TAG_ABI, i))) { mWidgets.selectComboIndex(Ctrl.COMBO_TAG_ABI, i); reloadSkinCombo(); @@ -1076,7 +1139,7 @@ private void fillExistingAvdInfo(AvdInfo avd) { } } - Map props = avd.getProperties(); + Map props = avdAgent.getAvd().getProperties(); if (props != null) { String snapshots = props.get(AvdManager.AVD_INI_SNAPSHOT_PRESENT); @@ -1130,15 +1193,15 @@ private void fillExistingAvdInfo(AvdInfo avd) { // the AVD .ini skin path is relative to the SDK folder *or* is a numeric size. String skinIniPath = props.get(AvdManager.AVD_INI_SKIN_PATH); if (skinIniPath != null) { - File skinFolder = new File(mAvdManager.getLocalSdk().getLocation(), skinIniPath); - - for (int i = 0; i < mComboSkins.size(); i++) { - if (mComboSkins.get(i).hasPath() && - skinFolder.equals(mComboSkins.get(i).getPath())) { - mWidgets.selectComboIndex(Ctrl.COMBO_SKIN, i); - defaultSkinType = null; - break; - } + File skinFolder = getAvdPath(skinIniPath); + if (skinFolder != null) + for (int i = 0; i < mComboSkins.size(); i++) { + if (mComboSkins.get(i).hasPath() && + skinFolder.equals(mComboSkins.get(i).getPath())) { + mWidgets.selectComboIndex(Ctrl.COMBO_SKIN, i); + defaultSkinType = null; + break; + } } } @@ -1203,19 +1266,22 @@ private void fillExistingAvdInfo(AvdInfo avd) { } } - @SuppressWarnings("deprecation") + private File getAvdPath(String subDirectory) + { + File file = null; + try { + file = new File(mAvdManager.getBaseAvdFolder(), subDirectory); + } catch (AndroidLocationException e) { + mSdkContext.getSdkLog().error(e, subDirectory); + } + return file; + } + + private void fillInitialDeviceInfo(Device device) { - String name = device.getManufacturer(); - if (!name.equals("Generic") && // TODO define & use constants - !name.equals("User") && - device.getName().indexOf(name) == -1) { - name = " by " + name; - } else { - name = ""; - } - name = "AVD for " + device.getName() + name; + String name = "AVD for " + device.getDisplayName(); // sanitize the name - name = name.replaceAll("[^0-9a-zA-Z_-]+", " ").trim().replaceAll("[ _]+", "_"); + //name = name.replaceAll("[^0-9a-zA-Z_-]+", " ").trim().replaceAll("[ _]+", "_"); mWidgets.setText(Ctrl.TEXT_AVD_NAME, name); // Select the device @@ -1243,112 +1309,15 @@ private void fillInitialDeviceInfo(Device device) { * @return A non-null ISystemImage array. Can be empty. */ @NonNull - private ISystemImage[] getSystemImages(IAndroidTarget target) { - if (target != null) { - ISystemImage[] images = target.getSystemImages(); - - if ((images == null || images.length == 0) && !target.isPlatform()) { - // This is an add-on and it does not provide any system image. - - // Before LMP / Tools 23.0.4, the behavior was to provide the - // parent (platform) system-image using this code: - // - // images = target.getParent().getSystemImages(); - // - // After tools 23.0.4, the behavior is to NOT provide the - // platform system-image for the add-on. - } - - if (images != null) { - return images; - } - } - - return new ISystemImage[0]; - } - - // Code copied from DeviceMenuListener in ADT; unify post release - - private static final String NEXUS = "Nexus"; //$NON-NLS-1$ - private static final String GENERIC = "Generic"; //$NON-NLS-1$ - private static Pattern PATTERN = Pattern.compile( - "(\\d+\\.?\\d*)in (.+?)( \\(.*Nexus.*\\))?"); //$NON-NLS-1$ - - private static int nexusRank(Device device) { - @SuppressWarnings("deprecation") - String name = device.getName(); - if (name.endsWith(" One")) { //$NON-NLS-1$ - return 1; - } - if (name.endsWith(" S")) { //$NON-NLS-1$ - return 2; - } - if (name.startsWith("Galaxy")) { //$NON-NLS-1$ - return 3; - } - if (name.endsWith(" 7")) { //$NON-NLS-1$ - return 4; - } - if (name.endsWith(" 10")) { //$NON-NLS-1$ - return 5; - } - if (name.endsWith(" 4")) { //$NON-NLS-1$ - return 6; - } - - return 7; - } - - private static boolean isGeneric(Device device) { - return device.getManufacturer().equals(GENERIC); - } - - @SuppressWarnings("deprecation") - private static boolean isNexus(Device device) { - return device.getName().contains(NEXUS); + private Collection getSystemImages(IAndroidTarget target) { + return mSdkTargets.getSystemImages(target); } private static String getGenericLabel(Device d) { - // * Replace "'in'" with '"' (e.g. 2.7" QVGA instead of 2.7in QVGA) - // * Use the same precision for all devices (all but one specify decimals) - // * Add some leading space such that the dot ends up roughly in the - // same space - // * Add in screen resolution and density - @SuppressWarnings("deprecation") - String name = d.getName(); - if (name.equals("3.7 FWVGA slider")) { //$NON-NLS-1$ - // Fix metadata: this one entry doesn't have "in" like the rest of them - name = "3.7in FWVGA slider"; //$NON-NLS-1$ - } - - Matcher matcher = PATTERN.matcher(name); - if (matcher.matches()) { - String size = matcher.group(1); - String n = matcher.group(2); - int dot = size.indexOf('.'); - if (dot == -1) { - size = size + ".0"; - dot = size.length() - 2; - } - for (int i = 0; i < 2 - dot; i++) { - size = ' ' + size; - } - name = size + "\" " + n; - } - - return String.format(java.util.Locale.US, "%1$s (%2$s)", name, + return String.format(java.util.Locale.US, "%1$s (%2$s)", d.getDisplayName(), getResolutionString(d)); } - private static String getNexusLabel(Device d) { - @SuppressWarnings("deprecation") - String name = d.getName(); - Screen screen = d.getDefaultHardware().getScreen(); - float length = (float) screen.getDiagonalLength(); - return String.format(java.util.Locale.US, "%1$s (%3$s\", %2$s)", - name, getResolutionString(d), Float.toString(length)); - } - @Nullable private static String getResolutionString(Device device) { Screen screen = device.getDefaultHardware().getScreen(); @@ -1359,9 +1328,6 @@ private static String getResolutionString(Device device) { screen.getPixelDensity().getResourceValue()); } - //------- - - /** * AVD skin type. Order defines the order of the skin combo list. */ @@ -1484,5 +1450,47 @@ public void onRadioSdCardSizeChanged() { validatePage(); } + private ISystemImage getSystemImage(IAndroidTarget target, IdDisplay tag, String abiType) + { + if (target != null) + { + Collection systemImages = getSystemImages(target); + for (SystemImage sysImg : systemImages) { + if ((sysImg.getTag().equals(tag)) && (sysImg.getAbiType().equals(abiType))) { + return sysImg; + } + } + } + return null; + } + private Pair isAvdNameConflicting(String name) + { + boolean ignoreCase = SdkConstants.currentPlatform() == 2; + AvdInfo[] allAvdList = mAvdManager.getAllAvds(); + for (AvdInfo info : allAvdList) + { + String name2 = info.getName(); + if ((name2.equals(name)) || ((ignoreCase) && (name2.equalsIgnoreCase(name)))) + { + if (info.getStatus() == AvdInfo.AvdStatus.OK) { + return Pair.of(AvdConflict.CONFLICT_EXISTING_AVD, name2); + } + return Pair.of(AvdConflict.CONFLICT_INVALID_AVD, name2); + } + } + try + { + File file = AvdInfo.getDefaultIniFile(mAvdManager, name); + if (file.exists()) { + return Pair.of(AvdConflict.CONFLICT_EXISTING_PATH, file.getPath()); + } + file = AvdInfo.getDefaultAvdFolder(mAvdManager, name, mSdkContext.getHandler().getFileOp(), false); + if (file.exists()) { + return Pair.of(AvdConflict.CONFLICT_EXISTING_PATH, file.getPath()); + } + } + catch (AndroidLocationException e) {} + return Pair.of(AvdConflict.NO_CONFLICT, null); + } } diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationSwtView.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationSwtView.java index dd2cef89..7dc1ad8d 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationSwtView.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdCreationSwtView.java @@ -18,11 +18,12 @@ import com.android.annotations.NonNull; import com.android.annotations.Nullable; -import com.android.sdkuilib.internal.repository.icons.ImageFactory; import com.android.sdkuilib.internal.widgets.AvdCreationPresenter.Ctrl; import com.android.sdkuilib.internal.widgets.AvdCreationPresenter.IWidgetAdapter; import com.android.sdkuilib.ui.GridDialog; +import com.android.sdkuilib.widgets.MessageBoxLog; +import org.eclipse.andmore.base.resources.ImageFactory; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.swt.SWT; import org.eclipse.swt.events.ModifyEvent; diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdDetailsDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdDetailsDialog.java index 2f6b8800..b4172a43 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdDetailsDialog.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdDetailsDialog.java @@ -16,35 +16,35 @@ package com.android.sdkuilib.internal.widgets; -import com.android.sdklib.AndroidVersion; -import com.android.sdklib.IAndroidTarget; -import com.android.sdklib.internal.avd.AvdInfo; -import com.android.sdklib.internal.avd.AvdInfo.AvdStatus; -import com.android.sdklib.internal.avd.AvdManager; -import com.android.sdkuilib.ui.GridDataBuilder; -import com.android.sdkuilib.ui.GridLayoutBuilder; -import com.android.sdkuilib.ui.SwtBaseDialog; +import java.util.HashMap; +import java.util.Map; import org.eclipse.swt.SWT; 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.Label; import org.eclipse.swt.widgets.Shell; -import java.util.HashMap; -import java.util.Map; +import com.android.sdklib.internal.avd.AvdInfo.AvdStatus; +import com.android.sdklib.internal.avd.AvdManager; +import com.android.sdkuilib.internal.repository.avd.AvdAgent; +import com.android.sdkuilib.ui.GridDataBuilder; +import com.android.sdkuilib.ui.GridLayoutBuilder; +import com.android.sdkuilib.ui.SwtBaseDialog; /** * Dialog displaying the details of an AVD. */ final class AvdDetailsDialog extends SwtBaseDialog { - private final AvdInfo mAvdInfo; + private final AvdAgent mAvdAgent; + private volatile int row = 0; - public AvdDetailsDialog(Shell shell, AvdInfo avdInfo) { + public AvdDetailsDialog(Shell shell, AvdAgent avdAgent) { super(shell, SWT.APPLICATION_MODAL, "AVD details"); - mAvdInfo = avdInfo; + mAvdAgent = avdAgent; } /** @@ -61,68 +61,59 @@ protected void createContents() { Composite c = new Composite(shell, SWT.NONE); c.setLayout(gl = new GridLayout(2, false)); gl.marginHeight = gl.marginWidth = 0; - c.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - - if (mAvdInfo != null) { - displayValue(c, "Name:", mAvdInfo.getName()); - displayValue(c, "CPU/ABI:", AvdInfo.getPrettyAbiType(mAvdInfo)); - - displayValue(c, "Path:", mAvdInfo.getDataFolderPath()); - - if (mAvdInfo.getStatus() != AvdStatus.OK) { - displayValue(c, "Error:", mAvdInfo.getErrorMessage()); + GridData gridData = new GridData(); + gridData.horizontalAlignment = GridData.FILL; + gridData.grabExcessHorizontalSpace = true; + c.setLayoutData(gridData); + //Display display = c.getDisplay(); + //c.setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND)); + //c.setForeground(display.getSystemColor(SWT.COLOR_INFO_FOREGROUND)); + + if (mAvdAgent != null) { + displayValue(c, "Name:", mAvdAgent.getAvd().getName()); + displayValue(c, "CPU/ABI:", mAvdAgent.getPrettyAbiType()); + displayValue(c, "Path:", mAvdAgent.getPath()); + if (mAvdAgent.getAvd().getStatus() != AvdStatus.OK) { + displayValue(c, "Error:", mAvdAgent.getAvd().getErrorMessage()); } else { - IAndroidTarget target = mAvdInfo.getTarget(); - AndroidVersion version = target.getVersion(); - displayValue(c, "Target:", String.format("%s (API level %s)", - target.getName(), version.getApiString())); - - // display some extra values. - Map properties = mAvdInfo.getProperties(); - if (properties != null) { - String skin = properties.get(AvdManager.AVD_INI_SKIN_NAME); - if (skin != null) { - displayValue(c, "Skin:", skin); - } - - String sdcard = properties.get(AvdManager.AVD_INI_SDCARD_SIZE); - if (sdcard == null) { - sdcard = properties.get(AvdManager.AVD_INI_SDCARD_PATH); - } - if (sdcard != null) { - displayValue(c, "SD Card:", sdcard); - } - - String snapshot = properties.get(AvdManager.AVD_INI_SNAPSHOT_PRESENT); - if (snapshot != null) { - displayValue(c, "Snapshot:", snapshot); - } - - // display other hardware - HashMap copy = new HashMap(properties); - // remove stuff we already displayed (or that we don't want to display) - copy.remove(AvdManager.AVD_INI_ABI_TYPE); - copy.remove(AvdManager.AVD_INI_CPU_ARCH); - copy.remove(AvdManager.AVD_INI_SKIN_NAME); - copy.remove(AvdManager.AVD_INI_SKIN_PATH); - copy.remove(AvdManager.AVD_INI_SDCARD_SIZE); - copy.remove(AvdManager.AVD_INI_SDCARD_PATH); - copy.remove(AvdManager.AVD_INI_IMAGES_1); - copy.remove(AvdManager.AVD_INI_IMAGES_2); - - if (copy.size() > 0) { - Label l = new Label(shell, SWT.SEPARATOR | SWT.HORIZONTAL); - l.setLayoutData(new GridData( - GridData.FILL, GridData.CENTER, false, false, 2, 1)); - - c = new Composite(shell, SWT.NONE); - c.setLayout(gl = new GridLayout(2, false)); - gl.marginHeight = gl.marginWidth = 0; - c.setLayoutData(new GridData(GridData.FILL_BOTH)); - - for (Map.Entry entry : copy.entrySet()) { - displayValue(c, entry.getKey() + ":", entry.getValue()); - } + displayValue(c, "Target:", mAvdAgent.getTargetDisplayName()); + displayValue(c, "Skin:", mAvdAgent.getSkin()); + String sdcard = mAvdAgent.getSdcard(); + if (!sdcard.isEmpty()) { + displayValue(c, "SD Card:", sdcard); + } + String snapshot = mAvdAgent.getSnapshot(); + if (!snapshot.isEmpty()) { + displayValue(c, "Snapshot:", snapshot); + } + // display other hardware + HashMap copy = new HashMap(mAvdAgent.getAvd().getProperties()); + // remove stuff we already displayed (or that we don't want to display) + copy.remove(AvdManager.AVD_INI_ABI_TYPE); + copy.remove(AvdManager.AVD_INI_CPU_ARCH); + copy.remove(AvdManager.AVD_INI_SKIN_NAME); + copy.remove(AvdManager.AVD_INI_SKIN_PATH); + copy.remove(AvdManager.AVD_INI_SDCARD_SIZE); + copy.remove(AvdManager.AVD_INI_SDCARD_PATH); + copy.remove(AvdManager.AVD_INI_IMAGES_1); + copy.remove(AvdManager.AVD_INI_IMAGES_2); + + if (copy.size() > 0) { + Label l = new Label(shell, SWT.SEPARATOR | SWT.HORIZONTAL); + l.setLayoutData(new GridData( + GridData.FILL, GridData.HORIZONTAL_ALIGN_BEGINNING, false, false, 2, 1)); + //display = l.getDisplay(); + //l.setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND)); + c = new Composite(shell, SWT.NONE); + c.setLayout(gl = new GridLayout(2, false)); + //display = c.getDisplay(); + gl.marginHeight = gl.marginWidth = 0; + //c.setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND)); + c.setLayoutData(new GridData(GridData.FILL_BOTH)); + //c.setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND)); + //c.setForeground(display.getSystemColor(SWT.COLOR_INFO_FOREGROUND)); + for (Map.Entry entry : copy.entrySet()) { + displayValue(c, entry.getKey() + ":", entry.getValue()); } } } @@ -147,14 +138,25 @@ protected void postCreate() { * @param label the label of the value to display. * @param value the string value to display. */ - private void displayValue(Composite parent, String label, String value) { - Label l = new Label(parent, SWT.NONE); - l.setText(label); - l.setLayoutData(new GridData(GridData.END, GridData.CENTER, false, false)); - - l = new Label(parent, SWT.NONE); - l.setText(value); - l.setLayoutData(new GridData(GridData.FILL, GridData.CENTER, true, false)); + private void displayValue(Composite parent, String key, String value) { + Label label = new Label(parent, SWT.LEFT); + label.setLayoutData(new GridData(GridData.FILL, GridData.VERTICAL_ALIGN_CENTER, false, false)); + Display display = label.getDisplay(); + if ((row & 1) == 0) { + label.setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND)); + label.setForeground(display.getSystemColor(SWT.COLOR_INFO_FOREGROUND)); + } + label.setText(key); + + label = new Label(parent, SWT.LEFT); + label.setLayoutData(new GridData(GridData.FILL, GridData.VERTICAL_ALIGN_CENTER, true, false)); + display = label.getDisplay(); + if ((row & 1) == 0) { + label.setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND)); + label.setForeground(display.getSystemColor(SWT.COLOR_INFO_FOREGROUND)); + } + label.setText(value); + ++row; } // End of hiding from SWT Designer diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdSelector.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdSelector.java index 7d05df97..49ff11ae 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdSelector.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdSelector.java @@ -16,31 +16,17 @@ package com.android.sdkuilib.internal.widgets; -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.prefs.AndroidLocation.AndroidLocationException; -import com.android.sdklib.IAndroidTarget; -import com.android.sdklib.SystemImage; -import com.android.sdklib.internal.avd.AvdInfo; -import com.android.sdklib.internal.avd.AvdInfo.AvdStatus; -import com.android.sdklib.internal.avd.AvdManager; -import com.android.sdklib.internal.repository.ITask; -import com.android.sdklib.internal.repository.ITaskMonitor; -import com.android.sdklib.internal.repository.updater.SettingsController; -import com.android.sdklib.repository.descriptors.IdDisplay; -import com.android.sdkuilib.internal.repository.icons.ImageFactory; -import com.android.sdkuilib.internal.repository.icons.ImageFactory.Filter; -import com.android.sdkuilib.internal.repository.ui.AvdManagerWindowImpl1; -import com.android.sdkuilib.internal.tasks.ProgressTask; -import com.android.sdkuilib.repository.AvdManagerWindow.AvdInvocationContext; -import com.android.sdkuilib.ui.GridDialog; -import com.android.utils.GrabProcessOutput; -import com.android.utils.GrabProcessOutput.IProcessOutput; -import com.android.utils.GrabProcessOutput.Wait; -import com.android.utils.ILogger; -import com.android.utils.NullLogger; +import java.awt.DisplayMode; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Formatter; +import java.util.Locale; +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.andmore.sdktool.SdkContext; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.viewers.DecorationOverlayIcon; @@ -49,12 +35,11 @@ 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.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; @@ -62,18 +47,37 @@ import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.MessageBox; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableColumn; import org.eclipse.swt.widgets.TableItem; -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.Formatter; -import java.util.Locale; +import com.android.SdkConstants; +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.prefs.AndroidLocation.AndroidLocationException; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.internal.avd.AvdInfo; +import com.android.sdklib.internal.avd.AvdInfo.AvdStatus; +import com.android.sdklib.internal.avd.AvdManager; +import com.android.sdklib.repository.IdDisplay; +import com.android.sdklib.repository.targets.SystemImage; +import com.android.sdkuilib.internal.repository.ITask; +import com.android.sdkuilib.internal.repository.ITaskMonitor; +import com.android.sdkuilib.internal.repository.avd.AvdAgent; +import com.android.sdkuilib.internal.repository.avd.SdkTargets; +import com.android.sdkuilib.internal.repository.ui.AvdManagerWindowImpl1; +import com.android.sdkuilib.internal.tasks.ProgressTask; +import com.android.sdkuilib.repository.AvdManagerWindow.AvdInvocationContext; +import com.android.sdkuilib.ui.AvdDisplayMode; +import com.android.sdkuilib.ui.GridDialog; +import com.android.sdkuilib.widgets.MessageBoxLog; +import com.android.utils.GrabProcessOutput; +import com.android.utils.GrabProcessOutput.IProcessOutput; +import com.android.utils.GrabProcessOutput.Wait; +import com.android.utils.ILogger; +import com.android.utils.NullLogger; /** @@ -86,11 +90,9 @@ public final class AvdSelector { private static int NUM_COL = 2; - private final DisplayMode mDisplayMode; - + private final AvdDisplayMode mDisplayMode; + private final SdkTargets mSdkTargets; private AvdManager mAvdManager; - private final String mOsSdkPath; // TODO consider converting to File later - private Table mTable; private Button mDeleteButton; private Button mDetailsButton; @@ -111,43 +113,10 @@ public final class AvdSelector { private ImageFactory mImageFactory; private Image mBrokenImage; private Image mInvalidImage; - - private SettingsController mController; - - private final ILogger mSdkLog; + private final SdkContext mSdkContext; private boolean mInternalRefresh; - - /** - * The display mode of the AVD Selector. - */ - public static enum DisplayMode { - /** - * Manager mode. Invalid AVDs are displayed. Buttons to create/delete AVDs - */ - MANAGER, - - /** - * Non manager mode. Only valid AVDs are displayed. Cannot create/delete AVDs, but - * there is a button to open the AVD Manager. - * In the "check" selection mode, checkboxes are displayed on each line - * and {@link AvdSelector#getSelected()} returns the line that is checked - * even if it is not the currently selected line. Only one line can - * be checked at once. - */ - SIMPLE_CHECK, - - /** - * Non manager mode. Only valid AVDs are displayed. Cannot create/delete AVDs, but - * there is a button to open the AVD Manager. - * In the "select" selection mode, there are no checkboxes and - * {@link AvdSelector#getSelected()} returns the line currently selected. - * Only one line can be selected at once. - */ - SIMPLE_SELECTION, - } - /** * A filter to control the whether or not an AVD should be displayed by the AVD Selector. */ @@ -159,10 +128,10 @@ public interface IAvdFilter { /** * Called to decided whether an AVD should be displayed. - * @param avd the AVD to test. + * @param avdAgent Agent containing the AVD to test. * @return true if the AVD should be displayed. */ - boolean accept(AvdInfo avd); + boolean accept(AvdAgent avdAgent); /** * Called after {@link #accept(AvdInfo)} has been called on all the AVDs. @@ -187,9 +156,9 @@ public void prepare() { } @Override - public boolean accept(AvdInfo avd) { - if (avd != null) { - return mTarget.canRunOn(avd.getTarget()); + public boolean accept(AvdAgent avdAgent) { + if (avdAgent != null) { + return mTarget.canRunOn(avdAgent.getTarget()); } return false; @@ -208,27 +177,24 @@ public void cleanup() { * {@link IAndroidTarget} will be displayed. * * @param parent The parent composite where the selector will be added. - * @param osSdkPath The SDK root path. When not null, enables the start button to start - * an emulator on a given AVD. + * @param sdkContext SDK handler and repo manager * @param manager the AVD manager. * @param filter When non-null, will allow filtering the AVDs to display. * @param displayMode The display mode ({@link DisplayMode}). * @param sdkLog The logger. Cannot be null. */ public AvdSelector(Composite parent, - String osSdkPath, - AvdManager manager, + SdkContext sdkContext, IAvdFilter filter, - DisplayMode displayMode, - ILogger sdkLog) { - mOsSdkPath = osSdkPath; - mAvdManager = manager; + AvdDisplayMode displayMode) { + mSdkTargets = new SdkTargets(sdkContext); + mSdkContext = sdkContext; + mAvdManager = sdkContext.getAvdManager(); mTargetFilter = filter; mDisplayMode = displayMode; - mSdkLog = sdkLog; // get some bitmaps. - mImageFactory = new ImageFactory(parent.getDisplay()); + mImageFactory = mSdkContext.getSdkHelper().getImageFactory(); mBrokenImage = mImageFactory.getImageByName("warning_icon16.png"); mInvalidImage = mImageFactory.getImageByName("reject_icon16.png"); @@ -239,15 +205,9 @@ public AvdSelector(Composite parent, gl.marginHeight = gl.marginWidth = 0; group.setLayoutData(new GridData(GridData.FILL_BOTH)); group.setFont(parent.getFont()); - group.addDisposeListener(new DisposeListener() { - @Override - public void widgetDisposed(DisposeEvent arg0) { - mImageFactory.dispose(); - } - }); int style = SWT.FULL_SELECTION | SWT.SINGLE | SWT.BORDER; - if (displayMode == DisplayMode.SIMPLE_CHECK) { + if (displayMode == AvdDisplayMode.SIMPLE_CHECK) { style |= SWT.CHECK; } mTable = new Table(group, style); @@ -261,7 +221,7 @@ public void widgetDisposed(DisposeEvent arg0) { buttons.setLayoutData(new GridData(GridData.FILL_VERTICAL)); buttons.setFont(group.getFont()); - if (displayMode == DisplayMode.MANAGER) { + if (displayMode == AvdDisplayMode.MANAGER) { mNewButton = new Button(buttons, SWT.PUSH | SWT.FLAT); mNewButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mNewButton.setText("Create..."); @@ -289,7 +249,7 @@ public void widgetSelected(SelectionEvent arg0) { @SuppressWarnings("unused") Label spacing = new Label(buttons, SWT.NONE); - if (displayMode == DisplayMode.MANAGER) { + if (displayMode == AvdDisplayMode.MANAGER) { mEditButton = new Button(buttons, SWT.PUSH | SWT.FLAT); mEditButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mEditButton.setText("Edit..."); @@ -349,7 +309,7 @@ public void widgetSelected(SelectionEvent arg0) { } }); - if (displayMode != DisplayMode.MANAGER) { + if (displayMode != AvdDisplayMode.MANAGER) { mManagerButton = new Button(buttons, SWT.PUSH | SWT.FLAT); mManagerButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); mManagerButton.setText("Manager..."); @@ -401,14 +361,13 @@ public void widgetSelected(SelectionEvent e) { * @param sdkLog The logger. Cannot be null. */ public AvdSelector(Composite parent, - String osSdkPath, - AvdManager manager, - DisplayMode displayMode, - ILogger sdkLog) { - this(parent, osSdkPath, manager, (IAvdFilter)null /* filter */, displayMode, sdkLog); + SdkContext sdkContext, + AvdDisplayMode displayMode) { + this(parent, sdkContext, (IAvdFilter)null /* filter */, displayMode); } /** + * *** NOT REFERENCED *** * Creates a new SDK Target Selector, and fills it with a list of {@link AvdInfo}, filtered * by an {@link IAndroidTarget}. *

Only the {@link AvdInfo} able to run applications developed for the given @@ -421,20 +380,10 @@ public AvdSelector(Composite parent, * @param sdkLog The logger. Cannot be null. */ public AvdSelector(Composite parent, - String osSdkPath, - AvdManager manager, + SdkContext sdkContext, IAndroidTarget filter, - DisplayMode displayMode, - ILogger sdkLog) { - this(parent, osSdkPath, manager, new TargetBasedFilter(filter), displayMode, sdkLog); - } - - /** - * Sets an optional SettingsController. - * @param controller the controller. - */ - public void setSettingsController(SettingsController controller) { - mController = controller; + AvdDisplayMode displayMode) { + this(parent, sdkContext, new TargetBasedFilter(filter), displayMode); } /** @@ -479,7 +428,7 @@ public boolean refresh(boolean reload) { } } - AvdInfo selected = getSelected(); + AvdAgent selected = getSelected(); fillTable(mTable); setSelection(selected); return true; @@ -544,7 +493,21 @@ public void setSelectionListener(SelectionListener selectionListener) { mSelectionListener = selectionListener; } - /** + /** + * Sets the current target selection. + *

+ * If the selection is actually changed, this will invoke the selection listener + * (if any) with a null event. + * + * @param target the target to be selected. Use null to deselect everything. + * @return true if the target could be selected, false otherwise. + */ + public void setSelection(AvdInfo avd) { + AvdAgent target = new AvdAgent(mSdkTargets.getTargetForSysImage((SystemImage)avd.getSystemImage()), avd); + setSelection(target); + } + + /** * Sets the current target selection. *

* If the selection is actually changed, this will invoke the selection listener @@ -553,15 +516,16 @@ public void setSelectionListener(SelectionListener selectionListener) { * @param target the target to be selected. Use null to deselect everything. * @return true if the target could be selected, false otherwise. */ - public boolean setSelection(AvdInfo target) { + public boolean setSelection(AvdAgent target) { boolean found = false; boolean modified = false; int selIndex = mTable.getSelectionIndex(); int index = 0; for (TableItem i : mTable.getItems()) { - if (mDisplayMode == DisplayMode.SIMPLE_CHECK) { - if ((AvdInfo) i.getData() == target) { + AvdAgent avdAgent = (AvdAgent)i.getData(); + if (mDisplayMode == AvdDisplayMode.SIMPLE_CHECK) { + if (avdAgent == target) { found = true; if (!i.getChecked()) { modified = true; @@ -572,7 +536,7 @@ public boolean setSelection(AvdInfo target) { i.setChecked(false); } } else { - if ((AvdInfo) i.getData() == target) { + if (avdAgent == target) { found = true; if (index != selIndex) { mTable.setSelection(index); @@ -584,13 +548,10 @@ public boolean setSelection(AvdInfo target) { index++; } } - if (modified && mSelectionListener != null) { mSelectionListener.widgetSelected(null); } - enableActionButtons(); - return found; } @@ -600,20 +561,19 @@ public boolean setSelection(AvdInfo target) { * * @return The currently selected item or null. */ - public AvdInfo getSelected() { - if (mDisplayMode == DisplayMode.SIMPLE_CHECK) { + public AvdAgent getSelected() { + if (mDisplayMode == AvdDisplayMode.SIMPLE_CHECK) { for (TableItem i : mTable.getItems()) { if (i.getChecked()) { - return (AvdInfo) i.getData(); + return (AvdAgent) i.getData(); } } } else { int selIndex = mTable.getSelectionIndex(); if (selIndex >= 0) { - return (AvdInfo) mTable.getItem(selIndex).getData(); + return (AvdAgent) mTable.getItem(selIndex).getData(); } } - return null; } @@ -709,7 +669,7 @@ public void widgetSelected(SelectionEvent e) { public void widgetDefaultSelected(SelectionEvent e) { if (e.item instanceof TableItem) { TableItem i = (TableItem) e.item; - if (mDisplayMode == DisplayMode.SIMPLE_CHECK) { + if (mDisplayMode == AvdDisplayMode.SIMPLE_CHECK) { i.setChecked(true); } enforceSingleSelection(i); @@ -717,7 +677,7 @@ public void widgetDefaultSelected(SelectionEvent e) { } // whether or not we display details. default: true when not in SIMPLE_CHECK mode. - boolean showDetails = mDisplayMode != DisplayMode.SIMPLE_CHECK; + boolean showDetails = mDisplayMode != AvdDisplayMode.SIMPLE_CHECK; if (mSelectionListener != null) { mSelectionListener.widgetDefaultSelected(e); @@ -736,7 +696,7 @@ public void widgetDefaultSelected(SelectionEvent e) { * This makes the chekboxes act as radio buttons. */ private void enforceSingleSelection(TableItem item) { - if (mDisplayMode == DisplayMode.SIMPLE_CHECK) { + if (mDisplayMode == AvdDisplayMode.SIMPLE_CHECK) { if (item.getChecked()) { Table parentTable = item.getParent(); for (TableItem i2 : parentTable.getItems()) { @@ -757,9 +717,10 @@ private void enforceSingleSelection(TableItem item) { * The table columns are: *

    *
  • column 0: sdk name - *
  • column 1: sdk vendor - *
  • column 2: sdk api name - *
  • column 3: sdk version + *
  • column 1: sdk target + *
  • column 2: sdk platform + *
  • column 3: sdk API level + *
  • column 4: CPU/ABI *
*/ private void fillTable(final Table table) { @@ -768,13 +729,12 @@ private void fillTable(final Table table) { // get the AVDs AvdInfo avds[] = null; if (mAvdManager != null) { - if (mDisplayMode == DisplayMode.MANAGER) { + if (mDisplayMode == AvdDisplayMode.MANAGER) { avds = mAvdManager.getAllAvds(); } else { avds = mAvdManager.getValidAvds(); } } - if (avds != null && avds.length > 0) { Arrays.sort(avds, new Comparator() { @Override @@ -782,19 +742,17 @@ public int compare(AvdInfo o1, AvdInfo o2) { return o1.compareTo(o2); } }); - table.setEnabled(true); - if (mTargetFilter != null) { mTargetFilter.prepare(); } - for (AvdInfo avd : avds) { - if (mTargetFilter == null || mTargetFilter.accept(avd)) { + AvdAgent avdAgent = new AvdAgent(mSdkTargets.getTargetForSysImage((SystemImage)avd.getSystemImage()), avd); + if (mTargetFilter == null || mTargetFilter.accept(avdAgent)) { TableItem item = new TableItem(table, SWT.NONE); - item.setData(avd); + item.setData(avdAgent); item.setText(0, avd.getName()); - if (mDisplayMode == DisplayMode.MANAGER) { + if (mDisplayMode == AvdDisplayMode.MANAGER) { AvdStatus status = avd.getStatus(); boolean isOk = status == AvdStatus.OK; @@ -804,26 +762,16 @@ public int compare(AvdInfo o1, AvdInfo o2) { Image img = getTagImage(avd.getTag(), isOk, isRepair, isInvalid); item.setImage(0, img); } - IAndroidTarget target = avd.getTarget(); - if (target != null) { - item.setText(1, target.getFullName()); - item.setText(2, target.getVersionName()); - item.setText(3, target.getVersion().getApiString()); - item.setText(4, AvdInfo.getPrettyAbiType(avd)); - } else { - item.setText(1, "?"); - item.setText(2, "?"); - item.setText(3, "?"); - item.setText(4, "?"); - } + item.setText(1, avdAgent.getTargetFullName()); + item.setText(2, avdAgent.getTargetVersionName()); + item.setText(3, avd.getAndroidVersion().getApiString()); + item.setText(4, AvdInfo.getPrettyAbiType(avd)); } } - if (mTargetFilter != null) { mTargetFilter.cleanup(); } } - if (table.getItemCount() == 0) { table.setEnabled(false); TableItem item = new TableItem(table, SWT.NONE); @@ -849,20 +797,18 @@ private Image getTagImage(IdDisplay tag, (isRepair ? 1 : 0), (isInvalid ? 1 : 0), fname); - return mImageFactory.getImageByName(fname, kname, new Filter() { + if (isOk) + return mImageFactory.getImageByName(fname); + + return mImageFactory.getImageByName(fname, kname, new ImageFactory.ImageEditor() { @Override - public Image filter(Image source) { - // We don't need an overlay for good AVDs. - if (isOk) { - return source; - } - + public ImageData edit(Image source) { Image overlayImg = isRepair ? mBrokenImage : mInvalidImage; ImageDescriptor overlayDesc = ImageDescriptor.createFromImage(overlayImg); DecorationOverlayIcon overlaid = new DecorationOverlayIcon(source, overlayDesc, IDecoration.BOTTOM_RIGHT); - return overlaid.createImage(); + return overlaid.getImageData(); } }); } @@ -873,19 +819,17 @@ public Image filter(Image source) { * Unlike {@link #getSelected()} this will always return the item being selected * in the list, ignoring the check boxes state in {@link DisplayMode#SIMPLE_CHECK} mode. */ - private AvdInfo getTableSelection() { + private AvdAgent getTableSelection() { int selIndex = mTable.getSelectionIndex(); if (selIndex >= 0) { - return (AvdInfo) mTable.getItem(selIndex).getData(); + return (AvdAgent) mTable.getItem(selIndex).getData(); } - return null; } /** * Updates the enable state of the Details, Start, Delete and Update buttons. */ - @SuppressWarnings("null") private void enableActionButtons() { if (mIsEnabled == false) { mDetailsButton.setEnabled(false); @@ -901,13 +845,12 @@ private void enableActionButtons() { mRepairButton.setEnabled(false); } } else { - AvdInfo selection = getTableSelection(); + AvdAgent selection = getTableSelection(); boolean hasSelection = selection != null; mDetailsButton.setEnabled(hasSelection); - mStartButton.setEnabled(mOsSdkPath != null && - hasSelection && - selection.getStatus() == AvdStatus.OK); + mStartButton.setEnabled(hasSelection && + selection.getAvd().getStatus() == AvdStatus.OK); if (mEditButton != null) { mEditButton.setEnabled(hasSelection); @@ -916,62 +859,59 @@ private void enableActionButtons() { mDeleteButton.setEnabled(hasSelection); } if (mRepairButton != null) { - mRepairButton.setEnabled(hasSelection && isAvdRepairable(selection.getStatus())); + mRepairButton.setEnabled(hasSelection && isAvdRepairable(selection.getAvd().getStatus())); } } } private void onNew() { AvdCreationDialog dlg = new AvdCreationDialog(mTable.getShell(), - mAvdManager, - mImageFactory, - mSdkLog, + mSdkContext, + mSdkTargets, null); if (dlg.open() == Window.OK) { - refresh(false /*reload*/); + refresh(false); //reload } } - @SuppressWarnings("deprecation") private void onEdit() { - AvdInfo avdInfo = getTableSelection(); + AvdAgent avdAgent = getTableSelection(); GridDialog dlg; - if (!avdInfo.getDeviceName().isEmpty()) { + if (!avdAgent.getAvd().getDeviceName().isEmpty()) { dlg = new AvdCreationDialog(mTable.getShell(), - mAvdManager, - mImageFactory, - mSdkLog, - avdInfo); - } else { - dlg = new LegacyAvdEditDialog(mTable.getShell(), - mAvdManager, - mImageFactory, - mSdkLog, - avdInfo); - } - - - if (dlg.open() == Window.OK) { - refresh(false /*reload*/); + mSdkContext, + mSdkTargets, + avdAgent); + if (dlg.open() == Window.OK) { + refresh(false); //reload + } else { + // create a dialog with ok button and a warning icon + MessageBox dialog = + new MessageBox(mTable.getShell(), SWT.ICON_WARNING| SWT.OK); + dialog.setText("Legacy device not supported"); + dialog.setMessage(avdAgent.getAvd().getName() + " has is assigned a legacy device no longer supported by the Android SDK"); + // open dialog and await user selection + dialog.open(); + } } } private void onDetails() { - AvdInfo avdInfo = getTableSelection(); + AvdAgent avdAgent = getTableSelection(); - AvdDetailsDialog dlg = new AvdDetailsDialog(mTable.getShell(), avdInfo); + AvdDetailsDialog dlg = new AvdDetailsDialog(mTable.getShell(), avdAgent); dlg.open(); } private void onDelete() { - final AvdInfo avdInfo = getTableSelection(); + final AvdAgent avdAgent = getTableSelection(); // get the current Display final Display display = mTable.getDisplay(); // check if the AVD is running - if (mAvdManager.isAvdRunning(avdInfo)) { + if (mAvdManager.isAvdRunning(avdAgent.getAvd(), mSdkContext.getSdkLog())) { display.asyncExec(new Runnable() { @Override public void run() { @@ -980,7 +920,7 @@ public void run() { "Delete Android Virtual Device", String.format( "The Android Virtual Device '%1$s' is currently running in an emulator and cannot be deleted.", - avdInfo.getName())); + avdAgent.getAvd().getName())); } }); return; @@ -996,7 +936,7 @@ public void run() { "Delete Android Virtual Device", String.format( "Please confirm that you want to delete the Android Virtual Device named '%s'. This operation cannot be reverted.", - avdInfo.getName())); + avdAgent.getAvd().getName())); } }); @@ -1004,19 +944,19 @@ public void run() { return; } - // log for this action. - ILogger log = mSdkLog; - if (log == null || log instanceof MessageBoxLog) { + ILogger log = null; + // log for this action. + if (log == null || log instanceof MessageBoxLog) { // If the current logger is a message box, we use our own (to make sure // to display errors right away and customize the title). log = new MessageBoxLog( - String.format("Result of deleting AVD '%s':", avdInfo.getName()), + String.format("Result of deleting AVD '%s':", avdAgent.getAvd().getName()), display, false /*logErrorsOnly*/); } // delete the AVD - boolean success = mAvdManager.deleteAvd(avdInfo, log); + boolean success = mAvdManager.deleteAvd(avdAgent.getAvd(), log); // display the result if (log instanceof MessageBoxLog) { @@ -1034,38 +974,37 @@ public void run() { * For now this only supports fixing the wrong value in image.sysdir.* */ private void onRepair() { - final AvdInfo avdInfo = getTableSelection(); + final AvdAgent avdAgent = getTableSelection(); // get the current Display final Display display = mTable.getDisplay(); - // log for this action. - ILogger log = mSdkLog; + ILogger log = null; if (log == null || log instanceof MessageBoxLog) { // If the current logger is a message box, we use our own (to make sure // to display errors right away and customize the title). log = new MessageBoxLog( - String.format("Result of updating AVD '%s':", avdInfo.getName()), + String.format("Result of updating AVD '%s':", avdAgent.getAvd().getName()), display, false /*logErrorsOnly*/); } - if (avdInfo.getStatus() == AvdStatus.ERROR_IMAGE_DIR) { + if (avdAgent.getAvd().getStatus() == AvdStatus.ERROR_IMAGE_DIR) { // delete the AVD try { - mAvdManager.updateAvd(avdInfo, log); + mAvdManager.updateAvd(avdAgent.getAvd(), avdAgent.getAvd().getProperties()); } catch (IOException e) { - log.error(e, null); + log.error(e, null); } refresh(false /*reload*/); - } else if (avdInfo.getStatus() == AvdStatus.ERROR_DEVICE_CHANGED) { + } else if (avdAgent.getAvd().getStatus() == AvdStatus.ERROR_DEVICE_CHANGED) { try { - mAvdManager.updateDeviceChanged(avdInfo, log); + mAvdManager.updateDeviceChanged(avdAgent.getAvd(), log); } catch (IOException e) { log.error(e, null); } refresh(false /*reload*/); - } else if (avdInfo.getStatus() == AvdStatus.ERROR_DEVICE_MISSING) { + } else if (avdAgent.getAvd().getStatus() == AvdStatus.ERROR_DEVICE_MISSING) { onEdit(); } } @@ -1076,7 +1015,7 @@ private void onAvdManager() { Display display = mTable.getDisplay(); // log for this action. - ILogger log = mSdkLog; + ILogger log = null; if (log == null || log instanceof MessageBoxLog) { // If the current logger is a message box, we use our own (to make sure // to display errors right away and customize the title). @@ -1087,7 +1026,7 @@ private void onAvdManager() { AvdManagerWindowImpl1 win = new AvdManagerWindowImpl1( mTable.getShell(), log, - mOsSdkPath, + mSdkContext, AvdInvocationContext.DIALOG); win.open(); @@ -1101,24 +1040,23 @@ private void onAvdManager() { } private void onStart() { - AvdInfo avdInfo = getTableSelection(); + AvdAgent avdAgent = getTableSelection(); - if (avdInfo == null || mOsSdkPath == null) { + if (avdAgent == null) { return; } - + File osSdkPath = mSdkContext.getLocation(); AvdStartDialog dialog = new AvdStartDialog( mTable.getShell(), - avdInfo, - new File(mOsSdkPath), - mController, - mSdkLog); + avdAgent, + osSdkPath, + mSdkContext.getSdkLog()); if (dialog.open() == Window.OK) { - String path = mOsSdkPath + File.separator + String path = osSdkPath + File.separator + SdkConstants.OS_SDK_TOOLS_FOLDER + SdkConstants.FN_EMULATOR; - final String avdName = avdInfo.getName(); + final String avdName = avdAgent.getAvd().getName(); // build the command line based on the available parameters. ArrayList list = new ArrayList(); @@ -1260,4 +1198,5 @@ private boolean isAvdRepairable(AvdStatus avdStatus) { || avdStatus == AvdStatus.ERROR_DEVICE_CHANGED || avdStatus == AvdStatus.ERROR_DEVICE_MISSING; } + } diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdStartDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdStartDialog.java index 00eb1aa6..a8dfe645 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdStartDialog.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/AvdStartDialog.java @@ -20,10 +20,10 @@ import com.android.sdklib.devices.DeviceManager; import com.android.sdklib.internal.avd.AvdInfo; import com.android.sdklib.internal.avd.AvdManager; -import com.android.sdklib.internal.repository.updater.SettingsController; +import com.android.sdkuilib.internal.repository.avd.AvdAgent; import com.android.sdkuilib.ui.GridDialog; +import com.android.sdkuilib.ui.ResolutionChooserDialog; import com.android.utils.ILogger; -import com.android.utils.SdkUtils; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.jface.window.Window; @@ -78,7 +78,6 @@ final class AvdStartDialog extends GridDialog { private final AvdInfo mAvd; private final File mSdkLocation; - private final SettingsController mSettingsController; private final DeviceManager mDeviceManager; private Text mScreenSize; @@ -98,12 +97,10 @@ final class AvdStartDialog extends GridDialog { private boolean mSnapshotLaunch = true; private Button mSnapshotLaunchCheckbox; - AvdStartDialog(Shell parentShell, AvdInfo avd, File sdkLocation, - SettingsController settingsController, ILogger sdkLog) { + AvdStartDialog(Shell parentShell, AvdAgent avdAgent, File sdkLocation, ILogger sdkLog) { super(parentShell, 2, false); - mAvd = avd; + mAvd = avdAgent.getAvd(); mSdkLocation = sdkLocation; - mSettingsController = settingsController; mDeviceManager = DeviceManager.createInstance(mSdkLocation, sdkLog); if (mAvd == null) { throw new IllegalArgumentException("avd cannot be null"); @@ -330,12 +327,6 @@ protected void okPressed() { String dpi = mMonitorDpi.getText(); if (dpi.length() > 0) { sMonitorDpi = Integer.parseInt(dpi); - - // if there is a setting controller, save it - if (mSettingsController != null) { - mSettingsController.setMonitorDensity(sMonitorDpi); - mSettingsController.saveSettings(); - } } // now the scale factor @@ -434,9 +425,6 @@ private void setScale(float scale) { * can tell us. */ private int getMonitorDpi() { - if (mSettingsController != null) { - sMonitorDpi = mSettingsController.getSettings().getMonitorDensity(); - } if (sMonitorDpi == -1) { // first time? try to get a value sMonitorDpi = Toolkit.getDefaultToolkit().getScreenResolution(); diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/DeviceCreationDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/DeviceCreationDialog.java index 263ca210..bf482e25 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/DeviceCreationDialog.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/DeviceCreationDialog.java @@ -44,7 +44,7 @@ import com.android.sdklib.devices.Software; import com.android.sdklib.devices.State; import com.android.sdklib.devices.Storage; -import com.android.sdkuilib.internal.repository.icons.ImageFactory; +import org.eclipse.andmore.base.resources.ImageFactory; import com.android.sdkuilib.ui.GridDataBuilder; import com.android.sdkuilib.ui.GridDialog; import com.android.sdkuilib.ui.GridLayoutBuilder; @@ -68,7 +68,6 @@ import org.eclipse.swt.widgets.Text; import java.util.Collection; -import java.util.List; import java.util.Map; public class DeviceCreationDialog extends GridDialog { diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/HardwarePropertyChooser.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/HardwarePropertyChooser.java deleted file mode 100644 index b471cc79..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/HardwarePropertyChooser.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * 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.sdkuilib.internal.widgets; - -import com.android.sdklib.internal.avd.HardwareProperties.HardwareProperty; -import com.android.sdklib.internal.avd.HardwareProperties.HardwarePropertyType; -import com.android.sdkuilib.ui.GridDialog; - -import org.eclipse.swt.SWT; -import org.eclipse.swt.events.SelectionAdapter; -import org.eclipse.swt.events.SelectionEvent; -import org.eclipse.swt.widgets.Combo; -import org.eclipse.swt.widgets.Composite; -import org.eclipse.swt.widgets.Label; -import org.eclipse.swt.widgets.Shell; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.TreeSet; - -/** - * Dialog to choose a hardware property - */ -class HardwarePropertyChooser extends GridDialog { - - private final Map mProperties; - private final Collection mExceptProperties; - private HardwareProperty mChosenProperty; - private Label mTypeLabel; - private Label mDescriptionLabel; - - HardwarePropertyChooser(Shell parentShell, - Map properties, - Collection exceptProperties) { - super(parentShell, 2, false); - mProperties = properties; - mExceptProperties = exceptProperties; - } - - public HardwareProperty getProperty() { - return mChosenProperty; - } - - @Override - public void createDialogContent(Composite parent) { - Label l = new Label(parent, SWT.NONE); - l.setText("Property:"); - - final Combo c = new Combo(parent, SWT.DROP_DOWN | SWT.READ_ONLY); - // simple list for index->name resolution. - final ArrayList indexToName = new ArrayList(); - - // Sort the combo entries by display name if available, otherwise by hardware name. - Set> entries = - new TreeSet>( - new Comparator>() { - @Override - public int compare(Entry entry0, - Entry entry1) { - String s0 = entry0.getValue().getAbstract(); - String s1 = entry1.getValue().getAbstract(); - if (s0 != null && s1 != null) { - return s0.compareTo(s1); - } - return entry0.getKey().compareTo(entry1.getKey()); - } - }); - entries.addAll(mProperties.entrySet()); - - for (Entry entry : entries) { - if (entry.getValue().isValidForUi() && - mExceptProperties.contains(entry.getKey()) == false) { - c.add(entry.getValue().getAbstract()); - indexToName.add(entry.getKey()); - } - } - boolean hasValues = true; - if (indexToName.size() == 0) { - hasValues = false; - c.add("No properties"); - c.select(0); - c.setEnabled(false); - } - - c.addSelectionListener(new SelectionAdapter() { - @Override - public void widgetSelected(SelectionEvent event) { - int index = c.getSelectionIndex(); - String name = indexToName.get(index); - processSelection(name, true /* pack */); - } - }); - - l = new Label(parent, SWT.NONE); - l.setText("Type:"); - - mTypeLabel = new Label(parent, SWT.NONE); - - l = new Label(parent, SWT.NONE); - l.setText("Description:"); - - mDescriptionLabel = new Label(parent, SWT.NONE); - - if (hasValues) { - c.select(0); - processSelection(indexToName.get(0), false /* pack */); - } - } - - private void processSelection(String name, boolean pack) { - mChosenProperty = name == null ? null : mProperties.get(name); - - String type = "Unknown"; - String desc = "Unknown"; - - if (mChosenProperty != null) { - desc = mChosenProperty.getDescription(); - HardwarePropertyType vt = mChosenProperty.getType(); - if (vt != null) { - type = vt.getName(); - } - } - - mTypeLabel.setText(type); - mDescriptionLabel.setText(desc); - - if (pack) { - getShell().pack(); - } - } - -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/LegacyAvdEditDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/LegacyAvdEditDialog.java deleted file mode 100644 index ee892637..00000000 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/LegacyAvdEditDialog.java +++ /dev/null @@ -1,1439 +0,0 @@ -/* - * 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.sdkuilib.internal.widgets; - -import com.android.SdkConstants; -import com.android.io.FileWrapper; -import com.android.prefs.AndroidLocation.AndroidLocationException; -import com.android.sdklib.IAndroidTarget; -import com.android.sdklib.ISystemImage; -import com.android.sdklib.SystemImage; -import com.android.sdklib.internal.avd.AvdInfo; -import com.android.sdklib.internal.avd.AvdManager; -import com.android.sdklib.internal.avd.AvdManager.AvdConflict; -import com.android.sdklib.internal.avd.HardwareProperties; -import com.android.sdklib.internal.avd.HardwareProperties.HardwareProperty; -import com.android.sdklib.internal.project.ProjectProperties; -import com.android.sdklib.repository.descriptors.IdDisplay; -import com.android.sdklib.repository.local.LocalSdk; -import com.android.sdkuilib.internal.repository.icons.ImageFactory; -import com.android.sdkuilib.ui.GridDialog; -import com.android.utils.ILogger; -import com.android.utils.Pair; - -import org.eclipse.jface.dialogs.IDialogConstants; -import org.eclipse.jface.viewers.CellEditor; -import org.eclipse.jface.viewers.CellLabelProvider; -import org.eclipse.jface.viewers.ComboBoxCellEditor; -import org.eclipse.jface.viewers.EditingSupport; -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.SelectionChangedEvent; -import org.eclipse.jface.viewers.TableViewer; -import org.eclipse.jface.viewers.TableViewerColumn; -import org.eclipse.jface.viewers.TextCellEditor; -import org.eclipse.jface.viewers.Viewer; -import org.eclipse.jface.viewers.ViewerCell; -import org.eclipse.jface.window.Window; -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.events.VerifyEvent; -import org.eclipse.swt.events.VerifyListener; -import org.eclipse.swt.graphics.Point; -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.FileDialog; -import org.eclipse.swt.widgets.Group; -import org.eclipse.swt.widgets.Label; -import org.eclipse.swt.widgets.Shell; -import org.eclipse.swt.widgets.Table; -import org.eclipse.swt.widgets.TableColumn; -import org.eclipse.swt.widgets.Text; - -import java.io.File; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; -import java.util.TreeMap; -import java.util.regex.Matcher; - -/** - * AVD creation or edit dialog. - * - * @deprecated Replaced by {@link AvdCreationDialog} - */ -final class LegacyAvdEditDialog extends GridDialog { - - private static final String ABI_SYS_IMG_DATA_KEY = "systemImagesData"; //$NON-NLS-1$ - - private final AvdManager mAvdManager; - private final TreeMap mCurrentTargets = - new TreeMap(); - - private final Map mHardwareMap; - private final Map mProperties = new HashMap(); - // a list of user-edited properties. - private final ArrayList mEditedProperties = new ArrayList(); - private final ImageFactory mImageFactory; - private final ILogger mSdkLog; - /** - * The original AvdInfo if we're editing an existing AVD. - * Null when we're creating a new AVD. - */ - private final AvdInfo mEditAvdInfo; - - private Text mAvdName; - private Combo mTargetCombo; - - private Combo mTagAbiCombo; - private String mAbiType; - - private Button mSdCardSizeRadio; - private Text mSdCardSize; - private Combo mSdCardSizeCombo; - - private Text mSdCardFile; - private Button mBrowseSdCard; - private Button mSdCardFileRadio; - - private Button mSnapshotCheck; - - private Button mSkinListRadio; - private Combo mSkinCombo; - - private Button mSkinSizeRadio; - private Text mSkinSizeWidth; - private Text mSkinSizeHeight; - - private TableViewer mHardwareViewer; - private Button mDeleteHardwareProp; - - private Button mForceCreation; - private Button mOkButton; - private Label mStatusIcon; - private Label mStatusLabel; - private Composite mStatusComposite; - - /** - * {@link VerifyListener} for {@link Text} widgets that should only contains numbers. - */ - private final VerifyListener mDigitVerifier = new VerifyListener() { - @Override - public void verifyText(VerifyEvent event) { - int count = event.text.length(); - for (int i = 0 ; i < count ; i++) { - char c = event.text.charAt(i); - if (c < '0' || c > '9') { - event.doit = false; - return; - } - } - } - }; - - /** - * Callback when the AVD name is changed. - * When creating a new AVD, enables the force checkbox if the name is a duplicate. - * When editing an existing AVD, it's OK for the name to match the existing AVD. - */ - private class CreateNameModifyListener implements ModifyListener { - @Override - public void modifyText(ModifyEvent e) { - String name = mAvdName.getText().trim(); - if (mEditAvdInfo == null || !name.equals(mEditAvdInfo.getName())) { - // Case where we're creating a new AVD or editing an existing one - // and the AVD name has been changed... check for name uniqueness. - - Pair conflict = mAvdManager.isAvdNameConflicting(name); - if (conflict.getFirst() != AvdManager.AvdConflict.NO_CONFLICT) { - // If we're changing the state from disabled to enabled, make sure - // to uncheck the button, to force the user to voluntarily re-enforce it. - // This happens when editing an existing AVD and changing the name from - // the existing AVD to another different existing AVD. - if (!mForceCreation.isEnabled()) { - mForceCreation.setEnabled(true); - mForceCreation.setSelection(false); - } - } else { - mForceCreation.setEnabled(false); - mForceCreation.setSelection(false); - } - } else { - // Case where we're editing an existing AVD with the name unchanged. - - mForceCreation.setEnabled(false); - mForceCreation.setSelection(false); - } - validatePage(); - } - } - - /** - * {@link ModifyListener} used for live-validation of the fields content. - */ - private class ValidateListener extends SelectionAdapter implements ModifyListener { - @Override - public void modifyText(ModifyEvent e) { - validatePage(); - } - - @Override - public void widgetSelected(SelectionEvent e) { - super.widgetSelected(e); - validatePage(); - } - } - - /** - * Creates the dialog. Caller should then use {@link Window#open()} and - * refresh if the status is {@link Window#OK}. - * - * @param parentShell The parent shell. - * @param avdManager The existing {@link AvdManager} to use. Must not be null. - * @param imageFactory An existing {@link ImageFactory} to use. Must not be null. - * @param log An existing {@link ILogger} where output will go. Must not be null. - * @param editAvdInfo An optional {@link AvdInfo}. When null, the dialog is used - * to create a new AVD. When non-null, the dialog is used to edit this AVD. - */ - protected LegacyAvdEditDialog(Shell parentShell, - AvdManager avdManager, - ImageFactory imageFactory, - ILogger log, - AvdInfo editAvdInfo) { - super(parentShell, 2, false); - mAvdManager = avdManager; - mImageFactory = imageFactory; - mSdkLog = log; - mEditAvdInfo = editAvdInfo; - - File hardwareDefs = null; - - LocalSdk localSdk = avdManager.getLocalSdk(); - if (localSdk != null) { - File sdkPath = localSdk.getLocation(); - if (sdkPath != null) { - hardwareDefs = new File(new File(sdkPath, SdkConstants.OS_SDK_TOOLS_LIB_FOLDER), - SdkConstants.FN_HARDWARE_INI); - } - } - - if (hardwareDefs == null) { - log.error(null, "Failed to load file %s from SDK", SdkConstants.FN_HARDWARE_INI); - mHardwareMap = new HashMap(); - } else { - mHardwareMap = HardwareProperties.parseHardwareDefinitions( - hardwareDefs, null /*sdkLog*/); - } - } - - @Override - public void create() { - super.create(); - - Point p = getShell().getSize(); - if (p.x < 400) { - p.x = 400; - } - getShell().setSize(p); - } - - @Override - protected Control createContents(Composite parent) { - Control control = super.createContents(parent); - getShell().setText(mEditAvdInfo == null ? "Create new Android Virtual Device (AVD)" - : "Edit Android Virtual Device (AVD)"); - - mOkButton = getButton(IDialogConstants.OK_ID); - - fillExistingAvdInfo(); - validatePage(); - - return control; - } - - @Override - public void createDialogContent(final Composite parent) { - GridData gd; - GridLayout gl; - - Label label = new Label(parent, SWT.NONE); - label.setText("Name:"); - String tooltip = "Name of the new Android Virtual Device"; - label.setToolTipText(tooltip); - - mAvdName = new Text(parent, SWT.BORDER); - mAvdName.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - mAvdName.addModifyListener(new CreateNameModifyListener()); - mAvdName.setToolTipText(tooltip); - - label = new Label(parent, SWT.NONE); - label.setText("Target:"); - tooltip = "The version of Android to use in the virtual device"; - label.setToolTipText(tooltip); - - mTargetCombo = new Combo(parent, SWT.READ_ONLY | SWT.DROP_DOWN); - mTargetCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - mTargetCombo.setToolTipText(tooltip); - mTargetCombo.addSelectionListener(new SelectionAdapter() { - @Override - public void widgetSelected(SelectionEvent e) { - super.widgetSelected(e); - reloadSkinCombo(); - reloadTagAbiCombo(); - validatePage(); - } - }); - - //ABI group - label = new Label(parent, SWT.NONE); - label.setText("CPU/ABI:"); - tooltip = "The CPU/ABI to use in the virtual device"; - label.setToolTipText(tooltip); - - mTagAbiCombo = new Combo(parent, SWT.READ_ONLY | SWT.DROP_DOWN); - mTagAbiCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - mTagAbiCombo.setToolTipText(tooltip); - mTagAbiCombo.addSelectionListener(new SelectionAdapter() { - @Override - public void widgetSelected(SelectionEvent e) { - super.widgetSelected(e); - validatePage(); - } - }); - mTagAbiCombo.setEnabled(false); - - // --- sd card group - label = new Label(parent, SWT.NONE); - label.setText("SD Card:"); - label.setLayoutData(new GridData(GridData.BEGINNING, GridData.BEGINNING, - false, false)); - - final Group sdCardGroup = new Group(parent, SWT.NONE); - sdCardGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - sdCardGroup.setLayout(new GridLayout(3, false)); - - mSdCardSizeRadio = new Button(sdCardGroup, SWT.RADIO); - mSdCardSizeRadio.setText("Size:"); - mSdCardSizeRadio.setToolTipText("Create a new SD Card file"); - mSdCardSizeRadio.addSelectionListener(new SelectionAdapter() { - @Override - public void widgetSelected(SelectionEvent arg0) { - boolean sizeMode = mSdCardSizeRadio.getSelection(); - enableSdCardWidgets(sizeMode); - validatePage(); - } - }); - - ValidateListener validateListener = new ValidateListener(); - - mSdCardSize = new Text(sdCardGroup, SWT.BORDER); - mSdCardSize.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - mSdCardSize.addVerifyListener(mDigitVerifier); - mSdCardSize.addModifyListener(validateListener); - mSdCardSize.setToolTipText("Size of the new SD Card file (must be at least 9 MiB)"); - - mSdCardSizeCombo = new Combo(sdCardGroup, SWT.DROP_DOWN | SWT.READ_ONLY); - mSdCardSizeCombo.add("KiB"); - mSdCardSizeCombo.add("MiB"); - mSdCardSizeCombo.add("GiB"); - mSdCardSizeCombo.select(1); - mSdCardSizeCombo.addSelectionListener(validateListener); - - mSdCardFileRadio = new Button(sdCardGroup, SWT.RADIO); - mSdCardFileRadio.setText("File:"); - mSdCardFileRadio.setToolTipText("Use an existing file for the SD Card"); - - mSdCardFile = new Text(sdCardGroup, SWT.BORDER); - mSdCardFile.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - mSdCardFile.addModifyListener(validateListener); - mSdCardFile.setToolTipText("File to use for the SD Card"); - - mBrowseSdCard = new Button(sdCardGroup, SWT.PUSH); - mBrowseSdCard.setText("Browse..."); - mBrowseSdCard.setToolTipText("Select the file to use for the SD Card"); - mBrowseSdCard.addSelectionListener(new SelectionAdapter() { - @Override - public void widgetSelected(SelectionEvent arg0) { - onBrowseSdCard(); - validatePage(); - } - }); - - mSdCardSizeRadio.setSelection(true); - enableSdCardWidgets(true); - - // --- snapshot group - - label = new Label(parent, SWT.NONE); - label.setText("Snapshot:"); - label.setLayoutData(new GridData(GridData.BEGINNING, GridData.BEGINNING, - false, false)); - - final Group snapshotGroup = new Group(parent, SWT.NONE); - snapshotGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - snapshotGroup.setLayout(new GridLayout(3, false)); - - mSnapshotCheck = new Button(snapshotGroup, SWT.CHECK); - mSnapshotCheck.setText("Enabled"); - mSnapshotCheck.setToolTipText( - "Emulator's state will be persisted between emulator executions"); - - // --- skin group - label = new Label(parent, SWT.NONE); - label.setText("Skin:"); - label.setLayoutData(new GridData(GridData.BEGINNING, GridData.BEGINNING, - false, false)); - - final Group skinGroup = new Group(parent, SWT.NONE); - skinGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - skinGroup.setLayout(new GridLayout(4, false)); - - mSkinListRadio = new Button(skinGroup, SWT.RADIO); - mSkinListRadio.setText("Built-in:"); - mSkinListRadio.setToolTipText("Select an emulated screen size provided by the current Android target"); - mSkinListRadio.addSelectionListener(new SelectionAdapter() { - @Override - public void widgetSelected(SelectionEvent arg0) { - boolean listMode = mSkinListRadio.getSelection(); - enableSkinWidgets(listMode); - validatePage(); - } - }); - - mSkinCombo = new Combo(skinGroup, SWT.READ_ONLY | SWT.DROP_DOWN); - mSkinCombo.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL)); - gd.horizontalSpan = 3; - mSkinCombo.addSelectionListener(new SelectionAdapter() { - @Override - public void widgetSelected(SelectionEvent arg0) { - // get the skin info - loadSkin(); - } - }); - - mSkinSizeRadio = new Button(skinGroup, SWT.RADIO); - mSkinSizeRadio.setText("Resolution:"); - mSkinSizeRadio.setToolTipText("Select a custom emulated screen size"); - - mSkinSizeWidth = new Text(skinGroup, SWT.BORDER); - mSkinSizeWidth.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - mSkinSizeWidth.addVerifyListener(mDigitVerifier); - mSkinSizeWidth.addModifyListener(validateListener); - mSkinSizeWidth.setToolTipText("Width in pixels of the emulated screen size"); - - new Label(skinGroup, SWT.NONE).setText("x"); - - mSkinSizeHeight = new Text(skinGroup, SWT.BORDER); - mSkinSizeHeight.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - mSkinSizeHeight.addVerifyListener(mDigitVerifier); - mSkinSizeHeight.addModifyListener(validateListener); - mSkinSizeHeight.setToolTipText("Height in pixels of the emulated screen size"); - - mSkinListRadio.setSelection(true); - enableSkinWidgets(true); - - // --- hardware group - label = new Label(parent, SWT.NONE); - label.setText("Hardware:"); - label.setLayoutData(new GridData(GridData.BEGINNING, GridData.BEGINNING, - false, false)); - - final Group hwGroup = new Group(parent, SWT.NONE); - hwGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - hwGroup.setLayout(new GridLayout(2, false)); - - createHardwareTable(hwGroup); - - // composite for the side buttons - Composite hwButtons = new Composite(hwGroup, SWT.NONE); - hwButtons.setLayoutData(new GridData(GridData.FILL_VERTICAL)); - hwButtons.setLayout(gl = new GridLayout(1, false)); - gl.marginHeight = gl.marginWidth = 0; - - Button b = new Button(hwButtons, SWT.PUSH | SWT.FLAT); - b.setText("New..."); - b.setToolTipText("Add a new hardware property"); - b.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - b.addSelectionListener(new SelectionAdapter() { - @Override - public void widgetSelected(SelectionEvent event) { - HardwarePropertyChooser dialog = new HardwarePropertyChooser(parent.getShell(), - mHardwareMap, mProperties.keySet()); - if (dialog.open() == Window.OK) { - HardwareProperty choice = dialog.getProperty(); - if (choice != null) { - mProperties.put(choice.getName(), choice.getDefault()); - mHardwareViewer.refresh(); - } - } - } - }); - mDeleteHardwareProp = new Button(hwButtons, SWT.PUSH | SWT.FLAT); - mDeleteHardwareProp.setText("Delete"); - mDeleteHardwareProp.setToolTipText("Delete the selected hardware property"); - mDeleteHardwareProp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - mDeleteHardwareProp.addSelectionListener(new SelectionAdapter() { - @Override - public void widgetSelected(SelectionEvent arg0) { - ISelection selection = mHardwareViewer.getSelection(); - if (selection instanceof IStructuredSelection) { - String hwName = (String)((IStructuredSelection)selection).getFirstElement(); - mProperties.remove(hwName); - mHardwareViewer.refresh(); - } - } - }); - mDeleteHardwareProp.setEnabled(false); - - // --- end hardware group - - mForceCreation = new Button(parent, SWT.CHECK); - mForceCreation.setText("Override the existing AVD with the same name"); - mForceCreation.setToolTipText("There's already an AVD with the same name. Check this to delete it and replace it by the new AVD."); - mForceCreation.setLayoutData(new GridData(GridData.BEGINNING, GridData.CENTER, - true, false, 2, 1)); - mForceCreation.setEnabled(false); - mForceCreation.addSelectionListener(validateListener); - - // add a separator to separate from the ok/cancel button - label = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL); - label.setLayoutData(new GridData(GridData.FILL, GridData.CENTER, true, false, 3, 1)); - - // add stuff for the error display - mStatusComposite = new Composite(parent, SWT.NONE); - mStatusComposite.setLayoutData(new GridData(GridData.FILL, GridData.CENTER, - true, false, 3, 1)); - mStatusComposite.setLayout(gl = new GridLayout(2, false)); - gl.marginHeight = gl.marginWidth = 0; - - mStatusIcon = new Label(mStatusComposite, SWT.NONE); - mStatusIcon.setLayoutData(new GridData(GridData.BEGINNING, GridData.BEGINNING, - false, false)); - mStatusLabel = new Label(mStatusComposite, SWT.NONE); - mStatusLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - mStatusLabel.setText(" \n "); //$NON-NLS-1$ - - reloadTargetCombo(); - } - - /** - * Creates the UI for the hardware properties table. - * This creates the {@link Table}, and several viewers ({@link TableViewer}, - * {@link TableViewerColumn}) and adds edit support for the 2nd column - */ - private void createHardwareTable(Composite parent) { - final Table hardwareTable = new Table(parent, SWT.SINGLE | SWT.FULL_SELECTION); - GridData gd = new GridData(GridData.FILL_HORIZONTAL | GridData.FILL_VERTICAL); - gd.widthHint = 200; - gd.heightHint = 100; - hardwareTable.setLayoutData(gd); - hardwareTable.setHeaderVisible(true); - hardwareTable.setLinesVisible(true); - - // -- Table viewer - mHardwareViewer = new TableViewer(hardwareTable); - mHardwareViewer.addSelectionChangedListener(new ISelectionChangedListener() { - @Override - public void selectionChanged(SelectionChangedEvent event) { - // it's a single selection mode, we can just access the selection index - // from the table directly. - mDeleteHardwareProp.setEnabled(hardwareTable.getSelectionIndex() != -1); - } - }); - - // only a content provider. Use viewers per column below (for editing support) - mHardwareViewer.setContentProvider(new IStructuredContentProvider() { - @Override - public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { - // we can just ignore this. we just use mProperties directly. - } - - @Override - public Object[] getElements(Object arg0) { - return mProperties.keySet().toArray(); - } - - @Override - public void dispose() { - // pass - } - }); - - // -- column 1: prop abstract name - TableColumn col1 = new TableColumn(hardwareTable, SWT.LEFT); - col1.setText("Property"); - col1.setWidth(150); - TableViewerColumn tvc1 = new TableViewerColumn(mHardwareViewer, col1); - tvc1.setLabelProvider(new CellLabelProvider() { - @Override - public void update(ViewerCell cell) { - String name = cell.getElement().toString(); - HardwareProperty prop = mHardwareMap.get(name); - if (prop != null) { - cell.setText(prop.getAbstract()); - } else { - cell.setText(String.format("%1$s (Unknown)", name)); - } - } - }); - - // -- column 2: prop value - TableColumn col2 = new TableColumn(hardwareTable, SWT.LEFT); - col2.setText("Value"); - col2.setWidth(50); - TableViewerColumn tvc2 = new TableViewerColumn(mHardwareViewer, col2); - tvc2.setLabelProvider(new CellLabelProvider() { - @Override - public void update(ViewerCell cell) { - String value = mProperties.get(cell.getElement()); - cell.setText(value != null ? value : ""); - } - }); - - // add editing support to the 2nd column - tvc2.setEditingSupport(new EditingSupport(mHardwareViewer) { - @Override - protected void setValue(Object element, Object value) { - String hardwareName = (String)element; - HardwareProperty property = mHardwareMap.get(hardwareName); - int index; - switch (property.getType()) { - case INTEGER: - mProperties.put((String)element, (String)value); - break; - case DISKSIZE: - if (HardwareProperties.DISKSIZE_PATTERN.matcher((String)value).matches()) { - mProperties.put((String)element, (String)value); - } - break; - case BOOLEAN: - index = (Integer)value; - mProperties.put((String)element, HardwareProperties.getBooleanValue(index)); - break; - case STRING_ENUM: - case INTEGER_ENUM: - // For a combo, value is the index of the enum to use. - index = (Integer)value; - String[] values = property.getEnum(); - if (values != null && values.length > index) { - mProperties.put((String)element, values[index]); - } - break; - } - mHardwareViewer.refresh(element); - } - - @Override - protected Object getValue(Object element) { - String hardwareName = (String)element; - HardwareProperty property = mHardwareMap.get(hardwareName); - String value = mProperties.get(hardwareName); - switch (property.getType()) { - case INTEGER: - // intended fall-through. - case DISKSIZE: - return value; - case BOOLEAN: - return HardwareProperties.getBooleanValueIndex(value); - case STRING_ENUM: - case INTEGER_ENUM: - // For a combo, we need to return the index of the value in the enum - String[] values = property.getEnum(); - if (values != null) { - for (int i = 0; i < values.length; i++) { - if (values[i].equals(value)) { - return i; - } - } - } - } - - return null; - } - - @Override - protected CellEditor getCellEditor(Object element) { - String hardwareName = (String)element; - HardwareProperty property = mHardwareMap.get(hardwareName); - switch (property.getType()) { - // TODO: custom TextCellEditor that restrict input. - case INTEGER: - // intended fall-through. - case DISKSIZE: - return new TextCellEditor(hardwareTable); - case BOOLEAN: - return new ComboBoxCellEditor(hardwareTable, - new String[] { - HardwareProperties.getBooleanValue(0), - HardwareProperties.getBooleanValue(1), - }, - SWT.READ_ONLY | SWT.DROP_DOWN); - case STRING_ENUM: - case INTEGER_ENUM: - String[] values = property.getEnum(); - if (values != null && values.length > 0) { - return new ComboBoxCellEditor(hardwareTable, - values, - SWT.READ_ONLY | SWT.DROP_DOWN); - } - } - return null; - } - - @Override - protected boolean canEdit(Object element) { - String hardwareName = (String)element; - HardwareProperty property = mHardwareMap.get(hardwareName); - return property != null; - } - }); - - - mHardwareViewer.setInput(mProperties); - } - - // -- Start of internal part ---------- - // Hide everything down-below from SWT designer - //$hide>>$ - - /** - * When editing an existing AVD info, fill the UI that has just been created with - * the values from the AVD. - */ - public void fillExistingAvdInfo() { - if (mEditAvdInfo == null) { - return; - } - - mAvdName.setText(mEditAvdInfo.getName()); - - Map props = mEditAvdInfo.getProperties(); - - IAndroidTarget target = mEditAvdInfo.getTarget(); - if (target != null && !mCurrentTargets.isEmpty()) { - // Try to select the target in the target combo. - // This will fail if the AVD needs to be repaired. - // - // This is a linear search but the list is always - // small enough and we only do this once. - int n = mTargetCombo.getItemCount(); - for (int i = 0;i < n; i++) { - if (target.equals(mCurrentTargets.get(mTargetCombo.getItem(i)))) { - mTargetCombo.select(i); - reloadTagAbiCombo(); - reloadSkinCombo(); - break; - } - } - } - - // select the abi type - ISystemImage[] systemImages = getSystemImages(target); - if (target != null && systemImages.length > 0) { - mTagAbiCombo.setEnabled(systemImages.length > 1); - String abiType = AvdInfo.getPrettyAbiType(mEditAvdInfo); - int n = mTagAbiCombo.getItemCount(); - for (int i = 0; i < n; i++) { - if (abiType.equals(mTagAbiCombo.getItem(i))) { - mTagAbiCombo.select(i); - reloadSkinCombo(); - break; - } - } - } - - if (props != null) { - - // First try the skin name and if it doesn't work fallback on the skin path - nextSkin: for (int s = 0; s < 2; s++) { - String skin = props.get(s == 0 ? AvdManager.AVD_INI_SKIN_NAME - : AvdManager.AVD_INI_SKIN_PATH); - if (skin != null && skin.length() > 0) { - Matcher m = AvdManager.NUMERIC_SKIN_SIZE.matcher(skin); - if (m.matches() && m.groupCount() == 2) { - enableSkinWidgets(false); - mSkinListRadio.setSelection(false); - mSkinSizeRadio.setSelection(true); - mSkinSizeWidth.setText(m.group(1)); - mSkinSizeHeight.setText(m.group(2)); - break nextSkin; - } else { - enableSkinWidgets(true); - mSkinSizeRadio.setSelection(false); - mSkinListRadio.setSelection(true); - - int n = mSkinCombo.getItemCount(); - for (int i = 0; i < n; i++) { - if (skin.equals(mSkinCombo.getItem(i))) { - mSkinCombo.select(i); - break nextSkin; - } - } - } - } - } - - String sdcard = props.get(AvdManager.AVD_INI_SDCARD_PATH); - if (sdcard != null && sdcard.length() > 0) { - enableSdCardWidgets(false); - mSdCardSizeRadio.setSelection(false); - mSdCardFileRadio.setSelection(true); - mSdCardFile.setText(sdcard); - } - - sdcard = props.get(AvdManager.AVD_INI_SDCARD_SIZE); - if (sdcard != null && sdcard.length() > 0) { - String[] values = new String[2]; - long sdcardSize = AvdManager.parseSdcardSize(sdcard, values); - - if (sdcardSize != AvdManager.SDCARD_NOT_SIZE_PATTERN) { - enableSdCardWidgets(true); - mSdCardFileRadio.setSelection(false); - mSdCardSizeRadio.setSelection(true); - - mSdCardSize.setText(values[0]); - - String suffix = values[1]; - int n = mSdCardSizeCombo.getItemCount(); - for (int i = 0; i < n; i++) { - if (mSdCardSizeCombo.getItem(i).startsWith(suffix)) { - mSdCardSizeCombo.select(i); - } - } - } - } - - String snapshots = props.get(AvdManager.AVD_INI_SNAPSHOT_PRESENT); - if (snapshots != null && snapshots.length() > 0) { - mSnapshotCheck.setSelection(snapshots.equals("true")); - } - } - - mProperties.clear(); - - if (props != null) { - for (Entry entry : props.entrySet()) { - HardwareProperty prop = mHardwareMap.get(entry.getKey()); - if (prop != null && prop.isValidForUi()) { - mProperties.put(entry.getKey(), entry.getValue()); - } - } - } - - // Cleanup known non-hardware properties - mProperties.remove(AvdManager.AVD_INI_ABI_TYPE); - mProperties.remove(AvdManager.AVD_INI_CPU_ARCH); - mProperties.remove(AvdManager.AVD_INI_SKIN_PATH); - mProperties.remove(AvdManager.AVD_INI_SKIN_NAME); - mProperties.remove(AvdManager.AVD_INI_SDCARD_SIZE); - mProperties.remove(AvdManager.AVD_INI_SDCARD_PATH); - mProperties.remove(AvdManager.AVD_INI_SNAPSHOT_PRESENT); - mProperties.remove(AvdManager.AVD_INI_IMAGES_1); - mProperties.remove(AvdManager.AVD_INI_IMAGES_2); - - mHardwareViewer.refresh(); - } - - @Override - protected void okPressed() { - if (createAvd()) { - super.okPressed(); - } - } - - /** - * Enable or disable the sd card widgets. - * @param sizeMode if true the size-based widgets are to be enabled, and the file-based ones - * disabled. - */ - private void enableSdCardWidgets(boolean sizeMode) { - mSdCardSize.setEnabled(sizeMode); - mSdCardSizeCombo.setEnabled(sizeMode); - - mSdCardFile.setEnabled(!sizeMode); - mBrowseSdCard.setEnabled(!sizeMode); - } - - /** - * Enable or disable the skin widgets. - * @param listMode if true the list-based widgets are to be enabled, and the size-based ones - * disabled. - */ - private void enableSkinWidgets(boolean listMode) { - mSkinCombo.setEnabled(listMode); - - mSkinSizeWidth.setEnabled(!listMode); - mSkinSizeHeight.setEnabled(!listMode); - } - - - private void onBrowseSdCard() { - FileDialog dlg = new FileDialog(getContents().getShell(), SWT.OPEN); - dlg.setText("Choose SD Card image file."); - - String fileName = dlg.open(); - if (fileName != null) { - mSdCardFile.setText(fileName); - } - } - - - - private void reloadTargetCombo() { - String selected = null; - int index = mTargetCombo.getSelectionIndex(); - if (index >= 0) { - selected = mTargetCombo.getItem(index); - } - - mCurrentTargets.clear(); - mTargetCombo.removeAll(); - - boolean found = false; - index = -1; - - LocalSdk localSdk = mAvdManager.getLocalSdk(); - if (localSdk != null) { - for (IAndroidTarget target : localSdk.getTargets()) { - String name; - if (target.isPlatform()) { - name = String.format("%s - API Level %s", - target.getName(), - target.getVersion().getApiString()); - } else { - name = String.format("%s (%s) - API Level %s", - target.getName(), - target.getVendor(), - target.getVersion().getApiString()); - } - mCurrentTargets.put(name, target); - mTargetCombo.add(name); - if (!found) { - index++; - found = name.equals(selected); - } - } - } - - mTargetCombo.setEnabled(mCurrentTargets.size() > 0); - - if (found) { - mTargetCombo.select(index); - } - - reloadSkinCombo(); - } - - private void reloadSkinCombo() { - String selected = null; - int index = mSkinCombo.getSelectionIndex(); - if (index >= 0) { - selected = mSkinCombo.getItem(index); - } - - mSkinCombo.removeAll(); - mSkinCombo.setEnabled(false); - - index = mTargetCombo.getSelectionIndex(); - if (index >= 0) { - - String targetName = mTargetCombo.getItem(index); - - boolean found = false; - IAndroidTarget target = mCurrentTargets.get(targetName); - if (target != null) { - mSkinCombo.add(String.format("Default (%s)", target.getDefaultSkin())); - - index = -1; - for (File skin : target.getSkins()) { - mSkinCombo.add(skin.getName()); - if (!found) { - index++; - found = skin.equals(selected); - } - } - - mSkinCombo.setEnabled(true); - - if (found) { - mSkinCombo.select(index); - } else { - mSkinCombo.select(0); // default - loadSkin(); - } - } - } - } - - /** - * Reload all the abi types in the selection list - */ - private void reloadTagAbiCombo() { - String selected = null; - boolean found = false; - - int index = mTargetCombo.getSelectionIndex(); - if (index >= 0) { - String targetName = mTargetCombo.getItem(index); - IAndroidTarget target = mCurrentTargets.get(targetName); - - ISystemImage[] systemImages = getSystemImages(target); - - // keep a reference to the array into the combo app data field - // so that we can lookup the tag/abi later in getSelectedAbiType() - mTagAbiCombo.setData(ABI_SYS_IMG_DATA_KEY, systemImages); - - mTagAbiCombo.setEnabled(systemImages.length > 1); - - // If user explicitly selected an ABI before, preserve that option - // If user did not explicitly select before (only one option before) - // force them to select - index = mTagAbiCombo.getSelectionIndex(); - if (index >= 0 && mTagAbiCombo.getItemCount() > 1) { - selected = mTagAbiCombo.getItem(index); - } - - mTagAbiCombo.removeAll(); - - int i; - for ( i = 0; i < systemImages.length ; i++ ) { - String prettyAbiType = AvdInfo.getPrettyAbiType(systemImages[i]); - mTagAbiCombo.add(prettyAbiType); - if (!found) { - found = prettyAbiType.equals(selected); - if (found) { - mTagAbiCombo.select(i); - } - } - } - - if (systemImages.length == 1) { - mTagAbiCombo.select(0); - } - } - } - - /** - * Validates the fields, displays errors and warnings. - * Enables the finish button if there are no errors. - */ - private void validatePage() { - String error = null; - String warning = null; - - // Validate AVD name - String avdName = mAvdName.getText().trim(); - boolean hasAvdName = avdName.length() > 0; - boolean isCreate = mEditAvdInfo == null || !avdName.equals(mEditAvdInfo.getName()); - - if (hasAvdName && !AvdManager.RE_AVD_NAME.matcher(avdName).matches()) { - error = String.format( - "AVD name '%1$s' contains invalid characters.\nAllowed characters are: %2$s", - avdName, AvdManager.CHARS_AVD_NAME); - } - - // Validate target - if (hasAvdName && error == null && mTargetCombo.getSelectionIndex() < 0) { - error = "A target must be selected in order to create an AVD."; - } - - // validate abi type if the selected target supports multi archs. - if (hasAvdName && error == null && mTargetCombo.getSelectionIndex() > 0) { - int index = mTargetCombo.getSelectionIndex(); - String targetName = mTargetCombo.getItem(index); - IAndroidTarget target = mCurrentTargets.get(targetName); - ISystemImage[] systemImages = getSystemImages(target); - if (systemImages.length > 1 && mTagAbiCombo.getSelectionIndex() < 0) { - error = "An ABI type must be selected in order to create an AVD."; - } - } - - // Validate SDCard path or value - if (error == null) { - // get the mode. We only need to check the file since the - // verifier on the size Text will prevent invalid input - boolean sdcardFileMode = mSdCardFileRadio.getSelection(); - if (sdcardFileMode) { - String sdName = mSdCardFile.getText().trim(); - if (sdName.length() > 0 && !new File(sdName).isFile()) { - error = "SD Card path isn't valid."; - } - } else { - String valueString = mSdCardSize.getText(); - if (valueString.length() > 0) { - long value = 0; - try { - value = Long.parseLong(valueString); - - int sizeIndex = mSdCardSizeCombo.getSelectionIndex(); - if (sizeIndex >= 0) { - // index 0 shifts by 10 (1024=K), index 1 by 20, etc. - value <<= 10*(1 + sizeIndex); - } - - if (value < AvdManager.SDCARD_MIN_BYTE_SIZE || - value > AvdManager.SDCARD_MAX_BYTE_SIZE) { - value = 0; - } - } catch (Exception e) { - // ignore, we'll test value below. - } - if (value <= 0) { - error = "SD Card size is invalid. Range is 9 MiB..1023 GiB."; - } else if (mEditAvdInfo != null) { - // When editing an existing AVD, compare with the existing - // sdcard size, if any. It only matters if there was an sdcard setting - // before. - Map props = mEditAvdInfo.getProperties(); - if (props != null) { - String original = - mEditAvdInfo.getProperties().get(AvdManager.AVD_INI_SDCARD_SIZE); - if (original != null && original.length() > 0) { - long originalSize = - AvdManager.parseSdcardSize(original, null/*parsedStrings*/); - if (originalSize > 0 && value != originalSize) { - warning = "A new SD Card file will be created.\nThe current SD Card file will be lost."; - } - } - } - } - } - } - } - - // validate the skin - if (error == null) { - // get the mode, we should only check if it's in size mode since - // the built-in list mode is always valid. - if (mSkinSizeRadio.getSelection()) { - // need both with and heigh to be non null - String width = mSkinSizeWidth.getText(); // no need for trim, since the verifier - String height = mSkinSizeHeight.getText(); // rejects non digit. - - if (width.length() == 0 || height.length() == 0) { - error = "Skin size is incorrect.\nBoth dimensions must be > 0."; - } - } - } - - // Check for duplicate AVD name - if (isCreate && hasAvdName && error == null && !mForceCreation.getSelection()) { - Pair conflict = mAvdManager.isAvdNameConflicting(avdName); - assert conflict != null; - switch(conflict.getFirst()) { - case NO_CONFLICT: - break; - case CONFLICT_EXISTING_AVD: - case CONFLICT_INVALID_AVD: - error = String.format( - "The AVD name '%s' is already used.\n" + - "Check \"Override the existing AVD\" to delete the existing one.", - avdName); - break; - case CONFLICT_EXISTING_PATH: - error = String.format( - "Conflict with %s\n" + - "Check \"Override the existing AVD\" to delete the existing one.", - conflict.getSecond()); - break; - default: - // Hmm not supposed to happen... probably someone expanded the - // enum without adding something here. In this case just do an - // assert and use a generic error message. - error = String.format( - "Conflict %s with %s.\n" + - "Check \"Override the existing AVD\" to delete the existing one.", - conflict.getFirst().toString(), - conflict.getSecond()); - assert false; - break; - } - } - - if (error == null && mEditAvdInfo != null && isCreate) { - warning = String.format("The AVD '%1$s' will be duplicated into '%2$s'.", - mEditAvdInfo.getName(), - avdName); - } - - // Validate the create button - boolean can_create = hasAvdName && error == null; - if (can_create) { - can_create &= mTargetCombo.getSelectionIndex() >= 0; - } - mOkButton.setEnabled(can_create); - - // Adjust the create button label as needed - if (isCreate) { - mOkButton.setText("Create AVD"); - } else { - mOkButton.setText("Edit AVD"); - } - - // -- update UI - if (error != null) { - mStatusIcon.setImage(mImageFactory.getImageByName("reject_icon16.png")); //$NON-NLS-1$ - mStatusLabel.setText(error); - } else if (warning != null) { - mStatusIcon.setImage(mImageFactory.getImageByName("warning_icon16.png")); //$NON-NLS-1$ - mStatusLabel.setText(warning); - } else { - mStatusIcon.setImage(null); - mStatusLabel.setText(" \n "); //$NON-NLS-1$ - } - - mStatusComposite.pack(true); - } - - private void loadSkin() { - int targetIndex = mTargetCombo.getSelectionIndex(); - if (targetIndex < 0) { - return; - } - - // resolve the target. - String targetName = mTargetCombo.getItem(targetIndex); - IAndroidTarget target = mCurrentTargets.get(targetName); - if (target == null) { - return; - } - - // get the skin name - String skinName = null; - int skinIndex = mSkinCombo.getSelectionIndex(); - if (skinIndex < 0) { - return; - } else if (skinIndex == 0) { // default skin for the target - File skin = target.getDefaultSkin(); - if (skin != null) { - skinName = skin.getName(); - } - } else { - skinName = mSkinCombo.getItem(skinIndex); - } - - // load the skin properties - String path = target.getPath(IAndroidTarget.SKINS); - File skin = new File(path, skinName); - if (skin.isDirectory() == false && target.isPlatform() == false) { - // it's possible the skin is in the parent target - path = target.getParent().getPath(IAndroidTarget.SKINS); - skin = new File(path, skinName); - } - - if (skin.isDirectory() == false) { - return; - } - - // now get the hardware.ini from the add-on (if applicable) and from the skin - // (if applicable) - HashMap hardwareValues = new HashMap(); - if (target.isPlatform() == false) { - FileWrapper targetHardwareFile = new FileWrapper(target.getLocation(), - AvdManager.HARDWARE_INI); - if (targetHardwareFile.isFile()) { - Map targetHardwareConfig = ProjectProperties.parsePropertyFile( - targetHardwareFile, null /*log*/); - - if (targetHardwareConfig != null) { - hardwareValues.putAll(targetHardwareConfig); - } - } - } - - // from the skin - FileWrapper skinHardwareFile = new FileWrapper(skin, AvdManager.HARDWARE_INI); - if (skinHardwareFile.isFile()) { - Map skinHardwareConfig = ProjectProperties.parsePropertyFile( - skinHardwareFile, null /*log*/); - - if (skinHardwareConfig != null) { - hardwareValues.putAll(skinHardwareConfig); - } - } - - // now set those values in the list of properties for the AVD. - // We just check that none of those properties have been edited by the user yet. - for (Entry entry : hardwareValues.entrySet()) { - if (mEditedProperties.contains(entry.getKey()) == false) { - mProperties.put(entry.getKey(), entry.getValue()); - } - } - - mHardwareViewer.refresh(); - } - - /** - * Creates an AVD from the values in the UI. Called when the user presses the OK button. - */ - private boolean createAvd() { - String avdName = mAvdName.getText().trim(); - int index = mTargetCombo.getSelectionIndex(); - - // quick check on the name and the target selection - if (avdName.length() == 0 || index < 0) { - return false; - } - - // resolve the target. - String targetName = mTargetCombo.getItem(index); - IAndroidTarget target = mCurrentTargets.get(targetName); - if (target == null) { - return false; - } - - // get the tag & abi type - IdDisplay tag = SystemImage.DEFAULT_TAG; - mAbiType = SdkConstants.ABI_ARMEABI; - Object appData = mTagAbiCombo.getData(ABI_SYS_IMG_DATA_KEY); - if (appData instanceof ISystemImage[]) { - int abiIndex = mTagAbiCombo.getSelectionIndex(); - ISystemImage[] systemImages = (ISystemImage[]) appData; - if (abiIndex >= 0 && abiIndex < systemImages.length) { - ISystemImage systemImage = systemImages[abiIndex]; - tag = systemImage.getTag(); - mAbiType = systemImage.getAbiType(); - } - } - - // get the SD card data from the UI. - String sdName = null; - if (mSdCardSizeRadio.getSelection()) { - // size mode - String value = mSdCardSize.getText().trim(); - if (value.length() > 0) { - sdName = value; - // add the unit - switch (mSdCardSizeCombo.getSelectionIndex()) { - case 0: - sdName += "K"; //$NON-NLS-1$ - break; - case 1: - sdName += "M"; //$NON-NLS-1$ - break; - case 2: - sdName += "G"; //$NON-NLS-1$ - break; - default: - // shouldn't be here - assert false; - } - } - } else { - // file mode. - sdName = mSdCardFile.getText().trim(); - } - - // get the Skin data from the UI - String skinName = null; - if (mSkinListRadio.getSelection()) { - // built-in list provides the skin - int skinIndex = mSkinCombo.getSelectionIndex(); - if (skinIndex > 0) { - // index 0 is the default, we don't use it - skinName = mSkinCombo.getItem(skinIndex); - } - } else { - // size mode. get both size and writes it as a skin name - // thanks to validatePage() we know the content of the fields is correct - skinName = mSkinSizeWidth.getText() + "x" + mSkinSizeHeight.getText(); //$NON-NLS-1$ - } - - ILogger log = mSdkLog; - if (log == null || log instanceof MessageBoxLog) { - // If the current logger is a message box, we use our own (to make sure - // to display errors right away and customize the title). - log = new MessageBoxLog( - String.format("Result of creating AVD '%s':", avdName), - getContents().getDisplay(), - false /*logErrorsOnly*/); - } - - File avdFolder = null; - try { - avdFolder = AvdInfo.getDefaultAvdFolder(mAvdManager, avdName); - } catch (AndroidLocationException e) { - return false; - } - - boolean force = mForceCreation.getSelection(); - boolean snapshot = mSnapshotCheck.getSelection(); - - boolean success = false; - AvdInfo avdInfo = mAvdManager.createAvd( - avdFolder, - avdName, - target, - tag, - mAbiType, - null, //skinFolder - skinName, - sdName, - mProperties, - null, // bootProps - snapshot, - force, - mEditAvdInfo != null, //edit existing - log); - - success = avdInfo != null; - - if (log instanceof MessageBoxLog) { - ((MessageBoxLog) log).displayResult(success); - } - return success; - } - - /** - * Returns the list of system images of a target. - *

- * If target is null, returns an empty list. - * If target is an add-on with no system images, return the list from its parent platform. - * - * @param target An IAndroidTarget. Can be null. - * @return A non-null ISystemImage array. Can be empty. - */ - private ISystemImage[] getSystemImages(IAndroidTarget target) { - if (target != null) { - ISystemImage[] images = target.getSystemImages(); - - if ((images == null || images.length == 0) && !target.isPlatform()) { - // If an add-on does not provide any system images, use the ones from the parent. - images = target.getParent().getSystemImages(); - } - - if (images != null) { - return images; - } - } - - return new ISystemImage[0]; - } - - // End of hiding from SWT Designer - //$hide<<$ -} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/repository/AvdManagerWindow.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/repository/AvdManagerWindow.java index 01972988..30af250d 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/repository/AvdManagerWindow.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/repository/AvdManagerWindow.java @@ -17,9 +17,9 @@ package com.android.sdkuilib.repository; import com.android.sdkuilib.internal.repository.ui.AvdManagerWindowImpl1; -import com.android.sdkuilib.internal.widgets.AvdSelector; -import com.android.utils.ILogger; +import org.eclipse.andmore.sdktool.SdkCallAgent; +import org.eclipse.andmore.sdktool.SdkContext; import org.eclipse.swt.widgets.Shell; /** @@ -70,20 +70,34 @@ public enum AvdInvocationContext { * Creates a new window. Caller must call open(), which will block. * * @param parentShell Parent shell. - * @param sdkLog Logger. Cannot be null. - * @param osSdkRoot The OS path to the SDK root. + * @param sdkCallAgent Mediates between application and UI layer * @param context The {@link AvdInvocationContext} to change the behavior depending on who's * opening the SDK Manager. */ public AvdManagerWindow( Shell parentShell, - ILogger sdkLog, - String osSdkRoot, + SdkCallAgent sdkCallAgent, + AvdInvocationContext context) { + this(parentShell, + sdkCallAgent.getSdkContext(), + context); + } + + /** + * Creates a new window. Caller must call open(), which will block. + * + * @param parentShell Parent shell. + * @param sdkContext SDK handler and repo manager + * @param context The {@link AvdInvocationContext} to change the behavior depending on who's + * opening the SDK Manager. + */ + public AvdManagerWindow( + Shell parentShell, + SdkContext sdkContext, AvdInvocationContext context) { mWindow = new AvdManagerWindowImpl1( parentShell, - sdkLog, - osSdkRoot, + sdkContext, context); } diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/ISdkChangeListener.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/repository/ISdkChangeListener.java similarity index 95% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/ISdkChangeListener.java rename to andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/repository/ISdkChangeListener.java index 5c0cab86..9cca7aa8 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/repository/ISdkChangeListener.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/repository/ISdkChangeListener.java @@ -1,54 +1,54 @@ -/* - * Copyright (C) 2010 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.sdklib.repository; - - -/** - * Interface for listeners on SDK modifications by the SDK Manager UI. - * This notifies when the SDK manager is first loading the SDK or before/after it installed - * a package. - */ -public interface ISdkChangeListener { - /** - * Invoked when the content of the SDK is being loaded by the SDK Manager UI - * for the first time. - * This is generally followed by a call to {@link #onSdkReload()} - * or by a call to {@link #preInstallHook()}. - */ - void onSdkLoaded(); - - /** - * Invoked when the SDK Manager UI is about to start installing packages. - * This will be followed by a call to {@link #postInstallHook()}. - */ - void preInstallHook(); - - /** - * Invoked when the SDK Manager UI is done installing packages. - * Some new packages might have been installed or the user might have cancelled the operation. - * This is generally followed by a call to {@link #onSdkReload()}. - */ - void postInstallHook(); - - /** - * Invoked when the content of the SDK is being reloaded by the SDK Manager UI, - * typically after a package was installed. The SDK content might or might not - * have changed. - */ - void onSdkReload(); -} - +/* + * Copyright (C) 2010 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.sdkuilib.repository; + + +/** + * Interface for listeners on SDK modifications by the SDK Manager UI. + * This notifies when the SDK manager is first loading the SDK or before/after it installed + * a package. + */ +public interface ISdkChangeListener { + /** + * Invoked when the content of the SDK is being loaded by the SDK Manager UI + * for the first time. + * This is generally followed by a call to {@link #onSdkReload()} + * or by a call to {@link #preInstallHook()}. + */ + void onSdkLoaded(); + + /** + * Invoked when the SDK Manager UI is about to start installing packages. + * This will be followed by a call to {@link #postInstallHook()}. + */ + void preInstallHook(); + + /** + * Invoked when the SDK Manager UI is done installing packages. + * Some new packages might have been installed or the user might have cancelled the operation. + * This is generally followed by a call to {@link #onSdkReload()}. + */ + void postInstallHook(); + + /** + * Invoked when the content of the SDK is being reloaded by the SDK Manager UI, + * typically after a package was installed. The SDK content might or might not + * have changed. + */ + void onSdkReload(); +} + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/repository/SdkUpdaterWindow.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/repository/SdkUpdaterWindow.java index b91f03c6..960eec46 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/repository/SdkUpdaterWindow.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/repository/SdkUpdaterWindow.java @@ -16,10 +16,13 @@ package com.android.sdkuilib.repository; -import com.android.sdklib.repository.ISdkChangeListener; +import com.android.repository.api.RepoManager; +import com.android.sdklib.repository.AndroidSdkHandler; import com.android.sdkuilib.internal.repository.ISdkUpdaterWindow; import com.android.sdkuilib.internal.repository.ui.SdkUpdaterWindowImpl2; -import com.android.utils.ILogger; + +import org.eclipse.andmore.sdktool.SdkCallAgent; +import org.eclipse.andmore.sdktool.SdkContext; import org.eclipse.swt.widgets.Shell; @@ -74,18 +77,32 @@ public enum SdkInvocationContext { * Creates a new window. Caller must call open(), which will block. * * @param parentShell Parent shell. - * @param sdkLog Logger. Cannot be null. - * @param osSdkRoot The OS path to the SDK root. + * @param sdkCallAgent Mediates between application and UI layer + * @param context The {@link SdkInvocationContext} to change the behavior depending on who's + * opening the SDK Manager. + */ + public SdkUpdaterWindow( + Shell parentShell, + SdkCallAgent sdkCallAgent, + SdkInvocationContext context) { + + this(parentShell, sdkCallAgent.getSdkContext(), context); + } + + /** + * Creates a new window. Caller must call open(), which will block. + * + * @param parentShell Parent shell. + * @param sdkContext SDK handler and repo manager * @param context The {@link SdkInvocationContext} to change the behavior depending on who's * opening the SDK Manager. */ public SdkUpdaterWindow( Shell parentShell, - ILogger sdkLog, - String osSdkRoot, + SdkContext sdkContext, SdkInvocationContext context) { - mWindow = new SdkUpdaterWindowImpl2(parentShell, sdkLog, osSdkRoot, context); + mWindow = new SdkUpdaterWindowImpl2(parentShell, sdkContext, context); } /** diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AdtUpdateDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/AdtUpdateDialog.java similarity index 52% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AdtUpdateDialog.java rename to andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/AdtUpdateDialog.java index 5fcd1f77..76ce1b63 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/repository/ui/AdtUpdateDialog.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/AdtUpdateDialog.java @@ -14,29 +14,32 @@ * limitations under the License. */ -package com.android.sdkuilib.internal.repository.ui; - +package com.android.sdkuilib.ui; +import com.android.repository.api.RemotePackage; +import com.android.repository.api.RepoPackage; +import com.android.repository.api.UpdatablePackage; +import com.android.repository.impl.meta.TypeDetails; import com.android.sdklib.AndroidVersion; -import com.android.sdklib.internal.repository.packages.ExtraPackage; -import com.android.sdklib.internal.repository.packages.Package; -import com.android.sdklib.internal.repository.packages.PlatformPackage; -import com.android.sdklib.internal.repository.packages.PlatformToolPackage; -import com.android.sdklib.internal.repository.packages.ToolPackage; -import com.android.sdklib.internal.repository.sources.SdkSource; -import com.android.sdklib.internal.repository.updater.PackageLoader; -import com.android.sdklib.internal.repository.updater.SettingsController; -import com.android.sdklib.internal.repository.updater.PackageLoader.IAutoInstallTask; -import com.android.sdkuilib.internal.repository.SwtUpdaterData; -import com.android.sdkuilib.internal.repository.core.SdkLogAdapter; +import com.android.sdklib.repository.meta.DetailsTypes.ExtraDetailsType; +import com.android.sdklib.repository.meta.DetailsTypes.PlatformDetailsType; +import com.android.sdkuilib.internal.repository.PackageManager.RemotePackageHandler; +import com.android.sdkuilib.internal.repository.PackageManager.UpdatablePackageHandler; +import com.android.sdkuilib.internal.repository.Settings; +import com.android.sdkuilib.internal.repository.content.PackageType; +import com.android.sdkuilib.internal.repository.content.PkgItem; +import com.android.sdkuilib.internal.repository.ui.LogWindow; +import com.android.sdkuilib.internal.repository.ui.SdkProgressFactory; import com.android.sdkuilib.internal.tasks.ProgressView; -import com.android.sdkuilib.internal.tasks.ProgressViewFactory; +import com.android.sdkuilib.repository.SdkUpdaterWindow.SdkInvocationContext; import com.android.sdkuilib.ui.GridDataBuilder; import com.android.sdkuilib.ui.GridLayoutBuilder; import com.android.sdkuilib.ui.SwtBaseDialog; import com.android.utils.ILogger; import com.android.utils.Pair; +import org.eclipse.andmore.sdktool.SdkCallAgent; +import org.eclipse.andmore.sdktool.SdkContext; import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.layout.GridLayout; @@ -46,11 +49,13 @@ import org.eclipse.swt.widgets.Shell; import java.io.File; +import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.Set; /** + * TODO - Integrate this class with SdkUpdaterWindow * This is a private implementation of UpdateWindow for ADT, * designed to install a very specific package. *

@@ -72,16 +77,15 @@ public class AdtUpdateDialog extends SwtBaseDialog { public static final int USE_MAX_REMOTE_API_LEVEL = 0; private static final String APP_NAME = "Android SDK Manager"; - private final SwtUpdaterData mUpdaterData; + private final SdkContext mSdkContext; private Boolean mResultCode = Boolean.FALSE; - private Map mResultPaths = null; - private SettingsController mSettingsController; + private Map mResultPaths = null; private PackageFilter mPackageFilter; - private PackageLoader mPackageLoader; private ProgressBar mProgressBar; private Label mStatusText; + private LogWindow mLogWindow; /** * Creates a new {@link AdtUpdateDialog}. @@ -89,39 +93,37 @@ public class AdtUpdateDialog extends SwtBaseDialog { * {@link #installPlatformPackage} after this. * * @param parentShell The existing parent shell. Must not be null. - * @param sdkLog An SDK logger. Must not be null. - * @param osSdkRoot The current SDK root OS path. Must not be null or empty. - */ + * @param sdkCallAgent Mediator between application and UI layer + */ public AdtUpdateDialog( Shell parentShell, - ILogger sdkLog, - String osSdkRoot) { + SdkCallAgent sdkCallAgent) { super(parentShell, SWT.NONE, APP_NAME); - mUpdaterData = new SwtUpdaterData(osSdkRoot, sdkLog); + mSdkContext = sdkCallAgent.getSdkContext(); } /** - * Displays the update dialog and triggers installation of the requested {@code extra} - * package with the specified vendor and path attributes. + * Displays the update dialog and triggers installation of the requested platform + * package with the specified API level. *

* Callers must not try to reuse this dialog after this call. * - * @param vendor The extra package vendor string to match. - * @param path The extra package path string to match. + * @param apiLevel The platform API level to match. + * The special value {@link #USE_MAX_REMOTE_API_LEVEL} means to use + * the highest API level available on the remote repository. * @return A boolean indicating whether the installation was successful (meaning the package * was either already present, or got installed or updated properly) and a {@link File} * with the path to the root folder of the package. The file is null when the boolean * is false, otherwise it should point to an existing valid folder. - * @wbp.parser.entryPoint */ - public Pair installExtraPackage(String vendor, String path) { - mPackageFilter = createExtraFilter(vendor, path); + public Pair installPlatformPackage(int apiLevel) { + mPackageFilter = createPlatformFilter(apiLevel); open(); File installPath = null; if (mResultPaths != null) { - for (Entry entry : mResultPaths.entrySet()) { - if (entry.getKey() instanceof ExtraPackage) { + for (Entry entry : mResultPaths.entrySet()) { + if (entry.getKey().getMetaPackage().getPackageType() == PackageType.platforms) { installPath = entry.getValue(); break; } @@ -132,55 +134,54 @@ public Pair installExtraPackage(String vendor, String path) { } /** - * Displays the update dialog and triggers installation of platform-tools package. + * Displays the update dialog and triggers installation of the requested {@code extra} + * package with the specified vendor and path attributes. *

* Callers must not try to reuse this dialog after this call. * + * @param vendor The extra package vendor string to match. + * @param path The extra package path string to match. * @return A boolean indicating whether the installation was successful (meaning the package * was either already present, or got installed or updated properly) and a {@link File} * with the path to the root folder of the package. The file is null when the boolean * is false, otherwise it should point to an existing valid folder. * @wbp.parser.entryPoint */ - public Pair installPlatformTools() { - mPackageFilter = createPlatformToolsFilter(); + public Pair installExtraPackage(String vendor, String path) { + mPackageFilter = createExtraFilter(vendor, path); open(); File installPath = null; if (mResultPaths != null) { - for (Entry entry : mResultPaths.entrySet()) { - if (entry.getKey() instanceof ExtraPackage) { + for (Entry entry : mResultPaths.entrySet()) { + if (entry.getKey().getMetaPackage().getPackageType() == PackageType.extras) { installPath = entry.getValue(); break; } } } - return Pair.of(mResultCode, installPath); } /** - * Displays the update dialog and triggers installation of the requested platform - * package with the specified API level. + * Displays the update dialog and triggers installation of platform-tools package. *

* Callers must not try to reuse this dialog after this call. * - * @param apiLevel The platform API level to match. - * The special value {@link #USE_MAX_REMOTE_API_LEVEL} means to use - * the highest API level available on the remote repository. * @return A boolean indicating whether the installation was successful (meaning the package * was either already present, or got installed or updated properly) and a {@link File} * with the path to the root folder of the package. The file is null when the boolean * is false, otherwise it should point to an existing valid folder. + * @wbp.parser.entryPoint */ - public Pair installPlatformPackage(int apiLevel) { - mPackageFilter = createPlatformFilter(apiLevel); + public Pair installPlatformTools() { + mPackageFilter = createPlatformToolsFilter(); open(); File installPath = null; if (mResultPaths != null) { - for (Entry entry : mResultPaths.entrySet()) { - if (entry.getKey() instanceof PlatformPackage) { + for (Entry entry : mResultPaths.entrySet()) { + if (entry.getKey().getMetaPackage().getPackageType() == PackageType.platform_tools) { installPath = entry.getValue(); break; } @@ -214,11 +215,7 @@ protected void createContents() { Shell shell = getShell(); shell.setMinimumSize(new Point(450, 100)); shell.setSize(450, 100); - - mUpdaterData.setWindowShell(shell); - GridLayoutBuilder.create(shell).columns(1); - Composite composite1 = new Composite(shell, SWT.NONE); composite1.setLayout(new GridLayout(1, false)); GridDataBuilder.create(composite1).fill().grab(); @@ -229,64 +226,101 @@ protected void createContents() { mStatusText = new Label(composite1, SWT.NONE); mStatusText.setText("Status Placeholder"); //$NON-NLS-1$ placeholder GridDataBuilder.create(mStatusText).hFill().hGrab(); + createLogWindow(); } + /** + * Creates the log window. + *

+ * If this is invoked from an IDE, we also define a secondary logger so that all + * messages flow to the IDE log. This may or may not be what we want in the end + * (e.g. a middle ground would be to repeat error, and ignore normal/verbose) + */ + private void createLogWindow() { + mLogWindow = new LogWindow(getShell(), mSdkContext.getSdkLog()); + mLogWindow.open(); + } + + @Override protected void postCreate() { - ProgressViewFactory factory = new ProgressViewFactory(); - factory.setProgressView(new ProgressView( - mStatusText, - mProgressBar, - null /*buttonStop*/, - new SdkLogAdapter(mUpdaterData.getSdkLog()))); - mUpdaterData.setTaskFactory(factory); - - setupSources(); - initializeSettings(); + // This class delegates all logging to the mLogWindow window + // and filters errors to make sure the window is visible when + // an error is logged. + SdkProgressFactory.ISdkLogWindow logAdapter = new SdkProgressFactory.ISdkLogWindow() { + @Override + public void setDescription(String description) { + mLogWindow.setDescription(description); + } + + @Override + public void log(String log) { + mLogWindow.log(log); + } - if (mUpdaterData.checkIfInitFailed()) { + @Override + public void logVerbose(String log) { + mLogWindow.logVerbose(log); + } + + @Override + public void logError(String log) { + mLogWindow.logError(log); + } + @Override + public void show() + { + // Run the window visibility check/toggle on the UI thread. + // Note: at least on Windows, it seems ok to check for the window visibility + // on a sub-thread but that doesn't seem cross-platform safe. We shouldn't + // have a lot of error logging, so this should be acceptable. If not, we could + // cache the visibility state. + if (getShell() != null && !getShell().isDisposed()) { + getShell().getDisplay().syncExec(new Runnable() { + @Override + public void run() { + if (!mLogWindow.isVisible()) { + mLogWindow.setVisible(true); + } + } + }); + } + } + }; + SdkProgressFactory factory = new SdkProgressFactory(mStatusText, mProgressBar, null, logAdapter); + initializeSettings(); + if (mSdkContext.hasError()) + { + ILogger logger = (ILogger)factory; + Iterator iterator = mSdkContext.getLogMessages().iterator(); + while(iterator.hasNext()) + logger.error(null, iterator.next()); close(); return; } - - mUpdaterData.broadcastOnSdkLoaded(); - - mPackageLoader = new PackageLoader(mUpdaterData); + mSdkContext.setSdkLogger(factory); + mSdkContext.setSdkProgressIndicator(factory); } @Override protected void eventLoop() { - mPackageLoader.loadPackagesWithInstallTask( - mPackageFilter.installFlags(), - new IAutoInstallTask() { - @Override - public Package[] filterLoadedSource(SdkSource source, Package[] packages) { - for (Package pkg : packages) { - mPackageFilter.visit(pkg); - } - return packages; - } - - @Override - public boolean acceptPackage(Package pkg) { + /* + public boolean acceptPackage(PkgItem pkg) { // Is this the package we want to install? return mPackageFilter.accept(pkg); } - @Override - public void setResult(boolean success, Map installPaths) { + public void setResult(boolean success, Map installPaths) { // Capture the result from the installation. mResultCode = Boolean.valueOf(success); mResultPaths = installPaths; } - @Override - public void taskCompleted() { + public void taskCompleted() { // We can close that window now. close(); } - }); - +*/ super.eventLoop(); } @@ -299,22 +333,23 @@ public void taskCompleted() { // --- Internals & UI Callbacks ----------- - /** - * Used to initialize the sources. - */ - private void setupSources() { - mUpdaterData.setupDefaultSources(); - } - /** * Initializes settings. + * This must be called after addExtraPages(), which created a settings page. + * Iterate through all the pages to find the first (and supposedly unique) setting page, + * and use it to load and apply these settings. */ - private void initializeSettings() { - mSettingsController = mUpdaterData.getSettingsController(); - mSettingsController.loadSettings(); - mSettingsController.applySettings(); + private boolean initializeSettings() { + Settings settings = new Settings(); + if (settings.initialize(mSdkContext.getSdkLog())) + { + mSdkContext.setSettings(settings); + return true; + } + return false; } + // ---- private static abstract class PackageFilter { @@ -322,10 +357,10 @@ private static abstract class PackageFilter { abstract int installFlags(); /** Visit a new package definition, in case we need to adjust the filter dynamically. */ - abstract void visit(Package pkg); + abstract void visit(PkgItem pkg); /** Checks whether this is the package we've been looking for. */ - abstract boolean accept(Package pkg); + abstract boolean accept(PkgItem pkg); } public static PackageFilter createExtraFilter( @@ -336,33 +371,30 @@ public static PackageFilter createExtraFilter( String mPath = path; @Override - boolean accept(Package pkg) { - if (pkg instanceof ExtraPackage) { - ExtraPackage ep = (ExtraPackage) pkg; - if (ep.getVendorId().equals(mVendor)) { - // Check actual extra field first - if (ep.getPath().equals(mPath)) { - return true; - } - // If not, check whether this is one of the values. - for (String oldPath : ep.getOldPaths()) { - if (oldPath.equals(mPath)) { - return true; - } - } + boolean accept(PkgItem pkg) { + if (pkg.getMetaPackage().getPackageType() == PackageType.extras) { + TypeDetails details = pkg.getMainPackage().getTypeDetails(); + if (details instanceof ExtraDetailsType) { + ExtraDetailsType extraDetailsType = (ExtraDetailsType)details; + if (extraDetailsType.getVendor().getId().equals(mVendor)) { + // Check actual extra field + if (pkg.getMainPackage().getPath().equals(mPath)) { + return true; + } + } } - } + } return false; } @Override - void visit(Package pkg) { + void visit(PkgItem pkg) { // nop } @Override int installFlags() { - return SwtUpdaterData.TOOLS_MSG_UPDATED_FROM_ADT; + return SdkCallAgent.TOOLS_MSG_UPDATED_FROM_ADT; } }; } @@ -370,18 +402,18 @@ int installFlags() { private PackageFilter createPlatformToolsFilter() { return new PackageFilter() { @Override - boolean accept(Package pkg) { - return pkg instanceof PlatformToolPackage; + boolean accept(PkgItem pkg) { + return pkg.getMetaPackage().getPackageType() == PackageType.platform_tools; } @Override - void visit(Package pkg) { + void visit(PkgItem pkg) { // nop } @Override int installFlags() { - return SwtUpdaterData.TOOLS_MSG_UPDATED_FROM_ADT; + return SdkCallAgent.TOOLS_MSG_UPDATED_FROM_ADT; } }; } @@ -392,35 +424,41 @@ public static PackageFilter createPlatformFilter(final int apiLevel) { boolean mFindMaxApi = apiLevel == USE_MAX_REMOTE_API_LEVEL; @Override - boolean accept(Package pkg) { - if (pkg instanceof PlatformPackage) { - PlatformPackage pp = (PlatformPackage) pkg; - AndroidVersion v = pp.getAndroidVersion(); - return !v.isPreview() && v.getApiLevel() == mApiLevel; + boolean accept(PkgItem pkg) { + if (pkg.getMetaPackage().getPackageType() == PackageType.platforms) { + RepoPackage remotePackage = pkg.getMainPackage(); + TypeDetails details = remotePackage.getTypeDetails(); + if (details instanceof PlatformDetailsType) { + PlatformDetailsType platformDetailsType = (PlatformDetailsType)details; + AndroidVersion androidVersion = platformDetailsType.getAndroidVersion(); + return !androidVersion.isPreview() && androidVersion.getApiLevel() == mApiLevel; + } } return false; } @Override - void visit(Package pkg) { + void visit(PkgItem pkg) { // Try to find the max API in all remote packages - if (mFindMaxApi && - pkg instanceof PlatformPackage && - !pkg.isLocal()) { - PlatformPackage pp = (PlatformPackage) pkg; - AndroidVersion v = pp.getAndroidVersion(); - if (!v.isPreview()) { - int api = v.getApiLevel(); - if (api > mApiLevel) { - mApiLevel = api; - } + if (mFindMaxApi && (pkg.getMetaPackage().getPackageType() == PackageType.platforms)) { + RepoPackage remotePackage = pkg.getMainPackage(); + TypeDetails details = remotePackage.getTypeDetails(); + if ((details instanceof PlatformDetailsType) && (remotePackage instanceof RemotePackage)) { + PlatformDetailsType platformDetailsType = (PlatformDetailsType)details; + AndroidVersion androidVersion = platformDetailsType.getAndroidVersion(); + if (!androidVersion.isPreview()) { + int api = androidVersion.getApiLevel(); + if (api > mApiLevel) { + mApiLevel = api; + } + } } } } @Override int installFlags() { - return SwtUpdaterData.TOOLS_MSG_UPDATED_FROM_ADT; + return SdkCallAgent.TOOLS_MSG_UPDATED_FROM_ADT; } }; } @@ -433,24 +471,26 @@ public static PackageFilter createNewSdkFilter(final Set apiLevels) { boolean mNeedPlatformTools = true; @Override - boolean accept(Package pkg) { - if (!pkg.isLocal()) { - if (pkg instanceof PlatformPackage) { - PlatformPackage pp = (PlatformPackage) pkg; - AndroidVersion v = pp.getAndroidVersion(); - if (!v.isPreview()) { - int level = v.getApiLevel(); - if ((mFindMaxApi && level == mMaxApiLevel) || - (level > 0 && apiLevels.contains(level))) { - return true; - } - } - } else if (mNeedTools && pkg instanceof ToolPackage) { + boolean accept(PkgItem pkg) { + RepoPackage repoPackage = pkg.getMainPackage(); + if (repoPackage instanceof RemotePackage) { + TypeDetails details = repoPackage.getTypeDetails(); + if ((details instanceof PlatformDetailsType)) { + PlatformDetailsType platformDetailsType = (PlatformDetailsType)details; + AndroidVersion androidVersion = platformDetailsType.getAndroidVersion(); + if (!androidVersion.isPreview()) { + int level = androidVersion.getApiLevel(); + if ((mFindMaxApi && level == mMaxApiLevel) || + (level > 0 && apiLevels.contains(level))) { + return true; + } + } + } else if (mNeedTools && (pkg.getMetaPackage().getPackageType() == PackageType.tools)) { // We want a tool package. There should be only one, // but in case of error just take the first one. mNeedTools = false; return true; - } else if (mNeedPlatformTools && pkg instanceof PlatformToolPackage) { + } else if (mNeedPlatformTools && (pkg.getMetaPackage().getPackageType() == PackageType.platform_tools)) { // We want a platform-tool package. There should be only one, // but in case of error just take the first one. mNeedPlatformTools = false; @@ -461,31 +501,31 @@ boolean accept(Package pkg) { } @Override - void visit(Package pkg) { + void visit(PkgItem pkg) { // Try to find the max API in all remote packages - if (mFindMaxApi && - pkg instanceof PlatformPackage && - !pkg.isLocal()) { - PlatformPackage pp = (PlatformPackage) pkg; - AndroidVersion v = pp.getAndroidVersion(); - if (!v.isPreview()) { - int api = v.getApiLevel(); - if (api > mMaxApiLevel) { - mMaxApiLevel = api; - } + if (mFindMaxApi && (pkg.getMetaPackage().getPackageType() == PackageType.platforms)) { + RepoPackage remotePackage = pkg.getMainPackage(); + TypeDetails details = remotePackage.getTypeDetails(); + if ((details instanceof PlatformDetailsType) && (remotePackage instanceof RemotePackage)) { + PlatformDetailsType platformDetailsType = (PlatformDetailsType)details; + AndroidVersion androidVersion = platformDetailsType.getAndroidVersion(); + if (!androidVersion.isPreview()) { + int api = androidVersion.getApiLevel(); + if (api > mMaxApiLevel) { + mMaxApiLevel = api; + } + } } } } @Override int installFlags() { - return SwtUpdaterData.NO_TOOLS_MSG; + return SdkCallAgent.NO_TOOLS_MSG; } }; } - - // End of hiding from SWT Designer //$hide<<$ diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/AvdDisplayMode.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/AvdDisplayMode.java new file mode 100644 index 00000000..573154ca --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/AvdDisplayMode.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2017 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.sdkuilib.ui; + +import com.android.sdkuilib.internal.widgets.AvdSelector; + +/** + * The display mode of the AVD Selector. + */ +public enum AvdDisplayMode { + /** + * Manager mode. Invalid AVDs are displayed. Buttons to create/delete AVDs + */ + MANAGER, + + /** + * Non manager mode. Only valid AVDs are displayed. Cannot create/delete AVDs, but + * there is a button to open the AVD Manager. + * In the "check" selection mode, checkboxes are displayed on each line + * and {@link AvdSelector#getSelected()} returns the line that is checked + * even if it is not the currently selected line. Only one line can + * be checked at once. + */ + SIMPLE_CHECK, + + /** + * Non manager mode. Only valid AVDs are displayed. Cannot create/delete AVDs, but + * there is a button to open the AVD Manager. + * In the "select" selection mode, there are no checkboxes and + * {@link AvdSelector#getSelected()} returns the line currently selected. + * Only one line can be selected at once. + */ + SIMPLE_SELECTION, + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/AvdSelectorWindow.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/AvdSelectorWindow.java new file mode 100644 index 00000000..dee8cdb7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/AvdSelectorWindow.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2017 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.sdkuilib.ui; + +import org.eclipse.andmore.sdktool.SdkCallAgent; +import org.eclipse.andmore.sdktool.SdkContext; +import org.eclipse.andmore.sdktool.Utilities; +import org.eclipse.andmore.sdktool.Utilities.Compatibility; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.TableItem; + +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.internal.avd.AvdInfo; +import com.android.sdklib.internal.avd.AvdManager; +import com.android.sdklib.repository.targets.SystemImage; +import com.android.sdkuilib.internal.repository.avd.AvdAgent; +import com.android.sdkuilib.internal.repository.avd.SdkTargets; +import com.android.sdkuilib.internal.widgets.AvdSelector; +import com.android.sdkuilib.internal.widgets.AvdSelector.IAvdFilter; + +/** + * A control to select an Android Virtual Device (AVD) + * @author Andrew Bowley + * + */ +public class AvdSelectorWindow { + + private final AvdSelector avdSelector; + private final SdkContext sdkContext; + private final SdkTargets sdkTargets; + + public AvdSelectorWindow(Composite parent, SdkCallAgent sdkCallAgent) { + this.sdkContext = sdkCallAgent.getSdkContext(); + sdkTargets = new SdkTargets(sdkContext); + avdSelector = new AvdSelector(parent, sdkContext, (IAvdFilter)null, AvdDisplayMode.SIMPLE_CHECK); + } + + /** + * Sets the current target selection. + *

+ * If the selection is actually changed, this will invoke the selection listener + * (if any) with a null event. + * + * @param target the target to be selected. Use null to deselect everything. + * @return true if the target could be selected, false otherwise. + */ + public void setSelection(AvdInfo avd) { + avdSelector.setSelection(avd); + } + + /** + * Returns the currently selected item. In {@link DisplayMode#SIMPLE_CHECK} mode this will + * return the {@link AvdInfo} that is checked instead of the list selection. + * + * @return The currently selected item or null. + */ + public AvdInfo getSelected() { + AvdAgent avdAgent = avdSelector.getSelected(); + return avdAgent != null ? avdAgent.getAvd() : null; + } + + /** + * Sets the table grid layout data. + * + * @param heightHint If > 0, the height hint is set to the requested value. + */ + public void setTableHeightHint(int heightHint) { + avdSelector.setTableHeightHint(heightHint); + } + + /** + * Sets a selection listener. Set it to null to remove it. + * The listener will be called after this table processed its selection + * events so that the caller can see the updated state. + *

+ * The event's item contains a {@link TableItem}. + * The {@link TableItem#getData()} contains an {@link IAndroidTarget}. + *

+ * It is recommended that the caller uses the {@link #getSelected()} method instead. + *

+ * The default behavior for double click (when not in {@link DisplayMode#SIMPLE_CHECK}) is to + * display the details of the selected AVD.
+ * To disable it (when you provide your own double click action), set + * {@link SelectionEvent#doit} to false in + * {@link SelectionListener#widgetDefaultSelected(SelectionEvent)} + * + * @param selectionListener The new listener or null to remove it. + */ + public void setSelectionListener(SelectionListener selectionListener) { + avdSelector.setSelectionListener(selectionListener); + } + + /** + * Enables the receiver if the argument is true, and disables it otherwise. + * A disabled control is typically not selectable from the user interface + * and draws with an inactive or "grayed" look. + * + * @param enabled the new enabled state. + */ + public void setEnabled(boolean enabled) { + avdSelector.setEnabled(enabled); + } + + /** + * Sets a new AVD manager and updates AVD filter parameters + * This also refreshes the display + * @param manager the AVD manager. + */ + public void setManager(AvdManager manager, IAndroidTarget target, AndroidVersion minApiVersion) { + avdSelector.setManager(manager); + avdSelector.refresh(false); + avdSelector.setFilter(getCompatibilityFilter(target, minApiVersion)); + } + + private IAvdFilter getCompatibilityFilter(IAndroidTarget target, AndroidVersion minApiVersion) { + return new IAvdFilter() { + + @Override + public void prepare() { + } + + @Override + public void cleanup() { + } + + @Override + public boolean accept(AvdAgent avdAgent) { + AvdInfo info = avdAgent.getAvd(); + Compatibility c = + Utilities.canRun(info, getAndroidTargetFor(info), target, minApiVersion); + return (c == Compatibility.NO) ? false : true; + } + }; + } + + private IAndroidTarget getAndroidTargetFor(AvdInfo info) { + return sdkTargets.getTargetForSysImage((SystemImage)info.getSystemImage()); + } +} diff --git a/android-core/plugins/org.eclipse.andmore/src/org/eclipse/andmore/internal/launch/DeviceChooserDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/DeviceChooserDialog.java similarity index 87% rename from android-core/plugins/org.eclipse.andmore/src/org/eclipse/andmore/internal/launch/DeviceChooserDialog.java rename to andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/DeviceChooserDialog.java index bd445ad1..d8502bfe 100644 --- a/android-core/plugins/org.eclipse.andmore/src/org/eclipse/andmore/internal/launch/DeviceChooserDialog.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/DeviceChooserDialog.java @@ -1,824 +1,823 @@ -/* - * 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 org.eclipse.andmore.internal.launch; - -import com.android.ddmlib.AndroidDebugBridge; -import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; -import com.android.ddmlib.Client; -import com.android.ddmlib.IDevice; -import com.android.ddmlib.IDevice.DeviceState; -import com.android.ddmuilib.ImageLoader; -import com.android.ddmuilib.TableHelper; -import com.android.sdklib.AndroidVersion; -import com.android.sdklib.IAndroidTarget; -import com.android.sdklib.internal.avd.AvdInfo; -import com.android.sdkuilib.internal.widgets.AvdSelector; -import com.android.sdkuilib.internal.widgets.AvdSelector.DisplayMode; -import com.android.sdkuilib.internal.widgets.AvdSelector.IAvdFilter; - -import org.eclipse.andmore.ddms.DdmsPlugin; -import org.eclipse.andmore.internal.editors.IconFactory; -import org.eclipse.andmore.internal.sdk.AdtConsoleSdkLog; -import org.eclipse.andmore.internal.sdk.Sdk; -import org.eclipse.jface.dialogs.Dialog; -import org.eclipse.jface.dialogs.IDialogConstants; -import org.eclipse.jface.viewers.ILabelProviderListener; -import org.eclipse.jface.viewers.IStructuredContentProvider; -import org.eclipse.jface.viewers.ITableLabelProvider; -import org.eclipse.jface.viewers.StructuredSelection; -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.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.Control; -import org.eclipse.swt.widgets.Display; -import org.eclipse.swt.widgets.Label; -import org.eclipse.swt.widgets.Shell; -import org.eclipse.swt.widgets.Table; - -import java.util.ArrayList; -import java.util.List; - -/** - * A dialog that lets the user choose a device to deploy an application. - * The user can either choose an exiting running device (including running emulators) - * or start a new emulator using an Android Virtual Device configuration that matches - * the current project. - */ -public class DeviceChooserDialog extends Dialog implements IDeviceChangeListener { - - private final static int ICON_WIDTH = 16; - - private Table mDeviceTable; - private TableViewer mViewer; - private AvdSelector mPreferredAvdSelector; - - private Image mDeviceImage; - private Image mEmulatorImage; - private Image mMatchImage; - private Image mNoMatchImage; - private Image mWarningImage; - - private final DeviceChooserResponse mResponse; - private final String mPackageName; - private final IAndroidTarget mProjectTarget; - private final AndroidVersion mMinApiVersion; - private final Sdk mSdk; - - private Button mDeviceRadioButton; - private Button mUseDeviceForFutureLaunchesCheckbox; - private boolean mUseDeviceForFutureLaunches; - - private boolean mDisableAvdSelectionChange = false; - - /** - * Basic Content Provider for a table full of {@link IDevice} objects. The input is - * a {@link AndroidDebugBridge}. - */ - private class ContentProvider implements IStructuredContentProvider { - @Override - public Object[] getElements(Object inputElement) { - if (inputElement instanceof AndroidDebugBridge) { - return findCompatibleDevices(((AndroidDebugBridge)inputElement).getDevices()); - } - - return new Object[0]; - } - - private Object[] findCompatibleDevices(IDevice[] devices) { - if (devices == null) { - return null; - } - - List compatibleDevices = new ArrayList(devices.length); - for (IDevice device : devices) { - AndroidVersion deviceVersion = Sdk.getDeviceVersion(device); - if (deviceVersion == null || deviceVersion.canRun(mMinApiVersion)) { - compatibleDevices.add(device); - } - } - - return compatibleDevices.toArray(); - } - - @Override - public void dispose() { - // pass - } - - @Override - public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { - // pass - } - } - - /** - * A Label Provider for the {@link TableViewer} in {@link DeviceChooserDialog}. - * It provides labels and images for {@link IDevice} objects. - */ - private class LabelProvider implements ITableLabelProvider { - - @Override - public Image getColumnImage(Object element, int columnIndex) { - if (element instanceof IDevice) { - IDevice device = (IDevice)element; - switch (columnIndex) { - case 0: - return device.isEmulator() ? mEmulatorImage : mDeviceImage; - - case 2: - // check for compatibility. - if (device.isEmulator() == false) { // physical device - // get the version of the device - AndroidVersion deviceVersion = Sdk.getDeviceVersion(device); - if (deviceVersion == null) { - return mWarningImage; - } else { - if (!deviceVersion.canRun(mMinApiVersion)) { - return mNoMatchImage; - } - - // if the project is compiling against an add-on, - // the optional API may be missing from the device. - return mProjectTarget.isPlatform() ? - mMatchImage : mWarningImage; - } - } else { - // get the AvdInfo - AvdInfo info = mSdk.getAvdManager().getAvd(device.getAvdName(), - true /*validAvdOnly*/); - AvdCompatibility.Compatibility c = - AvdCompatibility.canRun(info, mProjectTarget, - mMinApiVersion); - switch (c) { - case YES: - return mMatchImage; - case NO: - return mNoMatchImage; - case UNKNOWN: - return mWarningImage; - } - } - } - } - - return null; - } - - @Override - public String getColumnText(Object element, int columnIndex) { - if (element instanceof IDevice) { - IDevice device = (IDevice)element; - switch (columnIndex) { - case 0: - return device.getName(); - case 1: - if (device.isEmulator()) { - return device.getAvdName(); - } else { - return "N/A"; // devices don't have AVD names. - } - case 2: - if (device.isEmulator()) { - AvdInfo info = mSdk.getAvdManager().getAvd(device.getAvdName(), - true /*validAvdOnly*/); - if (info == null) { - return "?"; - } - return info.getTarget().getFullName(); - } else { - String deviceBuild = device.getProperty(IDevice.PROP_BUILD_VERSION); - if (deviceBuild == null) { - return "unknown"; - } - return deviceBuild; - } - case 3: - String debuggable = device.getProperty(IDevice.PROP_DEBUGGABLE); - if (debuggable != null && debuggable.equals("1")) { //$NON-NLS-1$ - return "Yes"; - } else { - return ""; - } - case 4: - return getStateString(device); - } - } - - return null; - } - - @Override - public void addListener(ILabelProviderListener listener) { - // pass - } - - @Override - public void dispose() { - // pass - } - - @Override - public boolean isLabelProperty(Object element, String property) { - // pass - return false; - } - - @Override - public void removeListener(ILabelProviderListener listener) { - // pass - } - } - - public static class DeviceChooserResponse { - private AvdInfo mAvdToLaunch; - private IDevice mDeviceToUse; - private boolean mUseDeviceForFutureLaunches; - - public void setDeviceToUse(IDevice d) { - mDeviceToUse = d; - mAvdToLaunch = null; - } - - public void setAvdToLaunch(AvdInfo avd) { - mAvdToLaunch = avd; - mDeviceToUse = null; - } - - public IDevice getDeviceToUse() { - return mDeviceToUse; - } - - public AvdInfo getAvdToLaunch() { - return mAvdToLaunch; - } - - public void setUseDeviceForFutureLaunches(boolean en) { - mUseDeviceForFutureLaunches = en; - } - - public boolean useDeviceForFutureLaunches() { - return mUseDeviceForFutureLaunches; - } - } - - public DeviceChooserDialog(Shell parent, DeviceChooserResponse response, String packageName, - IAndroidTarget projectTarget, AndroidVersion minApiVersion, - boolean useDeviceForFutureLaunches) { - super(parent); - - mResponse = response; - mPackageName = packageName; - mProjectTarget = projectTarget; - mMinApiVersion = minApiVersion; - mSdk = Sdk.getCurrent(); - mUseDeviceForFutureLaunches = useDeviceForFutureLaunches; - - AndroidDebugBridge.addDeviceChangeListener(this); - loadImages(); - } - - private void cleanup() { - // done listening. - AndroidDebugBridge.removeDeviceChangeListener(this); - } - - @Override - protected void okPressed() { - cleanup(); - super.okPressed(); - } - - @Override - protected void cancelPressed() { - cleanup(); - super.cancelPressed(); - } - - @Override - protected Control createContents(Composite parent) { - Control content = super.createContents(parent); - - // this must be called after createContents() has happened so that the - // ok button has been created (it's created after the call to createDialogArea) - updateDefaultSelection(); - - return content; - } - - /** - * Create the button bar: We override the Dialog implementation of this method - * so that we can create the checkbox at the same level as the 'Cancel' and 'OK' buttons. - */ - @Override - protected Control createButtonBar(Composite parent) { - Composite composite = new Composite(parent, SWT.NONE); - - GridLayout layout = new GridLayout(1, false); - layout.marginHeight = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_MARGIN); - composite.setLayout(layout); - composite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - - mUseDeviceForFutureLaunchesCheckbox = new Button(composite, SWT.CHECK); - mUseDeviceForFutureLaunchesCheckbox.setSelection(mUseDeviceForFutureLaunches); - mResponse.setUseDeviceForFutureLaunches(mUseDeviceForFutureLaunches); - mUseDeviceForFutureLaunchesCheckbox.setText("Use same device for future launches"); - mUseDeviceForFutureLaunchesCheckbox.addSelectionListener(new SelectionAdapter() { - @Override - public void widgetSelected(SelectionEvent e) { - mUseDeviceForFutureLaunches = - mUseDeviceForFutureLaunchesCheckbox.getSelection(); - mResponse.setUseDeviceForFutureLaunches(mUseDeviceForFutureLaunches); - } - }); - mUseDeviceForFutureLaunchesCheckbox.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - - createButton(composite, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL, true); - createButton(composite, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false); - - return composite; - } - - @Override - protected Control createDialogArea(Composite parent) { - // set dialog title - getShell().setText("Android Device Chooser"); - - Composite top = new Composite(parent, SWT.NONE); - top.setLayout(new GridLayout(1, true)); - - String msg; - if (mProjectTarget.isPlatform()) { - msg = String.format("Select a device with min API level %s.", - mMinApiVersion.getApiString()); - } else { - msg = String.format("Select a device compatible with target %s.", - mProjectTarget.getFullName()); - } - Label label = new Label(top, SWT.NONE); - label.setText(msg); - - mDeviceRadioButton = new Button(top, SWT.RADIO); - mDeviceRadioButton.setText("Choose a running Android device"); - mDeviceRadioButton.addSelectionListener(new SelectionAdapter() { - @Override - public void widgetSelected(SelectionEvent e) { - boolean deviceMode = mDeviceRadioButton.getSelection(); - - mDeviceTable.setEnabled(deviceMode); - mPreferredAvdSelector.setEnabled(!deviceMode); - - if (deviceMode) { - handleDeviceSelection(); - } else { - mResponse.setAvdToLaunch(mPreferredAvdSelector.getSelected()); - } - - enableOkButton(); - } - }); - mDeviceRadioButton.setSelection(true); - - - // offset the selector from the radio button - Composite offsetComp = new Composite(top, SWT.NONE); - offsetComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - GridLayout layout = new GridLayout(1, false); - layout.marginRight = layout.marginHeight = 0; - layout.marginLeft = 30; - offsetComp.setLayout(layout); - - mDeviceTable = new Table(offsetComp, SWT.SINGLE | SWT.FULL_SELECTION | SWT.BORDER); - GridData gd; - mDeviceTable.setLayoutData(gd = new GridData(GridData.FILL_BOTH)); - gd.heightHint = 100; - - mDeviceTable.setHeaderVisible(true); - mDeviceTable.setLinesVisible(true); - - TableHelper.createTableColumn(mDeviceTable, "Serial Number", - SWT.LEFT, "AAA+AAAAAAAAAAAAAAAAAAA", //$NON-NLS-1$ - null /* prefs name */, null /* prefs store */); - - TableHelper.createTableColumn(mDeviceTable, "AVD Name", - SWT.LEFT, "AAAAAAAAAAAAAAAAAAA", //$NON-NLS-1$ - null /* prefs name */, null /* prefs store */); - - TableHelper.createTableColumn(mDeviceTable, "Target", - SWT.LEFT, "AAA+Android 9.9.9", //$NON-NLS-1$ - null /* prefs name */, null /* prefs store */); - - TableHelper.createTableColumn(mDeviceTable, "Debug", - SWT.LEFT, "Debug", //$NON-NLS-1$ - null /* prefs name */, null /* prefs store */); - - TableHelper.createTableColumn(mDeviceTable, "State", - SWT.LEFT, "bootloader", //$NON-NLS-1$ - null /* prefs name */, null /* prefs store */); - - // create the viewer for it - mViewer = new TableViewer(mDeviceTable); - mViewer.setContentProvider(new ContentProvider()); - mViewer.setLabelProvider(new LabelProvider()); - mViewer.setInput(AndroidDebugBridge.getBridge()); - - mDeviceTable.addSelectionListener(new SelectionAdapter() { - /** - * Handles single-click selection on the device selector. - * {@inheritDoc} - */ - @Override - public void widgetSelected(SelectionEvent e) { - handleDeviceSelection(); - } - - /** - * Handles double-click selection on the device selector. - * Note that the single-click handler will probably already have been called. - * {@inheritDoc} - */ - @Override - public void widgetDefaultSelected(SelectionEvent e) { - handleDeviceSelection(); - if (isOkButtonEnabled()) { - okPressed(); - } - } - }); - - Button radio2 = new Button(top, SWT.RADIO); - radio2.setText("Launch a new Android Virtual Device"); - - // offset the selector from the radio button - offsetComp = new Composite(top, SWT.NONE); - offsetComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); - layout = new GridLayout(1, false); - layout.marginRight = layout.marginHeight = 0; - layout.marginLeft = 30; - offsetComp.setLayout(layout); - - mPreferredAvdSelector = new AvdSelector(offsetComp, - mSdk.getSdkOsLocation(), - mSdk.getAvdManager(), - new NonRunningAvdFilter(), - DisplayMode.SIMPLE_SELECTION, - new AdtConsoleSdkLog()); - mPreferredAvdSelector.setTableHeightHint(100); - mPreferredAvdSelector.setEnabled(false); - mPreferredAvdSelector.setSelectionListener(new SelectionAdapter() { - /** - * Handles single-click selection on the AVD selector. - * {@inheritDoc} - */ - @Override - public void widgetSelected(SelectionEvent e) { - if (mDisableAvdSelectionChange == false) { - mResponse.setAvdToLaunch(mPreferredAvdSelector.getSelected()); - enableOkButton(); - } - } - - /** - * Handles double-click selection on the AVD selector. - * - * Note that the single-click handler will probably already have been called - * but the selected item can have changed in between. - * - * {@inheritDoc} - */ - @Override - public void widgetDefaultSelected(SelectionEvent e) { - widgetSelected(e); - if (isOkButtonEnabled()) { - okPressed(); - } - } - }); - - return top; - } - - private void loadImages() { - ImageLoader ddmUiLibLoader = ImageLoader.getDdmUiLibLoader(); - Display display = DdmsPlugin.getDisplay(); - IconFactory factory = IconFactory.getInstance(); - - if (mDeviceImage == null) { - mDeviceImage = ddmUiLibLoader.loadImage(display, - "device.png", //$NON-NLS-1$ - ICON_WIDTH, ICON_WIDTH, - display.getSystemColor(SWT.COLOR_RED)); - } - if (mEmulatorImage == null) { - mEmulatorImage = ddmUiLibLoader.loadImage(display, - "emulator.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$ - display.getSystemColor(SWT.COLOR_BLUE)); - } - - if (mMatchImage == null) { - mMatchImage = factory.getIcon("match", //$NON-NLS-1$ - IconFactory.COLOR_GREEN, - IconFactory.SHAPE_DEFAULT); - } - - if (mNoMatchImage == null) { - mNoMatchImage = factory.getIcon("error", //$NON-NLS-1$ - IconFactory.COLOR_RED, - IconFactory.SHAPE_DEFAULT); - } - - if (mWarningImage == null) { - mWarningImage = factory.getIcon("warning", //$NON-NLS-1$ - SWT.COLOR_YELLOW, - IconFactory.SHAPE_DEFAULT); - } - - } - - /** - * 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 "??"; - } - - /** - * 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) - */ - @Override - public void deviceConnected(IDevice device) { - final DeviceChooserDialog dialog = this; - exec(new Runnable() { - @Override - public void run() { - if (mDeviceTable.isDisposed() == false) { - // refresh all - mViewer.refresh(); - - // update the selection - updateDefaultSelection(); - - // update the display of AvdInfo (since it's filtered to only display - // non running AVD.) - refillAvdList(false /*reloadAvds*/); - } else { - // table is disposed, we need to do something. - // lets remove ourselves from the listener. - AndroidDebugBridge.removeDeviceChangeListener(dialog); - } - - } - }); - } - - /** - * 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) - */ - @Override - public void deviceDisconnected(IDevice device) { - deviceConnected(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, int) - */ - @Override - public void deviceChanged(final IDevice device, int changeMask) { - if ((changeMask & (IDevice.CHANGE_STATE | IDevice.CHANGE_BUILD_INFO)) != 0) { - final DeviceChooserDialog dialog = this; - exec(new Runnable() { - @Override - public void run() { - if (mDeviceTable.isDisposed() == false) { - // refresh the device - mViewer.refresh(device); - - // update the defaultSelection. - updateDefaultSelection(); - - // update the display of AvdInfo (since it's filtered to only display - // non running AVD). This is done on deviceChanged because the avd name - // of a (emulator) device may be updated as the emulator boots. - - refillAvdList(false /*reloadAvds*/); - - // if the changed device is the current selection, - // we update the OK button based on its state. - if (device == mResponse.getDeviceToUse()) { - enableOkButton(); - } - - } else { - // table is disposed, we need to do something. - // lets remove ourselves from the listener. - AndroidDebugBridge.removeDeviceChangeListener(dialog); - } - } - }); - } - } - - /** - * Returns whether the dialog is in "device" mode (true), or in "avd" mode (false). - */ - private boolean isDeviceMode() { - return mDeviceRadioButton.getSelection(); - } - - /** - * Enables or disables the OK button of the dialog based on various selections in the dialog. - */ - private void enableOkButton() { - Button okButton = getButton(IDialogConstants.OK_ID); - - if (isDeviceMode()) { - okButton.setEnabled(mResponse.getDeviceToUse() != null && - mResponse.getDeviceToUse().isOnline()); - } else { - okButton.setEnabled(mResponse.getAvdToLaunch() != null); - } - } - - /** - * Returns true if the ok button is enabled. - */ - private boolean isOkButtonEnabled() { - Button okButton = getButton(IDialogConstants.OK_ID); - return okButton.isEnabled(); - } - - /** - * Executes the {@link Runnable} in the UI thread. - * @param runnable the runnable to execute. - */ - private void exec(Runnable runnable) { - try { - Display display = mDeviceTable.getDisplay(); - display.asyncExec(runnable); - } catch (SWTException e) { - // tree is disposed, we need to do something. lets remove ourselves from the listener. - AndroidDebugBridge.removeDeviceChangeListener(this); - } - } - - private void handleDeviceSelection() { - int count = mDeviceTable.getSelectionCount(); - if (count != 1) { - handleSelection(null); - } else { - int index = mDeviceTable.getSelectionIndex(); - Object data = mViewer.getElementAt(index); - if (data instanceof IDevice) { - handleSelection((IDevice)data); - } else { - handleSelection(null); - } - } - } - - private void handleSelection(IDevice device) { - mResponse.setDeviceToUse(device); - enableOkButton(); - } - - /** - * Look for a default device to select. This is done by looking for the running - * clients on each device and finding one similar to the one being launched. - *

- * This is done every time the device list changed unless there is a already selection. - */ - private void updateDefaultSelection() { - if (mDeviceTable.getSelectionCount() == 0) { - AndroidDebugBridge bridge = AndroidDebugBridge.getBridge(); - - IDevice[] devices = bridge.getDevices(); - - for (IDevice device : devices) { - Client[] clients = device.getClients(); - - for (Client client : clients) { - - if (mPackageName.equals(client.getClientData().getClientDescription())) { - // found a match! Select it. - mViewer.setSelection(new StructuredSelection(device)); - handleSelection(device); - - // and we're done. - return; - } - } - } - } - - handleDeviceSelection(); - } - - private final class NonRunningAvdFilter implements IAvdFilter { - - private IDevice[] mDevices; - - @Override - public void prepare() { - mDevices = AndroidDebugBridge.getBridge().getDevices(); - } - - @Override - public boolean accept(AvdInfo avd) { - if (mDevices != null) { - for (IDevice d : mDevices) { - // do not accept running avd's - if (avd.getName().equals(d.getAvdName())) { - return false; - } - - // only accept avd's that can actually run the project - AvdCompatibility.Compatibility c = - AvdCompatibility.canRun(avd, mProjectTarget, mMinApiVersion); - return (c == AvdCompatibility.Compatibility.NO) ? false : true; - } - } - - return true; - } - - @Override - public void cleanup() { - mDevices = null; - } - } - - /** - * Refills the AVD list keeping the current selection. - */ - private void refillAvdList(boolean reloadAvds) { - // save the current selection - AvdInfo selected = mPreferredAvdSelector.getSelected(); - - // disable selection change. - mDisableAvdSelectionChange = true; - - // refresh the list - mPreferredAvdSelector.refresh(false); - - // attempt to reselect the proper avd if needed - if (selected != null) { - if (mPreferredAvdSelector.setSelection(selected) == false) { - // looks like the selection is lost. this can happen if an emulator - // running the AVD that was selected was launched from outside of Eclipse). - mResponse.setAvdToLaunch(null); - enableOkButton(); - } - } - - // enable the selection change - mDisableAvdSelectionChange = false; - } -} - +/* + * 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.sdkuilib.ui; + +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; +import com.android.ddmlib.Client; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.IDevice.DeviceState; +import com.android.ddmuilib.TableHelper; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.internal.avd.AvdInfo; +import com.android.sdklib.repository.targets.SystemImage; +import com.android.sdkuilib.internal.repository.avd.AvdAgent; +import com.android.sdkuilib.internal.repository.avd.SdkTargets; +import com.android.sdkuilib.internal.widgets.AvdSelector; +import com.android.sdkuilib.internal.widgets.AvdSelector.IAvdFilter; + +import org.eclipse.andmore.base.resources.IEditorIconFactory; +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.andmore.sdktool.SdkCallAgent; +import org.eclipse.andmore.sdktool.SdkContext; +import org.eclipse.andmore.sdktool.Utilities; +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.viewers.ILabelProviderListener; +import org.eclipse.jface.viewers.IStructuredContentProvider; +import org.eclipse.jface.viewers.ITableLabelProvider; +import org.eclipse.jface.viewers.StructuredSelection; +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.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.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Table; + +import java.util.ArrayList; +import java.util.List; + +/** + * A dialog that lets the user choose a device to deploy an application. + * The user can either choose an exiting running device (including running emulators) + * or start a new emulator using an Android Virtual Device configuration that matches + * the current project. + */ +public class DeviceChooserDialog extends Dialog implements IDeviceChangeListener { + + private Table mDeviceTable; + private TableViewer mViewer; + private AvdSelector mPreferredAvdSelector; + + private Image mDeviceImage; + private Image mEmulatorImage; + private Image mMatchImage; + private Image mNoMatchImage; + private Image mWarningImage; + + private final DeviceChooserResponse mResponse; + private final String mPackageName; + private final IAndroidTarget mProjectTarget; + private final AndroidVersion mMinApiVersion; + private final SdkContext mSdkContext; + private final SdkTargets mSdkTargets; + private final IEditorIconFactory mIconFactory; + + private Button mDeviceRadioButton; + private Button mUseDeviceForFutureLaunchesCheckbox; + private boolean mUseDeviceForFutureLaunches; + + private boolean mDisableAvdSelectionChange = false; + + /** + * Basic Content Provider for a table full of {@link IDevice} objects. The input is + * a {@link AndroidDebugBridge}. + */ + private class ContentProvider implements IStructuredContentProvider { + @Override + public Object[] getElements(Object inputElement) { + if (inputElement instanceof AndroidDebugBridge) { + return findCompatibleDevices(((AndroidDebugBridge)inputElement).getDevices()); + } + + return new Object[0]; + } + + private Object[] findCompatibleDevices(IDevice[] devices) { + if (devices == null) { + return null; + } + + List compatibleDevices = new ArrayList(devices.length); + for (IDevice device : devices) { + AndroidVersion deviceVersion = Utilities.getDeviceVersion(device); + if (deviceVersion == null || deviceVersion.canRun(mMinApiVersion)) { + compatibleDevices.add(device); + } + } + + return compatibleDevices.toArray(); + } + + @Override + public void dispose() { + // pass + } + + @Override + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + // pass + } + } + + /** + * A Label Provider for the {@link TableViewer} in {@link DeviceChooserDialog}. + * It provides labels and images for {@link IDevice} objects. + */ + private class LabelProvider implements ITableLabelProvider { + + @Override + public Image getColumnImage(Object element, int columnIndex) { + if (element instanceof IDevice) { + IDevice device = (IDevice)element; + switch (columnIndex) { + case 0: + return device.isEmulator() ? mEmulatorImage : mDeviceImage; + + case 2: + // check for compatibility. + if (device.isEmulator() == false) { // physical device + // get the version of the device + AndroidVersion deviceVersion = Utilities.getDeviceVersion(device); + if (deviceVersion == null) { + return mWarningImage; + } else { + if (!deviceVersion.canRun(mMinApiVersion)) { + return mNoMatchImage; + } + + // if the project is compiling against an add-on, + // the optional API may be missing from the device. + return mProjectTarget.isPlatform() ? + mMatchImage : mWarningImage; + } + } else { + // get the AvdInfo + AvdInfo info = mSdkContext.getAvdManager().getAvd(device.getAvdName(), + true /*validAvdOnly*/); + if (info == null) + return mWarningImage; + IAndroidTarget avdTarget = getAndroidTargetFor(info); + Utilities.Compatibility c = + Utilities.canRun(info, mProjectTarget, avdTarget, mMinApiVersion); + switch (c) { + case YES: + return mMatchImage; + case NO: + return mNoMatchImage; + case UNKNOWN: + return mWarningImage; + } + } + } + } + + return null; + } + + @Override + public String getColumnText(Object element, int columnIndex) { + if (element instanceof IDevice) { + IDevice device = (IDevice)element; + switch (columnIndex) { + case 0: + return device.getName(); + case 1: + if (device.isEmulator()) { + return device.getAvdName(); + } else { + return "N/A"; // devices don't have AVD names. + } + case 2: + if (device.isEmulator()) { + AvdInfo info = mSdkContext.getAvdManager().getAvd(device.getAvdName(), + true /*validAvdOnly*/); + if (info == null) { + return "?"; + } + return getAndroidTargetFor(info).getFullName(); + } else { + String deviceBuild = device.getProperty(IDevice.PROP_BUILD_VERSION); + if (deviceBuild == null) { + return "unknown"; + } + return deviceBuild; + } + case 3: + String debuggable = device.getProperty(IDevice.PROP_DEBUGGABLE); + if (debuggable != null && debuggable.equals("1")) { //$NON-NLS-1$ + return "Yes"; + } else { + return ""; + } + case 4: + return getStateString(device); + } + } + + return null; + } + + @Override + public void addListener(ILabelProviderListener listener) { + // pass + } + + @Override + public void dispose() { + // pass + } + + @Override + public boolean isLabelProperty(Object element, String property) { + // pass + return false; + } + + @Override + public void removeListener(ILabelProviderListener listener) { + // pass + } + } + + public static class DeviceChooserResponse { + private AvdInfo mAvdToLaunch; + private IDevice mDeviceToUse; + private boolean mUseDeviceForFutureLaunches; + + public void setDeviceToUse(IDevice d) { + mDeviceToUse = d; + mAvdToLaunch = null; + } + + public void setAvdToLaunch(AvdInfo avd) { + mAvdToLaunch = avd; + mDeviceToUse = null; + } + + public IDevice getDeviceToUse() { + return mDeviceToUse; + } + + public AvdInfo getAvdToLaunch() { + return mAvdToLaunch; + } + + public void setUseDeviceForFutureLaunches(boolean en) { + mUseDeviceForFutureLaunches = en; + } + + public boolean useDeviceForFutureLaunches() { + return mUseDeviceForFutureLaunches; + } + } + + public DeviceChooserDialog(Shell parent, SdkCallAgent sdkCallAgent, DeviceChooserResponse response, String packageName, + IAndroidTarget projectTarget, AndroidVersion minApiVersion, + boolean useDeviceForFutureLaunches) { + super(parent); + + mSdkContext = sdkCallAgent.getSdkContext(); + mSdkTargets = new SdkTargets(mSdkContext); + mIconFactory = sdkCallAgent.getEditorIconFactory(); + mResponse = response; + mPackageName = packageName; + mProjectTarget = projectTarget; + mMinApiVersion = minApiVersion; + mUseDeviceForFutureLaunches = useDeviceForFutureLaunches; + + AndroidDebugBridge.addDeviceChangeListener(this); + loadImages(parent.getDisplay()); + } + + private void cleanup() { + // done listening. + AndroidDebugBridge.removeDeviceChangeListener(this); + } + + @Override + protected void okPressed() { + cleanup(); + super.okPressed(); + } + + @Override + protected void cancelPressed() { + cleanup(); + super.cancelPressed(); + } + + @Override + protected Control createContents(Composite parent) { + Control content = super.createContents(parent); + + // this must be called after createContents() has happened so that the + // ok button has been created (it's created after the call to createDialogArea) + updateDefaultSelection(); + + return content; + } + + /** + * Create the button bar: We override the Dialog implementation of this method + * so that we can create the checkbox at the same level as the 'Cancel' and 'OK' buttons. + */ + @Override + protected Control createButtonBar(Composite parent) { + Composite composite = new Composite(parent, SWT.NONE); + + GridLayout layout = new GridLayout(1, false); + layout.marginHeight = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_MARGIN); + composite.setLayout(layout); + composite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + mUseDeviceForFutureLaunchesCheckbox = new Button(composite, SWT.CHECK); + mUseDeviceForFutureLaunchesCheckbox.setSelection(mUseDeviceForFutureLaunches); + mResponse.setUseDeviceForFutureLaunches(mUseDeviceForFutureLaunches); + mUseDeviceForFutureLaunchesCheckbox.setText("Use same device for future launches"); + mUseDeviceForFutureLaunchesCheckbox.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + mUseDeviceForFutureLaunches = + mUseDeviceForFutureLaunchesCheckbox.getSelection(); + mResponse.setUseDeviceForFutureLaunches(mUseDeviceForFutureLaunches); + } + }); + mUseDeviceForFutureLaunchesCheckbox.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + + createButton(composite, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL, true); + createButton(composite, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false); + + return composite; + } + + @Override + protected Control createDialogArea(Composite parent) { + // set dialog title + getShell().setText("Android Device Chooser"); + + Composite top = new Composite(parent, SWT.NONE); + top.setLayout(new GridLayout(1, true)); + + String msg; + if (mProjectTarget.isPlatform()) { + msg = String.format("Select a device with min API level %s.", + mMinApiVersion.getApiString()); + } else { + msg = String.format("Select a device compatible with target %s.", + mProjectTarget.getFullName()); + } + Label label = new Label(top, SWT.NONE); + label.setText(msg); + + mDeviceRadioButton = new Button(top, SWT.RADIO); + mDeviceRadioButton.setText("Choose a running Android device"); + mDeviceRadioButton.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + boolean deviceMode = mDeviceRadioButton.getSelection(); + + mDeviceTable.setEnabled(deviceMode); + mPreferredAvdSelector.setEnabled(!deviceMode); + + if (deviceMode) { + handleDeviceSelection(); + } else { + mResponse.setAvdToLaunch(mPreferredAvdSelector.getSelected().getAvd()); + } + + enableOkButton(); + } + }); + mDeviceRadioButton.setSelection(true); + + + // offset the selector from the radio button + Composite offsetComp = new Composite(top, SWT.NONE); + offsetComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + GridLayout layout = new GridLayout(1, false); + layout.marginRight = layout.marginHeight = 0; + layout.marginLeft = 30; + offsetComp.setLayout(layout); + + mDeviceTable = new Table(offsetComp, SWT.SINGLE | SWT.FULL_SELECTION | SWT.BORDER); + GridData gd; + mDeviceTable.setLayoutData(gd = new GridData(GridData.FILL_BOTH)); + gd.heightHint = 100; + + mDeviceTable.setHeaderVisible(true); + mDeviceTable.setLinesVisible(true); + + TableHelper.createTableColumn(mDeviceTable, "Serial Number", + SWT.LEFT, "AAA+AAAAAAAAAAAAAAAAAAA", //$NON-NLS-1$ + null /* prefs name */, null /* prefs store */); + + TableHelper.createTableColumn(mDeviceTable, "AVD Name", + SWT.LEFT, "AAAAAAAAAAAAAAAAAAA", //$NON-NLS-1$ + null /* prefs name */, null /* prefs store */); + + TableHelper.createTableColumn(mDeviceTable, "Target", + SWT.LEFT, "AAA+Android 9.9.9", //$NON-NLS-1$ + null /* prefs name */, null /* prefs store */); + + TableHelper.createTableColumn(mDeviceTable, "Debug", + SWT.LEFT, "Debug", //$NON-NLS-1$ + null /* prefs name */, null /* prefs store */); + + TableHelper.createTableColumn(mDeviceTable, "State", + SWT.LEFT, "bootloader", //$NON-NLS-1$ + null /* prefs name */, null /* prefs store */); + + // create the viewer for it + mViewer = new TableViewer(mDeviceTable); + mViewer.setContentProvider(new ContentProvider()); + mViewer.setLabelProvider(new LabelProvider()); + mViewer.setInput(AndroidDebugBridge.getBridge()); + + mDeviceTable.addSelectionListener(new SelectionAdapter() { + /** + * Handles single-click selection on the device selector. + * {@inheritDoc} + */ + @Override + public void widgetSelected(SelectionEvent e) { + handleDeviceSelection(); + } + + /** + * Handles double-click selection on the device selector. + * Note that the single-click handler will probably already have been called. + * {@inheritDoc} + */ + @Override + public void widgetDefaultSelected(SelectionEvent e) { + handleDeviceSelection(); + if (isOkButtonEnabled()) { + okPressed(); + } + } + }); + + Button radio2 = new Button(top, SWT.RADIO); + radio2.setText("Launch a new Android Virtual Device"); + + // offset the selector from the radio button + offsetComp = new Composite(top, SWT.NONE); + offsetComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL)); + layout = new GridLayout(1, false); + layout.marginRight = layout.marginHeight = 0; + layout.marginLeft = 30; + offsetComp.setLayout(layout); + + mPreferredAvdSelector = new AvdSelector(offsetComp, + mSdkContext, + new NonRunningAvdFilter(), + AvdDisplayMode.SIMPLE_SELECTION); + mPreferredAvdSelector.setTableHeightHint(100); + mPreferredAvdSelector.setEnabled(false); + mPreferredAvdSelector.setSelectionListener(new SelectionAdapter() { + /** + * Handles single-click selection on the AVD selector. + * {@inheritDoc} + */ + @Override + public void widgetSelected(SelectionEvent e) { + if (mDisableAvdSelectionChange == false) { + mResponse.setAvdToLaunch(mPreferredAvdSelector.getSelected().getAvd()); + enableOkButton(); + } + } + + /** + * Handles double-click selection on the AVD selector. + * + * Note that the single-click handler will probably already have been called + * but the selected item can have changed in between. + * + * {@inheritDoc} + */ + @Override + public void widgetDefaultSelected(SelectionEvent e) { + widgetSelected(e); + if (isOkButtonEnabled()) { + okPressed(); + } + } + }); + + return top; + } + + private void loadImages(Display display) { + ImageFactory imageFactory = mSdkContext.getSdkHelper().getImageFactory(); + + if (mDeviceImage == null) { + mDeviceImage = imageFactory.getImageByName("device.png"); //$NON-NLS-1$ + } + if (mEmulatorImage == null) { + mEmulatorImage = imageFactory.getImageByName("emulator.png"); //$NON-NLS-1$ + } + + if (mMatchImage == null) { + mMatchImage = mIconFactory.getColorIcon("match", //$NON-NLS-1$ + SWT.COLOR_DARK_GREEN); + } + + if (mNoMatchImage == null) { + mNoMatchImage = mIconFactory.getColorIcon("error", //$NON-NLS-1$ + SWT.COLOR_DARK_RED); + } + + if (mWarningImage == null) { + mWarningImage = mIconFactory.getColorIcon("warning", //$NON-NLS-1$ + SWT.COLOR_YELLOW); + } + + } + + /** + * 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 "??"; + } + + /** + * 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) + */ + @Override + public void deviceConnected(IDevice device) { + final DeviceChooserDialog dialog = this; + exec(new Runnable() { + @Override + public void run() { + if (mDeviceTable.isDisposed() == false) { + // refresh all + mViewer.refresh(); + + // update the selection + updateDefaultSelection(); + + // update the display of AvdInfo (since it's filtered to only display + // non running AVD.) + refillAvdList(false /*reloadAvds*/); + } else { + // table is disposed, we need to do something. + // lets remove ourselves from the listener. + AndroidDebugBridge.removeDeviceChangeListener(dialog); + } + + } + }); + } + + /** + * 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) + */ + @Override + public void deviceDisconnected(IDevice device) { + deviceConnected(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, int) + */ + @Override + public void deviceChanged(final IDevice device, int changeMask) { + if ((changeMask & (IDevice.CHANGE_STATE | IDevice.CHANGE_BUILD_INFO)) != 0) { + final DeviceChooserDialog dialog = this; + exec(new Runnable() { + @Override + public void run() { + if (mDeviceTable.isDisposed() == false) { + // refresh the device + mViewer.refresh(device); + + // update the defaultSelection. + updateDefaultSelection(); + + // update the display of AvdInfo (since it's filtered to only display + // non running AVD). This is done on deviceChanged because the avd name + // of a (emulator) device may be updated as the emulator boots. + + refillAvdList(false /*reloadAvds*/); + + // if the changed device is the current selection, + // we update the OK button based on its state. + if (device == mResponse.getDeviceToUse()) { + enableOkButton(); + } + + } else { + // table is disposed, we need to do something. + // lets remove ourselves from the listener. + AndroidDebugBridge.removeDeviceChangeListener(dialog); + } + } + }); + } + } + + /** + * Returns whether the dialog is in "device" mode (true), or in "avd" mode (false). + */ + private boolean isDeviceMode() { + return mDeviceRadioButton.getSelection(); + } + + /** + * Enables or disables the OK button of the dialog based on various selections in the dialog. + */ + private void enableOkButton() { + Button okButton = getButton(IDialogConstants.OK_ID); + + if (isDeviceMode()) { + okButton.setEnabled(mResponse.getDeviceToUse() != null && + mResponse.getDeviceToUse().isOnline()); + } else { + okButton.setEnabled(mResponse.getAvdToLaunch() != null); + } + } + + /** + * Returns true if the ok button is enabled. + */ + private boolean isOkButtonEnabled() { + Button okButton = getButton(IDialogConstants.OK_ID); + return okButton.isEnabled(); + } + + /** + * Executes the {@link Runnable} in the UI thread. + * @param runnable the runnable to execute. + */ + private void exec(Runnable runnable) { + try { + Display display = mDeviceTable.getDisplay(); + display.asyncExec(runnable); + } catch (SWTException e) { + // tree is disposed, we need to do something. lets remove ourselves from the listener. + AndroidDebugBridge.removeDeviceChangeListener(this); + } + } + + private void handleDeviceSelection() { + int count = mDeviceTable.getSelectionCount(); + if (count != 1) { + handleSelection(null); + } else { + int index = mDeviceTable.getSelectionIndex(); + Object data = mViewer.getElementAt(index); + if (data instanceof IDevice) { + handleSelection((IDevice)data); + } else { + handleSelection(null); + } + } + } + + private void handleSelection(IDevice device) { + mResponse.setDeviceToUse(device); + enableOkButton(); + } + + /** + * Look for a default device to select. This is done by looking for the running + * clients on each device and finding one similar to the one being launched. + *

+ * This is done every time the device list changed unless there is a already selection. + */ + private void updateDefaultSelection() { + if (mDeviceTable.getSelectionCount() == 0) { + AndroidDebugBridge bridge = AndroidDebugBridge.getBridge(); + + IDevice[] devices = bridge.getDevices(); + + for (IDevice device : devices) { + Client[] clients = device.getClients(); + + for (Client client : clients) { + + if (mPackageName.equals(client.getClientData().getClientDescription())) { + // found a match! Select it. + mViewer.setSelection(new StructuredSelection(device)); + handleSelection(device); + + // and we're done. + return; + } + } + } + } + + handleDeviceSelection(); + } + + private final class NonRunningAvdFilter implements IAvdFilter { + + private IDevice[] mDevices; + + @Override + public void prepare() { + mDevices = AndroidDebugBridge.getBridge().getDevices(); + } + + @Override + public boolean accept(AvdAgent avdAgent) { + AvdInfo info = avdAgent.getAvd(); + if (mDevices != null) { + for (IDevice d : mDevices) { + // do not accept running avd's + if (info.getName().equals(d.getAvdName())) { + return false; + } + + // only accept avd's that can actually run the project + Utilities.Compatibility c = + Utilities.canRun(avdAgent.getAvd(), getAndroidTargetFor(info), mProjectTarget, mMinApiVersion); + return (c == Utilities.Compatibility.NO) ? false : true; + } + } + + return true; + } + + @Override + public void cleanup() { + mDevices = null; + } + } + + /** + * Refills the AVD list keeping the current selection. + */ + private void refillAvdList(boolean reloadAvds) { + // save the current selection + AvdAgent selected = mPreferredAvdSelector.getSelected(); + + // disable selection change. + mDisableAvdSelectionChange = true; + + // refresh the list + mPreferredAvdSelector.refresh(false); + + // attempt to reselect the proper avd if needed + if (selected != null) { + if (mPreferredAvdSelector.setSelection(selected) == false) { + // looks like the selection is lost. this can happen if an emulator + // running the AVD that was selected was launched from outside of Eclipse). + mResponse.setAvdToLaunch(null); + enableOkButton(); + } + } + + // enable the selection change + mDisableAvdSelectionChange = false; + } + + private IAndroidTarget getAndroidTargetFor(AvdInfo info) { + return mSdkTargets.getTargetForSysImage((SystemImage)info.getSystemImage()); + } +} + diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/ResolutionChooserDialog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/ResolutionChooserDialog.java similarity index 95% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/ResolutionChooserDialog.java rename to andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/ResolutionChooserDialog.java index e5e1d6cf..a835014d 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/ResolutionChooserDialog.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/ui/ResolutionChooserDialog.java @@ -14,9 +14,7 @@ * limitations under the License. */ -package com.android.sdkuilib.internal.widgets; - -import com.android.sdkuilib.ui.GridDialog; +package com.android.sdkuilib.ui; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.swt.SWT; diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/MessageBoxLog.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/widgets/MessageBoxLog.java similarity index 95% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/MessageBoxLog.java rename to andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/widgets/MessageBoxLog.java index 68463ee0..e7b9d4c8 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/MessageBoxLog.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/widgets/MessageBoxLog.java @@ -14,9 +14,10 @@ * limitations under the License. */ -package com.android.sdkuilib.internal.widgets; +package com.android.sdkuilib.widgets; import com.android.annotations.NonNull; +import com.android.sdkuilib.internal.widgets.IMessageBoxLogger; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.swt.widgets.Display; diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/SdkTargetSelector.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/widgets/SdkTargetSelector.java similarity index 96% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/SdkTargetSelector.java rename to andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/widgets/SdkTargetSelector.java index bee160f6..5634cde5 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/internal/widgets/SdkTargetSelector.java +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdkuilib/widgets/SdkTargetSelector.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.sdkuilib.internal.widgets; +package com.android.sdkuilib.widgets; import com.android.SdkConstants; import com.android.sdklib.IAndroidTarget; diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkCallAgent.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkCallAgent.java new file mode 100644 index 00000000..bed8f6fc --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkCallAgent.java @@ -0,0 +1,96 @@ +/** + * + */ +package org.eclipse.andmore.sdktool; + +import org.eclipse.andmore.base.resources.IEditorIconFactory; +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.andmore.base.resources.JFaceImageLoader; +import org.eclipse.andmore.base.resources.PluginResourceProvider; +import org.eclipse.swt.graphics.Image; + +import com.android.repository.api.RepoManager; +import com.android.sdklib.repository.AndroidSdkHandler; +import com.android.utils.ILogger; + +/** + * @author andrew + * + */ +public class SdkCallAgent { + public static final int NO_TOOLS_MSG = 0; + public static final int TOOLS_MSG_UPDATED_FROM_ADT = 1; + public static final int TOOLS_MSG_UPDATED_FROM_SDKMAN = 2; + + private final SdkContext sdkContext; + private final ILogger consoleLogger; + private IEditorIconFactory iconEditorFactory; + + /** + * Construct SdkCallAgent object to mediate between application and UI layer + * @param sdkHandler SDK handler + * @param repoManager Repository manager + * @param consoleLogger Console logger to persist all messages + */ + public SdkCallAgent( + AndroidSdkHandler sdkHandler, + RepoManager repoManager, + ILogger consoleLogger) + { + this.sdkContext = new SdkContext(sdkHandler, repoManager); + sdkContext.setSdkLogger(consoleLogger); + this.consoleLogger = consoleLogger; + } + + /** + * Construct SdkCallAgent object to mediate between application and UI layer requiring an icon factory + * @param sdkHandler SDK handler + * @param repoManager Repository manager + * @param iconEditorFactory Icon factory to provide editor icons + * @param consoleLogger Console logger to persist all messages + */ + public SdkCallAgent( + AndroidSdkHandler sdkHandler, + RepoManager repoManager, + IEditorIconFactory iconEditorFactory, + ILogger consoleLogger) + { + this(sdkHandler, repoManager, consoleLogger); + this.iconEditorFactory = iconEditorFactory; + } + + public SdkContext getSdkContext() { + SdkHelper helper = sdkContext.getSdkHelper(); + if (helper.getImageFactory() == null) + helper.setImageFactory(getImageLoader(new SdkResourceProvider())); + return sdkContext; + } + + public IEditorIconFactory getEditorIconFactory() { + if (iconEditorFactory ==null) + // Icon factory not set. Do not throw exception, but handle gracefully. + return new IEditorIconFactory(){ + + @Override + public Image getColorIcon(String osName, int color) { + // Return generic image to avoid NPE + return sdkContext.getSdkHelper().getImageByName("nopkg_icon_16.png"); + }};// + return iconEditorFactory; + } + + public void dispose() + { + sdkContext.getSdkHelper().dispose(); + } + + /** + * Set image loader if not already set + */ + public ImageFactory getImageLoader(PluginResourceProvider provider) + { + JFaceImageLoader imageLoader = new JFaceImageLoader(provider); + imageLoader.setLogger(consoleLogger); + return imageLoader; + } +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkContext.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkContext.java new file mode 100644 index 00000000..ca3bbd0b --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkContext.java @@ -0,0 +1,208 @@ +package org.eclipse.andmore.sdktool; + +import java.io.File; +import java.util.ArrayList; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.prefs.AndroidLocation; +import com.android.repository.api.LocalPackage; +import com.android.repository.api.ProgressIndicator; +import com.android.repository.api.ProgressIndicatorAdapter; +import com.android.repository.api.RemotePackage; +import com.android.repository.api.RepoManager; +import com.android.repository.impl.meta.RepositoryPackages; +import com.android.repository.io.FileOp; +import com.android.sdklib.devices.DeviceManager; +import com.android.sdklib.internal.avd.AvdManager; +import com.android.sdklib.repository.AndroidSdkHandler; +import com.android.sdkuilib.internal.repository.PackageManager; +import com.android.sdkuilib.internal.repository.Settings; +import com.android.utils.ILogger; + +public class SdkContext { + + private final AndroidSdkHandler handler; + private final RepoManager repoManager; + private final DeviceManager deviceManager; + private final PackageManager packageManager; + private final SdkHelper sdkHelper; + private final AtomicBoolean hasWarning = new AtomicBoolean(); + private final AtomicBoolean hasError = new AtomicBoolean(); + private final ArrayList logMessages = new ArrayList(); + private Settings settings; + private ProgressIndicator sdkProgressIndicator; + private ILogger sdkLogger; + + public SdkContext(AndroidSdkHandler handler, RepoManager repoManager) { + super(); + this.handler = handler; + this.repoManager = repoManager; + this.sdkHelper = new SdkHelper(); + deviceManager = DeviceManager.createInstance(handler.getLocation(), loggerInstance()); + packageManager = new PackageManager(this); + } + + public void setSettings(Settings settings) + { + this.settings = settings; + } + + public Settings getSettings() + { + if (settings == null) + settings = new Settings(); + return settings; + } + + public PackageManager getPackageManager() + { + return packageManager; + } + public SdkHelper getSdkHelper() + { + return sdkHelper; + } + + public AndroidSdkHandler getHandler() { + return handler; + } + + public RepoManager getRepoManager() { + return repoManager; + } + + public AvdManager getAvdManager() + { + String avdFolder = null; + AvdManager avdManager = null; + ILogger logger = loggerInstance(); + try { + avdFolder = AndroidLocation.getAvdFolder(); + avdManager = AvdManager.getInstance(handler, new File(avdFolder), logger); + } catch (AndroidLocation.AndroidLocationException e) { + logger.error(e, "Error obtaining AVD Manager"); + } + return avdManager; + } + + public RepositoryPackages getPackages() { + return repoManager.getPackages(); + } + + public FileOp getFileOp() { + return handler.getFileOp(); + } + + public File getLocalPath() { + return repoManager.getLocalPath(); + } + + public File getLocation() { + return handler.getLocation(); + } + + public Map getRemotePackages() { + return getPackages().getRemotePackages(); + } + + public Map getLocalPackages() { + return getPackages().getLocalPackages(); + } + + public DeviceManager getDeviceManager() { + return deviceManager; + } + + public boolean hasWarning() { + return hasWarning.get(); + } + + public boolean hasError() { + return hasError.get(); + } + + public ArrayList getLogMessages() { + return logMessages; + } + + public ILogger loggerInstance() { + hasWarning.set(false); + hasError.set(false); + logMessages.clear(); + return new ILogger() { + @Override + public void error(@Nullable Throwable throwable, @Nullable String errorFormat, + Object... arg) { + hasError.set(true); + if (errorFormat != null) { + logMessages.add(String.format("Error: " + errorFormat, arg)); + } + + if (throwable != null) { + logMessages.add(throwable.getMessage()); + } + } + + @Override + public void warning(@NonNull String warningFormat, Object... arg) { + hasWarning.set(true); + logMessages.add(String.format("Warning: " + warningFormat, arg)); + } + + @Override + public void info(@NonNull String msgFormat, Object... arg) { + logMessages.add(String.format(msgFormat, arg)); + } + + @Override + public void verbose(@NonNull String msgFormat, Object... arg) { + info(msgFormat, arg); + } + }; + + } + + public void setSdkProgressIndicator(ProgressIndicator sdkProgressIndicator) { + this.sdkProgressIndicator = sdkProgressIndicator; + } + + public void setSdkLogger(ILogger sdkLogger) { + this.sdkLogger = sdkLogger; + } + + public ProgressIndicator getProgressIndicator() { + if (sdkProgressIndicator != null) + return sdkProgressIndicator; + return new ProgressIndicatorAdapter() + { + ILogger logger = getSdkLog(); + @Override + public void logWarning(@NonNull String s, @Nullable Throwable e) { + if (s != null) + logger.warning(s); + if (e != null) + logger.warning(e.getMessage()); + } + + @Override + public void logError(@NonNull String s, @Nullable Throwable e) { + logger.error(e, s); + } + + @Override + public void logInfo(@NonNull String s) { + logger.info(s); + } + }; + } + + public ILogger getSdkLog() { + if (sdkLogger != null) + return sdkLogger; + return loggerInstance(); + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkHelper.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkHelper.java new file mode 100644 index 00000000..5a3e7565 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkHelper.java @@ -0,0 +1,108 @@ +package org.eclipse.andmore.sdktool; + +import java.util.ArrayList; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdkuilib.repository.ISdkChangeListener; +import com.android.utils.ILogger; + +public class SdkHelper { + private Shell mWindowShell; + + /** The current {@link ImageFactory}. */ + private ImageFactory mImageFactory; + + private final ArrayList mListeners = new ArrayList(); + + /** Adds a listener ({@link ISdkChangeListener}) that is notified when the SDK is reloaded. */ + public void addListeners(ISdkChangeListener listener) { + if (mListeners.contains(listener) == false) { + mListeners.add(listener); + } + } + + /** Removes a listener ({@link ISdkChangeListener}) that is notified when the SDK is reloaded. */ + public void removeListener(ISdkChangeListener listener) { + mListeners.remove(listener); + } + + /** + * Safely invoke all the registered {@link ISdkChangeListener#onSdkLoaded()}. + * This can be called from any thread. + */ + public void broadcastOnSdkLoaded(ILogger logger) { + if (!mListeners.isEmpty()) { + runOnUiThread(new Runnable() { + @Override + public void run() { + for (ISdkChangeListener listener : mListeners) { + try { + listener.onSdkLoaded(); + } catch (Throwable t) { + logger.error(t, null); + } + } + } + }); + } + } + + public void setWindowShell(Shell windowShell) { + mWindowShell = windowShell; + } + + public Shell getWindowShell() { + return mWindowShell; + } + + public void setImageFactory(ImageFactory imageFactory) { + mImageFactory = imageFactory; + } + + /** + * Returns image factory. + * @return ImageFactory object + */ + public ImageFactory getImageFactory() { + return mImageFactory; + } + + public void dispose() { + if (mImageFactory != null) + mImageFactory.dispose(); + } + /** + * Loads an image given its filename (with its extension). + * Might return null if the image cannot be loaded.
+ * The image is cached. Successive calls will return the same object.
+ * The image is automatically disposed when {@link ImageFactory} is disposed. + * + * @param imageName The filename (with extension) of the image to load. + * @return A new or existing {@link Image}. The caller must NOT dispose the image (the + * image will disposed by {@link #dispose()}). The returned image can be null if the + * expected file is missing. + */ + @Nullable + public Image getImageByName(@NonNull String imageName) { + return mImageFactory != null ? mImageFactory.getImageByName(imageName, imageName, null) : null; + } + + /** + * Runs the runnable on the UI thread using {@link Display#syncExec(Runnable)}. + * + * @param r Non-null runnable. + */ + protected void runOnUiThread(@NonNull Runnable r) { + if (mWindowShell != null && !mWindowShell.isDisposed()) { + mWindowShell.getDisplay().syncExec(r); + } + } + + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkResourceProvider.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkResourceProvider.java new file mode 100644 index 00000000..c1f49235 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkResourceProvider.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2017 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 org.eclipse.andmore.sdktool; + +import org.eclipse.andmore.base.resources.PluginResourceProvider; +import org.eclipse.jface.resource.ImageDescriptor; + +public class SdkResourceProvider implements PluginResourceProvider { + + @Override + public ImageDescriptor descriptorFromPath(String imagePath) { + return SdkUserInterfacePlugin.getImageDescriptor(imagePath); + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkUserInterfacePlugin.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkUserInterfacePlugin.java new file mode 100644 index 00000000..9ec6e6f6 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/SdkUserInterfacePlugin.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2017 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 org.eclipse.andmore.sdktool; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.ui.plugin.AbstractUIPlugin; +import org.osgi.framework.BundleContext; + +/** + * Plugin activator to manage resources such as images + * @author Andrew Bowley + * + */ +public class SdkUserInterfacePlugin extends AbstractUIPlugin { + + public static final String PLUGIN_ID = "org.eclipse.andmore.sdkuilib"; //$NON-NLS-1$ + + private static SdkUserInterfacePlugin instance; + + public SdkUserInterfacePlugin() { + super(); + instance = this; + } + + public static SdkUserInterfacePlugin instance() + { + return instance; + } + + /** + * Starts up this plug-in. + *

+ * This method should be overridden in subclasses that need to do something + * when this plug-in is started. Implementors should call the inherited method + * at the first possible point to ensure that any system requirements can be met. + *

+ *

+ * If this method throws an exception, it is taken as an indication that + * plug-in initialization has failed; as a result, the plug-in will not + * be activated; moreover, the plug-in will be marked as disabled and + * ineligible for activation for the duration. + *

+ *

+ * Note 1: This method is automatically invoked by the platform + * the first time any code in the plug-in is executed. + *

+ *

+ * Note 2: This method is intended to perform simple initialization + * of the plug-in environment. The platform may terminate initializers + * that do not complete in a timely fashion. + *

+ *

+ * Note 3: The class loader typically has monitors acquired during invocation of this method. It is + * strongly recommended that this method avoid synchronized blocks or other thread locking mechanisms, + * as this would lead to deadlock vulnerability. + *

+ *

+ * Note 4: The supplied bundle context represents the plug-in to the OSGi framework. + * For security reasons, it is strongly recommended that this object should not be divulged. + *

+ *

+ * Note 5: This method and the {@link #stop(BundleContext)} may be called from separate threads, + * but the OSGi framework ensures that both methods will not be called simultaneously. + *

+ * Clients must never explicitly call this method. + * + * @param context the bundle context for this plug-in + * @exception Exception if this plug-in did not start up properly + * @since 3.0 + */ + @Override + public void start(BundleContext context) throws Exception { + super.start(context); + } + + /** + * Stops this plug-in. + *

+ * This method should be re-implemented in subclasses that need to do something + * when the plug-in is shut down. Implementors should call the inherited method + * as late as possible to ensure that any system requirements can be met. + *

+ *

+ * Plug-in shutdown code should be robust. In particular, this method + * should always make an effort to shut down the plug-in. Furthermore, + * the code should not assume that the plug-in was started successfully. + *

+ *

+ * Note 1: If a plug-in has been automatically started, this method will be automatically + * invoked by the platform when the platform is shut down. + *

+ *

+ * Note 2: This method is intended to perform simple termination + * of the plug-in environment. The platform may terminate invocations + * that do not complete in a timely fashion. + *

+ *

+ * Note 3: The supplied bundle context represents the plug-in to the OSGi framework. + * For security reasons, it is strongly recommended that this object should not be divulged. + *

+ *

+ * Note 4: This method and the {@link #start(BundleContext)} may be called from separate threads, + * but the OSGi framework ensures that both methods will not be called simultaneously. + *

+ * Clients must never explicitly call this method. + * + * @param context the bundle context for this plug-in + * @exception Exception if this method fails to shut down this plug-in + * @since 3.0 + */ + @Override + public void stop(BundleContext context) throws Exception { + super.stop(context); + } + + /** + * Returns an image descriptor for the image file at the given plug-in + * relative path + */ + public static ImageDescriptor getImageDescriptor(String path) { + return imageDescriptorFromPlugin(PLUGIN_ID, path); + } + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/Utilities.java b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/Utilities.java new file mode 100644 index 00000000..b3e31e1b --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/org/eclipse/andmore/sdktool/Utilities.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2017 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 org.eclipse.andmore.sdktool; + +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ddmlib.IDevice; +import com.android.sdklib.AndroidVersion; +import com.android.sdklib.IAndroidTarget; +import com.android.sdklib.devices.Device; +import com.android.sdklib.internal.avd.AvdInfo; + +/** + * @author Andrew Bowley + * + * 11-11-2017 + */ +public class Utilities { + public enum Compatibility { + YES, + NO, + UNKNOWN, + }; + + /** + * Format file size given value as number of bytes. + * Taken from deprecated Archive class + * @param size Number of bytes + * @return text size formatted according to scale up to gigabytes + */ + public static String formatFileSize(long size) { + String sizeStr; + if (size < 1024) { + sizeStr = String.format("%d Bytes", size); + } else if (size < 1024 * 1024) { + sizeStr = String.format("%d KiB", Math.round(size / 1024.0)); + } else if (size < 1024 * 1024 * 1024) { + sizeStr = String.format("%.1f MiB", + Math.round(10.0 * size / (1024 * 1024.0))/ 10.0); + } else { + sizeStr = String.format("%.1f GiB", + Math.round(10.0 * size / (1024 * 1024 * 1024.0))/ 10.0); + } + + return String.format("Size: %1$s", sizeStr); + } + + + @Nullable + public static AndroidVersion getDeviceVersion(@NonNull IDevice device) { + try { + Future future = device.getSystemProperty(IDevice.PROP_BUILD_API_LEVEL); + String apiLevel = null; + apiLevel = future.get(); + if (apiLevel == null) { + return null; + } + future = device.getSystemProperty(IDevice.PROP_BUILD_CODENAME); + return new AndroidVersion(Integer.parseInt(apiLevel), + future.get()); + } catch (NumberFormatException | InterruptedException | ExecutionException e) { + return null; + } + } + + /** + * Returns whether the specified AVD can run the given project that is built against + * a particular SDK and has the specified minApiLevel. + * @param avd AVD to check compatibility for + * @param avdTarget AVD target + * @param projectTarget project build target + * @param minApiVersion project min api level + * @return whether the given AVD can run the given application + */ + public static Compatibility canRun(AvdInfo avd, IAndroidTarget avdTarget, IAndroidTarget projectTarget, + AndroidVersion minApiVersion) { + if (avd == null) { + return Compatibility.UNKNOWN; + } + + if (avdTarget == null) { + return Compatibility.UNKNOWN; + } + + // for platform targets, we only need to check the min api version + if (projectTarget.isPlatform()) { + return avdTarget.getVersion().canRun(minApiVersion) ? + Compatibility.YES : Compatibility.NO; + } + + // for add-on targets, delegate to the add on target to check for compatibility + return projectTarget.canRunOn(avdTarget) ? Compatibility.YES : Compatibility.NO; + } +} diff --git a/andmore-swt/org.eclipse.andmore.swt/.classpath b/andmore-swt/org.eclipse.andmore.swt/.classpath index 54f49f95..2349129f 100644 --- a/andmore-swt/org.eclipse.andmore.swt/.classpath +++ b/andmore-swt/org.eclipse.andmore.swt/.classpath @@ -13,7 +13,7 @@ - + diff --git a/andmore-swt/org.eclipse.andmore.swt/META-INF/MANIFEST.MF b/andmore-swt/org.eclipse.andmore.swt/META-INF/MANIFEST.MF index 65bf4bfc..98b977f9 100644 --- a/andmore-swt/org.eclipse.andmore.swt/META-INF/MANIFEST.MF +++ b/andmore-swt/org.eclipse.andmore.swt/META-INF/MANIFEST.MF @@ -29,11 +29,17 @@ Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Export-Package: com.android, com.android.annotations, com.android.annotations.concurrency, + com.android.ddmlib, + com.android.ddmlib.log, + com.android.ddmlib.logcat, + com.android.ddmlib.testrunner, + com.android.ddmlib.utils, com.android.dvlib, com.android.ide.common.blame, com.android.ide.common.internal, com.android.ide.common.rendering, com.android.ide.common.rendering.api, + com.android.ide.common.repository, com.android.ide.common.res2, com.android.ide.common.resources, com.android.ide.common.resources.configuration, @@ -44,15 +50,12 @@ Export-Package: com.android, com.android.prefs, com.android.repository, com.android.repository.api, + com.android.repository.impl.meta, com.android.repository.io, + com.android.repository.io.impl, com.android.repository.testframework, com.android.repository.util, com.android.resources, - com.android.ddmlib, - com.android.ddmlib.log, - com.android.ddmlib.logcat, - com.android.ddmlib.testrunner, - com.android.ddmlib.utils, com.android.sdklib, com.android.sdklib.build, com.android.sdklib.devices, @@ -60,6 +63,7 @@ Export-Package: com.android, com.android.sdklib.internal.build, com.android.sdklib.internal.project, com.android.sdklib.repository, + com.android.sdklib.repository.legacy, com.android.sdklib.repository.legacy.remote.internal, com.android.sdklib.repository.meta, com.android.sdklib.repository.targets, @@ -83,6 +87,7 @@ Export-Package: com.android, org.apache.http.entity.mime, org.apache.http.entity.mime.content, org.eclipse.andmore.base, + org.eclipse.andmore.base.resources, org.kxml2.io, org.kxml2.kdom, org.kxml2.wap, diff --git a/andmore-swt/org.eclipse.andmore.swt/pom.xml b/andmore-swt/org.eclipse.andmore.swt/pom.xml index 3e48751e..099e30e4 100644 --- a/andmore-swt/org.eclipse.andmore.swt/pom.xml +++ b/andmore-swt/org.eclipse.andmore.swt/pom.xml @@ -211,11 +211,6 @@ 4.1 - com.android.tools - sdk-common - ${android.tools.version} - - com.android.tools repository ${android.tools.version} diff --git a/andmore-swt/org.eclipse.andmore.swt/src/org/eclipse/andmore/base/resources/IEditorIconFactory.java b/andmore-swt/org.eclipse.andmore.swt/src/org/eclipse/andmore/base/resources/IEditorIconFactory.java new file mode 100644 index 00000000..278ad4f1 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/src/org/eclipse/andmore/base/resources/IEditorIconFactory.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2017 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 org.eclipse.andmore.base.resources; + +import org.eclipse.swt.graphics.Image; + +/** + * Interface for Factory to generate icons for Android Editors + * @author Andrew Bowley + * + */ +public interface IEditorIconFactory { + + /** + * Returns an Image for a given icon name. + *

+ * Callers should not dispose it. + * + * @param osName The leaf name, without the extension, of an existing icon in the + * editor's "icons" directory. If it doesn't exist, a default icon will be + * generated automatically based on the name. + * @param color The color of the text in the automatically generated icons + */ + Image getColorIcon(String osName, int color); +} diff --git a/andmore-swt/org.eclipse.andmore.swt/src/org/eclipse/andmore/base/resources/ImageFactory.java b/andmore-swt/org.eclipse.andmore.swt/src/org/eclipse/andmore/base/resources/ImageFactory.java new file mode 100644 index 00000000..c1b337a7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/src/org/eclipse/andmore/base/resources/ImageFactory.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2017 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 org.eclipse.andmore.base.resources; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; + +public interface ImageFactory { + /** + * Produces an edited version of given image + * {@link ImageFactory#getImageByName(String, String, ImageEditor)}. + */ + public interface ImageEditor { + /** + * The editor implementation needs to create an image data object based on a given image, or
+ * if no modification is necessary, return null.
+ *

+ * + * @param source A non-null source image. + * @return {@link ImageData} object, which can be null if no change required + */ + @NonNull public ImageData edit(@NonNull Image source); + } + + /** + * Produces an replacement version of given image + * {@link ImageFactory#getImageByName(String, String, Filter)}. + */ + public interface ReplacementImager { + /** + * The editor implementation needs to create an image data object based on a given image, or
+ * if no modification is necessary, return null.
+ *

+ * + * @param source A non-null source image. + * @return {@link ImageData} object, which can be null if no change required + */ + @NonNull public ImageData create(); + } + + /** + * Loads an image given its filename (with its extension). + * Might return null if the image cannot be loaded.
+ * The image is cached. Successive calls will return the same object.
+ * + * @param imageName The filename (with extension) of the image to load. + * @return {@link Image} object or null if the image file is not found. The caller must NOT dispose the image. + */ + @Nullable + Image getImageByName(String imageName); + + /** + * Returns an image given its filename (with its extension). + * Might return null if the image cannot be loaded.
+ * @param imageName The filename (with extension) of the image to load. + * @return {@link ImageDescriptor} object or null if the image file is not found. + */ + @Nullable + ImageDescriptor getDescriptorByName(String imageName); + + /** + * Loads an image given its filename (with its extension), caches it using the given + * {@code KeyName} name and applies a filter to it. + * Might return null if the image cannot be loaded. + * The image is cached. Successive calls using {@code KeyName} will return the same + * object directly (the filter is not re-applied in this case.)
+ *

+ * @param imageName Filename (with extension) of the image to load. + * @param keyName Image key reference + * @param imageEditor Image editor + * @return {@link Image} or null if the image file is not found. The caller must NOT dispose the image. + */ + @Nullable + Image getImageByName(String imageName,String keyName, ImageEditor imageEditor); + + /** + * Loads an image given its filename (with its extension) and if not found, + * uses supplied {@code ReplacementImager} to create a replacement. + * Might return null if the image cannot be loaded. + * The image is cached. Successive calls using {@code KeyName} will return the same + * object directly
+ *

+ * @param imageName Filename (with extension) of the image to load. + * @param keyName Image key reference + * @param imageEditor Image editor + * @return {@link Image} or null if the image file is not found. The caller must NOT dispose the image. + */ + @Nullable + Image getImageByName(String imageName, ReplacementImager replacementImager); + + /** + * Returns image for given image file path + * @param imagePath A valid file system path relative to the bundle location eg. "icons/smile.gif" + * @return {@link Image} object or null if the image file is not found. The caller must NOT dispose the image + */ + Image getImage(String imagePath); + + /** + * Dispose all image resources + */ + void dispose(); + +} \ No newline at end of file diff --git a/andmore-swt/org.eclipse.andmore.swt/src/org/eclipse/andmore/base/resources/JFaceImageLoader.java b/andmore-swt/org.eclipse.andmore.swt/src/org/eclipse/andmore/base/resources/JFaceImageLoader.java new file mode 100644 index 00000000..3c13a689 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.swt/src/org/eclipse/andmore/base/resources/JFaceImageLoader.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2017 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 org.eclipse.andmore.base.resources; + +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.core.runtime.FileLocator; +import org.eclipse.core.runtime.Path; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.resource.JFaceResources; +import org.eclipse.jface.resource.LocalResourceManager; +import org.eclipse.jface.resource.ResourceManager; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.widgets.Display; +import org.osgi.framework.Bundle; +import org.osgi.framework.FrameworkUtil; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.utils.ILogger; + +public class JFaceImageLoader implements ImageFactory { + + /** Image file location when using {@link #getImageByName(String)} */ + public static String ICONS_PATH = "icons/"; + + protected Bundle bundle; + protected PluginResourceProvider provider; + protected final Map filterMap = new HashMap<>(); + protected ResourceManager resourceManager; + protected ILogger logger; + + /** + * Loads bundle images + * Images are loaded using a path relative to the bundle location. + * + * Instances are mangaged by a JFace resource manager, and thus should never be disposed by the image consumer. + * + */ + + /** + * Construct an ImageLoader object using given UI plugin instance. + * This object provides imageDescriptorFromPlugin() method + * @param bundle + */ + public JFaceImageLoader(@NonNull PluginResourceProvider provider) + { + this.provider = provider; + createResourceManager(); + } + + /** + * Construct an ImageLoader object using given bundle instance + * @param bundle + */ + public JFaceImageLoader(@NonNull Bundle bundle) + { + this.bundle = bundle; + createResourceManager(); + } + + /** + * Construct an ImageLoader object using given class of plugin associated with the bundle + * @param bundleClass + */ + public JFaceImageLoader(Class bundleClass) + { + this(FrameworkUtil.getBundle(bundleClass)); + } + + public void setLogger(ILogger logger) { + this.logger = logger; + } + + /* (non-Javadoc) + * @see org.eclipse.andmore.base.resources.ImageFactory#getImageByName(java.lang.String) + */ + @Override + @Nullable + public Image getImageByName(String imageName) { + return getImage(ICONS_PATH + imageName); + } + + + /* (non-Javadoc) + * @see org.eclipse.andmore.base.resources.ImageFactory#getImageByName(java.lang.String, java.lang.String, org.eclipse.andmore.base.resources.JFaceImageLoader.ImageEditor) + */ + @Override + @Nullable + public Image getImageByName(String imageName, + String keyName, + ImageEditor imageEditor) { + if (imageEditor == null) // No imageEditor means just load image. The keyName is irrelevant. + return getImageByName(imageName); + String imagePath = ICONS_PATH + imageName; + Image image = null; + ImageDescriptor imageDescriptor = descriptorFromPath(imagePath); + if (imageDescriptor != null) { + ImageDescriptor imagefilterDescriptor = filterMap.get(keyName); + if (imagefilterDescriptor == null) { + // Assume filter input = output + imagefilterDescriptor = imageDescriptor; + image = resourceManager.createImage(imageDescriptor); + ImageData imageData = imageEditor.edit(image); + if (imageData != null) { + // Create new image from data + imagefilterDescriptor = ImageDescriptor.createFromImageData(imageData); + image = resourceManager.createImage(imagefilterDescriptor); + } + filterMap.put(keyName, imagefilterDescriptor); + } + else + image = resourceManager.createImage(imagefilterDescriptor); + } + return image; + } + + /* (non-Javadoc) + * @see org.eclipse.andmore.base.resources.ImageFactory#getImage(java.lang.String) + */ + @Override + public Image getImage(String imagePath) { + Image image = null; + ImageDescriptor imageDescriptor = descriptorFromPath(imagePath); + if (imageDescriptor != null) { + image = resourceManager.createImage(imageDescriptor); + if ((image == null) && (logger != null)) + logger.error(null, "Image creation failed for image path " + imagePath); + } + return image; + } + + /* (non-Javadoc) + * @see org.eclipse.andmore.base.resources.ImageFactory#dispose() + */ + @Override + public void dispose() { + // Garbage collect system resources + if (resourceManager != null) + { + resourceManager.dispose(); + resourceManager = null; + } + } + + @Override + @Nullable + public Image getImageByName(String imageName, ReplacementImager replacementImager) { + String imagePath = ICONS_PATH + imageName; + ImageDescriptor imageDescriptor = descriptorFromPath(imagePath); + if (imageDescriptor == null) { + ImageDescriptor replacementImageDescriptor = filterMap.get(imagePath); + if (replacementImageDescriptor == null) { + replacementImageDescriptor = ImageDescriptor.createFromImageData(replacementImager.create()); + filterMap.put(imagePath, replacementImageDescriptor); + } + return resourceManager.createImage(replacementImageDescriptor); + } + return resourceManager.createImage(imageDescriptor); + } + + @Override + @Nullable + public ImageDescriptor getDescriptorByName(String imageName) { + String imagePath = ICONS_PATH + imageName; + return descriptorFromPath(imagePath); + } + + protected ImageDescriptor descriptorFromPath(String imagePath) { + if (provider != null) { + ImageDescriptor descriptor = provider.descriptorFromPath(imagePath); + if ((logger != null) && (descriptor == null)) + logger.error(null, "Image descriptor null for image path: " +imagePath); + return descriptor; + } + ImageDescriptor imageDescriptor = null; + // An image descriptor is an object that knows how to create an SWT image. + URL url = FileLocator.find(bundle, new Path(imagePath), null); + if (url != null) { + imageDescriptor = ImageDescriptor.createFromURL(url); + if (logger != null) { + if (imageDescriptor != null) + logger.info("Image file found at " + url.toString()); + else + logger.error(null, "Image descriptor null for URL: " + url.toString()); + } + } + else if (logger != null) + logger.error(null, "Image path not found: " + imagePath); + return imageDescriptor; + } + + /** + * Returns local Resource Manager + * @return ResourceManager object + */ + protected void createResourceManager() { + if (resourceManager == null) + Display.getDefault().syncExec(new Runnable() { + + @Override + public void run() + { + // getResources() returns the ResourceManager for the current display. + // May only be called from a UI thread. + resourceManager = new LocalResourceManager(JFaceResources.getResources()); + } + }); + } + + + +} diff --git a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/project/IPropertySource.java b/andmore-swt/org.eclipse.andmore.swt/src/org/eclipse/andmore/base/resources/PluginResourceProvider.java similarity index 68% rename from andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/project/IPropertySource.java rename to andmore-swt/org.eclipse.andmore.swt/src/org/eclipse/andmore/base/resources/PluginResourceProvider.java index 92bd6b93..9ca86fb4 100644 --- a/andmore-swt/org.eclipse.andmore.sdkuilib/src/main/java/com/android/sdklib/internal/project/IPropertySource.java +++ b/andmore-swt/org.eclipse.andmore.swt/src/org/eclipse/andmore/base/resources/PluginResourceProvider.java @@ -1,26 +1,22 @@ -/* - * Copyright (C) 2012 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.sdklib.internal.project; - -/** - * A source able to return properties by name. - * - */ -public interface IPropertySource { - String getProperty(String name); - void debugPrint(); -} +/* + * Copyright (C) 2017 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 org.eclipse.andmore.base.resources; + +import org.eclipse.jface.resource.ImageDescriptor; + +public interface PluginResourceProvider { + ImageDescriptor descriptorFromPath(String imagePath); +} diff --git a/android-core/features/org.eclipse.andmore.package/feature.xml b/android-core/features/org.eclipse.andmore.package/feature.xml index edc5a300..bf96ac2f 100644 --- a/android-core/features/org.eclipse.andmore.package/feature.xml +++ b/android-core/features/org.eclipse.andmore.package/feature.xml @@ -1,46 +1,54 @@ - - - - - Android Developer Tools - - - - Copyright (C) 2007-2014 The Android Open Source Project - - - - %license - - - - - - - - - - - - - - - - - - - - - - + + + + + Android Developer Tools + + + + Copyright (C) 2007-2014 The Android Open Source Project + + + + %license + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android-core/features/org.eclipse.andmore/feature.xml b/android-core/features/org.eclipse.andmore/feature.xml index 8d209d87..895806e3 100644 --- a/android-core/features/org.eclipse.andmore/feature.xml +++ b/android-core/features/org.eclipse.andmore/feature.xml @@ -1,120 +1,177 @@ - - - - - %feature.description - - - - %feature.copyright - - - - %license - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + %feature.description + + + + %feature.copyright + + + + %license + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android-core/plugins/org.eclipse.andmore.base/.classpath b/android-core/plugins/org.eclipse.andmore.base/.classpath index 62c0fbc5..7498423d 100644 --- a/android-core/plugins/org.eclipse.andmore.base/.classpath +++ b/android-core/plugins/org.eclipse.andmore.base/.classpath @@ -1,17 +1,7 @@ - - - - - - - - - - - - - - - - - + + + + + + + diff --git a/android-core/plugins/org.eclipse.andmore.base/.settings/org.eclipse.jdt.core.prefs b/android-core/plugins/org.eclipse.andmore.base/.settings/org.eclipse.jdt.core.prefs index ea661960..c137e176 100644 --- a/android-core/plugins/org.eclipse.andmore.base/.settings/org.eclipse.jdt.core.prefs +++ b/android-core/plugins/org.eclipse.andmore.base/.settings/org.eclipse.jdt.core.prefs @@ -1,98 +1,98 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore -org.eclipse.jdt.core.compiler.annotation.nonnull=com.android.annotations.NonNull -org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=com.android.annotations.NonNullByDefault -org.eclipse.jdt.core.compiler.annotation.nonnullisdefault=disabled -org.eclipse.jdt.core.compiler.annotation.nullable=com.android.annotations.Nullable -org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.debug.lineNumber=generate -org.eclipse.jdt.core.compiler.debug.localVariable=generate -org.eclipse.jdt.core.compiler.debug.sourceFile=generate -org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.autoboxing=ignore -org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning -org.eclipse.jdt.core.compiler.problem.deadCode=warning -org.eclipse.jdt.core.compiler.problem.deprecation=warning -org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled -org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled -org.eclipse.jdt.core.compiler.problem.discouragedReference=warning -org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore -org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning -org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled -org.eclipse.jdt.core.compiler.problem.fieldHiding=warning -org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning -org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning -org.eclipse.jdt.core.compiler.problem.forbiddenReference=error -org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning -org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled -org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning -org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning -org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore -org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning -org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning -org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore -org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning -org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled -org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning -org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error -org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled -org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning -org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore -org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning -org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning -org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore -org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning -org.eclipse.jdt.core.compiler.problem.nullReference=warning -org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning -org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning -org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore -org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning -org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore -org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning -org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning -org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error -org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning -org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning -org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning -org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore -org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore -org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning -org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore -org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore -org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled -org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning -org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled -org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled -org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore -org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning -org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled -org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning -org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error -org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore -org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning -org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore -org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning -org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled -org.eclipse.jdt.core.compiler.problem.unusedImport=warning -org.eclipse.jdt.core.compiler.problem.unusedLabel=warning -org.eclipse.jdt.core.compiler.problem.unusedLocal=warning -org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning -org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore -org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled -org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled -org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled -org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning -org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning -org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore +org.eclipse.jdt.core.compiler.annotation.nonnull=com.android.annotations.NonNull +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=com.android.annotations.NonNullByDefault +org.eclipse.jdt.core.compiler.annotation.nonnullisdefault=disabled +org.eclipse.jdt.core.compiler.annotation.nullable=com.android.annotations.Nullable +org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=warning +org.eclipse.jdt.core.compiler.problem.deprecation=warning +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore +org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=warning +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning +org.eclipse.jdt.core.compiler.problem.forbiddenReference=error +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning +org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning +org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning +org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning +org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning +org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning +org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning +org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedImport=warning +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning +org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/android-core/plugins/org.eclipse.andmore.base/META-INF/MANIFEST.MF b/android-core/plugins/org.eclipse.andmore.base/META-INF/MANIFEST.MF index ab73938d..831e9cb8 100644 --- a/android-core/plugins/org.eclipse.andmore.base/META-INF/MANIFEST.MF +++ b/android-core/plugins/org.eclipse.andmore.base/META-INF/MANIFEST.MF @@ -1,89 +1,17 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: Common Android Utilities -Bundle-SymbolicName: org.eclipse.andmore.base;singleton:=true -Bundle-Version: 0.5.2.qualifier -Require-Bundle: org.eclipse.ui, - org.eclipse.core.runtime, - org.apache.httpcomponents.httpclient;bundle-version="4.1.3", - org.apache.httpcomponents.httpcore;bundle-version="4.1.4", - org.apache.commons.logging;bundle-version="1.1.1", - org.apache.commons.codec;bundle-version="1.4.0", - org.apache.commons.compress;bundle-version="1.6.0", - com.google.gson;bundle-version="2.2.4" -Bundle-ActivationPolicy: lazy -Bundle-Vendor: Eclipse Andmore -Bundle-ClassPath: ., - libs/annotations.jar, - libs/common.jar, - libs/guava-17.0.jar, - libs/httpmime-4.1.jar, - libs/kxml2-2.3.0.jar, - libs/layoutlib-api.jar, - libs/sdklib.jar, - libs/sdkstats.jar, - libs/dvlib.jar, - libs/sdk-common.jar -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 -Export-Package: com.android, - com.android.annotations, - com.android.annotations.concurrency, - com.android.dvlib, - com.android.ide.common.internal, - com.android.ide.common.packaging, - com.android.ide.common.rendering, - com.android.ide.common.rendering.api, - com.android.ide.common.rendering.legacy, - com.android.ide.common.res2, - com.android.ide.common.resources, - com.android.ide.common.resources.configuration, - com.android.ide.common.sdk, - com.android.ide.common.util, - com.android.ide.common.xml, - com.android.io, - com.android.layoutlib.api, - com.android.prefs, - com.android.resources, - com.android.sdklib, - com.android.sdklib.build, - com.android.sdklib.devices, - com.android.sdklib.internal.avd, - com.android.sdklib.internal.build, - com.android.sdklib.internal.project, - com.android.sdklib.internal.repository, - com.android.sdklib.internal.repository.archives, - com.android.sdklib.internal.repository.packages, - com.android.sdklib.internal.repository.sources, - com.android.sdklib.internal.repository.updater, - com.android.sdklib.io, - com.android.sdklib.repository, - com.android.sdklib.repository.descriptors, - com.android.sdklib.repository.local, - com.android.sdklib.util, - com.android.sdkstats, - com.android.util, - com.android.utils, - com.android.xml, - com.google.common.annotations, - com.google.common.base, - com.google.common.base.internal, - com.google.common.cache, - com.google.common.collect, - com.google.common.eventbus, - com.google.common.hash, - com.google.common.io, - com.google.common.math, - com.google.common.net, - com.google.common.primitives, - com.google.common.reflect, - com.google.common.util.concurrent, - org.apache.http.entity.mime, - org.apache.http.entity.mime.content, - org.eclipse.andmore.base, - org.kxml2.io, - org.kxml2.kdom, - org.kxml2.wap, - org.kxml2.wap.syncml, - org.kxml2.wap.wml, - org.kxml2.wap.wv, - org.xmlpull.v1 +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Common Android Utilities +Bundle-SymbolicName: org.eclipse.andmore.base;singleton:=true +Bundle-Version: 0.5.2.qualifier +Require-Bundle: org.eclipse.ui, + org.eclipse.core.runtime, + org.apache.httpcomponents.httpclient;bundle-version="4.1.3", + org.apache.httpcomponents.httpcore;bundle-version="4.1.4", + org.apache.commons.logging;bundle-version="1.1.1", + org.apache.commons.codec;bundle-version="1.4.0", + org.apache.commons.compress;bundle-version="1.6.0", + com.google.gson;bundle-version="2.2.4" +Bundle-ActivationPolicy: lazy +Bundle-Vendor: Eclipse Andmore +Bundle-ClassPath: . +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 diff --git a/android-core/plugins/org.eclipse.andmore.base/libs/annotations.jar b/android-core/plugins/org.eclipse.andmore.base/libs/annotations.jar deleted file mode 100644 index 4010e30a1186d869c8778010b609b358ce01215f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4229 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 diff --git a/android-core/plugins/org.eclipse.andmore.base/libs/common.jar b/android-core/plugins/org.eclipse.andmore.base/libs/common.jar deleted file mode 100644 index bbcf3dde95dc4ea28f401e02fd1646faa9076fa2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 87588 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 diff --git a/android-core/plugins/org.eclipse.andmore.base/libs/dvlib.jar b/android-core/plugins/org.eclipse.andmore.base/libs/dvlib.jar deleted file mode 100644 index 842d87e34d8d6424139d3f5c36a3d7b7223edfc8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19925 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�N8k| zT{tWZWVJluOj3@ek%m>GO2>SqNmO6Ls>*8Xu;`1QFGNDB7pu$YI9aGYwEZlK7#}fJ zBgnr+W}hkc*}RA>IIy;HP-ryEJ7CG0CHMq@RaJi_eYJ6z$-FAKLoArF{kc_LolBPZ@Pv|MT1h74jS4@6*&_U7mT#-Wu0puN@5GlV6Ccy2$Uu{D5F|((lk< zx5}aQy!kcq3)yRQSbYLFgZh-TZnluxWQc%q4#eIsZ_r0IA^t zycm{~ocp9t8_s>u2CpgRzp!FZF~f$cNgjgZ=Cd^Tv-9j1e~Uxv#pG``ql(x`MVu)G zX7ba-AeWJzBMMs2qQE-Im|hGY;!r1oZd8vw?7CC-T(O6bx>;<{U4u|UXi7epF&JYa zSoT<;v>}yLmQ|feCfta{mkrAjV4jeU8gN`lm-f8ss_0$xxow&NPhSVTHa_k|_m3Xd z{KvopFO)3Q69<%BPz(?z< z$jR!9vJqU?SEdK>hhD*MXpinN-9(XX0**-15a+nK@qGhnn1vstXyUb0)IGwm+a4Qv zK1U#8EBT@orWg=3b9@?h1dp$<{o_>(Bw~6z5eeY^$fN9m zJ6XbUN(EkFb&g&v%al{*L43l2LbcePhg>?9a3ghUGwW!d#hJSpOUMuQW$|;F2QEti z_I1BG5Gsj+aD=(=OO{RCZNx#BE}%*hRe)svnqRN5CEYpQneu#jC%xXGi2X)pP9WFg zi`?#40WAY4ZmCdXKn?B^QlI~Lf*q1peiMI>xCEj92cBTIzngE;w9C36{BSoVdD8xf z6GS0$8b29xB2>_NtW<;vvgM;I2D*?eOsN*{aGt7nXp?y{(K}e5mE*4d zatEhiPnk&cDd>8G6wI*_i4j%WhJYAUl3&Mm-8jzZBxI8{pEk%zkdBl0P23*FvI&BA zqz##1=}&%Tx^ll*gQg2z2zIn?Lv4Wb#%tKHoiqnY0KIosqwKrb7FwSquW6pix6cJS zzf?q%esWH8pOmKD6RQWhA6+_BeM68nNeHht!Ru2*ve= zHYo=Z(M=dKE(5O~d2TfctTnIN4=F)%q@hUrzIbvJpY3XxjF)33c$$G$cU=oK*(cLjE>W+u$<+t&t&qlX9rXZJWI7USq zhg6gsMUf1;)C3Yn?rbhd$nZgv830=yYC%FQ2pDYWyt zLJW|MtSVl;qjbkq8Z}RJTHr-(ao|FJCnF;0jayBrs?e)=z|+Iz9f+`~B(wOa4e83X zhY>fNgHkQ_gaz?#n=_=F0}HZ-iEu(iL#E2?$3@KZhqLB36CsD;2-!4q>&-R>&9BHH z{wF^9qynuOf<1wfpqUNzm>^iIT@zS=XNtZC#JczjYHcy$lm3$Jduo4YN@GHHG})oN z@(kKf-o%O=S`pu*kPH~Fn!_%_x8t4vw1(Ql8jvFa_Ek~>60U!Sp-fYTMN!5sdN_WGTSa+i2Xv$}?+uqn z#5?zod;$H%QbW_<@`A?RiAI~(L4Z_JK;TSSE3@Dx~f!-U3(%M&aD^gT8A8UIyMO|7!kqEwCH!# zB4JauHKc!Vx{l+H9mmh@T$10Ta7CmAaM%EV&?|}(`fOS_!GYbBHMneapSh~9A=4-P zNH4i?!Q1V6KAKR8{v=vB1rIczW4@!%LmQW91Yi^jze8NAQo^50r5c8TzluI#r+9kDr#i6Ih!JgMPX8ra#$4Bgiftu zeJHJnLRAPglAEtx$kwPkb_YnTrC#uBmTA}DrMXh8u^r=wl|p)^_6$q<-4q0itI8J6 zO6?g8g}P+B)py)tvX>h~<7|3vVx|(j2CcWwDH|)*d0#!-`lTb?xJYUqsqWpBhFY9q zs5Lrx`XMP*-64nGe_mKRoJV(>KlOXcx9b@>dcnMfd-MMG|D;p>YraxGYr}sl?n=c?XCx)$&nco)qEqC#1b$)!%A`2sIoJiB zhR_0pnmAA*l*&+d28?y`_|%Hgf)mCUU~WI{90_D~R4u6U>7elt`_^eg_TOf)ny&Qk zp2rtnPt9LPe;%LrNq-#4CEL+M1nXmm>r+GAN@FMQwNULO5)Iv2BSI#{xx%R!B_KK0 zQ3(oR>%`k}{YDPczwhE@4J;W`jlK0_>llT9?{n0^?N?ND!CXt*uTTpXuGw8tk8Fmn zY?#_Kx;i}%ZttO6U~n9+LSnO7D{}GMHMfpQ4_KLyWt)vtU`2;bmRc`qz+&##+GP%x zo--0TUXr>>)5pUW?sTdjgeMORO(0%p*@zp|pbmfqgRlsrk(ZB{S21Y$=q{As+DX@} zZ|N*j;#zK4{&Ns~E-c|PS!BZAG-8^K5U_E>CHrSq)9lC~Orp4YfKL9g#oHv6HDKW| zX_q<;7bEJJJ!&n>%*;`dnKPUE=u*i#TV1hgmUCV02!nxjt;vKXg4ko;GS#u%bUkWV zC_^v@Cx-qS(^TckU4kW()l9}^)Wb4f!zxc2;^=DT)Y0Fi7%2A_@b;o{nKL2lvuo9a3u|W|K!I z&9M{Z(R3Y9hXlfRL0qmSoOQ9$OV5u^Cnz2#!& zZ{kz&-`GVJHu=KLIohbeOAAhQbr34*8)#$D6~r70(jOs4fV1&wKKRwH_4(gW;^%DG zh(Dq_9zX`ed~k%|0$63zw!r`f33;zn>i{NIsUy{ z@%NWRQ9|K6*80;rp2Z8eq{DLo3||~?eom|6No^moNf4E7f3*aXQ2= zMy+jnrSbygjWWH&B18*l{h=-VmZ}WBQ_4nhxoU1M{2Jy(O@(sMXsq-+4HoR+D0V|_ zr^Y&MqnP@nLKViMGdA`@Qu{P5Iq@bf2B)BnFe`GQ#o0tjwMv$>;Aq~Ni&&V=*|MP| zixuU2*N|f3HUDz+>HD)J*2vD)Cm`AF2!z`5MYAiVUw~eGHH9JOAwXXjIr#~~9nIO(8|Z?o=F?ds zhD!Lx7i|fij3s*yCQBaG7=5FSL@|NXN8{i3-rWOpznYKVDvW7c?#LahOrA>E>FF_i zROZGz>Mn*P%HKiY9~ejq_#7vSkm4%`w~sm3AY3AzYckQxHVGszku!<$Fx#NQS%cp> zg)#BFcl)nreO#jm8}|EI$NQ&7^}i0R{x_l90{>xuNm$R}ztmg&=J!bnwlN6zGR5CawYuGsHN>2cJ9sYDmbvMxTV2ES80e_8L%o z?yYC8?)!{mqHb@WcYqyoOw_R!()4#w7V44}h8x+bW*AO}n*N;b2LT=qL)ptAjDV#` zP=CF~T59M(nrE%FcZ$(LrysJ)fpKKSNhQEt~4g!Ds8ECnjUCkyOmn?<-o~t6T6S%&OFYRNO8r_9syra!XPV_G?s*% z@hWJv@ZXPBfUDH91jRJ|+KHhkMRj+u0W^x85f<}eA=b=RnuK6qvWrBwSjpLlcw|G% zXX>8#)fQ1g3rUfX(Q9kaRVU?>K9Rbe+m)Yh8txka*pmTJR(&#OedHVP2i@4=np7O= zBpdCA3}i7K6!%7-)fi<9V`Xy*HSIgJeJOG?R!@)u8>Kmo|R}_+# z>qMq@(Y@}(KU1$5IWOG3Ih_L7VEK3B*du?s3?S{ol4|1IE@XoGrx#JOvsTbv4#9B`ll$PB*tl9~Y+*uv(EmgtSd3(8L1L!vm+ z*n>hxs*dssa~hr~ng#3{Ex(WO*s&hS?PL{*3-{)n0Wi(6P0vv4p7R8AVrnp5w#(jK z_0jSRfYH=Tr=TO>-*2~eKpy%AanCuB4YlhDEeoSG5?`i{f(mk%nuUR z6|j`Bhr3PUW1ygb)vtssgfPLsuWqi1SHheQn)){7s-6|nqzly5tV+ehEX-oydEVs8 zY#C#lcs>=vvFGb0U<{jlkqvL#)WZS zM(DsA#IMtv7>4fygbPR5IQrt?+=z^28xE7;+}PJ^MY`VQo>uO2g%_aB#_D|s0QXJC zySdRr#|yfRA;t^Ig%ZpNQ=_xmL`0v)loq38lkSbfl#+<5tUOaHEev-z(sqf=tC)av z)TFnX@1(H(6|1$(>NGib>7V34S)`T11*vbZhM(emC!a%Vv1XsEte%)i?6sJe*;Qe1 zvQ(!{?{;Yex|yeja-tmrvYF_HN0ei1si8cVq+KZ}F%FB!XAOYogsJXe4vOMn1Syr7=S24)!H@hUrz(k6-SYS1x7A#oy zDXx1$$HlPw6AfE~fzgtUd3|!_OKdtuTdB>VNP_$M-JN;4foW|_Y&o3bfV+HxyOy_y zI&pMBtIjCNaA=FEoW!iya^22p+0hcv0_;F$Tj@JGG&Z|Ir-(U|*xXd1+1!v}3e{v~ zPqLrTDf@C!MwgJ@t-)KVS+cohq2jg>sO>4VV5*}66QzE_IzHaL)zYgjofWR7rHuw= zxjg;u6|}U2sR3Tn0S4rHgx&vAUa@9!{w~ z46?doQEbKlkqxqvWRKx16a@)=;T&UrX^J1}P+J&`Dh17s63=+0lhQ3FO&M9Q^6pMv zPwSUgHE`6W75k>`>MB!-mNXZhNbiIRDlScN-Lv%#r_iNG>63(+$Iec>hwWd9-}x)%Z>+#RxrO{-e%7eu>EPrV zq2$rGh?kj`CHu(Q03EBq()S_$#+JOdH@``^cP^o2F7X@cBBKm(cDqDzX4&-(YaNzV zBTy1$=nFiLFfdKAEio~lZXPJ?Mc!W43;NYN6}k!8)I;8Y`g*hBp*agDeM2sFw-cr{ zVmhS^Y2#8zUNO=_avn4d*qvHseCCTqQl+C@xUAUMbG&EyBUwu2G6sK5&)$dRwXMtP zaDxdgPl{+gD`3ieM&SbBp-(p>WjDa@OX+Wr`qNU3rnK-0(wwcpx|a%j-A`mx?XNiI zT)X0Yh^4_@pql&IgB(%{CR8J+boJ)kQA?9;H#!ch&TpV=4-+$>@$t}n>u6Zo#o!Zj z*W!tYKW$NLUTe*?D-@xhc8;5TyPl~^V1kN*EJR;J6u{kkoiBB5iARCYs+~uosnt%+ z`P=A*rLbjYDIL6YM&o?+OtJbeA*!(D>#$nH>_M72Kbq;Un452C_Qpd${*k8(n4Qbw z@ZHl||5G5%fBp9Si#xkAf5%@{DoHr3D)}@x%u`I_oO6QY0r~uqPNNHo7(7L%Rd3X90Ls$i5A)@_n_;oY62)lP>*_c? z+bOdBoEdU({rP#M@nc$#F&bEV(f~J^u_dk_yz1^w9d!9l)PNvds7Xm%dwS0um35^v zwK#|MWtCWaYhp(kbo`Vants#@xJ#{>aJb@1bvKOo5{EPc zO;jWb=SAZ{)7eW!8|9^kZ7O~^Y_;);YQFxDRXvHo@^)7CD!iz20JX@@LESk=d8P0s z#Bybu2*==IPg4gBrQ<92mgE=?E8@%&KY-Whb7sw%n%u$6)&o70WTjnPUtT#4UBjWE z^9f5K*2{#8Xm(HK7q!O}Gtw{n*+e4e1=IS*g7ee{C5M7A(NU36Ja*KSmifEFb=#yr zT&(uw*1gX_X0@LFOkgE7Z2(7;9`>WfA0(Ue^?}K2^&P0OC@B_Etw(E`1r51|NS1RY zQyn6Yb*E*J86{%lUG4hJ`@tvAnyvM?4NxSuqv$h-`;ppX<}97j?S*pi7LQuPMRPHL zm-*pe*7BYtG1VZIGtSm6R3$S3?8Ck6L$ztWX;JI05<43{;j?y_`Rqe<6m(nJQ~KkN zO>D;4E~b_qtNt@Ha{cVIHt{=SM2nWf!=PKcH;n94`~FdC6A&3ZOEr)gXLt9pMiNKf z0M}0_Wyg_e?0|31C0kakS!kJT7wzSKnpSREJwbd%c#4;Sy+mebN&R?XtA02`pQ2L4YHR?u%;AY?XiErEAg zBr*zHzc}-mA`K{vw}r$n(F^Sl0p`-dMv&{@M-rc7X<=g(^h8Ka@JM0lHk9H_Brnk< zExIW{;;k~G0Q2*EPRMc)h{ii84c$x%9*E-vo#QOStdez802;Sr7QKrVzhsFFE<@Of zex{JP`eEFKj3k{O>dN z{ylgsei%ZAyR-1YU2^S_V~1!rt*e#iv)1XCzPc~b;F z(HH@YYA}=EofbnDf?+$pWuFib#IT22>( zN~Xg79Q2S$jzON#(5^>3>edXa+7eFa%qM8x$GuP6d@qNch-)0^BNk*fm8DKC>Xr^B zNA$5rj+{yb?zh^J38smii75#Xf~znari@a7DaD~&lk-H)y2&|k1|J^u;hW1M*{qqA z67?vf%9vN>lzfEFwC7*Iej1aPRk8cQu8X0I1P_uCaXc@|{3BJ=xoP=5Ot&K_MP7$}06tBFhSqz6erzh)LP0jxN^O zR8Rn`-12+SXq(o~Mz|U21dH$7F0}m0{);*S9Sb88o%D6$26~s$Rt$ho(a_DIobwhl zPty&gobwejb!_Nh2+#K7xQ5ZjReIH}z3vv-gwoB!IPQ2=nZahgS<-r8LuF!pV!rai zrpfj5vl@;3j_zyx0H^}7akNAT6`@Nwf^aQmQ0otPB$nqFh}MPvAv7i2KIiK-i3-CY z?gh}CDe`-1zo2k_qOhllTnkoxrZBfCM?T;Kod`11u6{Tqk(pcHRxKZ(~B4&Ci zeHGN)QZZ#4lKg^@v0YWqNx7yzJZ^;qwPvRPe{pHe=<_Au6K-|%F;Mea8I7G zT~aT-k}l0kZskxJeY`5?RAuh?tHP49eQbizxG8c?h4KQE@?a18!bW~YX5vDf&^|L& ztGWiOf-Y#&G-$IT`00y$F{VGgf-YdQqDG^FE`GDD#+Y(Kzgd0N%Cf!By17qNIlqRZ zf-ZV-2G>RYk_G|&2eXgxWc#Egm|a&qH^AFQevOCO%dxU;z0ORA)&qpBPB=%1ucih4??lLZ6{*O5B_tSZCFEH@#;mMx% z^^#+VH%>F5$VSSF0L)BC4pWeBk}n10C85K4<0t6E5M<|-iL6F`E{~|SY03)_#`i>& zzKi9B&&WP)ruyWH8a0dViW-%Pi)Q5(d5bS;y>8Zel}h;8H6Pg(3*#pdN+z>Q9t5!J z`C}MHkNC0Xl6NackKu_=OXF+#^U%~z6XWajRh^PYl_00|RXNIy(%2KZ6)@u`R>}+A zZkBqu%8Q>*^p8GyH8PXF6_;~IUPU$2C{He+qxT=X6gsY@teW++%RRhKV1Ql0aqKo^ z2T=4i$?q(#{atH8Te8>KL0-N1*eNEHC`RSd;oEf5zta zh_CL1ER<=SX=y9fl{MwZ@XISmT1s^V=vs28RD2hudU<=t@$;XtVoyX{spf z+A&g;la-YdlnKI((A;PrQ5SYuK$LbteRP4Q#AzTv`Son@JNh)1Px_S(uk6#PXl-fj zX-#ywid}_nF*{=iu#5`FlarMh&`}rF735Yq$b(!|Xf=X6i3gPmJGqQdC?j#PuI*Hm zD#%O8bhOpvhI;mBbat!kr)m538X=8nD7EV6I(1+50V_88c`2&NhHDGdbxMkQb?~0R zfI{~GT^8+K+(z%1iNnJwV{L*F*WYH;>pM~u90i@ID`_;Yw3Ux&Dv>QVLn>?U%Te3W z;#OvTNkdpbX@A;wGV9Gn{46GN!KW&W)1~H5kw1ca8N*_&yl19*t}4Grxa-BV*@UzS z$X;_Nz2C0}^&uwP72Y=f6U@4pADIz?GpeL#GdU>H`RK{eRs~qS|GzkU%b-Z2ZA-Xm z+-cn1-QC^Y-QC@t!W{~CcXxMaTpD+$k;WZ*xc9wxXXZuBo%z0u%B=cVkr5|X?z8uj z1Ohs`EO&*e@9auwG6Chhn4=3gtI7?z1Krt_S5{J~f0D!v@<%x+ga(t^3W#8%xeW-T z+Xv&hiWdzTof&_R3&FQ40h;}Teu6C{xG_N;^HEUl(yYRR=BK@ z9g-%*yiS~p66nW+vxB(oA-X_VT4Dpdrx}O;hFub8vpDeo_?1Nn`TNyys1d@TrVk<~ zYhXWn0Em^0Tw+`ChlJSV#dfsF#s>dAa>#c zkxJ|BUg(dx1g8bHPXYZaLggHX>9MFo7a-otTRt(g8xs^NYnxo4KqDW|uU$%-6Z3KT;m43Igb7MW%+FE5 z)xL=n1l`|=FHU|=gn=xP#P*tufneLc(BRSZBo3n64Y4-R4Kgi@Em1*q zk#EBi!xi&>i8qM>Dt`mCtHlmu21ssymruC4v2tlS0H4HKwRg-tb|oehi51ISE^u8l zvyLZtzrys=H}Z{L*QMo<++^7@R<2`gukB#;f9tFwZ4MLum6%BZiIY~5Z1Zg3AD2UJ z`7IV8xd?AH^39wz7iAsSjJrLt?s#9CK)mSNtiiEk?Vx^u-~n~TfQG?I&AeAOD`h-r z-bqgt$BX19cu4JM9Vr2_7^)o9kMe^Zx7c!NmHJvBsPX7o7HjR%!zVy7U0#O)jfzzn zU`WJe7aZ#i34BzLS-1xn}CuO*PnGGT1`A+xB#kz%JY*PePTwr#W zHrDO3h56MqI_!d{et_x=0!n3b)H(gc=cicA(PkO66gw5o{+jMoUKm{Lz=^{d6Hve7 z#En58ARd`kUGO~iz%UHaM7X-w{scBUqh8<7bK9UNnMyia(n`sCx-??RN(K%tQKh?9 z(nfzsgjp1*g|+SBq45XDtGNf~sIDSX^N*$XLYtVHz_t-z84b z$yF{e5c8Q$3AZT491SxogcDK$T^-Rww`kqnE+=b91i_P;lY(i21|OAQDa;)8ea(+u z7Y~YUc?A+DAtcJ%0MsLUVVaGf&!TZ8?}pl?QC1Bzn0y4#XjGX%yvi6Ow~Snyt*a)9 zc2UWwiaBa1zP}*RL*|G`G=N*lsEb+9m`E#!CJti-S4t|#c+3ufs!I}9Ou?+Cwtx`YDD*)^MoHsKwWyiJ z9rzu0H~jtWoPd|QZr z&iq{!o=ki?pZ=Wj*OztKDLf_nbRx2Qc(8d0Y&pB?9G?D~mhfo7wX@=X-EEP0Vkhvc zemP~(iN}w?<(qq7+oH?6wEw&a8n-~?An@tb3Zs}XM*-ow5~2WTd}<1(7qqpa3`I)D zKCSJ|i@t7~d*Uh=kSIxqVHUnAE#x|@p30{DbcHa1C@|)Qs&Hgfa%N!+xs<5=LnIYe zhFqe`id51INr~1;ITIjHq)({f@tB4Cz+S=}IYG@Umo+xLRgO`$HY;B1{n_9_{AbhW z@Z`^yx2wgW?O~0G14rN1qx|8= z@zu=u{EVc6%HgWX^cX@b{+A_S_Vue!_VxIN0xsA4=iXIFxKHi&&f@)21HzZ?K#enn z@;3|9Nc7E^b39Bqik_diu691(liH(bt$0Rq-d`z&!_izQe-Z+uT11MQ{ny-awg|@M zAMBIqx#KN-iFQz%cXZYYhYCgoYQ;|u$0HtMKpYH7+_dfPg)_@?2X9x5zK<<51ZVoB=h zlwI_tIO1_UZD4hLYy+=!@7!W-lY3kz@V-3EM%y!suTr4=+Hmedgi#+SX>%o-Qw8T1 z%AAsL7#ysgZAc0o_xRXugSQ~wd(hx#Kx&kLcF*B&s*nU!dn6$lN6g;FP)n~5(H73| zdMwd6?5x%tLR1!PeM5KlvaFLhXKsf3f*9dJSFp+QEKK-y8KBpB{Z;!g-qiA^Xg1(I zd8EuNO4db?2MBd&>FoPQ3-eD=C?+{lY}*jsYu@MYM#`VB+yOnGSB=5+?}n*>*Q%D^SQ4Gl$wE<@}BKEdtwe|Gq$5TL}*% z=clrz-q8VkxrbKkT_%6&&jy2h4i^>0EKr^*VsEqfAA;|W!L?rZdBXnINe!($hjpFd zJR12R8sM7CGkmqYfdu_v(8g3_3@nDn#BctVxXvNWP=+&(6b}AO;7GlEu0U7i2S1R& zihgUvar~7A2)aNCI)dYjO_r@$I&?4xFi|kn3;fo~q;z<2`Pz-pbFnlT;{B5HWxsH= z==~C-kH0WEn9L8T@cv*rGfghmz3>LMkk{e;%auq7 z%e<#{7{85=K1i_3CSL&dIc|m>eh?76q7=tbF904&KqwQ$f#w;F2T7rM`nwj^nFtxB zK6@41Tme$^V%c3%`VSZytPivQx{;(7MzN>v8J?ld(JoSvy7q{BP;!f7DZoZ`cQzZ1 zB2HjTtD(Ou*nJRRs&Odqi}Q~ ztOs!>fRwPw^+W=|S~`+=S9v9EvoxYX@7gusElld>BnVY+PLpL`{9sh2LFBHd!c|0ugtVO_FEW5PIC$1vs$9yC*ZHO-j^n)3> zkkK5B69RvO;k~edYIHD!1!^<|47Ob8AoLrf(nT02Z_7F2Tfg}&wL;k#!o7pn2nV}a z5(JE7$|4DJ;Ep&$*Pah}!SOQU5WNYu#VZ+NlW40b>=`G24-lbI+7)R;gSf^Eea?Mk55j15cQwNg00@j&r>2|+_CSsjvVwFg02c!^*2g1PUMW@zh`lyt| zxHi>mR_GH!^pDoK!)2;BvUJ)CLh>az095{_{g&I1`jX-1BRm-o+9HG&HH3O9T-6BV z5V}NZh7+$B;BZe_!F3=2CpWh$(BKXEc{|E)VfkbRHEn^f)JJWfJ&qw@J?x4J;uf4aHf&4-=~2r z`)9nEWM$I{ivayKX?sDA+fP&rF{Nxgg`1Wyjubo*tgA%wyzTLDG8YzE{-z9o{_pF4 zMnCkP-`{*KE_r>jJ`j zpaDff0`I!UJSlc%O$t@hE@sj;70RS$+@#zfi$88kauj<(lXNk^(>w}0W6<8U-)cc- zqI z8PZXklLt*uX^XFJ!Qe0@9Fk~d(!anI%PPl7DxfJ`0+wQ2&5EX5Z$S9=|FB>#PcdPd z#gV{~i`Tp6BK+Q4cVds;K@}=KvGH4+Y$)!(Xx7L*>xL9RpO%d5 zQL)QCP8Cw9t0F#kI!j_ z6gcZGqb9k9%`r#RGl)ZH8QUfx)7Ua&xZ~o~E%xF`CFIC2h4P^+dY^od+L_li^9ast zjwQb;g!a-cQUymk6jts86YX!D;{EBTthR`hh_T%c#$U^tR$#@_ zMAhR9oO^@!_E#8%-XWap`L*7c9Sw12d{fIZ+Or0(P;>OE8zQsyz@51cj0p}rS7YWd zXW0|@YOsjv<+ox&Ia!iKC4r>l38v%2ubva{0$ECN19Nztl}%y`jibLl67Y+xRg(ML zYkjhvI)T@{WNU^IDKPRA2t^J2Le7|-K*Q>SbK8&_+Z;L&5@Y-7TC&jvHiWx$83D}X zhmad#jJ|Bz>7lNXD6b658)tu-;ljQIY2@Z=|k+X+{nS_nJ~t8}~i95{9m zONmqsFB=WBlKL@Y2K!Gp_KkIfcEeU;^o+$6)(Io`@k__OpRC|^zSGj-F}s;}<$ZX| z)~@nBRSK+DC`>u$pe?un;&PoIOI3tYQG%7OY{A~}fLNOWGCjz!g$G8NPLQA;=;jL3c}Qq+|b10SOP*&mVU&8 zX_r=`hX9dRS0qnk`O4GlmSp3=FrLpb`3fMx55!^oAFrs;Ay?tGvdjk|yJ$f+^z;}& zd@;eNqSV$xH~jv!%2dIp(ySThX@Pog|^dyE=KWJXr_g-PUak?eMiFWrn>WF8Q4RHLS2Re zU4|mvIzw9@XtP(prGlIq7wNP?{YV)2$XmC_Lc~j^4?WA=ffsv!?r*4P*+u5ZX4{}^ zOx1{Pkr_;#mdHwC60a6rt-l#8Dj1^W<_>-8z!aHc4P5Fpdi8JPe1iLl^@zN`M6 z%KL?bvWBTE%H&j)>lfj@?W9yEhc@AL*p2x*<2F8nju}4m)y>8fSC>G3khU!72D_z# zGA+F85+x4YFB~(W)??WyrWz?$(yIlbyW;`d^Qo5_yD5G}?0kW49#yH= zG_RKj&gJxlPnt%5B@PI|@ZjLhg(B4+#@jF}ubCcl>%ix?V$1jbiYQBS8gJGeL%5ad zPOe8`Or97cDid%X8MV5_(?%9MpLQCLjhSBMq%GqYnrg-vgM0?#@(zvxUN?xWLl5CH z&)9&l#oH6~_Ao|SDz&@bu12AShf=(Q6N6Y9np0368Rd_Y7zJlTz#hevC;K!JeVB{9 zP6O`KP8W2D`g??h;Zs~tlHEjQaT8sgci_gXqfF-`pzZ|>?%&Tj<3Yc{=7>d!2vM4M z;YOK@9`i;$P+mHQTTyVHC5UBde)*U;V+Yf*vyVi>^Xml6^I_yJrnl-F< z-Z2;dJ0kdIqp3cP8LX1M@Q8SUo+yh`!-3(i#%?iNrrKCJgKI*)c;i3Oat4PTpk)kG z(=$TCGUY~rx}($fR5X8{hVQ^oM~}put^QKr>o7U17cj)_Uudn)A>bTvhGc{|w#MW{ zADFAJy-AMY7f+%CMY{U`oCQX&?;6j$r}osMIIkO`1lS`Qst1mCie_gGoFC#m3X1cj z+X7oQqz>d_YMIlI;H^RoF_wHT(&nb_$A}_!5G*Z26fr{i9!C+{o8jrtgQxM|fC%l~ z@U^GGG<=VL&jV}h9Mv#Hc^~cY9NlnwxY)cd1kM6&Hg>t^**j5c+w@Xly6SDiaV|Jo zhZHt;$$M9(A9Wlt-fDVu*zUGBE;cHj9=Er7I(OycKWY)4wQx4KzscaX-f(=QEXTaF!uG@NN&?aY(No z-55g?c6f$0mGugw4 zj6Zm5)SW#&c>CibFY>5JbEe;!0QjKE4>+Q?r!Y@1OzZIGQfH7jFVSOAxpxlN->yiL z^TwK=q&)TRE%yd7?5-8CQQ+c*C&7V7^LI9-MTMZZ|7|2oLNe*GIJw2PUm z%YTgg!j!+9f|Zc?(q`$lv^)Kq!CPt!!hSa@mnaJ*12sh&3zXLR(=nY3XtvCsYJw-VX?T{3F!O#ZFMvYbz$6QReFyqciQMjDIi!>X2>N{ z41NRIz|xbdq1Z@bzl!vy)CIHU*z~e~lX@MC>4gjbA5wu6%M!s?OTNdrZ?L#eh+GNGYNM7(N<4C{x_8~r{ry4 z3}wQmauY4@eQ9Alnj}d-S*p~yUo@NKxrw|?``nF5HF_+pBHBH$@zJ$ztNb3^&4sC2 z3SmSqWT$NH3Ynct1e<~NqHaRipAj>r8g)`?9@=%@{ z7$tV1LByxvR0NGIrc_uaQyHV#6wOjBco>H%Q7MdBIjUm5jSQ)B*yvVSq1)kgkhWJb zwl^=eVue9}l8c-@BeachOZq+JU z(M53#!}L(XdbULfhk>cghXNqCTaQ-h*RGhlHpKrXBW4DI$;B|d?#46Etl4GhOP-Q% zWqg<)^B7nhzkfV^Pz0jciX)Bap!x+VRTzscJyas8MV}uoht;IPr=5cvade27R+vdOTCbP3jn*gN zml8IOA<;*GF0gB{+Vw7$r*)_>|3oKJ5mRES<{M%<@_>btNrk%;secPj>LJbJZ;k{Q zX{NwQPdj=%OT-(I=B!bGxBg9(v2I&3T0U~;h6kW`_mNpaN>|J+SI`>6iG@zWOB|Wj8y`tNZDjf^oLxksXPme zz9(awJIM^mRNJHv*@5F=!+4hLAskzk53wc^E!X9kvry(5UHNhYoSKsJY}@JsEKKBv zq}^IK2s8?-g6vp#mghrSIlLmla(GM%)A<;})$@^`o7v6=gmN1ommVm$ti?IlMMDaN z5(tWT2nI(qW#gjW89z#$GKe+CH_9|4E@as~;DCHWlv#Y=c?QR+2I0mqKcwe>OwH*$ zu=EiHqv(<;$nnM;x1PN_yQ`8_m};;LiFzK=)?^34P$=c5 zzUo(c*`d*#j&P)J)?SGLLY|vZ*Q}x20}=SrDfGxrEC4hYAC;)pn>)@_KsszK51S5! zFhs*UuT2PqKDX@o-AY>Js?YxYBs7wBg-Yq=k~l<(-ZT+p4f^0BT2$Lw0Fg%79(w1qEeJoL0?i5(Y1y9_yvE>YpI+1 zMmrZ!Gx+< z^fER`kVCsr70*15X1`og^a*X7A;b;)6;c7wHc^1f@q6$?5M`Zjhv3TxT!~BM0#`{l zPo!t0c<D_1KA`~T$ksM&pa6d?J}I2(^CAs?_h(8-D@N6~dTK{bbQhpyWvmvTz! zbY0=h>DwArttuxR-rj=vO9X%n$?~UQWzNiHp!eP;V*-p6kfSa+Ig^JMwl6QQ?l!Ns z&t?PseZUQ&5lJHU){zD^G3w5Ve6fX)Rzf=o^>K)7VLH$T;5Xs+)WL>~y)Z`@rG>W< zif9jWkgfoZ@kYBc)6KSk4&f2YQX7DhO}P~|J3L>{G#y(G$2~=*Rh2owsJ#4_t9_en zs?gbaz3xwQ>5eHFE(C4sk+~{~yPsy#USl-Y6{Vv%X1|(QMf!UD!-DiIFo|>~k302_ zV}ab?{=_Q++4Pz;Fqii~JQjC|mCT`zW2x=jtMku(n^$$zWRmr65z18?szzW_1hYP+ z2zAwJ!BMajgtaHR|ImUIq1El#+PmDyA_+)i?Rp#lV(vE{r`Xp!U8?(Hj@F*)e zX6X}_>DhP&F@c8MBBNQhg!HI{BvZD4y0)3FFnK5>bK^LZtlJ6o$^y1pY9Rm6$%D-bjOUKTTj2K6T`1KYzLKpGo|=D{r~+X}xX>%#x$tyrfw5d$!tPV0KFkjSF`2M`K$sH=^inMw0Om?uqHJx)-pU(er-HGW1 za!{dwG(f^rL{VnGDZHR89f{P6oU2LkngXmu&nnF~*zQ9+{~9M@jU0VduFt@%QIq_4 z@M&RRq?qq!1N4gZNE`CTOK9#7$p8|x!K2BGx)7iIu83`8M4dw_ZeLz+Z6{5M-%&#x zx0Y=*Rc7It7nwex`*@k5DA`vu49_AA&+jsDDosD>2PJJ?n31CwJP{pS)2@YZ zR^LJDy%1$@VcgNG;&g|!l72M)(3s)jd|~*<+p{%Vl{e%I=aB!c{R-c|*~0t_>%`sc zP5yyz!bUD;rvC!I|3JGMHM=jB6so^Haw8EfJOEu=v4Cw!?mHM#jUDA8Saf0c8lm*L zj6)JQki4sXZE3l0m!dE7V;92$7Y&bxx|#c6sqT|>%!${-V~w0B{U>L}rqAQ%N%r05 zUE|x>PTxC(;rDZ+&}(hXt#sG}w{9Y%-W*m~W33CpZ!*Zhkxz5Xm4xJaWO2x2VNaQh zDq}C6P!C~Elzlg74$|&PBrT4q_;#)An&03uEUtj2JMg0Hb;g+3q)g%l+tUi(y7CH8 z$;<-2GP~2w%1r97ah1QdDOs|7t?A;T>00VZn9c)8-mnl5*VGnrfJ$zq+6qu5EmKGb za-#<5k5MYN5j|$p)2jyFRdx*Cku8Xx#C~U}aaKW@tz|Z}vql5tf?{ahYGgCrZ&X@U zkcaS=omX8uD9aS6NXxz4m4 zqbgsLL1jhImqB!N$Z(!P}(-k~SY-Hgek=ifFnSI>_$D3m%PUw+e}OxM66*h%y%e+6~YNirG1KVH43Ih zzKK<7sFgl@D_r$%^~ao<3LU1ml1rBky=`TfPBehbJF_6x!6EHxfg-@W+{RQ}E^13u{-qBN&H91gcWPVXGG^wkS*($`cvlrQE7Xni43Jyy@0Bhdd zRauWf1EK-(%ePMnsP~xG;p-sGZiO*_UKyJd{vpFWqJDQIqr9T{t)Xvcka!r)0+6do zB0`eH?T8KBO9|oJmj$m-M~b`(O1a_t$lXF+@sDxC3YIebp@&0=_yzfl76z-N-ni)! zg`iHPr@ZPO?1wpIJ=t(7{fVU@A=9A#UZVZ;0nnULDHx71q?t$@yB_w?>9{s|JQ2|Yk=^PSPAKVP!bS~6y)m(l%edxM#adXu6Yl{5}@ zH|nF{_-=lxU)MAj{yNq+O63{c-1`;%(~fDgKPzc|p%V)?xyAL9Gbi?l-k`p34QV3V zLopWPwtSp?Dl)0&j!XnU`|-Bdse>1QpB=Sp4|g}t2)6_)?u`f{K4*C&3RhzpZ-kkK zMgG|%6zQ#dr8?4AkH4mip+4YvMqX$`T~a8*1M?mmDPy)NeGkG-YLvy$ZRUCyiOWw*`tnMiL5683ZblWb>Lf9E>~X44(Xc5CEQ9A0XbF1`V(%c;c8ANR6w}G{BQYOK9H?k((pb4 zqNQ5sb@V58?ZO52Xo!X{#~jj?eI>$uldx#JxF~0TctY{at;l0K|enxnA<+qwYKl~cwfh(5Vcm9)z1$k7V2E|$F_oF(N zcQE7KO(-He1pg>Zlu;>51ius&ga6jo<=+Xn|5cbMo4K1gyO{mAhcrh`=F4mHD;m+A zSclTeF`3iNGc*BRV4)o`Uoku|XDK16m`q+yMcu?Fu82@Z2smjn5tr8=F#dfe=sx=q z7hlI8wBBMU3JrEQu9&(NnS<)Fq4szt=gsdai|4@L)9V>Dkmgx(1d|$?(oZ2%a|1LriktkXFG4_Ipf{Q829Dzk|1 zJ*{+2iYItVnypiBgYKbvrjn`;z=@n%I!tE@dd*F0w6>okWZIm|KU!yEGwXRe&9=4l zj0`!j#3@@392YB`if9140hj;`Hx&;c=8T2>acT|IFtCO^k-Moh%eZKx7S<V+&DEi+bvtB z7z^*G)jQ>=t&xR?BG$>VXP`WhaEBx}En3e=r&@WZgx=r59kv#0w@X0zY0`ydy}b~? zCS6=k1laI zKfW~(fG!zngS^j&pd-%hj}xzie|;b7@(X5BG)I?-zqx!{A4Hi$SA?3QpHJ3KVZ6WIyf{rnvy1Lhb>GD7yfU9`NN#=o}8)Sx79@Ezok3%lQhbL!v^2=AcT z)9opP4KG5N(qBm9?mxd(M0pno@C~TZj)ab5umK32PheSuSk~6_DR`fU3>%wiA zLeeU-HgtA0`W$*9Z|OL+D2uBbXD%{ggR7^NS#j^*8u3eipBb6F#9p|n{AZR(%9o~Q zJu6xc$3esq9mz=E4VOH1m0d1n^b#VZuC4By~)n1+zaw7ayOk~0Q zuOp!UuEQq(e<&vYbEBlO$e;IRKbo7LE3W%G5C+)5635SV(Dca>chHe_X`XDqHPT?`{D<$! z_kE`0B-`ihZAah(&JZjw!g8!T;!@MHaE;i2`u#jAqDt|ldM*Oa!1BB64`Lwa0UHP) z0v#1R{kt^rdi_*Jor@X=Sarnd;D+4kD9`t8wCmi!CoFFA+eUNhV`=dfpxICgJYYPu zbV6J_HA<{wyxy+5_()c4WkFeKrt7&g+e@5*_Dn2{g{e6&EmAv0FWYdnm(t2hEtk5& zy3+JF_=Ku+Qe`b;y%xj(A zaa~nU-pVI+;PO)lp%fVuoCGp<`Vmy-?Q6QdZdwfQ(00%S%|aup2ALg^zx!2GHVaA! zjuc!hXt6~CDpQ0PAKg(c5Dke>?jChMOm~euOk@Ab)3Txsvuif(^d0s#Mp<<{I;Hfi zbQm#-_p)KwewZvA^WFQCiGGo&&Ct;zU6W*Diw{`VPx#0tVNN zWC$I@V1$T$%t+gR{j*p#_4_5l5?Dqv61?{hs;m?Ep8DR>;)mn)Jt6tgi)yY5v)S#3 z#?0o~_5edhN%nB~4ozjRdFBgP!u_8XK~dCQlq$bs;h(c$LQ88XRjRk#@aqSe(TUkI zxO$m3#6RY_xP!q6TkLCvw6*u;M0v>=ls+X>1iOW4zEQj4TfJyYDO1t4d8cAO6x7>K zSMYi*Gfp`~`Sehc`(g!)`St4Jo-4z3`Jt&)HzoAsHUZ_@|g z)T%;E1y-cw*6h1b)*B@zub%2vtae5`wb{v-nGT28{kIN1xNAIpVhkA`@9Iv7SHre` zkUA0ub0R>sV-3*tV(uBVIcC*@_eDG+x~O>&pAI#_4fS5M%tF(DfUubv2>m&kpcmk( z{c8^v-Y&5?&WwB0a`+*$QEp&_#F{%Yeo5Qy;+r$u{C#A z=Fe<%&O?T#JzA84t$Y6+`S_DE#c+9j?|XtlJbez)^D-~I$9yy*{R~ko!k?J)r#-Qb ztj#cwc+&{nU*}q&GA`GlO~$m)>4}!JcroV(N_Qw45bb(ztJ3 zfI@`|0g#^puk6h1bvW{z4yVk-Xmy~&JW-r{u}U8zZ2XY$x?!X^B$!N!pd?HLi0}J_ zkXl*&j~j8_X~Uf`XE%T}J|Z*QiBWdAEpB*4H=KT8<_!HCIq>_n@P!<&NDl`2G>l;b572RaNYgraW~n>zrM6gDPY@x(c!*%y zY7um6q5!#sKBDm*`|mGbagY2#$ytK@?Odh+)P>=nVHs{cJTE=C>!) zn~n5ZjaupoVugjq9%0ut=9N?+Y|~&Rx=>==vnvMXufRh7$Z-vD$_#KxR4mpv9CF*( z(S4pI`HX0HG^I=2^2Xv@32D^hg-*a^tJ!uKfS!z!JL5E{DWY!ql)STya+OnwxbX%@jkJ3(W*538V%xs^_r|%GkIB;{dEe?uFJ@B>i)%(_J zzWxGwMkD9*cEL?xpCmMarnN}}C@`QjKs(PAJ?0!RVtK4=i*Bs2bu2GRl$xve{#i5w zR4qNYziio@k^k4X(fgH&a_K8Yfq| zTRlmCz5l-DeX}nr*+s^eJs?_O<`)tlSQ&4;j0$x?mE8}Dk%^|p(@tU1d^{OcfYYpax`hlptv@W4q5h z`GqS(v3jlBW86%~=YWTeH|C&%->`NAzWEE{CnhSlFj+DgdkneEQ>m2JEIb}EdpNJB!a&LLl_&G7?>IK^H-Yo=6PI5TZ%uJQ6 zL7AQJb@5g@+yrDUHH2Kw#v62Gc)*a);bz7C>dS4M=Jam9(9bdtmd}*TP=3C4L zc0Ql(V@_bSagv}49LO{z&vu`?9dnC7qH=T3t!5mod z-Uzq-P}O?)JIVmV)ZlI#HBZi?^QX!F2Mwi3#{PEcX%Ip(?Ib!6ApOGl9gloeQTNqM zP!^pJtN(4={ofIU^#5GXirKr_x%?Mt#4kn;W{?p{yl>vR8T|r8RJWb|6s})BiY{JlbVr)7S=%Kpr=9mR@;Zd1+Dp=zqSnwDXGY%b4>>=7$LNIMXD-0@uYh} zOh)s)shGwc?mC)?BAqzKQykMJ&w0dGgs&v|pB21Tu(NFb*ZnMgo&RSN7xVw^j#XTp zt?VuSTaTec%}xVH4auKgYSmPaP*i`t8AiRDR8d&xTqeXas8Ds=mT-N!O~yHkmh@}v zVfaAsjPj1?TYqACsuW-LQJCw06Wupe(dj&CbbM?BqdH9OxF}IRpmStCN>Pu_BU(a8#o7+fUKBF^NiN@{7DNgm~G`ka; zD^x7<%u8N_HPvM4;DoO6AX7Ex1*2)k^=Zfgrc*JWAdJUOL8`tA`)$?aL%w!T3sRyZ zPuC>TmGhqcN$xsDa6r}`ySuyCz;vm76^wJ*g>8wo+ysY7yC?I)V$yAIwoFIujbESR ziTyI&N_AAX7K>EF1fD*cso!*?ZCck-H|h~qm_Nf1sF`_>euX6BA+z>zOXEsj!Nd{_ zmF~t6@N$#w@7>nI70`ILM+cf2!^*~pP-`?M2*NrEYG-F-h*}1RN(8q#QV1dK_ewf4ugHE4vCVo-U#oP| zEYYLLK_kXBK%y_(vV5KlA$x(nIz$}8u9Co10!KS;7eM0zLBEg8X2%m8WTk6}8Gv4- zx@f;yvA(z}kLwb!_yc@{I@4bQE3A?7qGqG^f&@lO?rKyIK$G*Q?=S0{QS>Wt@1iep zJLv%+pw;6WV4%tRf{}ZZWJLu=WKFhLi$ZCBcFrXG=laJzArVf{a@kqUb_nUm%r5Pv zf#Ms+4>GQ}G)uoYb+aTs(t|US`uio&d)_ZjTlUK`qv5Z>VpAHf z2dH%q%uH{nuN*vfxxetDh$7*x*y_*FN&OD09XnK!0ug;<)Ig_O!?{0nQDVdtA>~Q! zQMM|oxxaJ`$qsyUj>hDTa?!ETxp5!FVr_v#3`m2E55l`g5R`#Jy&@I_yRI_n!fY zVq)Q7_Fw%7h5xM|@!#?L|B7bnMz(Hd3g-Vc(*2L#M5>yN%7!>9-+7v?1jra!C~2(> zm0W|A<9JbgdJ@w28mhX{VJ}uGnn@fTLK#&c)~Ub*!)cl?Aq$o3dWQUMi$LXK*TU!n z&D^e$huhN?-^n(g#_L<2z~^^IaC2NG)P2lBW!xPjtpVv6+A61NBfWtqm?&cx5x6JR zlzq;i63VOSmmo}h@f*o00ZLC=V?L#8)q!{zf8z+JDy=+@7V(h1rQ6chq9fVq%41%Y z+{b9~UkjV1Ls)>WCkuA=+or=5OUJRfDP#|9fZZ0&rjelx9L0%U)t)s_XJLvQQRI_J z)EsWo(E0#(k0rvtYMD{p$xgam0*YwC1mb1mqE+5ekGFZ-By(rRE{(86CM%=oY zk}jdEArGQ93y%n^=DSK1d@@D;a>wq!TuRx=*AJ82Jynqox}R~rC`|~U|A1+B3_fd8 zqAz$<>KuBgR9Zt0D<kF90+XM%+-2X|mbaB)*hiDI9?%W^t7c@%MG9r8=C&~8_})RGsqt-M@+48GTex$>t&#jL5lXuNf@qgFc#2a1 zp=Ve^f^nn-J=Fo$&EM2K`RrolWS6@Dq0Ui8M0uY8e>?@Pn2TvTeC0IWzszX_|34kt z-*Z@_%A5VVGV;g1RE3sMXeOJj!Z2IvD)nR41@SchNn6l-t%iRmOs(zdH(*y!%kdd; zjmkx_{_=Y<-==9$ICX#oksVZ$z|@p^#)Mp6y8i5*>ts5>opzPmuBBY;Q*4R0r>v)! zN%q;T&%4L$FG*-OqYV}&2+WRO2OG$(jiU<(1jy=53Cz|HEFr+};vg)xa>K-cMfoI#utD;VZDeXYAlB$m+@IntyCaw2OhMPLi(X5U~u^qH&{}lT~ z3%Hk@94}ttvi=2c%5%ugYN-S^3#$!Pq;-kfLPC3Rv5~-+r&(&JScX-|m(L7hYJyv@ z#fi2Ahl!;=Ut`>-A(vaP6-#}P9N7I+E+a^QQG3!3g?cOL%*0-%#%ivqi=>HKLnv31 zYBu3H4Xnswr@}-OdNB}>Q4y0~WZ$oWfFxh{2vS>4NT5HIqOXEgXRMO4Zd8I@E`~1# zJc&*f>ix1pWguoB>+IGruaIYj>#WN0y0$MWUQ5cZXvYRnd;am`rDseFj{&KkBH*ev ziYa38jrzpdk$YA4`f|Af^!J88G!Imxct6aNv=372eamKGAaJ1IUIW=PfibWjc>OTS zWjU+XOeCu++~wA61c?wLc^5Z@?;r`Vtfl234?`-@()}sA4pJbdNs1YiWPVo?%Op9G znzS%Mm+ytA;GXC4rda=np9&fKJ;SN7Gn0g?XE>vwU$GKy!oh|SY(i~WNzuxW%APy1 z)ybGdtRO1kKnxV#l*OT>>OEKJsV-+MPrSz(dN{gxr9HoG@qMSP1vEEbWR(hBj;D^` zFv@zljiJvrjsYH%UBiw((^_}bC7T9*n=tEdUF(_mo>R#LItz3i zYYpfn?#wYSx(w>1g>24%` zi}(Ak-U~4{0^dsh#^|QECB0<0A*rf~GPkkVwP$muBt6}9DM0LAuqhxAHH&~+nr=_0 z+U?XkU%v$rfv~SBe~b_C#%5T7!E^^R)(^DM-FTu2LcGI+Ja5>pP?8Qf*YX#DKIlgU zTN;bsvH7Ljy5f{{5R!QOpleX0CSb`e6s$hJzisaQ=SWVr%}r? zN}lFfE+6b!>7ITQu?6!9uPCMZmPTWHX5X>lSAk2U)^L`2{hEmm#-`r~4EvKFFU)nD z$SC&!=e1ZB<2tqg=%osWw5&9!p*L1`4sb$_MeM(7muPH^^0FqR!^hmqI^s=xAd8sA7ZDhHs656CTxus?2tzT~-^|Gi@nCK^sMb;v^1Dw%ApH%y5BB zCkIZh2CFH9oJSaC=q_!u6P<5$PnZp}7)_}E0E=6a>iF;}-|sgHI8uxs8700`4Eo8G z%^_ZfG>+E!$=5H_xHIobX3WStQ*sModdOoFFg%sF1{Yk5y7qf8$Q@{a3nCRmFY6+$ z-1Bq6-|v5;{!wlzT|UTgzZ_&;;Qy5n`SX+JUrY}P**cm!=~@_C=^Or&2nvjpg7Kq+ z3)DdtiWQ9d31yDi>DN0D_{9T>XW|t!!~z7{?|kPd&Zfsb^ zHJW|rK>5x0*@T+eJ->r=3Kpbkq8CjP&WvLV=QYp!m^fGzvuKGfbV&0I)R}LJ$X(YAV{y&aguc1p@oxf>v1v=8H+}E47Ze_st2vFD(7nb7eK!oQOGHfq<%!qllx zZ7q9&=@_e%a5#%td&P_;5M5k}7)`bR0ZI~3=6XH{*wqRT)IZnb;LULPr8{Y;rh=!; zqzgH%Hl$U;SdujXdlp%clMiyXU{;}F0Z9Zvqs(J9&$410V^DciCg)qD);xvK;wa-N zLtW{+M2&T>f&Zo{vcd2&(N+jh3;Zq-f?xAa zCSM)#|L~Q64p5%|Izaz^RUH(qW$g?NO!dFkr2M^8&Q#J+KoUXvNK4)DwQXt9L;?c| zJl6J^Ap!v&fHd>_PDB-#Lto^4q7D*_&cJCt*l{Fs4H}1v6Ia#}SI{tF?j=u=IChe1 zYmi)+vs`vD-umF&W#4vm_4(lfy)A&`aNJV5hhoIEZsi-qY1dLN&|8eb6Mw3P{dCvcp4r@Hys3na@eDw|ASQ%{|-8oX#B)cA~R(6F7%v!B4 zmb^^}pdTFJf-v{AS|dTk8EZ+$36;Cn2)R|pOn=cffK~s_PAz6-0RAU5E0mDM{e0G^ z#~F3d*>KcrQ^cX%Geo3m3{_@1^(n#AbJ2C6ioyoa)@ofgBIgN+A}>+rfrV0uYn&;1 zB9PdrmQ^yKIy*U1b4{CUBeCsv*RA>t z1OXL}dQdB}Lb8O2c!r3fbueA1_8jJot$qS*(Pfm8UVVwf zmI92o!MhAiFcIDTiVG9wIw#Rm`o4(g{^fiCW<44*BId*E-VdQAwfek8rm(ZTXNTU0 z1N(lPF1cV9x7mB=ZFCx@YqRweW0)VvX(Qzb4m?`7_xT|3C4BJ?$pQ64bZ%ht7?Y%D z=lNDV%Q&bfhz0rJna6oovf?bTofXB$o|b*4s|vU?6zflW+-dOzxH+w`#@9TRpnw}{ zBf+j<{YVFZ>hGcqfrppp5;Yc9s8ZHB9X@H;uD%1+1+q3-Lr98lqs%20=ob~hT9U)S zaj-#?qJ*eij~ibKDhT|@dDX}v1?CI|l9&lI~x?JjY#KI$-h@!!VSV z01`{eCB1=oEhCs4sV+nC_Ch5+9mNLQM$Z)iGAj^fz!)e7B@Fuv$PwF^&Zbk=2`Vhl zqh8P!5>f4Knio@7iG3}+tf_a0!CZA-mUT!$_}X2*=!*R_oD5 z?kjOE{pH#U`u{F@{ZoDY4|X!n(t1`FY4{U$%xAWhy_p^N&>n&^)4r7swjG41*LZ`mPx2_XO?GoAa4tl%x~qpi2Sqou2FI6W-% z!PtoHWQV#r34>W;PNd;>C{Jj14@LIBThusbtwf$ z6_*fMTDcF_EO8*-jxBXyT1Y)qEIu!JeyTzV;k7Oy@R22cmVE^HavP^qlk-Nj;5N1_ zp`t_K4$-M3c16IT&ko9m_m-4K{Zus()nR(;YyyjNUCSSl7ij%kfh95#ERHj3;4ov7 z8mQ5*$hi6bH687IfqKEXBMdqhJ8Pa6VScR`hG3aik)L&QouF$7Qmt4_FenQKbk%{B zdkexeWmn4-o>bnZU1zu%tCcPC?o*cjiD&7U=A1%Bn0PUuKJKgz%`xnt$ZIpGP}-=5 zm9OaFA2_Z$rnkipe1>92Mwjrpcf?@v(V51S!i*JpXAFD96(JbS&wy{9`rtk6*_LJh zbA%v}nHzs2_N<0ZrdDFVxaOOuvk82dCjJ!3_=wRXP0v@R7{6bHqXVVf*XReR7-9od z0q3^x5@e}#pS>dGklCGF>$jLK?KEeC3Gc+*FAQ-)JOjVLUrFF8{S90yww;dF!a;@h zov{TOP+>WzKPW}KF7^ie1QrpArzs{@)~Vg;J5@53w*HuW`mkad z4XQzkCa00DEBz>179F$)0}yYkyUV}B8^xgquz3OFY9DkDQK^2P0L?IT));x?T=bo zPN5mEA+)1xdi1&u4B`}+tDoe-ZRIXxwIB$vgDYUS+8|{ev?JM+I1kG|iHyEmKS@D6 z&u+b{SKB0qa9I~XsNgUL6og2m9z534Y$T6!3Y~0i1(D2`UT?WGsotTE+(17I5R*Hd z%D|(HyEp!8*p?(~C*!1sXRopbPB}h6(krJ+Sz`SFvk%cqYbGJ=8ZFAm{+C(S$l>oY z(o*DBp~a0A5-#s!#^DpRLGnt*3@hN1UVDS2p?bP}iRdW5;qhkFXQL5Ivq+UCzcR{| zbu4nO;_cQVEz=2xY3WkD-}Ba#pY^}g`M7qL2%VfY!@$Xuj?yu9dNhG+N##6a z%+W)9C^2?JRBN|w&#%|D7>N6|J!12cU^7Aelnskk^r+ZD^boJSb1=b(%G2nFJT6Qw zdMGo!SMMs)1H$7DE@!ll%#_IPj8 z)K|AfHP|1StSRSouEf~Er0yz}U`@-B<*`)~_sdGC{w*OY%c0M`E_4aMS>o{?KRKF% zi#)V3w0x#`yJXL~te2+)CU{NXkH-kTjeJTVH?~aXPXAtccv`^R3@8owj0dZjUhIw3 z`dkF6jeq2|deHKeHmqdf0uJj(y zxV7VfW~2{JsQ4mYn<25|kKlYxAQI#61s<6Tq6Q0yY7as^BJ{%g8WW!Ynb(EBH*hEH z>SDnD)FrsRUv9+#D`)@{yv38rs)@%W6Z&;&=vJwY~%izRDrAHvrA# zi;0p%{pEbEsekgZf@*R7qox;6HQC_df({LKyx{aDR;%VB2(X#JZj^{XevnhwxNKg$ zlgE-`^6SQA2PpKfC)B9WZ0GT6^G|jI)7OO%N_<$4-nGmBz0$hg5I_Gxng{$FY5r%J z@E02?{sqPV0}!ZaIU@?8c@;_3kFt0m75e4KmoP~|z$*rjDCRs8s*4lXF6ZqsWi~;M zT5VYT_zk^z2ivg=(BWghk45`{Tbm+Ntv20Vlr0H*oj$f_2}D^kMOA8-?2_aLq>KK#()S8#9nt4U znaSE%X4ROR)&jH#Z46lzjKwHmf6A56vq7Za3^&o*AIVv_8P|Otp&_4-$yUm^W zT1SxD-sq&G)bV>W^G4}Aej}uNluM29lBve_kJ0!MGT9`8whKnNM+8E6Dwbp_)~D*^ zwnt{UOr_w3FZUcn5D81`2y3%;xN43VTEiuPJJ;wqKdOrpG)ncG=rrd$BY+N9vR*?AW&q#II$ zkaRmbK3No;mU$WyW)C|-w*@TKe!}c8t8yEq+L=<6CS3q0{H+?H&SzXkqoys$%;qOX z_AS5%9VpT}Y@5$|(PjzVRe)~1`B z-Zo)`5dU zuO2Av@`Snbs$|TLnveMYR7ZfG32D>sD$%tmRyfPZ=zXsH0N!6XY7WO3 zkW@RzdnwqT2XL?$TG(Rzh-WTdAwT? zC+piD0p92&Z*<{U!HByMuv8O(4_xJunJl-7<@?F{UWF_5DO@l zl@N^>aI=;*uu5RGQrCed$ornlQFKPBgO1H&Z`El`mb0MVKD=Wtpa~6uaLE(>8DH|S zuih}#-Tv(e;|DsYZAYIvfLw<(oH}cb?$5a&NjqXn6ekO4Fn5uVABpKKDq43f_V`rQ zvo#rAD~=ge)RKOlBB|J3#1n0b>q%R#!zjhOnl1cly920QB}l7Ku`-jWy)kB@#8JP! zL|Uk^M{DM7sH~b>DH}bnB3!s#2d{VF8+o2uZZkew3eP9`sS1K0@i}g1!4y4zWw;Z)6TUOdd{*Imk>OpfuRU%3=wRQ2|W2&xg5q6*&V_T)J1;Df$h9~m>VA(6Bl+a zas|v&vhyG4p~o&?pz+Hv<_~21Ka|{`L*p+{?Z1S^pJKu?lJ84Fi6nTmv|2*hy!iwE z_cneTGSWKf4r@aJ`21;Ke(Uu&9C1lF=Se-6aMy{mcNc(V{i+-!qX5G>kc+i;tio8n zKGBp4R^z^1q-y`fwg-OIg{?%E57vgdvwJDTCYHx@JqK+zU0gs+Pxz1Gf^MqXrQVxf z2fg92A7n8Ch8!ZP;B)i9Wq%V(4mt1(hJT@;+OOY#{!-!pjFbMqzEsZ9(9w`c&`{sn z(Z<5iK+;;*;GgKJP+3C-Q3dHEN{}Qp|A(R=FgmrFPmIr^a;!SOUtuI)f2B!fCebh* z$As7sfL`^ZS9iyw$}~?vRh>F+yYi>Z)@s!yV=EYbMO71Qs(sy5bNBm2{nR;Q_xr;f z?YGJ8H9G0-K)NV>c^+-3-9kF~mLfdtA*8Bc8^Bb&qW+ZL3cpsmN6}|4?WFoZJgc#r zBm-CCh&2}_w=KCZvkB0g;2Gkj!&s#lNCI5pV5Uj`@nJ1s;80b?p8+$G?hu!hbjVcn z=TcIU+NqK{os3zdl7?smrS7kFS#*x&F)2=#{S=x+vhu1?rM+*E?_UU|? z4OCX7QX95x--qPcYZJ)8NuVTHBZK3p`>*WMHQ2rS_i(OC^ApCT z(v+|aBHm{rD$JEm}Sm+1M2jyiIZ#WEQFwUC*L;@jYdh5cFxqe!Zk#jLt ztMx4y%rs)e8mXr3aHOf$He^VS=o~p|k_fvuAfM3KQ6H@&1h)fAnMaL3cxYleGHKPf z?xk8uY9MDd>7^9mvW_#i!d6W~v}G2HdbPS48E3YHwx{{d3a$eZ)PYav62@IpX zLB{_01*&N0rM1xgK0S6|jYjjiCCzVr5RaWUwwd4gPFsQaU|D3CUf9@$FmH*`MBo;A z@co7wiZp-i%_(1+_1T(v?yTy_Zr#=m1QZmVqx{#XJv5_k7{4z8u{_f&_O7fybJs+v z2>D%pD$@odR4^vIDBO$l`GAxPG&)+`40dO;bq~q-L>{%WffMRMVBvCy$tim`F=>2f zWL1Yzr|Q0PmhLvLlhgPce-xjJBBV3kY7B?yMwfh3QX7OEZ8eQ@xrfVJp`7`MJ6J|; z7v36ue`sbRPb&rt3&2bg5V@N1W=yk$z=+f;Lc9hrc#ej5TvYa+VWxP|#mwkvP%Dp; zb0-tbCT3?Np@scWr~UNt-U(1hikHZ=e>_uG9&Lpd82%4JEmnh*LNz~O;l0Z)$C@io zQC#ly!dRhc8T&@49}ztU19(AT#F@D8D>NAGZ?EFZO8g3Cya;5vPwJ;4WW0E2;19`g zM&>+xkm{jLqs7*w95pxr%VG_63MTr+GlO|vY>{;MWcSQ2NxxTXU}g3)rlz%GuO-#C z9f7BIJ7`}3xKAD{MqV=wMyZlnYm9ZV&4{KktfdlangcvzQQQ)y(+CDFi3LTsKg?@& za;>&|_&+&dc%l-gvvBaDsdfA*NB{fwCSZg-Ls+eJVDj8j}0$85ItF9=3Y(CzLFV<13Vbm=fBoJ)ACC`SW-YnuuioyzJzsTJRv=7% zS@jM4oBPI}Yo);dQw1@#Gt_tZxALK|^<~5X_mR}(+=!xHj`z_QCbX94JhVh$7DGZJ z0zDt|eE8TlKX^v7vWdQ@Qz(Nbb-%$o#^zo*?K1?2duin4)Mb5`zVUu@H_rMEb_Y3T zIFc&~q9wE}T!SVFa%86|92wPFbKTHpu^{e-L!fAL*>J?_xa%hwuzaXA!#Vg}+!H~V zA^`TSNiwGCQv6RAFG3NhLF3sFIvA!aNP+PqaqxQhn;FsR%RRh#0q>j#JOrLrfy&=F zxZvH?$kF?t5{-n90q_(^YoVfomTA?4beOTMqfNjxisd}V$^eKu8;pkc$I_)&p#=jOj0`;gl>=yOdZC_PWA zH{4M)1-~DmV-zf5N<0nCa#L=^Q=kN0Z^lxfMiQi{8kT%W0QB$LjwxW&)y_!iH`E$l zwYE%MA9n{$%JR_M~CZ)6pA>-$?k+V7(dgOla* zCsmQt$Rz=H3zHO5su=MQ-qAmYt(Pwdux*iofvSzwc-R~CHtp14w^XFx(!ebiCLNB} zzZ{4Kj_08zcnCkdkXN7;OHJh=^b|+; ziS3{ni## z?du(?G~=XwFJ&XxS7mqsy`_2%PZfCv2jwxN1vO=DAE~3IiG>x!A>#&eV#H64>_f!5 z884Niol}))Qq7yG2@>?)>egO-#^PFgkJ8#43P(g(H3dYP*m6fF!gbg?JKg4lKGQ!u zON31F3LjFHp#|Oc1Qvxw^z*RaX*2~dtp7MsdQ9W$&97T)@zn?a=g#o|>0T?i+B+Cp z{_AlTDvBw58SH#;z!`<)w>Y7q?QVt5y9Da*TRM!VY5U! ztg^s1WTr+dVS*e>iE?|!XO(n`auLV5*eS-Qum3F2wwK~=lYSOQNGR$xJ0X^OjM zt7)h=QfFDZIYk0Eziw1l@A?dzTM0+2RlGCMX39)A;pnkxDT#MeLr6n&mh8)G(vCvG z%H(fEd1{NSLe>aSD3F#EV#8(*9z*tCL!2N@&VDM`b~q*TZu1Q&F^*uthr#FO8a_F~ z7u7OF({e)$dZvHti}+pTtdw3Rt)$OBBH;yYe0Iel(Qz6S{ABdo<6$6952^2h67SF+7EIc3ke&~Cee8vEQ>Aa$`))I zgdlW$M>Y#9)X%zlq)Qeza20SIoD_v?_#OW34u|GvkZ#79;-zB6da3G)as^c)JpBz7 zElPjCkfz@CXOePI{pSKO)Vl;t)vqn&T-}rpq9R4((yt)jPs876MUo)f6K`K~MMtGzL|nN8K`J~H0|a?|eAJPvSHWTV;W zlys*Nt%0 z#U0NdS&z^PrFw`@#IH;4=TR3}&IDFd|9L(W9?d=AK?KQtrHp<4?&s$}LkTlfkre69 zTdCKCIs6Efny(15zfoua!nU#Z@wfD5sSeD8^)0I+2&BVD%^<=OAB!&}bJ3?1JSYL=ngA4)vy?N0FM@kwI~K^ctw3ypMdFFu zGOb{ef6n1MCkSVQE$(aUntq#y4({8mcdkrsHz$G~{B|_$#rV4W6!-X5hWjPE^T+Sr z_N(tEL)H+o;fmkBOOmBP_4tE}x$^fWe)DjVr{N*w%~rZ*2hN7wCimd$orA~%{|VVu zvTgRgD}eUxCh=R=pncqfEBOn*Pkps`pbq5r*|RV{#EsPGD>ZMIR*2_wR6_Q3zEH0- zU`g*2l7~(&?EZbQ{6aW{SHbr1_b)$vZ3sM~;cOtdp@8}Ky5Ajn+e+UbT6T_W4XP`l z&tQ_dlL|RAi;_f-!F_l4+$N+W(_d|p@ZE@G=Z8^}2RDKWQ-|W)>P2r-ow#U7kvmZv z!7J$#nI?qA7%N=#tJ{|-WEVH)wr1AstIqBNCpV)^?)kk%bdrCFsSnMMU`lZy;akfU zTN1~mC1OoaBwP;505{oO5Y;!;4{7p=;L@MS>t|4dbeQnwusgryf0b?6Dg`Q8X#^8Uau&(sEbC%|wa5jJV8pF1mx$;DWY-(gdoAp&raKjrfWh0m9E>uSg%RK_dCrZd#Z?&V48?TiWD+%Uk6zJqK~#ACopV-%q`P~VA9 zM-9qL+t?{K`z7(T7LdTtW8ErzH@6k+U8C^w)hRn3uto0ft5)I)vqGLr(G`=k)uOQ) znc_syt~?W4#e(G#jb9awi>Z4hq?p_zk$5R%QlJlK_f2y*AZK)_XO$YHURS=MIRuXK zTsEh$ahRh}?xJOZ#XlOXa|IjE7QFq2qhJf4tg5wcgUK@Gb3wKta$3p|w}>NV9xZwS z-@nT@`@=wuc(rxX5Xl8EVeOpNt73$w+q{#bKTB5?@sZe!r!MI5uC?G*6T$sAM&0b? zB(qr(Pj31-DEIpp_lwP*obL#Kj{zKAjf$|G%6}$))CIHT5hxlFW@V`D>4A6?x{uu~ z1+b1?wzb_DLt9PYYoS#ElPaamrqf`abHYs)d~P|dQWG5g`HgH?ClX?X@|jh^7xV^xmS@ZrC(7oR{M3u`3W2D# z^9k-=nkQbnSBQBM$0YLXvdyz68XS+=V+;o56h18%?9?_^2W#w9sWlyz7wC(DtVOV~FT@>E#x5X#?+FgD zTw=O0ihkd+ZLogqy(P-pRFHA4{t$G&VEZ8SGSlxAUkfCJz%-N*)qrzibVF}^o>A|V zu`%rcXW=1B6i!h0D?RIeDktXOFoDf-G<1#ER}Rw@;!fi^Qq;fogT$R?d^v)~&M;sZ zaeLCLe`bBV%z^4A9oD-29jfWQjeCS^%X<`Tq6z>x``l&7$DZNa61fh@Ch?v<(IcY( z!l+B(KiuuJVU>ZD5g!|dvgfL%S1Q8cl$;-}uh7!G)0v<3ST^?MUtMBmb(7?7oiCq5 z+|Bh#<8kmZr}8DP+Rqg1GU0-Yz4%1^`X6^(ba;R8u&rM7h_!a2jV zd>hMG%~O4cfl;F^Mh97W4RJZn@XlsgXTV#OE6n=|ectH3Z=Go(A@y!w>ikkmhxiR z1hB4-TC(*Shdr@yuU|eP|LFGJJ#Vqzzck{_|E3ZDGm2;auTZ>#wUNW0Sntd~UP8V$ zq4bNW#1QNnH|8~X`Cv4g>80_znCj_>Vg}xNt!UrG8KN#}A-=1SNJ)7d!(NM^ANes? z%FbFo>P}8%a@rrBXSTnsWq*6D$?5?>i4WOxjGKw`&G0n@qzphcJRs}le^xAM8Vsbf z+-aDg{nga)TYkZg(`ejLqlpRKNpsjD%-$4-LS*?=oXOIaN~VAmTLK4|A*N8zAMWkg zD?T8!%x?cEXD&fo%NU_fX$iqez(xEmx;MqUY!bZvbGrO6>sjVqJb=FX>bdRyn=Ais zNqd6xz-)iIH`0~yOWHnA-PEMB2l+rs;S~e~<^{U|To=u+ZPx;yqzo$WbZqtq&2ZuR zFgx%?*mF{g>pk`RxwHAS@0J7D8;d`90--hwer{Vrl&cb^;5QObMWy)EAp3FeXsMcl zw;DshbcDa_x-;BHeP{(4O6+)C+)|PuVb*f#bwqS{c4Tz`P%Wboh}J9#CGg^Pwl=)?pL7`%(YyQ82VoiGOYb`@OkR(5zrg#ew zS@Uc`4u(89KEeSoxHbV?Owdu}N>E`Gc0}a-hf-mgDA)DZ1m39y02mlv#BVxdBV*%9 ztSikeP3`X^`t(uY1_;M}27WUMzNUb<&=YWN;bU@Mp*5M**&+72t9_;E?Tv`0FM)Lj zEE8>Ki+JTWr%S}D z^BBPe48LMR=_@9hzha_d60F?%*pX~->dgz7KYGc@L#xf7EB8#%o?yR!icgf6zfpH?EM3DR0086d_2<$wuFM7SjW8q$h@#3Z_R!QLq6n~PR%*N1<` zg!L`u$YEKn+JFnX>)Y4!9k6N*jv$*&W~ynKoTi?7UULW-#}$&2m`g%SAMvKYRa?C>ZFn z8EgMnOq~7qE0X?w1l>}mSG@lRoTtWGL8Wr}E*}>dP679^pNIw!JORI4ZLfc`lq&z2 zQ8P(;S@&Tt>~;Qtm_~V{8k*7Jg*)RNPxmJ8w+!8-!KQHRpft4T1$%H-o$D6 zZb)H=qA5?VOX14wpa>0{$E#sbdYIetAqgm*HGrnIGQ^((pIF^qaAF*JPQ z9@Eh&%65n+>Ow1psn7KKO0495avXpJjvGP4r>hpf(zZ-n<}m+CUrsw)>tX%l*cVXI ztBJmj9rugl`p<04U)TWr^VoGQ{+A9YQTgv)?nxaMqNpYUxe&DtK9+Ug{pL|j3FJ)_ zOkCpw$P_DC(}}4poJ{NZ&Pco8AYVc6A#ojsW*4KR-%n-iu!ikf&se}mff(w)px)EE zeb!rs{pDe`xAz-h57IrJhYQ?*09Cl^i6C4#!Qyj^7YvTaSTgG~B{e}mb2^`cu7V&- z$kcUfNF|YR^lZ`fN&;l7!(eCYIz&2sj+DV|BX7ui+TV z@%yk-|2ssuS}3xT1oZ|z)STWrJU&LlcTY^?Duyog z8OO_j4iKjkpM}?3!Dlgmq|_$yG&wjMdW1@LNhJx527}YbQ30d0Vp)ftc3%PM?_O zy;L~U5&iLWe$_&B;Ot}oL90Yc{AgElv#a(5I}lakBLcBGdi;GbRuE{M5;LMHLn02C ztcQYve0^9YMYPOCPF;B?u8l-|fD0u|x)_*`iLFhDJ8hn)aU}t+RI-Ll63JS8^O^b7 zHdV=rgG{$+@d#ooN{R1EbGr*<+v@d0ri*1fPBuBbg|o8eRo||vHj0xq0;;Tva^7P_ zs5w$Q%7nATbD*O*st}+brK1~~hKZhs6Md$O9ls@C!;D_d2G{r-;cMeDbz4jYn@AgO zPX_N4+XCeib<51cGP#*xCFuN!(Wjlc0m(YL9@B3+c8B?-Z8+~`b_?Dvd}8Y)J(j>O z{u$pQokppI8iHr$9Sw>Nrn%e8iMOOSs9BD`WR_A<1~)wN$tF|+@H@-MLr3)k*~_h? zRt0VpUrfKrA)+TXeA@c0UCy84JP!hKSInI|0#>w42-!)_ww!#)Y)r_}-^L>(9i|NH z;&eM=KA%I_m@vr{Q^I>;br8X>X2j8)-q~_YYMDB&1U$2)I@96Tz&`HI9nRo~;5GV? zo;93iEqU${@}%Q7`YRh#+FLX}xBu}wxb{?RaR1%vHORTRs4O-yz8=cPGf?OJ{G6)I z$yp&sD*g!8b_ApsNb^~bN+m)SGOuu4mX*t(HyzFH9{Y}U+SDwe{HFrmN46!5^YJg4 zM!py}mG`c#nZ|=1P%&#)_K$lB2UZ5Rtsqh*XSSSu4ylPp8Rv+-PkO#RbHeJG`Uij` z!}LVR+-Z6@S2`LUq8BtzUREG?RYZ$@!5Vndlwq+&t@Zk0BwwHh6HpXWO@NQTjry@R zTV@V^^(q{H`4sgR63Kri$^Ywj75I0*tH3Q2X7%t#FdaGb%>Qe5 z1>akFmy!)?-F0;K@vTOP01rry-^0nXTuNI_Amxbrtjg|usj-c`Pj70KL&w&YX=6bP z$S9!DDUvFavNCqZ0&AS#mC$>t9Qu2}evid!rqU*+j3}?DDRT0y;=HhPz&S4YF92g7 zYiNHs%>cP&_#s_sfnj_B++^v^KdJ*In43xdS8u-k_507&;V-9Y?P@U$Q@~uf0ERWP1a)_=>pR>*k_j z@UYgThyd$|j>q|~x$f6Y`_mqHh`>0z*H9g~gD68)U_6KMZ5>Gyp+TIrdt__CIAbpM zuU3@=heLHLweQ^0-H!6Pckn0q{&;3_ZVJF%&U(98+8{9e?ARyV%MBE;Ki)vm{Awixi4vVaR3!?Tdh*N~i@7RQYY55$>^b1MiIjVN)l!9Oha9luCx2r6Wx;IDXoofGr+4CE zJ>@z^HzPG+1w8Sw?JgB$zwuj%+kUq=gtM3a!8H%*2w!Qd{DNV^+Xsr6oL`4|{{ffv zSv${o3I^6`cg%dSl$jwwoB7_dxtC6nD^1*@5biB8Q6m#6nL~=yw!*3$&dh zx0hXZVUT<_tF^I4Es8_4ZXI-NVEWzZL6~65Ak8<4a^@sz>`w1X(dd(2So4EYtV;RU zW;rYbeHeav!F%;;mcPs?QmsWZUn1rT1Jj{oRgc8}R*btiC!8VE!yK#&K%M264ohg% zFblTlE&-29l>rYwmM!}$5jU1pidK(uDqJZe$^4gc&_G}Z_#jv<_61bicaYRay3Y(Q{>b4$E$!Rf04Ry z4j*H(@UO}~qdK;!0~8$c7V$3gT*A=bh5aJ*a|B{%#sxaDJI)CNoLiGIm2kgfcfZpm z%xCZ2)54y!D~BXPPJfc3ojNKag)yT`nME|I!Q(p{$yAIsJB=ZyUxp+(VZFk%T%xvCKxox6BO!U@oVaLNXy>D6OG;5 zOc}u2B;|uBUhH3;wj3`yxI28`U!PooK-Hx4kPOY+1Dl*9Sbqeb)~1*R1c|Rm9XI2@ zrZ8>xlt;Fo9XhQ`JUtK9syk2V)_T0(m^TYCS{^+d#~?E%(k?b-x}7lF(jP03vi^@UNqd(8rj z&B$SD*ZE}{mo4xw2Mj8Mnw%50kBV(xy_Dx0uz9e6nHQIF~3!UaU<+YcdahD?kZ^GDyt z3A<7^?X^la4$s(4I6J_!Q#lv{ijLyq9aJaqiJAVg+!=QCyJb^eiTyD# zg3(MVBW)`}{gfMrw_zWjQm+DC7womNC?R2o7VxtU+>FMhRjo7`i145~J8ia%*(?{K zoHt;rp#4a>+Fm{|!9WiLQ zB!?MNw)tBz_MB3ZlKgk3$2092o@_QBsZ2w2Ce|xRtC3r#f=Or=YboYy;FeP139yJ6 zmn~Xa+FLOeP4%DV+?#d)1Iff=`*Qg;y1j7eJ&+tcOjk${WP_d-J>5vy%y!tMuy(NM zYTd#(!a=`2@pwUPhbM?ZLBgFp%ZxFbVWI!ToE&MFskw-|!M@~j%raHOO0rNovDam^ zb)hAZb#Oye?qK#EgOVv!o&-wn$Cq|mW}P!;lC{c=K%Z`s(S+}u)Sr%76m>}YNtDMR z@-(I21eEVg`Wpzl+Tn>IVt^1S*)yh>DX!g@b@a6>etBv$i@VCR5eM;K{r#j z>Gu#Ffh~?nZ}gb`LNH(R%{f*Mc*SS0*fyj@bf0*kQ)tW7(jjc{%1(u~?Nb-slZe zJuUm)|2v1yl1MR>u$CNw6RLRjn79e~_N!bk-zA=y^^dhzuoChSUdhj>{3|#Ku$dxv z14wgZl&+}e#u9504?n3lAROM_Zq(-|_E}>&4M=fSsdr5GQad0)iJbWbeoNpJg@9OS zLgsglpsMOFbB%%>hDtd~<#A-tl(xuzTeI{fqR%%kt=Y-T?&Syzk`lSKuneTLZ|ug8 zYA^H8cg9dtv`CCCCLdvi&%tc-tL->O?pVQ^n*R0zky&}yEUS&BTs)imGv(K2LAVWc z+JxxJ?`gxu7CEetb4UCyynod`$00>4>0d?P;(ysd{nyDQ$G@3e{$~XI$J750+i}Vk zOZYFeNNs4FxdN*+G{5y~z##9e6|iD9d`{#VUszla2Xj3dQzNA5+FrDv=t?FD}@K`j~5 zDV8b{7wgLl@tH6isT9lXMa?L2>Ml+a)%DOTkEG+;a5U+Ra<9VE#sVi4^g|zN9?@%O zo(%9sJ@MVC2POM6|+_FqnjZ|np0^Nms>$E2G^e@CM8T^XWl%}Z}+8#Jd!!V)4! zmoN}XsQCL~Q=b;9UDjLlZtgMrQu~kB4&{<)`J9qtR%oKjh{_Tr-vhF3`n?8gI=gTE z0lWlj!_rXJU@h+olj2V@%9up(s2&oh8S^XAv=a0Du5_`rGWyA z`W}iL@P3w%W5VVzno_$T3~XEn1&ymy9=;X8b{D6#sUCYhn_om2IQ;JE&O&J92(2xN z$$;(kTM$-bv8IV20)>2qZRksd^H{?D5FHKxO__89N1p|KQba4WvKgBDDccG6_Yl*& zGd6$32cyEKsCYK(J$gquz0|aq!`2&H=Ah!;%*4&Xl-|!lQoGo}Iiu(PTTm(E%U{Nh zk4T;&PpV=@hmS#^y0Z@0Ma9zi{O1VsA0hH@LN>mG=Cx93K(1Z8p5POLvnvC;eU zywQC-&MLOq94y#r%FJ9F-uIzzVG%#rhJHG+ruzz?#%{MPWv9k9JX@xA^RjeBbuZpa z=7v)z_VKqO4C)>H6YDDk+5X!Q^luzI{@HAO6=DAfL4PmCWNiPX6w~}?7>cZG(OZo# z6rm@!_OiqorS8XPLIW|UfpfMyD-^e!21NEu_}OXoHQLZjGFDr{dOJ4md#v9;ua}3> z%=@CZ7Qv2P6=LLuqDGB?2%1f9q9RA=hb~ySOR}AMjSte9e!*B*raTw_kgs~}o{*OB(yI6^tu#KJk3LQRW$#(4z=)=gbA6+PS%EG_ zizvwSkb9_F0=5V__`AyPYyxdjBFWk>0LN9~n)=Z8y`_4a#qvsfD#or4H)l+{wsV+4 zPxSfxWw5(?=_z_oP>?iEQ%sHovvX)9e)`Nc5(5X%Aegeg_}Rb^O|zkWsX)6x18O1~ zYk70(b&tvs2hU$dEr!W#=9A9tp-?!AZfOuT%yo6jee0<3{w|^$5=VcQWAnutWKNO? zIbjKho(fByCDKc2q=$}>mX@&P>np7DHnTnKMNwQ;tz{dDH>jabnsjEbn)O>G9rRw} zM((!(y8{S(v_QF80hDRx7#^_k|1C*B#GdUKJl7R2DE&Avt*sVojD@)E}T zgjE8mF!&-Ruhww>44xL?{eco;k+X%g`K|C)b~?^o$WMNW#R@4k04qI`;HPDoBUOjL3ZInhk7uz23;z0qDY<1>CS+e{o6%7UK zU4%iP1>pH_+d$_pl?mEkfsFCr2D1Or9O<849{&tz|C}S8%KWACEYw_Xj1LFrr-3Fg zfa!G*wYL5NCKXqRu9;dKI?`C~JmQjA-zK!#6O1I*Z^rcYkP~5AXgLQHv%FzsGI`Fq z#qRig|GFj%L@5TbJ!J3=Gf_!MsXSoXUzxrfyc(C!HslPe*IaSnJ3tl#q(q9PDPv{D z7+nkpbDch8jynkrXYK%Y8|6^McxbjvId8N&m1fnr!f1_nY}!n{u|;LNEa-tXmt!sr z$yCntX6@L2gQ{iTK9(kkd1r#nKXEKL(x`I|qYmt_0QbnYk^@ zuAH-S{|dX1>rS+*QzkqoJdTO_q9(noa7NG;T$3HU8uXp5}Y1Z*l@`iKblNK2akt& z*|kMe#;F=%)2KtcBCNvEdY8PXN0vUGfbAmYn zi3`CH#I*#b&@|owy#qZved1g%xg8%8CnDF_F|T3eej~Frl&H^RHzrn=@2wSUR*Jf_ zL8wX+;pq6Mt)=2efA7o2cE; zt!}Wag(t*z)@xWL<&*HwDssK}LAwb!L-ZsU3!>j31{l!qKRH-=?L#l>d;`jwNt+E7 zUZi3!c<5D##v#OcaX>+u*^^1>gph7WwSeN}B!sMm zeV4jructwi8;1~32N)AcWBMPm{%nRO=oTVWwAZv%`kndGKy>Kl-NTBrfQ z61p>*_OU>kpPxPqc>VXEU-85(UKl-sG!(**#oA?0jCFm>?8+6^`DS$os7#FDAz|Q`GRv=Us&Hd4tCmtbzDpG(iEpcc4=qkm z^p(ud?6Nd~+1H4Qytjk9RFS)l8}ZV131dC-%;jsYbYBql^{=ThzR@YRFNj+=DlWjt zu&5-hPI*Xdi~`o7mVED^F{#zyG`~x>jysH-IxDA^u|?6hc$NL(1YkE|40`N~4$z4x z8POMHmkS6A1M};Cer9Cv2uhVShibwSo`YxF9*GDq&y%Mmp0^b$S>;kBVZuph3^17x zC5<^XG#timdm+foYi!ht_|LZ$+hi%ANnvNvPB2imvO1Tld|!=I9*e0HVbu|Z!iX=r zxzS$B*Q9Ki@$QbIjGPLGNWQ?*S3$4L=LjxzjBIC;ZCY`~C{po^n1MVa0$~fBt0ys1 zoc@~MRfzzZ(o1w59&sOcc0mTJoG@!_kxKvmg2Hn-{^TQh1?f@!_8!!Em=9x+zKAaV z2k#~EBmB$B9rBK4{jLX$rbV}?8I5tFHtFGU^2LkcPKa9|nc>Bd)`RMn@}-s*3H}dw zyzbkw{%{$A9g)d+6hcumn( z8XH#?L>sc}yR|beiA)jo5{co_8b1h9&IwzW60?)X1guxxY80tc>%+yqpWyQlK{(Jp zLqg!<@JYj7u!dJhNc=xmUGsOS#~MvT(h5=a=jA z8tRX~U|13aa~sL6O}Y;gSJ?!30X?)+-l)yxg1h$%rVoMOJN5sFG8v=ViKI-jo2l^p zS=$^56SAWi8zTDm`?8H-fkmCKbRYI#EJglxbn-t+_lnMTc2?%b4*wFuzHlkuMNxc6 zpjHskLdZdcHD(ls!!c(THN?#U5(6Lvp(VKu83FX_>u0mx*Jhvmz5<`1cqi=Ccl+tM zuU~wDB3F_$05V$%ad1nU>&5zEi*23n=j|7AipHNbM;|{lPEBgZk;?ojH;buB=Es+z zpzde}przbGO_CsV0PI4ng(j&D%1TUqR&LQwRxl8M`-PLU5Fb0Jj*_`ey?D)K0|nf} zYE>m?R9l`5oWv3KojA|Ne(Ec(Inllvv~aP?1}jwB!P=+<>|!=L+q%d6+Au*^tEOJs zO;p`3qe(dV5u^+vUVJlAW;51d7LU`Zu?!f4%Gd-o z9=AlNiLq!?yQxSOZAZT|9z&k=8_`yc9!S{jUL##Ek-_qj?B#3oXmWN;US-92UG?&+*>gEoiJi!781C=zxT_^}J40rzo0ZRjOcbfZ9;D z2CMoh`r*Lf2}&Hblv#O`k&PG%;fhs}7-~{GmVb2?dx)-*q0i<>CrW*u9#oZlHm2FJ zR>L}^u3RvDE9sC@fcr@+ik=A=TWX7A+NwC&uUz&Px|fl(UJi`KRM~ya78{8@=VY!i z8lHy>%BPWV#JT*V-Lk%P#WzIa0Bh=zFE&IcbL0z`>hPRslUW{V`IJoabCL^t>ir%-@1>MzhVHQ>$Bd=ybAmqS zEIMyxIrfc2w2E2i{5bV|#673ZdE42=lr2B5W7>TFNCj~bwJ%W0)z`JjD|)a6{*|yY zrtcv+ziLKYD0)!SqCjKrPsDTsZV%ZYeNfY}w^m1%NV>|&4~?nlLVgAThJ-ayqNGp2 zjB3ccUuRN&c&w>jG|{ql|1w078kDIx>nw-AqnoZbj{@+)T~0@eoh{Q|Tuu>{!=7Sp z)m&P=%z=-{mTd^*8_Lcg{$ZB`5G}?Iam>&&knC5nV=JOA5&uPeR>&I^>EAo5&@vtsYAeWtOOiWn@P1=nQFa*-+Rtmh0%QP0ER= z<~n+rtummW=s0l!GAAMbDF-^{x3fe3cCNC67MLPGu|t^Rr;VsWX_zqOmC#RaZ!yL^ zuGwL5+{^EvEv(m&u?P?L1QnP1+56sf+(J{HV$OvhBf#;YH1VOR643Kf13(4Hdn}n_ zm;N1RS524)G8U^C;k(}#IBv{wvtomn(64{Qn#nCr*$IxMB`Yk1%I$6qdMdr062(Zo z{Y`Vq+QGSw`4xWZ|KGa}CELH_n*JsHC|Ul6ZReg#v}vPK8xtfaD+atGj$#!6G%X~> zQVkUP(Rr78A*J)dlFAQFgFmgKnC1>(%!PD~Vu>3d5bxu^F1@c~3=J!3>q%gTH{y{oqze`3X=qn4!JV)zBEvZE)uE+C$>|_pgwvC|=`&P~ zF^V%R>L8W$TcMmE5#bSKJc|*azLh|U8Kn>DfZ?FN-)PBC))io4_O)m}1CQC*+qS2@ zwnXM@%TI~nP@#iQVj6<=vezg0PT1#EcIa}=r)-hmDM)3a1JE}-7A;;$p@R+PXmtsn z$#?KdChQEzT{HK#05*>BV4o3GU)tSBM_ww>nli}P3k9#!k)JOgd}UaUU?1y24ti$C zjENSK=!WQf^R-MsQ z73v-oH})8)njU62)^MN!Hm4Z!ke>qYwYe09HAO3Tv9(ncm$xR=x=YTj%B#X8hesb6 zU>PE8v4-vNKWV#9WmHpvXhrP9%bU!dvrIlhNcBwZw0fUYRA3F6TB$Ytu7D%fo{ZVb zH$L#23DAz(0>U}jD%qr_$(Z$;{o4{VI0Oibl7r)PkdZ@W()+yM5L|&Dn_Uz}=n>4K zS}iG8vR8T%L}`DtM`rfiiiVh}D;yD`m||J~ux9yS{w=yU^+aoUJE3OTef-#H8gCu5 zXBGXVKtycNR*94fX(uWcwA_LV2X#>h#+sW5hJdRXYM-fWf#o~c)q6j1idy;a58{^>5n| zlK;Ui$ln|F0ySUFITaKiTJYILc#5Su#? zP_zcggRID%zYWp1#GiN~<==j653%LS5uFVni3<1nU_%un!|$LAD);7qN%tawq9cR% z$yKyhGGSWyDM;QrckC{Lyaff3fOiH_F9E&}pWeOg`f zRnDY>2ki0d1lr{%q>%(^+U&c~8j%I0HixJ2imU9`+XBw;xUd#Q6-iT0F+WHc^r^ z5n%cPbzIT3XbG4Y=08=B;L&hokUz%jNVJ8R8_JoPR+DiC6f8cq-xa@wk?vKAz|@jYZ7B_2{3MZEFI$ym}{P< ztSYAl%kOLUUv2f&x_UVQ1dCdfi(ngs$Mgu&xC+ zYd>NhGcTxw5a}?asR(b(KlNFSTcX(5%Lk2r|d zW#G(q-I01j!i{)*G{k;p?Aw?g#d*gW($H7(d=ZY!Pp5?-c%LAJT-E>iA(9B12CM4# zbN=eN3Xj6rcQ=7C z&r5n;b0df*M{E=ktoFYnp-h2Iu4{hk4#8;*^q>vE(!D_j`GEQcS{f$BAX_;GBwq%H zJRxz?82m!aif;5@<`Wtp+%#}YO2^)=7K4^$+G1om;spuiKL&DQ7;Ox5 z?BxmY93=TJ5q|Ug|`bZ!Qh54h)9s7j%Mk$7@$Rso8X73E0WcFs}e4_hTFjY5n2!8%5OgR6`GR*%% z<1S%qZe#55PgX*KqUK*^$VVZyRhxDthz?MTT0woc!!4Ac0VW+L&7i<{yrQ)lDl6B; zj_8+guLU8Z86iCBH<&?4yf>^tMoCaO9&*4lPQD zk+5MFDRXpLgMN`T$(X3jRP^XW>mw~OyJw*$!*tEg!%;2Ac1<>kf?F=9marS!zi{?Y zF-RJzR_IO@7IE}rSE4J`_kYo4iLU`V9lVX&T$(54CS9-2o!(?|>i|JY;)`H{Td2=x zLcdF|9^Dp@C~VE`i+a5^-$4d4bR^v9VQmX3dKRo1mB#w1hsf+ zO0+Eq>IYiZS<4;&d^Nu2K&~Vvg+ zC?!1qapT7g*Ku%e<0%v`jccAclq?3U;rcarRYwXem;OqX)fdpESXaJsw%O%~q3@{y z7nV@3sc!^dmC}o^`+te=gcMfcxl0FCz60W3UbjSZxp~1jannQ;vGC0vMFuAUdvqLz{+Y z2gC4UJVf1Uf8U#8oldi#B3|glPYw+)Mi2AM&_%YL1S4?(-(#12Rv<5i$`71o&D=Xw zzoM)zNw$1ivWn};z9Y##^panWdHl`o#7v~gR--G=hH=%9rdLo^#xDa=#C}LP zVY(`Uu)uGdO4Y@GCR%t?+VvNV6Z0%L^t2Bbh)5#SwSfu*K z7{-aPj#v$UD9@S?i-NQUy_t?5Dk<$|g0pWMp2iFpMfCXkJGsc+7l;>Z-*Os~?t3#u|q3-{NRHYMq^0S(Ki ztq{20?8Jr9Ik!l)`k5rFxHx~OH;TQtI<;X-IBm7t=vO1zjdJzRz-8;?cWl|b(ul;D zv2BF+7^#VbXEzdJo~in-eFt$wa>CGi2VUQk!>Q-rcSa+iJIx+|>(LmEdPOKRTNmt> zAAD$!c~ycoq8q|A`G@$1e^V{}qu}#Kj>-!`8mq6yW)HYE7@`Xd z>lr%c)eKI5mqo+o^7Bf~{!Whmf^ySeW5ILP>hm>c>OG%mor)Od4Gf$&53URGOc!d^ zmMwgXK8Tm@7q30Aul%g9A#hZ6fVY)GkT7a5Jj|=POm>Qu$23gJILHe;nM7%#M@qBl z`)vG76c}JztAbDA!9kjZubpTHI(2O_kO4p6s~XlH{icjd>h7BeLTgL`-)!g$Ty7fu zIgO0hqY-VgLq3`XTyz){1CeR`flzi;BarS4N2kuUci?qtl`k-w_c)FlTu}&zaIA|G z?`}VL6k4?g_uq$+B4-dj3|~oC{=ZBE{`WzM@$W(Cw}Q0o+}C0m7UvcX95o!CxH+h~ z_&Q3SU7j+L(l3p1FDkVC!j zU+OC8D~%WEn%ZyCia+_5GdYdK&LvfYsDnfsz%7#4OCB-qKyAs;6FFxA!Ex7oPqa_n zNTonute&h_1@EC?4>uxE2s(7@CBVn8K7YPx@d86nL(4sPLQyr8NbV)fD?YU;F64(Y zgc1+#=%W0bg%j&qLVoSah1GlE-y{W`D3|Ey_(>l4Oc^3rZsG?(a)3?R>3ymEWS;^)a16NnBv_ zW9XxWC;4#oHX=$PNz3`SW5i_8Cw9na`GgLMIxu)GT=S*A+x9N<6WJ)5( zRH=}Ys{r0BX&XLF2o_oEM&`Z&glUNJk@`-or}(A(CPzNFKuLYZcoF50)l0F0wxHDR98fzZ%M@Zps} zKvgZi8p_C!%1|DU%`%l+sFVs=fKJO9@67G()KBw{Qb0YdwU+>F^i#(w=8BscYm*wV zk2XfG#cXwxxZ?oUdnkn2{YVKN4*rJ9vT;BaW%$a32wxxKf1e4d|NA!jAC=#ikoxaW z|Hz1{udXP{$R9Qg;&CY=0Wf+$veR^iDWMqDRiuLj-Pzr%!te9(i8M#z+-3VrYXlXM{xGZJ)G>?W zEkd)C7zfx;jqn(S`j<${fC|HUg*H=(A50b#Cm@#avExGcjCqG74Jq}LW-1brvgYjm zT^l3O+_rn*tMKM4_Dg8aa$mv7C!MZM0+WHd z1E)e5c-LwxC^s5^&9X8}!)iL(>vk4}DYQ1aqrc6MVHGo7W}?<~oGa(dZmll^2}Rmi zJQBDM>xsUM|Ct zjI|Lk$e%8~tXsuaTBPglJYG4hWNpK&cfwHFdAwiPD?(zqZ+zhnrKc8J)3@o)Dn?xe zJf(tokLi>(_QhfMzl|Erj&NBbdQ&oP$F$KH{4zPPw{>l3CHGu() zv6byeYUwc!g{6KFATP@~Sk3H{57l`(vQrM}yoDd9BdX~ZFrZ&;&C6nq!vLcGQ0h{_ zPf;C_kH%0t8^|POlU*vi0$K>!xs6lmg3?qh!s5(&Ycq^GTbNRC#B`yW6`dMi{Up_? zIHeRwgDms7iNS>xi8kfckMa$h0b;BiSu*01C&lRnmc8wXSRs$jMlA#)Eq*d#{IJtKHChoh)K(q{0||1R>gHsPJa$ee9k5uiD70N@Z(sU@N z=2;ZQ9h&R%(-dwe2a^?Ij$TSzonGfkIf$BOFqMlxCG@~+UkPjUW469Nt z=e-QSgE(LdZ; zm)WKd#<`Tsxv>+|HrrH#4$jR@1M!$JWt_t`r<2EAx3oA37XGKfwx?z z2!H0EmLVUo8bV>?lLr3NGnBYPI0o>L{ahr0zc6ZkMyU0N6Z~c*@ORw1aF0m<-W0Ym zHOm{MlR;(u0}DSL;@^1&?!5x@!T`S$jdh1U!kPL#^B|y1ef?V^uhfmGkpRJWno)|lageFDdU*z;*Jp>p|dFvSnq=4iz` z6V3A)%4H^RsHFk(yv~99{pPHsKaHNv=d+gz>n_KxNRQhL>!L=ydRmowjSKGJX+?mzAbHDTFhQeO^X9yY9RfFm~t{rj#H+=ADVz}KGB z9?HK<&V>KBMOeL*hCHA|QfNrp7u%E~r zJ-i2($})h-;CQppbB@@0E{I5aDgFZA+)sL)WO`rlwL&+IcSd*mA)QlocgD;p1=%gR z#qqM`(tXNdihBp|)BR8zi1T_05%ao*j+>`yN8QI4KSITeKEnD*0bO@cRmqFluWI|W z{Xq+CtDE)tn)>6-1#D`UQ{M}1Yj+Cm>Dt@pjc*{FqB3aaSJKm~u}^#MPO!ccgB7XH zehOa6lRY}t^>+X-oZ)w$!kt5Xs~$@VpB^wBWqaxfn>U4lDwKf8<7dLe6h>(1dZ)I6rc@fPtg++D2o>Ki6T=EHNj6i#D3d?E&$~;rjJHL@Fhg!J#EA?s9DRYWTTCPRx|Ay4uS~OQ8cQx+N!-!!?MgNtd_dnM89{PeL;iN zbM&k6LhHOA2OfUHfB@%5N&fmoB}U<45`PZAYIog0*3jZRv)v~|>MDd~X7sJ0RRpa` zboaG-2QXYr#v;d7ZHnnZZht|~2e7&oG{R}j5N?63n@I8%lf@Mwa@93@9RAr#YQS71 zwe?qqPAO1IYOER$p!hKqkb_CWb)3S(Do`cc-|U1x8na zw&!$pvekr!ZsRQlCI*!Ys*GyC9)Tj&Oj1`N8MSri-1xc)cEet()BrXKWsAvdIk+5jHIb7e9C8$>CRh#a zmj;A5JPNDH{fb;Wq6KNu>H65?T^lI$>^4(y2&JrK<9giE-zub}?_zb74ria9AaARw zlZwx(h~X7fl4ha=Vob~2ppxD9JBPqJzqZ6ZJgtNiUl{K%-Ug4ADKH@`K=8O*i5%}R zS!oo1Ez7wFQAS>TF=JVo4UJ4>(Vd|7N$#vdqwI>=EDtb}T *di`!Gn}X74FYO> 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 z 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 z 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%0A#F%Jj^5FlsZ#g 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

vCj#VY! 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

3>GL2md_0yA30&2^p)T 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�afVd0r_Xz-A{51Y0<0-$Gn7B&RyV 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∨RwSlwg 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

cwV(MISHJ`>m%fZ>**E`guA0qg;+0{2D}!H@ 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

t1week+nh#@sU65DcbH#E|my>s#l>`Sy@$_ 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%0D71n6>L43C65dddgIzuZa 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 ) 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

}iSpZpV3J`NN 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�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�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 Vp8+}g;n=QU)RwM06gJ?${{daZ+&lmP diff --git a/android-core/plugins/org.eclipse.andmore.base/libs/httpmime-4.1.jar b/android-core/plugins/org.eclipse.andmore.base/libs/httpmime-4.1.jar deleted file mode 100644 index 68f615848c9e9a0b9325dbed3ac447aafaca32b9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26813 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

yyC@7buJZ51Carm9dyla-q=ExQ=@zXmZej+^0Io zZh5vQJ=(ZVls{xtN6#QSUXaI7K2dyUY*|v-Hd@%()#j2uh{m1w&plQNUPx znYh+tZX99(&}Yw;a+IewZ%B33&xx$l7BG$0Oo1`(*K6xo! zMrMT%S$}pi=DaV9|Mof5%ta#+8evQ^=5Wu@B@|8^i7p&WKmee^=%%Z!I^!B2Aek4y z@Bk3hS9oX*moNxU*}zUKmbsV5v3`4i383!-jxdKssn8b9W6~it zRLVqGaV)mZlr$a)Hx36Rrl38tCG>o70lXJc$@2$F!9G5RQkwfpf9~#Ik`eDJy21(v ziy^{{;v{l8rk@pOA5C>p+oI-z|HM3?lVFE_1iIyChwD}0P^uU&6c{^}B}9H!(Pj`W zdy+I%NS7@jhCNW%3`DCv8?_U`g*NMcF_LEa9OFKUBn&?#dBIj|5Uh@Mn#2we+QH(7 zig`gVnQqMZHDgt!wt$x1ml(B%5_<`nRs!{)cr;R|q)k#NGh~wpFYpm__=Mo-pb%3_ zwBAy>BMcce|3;dhRJ)auOpPXOsn^wu=s*qTlOLDXC@(C^c_8e`O4D&vX#a;6hoKXy zjv%$`{(k24D(Fl$ek;ok2{^}ef}a`0yAaKN+GeA{>Q7{kORfAbbhMC>1UO=Cl5Yx7#RBzgN9 z+_xQuIt@DHF5%{0YFHGZogS^-jB~uq260?35mTT5KiOQ&Txs5~%bUCgX;pOi40y9~ z&~?|vA9L<*A|K;9b-bm0t$4=#oUbI{8K80B=CeOFtBC+kZZQ3uF_QEy#tv_qM;w!B zpWJ8OE9Aj)%4_xPaDp_&{l$WnF*A~^$dkGaRtRrW4_sdMyrMd$ZJ=m^B@)9}vc^6Ps6cK#B&7Gcf%x**%| zB^DwOj&?O9nB6g02?RsS3q&|vA>hJjC)65GUI4nLC_pC|byr4S8eu_*{$)09#y1Vz zPDOCy=CWqijbPe>>2V;acTXDv@2x$6 zM88VKsc!L8*9oHSh87cC4SHzFojh1|a7MS$h@@%QAQJ0}MI8vyY8HNr3M�@69pb zM+{@lVpz-7;H>B52w}It&jwPu!Pn8x4fcyUyH`GnAN1Ak5t7`{16Q0<#_a$lu(+}! z{d_YLsY78N1u^-L?}3R_rmTUC_yU-@v~`KAgHMvuhgK@3Qnl#bixtK9S7xOmu+!ve zn?-(Jz}(s6g9`e$3aJS&tklPZ@vvA6ry6~3`G^}^HBz276*62Z-2@C{Y;V1#eb`ahPj;bhQiX9^GSF1oWR^^isY@pT=C1d*WDPfg%-I{9^J^5JCZApJ20=7^(Pt>96%$WXEzKVAD2URqH+7UqLB=1YzJHdmPX+ zfI3CMpfEL03&X`ht7W-@HA~F>nWeN;>ET@gujw&?P<=npK5fIRU+iRjJZO;%aw(CHbnKyJXlQ2 zyBdQ!xQ@pF^E6j!y>j<7tIyEpAx8zG&FeSHBM#3F>Jm zmik=6c?09`Ym5kJ9yZ3jhWIZY?b@PjJ+|M9!D!LzAKryg(=QLOR%LubHEK?u;v4Cb z`{Y?ZnmXIdwD&Tn?r~4^E#F~`I`x-K-o*3ACXM>{3If@pnyv>wS0kEQM%S79Mi$T0 zT~)L=?!f-j<;X1Hme`(H2#Tl>oe%Q=}`HTQC1_W zO0^T!iO3>G)-#%_A=#vG(J$?eW-K}s?B&sN6R;O_Cj;QZF=nWkGI0jTnAdjJ>^FL5 z_?3tfTlP-FS+T(yea9kP1J2(%{H9ZU%hd3C!QmB7b`OyPbZn`+Gqu5=zG(3U$A2HY z#YfxsIYg-ALCfPccO=d8SZfckA4ig_1CLeITy1BUB#{+JK0v1WM$k1-+2(7oWOnKo zJzt^orqfKsY`Pd8%{j0cQ-`XrD46?-IOkb57 zU}wG_^K9GY>C?$Gh$y^c#~)%|L_KAcWmvQC?{+Tu>3ubl5uCuUQZc?3_()=Hmkb<} zFhZ&fK2eLl!+oQ;@A1~a?mhATwc9vJ`UmnYjcPQ2El+NMr9*vy1$`)-cI?^O6t^4lF^gyR3{aK;eDjzh z%I=wrSCxG#d1HYW4X-xW`*M%pv$0I{YIODtsP6ulUgio@LpLsFrs@FvC@>D)=j6mX zCVXP`h!p7`w6Io(c9U_oUI%HB1&hbTchNN=^3CvRPPqP_*6+%pF?|XASA+6Dx`~t< zmUm&WKtS_YKtM$QZ{0){Gbc+U8%wYM>L})F!1(B>Vg1NE+dF0NxUFyhPHfp;MBku~ z4|8SK{NrY`9WdE?pxx>UKBMQf39(2@%3(bwB3K>74XjikVtBb+LFgu}QevXAP)OM; zHqsWM;L~T2z3YA>x5TcsK6jn|nCpD=-uuY+vwQAbp3e;oBv-QtFT`mT{F*+9vj@uR zlMn_AM^Lms#OgB)A3tcq`dNX8A098oDD;#8M$je#?K2o>kk>F!0;a`78%714S5m7W zeU}2ZZ27@kN8~$*ZV8DezOoduTdo6<{N^^H<{m0CI}meBZtssskiYaD1N2|I0e{lBdN}#by_i^F z&`SsAB5Ns#INLv!pDTi-_-ixn?2HX$&6=FZIQ`DvSWs5l@rTXHftahvAB3P<(N-eb zvbU=%^UttpPgu-FRyKt5wG@eHr75Gzk#Qc}W#yPDPeg4YIOO4M$S@=VzWq2h=bEIb z2-Y5=(fc6=wW!IKwz0#+UOb>OhZlMk+K{KnBEi^FXxOidb-tI9P|q>LCLT@G*IQYD zN@%)iLoJiSHF@LTp@)SH=uylL=v=}Slk-$M2Zc|F8r`U45q^f{tMM4{79VC=RprSW zT5AjA%DgUW%9rsmj24tmziuy#Avn-^w7x8``UvyplRkhUo=285T2@+ax#>zsHj~B$ zY4cQH#agz5&ni`g;R?}SmT8ltxMICKY1uv}jn&b1Bz;ONLw%KafreL7S5Yioev(Gt zEoCUW-}Md&ovbhBwT@EDUDzs5MYN~?G`X>l9V*GiI;uxDy;NkB>&(ji*xnxr8k$mO zNlFw4ep`chhZSvWGZ%@Ia6DwusJZfXFw_?Jx#uR2{~RJW3S`L}eQPtNc04ejOvAfU zVmz0|im^=n$`kH#Y#r$bsu8TQ6#64R?>ZyabgkCR)z?^G-1MjxuO+gNN)BIz6or75 z=tQ1$pO7ZK&z9Smk|!Hpv0)|&?B_E5M>GsdmtyD{G1VyI7gNL(B7|I+5M~1tc@y%l zL?eO_8`*@+@tQO0Wy-Wc@U3Ye(_bb@un7H_!D^oGxeFES1>#7{h{jF@Lx#TL*wG`d z94FO0HP6-O6*qCm-B&m96x-x}Wm}0EJm_=&q$qd2Nv+yg@cpr8X6``$bUmhOPJ%uJf(bdV@6Y!6orO}Gr1(q8ujs{3ur<=fOC^6k*vSr&fA%kQ0|u@ zu#f|oex=0LvgvekdwJDwP!D}O)Wg08(hvDz{&H9-sH4yZ>4;!cdSbMK$`AAdWmJ|6 z{!nb54e;>K!6<@l8_I69aXwyOXPYvhd4RYc_CvzTx|R)dyoQ85;MvQ~DAX8JJ{Kj< zTW)FKF%pi4MHm>O^m%FH#h=@m2~>meG-v9>sO=YJmBYrzx5L(-qT6Wl)iIp;cuog1 ze!r-ogZe5v0vd}0(K!K509L}(EmI7+P1+~ymI=clq-r8VC6v*=OD*Vu0r5s;7zwf)x7%l-G28e6}TD;OFdEfr`TxbD4XVV`7I}j ztrIM`Dpb+PE|Ea;d;^ilg1HhOY6N91iZ?biw!CQ^-ToWRnFl%z#-C;0JIKGyilb!3 z!b;zIu7$RD22h7VT-8L=^M< zf@3RqX6P4dfSzJ7!YU!Zv|51%%>m=zL`7k!87g&w@?df?)>BmA+$AFC*oIll!K0xE zVeMF_MuxqN(G~113EprRUX^}c_;*XnOt9!Nw=I6?pfRHRmI!MjXK<6&1d z7&V4Mo!gIKYw&>%(@URVlEMzir156RHtn{2fx#}Q>PU^?@h~o{R`*&K-=ryCK9rfQ z7P-a39<~U|0~s7GYRBkuhYlRdT|>E{%vqF9=#!~{oG7&eLuYh!yr9%C(P&TNpGsa( zecmi^NPL`NH5MD;c|+R#u#ui9W^fsKr*)acv1Eb6id+zibiL@`5;B@6vLUF4=zxJ@ zMH!TER9-s@67VHSB8x1l=?v7I#`5SRwYb0%+NppF+A$nBj`%%={x#OZp~mM-_}Aht zu@bhl7b|=8SsZhEdslSbOR(}gN~fIR{E$B7FV}-PyG5tu)q`88zZduJFn_V9U1|RA zMEALUZELAHVWKDM%L89e_k+!p8;RYbs#2?IRg`_F#=&W6rh;?4UJBjRWApdr&RM_n z&YZgX;5!1;!V4D>uPXRr*z*V9! z)d5+2JsJyWu0;==r6qr6_OK9>EY=$#X@zzU2zqqk@J^LT0jBqiG&sZWsTngEcw>j$ z70X-_%bfK3vu5@#QTn2kmIy3LycXk*o;_rRz!ft27(h9T`AsE17;&~DbxhZlv{-aP zAYaFk;H?Gr!*LAT(c@C~EJ%D~@kbLlMrdOo_)pw)NW@1o%*fpM!#HW?WeTeUb&7H8++U{h}8Educ z6}5f5YG&u%>2CZ)1q#LG!3hP+f&o%bV-F1N?oO<{BYLAd4SPBjtabAj!Ey4n*#6D7 zL%r4=J_+>NBrYdb0drWt#1&5hUN+Ko?4xoUjUG85-*W_L&~0!SWkr2DNSohvoEL_Y zj}|@Mu<}GI0783yB}q9^sJc5S;CzMnsLK0PcB&*QS8e#vp>bh6x9A=S6V%6E5B6$gw{Mio+AZn~ zM@%EV>Su2z|0(ls*fEP96$}py%kke#*-92}fR5dUB`?H~rjfkz)eZi*2E%5aAQ{2wp%Dh!|X1n zX|?W(-6!kvc;Ez2IyB$9Ii>e=4m;S*r1-8-)RrR9#S=mENd_oZo@W5t`k8jcFMuHFN(?h_ODIYHzKG zxeqn%_Unx?u7Lgl{jVbCf4eLZ{znmGZEA1(FaF71h$Qnrpg=$=kpJqR{CBXZy@{)> znVrjj_$Pf;bu@6)Q2)x)%SwhR3yaVdvY`eV5yy;pSw^eLC>29Nt)bU$Se|6aW=;=J z1XTKuPDR&urrDx*roF5a96Q}kz)Hz%cyhLV=i2*w=l7iM{`~#I=|{fMl0CR!oZZJp zpyBOK9&qLN;|PDf<454|(45&LpR)$=8$O12ze@^{1;;mGj!^it=M9u=9jqxfWDcS8 zAI`AXyP;lDzv&vTy>=HIX#b`oqz1t{SLd5@vj%y8hCC^> zJM2(4>_%`Ldc?Kt1jEv9)Z=NVPgq8`gDz3$i*D%1f?bX4n6rQhUURPXQxxKfG#W(` zzJ8j>n1?un3T!D43@z_|L8S@VWW)&>&q!<0Zo`w=t5L6fD&)8oNLV2(k>il-6Wg=2 z-F~zd@7iZYz#Zf4Hr=x1)M?%_?jkU5vc9gefoMH#Mcq<%$PRik8{$bx%uZ1^+M-Ho zkW*+p84q&-4U}4(y!sUFv$@jIABwOc<^1lkhn!PsAo1OFxgrn-r!I3;5<^m?h<$Zzbv08Xyd98L=2p?e59s7gbREeOXysu?$D2v=^*WrA})#i9A@DR2UJnhCd}JAiiyEtoDbv&jy|cL z&S|m_M&TnoZ{{hjU!m}S7A2hx$@M@)9K08a*oU zZXl^0ztFTPqG8|qL5^+_Zp1q>skA?+jS!-M zkRJx`6@=CMb9kkFT0F-22o}mIVSgx%dZ2sc`Fr|yOp1R{6xr*YX;vAR0s1MW{OpAJ z?q4(zg0dNv7ziMs5_lk>e~i`tmr48IPXsX=v;XHn$W_18K{Z4Df#d0dnF0c-iwY{D zqUYHo&wRD6h(u0;igLqQM^aDM%fUmuwsYyzauexNZcSCU>f}9AS;z3q>wT&GI3Y;N zW%<0h0V~!dKJCfb<+|-a===UYoC8WTTnf$GdCYQ*_nD$7U zfad2PGmW70%DdGcAB*np9X>K6uV>s-W6(Fz@y_VJ8Nuo9Js+9y={^>Lvd1_;?Q;X% zGJ2+i?;dV{>iZBD5|%w*MDo|lq${raNaD;3E77guS+T@Q)_BK)WdbAnWJz&Ae#r49 zg1=-ZT2zSKG9N+!^Z8~Kj>$s*J!xOSM}w(}e~@gmn-nfun6KmjV<NxEpl3quQsJ)UH`RV2>;23Cq z_YXmA7(`mw%Zs><<>sW130!)B{gh$)j=A_jV+h8Oeh&F1N^69)rmMt&%UyUgMnY9g zX!I>D73r~@>A|IYl#0ZLs7^qwdB|fENSu}l)vFST@eVb;Z70$~QR1PxE~?EiIk1EeKBYa>N`Up7AGc4vjZz43#%V0+sc5qQPEG5JP4cMlK*}jT{)zJNO6S zwl4%;E89E3WtAU!C^F#bliktwUbx5O!(e20(;GTPjvtxXnOKVn-L#3~^&AAg0(mIj zva@N9Up*Vg2EPx?Ku_5oAp`L1Hv`uY%u;C=ZFC+-+Sa}mJ18v6ZIwXZu8Xj3|EkNA zWR+DDM4=OkEa{wIV%jm4$EM;zz%7Ehds~iwvR)Vt(MGx2@I|J!f936wj%~}7JuB2W zhsL%lO&ms#3zfXHtKGzQ|MWP!3_iJeGh%OyIKo~xTv&P6=CsMJR0ffQFLN-#G_@Hf zoCEm@+G@>~hV9|tPry3G1+*sX}&*+{MrPJZq(+s!VOVw$(k z&fofq&8sA$YNX~fYBA#RVUkfY6bR1Io;0FxnB7UQP3;t{ON|U`SphY!!LE7Wk>=nF zpJ%Gl)x!@1N|G1@KcqLx1N6UV#~azrwOA6(8v((KXh%$rW`eA4U=4rp45V}6Xr@_-%d z@$#R#u$Ub@6`^C=W>4yhO$c8>L5?JU=GrOU%jzI1)EL2981>RUnL-ClvJR4k-{+)NZP%bxsrU|+fsNA#j&NrU#PNJ+*Xj$ zmr5#G1>sq*wVMn*fBG&z9XB4_$36E+yG_-3UAI@$U>k(s9HXW3h@(t7aI2LoE;YO% zSZw}%rs?USPHD$hd6EniRhltI8ia1uqXiA)7e##VLk0|@F-8gEX%HBd%u&_S^bXiu zbD^ISK@AkQ-Vh_ygz>X>`(s;?MIV#vgDoh3a8~WjOW( z-(KU9-yRXTMYpwgucTZ@g|3v_iX#KnP;PO;rLAgX`&2s1v>P(;4C&twST@H^2D1>3 zEnS1xR*JRV3^fi@aIWVEbp#Sp5ZdXYexaLsyKat>y_wyfskeLrg}qCqt*(=-Pf^7*HZ#!K6w^hi)eys-<`y%aiNHG>|T6 zyS!0l{EeQPS+&J6JCoVDimwSf#T;`Q$mtmSTlFO1$aGY1q1e0|k~V^+-Z8Ot80Il>EUa z=MGvZARxzo81O%0{=fdk0I|+p4*+ro37j0?8_g|2P&Yp!+idygNm>}?~1|2`l9zjh*LpppO*@CDBou4q_taU~XVs--u@cZ#m7qqz6;Pd5DhsqFTe? zNCx+NMbkdYLu7CUYJ2B+F*i5ya52=12LN?Z=k=n@@horTOswLx#842rRScAf3O2y< z!kT_{EwzJ$w}i2hj5F085-P!Vn?Gl(iq{rEeasyW7XJ0>Ec%+Di-sQ?2kRno)Kpfb z;g^L6<3CT42v-{f1ejQOJ9v1p2=L<-;qt}7-*GAoY7bYjVxOYViEd8Rxl(8`H9@1j zppomMrlYdv#NII_nKU*kd&~%@u|+|ysROdaJXlyvFy~iCahZ|OB7`^B=QbNx>WVm4 z+e2z%&Q!r+D3z+JHZ+#Tdl=zh>7f;F(Mq-yA zSTbczU*g7h&ZiNxMVR#)KrW+a$YQ%Qf4Nz`WrdZaE~CYqZ`Bw{NZ25sa;r*abNuEq zMqP}Q{Q%xP0l?t2ph%5bJJtum!E8Z>2-D}X#SV76<9!NVz(uRXaW(oBowq~Q@1H#^ zXf*g4Z7{Ym@Yzm(9{8;_7}|Xa&Sy+5r6yXTm8HAt4lKbZQr816XktCP~P{>!vb@YH=R`-Ep^&u09yMCZ3#ChGvY}EWs$U&8m#K zaMYS;IkRXn1zf4)5YFo*L_I-2y>SG4;oOUy1&4l&umXeeu3w@beo^`7Zyn8Li7R(7 zczvjUq)I|I+vA93@gTwPXs|pW!d<_#wfhF_UBBd)NjwyXZe72CUoSj>j@~4`q{i~e z82yp%HIV#K?rS3Y4w=`-Sg=f(6QQLY&ze2{2FqwRvw^DPS!nt!LN#|Tc1Zd(SjF4* zXx*sa9y3FV{hrrhl?|#j)x^;Dkq(UkuU0U(oa0*LD?=?TUFqiry#c5S5u(P^^UXbi z&u7cY;*9tZTTWvv{P4Y00Y$K&=TwQ|6Ld-*J&LD|)%kX)ZLgVqGUwQXyk>AwRBME% z)ym!+sTR$f{7JDWRff~7*&h97iIjCz+QZA|9qNQ@Awg_k=U1O^)DkaLhV0Q&|Gls! z^bWM50Z>|5ZD$d8Tg<7lv0MIimbBf#(LIN6FnTVWc#sk~Et|-=(tL4}W)|$b#64Cc zr|`=;bl77QGuybMN#jz!EJ|J3dSc6^6xow%>15w1SZzC>;V4PGQ4+y%2NJ+CA&qQZ zsmp9e%JM>S41p6a*PZtCAfgveKHQM35uxYg+OM~KsP;iqxvN+Ds&rew2w8!kelD_X zpjL{YLQuIVQF&Xp_^L=yz4$7e`a!3GK{e&W;Sl4R?th>B@TKK!N@th06zTXuYhbJ%WmZ_#aq|-O{ z1H7hv%MrZS0gFaPbEeVDyhdnrSXaUstF2+*7M@pG{BR8<*)e-U`ynA)Q7^+yovkgs zaRaz9W}`s8PV=l)Hz?lc3lz`x&z_~OO3;QIzBSLroQd9;mNzy`AK%QJC7aVZGPUXA zk>J~X0};+OXKfX&el?w=kWE_S-x#=G`)vc+mU8WN&`X-m{yfEwxV>(p0ir2Qj~?MK zq_-XPy|>V9Eq~IG&Q7|-dg5zqtz>4PJ-(^U%0s5%T}))Ka5mmp?Me(8*_)ShOx+v6 zG!DtDU>8rA^UO!m*G_rl5r|EAwoMr}7)b5;;!@GqX)5YZ#^+X;nI4cmvzu)ExNXM~ zfw6acev6p)t{l6+uPn8*T)8I-mv>G0qGfMpZ<%!$#LPR^SawByurKJ)ZC|_Ra-Aqk z&*{}W);x+>=^(5N!9mSAXbAEIrUpVtEA%@fXGw3g%+mVFN2X~}a7qY&TG(oAiZ$Q+ zZ9KFadeP$2MsL2?q`PM%xPhnO0VdmfRN%U@+D8(}Qycw^pxvwW$gYmw@Bp{SGy7=} z9D3F++sfypt;0PDH?C+bx<>5-*BLPljba0udvDGo(jT+q2A~tO2Z+!!`wcx?xg~U! zTb8Fs(WEzK-I{W)Z0>Df@=H8C+O$NKPC8ib_|;}td8u`RSUS0f16@IQF|IH87e@4_ zWF-&E;4rkR#^&$1nPWfMzIMGoYEoX$Il1?GE$G4Kq)mf5YUgIvmC0SU;IAoaUT=Q} z&7*7GW&9qwhRgHkhI|%(Gs)5C!8~|sK_j$8D*GelvSHE`o>gpGDF*FZY>MGHH`6a1 z#I4e8uuFJ%Sm4FI02bKQ35xLXQ3WuXhxn<~zl5Vnf_^~$zjd~MYUTfnVz?VQ{EN(1 z{X3sf85jtt2jpMr$iHDVjQ<&e{tr4Lx414pD2N)c&w^SJKpBN|^GUCk*RPFjD-2|W z+G7<(L>gh(OrLJ2a2)E`^-vgdx?INP{wP=eBTMo9?&}L0Kx;v5Q<;F4P_asDiMwXz zyNgOl3oKQ<+2NKO=Y7>|j}4%Q1dh_0UQ&dL$P%-hP*Ii?ETN`p6;lMO0GG)^>k)({ znL)ACd0}_t*n?L>$RN3ahDz;;A|gD|un8uA_$&Rs>j`z{iVy0QOYtL^bT_TcE!VxL zqJ0(m&xB_t0)hS!N?)VuDnh*MVtRmw;=TM69pQG5mDTAh#&wR{sjgKEH#~TW_H1o& zORl^M&5BMe3r7?OfXK%Tin%}#6RprXm{Qy9MrbVK@C?kEfqP;r{Kj0q5HbUD!SdIJ ze}*R*-z0C?EMt5zc?QLT%JkA1)``{=?jx^PpeIt#aCeNpk-k_xQ++W<2FE$W*MEzn z|G^9W$_Vny(Sd;e(g6Vx{I5;>_l@`;6XRUa9w{p=r?VgC4=Cipz=P9ggp?Ds&|vGt z(55J3VPF%2f*VOvCloQVzsi~fi_qIFIBZuos5L_gubZvdw9Sjafop?YEA^WH8NjXe z-TwU8xxDoDm67q9{l5J%M-EfxKgsLL@xEQ3>+sj-`g}$KQXKQM3l3i^V)jU&D?359 zqA&dwnYw~lQ#va1kKtZ`4Vb37DX`|II(h#w*J%QIiI4!(v{ZkGC)wLPOFfc(|^lk}$Tu z=mZq`GG$GiyFGez8+lW7Syyi6SLCxKt!T84g(HpJi?j{Sj7|9oww0UYRWR~r!Z1H= zV=n80gq)`FH!@Cwrl>LCdPl4&czIKQg7U|1yOE1lfCe62EC++Xjivp!CFX;|uJ0wDw90XG_BRwXxjd!+O~}tM#O|#?TB! z{RI|uGZk;stre03OoR^>H}_B2YuKg9{+pq%0y$bNg8rnnre z2-__q$lweb+#Q0u6WrY;xLa@!Fu1$BySs%UxCeIzcLw*c+1kB7_I`J(y6Wxf?)P6; zpVQ|#=Xta>`<)%fOLK_U#E1+8`!Bx2FgsW3X5^I%4Q58KLEDQi*JaK}Y$!Q%k0# z2qC$aBI7(x0zL@XE!fFWS^RilCcA=7RJu0sO_mRjSfo^bUMjwyApX=3Vm9m~88OFt z4w3ilWO98lit?CM2wzGKhsIFIBz_miQ{R!L&sKp_^JZRb5KUTnNXzr1uxwDcd3)8(?M{-~HV8_1v) z7iL@DI_V#Yi@KYKJ*Onge$yjQC5p6V_xxZp6nRr=#~@K>@499v*!QT**LMcGqXu+%*C+j7*Q*!mISY_rf@?HQt_saU(Tng*+P zLi!o|p@!g*8lTQ!68TPze?Y35>``%bY|n3!vURM(9eN6M*)tX8Wz9{R3#!dsz+&=s zf?aZVVJY>a|D>iYl2pI4c2U^Mq%2QiF;7risTou-4zoDQ*Tn$=LI0cf{;|pBBh6Rp z?BM&95Q-&>IXq&%2nV3e;Sx+RRUyw-ft8|Hu5!U2!T|!V5cn%b7lrtwkwJVy*&*eQ zQ78JETb#j87eXbE?+E6YWODf*DrtRa6Q^Y8j>YXRqcKzE)e?rx=xk{*UyiFYGclr5 zV?VOd=kY*ik6qo#HPQls4MHF((W~R;%@#QA&c-)37j;Q9DM>G-;H@O9M?3 z8RYp()eTaEHhy+2J<3LZ5v5Y=48ifQaxM@%Si)Y8CuEsZ*ilTJpBK1|e_# zWMy=&O@l~mKGFWgH+RR_OomXIJo2#GS?LDd$eBHIPWDM2j&Q!*dgp6elx+)U2u>S{ zw4R=vDC722i2KeIDgV4x7A0e6!1z>lC4Y}{PySsh%yujwAT;VRLSFLGg23t4wGfFs3hD;lDcKvN5k4In%hu4~A7cXv4$wa+Rj*wCHU$sB?b;idP^Uwy*Rk%6j9@ z-KMtdE=dU3Bx*JkI>WfEHOW+aG(6uP$#f;$9?X3GWnv_}ny{cGdqVTXB5qGWe9%A2 z4&oi1tMh~dw+jXdCRgpuUZ03%_(d^3qTJhO_=WkUtsnT5yC9kq%kp{ZcS!ZY%x;-A znSCi+zR|GBB`q_JGHd-=eVJv($7Y-zn3m9~AUZ9=l~Q^08xs%Bx;}g)s+K@6@bPRJJ%hb*i#_dd zG8Ga^(US3YRkB>0+M=TON>J2QRT;?G1WaGy@Mbdn^>D5pyV@8%Q-!)G%;($-ezup7 zRf#KTD{JC+ac1|Xp5NHdkI@)1-atLyq=i#frkA_e*x8YSZ5p=(t$v~0J;GF6o)6dB zbk7y`Wzv6X|C`WZDkdd`I{b$n5rN5NPA=B4`jNC1lq5qHyRxb?=k8#m`I+8A?fs>) z-_4aSN!bz_l#W4CoY>8nbFLIM+Mx}b(*`QT;8y_RGn8-{0(j&EiEhVB!Ncm5-`)#e zV{PhIX7TspFl>_#%MIp|j-~mEkX2eFp9!TYrAi}5OcGMc1A?+S0_dJ{^EB1?yoodb z5NYu`ciBonTae@pE|?j!F!K zVz`?byTqnaAZdo^CywoL5m*4r<+yNE09~%wISa_izssa0rtj^nJ9(%1EKL#cvHGj? z{0ko;4mcQ!PCfa}iFG1ufF<;e;=}i!Yx(%Zp+(P+QS~2s)tmisV_hV6u^$KIM_;sC zogd48#vz0|Ck)WQ5aOcWkcMe*IfoBohhds*v46VM`PeYF3vJg9P@Kp^H?6+n@imM# zLPuvHKsCaGQhiiH3NOeQa7297g7MXuxyk%l=khw9;8CsG@WvI}8VL0Hkh6*K-O!X6 z*-(u*+R%kq-Y^xuX~`h~mC>u-`yAs^+VIp_k9e#ZHfzbB3vGYXl+-crr;b;-C&m#; z@Z~R$L8h>1@Rk4WkG6@Go&ty|Ek^-NIkV*g}Xy4;aYw15lnQpe5_Ab^D zKVXQ$YJbP)lFHOa`-QlLS6+x|fMU6AvRM%9cY?J=ecDtM+SD<+Y01s~(eRxQA z93SBA077lrwB`omkWLOhkgQcIinbuMapA@y8gH;5W^#v`H+2%u%rKp_J0SYaTa77f z9yD>!4e}n*Y@RI+d>`%CJkdkzw<9rG#QcG(>+Ep+F@4{KQH>^bc2zRqBmUWa(Kb%n zON_hE$J9B4v{Y4%W?dyT$x!#@N zEy;RIfKJLM(XFlbH~4F26v*lT5S5$w8|*@L zRY$ckT8Ho&=S`W;L>@QWAlY@jwY_B-N+o#q7S?-(0TipGV|*Ge4WY`K6To)+TkNPq z-pVuGG$?=_^7qEiksi??z<(=?aWLzzoLwg(x3O8E#X?9;g2ltD&J7p_rRcVLX2`{SwT^~ds0VTKCq zfb;y)GIzxU#Fnj^PqB!P>}X#v1f=MCBKxsia0I$>#YQ&YkItCT;q?1g z=Gy674g>B4PX}|yS!iqajqxc~>^RM2p0g!>!!Iyos@KtaUB4H!RnR!1Ti;z*#7s6f z91-(NfE9(320(4KYU?58r$CK%=q=J;lE3s{zi47&{~m!e2s0+LsRYNBwKxqp!zg}J z`VdybBLL9yZt0#0rFU6fB7qy7%?+k9!Jo@Yl6pq9d(|BR92t}7Q67(CV7;MCAx0hm zQfmx{vFGAB)m{nY5$8P;JlPVKx_brepgSm92V~OFN&(*wc+bB?zrI&_S6Ey8W$u@3 zW2xga*8Sa$6tJY)4CUjy*&YepI$a*&kBSvta*sK58wu?Cu#p|5K-1$dUJ$I6*{xB! zt4dtURm-83Q6(?6tvc4MrPen_NcZa?C6#_WIC`?VkvK#JzgL@RH$kheh1H+QVOsfWzS3?2S>u%^-fa|< zr6ZhPh3nW}a6$q$kEi7y$CJjh;k7$(EGk?D^n$8_&0hK4`tJ~YJV12K*!g*KPTQ20 z?-?#D51Ls>=+@2ZdQ&0_SE7g)Wi1l126IX~9O`=BJREl#Sw1it`k%5FQx@r~PHAC2 zH>;ZOFMMI^>s~EXEFT!`Y(V~FkcxK^X@cKk(wHvdyhmMe0(CCU(ueOaa@?qqgwm;f z%X22H!c9!{mBjs#HJoL&y+e4|Ka3q-Voez*^U*e*eOtp;r_JBawbEZB?v8JftkuQB z*54Q*5^vO2?*ky2Kw>eFkw&m7jt*hYoTiI1mmq*zkSo0J)8ybB${u#V8eq zJNcJ0VD5tghHG5FQ|RdYf?8X zVYc)^aUamjTb@eP?nP3#Kf`l=YW->Vu6`-%f;BPh^oJ8G_u^?sgAIYhHUXvB^U-ZG z-VT|*MH6TPTO1?ccLjA@7t~S>7Li{)@W(PeDI&(Ry66wjsGnu!=*qAo5Ts9qAIKCL zqe+6_)@0BzopyN$$_$fE+voSa0SBsQTj+pnCyDs&Ngzj%FwBr!$2w)SL5mAl53R~m z#SF@g`QC64lt%}<6uayg-3L+d_>+T0GV_y|!*yJ71#+$ryW}2q=dWDfthWA4{nN#^)@-$d`v^5dT7Q>rbfbW=88V2 zD&>^jtdFokdc>i5x+IA=M2deXwZCI1~0uJYWeu&>!Y-M&SF%yFfJYPX)0RgTHDlk?#Zw-N;p3IXs{4J`US2 zXa_U38CvG`^nGYcDsS$n$WUP)SLg-i$H8Rw`^(pSe<32#Dr&N65D72^3V->49S9frJEu0Aww0xKd((%gc~~ zyU&)(_v33aAE6zl{9|ALgpJ5c1hDQBpUiQw=CDDr$f0}}?DOXaL+dK@uDSNq^_o%Z zHq@l^_2xy16F#1&FpM?aBgg(6@TPZc{|0Td51)cUP^86!sJEW}S*~LA+66D7g=rER z?90mfo?fzRD)HxW_v*!q!J1fvM}6FTVG^FD)$He%rJ~!TRhSTb@6)X5{5Im?m-x_^ zn^&K}j6Xq`E@B^1vp28>rW{Oz}Bhu=&?52KRTHvpd8qK6+3ZHwS)HJ$jGs$=OER*VF`Q zWsoYh4wL%E$uVxq#$Z+HJDx_wxt%Lm)HKaiPEC5)NZiGHDnhC69edS|T;)Pg#?uGo~&N#M)?;Wg^KF}8!7;htbj19WvYsfy%b=&q~ zeBhPz%S$7HUVk(6FWw%uoBk{h#l-QAj|-^Wwp6ShnAX{1_I!|Q{i_xI!pYocTndl< zs@Z>u^=c^Qwkj>L`$o{bnf|rE#Xqc7&UWg{toF^mu6I}eFazZ>2HrQZ;E)*VOnx>= zHd9wTIs=05u8yTj;ImJiZgB-|#j2~h2(ELo$t`SAiSxTaz651Q*5PK)0GBiwiQD_| zT#i<`crlEyd-uu%qMaN4Zy`=6Eyu zgc431i3!80*c-HOYC|To;7ts?=_>~dmrl;4%y|;2^JqoQIqF%bT7r!uH8_Au43D3A z(G`)AkDPau$s?S^(S}D@p;4O zcjT?tA3AC<*w)1cma>;osIokL(GyJOPgM%F;;Kq`K9my-mU5#iQkSTa$PEf{_rsWU zisldZn17F>d9jGuP0kbum-Y+3+zFO5Qv{&{^JDC{`@h@KdwAAlA>6-4L-NlMvUWq@- zjHM5@4XNsVD_^NDxa5VEwS9b~onHFp79GWdoJf_rKa&dL5e=_Lk!B{%+<>7S57}T9|@QSy`|rF8*FlU2GrC>gjyO$6<)MZr z^=X^g2N%nRCJjs}28}Q1xzvU!3htx4+1yi$D~y`J1MNrCblZ%U88%zP{%SAGiA{np+d+1=&^4YS<1JV4bD>b_!KD8!0X76Z3k9A~dhLLK-w`Cfu zDH@^L#UYhPA4y<-26}awrjf{o{A!kzlW6X-MR!&8b_i9F;}BD!W?Cc{^d*>PyWA)_ zgphE^B;b4cSku`==Gj=+RjwM>g3ncNQpXQt9j(ncGypq=7}UZcd5kcXrI?f%fv}7Y z|9e#2cV|?&Q`~*0S$`L-sp<_;7#(&;YMYFRozV1(^e=8$<367#+8+}}75&gjFgnwa zHWZ4h3jAR+ykPYGgdSrTksAg@@pCXBf;JDIsmy69vjh}0D;S!k^pd&q-!2I+yM zi9>f7ufri+z9pxi&yh8-UgKBK?Yh`B%BM}*ZC3Bc;2o8ZB$!ZV@7k$CPEX{T+bxHw z?4rex!Wm?%U5rCP;KXEL=#zXvJquM;$E0MvyJ`ggqeq65B}z^M|2QEUe{FpYqIamB zs_FUN(ne=NgJ#El;?QSPX79a$bO;*i%8)>mSE9@3n4WF4)$*29`DB#8MC4{ZgIZ(J z`3qrbE$|~pLqBVfjRfC6VENW5AOE-#a@0)ctyyZ}CX&i=difD4f-@cd;#M*7No&c_ z3HPAu!L;@ZW6LI?m`Q!LrDPhTBRihP=_Fzi;g`WqVqA^otRJ2(ZB&u6&9lE%4656p z*R2AuNWDQXA>c-E@eV$=*;k)j#qWFeCGA02Q<=fldZxUyLLE-R5lRsa9f=N}ox2S) ztz(!1-hjY-%US1%R^hX=22S_e5#r(2*eLT}D1vk8#137=O2grG9+F45v&sop;RR!O zr+T<9qc~?q;Q^M!f^iVL88|BTmR{|CEv@p3S=xBGAIA3VwPT!DW9 zySsm+{Qqq$^1t0b|96Ypznu#5|2Jr-;qhH>5bxdKmeMZ~dzhET`Hb?nG0d7sHVB=z zxH;0C#({C|4zo10^|;BkHAUL{;COV|bw*T z1F-<}BV3ZNi}8rIr$8OR_MkO{OR{y*_9Unw_yurD{Wb(gJgjmfxQZksE{tuC9qfN z@gpP*L<01ee?)+kf%1U{R{U*Qo2(gxZB`YSzthQxt{fq&&6ksx)TKHy)) zM|emcC;)gt{hEa(O{ZEVJt)mvyi<%(oLC&L9;u$Gj#PnMu~4ilQ$w~OX~RUPP?wM zh;P!Od4hA5++zvGyiT=uEYz;+J>Ey3r@?fwNr z)mUJ}gv(^?Vi@uUSGgGH%rjgipqP$V;Nm}u8VE6`ISe7LJCGj)W&g(0=g1}(Qm>VX z{Tyn`Gf+@*&9bxdJPPdNInl6}X#JY;#iFg7dm1TvJlD&#Y3Sxor(&{bUNQO^vvjJK zjuk(peyV>Lr7I4B-RLS(viiBZSRwaTrhRDFkL{+mqtMu9k$FGUyXS1FIEWwf8mAh< z9b}~uU0zM#Z5%wubD>&el?1KdVrE5=u_b1NU5u!C1rWe8r?;#rN`w|4ZEMQcf0x~0 z_n#4*0}uorJIpg7BvFrGx5Jy53P8UZ8Bbaj@!1D8;Qiy-wqB*(BS^sMT& zpnDD~YWZ0-Pimq`tki>ZuQ;w>!z1(U8S?Y;H*=#enm4yUNnlEMLV72YYOoKhfdmt1 zYp?#wcVf0ML)xi{izCc(I%$lCPRUJsJrb!S`e#GRd4gNpU&&Pn%bqK;E2@XhsDrt65Fu`{_9F2`<;n$E?P+n zRu^Z*$X=*+p;s@jxmp`uDB^y-iG~@&s+dE?!6;|sw7M?=CW9XMDSW9qx@n>Q7o+Y) z=uNQuX4?|&$pBtT*;vJ>$i1ys!D2bIR@HQnoP^Mrl}ocppDh;l-pL8gB(CwH9rs#C zDTd&p6K7OdMSI0_o$0w%kA`vW_Uhm(qVJz7-@cu3`QJM6DnjwB{^se=kVTwWH$XYCR7?dy;s*FKu^5uN0aXm^j31V*52KngSoO_yVhWH0%H>P zY%&a42VYD%Q=WP5{y^?4e29_xfy`;DI?h^=#5&EdQ)bIsdZ$^8b~hMTWNv7~ObYZ?;{qm*lsOgK+tR=g{!XbPg;Xk-t=-A^LGI`;h$G_eq_76&*Vcy-Xx}d z{-nE#eyqD+-$;ixp8KN@PZ)QJd-ARX1b?+_9e$Z!3ci?hT6O(>6mNd})BHBy+;y4l z8GZbS%-HqcfCi+ZBJfc<3*XmhA@wgg=#mD;&7{$WRG8a68}<&3$m({(k*zij+(bwm~Uf+MrlVL z;~vJB|HD%n)e`Sy1KHC-KN5=IFu9qM535?-A|) zkJcFffz}@X0o55~yAZ*DxgpDcj$VTQ@p-NPvg-P!!h2#Z0f#M%=QfU9F^0pbMQ+Iv zii^0Nmp_?CC!k1vik1=6^kCR1>{+|nl%>hux!vqa;t`~bRny0iK3hJXJpPBzK1$gW z-5V+4yW(3OeGdnBPt)CPotJ;RE?|EOttW+MZR;a$#)rc3EFm)oa@(Ba)bz;Mw%MFd z>iWnbZf1sZWUH+gwrh}0I#RU9Y`X{X11=~%`TO|+Q<(aso;3YkfD7{WtnCe$2lRD0 zC~?pOMt9uy8w?5QBWZsaKu8`642r|>jouatio^5`+D?MWBY))Rw*&~uL3x9wF?|EK zqhO54AKCh~0KKwM&Y(I>-{9>mm@V>0u6_?duN+h`=o0hw=XMy3F!>{EzY@S-7Rnx! zhxr<`od(lK{>a(y2=JGK@&|2Uz6Nf`!32;$viBPS{>nnRgZeOEgSYcw9xz{{&;bec zf%OS2qRZmxl=46fV|L@r`Vf|H%d*RoJW(uiJX!GwYY2zvQ&ibfJb6wZjo~3;UGqoDcanzJ%ySA?cQ-WezuFGIm1hoP z3pF+uzr)oB95(0)h0I=J_xZSwlK0Vs%q_y@i#A}F&T<2_elY;~{vHzJdD3+=26xp9 ze`a1ZV?!h&3X4=;NAXwCn|zeKt@Y%CZhH|rhSJ;Q)0DJ3rUh_EoG`$gDzRX zh#@4%plUQ6IBWAe0UKYkSWu$W(l}XRcjl`SU%c*jUwalDkCbXce4kFE^Q=Oa-5zgtwp1e48zmb>&K}LAUU1QOAPf=P^FK1e@walB=L! z3`$wBypgr>s0vHOIe5D6krFA@R2+Vm)s#7t1=!_EF{unp6+tvn<#MBiTCIdvMH;bR zvhRj&ic#HYz4g4nDA|>^!-uvx4gj=t zz{8a0S*NoDeYn(AXF1)Ep(^zpaJ#FFmBu8nip7vyW)7z%yvsOPTo_TODpa4Gzed?k z3cg%7a5PA4bTMh7b|s~dGVFZKUx3QTS&yTJdP#K0#B9vdU*$Pc#mLr9a!$qpq(--@ zMjiJl#+95$Z{eqH0vaZ5Yh+?<=8wrC`jy&|Wt$U?D$OdN69XQ$R8vJWL6M~UsI0P( zQSq7tz65Ir38qbsZV_v>G`Yf$X#5;soc$VA&*u2)I!D-a_vA#CH&Vt&y~-vt8?p-3 zG?-b8|6qat=Wzp64!HRPmMDXYfCQ0gNwdK!DDJNTJtrF_2^Lo*nKQb z7}oh}77SmW5v24pg-!P!LOd!wmc_HuEbi(Yc1wxcvx zgf-m6$$9OJ`WdRK{M?x^ z_dfiBZ+-td=glyu&^dCMe^M590c%BY>d7WIRvdoXKF<%BgxYv2%x};jSwBQ}6+rf2 ztpw)hJmD#)#+~Sr4#`TWa>5RB*9tr8LYCeh)|)5nBMvPJJ8EXvTdltKpYEDcrH-)P zRcD{~!CU0m{VqsCc;Xyort3?7mG;-+Y_u_tS*ez0(8a=&s47%ol}pf=(5(MA4oQkt zi&cp=uL%q*pts!5s6TQJmJpC?Pd`ND_sVrbn8y9O4+}P5C)1xo3Ssa}aH3+ysohb8 z*cg)Q56FesdS%o{r6F~Bh17>>Aue6fL%a={&h|OMLWT?z&S_4cJ&?G6#5rNsVLBY# zftl7B)+^z+hWxyH#r zY{~6l$!&(1Ut!U|{+(1s|I#7|L?Z}{`|_{I@`;4=6B=G%Sj?~C=wHJc-#6TA3Xx(u zX`(xq@ZFT~-Rjs+a@kIVewV)}7hMcQcb3I;qH&+-N^Sr((vT!K3lo;Ou4D1NhW{C= zjr$zh^%AtsRP+nmlZhZ7@_-ln&JZE@kThqV_B;mg_zEWRb|nm7 zI$^>+)cQ(WA!^uKSHN&?2O$$jX};MrQ6`EetJyPBCaTOv7dj_zD6p|N$o_9}q3v~i zWlX}E=CXGFT^u4Ar*ONfaC-oC`FC$=Z}5hLd8T;qMnSjxSMeayzh$I&FlxUUt5*f; z`2S}q!2e_&?*C0s@C3%U)Bh{(l{5cOX(A7IW-ZPCqa&o~c=_rLUM)?nE4bg#HPkjBzv;Lk1_&&h#tA8gnd$4qKuf&H961-oB9BF-k!Q4x;_Bg52eHy1YH0pP=Yi8u z`OlnU3}OUg{9r;5Dh!DjO;E^piV2DIFcuQyk6aYKycb!4I_ytp*k zEd??uGI{n-7LXG;SO!2tksahj4`u;d1FS(nkP3(u2Huzg%mbzdA%iVIOaL7a0*FwI z*cb!i3i>1lEC8{Du*9$g*JB$Kfbjs9~ z3BnG@lkmiV(1E7O1Y)4b;6q^VPpAaQ64(oZnuF>{eX$|703)eKIIs&~OZ*WBd;{2$ zd_)C%0DHrrejpOkS9FLJKv?n-0sKz(iUolM;R5|bp)MdJ(ryrx4a@+jBkvA{@_`8e zj^y1TP%bb(pile}`;*tPBf5VSlpnN3@%j;B01_sDMSx5L0z@Afz+32lW1(oE0P=nKRzC{eBobv>3KBhcN*++%q#A2SQTu-H~z(Gn7-VNS{xqHNWvI8uyifin=AB{rgJcidiJ!`4n0LTDe7<3jH{Ho+6%x?% z_xW*UW~q&kXY_dn)?jp*S z#QfM!)Rglgy}#U9m(P_;DaREOe>>0L4EH9yqn^i`&r)#|&5;lc(v2|&O5RxS*BHl8Pud;`vtNW8JmlSsWG%;$-}0Zyb;9HrjAoG2Ci znfpyphBk=e*IwiKFM8 z%h;TuY+TtN3LTnUVh>am;+Nu)gIm{5hODU=4=TA3d56~#ohr=<{JLyTA=gFEh3YHg zrlBg!7_>Z2A(KVV$?66puY+Q_60yeQugRcOHn=}4bi?KxL@A@@vct4-hK;TwUihD$ zSa(`uaI#$^im@UUMZMhh8aWb~qdn|b_!10OIFoYE@CgfQGpN7lis?z^jH*)SF?io` zr@+VIPev|t-O1fC?Cae2lC)LiRphlDJ02gd%`=ym8Jb+PC26I1kL__%*RgvAk7mhl znp|@Qyx9M}C%W*H60Q^IU9lj)do8Lqa18XeiVAyQ!+Lz5G zRE?;lRiRP&c|^wCb6_!FjgZLUI?3zB657JYG3?Hbu zTD9qReZ{G^Pa1=O{OL*nZ&px@R5+z*#lmC6sHOewulT{0Qe9r)-l!2H{-`G` zSBUXxQAS0gS6S@o%6T0uu-KNgEkV7)Vl$o9gQOp5$crgdT+&1R5lX2Y9+CE9qu4*D z;*GN?$9#3S|7+K3Nl$Yvt=8q9?xdLa+4|fh7?>PNgNQ>QO3Eu*Sv{I=Q zo6d@@qTBb|qiQxAP{g~IWhEHr+^mvE1~F34a>!ccp*8W_Nn3`>57#7i(f&Ue6IfC= z9og(-u{tgjQ-)>9(P1tGHwy;Ux`sLenFA5*Sq_XwJJ6gM38@g3zw-u;`u(eT((Xh} zuLg&3!x^`h>~oxd)))jZC(g0FGxXj36}|Lvz|pVj6`h5wscBYV&ra>^QQRS(dfNFT z#EP#r3`i_tc1nE5;GIfY!=2ip#G&mu%*FC0*gTwz&r^zS(wjggB(iUgTRKu zgfG{$R@@|*ylY)*$&$Hu*Yj68k)WxUJJ}?pI3|k=WB*ur%zRizAF~B3E0pyMgzFE{ z@gWybTZ*^D;5sRwvU$|X@;YT@WkO<@Mt^e6OkTw@lnG*W*H!8?|3*vbHmR>Oc>GI{ zzaLXkLu}n#vJ3O?hYDg8=*O%=7>RV1=n%Lk+J;{B_OR?ClV4+{?itvwJkp4@ndY@o zB>hCvGF?LGoc6T8>|K8X?VN+vdhOkIe?{Xb7O2O|p#?jCOJvzKj^xWOdG7N03^Fzi z5E%K+jfSdhZsHzdg_dMU-7FJE=R9J~?QAgURRw0vVBFaolFQ;rn7^6iY_Ffd-daDx zsx+665^k$WMBxvb8sZrA1f5jT(ZwBvRLqPQH+>afD^#>r^N*H8)=dREbFeJrR)AI& zMxa!6zIRxK=j~Vv(k!l}?`^;u7EK(aR!Z>S+QE@qb&x9xmmtHOViU-e~Ol z7%Ge&Xj0%QN=JQGg=hw)9NMbJX4F_txlJ2Bpwzgn+_f0v(cm#rJ7qs?O-wAZvZ>bxjV{VMlxQAr!F1Uh7!>|CD97#o9u{#;gA-SDO!y?F zpjcy?n)dZ#8oNxgy%OKqy-?L&dU1Wl%}!0@T=zgFw3y9>W3tWmF6rql$@DdU#W+f0 z^kK^%b#$7!iZR~W0(z&cslclzm(b0McD{PLS4C01Nk7l@d4_QGaA?DyLTm9B(fXj8_n?fl zoLJj0_|1?vemCgL<37$m#qYcL0v<)Z$=w@Y8o2aUjgbNF-e%X`fh8J zsVR3%7KD@I_U3TD`d*GRvmH~1KMsET#&VnW-6_rK)v3nXHi(;&(oh91X`~zHGMZh( z>g=Lmjl*-cd=Can-9d+p!NyU?s)N5b;M!PiI6N$G_R~_B{BBySuO3(0JVO$ksP_|d zjHb&Y>T`XKL%;tpO6A1F!fMQ5Z{3Ae6;|;%Z2#f@TI^r=<51nw<|3QV!iz42^f=px z{bk+yK^>K-qvcgGRl0sd<-A2~F@~2tg#>Uwl|8w4$bT{AHj295akt*68kMM6LHwF| z(kEcN`332ymn9{MomC8IaFYs+Q*e@OQFu<2;CxOOV2>J)ztCR=6IqU}PBADs%Q7vl zb;cTb{gd?~ld=O<{e{b`8EN)Q@4R|lXq)BBymxOrQ+G6(>N35vZ*NiI>;AoET;ka= zc4#hqwE>ZwG_xx1@AthK`ZYnRtZqGOt8NpGj|LNtc>H1RhCdD;&vj+E*lgn~6)*3F z=Jo{%o)jT@oNzg3R{mf=CN8a?J2B%JCq$h+U~Yt96l*(D^3#nH|H5lHti!0%4U!`I z0hv*QP1(AT_!?wg9ohY?>yZgj4hP~d2j|~%?r!U?zDPrxT5DcA*;ATJ#S(DfZ!oP< zKNOc`>NB}Ex>52OE#s8k+5IVXn(n}BnTnOC=sz7RGT1PD zN-|hC!Zww8+7Axyh&`ebcVbRQR)}Vwmz|l)mPr(|*mO-8#!@VQBrz zpeSuEDM2Rnp+mJz7*Dq+k}uG%cjO^;iRoqkux`hUt7lT+5?gQ)t8d{E;ld3~ys=#? z*+I=BsC7?~hdXg9fe|1TleCx<*eF!yGbp@t55%Xeb*$X;?~fCA?2gaC*}CfxOtPC$ zqMjhXrS;>R)0gHPER#F}QO4Wt)p$}azUoREc-uy&W;cxKExw+L%$njSzW!lS@e-|U zla?#9aPnna?u@o6V>IvPfR)fo8QfPlr5C2tD{B`6`p_Xd9no|kZB4_j{4)EHH1=hN zHccz*n%$Sq(+BgfHvR7&Pg)%0VZe_wh1CNxxtOOSvaf>X@h4GXpI;vnR07zN z^GgO!U%;$5rb|sPaHeEI?ZyW55>Cfv$pOqVf7Fr*yu<$BG|rPCyjYJViqf8-NrnCc zNAhG!i!c@+aD>^|!tc{anj2~6ojQ3ugxTH;CrS>&(>V8=`_L8KsQB1{(PZJlpPVSq zyM<^T;@E;C&fI-}VO&%0uORK3;dj)bk3<;!Wb4D^-GBG^R#O76AK(Z420jK*iX9IK zi@-zwO<#WvRyPX5;pXU4x&YOwi&O!GNHIsDR=ms5 zoypah8n_A^SyDU&pOGafOWzX%;J@uBwPc90r2w8rW6qx6RVU@=a#SDQi+d9$uY+5d zs^xnb*0r97ae38e=}OiE`c!iDF*Zj7ke`g~awKki>)zV*Gqm2O^#=@3VDActcdZ{7 zcit0J9D(m)@~dg<_vUSWgZ4&lYfJ0*Zf$-8_FJxNZR_{;ZGKX^=PIw4e1(0bDrI5d zgw>(XdC+nx!Pli~|04g>j~Tu3X=?m8$^WaeuYjs@`QAnZX%T6V?(PohZt0TlZUjY; zPU-HB15yVhM7q06-~a;BeI&ob^?JRR>;L}Nx7Rw0dDof!%zkIyGw-nX%zoekGY&RU z;J)o<1a@0`w!0~!*G0?==-Z_Nz258WyD75O)h~{>0ir0i^F`Tg$Fhsvw%F~@2ci}F z-4x;6rRw(`6{+f`RtYaZOKK9G@{ywC?e7Z|-6%~p+0qGfi>~+XR*>$j+_9c34*~8$usi!&4fPAqHEx4!Y_5@-szOKxp0aL6wVP)%L z?v)L}jKIev4W+eyn|4z&IgL6&PZ!&@#a$G)nRC3fN<;Q+2^3t2w&8OwwXK8q%mEY) zEw1FF56g0c87m_x8zy)IH+`on#Zapv$zFX}r)n7ZM%l1{_++=rPYktEL`*|dKukls zSq!xnvP0E0AcW8iK-DleCv@UKCvpN$m*PDzzRWkajAyKSi8IZ;`ig%w#XvXK&sxS| z7h4w&W?H!Q6sFZx%0WQcK|tC;K+8cu+(AIifdETa5nXrOZ##IIm$p6^aGwSB?7zTj8B;77h-312X!FWAf%jO7bf_XR(MKD32C+=V_Q zfIduzKGcIgT!cOpfIe)7K70**cny6BsqhZ^5Dpd;1Oq}v=s-p4Ai@fMPWr@0mG~w} z=*rO-j1GNB1q+gd0ZGDwAV9{@hlMa8MWl`>qz=X4?Kcw4H)RG_c?MT;23I_-V9Qo8 zZY%iM?+_aXvJxT)~q6ybz3D?F6*XYsLKBSk0q?fSK*Eu~m$vrn8 zdu{@H-bkCY(4TpU@0^#+>~mBuZ5y?4b-b|;z5ep_vYFw`OLFI2wsNVW<4p_J<#iyt z?HPbG1^njtZnE_;%z4f1kFpa z+<0|rtEOj5A{K|#cX!-@DeaNwrH{|}b-vG>+D!I-GN%hmA6Dn|$^?9^x}3Elyo400 zAI@1l@X7*URUyq;pXat>_~ETqDBGJo4}z>EY1c1SA#8c zgLt@|a<0yRhj`{;X#HX6YJ>)PxCVKIMqaqa34R3*vp$PO69{G%Q?!dtfnDD*<}fri zLL<$n38cD;*$yL(-w>Qe8Jq_jjV5fO`UE#ueD{;OdpubwXJ3h84 zDlpL$_G}4Fi<(D|(U;s)z@Pd%&KD^^?Xw)dt_az`l=EJ)2VFiuUqVd*f1LofsiP*t zZ>+f$sfuDOqozV_kk`$rr>#HU2wR4yv`#8Bbs~?=O?qZar{LEOc`BAGA%p0V2H-}$ zunDZnS>@aFdkRVVZZK06}p$m6#hH zMF4J^E}KrR%3<|1tW_YW$%CIR&LGAl#%OHVfGz9cj6@WW2!-!6y zpm6S+mQ|pwxaWj>3V=HHVhot0`whPm)AYqs5r8`L0)mJ;S&3OV5dj3H@!HWTl%$5w zo7i;rx>f_Y8|@(9OqT(;TNGwO(QSs*mvSlX>G}Ylq;-_tPSfq^`YJy80jS$ALV-EY zw&@^G`c9m%6QmPt&23I!Afop-S@7TfZP!God^IB^1o4+Z=+OR1Z3anE=HP1cYilDE zl&j%1`(M={g`Z>tKWnPA4FjnN*#}8_4;E5c)7C}_(!$^KZ^f%` ztItb3$w29Pn&^O5_cW$}26m@rnX=hmBxRzEm{rQiUW^U^(x1Q(NP-U(5&zjZ^Q-XL1zj|seuMnc1dwP6s zCq<7Q7*vD9mg>Ysyj}ETZlMY z2F};cPe^5eCI%-<7`lB^Oue+Ws%9{W+}F~xP>J5W?nuSKlebL=6MEAOeRPGQQ zH`OeUWs`?ntFyOp-9Ykyu)VincXdrZfU_Z<<^@1F{qugiEa!-*xvI7OnPXHR`rt^a z$rRvG6*pnAOUn%d`(}upO7DrwevX{2Xz4g7hd_LW*Z%8WsSfFI&5^l@+EHJN@08k9 zDo)y6U&ZTW9(VDXnx&LNNfkz!6WBs5>`XpprzHqJ<4@^ zo2_!oEL{=&SlP>UQN`oE!ZpY(wrJZqiTHdm7?|}ERdYX&?d*;4ntzBmXjy|E)=LGzh0#CE za{{QSu1=Ea&TaG8k3PICV`m!?%!N= z%!Rlu@F|vI1SxzCHjwd zBIKs+BEAdF2dWLHxiFHt1J-LJ#OEu&O}J_d+7&v+RB>^*71{E9$=@v@Kz?w1xOPkV zmJdan3!eW|@p`h^CJMkgC*h`4s(Bu#qTx%t`c5U72LGMo)tOuyzua;tiV=}yk_bNv zn*e{92czF}A%3D(_a_Wu2YManq+rpGb22bl2P!GpxFZbh3IUW4uP@Q`w3Vh~3;su#OWlB_o!F9A|&rSM|@~HmV&B~WgrYd z9CRPBdo)PCpsov3mrm28ie^eCp~OD-KXRq^K{iEsgP4b9gcA8sOY*Xm5MO{Ygg_H6 zbU~V^906dR7U|jbalD@4;NTJR+FVa_SN#r^eE1bY9xw5NN(q9YQ1N3#g!~kYNAa{& z1BYj{t9k>XI`$gptKAhH~CTR>tB5TG0al zd1{sjb@QVBSf@N^b))b>CtT)h=PqoIy)aae6yLY-fzROsU`d{| z>eRdzqwd*c=i%$U??VC`jqdx;Z*LB<-#UY7BUkHZ!ZaL02KiWYBgy=vVZ{jxF`D{U zF<~_>4+zU?sA-bSECM&GZN4UXrlSv@8$IBf@p>uD6DhtBzKZqLxTxd6SI9;0Rf8o0 za{T_c^Ao@P>C%98l0a|h3irS-pmPA)@J+T|r%YOCtFL_VSaBOH zd9O1{put;xjxu{7*Qh{&kjO?bqh;_(Rv^fX!Ob>B2Yz~i)^+Zn9Y*4@BH4(s%_gPg zxK8{RIoYulxwGeDyM;vrjuv%t2`~yzcz3NAy3kR+wdx$XtZti1a^(?8Pj)8m6d3pL z#Gdkv>J>RR)rL;LS9g3`s2aW;CuG$kl>arIZ)~L=;Xx4?lJbYyG2sx+HqLUUymyPz zSX23sUz# zVlg7-taTts%?P^xj6V@NRvT`Wv=Q{XP2uMtZa)}E*q5aEZ4ehTBKS)xx2zVA7iACQ zroh2$RLyfVtwjmNJ=i!C7xdyYZ=}jR5i_J_lViZTi!~3f`GNZhfrwlc<2O1F!qpQ z;;k!(m3P(l5p!JN^}_26^kz~#3PK^+8jnHVU+MG3;%i zUQc%Bc{Gy?MU*>5(0MFXL(Eo`eY8&=XB&G${SJ1p^}LiF2vorh)P5rYbbSPadPIX# zt4Py%sDm>ku;gErBls+kkyeKWOoP6|CkCS42@mvojiX`1nC-H-Y?d1-+ZCm zg1DfH8HC9++n>lG*^22-LRrxOu;%=fgbpO66z2_6{MTSY?Gy;c2M6_@)U~G7Z8ad# z>cbQ3D^W4KJ2qS{Bpq{dL1b~VH)Ee%UNjZU=O#LB-+RgH^_Twh4D&BuU>2xT6;PMw zOwNiKm&}LG07`G=_QVB;<$f#k(u?^5{T5IMgB_vIWQY#ttSz41v_la7>W9t{zR6>2 zt6Rp}hWQBN0|@n|(dQ8Rq75z%?|u}fPXNQl7*9pqz0TKoZgGaBdm2or79L8<{A(Qw zT$HPJRICa+Dj+wyR)U%xbj5ys#n8t1BX zUomp&YNS$Tv{hJ;s2Mq7u9``he@-+VVtx9eM2{TR9)H^4+%>cJuPOv~#tQh}e6k=V zh=GNy=bJP+LSa>NNp3uZ9YKeD!z4;KW z^hPDMu}~4+^bn$d`8J6n1iN&iHu%yWcK;fCjd!)fim(1uIr{F?he?&x_$;k$K*I0UxkK0sc z{&!XU!AufFib!mB~`TEILUX5n`PMi(5}s zHN@ZKp+%l|z)NF?Z20GXJc_f+V2CJ;HO!^Ru^QGH@O?$0+|9}yTN;Yd6q&ziauY?3 z*g2lNiVe<>e7OQk(9)$X{9&3Mz4bG(%_*XY$TTJ@+#^2O*A=ta=`m7W?exhFGX@{? zT*`zs9p*0MnKgwSQ>x&@W>vdFH3Ug!B|{T~vq*wM6iF;hlPSK`k;H-2xxXX^;}xuw z9-lgqVR5})<81Keee+I~m+`X*K}%;CV$#x<2LclkrF+Ngg!E4~iDv=T5mVCyiro#= zaRU|0YQSN`*}_$;CdwrC1XhE?ua_@J}B&w99ECJ z`mJ09+4kr*n{QG`2Qb?=o)Fx&wYXlv-A*1d-W0A1Jj0UnB+^r77EIL}U>AAM;eS{V z;3NhzPc>5C9KBK_&^VoAyC!S=){~xM1eoq>6=4Phzs*^@NU_=0%}8gL5e@N2Gak=S zwjsy8CUs9a+<9zx7?qbQ&=;QjC?5l<6Hl5Ou0GtRiX$a7ySAuPvTxXa-gE1?{OEB8 zRx%R5IBFSPVDdVJ=Ua9m>*bkn(XY=mN=0_SN1WYaY>9exjFDNYU1_ z^2}3DG~_6Tcr-RcJwyu5zU&qnxkf$>E4m!8!8vjclhsy{x`C=Bq6vFXlnbRCNAv zm5$9R^#(R-{HnT%xi6|vzy!FSC#-rHvZY{HWY1`DoR>^|+q2&!7!RwiSsloXBTcJxK7` ztdpJQ-RgPkymDeI!X-0{g$A<;GtDN>=Pmti!Zhlcpl=4nbC8?}=;TG7wbut89AA6X zn|0gY+*j47UfZtxCv*dw20^!pB9-jQ2R7`0-? zAIZ1X@3v=pfI+d--Ov4YTI!ZFqq5DBMHlf%mpfs_ko?0?=!PA6bWWGuV{b~Y;D{=0 z;)qk^7tisMtFPtqwk5#8FYaaM@%LEHu*IwKoi-XhT=|kiNNsGP3rq;=v)CEa7bpxW% z6szICGD_^F=mr6R-xBkwtME_x%EtN=3uXcx^-|ZXYMB*yhwJ2?L+w6*nagW-dyX&p zsL(DzpC>kK90&^CimztnZqD6<>cf}&GvN-$g zJ>Y(4>e9A77c~hCWUaVFkQ?B9@N}Ts+`XWbKypQ4T-|K;L4DKSYm0Bo1%)>F3bb;$ z-)Ya&L}_+LcS`YP1oHbz_#QJ+InU~|N7sDJ9kN~uIpVD_rn+)vbIU2QgELkg{4(EN z<`r9gv*LY5(1shHjO0cW^n!LdK}NOTLoOa6i9&xMI)q!2GFwYQtWh=hg+d#}f(}FM zD8&mfjTh^M2_9!C5tl5(Ui_<__fTg&#U3hVAj{-{cTT(+iI1MeupKm{1#poEbD1)J ziFQ6HPwS70l&F-AtvC6!IYz&lFHYI#slotYmB*4wr;mG)uQwF;IGHTWBtuuIB%gmP z&Q6O&Ihn#zivoY0^rHl0^!B>SM`gy?!K%5EnB5Y%J}mEr6!i%!bUMG)*aZW5W{R># zGjHhWN|CQw8LzDfjvJGYN2s(!AM5j(?igqsxxneS7HV2^o^@lF(ey-89X4^?av!i~ zbfT=1$GI& zA!Oajyn=YB-cW)VAaWF22vJ=y2G95JS5Z;J>)pflOAoRTE0DR_mxF~!Oui*4V@gXH zwDa>6Vo!i3+{+ST%J(OD<2kjP@n`6j$`3>zX5b${N3)UWPQ%j?K#QN;2#3Cvc9k}G zY0M#_Bx@%}YiGY(+siBx&3aR-sp#)g=G*-)*m)Z?awfBj9L~Pr@Qn<@pWE z1IMio;PQs6o(l_fDZ^^!j^;ojTHM(~F+<3PIh53nO@zj7o5;mY-cRpq`e(OrIgPce zIt8Od%FMGVNG+?46UlV1$%?F=J@N%;`Jz)_3kbltbrrSpI8*yDr001`wksNV^%dn8 z8KGmXFNscAL|P<3`N9Tg4kEjenH>0_-Bw9e8xZ#k6GBZxV-%hx81T&M_-=3MKYkrI zwuo7uon5LlHNv4eJ%UnSoLy(YmhV`jMP9d}@U$CiL4L|>s{!Sl;{xtOZZ>Kyu<<}j z_MF{?NDc*Hv1QC4FfvA^eW5B_n#fgB;5TGEDhCk#9)UNPd;V<)V`k?-%3jaqS!|ScKmL8;%7370)SNl)N^SyJ){iP}| ztRO8R{z8>eUgEzR|JgJY)Lr8Z5Cq7bLwf7~HuuL(+%m3X!_9GdO`=0+U0Jw(@IC)j&huruuPS>tI)+Y=vHsy=_+|YM zDC#-OgFr}hXavHqcbC=Tr{wv*Cb(zC9n}5F>lfU=$L=nY^h54$N`&_BaDS3m|Ln;h zw%n(&{;8hlYyWTMf0A1Nvf~HTeg4~DP`;43*ssq0HyiGKr28C%zmOy$zm9uIzZeOB z!Th^pAg7Z5l)H{85%@dI57wc7_RD?Vp`Y4$zEl6w{TmC>FB5+B$$i$GUobD9-#6i3 z96I;G?x!dG1tzNgTd@D1C+99B%MZEhplrkYfPdv@`DZ`f&(QJ{B+qx^|AP2?zLsCN z`0pXRn>c^Sokfb4_bvKy2K}Gr+&_i>6Cuyn=ePQQok{;ML+%ci{g69DP96Vc$lWos zf7)^XpxIA=JYRCB->CmQcJ>Pf>W=`&Pqn+hcj4q8a%V+A=nojkH1}t4`KJND4=?}H zthlH7N3i*)GCb7p!p%P$&huUPxANU&cQ5$->o$HLr}>3<9QDuJxF5{E7qz($eBbN) z3-~7Xzk&bZ1HKP?e?j>R_9F9lu>bWt``vQj2j1N=xug$4qGgaq;d14sb%KhD)46@8m?8v#-_7L$-9cTsD)1q+G@b-`+ zM0y4c28{n}L;Jx0x^vLK8#1=H{g+MuyAAAr+ZfuJIN4j8{6B5c|IOCP)Y;zE$=KBS z|Fdc*2v-#0?*SHr00Ghc$5l%Iy0$0+=X$F`kyo%>jLf`ZuXs`nZe<4;X$V4Jo|`5I(eHCZ;K=U0e~%{pZ)r zWIEUV`EzUz5d3X@An=*#MY3a#VUZ`RL-e#@%M|)Ru`4kk_$cy5j|s8ZPlStmP=zbM(4BZYnhpoHKa<1^|E@m8P7WVJML-B zU*k=pWaZGKv9#KBl`m!>xWu$s+bd)LR`~2(ZRW&**Rb?pJ~UDce1s(=IR{NR-6=!a>ph1)Q;lh&fVXqqV zP0!79PV&VUX(GzGF3m3iN@NpTEP%Y<5qGQ(-CK$^T0$?s?e*dsjlyL+d0N(LoVMpF zcAwB%5sjmEz~Ndz-Bd%Lbr~z$qaaBdB@Y(UgFTEj0!zAJbVi&%X6V@SQx&UNk2n0I z&3Fw%zrZpD*JC${GcHk3*23vGAN+dh4cQIVJg`dwE4m@EGFC9G4FQivehWmU5sp}r zprxn`C5m(K45B)!9#J)Q=3$PiN3@6TF5I46a7)+>l}~nS@eSBsB((j4yVUN)pgL-| zw2$2GIjgaz1mzZ~SO$~A`aArSty!CxqbK>YLxz#7T`%57Q9L)7^X1o%|NY_qad*w) zPcg&5fPhk9fPhH<n9f-zhID3Sj}`-IDiDfoPv4iHK} zA7N}*S+}mSzijGmdfJUu7pMZk>|Nn6eOZjv)VU|#8*&xkzDd{M$DrUCybIRoMvXi@ zWomBXLf z%AKGx>-tIOq=j6XADF#$qupA8ac8+z$76V08SdmZO)Z=5jOADn#p0>lSGRR0+|MJy zj{KUGp3V-_40$4|>TH(M&th_Xp`_Cnja@fVE_ZDPC4b*PPnTh0A%jLwa}oE+H)M5( z!>qdTp77)|O`8m}IAVS9b`|b8ulQ{*|<>Nd0$Djkp&w^Hr zUO!yOom>t0@FI0u#Rtzi++Ow88KkL0j7rn#N_XOIF8U^JE0~PBm^}@ktdsontPZuj zMp}N+nEH-7JdV6EHDjMNmHl;kBA-hefu1Kk zg&X{b1|cC*!L`d#d?Ldzeksq{_)y$68WR!8hn#mHpM38qx3msB>D&7nVNd84m{tPE zk{w9(SYGQQf7olDuaVt^<#zGX_~ThUK{TKhoS%N4(JuS`!ytE=B)zIZ8UZ+Z<1vyh z+bkZ$p({Jlpi68g@wv_5`lKhCzrJtH77_OY$-L5R@FF({gtf7nvVtQrVuVoU(zH@( zi$Ewm&QjJP7FEi!3e|4k<9C7kO8|Gu6 zV6jg2r$93IehKpM12<0(k4#)(M5d=(W)<^8CrQqzQg}O$A63^M$exw;#$uut^KQICr=$*sWn*SMNt;?j zmifm`V}PYtrizOLC@gv3Ck1VtlYY(uaO(Rm`)C}J zCucIL2$zuv5vm>QswuI$pLn{TaJ~+f&s|tlagD&VSA5TtT*p@p*WIZCTOalCxdaHr zvy3=H$1lWDCYa8OE(UtAD?_O`MlVXR`eIxgQG3=J{aUk*f)ap{e0Xh6yYO5xJY8

6Xr(osx>@tKNWD4Di~9>D$DxU} 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

F3zASQFy6m2Y}piN-g_ zzC@d$_nzP1)9|5~KQfN^ma(S>D4s=Gwg~@{PF)snP*dTO^g%OX3XIheEck$zV(6Bc z1wL1>6^S;~K}U9YcyKQ^4npIPr(E#-Ha+61FUCum=dN{pa%QwZl=LZY$H~9%u`luY zz_zN6J6Dx+HEI9!v&c$O0)|k+lBt_>8|7w)B=u2pL)E9$32lGXpBLKtO4=I}pmHPn z1*|8|(sNtCjP-%s5op!OudrCkov;(6vo;0cNWFQ|MCGMNe{rS}C%=!f-?^tqlAQ2a z%N|a7{AMIS9=aPqV%TTcO_(I>B}50=pZFF=Rv&^%os-5%>? z6^2Q{VyZgEQye zV}5)@K9B(WQeD|Fxv6FMgQyQa=F*pevEJFC8ru$k&_nfm3oSDvdJz{1caPlUE=$WI zCVSz3q^wNsj1qqpqgbXs3w5>AvE%mkc{HCne5oT<%LM&OXP0i|Ti}t|C zo%Z=J8**5&n>m3(8m|v=Jb1O0-W#t$+mhIF9>!&0J+kvthrLYyB&mGiniW%|vzoQk ztZ6oXt5Iwkxo&vB{ZGxQF;i=`%#>y+bVdoh+Cm`jE11#9KxQ67=vuq_BBd@?E2vuk z$wxH(>-C@9B|^PemsOU1=^?h1=%8M7GGOrAsewm&*HfS||Kc1`q8Gl&f8i0i4i3vE zf3lUoM-GC$sD}!XH$24-)JNSt+oCF&WXSu!!S+P5LcfhS4{B^{Kqyr6pLl~$B zeTZ-9U?!JO>15Ok$gySG)bHEAN0gy>dR(~&hb6b4LFaq|Pg>(5TkMYB^o;WSXB<*z zKWCc2mL5h!gUnOf&?HyiaXyl#LB4p@g_9l2VZTmZQd(CWMOTv&1Lp*sOI=(RQB5|s zL#;uSqFm;rNGvJ3iEk29`}sT|1=Cy&^%8SM^8}|u@xpzH-Uezc9Pz3)BKx#Lor+?6 zoVv#6kK9uSh~t_6>KFeoXnaFK;%qZ_KGM@E8>Bb}Mhhv-9adhv=f=)RNbLMPKB#Hz zumx-B9rS8h%_R9XtwdfRBo#*GClKM@gYy;$_8ozyr(s~-la-(07h8$s1psCXysd)VquYH+U*^c%( zzUGi2@8@aK@wA#OUDTiPW0Y1fA&iwg!qgu5t&QQA>}#!VmhnUsJe` zwpH~Mo2s&Kn*wC!fBk3bG^%9RxD)Q4!R_=*1!}u#2>n{noHhPyAhYuN#^0)vo90)d z?sx8mVQ(Yv|7b55j_cg0CztL5111LP*72y&y-u7129=2YQ3RqxYbor1FR-S?mUD~m z7^>CO@A%zAxrK+7xmu?(p`J_AEIPydMvQ%iZw{rXWNp*LW8^~;sP*$K4;Cxu_Xkz` z_0!*Mz15PfA*;OPWqOX7RQ6+D_U0b6R0SD z3Oz^7z;eB@l9L2^Z{Ge zsw=FZrSr!=H-Sg2UQ$O@&{7+i%gL0_T{~DL(;cfLwaIhjJCe)OYKC$FQg`e6OMVtk z`7gr}f4-ni3(oY-m!()Qj{6`ZF!D0dyxE7+6+CaVrIYy2d8yK9!bNq{lB1Ko8j%I% z-L~ClFCydz-lzf~+bk)}ZVl4KZXNr*ls#pb710r$yFyz_h{Y`DoFxApH}8Mhhvu!P zoB#baVQa*EZP6)&{cVhoo=m)F2FaE6aNy&&uGngl;rugtMo&29TCZwLX;*%p;B0Lo<4fuccw~!P-?fe347w`bCefd;zQ2kBh#k>&(M?_fU#Y%bs0o@TFDA8ek|Jz9fsYB+Obs^ zc(HDz$zBw_S)1CefSr)dR0Y@^ZuES|RZTK(KN3&!m|bD;Fp8XE=2s;D=WF}(mI~v9 z%YI)Z3Tp&Rsip60;RDqjTlOR-*9R)=t~bq@KM3+fJ8k>Iz%)*|GB8iJ3{Q;s_fYj>BOqGn3I;p4~MTOX6*h7g^Aoq{?0`2pOIb1VG=1S ze@5TW@?z_C9p>DBlmFWA*C7F<(dVK8IZlt- z#`oAY-#-a`*)+wvqh&g-gkp3<@?QXRe!R$0en*uC8J((!KI9placiYaS1Y|xp!JO0po3~tZM z;?KRUve~xiNfq_LW012#(KO%C2DxG|bP*RQjCg!s@g?@+oqzjGXR%?rJ@+Y54CAH+ z3v(V^%7IJ>(8GC>p*G z%+F1hlvqO)fh`I3m?=&I#a_o{}XWh}t#-`DB9l5*? zl++e>->>YU-xTu|TFVr8Xa+b2&CSsCaV%Ufj`3r*H6~U|n_sdCHd%>#CUG)#@m8w# z=J)Y^20a|E7R19F263Cj9tl~FSu!|?(gV>v_#|-J_dYgB+_7m4A_g)g@0X}S6CPsev|M)8mt8*e3g`U)bgYyoj59hEHZi zmb=2FJzE1a>Is$$i~oo(dwa;^>wUivo)fTM(0+8FNU@q7kGRGNp90p#an)iOwyusT z3)&;%@lw&Ib)FiXU|hwPTn&`M*nfRv*B+<$4&ROSx^F=nZWO8blAF0BmMC>J>_I8% zll&tqpWIY%Oj>a4mHpx7?up_<&DcESd;Hrn zJP|A&A34ZMjCls{-7$GG@5jR4+oqz0FMHp$4M$^*8P-S*0Z#b#oBsVMy>i%~32;v* z)&uPQ&PUjvRP9Z6$)??k)+GTRvIllyt6rOueE@_MiRl!Gah zOfdo*qaMWqy1j~GgQ>G_7~PF(XUh)>v*&0iQVi)< z`2KeAHuI7i&0RE0oir2J5btW6g@P zaO_UPrywGZYe5!&8t6v*KT9k8W~djMIS2?U6o?3d|8Ghw>Mj-#M=K48GsNb9>s&fI z&;zIkNTj=(Ic`^0{_U42(kS0o1@v`b~ z8aZ%I4b4v)C97!d9j5OWscqirO{z0`4$4$Zd`oqZ6A=?}QwRR^vPNt9c)Ka+d@1LU zL$yx(IDF}SbPxZ}IoVcCBu)&4Sb2x<6>OnAhd-zcsST5Bs^|Pl3J|LC4Ur6`8xU=e zimRi&85?lnLUQ7mOCxa2P;ZWgnc@Px@kk*IZcJ{}@o*s&ZVh$ncb-xubR`T{7?vdw4gtWxmAW&R@dBSCDBQK7=)C!*n_m*nFE;hk9 zVYDTaZ%0}B3g-#;7H!}xHo-jMu+^7uM_X!y*MxgZHc%D6c|4$(tYDnT!=1yt1saTs zSFlcmZ0VxB#T&$nS1?a_Y<=Y0(U&^mr{Uhx4d}&zC`;jR+OQj7gL1JX)(M|&vix79 z6HHqzxkt8!isC?=r5Si(XcNhjknNveH3jllX+xmFq3_J->@e0$lb6e`zYBUE%V6KAT9eS(9kXO z$l1^>`zYQJEc3|P5G?DDyVPCMiFbl)`x<)luA#ZO6ZeE>AL|O>N#aLy<+qOurz(jq zL2g4ciFTGDgKO-QdQ!C^zxA3Yz4`;+lPSUuB-?M7D?$&Hx>_kd!wbaU_m^A4eprQ< z!EZymc(0y|c|*FSuaJxBFajB_!i#ysf`C^8@GKcKtU!vZ9k{-X8Rj!!KUz*2=V1e$ z91_HQRaRUZ)V^b@!uzarhQhW{;YmOl$w#SFx~;@Mw;Z^8YG zYa>^&pHcP$;Y9Gq5_}mm^gzigq7s*oE`kRYxK>mU)|GZi5!$ooKF^ixzREuPKJ695 zzTG|y9=Oj07l7BpJ>ZvcY`AiXOPFbdX_#r0X{Z!hAoYXwJ`tR?qzLg@ecwh_n(9Fm zJ{cKAdv#f2hVU%6PbS+<{=fp~jSQl=8Z9wHd{*7pm+hu}cn`0Q45GO@Dp^B#Mutmc zJS*+fg#{5^<&~@;}oU3YhvurTwg9p4Q`o(RZS0V;;1 zE%HTje=Yh2d0#r>1?!4V_Lc8SzvLC=L965y=V7ws73qPu>`abmj1LO35fRRUSz{o>WLsKJD!&9R_hVWro5G61s5GL>^U?uR8Zvlm1*&%$G zTR7x4eInR{e}EIAZafK>;C%coByzI87L36GAWx_pd%`H#7;g)UT)(dcYj6%&6Y9pD za0E8S-$Em&3ptl17w)?QB;i-0`2?=#Ne7O<)3GtyuhPzq~{ot!TGTrj}~>=;>xJ6I1a3_q7h zNQQkz9E=A_A#T0ppl-2{7e$>@kn_UsxDqCN?<5jXAgnDLU=ZFG888U*%|t5* zcS{%;guUeh48q^S2fkozX#rnwws?UrSX(Z@7rZSx;0xxK8PGKR+=aXr_8ECF7~Fxm z#ZB%4`;0CZiXr5DXbxfLH_~1HC#1c3+e$JtLr=-dcVvG#k4PyT1l- zNxv(9j8-S!PYdvuJ4US&Rv+=`g;NK44E=qks!1> zYID?n)fjntyhst8P1hJE`lJXEEOU~6`!=I04WA z3IGoDZS-vnn*d3G1A`xfX2^c{2bn8*9GR+c5WX8@KUz#8fK@gh#|^9h2cSYWA8$hr zM~<1{6M(iKpg?I(-WN?CIxJ(!;12MOLF#Age;323fLV(3F_b)xS3=1^~+DUn*dgrhficQiOBL?U2J7E6_z(-~o(~Ycu1#l~~jO|9({|vyF2|(GDjftkG zjrxqT`7vgQo;G?B=^Uj$4L}$98Eex5pd~MXTt{L~)n5;ol$F3gcZ(sTH;uSM>1Sn- zLOmz!uV9cuKIiYZVUR*U_l@C|2_V>11DHkKVfE`XbfKMl#kj}>5N#R()}rq4`h6I> z(9Z*7=wzM%ndT?8;@M_Zp*Ig0tfxJfW?`yZqUskeZNI!d(U+exxt? z7z{#-c=;h)xz`xg5WedYPP}FU8@@@g}EX-GU%pbgpeJPcF3kyKlRR&98Try2xu00Pn0bkc`uMH9et0T-5qhyncW?APnF#rc`u&b z9et0U9UO7bnjIW=`FQO4JFAF8@6(gTF2dRO%BO7t%-9en- z>Ig54DtQPE+L0?rxkrp}{v%Yc*AVx}7Bti&Mmp~XO^TjFsie@u(ZjCN135vLAYMI( z5sheiWL3@}H;^QBzV9&73#m#9gb8|vqJgW!8)@fbp+Y@N7)Q(?R+t#-{12#RxR)3g z**tv}a}_Zc{yd;cunGc_sv-bUfDAyQAYTwFs0=F9%hAWt%K`h@LmB=a&q1IH6O;y> z2=RJXRS$mq$D;=A^@-un-$QvqycnwD!6rCIgrKaxHk|n#XibP0OI0P<1owy%w9wav zH-8Ov4!K4I(M5PMRpIxxVa|_3okP7itNwxm$>&d?ry*XfRqfyv+#_1hZC@My{4W38X)cgyM&FV%+pX$zY~P{_MwA z&@7lKqCd;AJeUsa5x5u)lR~;lg(in}V%^v;D#N7EZt9@P;eisz?-s|Q7EmK_bDwkg z3SK+1H|z0xXgk;kmIONu?IiW)JthL*_GA;DWbRhCEe$Na2F%={j<3FqeK2GipqP!7+HutTfJsK=d z_6E`33_-dP9u*ghLY`F?i^84>kF{VgILBF#VA7jCa4_u+8912WCJP))aib3oCb?Mv z2h-dLgM*1~n!&+TH$LECvYT6QFx?G4_!V%&2@OVh9DF9t_EzZo_~J(Dd4M?MQL21h^RFTO@R^Dfr*z6c*LK)Mkh zrJ-vP&!orAkZxq-hU#|y`t0;1g=RD9>n>&u6i(0$_YN#q(~3y{H+ba zUb`rw@EN2E5>2XnZxI?-v5l(G8Pp0YO=?a2{6<&5jeLX(d`%=x>3l?yqxGt255|04 zSD`*p%!v_K%}@`H{F(K9yrD<|3|355)S++z6jodt@+5p46iqZuQcXNfT1~_WwtNlO zI9FBI->w|4gX;;fKR7l>6U_Mn>-m^_Tp~6wQS^yc*NJcsseJ5pV~jmU5i(dS%EVV! zo^TJ*e9m=a%smbfeON2nM5Aj>xQApu)p`KJ1p3A@#-6;ZbC`!fzR~(J)}D|EU6hA- zzWDkw<{poT53Ch^qSN&>+(SAaeLVnWBHWcW>=Kx-ye@&Y$0w2u`-8NHDWcVP&z4`Y z9)L43<60P6Pck7SGTG;gH*x7Ig|~+)!rOO`lK+-NoS(HGKsjNv-hs1M?<$49cj?-N zuov&zg|av5+J&@N>Dq<1cjVfIxR>YJg}V3GwF`N#-L(sS@6j~~VK3U%H1v{5L^|q{ zBfqHUo;|;)_g*yLtmmF3->mokeg0a{J$L?E@4aNcbk99gzI5-sKz?`6J!gJ*@4a|_ zaL+w!esHfZ?nL)S2i_j8$ZP23yZq+$4%|H&1!4ucnC~&O44;xeATuV$L=Ye{QZkA$ zqA?;75D{P#P!XV~VldVNauSEB6?PeaVKcTf^5du8#~eoupT-=A4j0B8e`WtOyoyqa za>D@S4SmMoqK8U{B4bd005gP!V{kFLz7?KOG65h}Fh%GNiXH%@vJOCsCKO5N*+5C6 zcV!MO!>D3}=JrgXBr&*hg>GR`YZb@J2!xV{k|I^3EMYVd?bE|mz__72NGp)5nS*hM zjSmv#iv_Wb6*NJ^A1kUQxSN{3z9;x(T{J7XnbbP^%UPm$#@{na7GwO{V0j!T>6 zfR37jzsl>u?+le{I#q=_ASWO{v|Jx5h_!sL2eEjPq4v zlTXcwO8)%py#C|tg3jga@{;yrL(|t?_W4<}%h_oiqx!F2#k)Tm+uLt`RCM0h1v-pt z8)=u`*r9PdAvc1Sh>jcz#d7T*kM@o9FK7Rl#ea2hI?#` zWHh$0as0iO!{cV7R;*!ur$EcBcz7len!V^$b5oP`nhbZQLPwLexsy|H?5g2UB393N zsq48UhPoi7af(a$nofLux0ln2whi~(GjgLAa$sWs#Uf}l3s*p?y-t49vn|hW2|aho zNUM@GI=wB;mVHsYBPHG`)QH1(;FsBOmf_PE@$BnQ3@iuVw zu-KDU;4*5-&Qfcbv`sWvr4{_`5L!Y)pDc< zuG*GtXJHS-7O3T7v|%#qJM+JUrSMlo*%f?Y)ECOqg^kFunDJ;1@90#0svhwR^0q&j zh2~C8HM9?GhMoLf$K$OxHSL-e)@8no3aAA!in9B+ru^Cu#MLCsO8b1c?3k%{dBa0U z8}DK_t43)41GSHZw(c0hP^L+Sb#N>=Z^@8%{l$tjwI!cMdM`;8rKFU2q%`Y#a5=zW zQ%yDODcvZu%!&LWp2dJPPQ^RD!;*nleCnHSZ?Ha@cmZu}1<=sCNyxf#h5(P8d+J2k z!}E+`NdC;jQQYq3g@Q*vtBIc4fGX)KiitJi{2c0SEW$`~%fr0->1-D*kp1t@Pe-*) zvX17}N|R^hu4MZ&K39)Hq;DBZt=euy1J_}g1@9*Fr<3LetTlNhiz+_F`K$0d@8&xH zPQq*dRQA4YZmivume6*yIPb@yc>1td`|mG7Z=^EUc4v+YDNhx}lTJnf@79CLz5VX* z0_T5)CRI4!9C!0L@V!@j*W9JCc2qPz*bPCAC~rO${BTup{w*SwQ|&Fbx;tPlC$%eXU&T-*;lzF5|3XME)4j~B z&>xS3DZyxF!Sw!5SOrQ=@pAI{Po2@&m(a|K#tN_6#P?p!@A^_3VsN&U?n`xQ%AfkC z!Cx~K`iBxXhGSwZ84Uj>t-=x)8uMIb8fo)nW$C90(`F=WB$j<~~<{{0_O2O}dH0a5UW)_5j) zcgF88EJ(rqXA0ZX!&Z#$q9jH1jQR>SILgemwMoWANyeOv3yL*ac@K};WE;b{j2EH9 z=!)Be!_j@IdF8fUW=;%@!bH8z>wG^LF-EEx-Ho4SdO%r+iwyNbA1 z#4JaOZ-*;~DJfMit*R%xv-~_KVzK?wrj+PK1X2s3`|4KP1h;7S6rIIueXf-JWT^Yg zYi<>nka?a!>UXsRiw=s?=vAhn;4P+c)TrY2>98bHa9@$)wrS~8S4@A$)$7C+w(==i zDmo*{Qm(DB*C{v(JZ&MO4&33-rx)qJXW*gRd^pfRd6wwK=^h70aajz1qsYYEZP6`M zyC_3ZZVloKQHgqL8eWVut{8TT-$GF{u1~iZ-mw}qU?-s7YPMKD6WL>J*=5eaX=9I) zg5kipVAL=P7@>Th#C{@hTa0$$yd_EBQbfhIMFvi1JuH%gA1<{uLWmpgJE*t5ifufx zMZ?9fJ1~wiy4=*alOZU7>JtWK$+=+GF5DJX<&RX!vgSU(xA*yIPL$Fie`$frA4Jp{!^kQUfoX=v5l)A^F|__muRYAoHd8M4x| z(F)6$CqKm97nv7Ud{$t8Gx)#G*sLqh{>z@srt-BF?^0;TW1QfSMVG#?JiCbflx09d zUAHAG7D;{(sN(CAa>CQBd{NUK9)XOxS)^`rq#!+^|t~90&qN^+JT+yu(GiiHG9jERRyrPAv z)MsX`st7rTw022D*;IR9+`id_9YJ3vH2^M`5~~u)!joIS+w;iv$lVKM`%-KuMlZ2` zS73OoZavVdSgiU`yrXdc%azDhR93g`d$OJL5&%hI$hu^^;4-<_cxviT+2fhK+8d%^ z)h@J=EV6mHtct!&Q}AUNagBiRh3M4x1yx>aa zr&G#7E~8!8n-l!~xMZ7o$>mk1{>nBZ{aXx@zR<^}-StC`nzM}wN15W&xOuOYNWW_~ zB#8mSsfRRtO`+wMzYXpGWN4nNCgH$R*hn~IX8xe29vo`}?f+u1>7_%-?D&w*7Yu7Q=6g zKvPwnn&@f#d#6}0NA^i_5QpkmBB4iFzrSiMciZ&4LA~vVj{5XPLbh{F{moXMb5re~ zK%rgBSYOajIFGUUV|72F?xtk3VDCL<-+$IMnK_kKyEZwk`qn&dI!b*=@-=7Ye?6lZ z79cA|H#0b`cz;QD-c~dIpv)rvvl1&>mmFn7+MSC}2(1wdaydhvte*|6f3Ve)a=%3~ zpY^KN=$7D_mFdEc%1<*DzHQNJpl$|EV>Ps}7P!YbHu;-5BjJ2;2o#=p-(SR+dC6B8 z$+x@La>O?V9OLKWXO>F#`0KW+G1z)yT~nWPTRbkYiaB5Uww=5K!PKFi8vn#{*J-@J ztZDw*&9Tf+tCD8Tfu$uleK{zyOcWkz(9rzde61m`kTNgG*4(F3l!=o2W%~0!RpKW5 zFE0huRxK)&aduxmRkFWNrM=NpX20I9G|4STZ7lz>-EJjKQGk`0jxqk_K8#r|6Z@>e)30rtb_K1zb~gNA#CDi6jFtP!HC_OkQcWazUY;87YHY&6)34KROKa z6EzflQh?O$-nPS956(gyYTI3z2|N({joXye{pn{QEhZx8-Aa*lokpUcPSp+U5b<7A-J_@ zad&HRx8PbJNN{&)ai@6k65I*yeDM95mAUr4<($dNOjb_z$sBAER7wIj@0h#)`(`qw z{j1YqDz&cG5|bpOZy66z*H;)$b{8T$1=C8*UJO%cEnQ}5*J~sgEHme4z~~%kFtZy};h4!jXLw2bV6ui7v;qumLPpo0Cgd%zNkQ?^>7CaO6;1 zr)2=1SPRMlR9~k$?QDWv)^u?s1=rX7{QsB~24|obh^T9CLJEu){Y#)p)6DwPH7 z>aYA-jFVgB$ufZlTSJ*;(nFz!MFxf$TzYWR06?r#pPQVck>^jd=FuMM!Yk>KR;DjCf)=k`kKg8#$ABanmO|F z{Xk>pmX38Lae0Au;mY`KhVb&8O=!15lQN{@G)r@j6>m7nMW)87#kjqU5xC5(dG4H@ zIp#NbmS@^EZBmf>m1+CnO|DIa>!5_zTb(iUUZzREdB=r0Zyl@EzXzzozr@ z8$1!}6dQI2hs>_q8uvOaFklKsUB35K`n1nx$-Kd^b5L!TCu#eKOJ|^HUZ>1{RP9ZE zhih17w@&%)djNjTcCgY7jD3y{qm*|&ChpVZA2tj5!Zl5}jE@j1nid^3>_DttM+sY! zbS42m6lqsbpS&SU!yBz{@{JY9+GX-B(Tj^CQE!mS+J>CHDpqZ^@6&U8Vm+1hp?fEp4tiwxeI;Eqr3uXfR&b0kNt1 zDN>c>_nJt+jry@CyV&WUB!v?E4dEg=5clz#t&BL;l%1=>W*XgoJ2{ACf$88*G8s~% z4{Ml8tZUs^;iAb9mj(P`CT;t$b1F;#yItwJAeteS0C(L~4;WnFM}vtpjh}@C|ifaXZxmAv_+v z?j73Z*CTxpI*Qa*^)yS2CJIwam3~--z-wks9pWT#;*t-I9&I0y8gVPB&45{H`F>*H?miq9{%<=z#8&RPc~G) z$B*he`O24gAMG@NSKaF!a&Xw>(Hrj~1)=*)o(kHthBH4`7b|M-Wp~>aaslytq7HwJ%89OWrnmgK}+gL#(bbpnPwbvfTjFf$M}lbK99rFdt$s=?VHIgYp= zb_~K&&}HuJDsBv%m<80*mV5;s6ssuPz@XA5=JoSgy_ZU8>_h2r6`T~%^b zAGz~uQdo5>N0*}Y0g&+3Yi8v$5XJX0KHYNj3OKB-E5Xu$CSV(2y|9xo*$5kc?*1jX z_;(Xi`NS8>mO>UaGr^l+k#G8bI6`M`6yz$;V!z`Q$kqyzKKmVN^C@XCDl3?;GSkR3 z{t~U%g(|BCWzqe@fKAZz)N`_cF@9+uHcEU==tP_Ls^21rr98@0-!RuN)WC_6#-JOe zlLu;YN{$LAyQw&M%je!@HYekPPajcH$Lu3M?t7bt*4rA*CN924&_4(+M1&iA87BZ~ z;PI~qyl${{!*)(LlM_@lz73!>KWuuwK|w?18F+;j!0Uz|L7@WH%7wVJf2qqiY#91O zJv!EdV=3-ThSyW|MRT;m*ev9qi$(-K;yq4xkHx|YN&509G@xa*WJ4UL=4csPvj{y5 z*($WaOR*7_39<{|;|ZtjO$TocQONa>v=+RU^9H^i^it&*F-buY$#q?}J84nu#vJDP zJ3es;^iQgtutUgjD(w}gUqnpJ|LnKRQS=R0IldgO3kz&MuFwTwAitYkETf>gr!6Oq zPRcPV^3Js~2OcFHq4kTRh`Rv00V*N3Qr;XoYex}ArvcTFCR0QAf{zA&H-TbR#52cR z3~-Z$!`4k9+P4EOxIHpG%a8)1vizpHpuv4k*afro;BKC=jud#Tf)kj3?mK{zLJ+gm zDNw=8JTNoZ{VlAO47M1Y)mUPl`n9DW*ssn|!z9;CvL37$Zj&S{Db+-ZpiuDfUCRH( zjleOp42Zh$Y+JWo?TyVN_n|vBPo}kP;yPjr_cl!X5Z~A2;?i-_hS${O|Al_rF2QO+ zWs8x@zUQHh7C@7d*7Mwee5#;)`a?TCeppJ$0%z?e8S5GyuZbEi>ZF}z0BRDeN;Sv0 z#n&F(&b^L7o39QbW635IZh-S5LiG2-z9Tb_r)Df@7u3mu~CTwbKM! zJPES~k=Il|?4PE=M0^)l}Vc*ta7Swq?7jBtO z{YcA4H$TmADkZ+ zBWzrf2O5}UN+X0PHWlpR8Q~cR+%Va}-yb38x2JUxgrwzSD za~-_AUF!e=IC}PO`vkwq?B$U4zD-*nH4Plz4)=b`WGsilLq46J=@Hfq;dk}-Ib=bHTQpT0@gusP!wN}xwG zISn>nhIXJ+_yB$;!{c~`ou2xX8>Gr)0%gNeJ>$c&AKHca4PKk!g0LwAEo@E-oi6j_ ztlLIT;C*LRVBPcMA3-ui%9~qL_M>Nd?gY+!7g4lUzT(U-M^X250_$TU&dQ^W8do=c z3VpbiFTnSIPKg1bvT1nQKXJi2iKPV?;cJq=_;)27^?5&C&HD7M>kDzXlH!8-!k(~i zxLBhTi51P3wU&8Agg&7$$d>*`!DEJ6Dp^1`;DC8b{mESdjTMDJQ(#T0R}*Ig1JrPF zJ>Qk^=EhUlCN$jfQi>EeNimowGx=Y+z&i|jCbC^+h&+W(g4x9{F)ov7EtgxJ4&|K~K;nBmIRJNXODETs`L1eXjtD?-CqY=* zkbWbwMBqS(Jd|x19ya-o{^7ieOlkcylUA;R2@zFiW@B#OkMMI2@;=MgX?c1w*c7Qw zzCGXTDq+W&&|AZ!wTss>OtwU+WVG4SOsOsc}kHf`(j70!gU* zhekmb@(Zsg`Dhz2dcKNDRHh8aN`HARC>%|?H1W%7iOLaqSby@?(hBwKG)Ay-Fd2<^ zU}idT{M3o8`2z#+AsF8T7~KZYU-y1Zy`D7dJVVaNtqlsDPMP75W& z&(6lTrtws;jDeyeyZ%l~s>>^56X!|6yVed>p|3`InqHl8M~1q{la@VyCzV(=|@_L*e@mxKuuY z(>8JG6Mb;Z$%kF2Gek%{Aq!nfIl(dQ%0#DPSOlSzz zYwkAb7I4!iRkXof#=+#&UAqnGLOhb+Oa2rnxQ3lzPtCYQ{O{AxjQ=qKaCwz>22s^_ z|ESPmlqcB0U7A%2=2ZvbYP;q+LW-tw`^C5S`Di49GW(E8ekNA;qz|S^Y@Nk0R!9p^ z-t8o<)?hJYmn~FC(qIy##U1qB;ztED+=UQQAmz#{r8x3YEJC8#rLElj9l+oK%pLVT z&OD51fnViEUxQ>n;MtOu;r^wT4^W*RZ8Ri5mHr<{_iG=shNz)KudkM zq7;Ynmt1au9@HozlfT7tEqx zQ0wcOaPF>t<`rOj_-1hq=fH;gFZnO?08!RAce=z*O1Fbw4b%g^iq9R`n+(|RX4eNj zyx|9zZY3)RlrMkqCIV+|@mVb{t#nnzev$j%jjaXh?;`;zw&N9f4zbS;tS0*=AACnY z5Ic)^yH0y^KWtR7%})2l4VhH41(272XeD;?+!hLO+@q5f7f!0m`ZTxQVSL`*-=fVv zcaV?W`!<~z2g`)PX%R!;LW*62LimZNzQPP+#y%WZlvOmSHn_Up29}`N;%GRx(D8%H z#7@bDwS8W`+SY}mPZR7vI`kuC70%TMOl6zow>2pMd4y| zZ-!1Yk;CPnPSG(s(f>q#d@+jpiY`Us?JaIX@yysbO^d9DVc)5-o`dUN&Jw|jQfVlMnw90w-y0<9*H+E%D}No(ZU z`-*uL?nzMx>ff#q3i}978xwgV#Chb877z4JTT2Uhlh{UZsepo3TVx;U@sX)~VvkNA zLbUutvj*0vqz`K}^-8~M>T9GQ0+^|#?jvgQ_ty;uq$pHyN=n?Ic;B+%R@9ajOY~4$ zMa3fOQ*?m8i2hvn($4PYt*?GZ{He94p;tx=k&{?ooa5ywqgN}i%R6ns&=9?yrJ&t+ zDZ=vTkjC$l{+oU~X_x}TAFPJ`TkXfYS+0%hb7YC)AC=;L92U~|+5OVq>{2Dre;R4p zQYMO31R$42E?P{6)X;mWjTUKksoDR9zlr9V825PFG}giv5@joDqOIbioHK8zq4alrzrY{k>(23>xqSI;Yj~I6ZIXk5zxbS~;C2qZf;`@3)HC^nyV|ahIO*%E>t`Bweu^E=~pVYlR9#f{>df} zMz=-qPZE))zZ#zSMgmfoHr`vn3>LdNS6FcLORkw!AXF1DW@3_P^m(-!<8%YbmDh6I z`mpg(lS19;$FY!36PWBfS!k@Ae(f_TtS_pX&-_LG>+4I!0>Ks9<$Ggr`ig15+wG1i zJ2S|wn#}EB-ZWh&xM@iM(+vCK;><>#Dx z`PI#gwkIq`p3lD^;5Yps!#4P7Z9`FUa*#F}Jd~&N25re*K6t@cb<#=zyt{Zh-D`VljFb-wT_6h2`onG(Dcx3a zMj6I;UUgcqb-B_qgP5^l0;Y4};66A0R8DS>c9@P=icj`b3tX~uuP`G*ot1Pq`Bgkg za@yxtyi6rAYSu+L=UW0D=9(Ik!poZ*cO>{uujsIG??qar&>)G*Liyo^9p5zAu69ob z;xV*;h1wxa(X&eCPS9)2KoK8Szo1v3z9A$X@_~Q1M^QZa;b#@$8DeuFgDy28fLbAN zN({cVDrA(g^kBki;TzN{E-8uB^s<;J9it$xMIEGPQqdeH72f@l%>FiUnT1!UTLO%j zA*md2o@gpm-N@1BJ^9qK;qM?xUVQvk%m^ka#*B$($%;vkDlad-NVy@P8w62HF=9)B zCF~*ldyg3V4lcd%KgQ0x43~z9DKD5gLza1vY|(bA>K&Qud%J@*jWx zQ&6sk7*MXKf z7?wFWLt!9~>V^lNGRtuaGfWqeRLmj*i3*b!M4!3hG`gV>&|7+*TrdN_1k@e|K;$)V|^kuBa1}gbxz?# zwNb7BWWcaUUn-HV+*I(` zaphJIk**{Y4z|oP?96m|Fpi!qKi=dvHMv86kkPo&x7hm^7SrFr9I+z#CwhX}72N!p zBSd$)hmtmDldBq^+ikFx+(<(*{Hj`!|wP_}sR$BOKb zKR?hYLsSdBP^4&|{|jhA8mh;@1gDx+4EQW+{k}zqL-!}EgJV@{30l%jtm(IlZ%Jbs zI}{qxE^#*Q9y(&fsZb*4T9;Mnicwy+K^)5Z#rF$1=)7fk?{j+SoBCLFCexFOYfXOp zmH`K0OBAS*QJ`(~(N}X1!?Uv#U!htzk>i$3ylFLoZd$W7dxI zZ(_U98ac@bbFrogT(U1!5otnQR6*+$Pyd4`L_sgQoEZUQDRdg(0a3Ap6Z)O+bfi7k z4BBMwxlLTV1*y}1!QkkPo{(J&!HWkJA<_TIjK3}u3Y{&jVJNBI-^-skR@navOY%W(?teF&CFiKV$M`1nR$nNfT7vh`75_;NZJBSNaEv{58LfXDao>laI7 zOBdUM%&a&ZRTCyGJmw#?=`-bgYM3l0S?iM;I9CU8T*OKqhAwMDxA<=ia?gfwCCuQq zTN8=pXPbR&p6;J{{;~(U=9fde_yI+6*L?M;Gqq0m<=XWaj8Mg6!x+0w&CgX+@a_}o8I zjf`5q`*>?EsiNGrpykjWPDx6d^Z!5K z;ebto(#VW>Wwnii=wuW7-f2Xs)_?S6l~zEn|CUR@dY+f`r*U#ovug>II_Rwl7$|7K zv9z65Bdw=nFtE*Uu;qDkOOy`6%mb-$)Mr)I;l>{Zm1L>bt1r5d_zCk`Gf)p|Xf9e_ zA4Yu_vSJ`;#9hNXQWGwwL5nEHQ&$(hR(}gAfC}Y>g)RW)h2dzyfz!&=#n8BfIk+3n zq_R~*2osdOq9T{cy7-KZDSsz+K}OmQJAQ+r)F7xNvh-sz8+CEyyFF?{HGMU@P{Tv| z3bilN1p>*S|HP6+tdqKEt@zbIV;u`hwg891J~&En`m}%}qjFY1{7m3e*whz<^nWru z(*kNF3|t5v>IkA@M@dSCb394NdXSj%d2cgTUM#ho6_%?sHc5S`P+n8$g4B-pJ3z4Y zG)32(l(ttfaWM6?q1A)}s~22`J$_aJruIkXaFa<|m%aXwPS*>5hFh!N3b%o6QtAC(HOzo-9PVazT7e+Cw) zbi?r!A-J^j?;qn()K*ne@7{9!A<;k|(XI!eHQrJi3u1Xbn; z%&Tl%;&%L(=lQ)#wOEz>Cl^{Ce7`u~_1u_zLh&oHD>;U%=tXPx&f;g2{C6p!-?*+c zpZ+!({-eE(85(?}{KqHzBzehK(R`-A&kCh#B2D+^qK&|l9>cWE5NexSe8m;Xoo ziV(Lc=vTo`4w5&eK9I&R0dvdTuTEa5;P^3b7Sf$=Z~QQa6@74!ujWLVfz&A`CRg&U z5A9&s+?={siw!m>7QxOfwCS3;q>V9w! z`O1(Jzi`*-KP{WkAh$wl!L{qz?&71%A3HQ>$n+aK$8(A@c0ueX(~%H=k{1@6P3Db3 zPB_k!X#?jWu-i%z6J+kSyWk!l6W}IE^MLkE$Czh7CGCM}{yRGDock%Tl30UIL$-Nm z`}Uef!^!22)koZx1eAYI{-dDYBzITEWAar};b;_=QuQCqQ>{hw_SQtPC4SbK){$$~ zFNas_A zz|)bSQpCactn%ut_d|xq?Mnr+v{Qx^mza$_Be#x6QED2I;jdi;L$rD9WrMZ916%TH z#xk>#P*<~sbGL9qZr#>C08m+%=9v}*F?1(}ffnWAVwv3jYh&<#!8G-f2L`K68Ri2^ zS-iA!Jv0C`7r0JBO-cb}?b~tmm5jyulKLM#QVV1O9md-+mE2E_La4C563Kx+R61R=V&)lNpz`bi0Z|+!>de^lUYy@v=e-jj7twUBQ|2B`YsO zUeV5ULscdAvO+?QsmRh@x|#DyD=%WU?Q=9Qx2=hy`;D`T2vE+Ix@4247Fk5wJf%>7K zW!9mN$h~aMy+$Sse&*S{TPp8J>TGoq{(+b-b}{&u2~8PcuPZ%#lPd=Nhv3%4kUz-` z_)=T$MlDY0pRC)|EiFtH8fY@agQHIP4n9=AJ6X}}ZM+*|&1Fq!6QY=1ivX-Kp$ttp zIrKAz0{WShh8#hPT}_i^9~jGrWI!2CNs|@&MJ|l|jMj-6NdmQ0OsHO|Tv(J{N(sbT zfV_05RR3s`jG9B~$hbxwiidg{Ia+(}HwCk?#I(1A)r%_({Ue8D+!iq-C7Y?Dy_|Ae zt5B0Ix0tj=!G_l>10P}ph1dVe4pu|y`jz9!1T;?fcm_GOoNQE`MxbP*3HR~}F&GLl z(eg2yj|}}{_nm@n!bfqF@{%nK{0XO8jT&h(b-}3(vTPLnSu73ij?mUpB|o;iiWJK^ zyGF*Ajb%-<1~UDP;PRo&qZX4(4{+LD*YXfo+ClaCU(p%iL><&&kaxvW;XxkLKBcP7 z4bgo5tw%12*T1Yv3Ptic=l;lzTh&B#Q&d8x5T|M`y=Li24|q+EQ(BE`v%a8YGni`! zr$D_qgt%IMc1a~1K_PfAB=}Ftv2p}nvmD=qb#=zka`Hb`6(3m3LV!EgP%aL1ixHzw zFM^;?DwjXWmp1UjVE*1O%H-GnrbTTFu5yOU;OA!iZb?J#KDkX2V>rt(RFXd&XC z_?eNKbox)<~@ z(|?|zZY$CCXOkwEal__~hs^TpD0RVe(bEkY4HY;3Gs~@q9WJFccLcJv?V8bi$IwVA zwMRhOL%Qi@XMvlafz=uR#w#CntFy+9yT&BkB=~R{GqNm->534oixjU|FBlxT<%ZFBwz3j|d z!jX+k6GchB1mfiftxEIkuydCALY8-fqs6q`ccB{~p)VQ__g;flukAIJv)KhpK?M%K z?!Ao5oEOL=G)CE;mvL{+@r#wS_<#j>EX^heD)xEoDp{-##(4L#sI@TdEAM{^r`T&? z9_iF=Sjr^SY?yC;PSXR4ikGvr%A|>RiWL(j#s%??RjyLQb(QHB6Y(dXJJOUxQJA%O z9j5wjqLuK%WJp#z7=kbHCAF}I@7`Qdg4D*&!RQf&|RC^eL~Q%XhD}*M-Bw8P3>8ISbCU#I-Upr zijwlPA)2=8nx#&sG4v#cnsy5%WVN7-SlglMPi7fT`tmvj<8!6l%OJt*y>%^OX{9M9 zPNRXD8K2rrVYRGY(XB{0JaS}i3q8Hf78G?^#c|r@ znOlXA6xyI(P!7x_?yPdN#>Io6X;PqN+gM{}(T3yA=?|Q9Pj^E&V(0)%gW# z;&P$%nzA6xh3jG>8}H7y(#dvT`-hb2WYEs%0?s;CZjr(WK7w3fEf=t1E|*ul zUoOoiYB3T8oPXa7rhwGuhE7-iq1p4ym7Cd@94~GRX(+i0Zh+@tik3c7ooC)c|Df;X zNV1pM2N_Z)(IidUO~z67^uw6oT9~TzCp_8lXb1q@sR5iuHb-p(ZFbnef7c^d-L4P6iReBqt>;82g7B_yH{~+B#8$X6U&`m1F@b|DX}M3m|@z*_pB$aj^@3 zd2igNjFPJd|H}HybCE*dOWSOVLDe*p5n9u1i!@1@e8{AHNP;|0{J;Ug>w^*x(U3MH zC`7R!Or8XCe|)E#v7_EOes%Y7%*$_&}C3k z@1q;y6%A#-4}0Q^^c#hnJ_U#m8+ExZSs<@*MZ>i=&$iv%o3f1*ej2%u1d(<{-#6A~ z?XA&wB-2OhbKpx8I)P2-)`QD3T{Txa!qP2c2s7?G zg&Q|jZtK0T0~bjBlFq*$ZK|l_n6KRf8)^J|Nl+5(_Zt&fXO*$H4fVXv1?aJ~#uDaw~ z5p38&Bo?E)ayLs+m}gV>U7nui=aRF*es|wfT;dNZ{7{&HMR~t&VjyQ|ltR1sD=xR= zu0G9jo3(d<=bJ|pT7BGDT`8N=a?OrZP*y%zN}Vg+a>+f1(U3HZAqhKZ=)}MQW;ll^3L(R zPw1k!?|@v1zxwgtZ*9#l)M@_sk)JHBW0qezCy; zRRI5oePqg0RQK|qf%hKjU)dX3l?pTfkNd$KjiKXBQiLrJ!Z*=GmyZGsf{ulXof~-9 zPjk&<8o%7uZxj;SzZ5)Ov+dt2Qy2_b7NZNZx?mSYv+dPQ^=7fP1Wie$xZLhd5?WE; zlR&X5QKY&rBU%OKyDB$T*Io~8d3b{IaYxE)5sf#a!HqYLr+lps7kuqc9vM&TO$lv{ zWd7}q)8g%orPu8mZdN-?V;3zYQ}SBQcGGhLtQYn76qV~&+Ut2&v#g%>b4DKatWWFh z_)XXCYjeG=?$$Kr3-a9 zeLDK+cjqr0^i&^^-qv{DxuQ4g?`6My(|++!>Ms=QE4x@w`_0Fdk7U^3p>Pu)&izMaLD7gDHy-gr=zX1vVDmXq(RT{>i7CRC zlXK6f$8!`s^-GWIK;PSgPyU+O>oD|0J6kKOblpsC7v4ryHupj(HxszZzgf~}$!nU- zF7?0FvvxbZ0jwlL;Je%Hlt>uVeapsbA^*T)oR6ft@O)k_SIG2k2%<7aHeUed4H5suuP}-h99J&U zgkN6`EPDJ+tB-FC1dLTQdJ0EDL}DAe7fOt={qCzetVDp9t3N4xB`;U=R)}a9ho?IP z?}#qTXOM<%;^kS&i-T7x85Iik3Ch0|4J^nzv+F8d=N6pq8oeZCJS#z!JHP!?3^_N+ z2ICZd2YYDL9mp>S;Bf7u2=udT;JS1jLF*E|he2A=w4xP-?5JoS&KMc_H)V~*x2nefF3@?ArzhT#uKOWz-$ z)@LcuvzsrYK$Hz9J@%q;tz`$40xZSU6dHqJ zd*;#x+sZk$@hPz&0;1BvCEnH>+rUDX`4+)jZa`K_ML(4C%=d;2g= zaweRv0-eqLi*}2SQ~LL{#8ntBn_s)RHiIu$>l;9}NL2dN)_Fo%twC>j!OBxJHCYHR z4U^YnZ&BttVa(i{4pYiY#aNa3{^(SS&LJ@`h@)1A&${9LIj3igqcET78S!0e*5aZM z&0?!;k}mz#UE!%Q@fYuV#Zu>1)6V>o!ELcZ%a6kr+!ZrYtHTy&(nbADo!w$}a!+Y8 z^OWITQN{AIkPM?i(#t>trpnO{$-AMAwqfK*zDlDBWxP)xjd_Z=W7>5FFW=?&IbDw; zk5ir4zq~$4AC_bOUEZ=GH@Kfw$uuOcK2N^LzQ%(==9SWcNdJh{mdZyxMBuTFpYn>z z&Y@IP=-mt05Oy4w`KZFbVSzxwodN>3MA7i(FbkK@?&^4YSkUjbZp@{#N}UJn*V zX(NpS_6!6u%6KVP^Cd_Sv;UJmx=h9jyATs^aWEwHofh;e*4$1^61 z(O(wNLfVWwZ=Wkqmsl+;s@PrD#aY$`)gPjAvMJaRwLV=xzo#P_EW8<6yLerHm$)c2bYB>{hkU@or#LN--)7e>U-C|18#-qZ2eyqJVT4{^ ziVG_;GyYmy^u5i#X*0T5`hL0Ixabw#CP%?P1jSyC!{!|d6Ec%_w~)p{3R8@wVEgF& zPD%2a|DBS<{e0QWeVEkRqjua@oWHY&j`P1eZ3ouoggf`cR_Q6?oUek*@H@ZQbGt5f z$7i;pR}A^%-`VC=UQ!s_KdGk}T00z#v(c`Ouk>0d`((EpCGyiSrD9_Ygi&m4dt|U5 zBR73JGPkroRlXl*dTytmeO3dfiyYcEI!Kv`-fRZR{H!W7#WZK z?`?}1hVMwToe0h%1ro8%#>2e+YVoq(uxOf{WNbcc8|74u=XfD5ng3d=aC?b+xAUrZ zCO1^R<;UMNT~y-@-mG61!iBL`_Qw)o42B(5=Nty3Lixs4&o%*`7&Dz0=>yi~D_i=* zz6f$GtR=P;f6aYjM4+7JuZM(CfhWeU=PigAvMv6qx1Axz!DF~k5x>FL-iv00;$8}K zLWs*l)?YG4M1Uz1@}sP8V{2YDxI2I)ci#!XkfE-MI^BhfYg%^h(7x7mzE?I$g^@bV zh4DqAVV>WNJzV@8buZJfJo-`9eq#T;$SZIqmwz|w)}%e->R@W|{8PID<*fI)+U)wR z@I(INibzpx2Jkj0Ezh5aeK%)B@gXv;{E6c%%AO$o4-1#h2o7|#eR9KrIU`YTG|szx zaC4T&b6KeHyLUU_h>wSEnN*3mF%7V9$GN#QKH_?`T^Mp#06#q9bX|TZ?9l4vIjP{o zXy4nCtxUGun&B;Pds`SiH&Az#ksjmcJT-4`)^cmBg`=@`rX5td6@Xnhl~Ey&i!|h;$eBLJk_crK5BnRHbinT^b1ROqHatZRcGU5VwBLB#%pQXy z@6FRaf0NZxX@B7gm>aSFZG7fc)x(03oWUR1I1=$C*i+nNwG3@Gwf?K&nVR;mGA#6r z=&zi$f-II|@7cNax=**JPC&JyWyWGyrD<7fuIBE4XAUszx$e4Fwmy5*3gWF8V}`yS zbzjTc`X%{8M0ttH&*ApEMke(bTb*!f-*(nh+b6g5Lwv1oh+}nnGDq1Pgc@9PF7jm&{V`) zF4@&tE}e32nk(qHi#|`VD?jJ5%Q|ppL=TR)Gnn>w$sJ41i%Z<(29{`DVw zD3n3u_nbaZX4@I!RdhmI1l)YHOWsAvZaRQ5<^rJhmhb>siGUYwMh72S&x$xZg>9qE z$^W+uyAki!GBbHw5pjR?0e`OMqV-I`?eVBC**4!_OLiCiDGmK;cSFV9z$Z|svvKss zcL;BaMV9bSVMw(~vIwg+hKRjuNAhGL?XgqWNzoZ@358h5gvoYp${=yo60ras@u?vq zHIZHqyTgE~%UvFMqYMyRrR&{pEx7i5DBT5zvr=@@L;j7z9ybUAO&V|kRG!M$l9_3#QU;>^=<9p9$X#v z$Q%n+QWfKN5x*bx`XXudJ|e;OlK0fsy&g>a_(+~v=Jgcq%7~&`%?Km&a*)0z*EB?|n%I(ejxW$Zey_J}i>NnfORx8B3#pfCORhJ$|72ZsO*1=&6q;p2 zdd*fK-_F`0WoM6&^s^a=(K1m)td4hIPSs^ZW7TC$W6foFWAzG||Ih;eV^qCSTUNb) zTi`|VwezgrI=i-gqGXkq> zg_P2ioQ%@Ao&7QW;_;ev78TK5CWWxqp^WM@At#~y>dPoLbn9?0x=(|w=F0+)A-s}i}TvFT|Y;U1Q z0~?$B(7)Ws9OHFFt?~N0v+;O>Qx8KN7%?6{iNFOCBe#Lg2&lX#f<5lEmjQ~_Qxu2R zYebtwFZSHQ=9&kr(_=)Pq~Rk3w?VK&huFjdA(+TC7&TH$zM#hL2qJ^ZC zcf}Q#C~3V0g89Rr5Yu{l5G=!J0j@hqY2r8dpbxkj6N)d{vy+Z7>wJ?PIJ-bp#z5Yurpi)cnE@_ zy~EISYQrDXkqCUC5`JgEG!K#)*iP$*58p*3$+xo@rcHl9GQrFvwCFA(Jb1yiJ(cvl zdiNw?=blPBUafl}@L5kOG@acraQYQ89Yzo_N1gP^Fk`wD;VR!wZ zv|L}nxOz$BbgBJb!wV1?KvrCPX{%-r6R>^Qs{Ec3k{-5#V-MQ1MViA_ul=^$N$|%C~XDT@eQI z<%Gu|t3VHaFllcqbb;0n7e0y*mG@;?flf0Z+U0GrkCm)SJt)D8J@)h$a1SGJPWTng z^XHT}oE*^@cdLJ|2{sB_m9;9m{vX&V0*kg@;oc40+Ivh@FL$pD_U=8VzDT-WhFylQ zN?CPYQ-M)?j_F)geK_D!h^zQp-FrXqYMA)vy{hYv;ME9m`8{D|E-WTYTzaqgx*OI( z=pU5$$50dma;T)Bp=;BMh2s1x{R#`Zwh{nHba(|Mau`KZ(f!e)0lvSOVFU;G@}?&` z7>NQwzbdn-={B@XR4tXKd)I=w;?{}@Hn&|Pb-1K&T*)WU+FtX^5Jb$Z3r zofs+6vfgWrLkS*BfzNyoWwOhkH|>Rgdgkm2&m8`uCwb9_zi@^Z{-P&-;q)&&_?eGV zKl553`qK>AONf*gMDvW!UeK#&R3G<@%;KIMwO)|*i#PVOx2~e@b*14;)U+4${sn=a zQF8pVbn_YQVLW?Z{C}dz=Me2m&&HLP>X_x9K#?lv~)l1%x zXI@VCj3{456Z^u2UU>F1w|$Y4zSKkif?fvh`%;hCOMJZ-spbohe&H`giMiFO#wUG8w2Z{3T`9OG@zf56`swa&nJh^S9$;YvnBLd zCiF4~MK6Tq<&60`*qH3Iu=WL!y`bkez#QP&jU4J%H;TjHDi#Lavio1>yQ4QJ3jf&t z*ZIGr|D8;6Ki&+|k)5#t``c6yoX3eiBkc08xH>6-1NTm})MusfQ6ucKl3bkxz`=Vk zE&UmDeC7zdbO2W;CU8hvH_u6VG9k%1KDjD0qYkzy0;6NwrepOKq_liLasyN_X8Wv5 z?JO~wkcJ+g{6{~v4z@)NqvIo_V{L9GvV1?*3si7olhDm`22LhqDf}O2ZygX-810MF zjdV+QBi-E~rF3_9Nq2XLbO?%cHwZ{dNrxaI4Fb~jzPaa|`|i2#{c+#>WF{r^uR(58*&b&Zt*EiA-i5pFEOrq)Ju7m@YulLVT?M7c8gV^I51HB2l3EJZ zH}{IOsin14tP&mAwhFTu3QFlUl6nmunzPuSs!}q!19)s>el8|w3J0*-BF`MoF^>qcM z!=9L?!Bes_TJIYg*8WZcTZLcU3QGS@rSuwZew5Mr-_WuibduY~Y&`VMxymMACnU zM<_zYR1mQI2|@DP0<*smFruMoRJZb+(VL}!O`-!*8o*XS5=xM8gQOlJ1&L@PkbVHt zsZT7S17O{OcZwDONdWK%04f03X~K{SB!FKN0JSL+aK_TW{|5LMK{0Dk-B=O;i~z6% z1+&s5;DW+{|075?sK5#E0>&E@ss-oTlM0wl2ty4RQNUn87$ED(1q?B0VGpVX$%B5> zAUT4t^neWktSkUN0>BxvN00$jAqhBvo(#b90=5uL^AZmdFOUcUHVAC?BPt|;R8;_T z5&+*i7~m#Z0&YVIu*`sc0MawiCNLOyB>)fw@xBEt2_%7dAyOfb$N`2N?9sCbkwl$) zd_2XHfQl^U@uN66drGfgN(Fg&dqA2O`c; zlRa<90RwVSg&bHQg~1cuCWU}y0QP$kOp8t=?UDr`IRvaVFee!p9s^8O0ccf_7l0@G zgBF->8BAvl0l+ix0X$E_GGKx2QUNvG!RAasaz3yH!T`900HC6<7yw`ZfC>P1;OgQ4 z02Kh@AON^_y7&Qr4FDww0BUqWu1Zb-u!HB(C==}3E=b0~IPBm;?1BMoEC3)+c_1xF zs=!>@;8_O0Cr4EKpw!_1J)R_!GeI92h1Yy8KnU!D*&1y0C<*-zy^E} z0l*aij3I5HftLh$`M@RmD;luofaL=tI)f{+3vxwTfgwJEE7BQUz+I3*v;d$F0l?;V zu>!yg0IL8v13yh&)Bu2Nl@J7g)L;gH6975^&>I6fV1N!Hp#Kdx8Sq=vR3FG8Eg^C+ ze-}BBKLa`Bq6~z1SHW}`ke~5nutg_;IfLv7JzzQkvjMrn$UuQGNCFD@LpC0AF>Zj< zdxQpz6iCFtPlG??=4qOvaRe#s_pG1ckIT0O%e>TL`r9paMH+ zARGn~Z6FCs0D%kyr2}m}ftCxP#*l6rAjty8o;)Do17{GF27*>d@DT*wATWlsRRp>X z$T^8X0DGAUPrh<(i0>LW81ZlYk=paa-0^Gfj?Ir+pCxnKavJnJ@bRq}n13<+fog@H? z0XjYN0CXcc4k<7jbaIBYOauWnsQEvhSU)%QP%&9H#lOR&GRV(}kW2z(^pH#rlF5Kf zEF}*6(H3McIR@#QL6iivx5f|wA4DJp1ifiMK)zD~_d?&!&|gRk1nxU1FU?Xle~p>l zB3|^;m>(A-4Sx4}jp9AqCxPG^1V_Ojcm)Z#fkrSNWRsG>VNI(I{!EktEXxq`HK^n% z4e*K)kY%qSJ7)4@-IzGq_64XY!k$VqqPI;c+4kkrltbUZgCtFz*o8I&(Ut2x!Ww}( zkxNAh=cbC3WnTbt2DG;z6YS}DC0ZMo8j*`NAbLW_f`w24!3&7jz|juKOF-_Ga&EGT zTlN`7j|G1W84G@30s?zL#sQK5kUfcjh#3p+gad9yKym{2E)b;tP#R)YCv>4L&Wc$r zV%zMA=m>t`J%l+GszhyLRwHoHE&*U10LcLO06=n}s*yD8Gfe0R?nLl}_GSe{6oe=S zgcKl>009jMYCzDiEeA>^K}tc? zXh6*m2n;~b140!L&(VND0R%lL@e>eYV9aMQ<^yCzX8>3OWssa<-xFj!RiJ7F;GP=6 z-`8{S!eFl|)CvBg9l{iV9ohr4-BqG~fIfsV0(G+@!2&0i1w!A*0*kuB!dG>o1qXGRg=2M*h1cqM z3k)5(Ja`$F38l=sj2($Q*6-7JwBP6Rc+VyCsLqA*IM2oN7|$i}1kI&2i_e8Oo6LnY ztIee}yUb-Z%g+@y+jt1s*YC3}Y|UUUVEZ4|XT+14vJO=+>)VPdEswca$DDAAYA=sn zd>-=>D2#R3y;U}4o}wqyhZiMZX1X{S^MWt*bkM#~81lj^M0LoxoinMLu!hse6D@Mk zyyI`s-2NGMVkfHSu=rQPtTQtSe%14FxlwZuqo5jrT&E?M(VAAjI{lJN-%&Kz!Bb;W zu%(018dkqM{gP7uDg6?9^@`D&M?WmRG3NxqFDsJK8c)A8y)mNNb!>Ue%Y00Cd||LB z`V*ryqCVGmUj57T#)N8_vE``+&fPpK`wX|l`eqfzsOq<40)q>@yT?}cS#F7S&8h-^ z%YtQjh0B7U@?ae_1Wk|fG;*tz#_nRO?dLL@^~df)s{Itp>(>W!3aSz1KC~1tW@J=r zD_WW#gmKfxK;d*cGmn}uUN}UI-FCR9Vqx$$}(q${jofn7v`?9h>zcu?g zJ^#m;q)jwB(bUF_TZT!Etx^H~7_m`Z#mCR-?gsv7*3{<}?NjWcnT=t$TzMjc!>-@4 z$)|{9A9W|0ws82XqQ!=Zr2VO^G0yh{%%fGeSSX)itP#&g^Y8{MT>fANr8I`#zMq8I z`pHin-8&c%cz&?9ND!3saIwYJhQThH_HepY>q4&YkAHJMWbICF;Q#7oW@yjNSl^%Q zW^`!JJrdKnFTn9UB9CW~tBsxYndzo{$auS_@s{dTD31M)XyfhCnl=8^4hXPMFJswv zMQd+4Pmf~RhqFR^%h$5-g3=x;x13$cH6I;MKep&>?a149w2kgGuJ0-JWvNa2FGpK2 zqwET0(k;%)G{DWOjWs(^Ez7t?%lna(@942e5~G)`31!JXFeIhP)%?L5rB5|33zq%K z5M(4@gOATxGQ&2;SDj7&qr@RAdUl4vDn(Y^D;=K^XH@%)vt5r@QNf%+dpstc;4_8B zn^g8ccy3o?@;z6jt=Z|EFqt_^AG12U3v`OOD7-nghDBk88IO6YSTr1bKL|@38(`oC+ zG~wG)&uW$B%loCgGSMBD*Gp4rDCHt%d(v*i+fCC_@>-*rl`6ANH_l~lU(qj18u>vL zQ2K3FadiG!9m}?4=dhZpH3kinh`~+h)+frK``r#(zmrF+LARVm@<=yTn9M0oyYUWm z3S|nnKgQU6F%?FPFv~@BT{a0>f)zbegcc&nK*_9)TgmwJ7E)4sKUT22UzlHfXd-^7 z`#T)N#7u;T@6veF6DH;k@5vm|c)v>w%V`(+^nZujcb|@#-h+Ptzwfy_U1_pmv5&hS zH~o3qb~>x*XQ`r%Oi9&rv~L`u#ioG^;vweYLuIy`#E&y{hX^>*fU-R(xgpfrBqCUOyrLm*;!X?>|N|LD_ zOuQVWe-8Tx_ihq-%?taU`XBDUipsd9gUa%ysaXYOf!@te#>Kr7Q{q8U&(KdezWBZ> z?*-KD-$bC3dg5~}xfGMlMIlUc9I|HOdlD#R8QCgY_P^CFKN=Ist<2K7u1wN-wT3J4 zZkoSCA-*n!t3#JXdrpdl?|L`nPsE?FKT-I>_~H1ei7twG-(|6EX5PhHJt%|S8h7GNlmWnhO0J($1IYFxKrKS;wDT`pYUiwl|EGLay4PDK<>tP$aU7zo=9+xW;TG zw)Xo1F+%OFo)Fiy*z1jC4;{8bN5g7KeYi|n{{atIN%V+a4Ki#f9ER`e7l`fRuwu?Vr9 zgdz}0A1e7C;as>pm;xS-kwTs{Bvky+ugqHoXH5oEEU{ZmT-7B?;*rLiXi$RE6XiVr z5OX|qK6x_z8t=i;jjuUZkN6C=d; zE8U5eO@8@6AP5AWKyVZb1n58@#B}oOEg(ez>02^;dL;?S1VAEYc%4}RXAdBs0C^9b z>!FIB-ps(^2tZf>A^^uaTZY%gsrcvh&UcC@JL?g8o!)$QC%;50oBhJn>~BtGs@F;4 z0Z0zOCjep;Eu5lDRImGCHhVcU08k17mM)x%go2Nfg8=3LNCE)56tE@&5DtJyb1U5` zfG7nuWg+SqK;Qy`5D*pW_BY>402rp%$@dD>6$V5jP~!k~0Vwk^8mJ*44NwyRkPei1 z3z-57sF?u44Tvm2d;&x_n3oS05OjcG24xxnVFidE!0|B-kQjgz1tbh`MF0U2ShZk) z?nu4PRwO{a0pt+?bHGvp?o2?kfUwb#su3{YjNWbwG1zXUefB#&kp1h6?oo z-Wm=YX}GI5Agu+)9$*w>Y$2DAJwXJfVqo$FCOQDA#GY&b)frHwlr05vgYt#At4Xv& z*V_v9Pf-8@r3*o6F`#+}pb7v9GPb@4suWhhd^;~sG%Zvr`JwP zYEQEt8D1N0-Z4MjiP|xb|3iK8Mz%50KYr#bJ-InI$;dF_4_OEl1HSaffsQ9MwPWsLMQ}%$xBM31 z*U3QN4WK{F5PcHRAJYRVBBX=_fZU@YB_PrsN`OZ}O0+`sLJ%?>kgsV0z5$uX9D?(M z(sv;M&x5q2hv<_aI4%T-%%2CDUksu*hBT?&S+s8)9QB1R4-Am_9MoMj=S;zH<&2qr zj}BiRNDWf7^m{&-@<1FAqNLyd2-fHoP&;geQhdr3|Ga_1aHS7h9!LU6)b#rkK#Hgx zdIK^MkjM;ILVzSFnmYr|bwCQK9d5v--;02dUhVJ(M)Bzv&>@0`2#QZVAjDv}dVno| zW)=S�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

  • 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

    xC$IMEK0+|Y{g-c|t6phKjf8)Mo?V6Y^FPBK zJ>3hsN-aXOuo~XkIiWCC!I!3f@n)m2_TCOUAn1GXn%&dVZwUS+aRVQ`4YYaF1v$DJ zh2*eGhT^!;rGhT3662)tP0I?xHSXqEzaya+ClA#P9aZyld?Ed)91;RZnp3%MJ$qzINa5{O;VyqyZ53wo}0Un}(8f$kP5^x;Fc_tNucUjn*xbBArs$`p(R~pYOYl_bMOX< z!h+RjO**zAC`>i5OkSTZ>1i&sK#sYFeR!vXsr>h@!^6zU*pJB&msq!GL-&R)ie1<+ z!}|c*YF={4^y8|basB!#@3zEDz>!w^0{=Tpi6lX0jtE-nN&0VI8L4@C_M+gz?XsZX z#hW5i!J6)_t0V0c*YpV1gooMFK_itdx5%+xlx;QF@t&U_q9;;DLcR5qNPx;@3+YU< z&rK&3*2FZ^_G2h=+58D@{?|UA3mWE??1`wnjjANFA{B z@V&9aoNny`w`uaivBO5nFTn$A@KH?FQE3K z(ny=U??ODo%IK#B-#J-6WuVI>PH>9ctRIk)P;3TnG4k@4KXQux()|PPsWM+pc8Tsj z&bm-fK3ZMRgAa_gu?GY#XXz=n8l%)(lz0#GU)MpB&+OqYenRQD0Zk?Sc|7Si-Gr~k zV{B-FwfoR=>9>2V#`(gOnYr*yy5%q9EOB2DBVdL&kp1h52Ug9xyi=PGQ6^q84H#+a zSX2VaA20!75nK}WpZK5AthA>^cVKjD$)T$Evtq=Z$^}j9+u#oF+HL~ZFm@yD&mp`f?JB%-rA`O)xx+KH~e?1k?sJhOxV`8&nr)ef91Y zjclUm0jaW}zyOXLNSU$G_lwXHX@YW5T>A5(c;?ZHuOzGUZH%}RdT;H!Pgkn=EauOy zM`-Oh6n-C1#j5%9i7r(`!Nx=Oc6G?Nd9~!_`a$s3)Q}w3l^LO#x}fnXr7n) zhyzEEHhk4$*s``^*1zM#dzxroq6P0W)Gl{>)(F}Q_TTBEWApiNU<+&0q&QHUo=&ma8G{p<}VW_DY`Riw!VT@UKFzR zFuN?0MD#$3gO6P+SBhj%4j)0hU!XQ1z%3Gen`eh4%v%s=SojCV>0h=BiI zk#DUNS=7%KQ7@$Eg*G~F$!fER!0M|;*dB6LY_p)@ZidH0p05B`q|$t*`fo;Boa#p6>F)Bw5&*y z%SIYh<$#S=)C*;Q8-(QYqpi4Y)j?WyCvq#TDA>xi&@`N#a_I_Zc#+4cII(c~gX<|w z6Om4adzN||W6P}RxHI3?FE+7i7 zmw|dpPujN<{V=lH!ec^dUz$srLU3z?Bg*)!+T<2h(#l*bA+V9iMbm zmEGu$r3F)x?+mkt$Zok9lO~N{FxaI0t(o57V4Ss!mLs9>XFVpsCH7W zJ!6+2uUJQ=2w=wXmv!M!C56#)N#c)^et^r{s8sf=GYKLrG}eBGIYxE)qt4H;E{K@- zkO_@CJZ;>rSbo}gM=GI5diPyIZ}rG4rd3#_4q~d@BVK(N;)V2NIbHs)?W>@l8_E

    %`;5ev56%d0Ao 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

    SSnSH`nF@M{es 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ðgPh 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%0DBBC`0lVFFK`&(ydvZ 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 L%W?{F21n2ZuH~^=w3WW;z)|DO;JR$oK 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�q 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°xYreY6$`QjURV`q%}g-!{|ld8Fs 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 diff --git a/android-core/plugins/org.eclipse.andmore.ddms/libs/ddmuilib.jar b/android-core/plugins/org.eclipse.andmore.ddms/libs/ddmuilib.jar deleted file mode 100644 index 01e4079b048020c9e9e1a7f00382dcb38f129fb3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 598267 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 z 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�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

    Q7Pi*RTN%!S#YX^nEC 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 z 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 bWS5KfK|6 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

    r(&4IQ1ZXrsr*IVTxx247@0zfs@lS=%0Bdek}jk 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
    qKR(L6xk3KyNca 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
      EPm^K+DqaK_ta?s3chr^pFe!9q zVsa7JH2-5)_l5f+?$4`=kPC=?O#_^HE?pnCz;uf~~TvOR%Z)m$Q+~FWY~$#P$EHJd_!g*8_9`wr;h{_ zt1;JL$73BencL`_h3mmdSEN-kwZ*>-n$2UyhT<)l$a>7Vr!B%EEMds))CftA)11cr zA%)#{dwvkkqrWgHEr^m0cB8tojfP${@~eoK(dhLhHSVu;amI*H?67^Q=8QgfQ6oo? zgK_0TIBJxDy^pcrAV?3(d4C0%>&qK+NC__~3%$@?(C;n|4zVa+Mv87ed31B(7>R2r zuw3}80?X@+UbDQqv|%3%;Stvu&GldeRwZjww5#1V)aFZ?L*o+V_(UkjNIFJIoQIu< z^}`sf5fK)H7v-Q7W=i5z7bKN4*DC8UFCmV(7C3N-94<)2_j|}PG>6lRah#E*uN=CL z5%Wrn`W6ql9Nt+TI=xgJ;LP$w3ZZ_g_@tZ2HF%yuZ9}*ro-F>Pf=|UVDnjW;iFX;> zt}bdCYgvTI!+z|15Kd9!ea|NNWTfNB$CK4w!jTKPB~Fl^C*%var718Nmt=?%cf}86 zWmDt8Ds(Fyqx9z)a^&DO?>aWu7HcRtWK!+6PJneDB^EBa-hfXKR$vzK|F3%Gyxxj)t(=_9g}xyH|&@Q{bFr`Bq4!VDVX zt>i+%eo^hz=ciwdo#%YYpR<;Ve#zF`Pnu8*1|A1Uibzn$s%Tbd(mYw_0n)8YG(AZ0cDdTVxz+t=Vk5?57Mn5oxH9KO&4+1XHIc@d#t(&2EGr1_*6xyOwtHQH*08vY6KINB>-gjbX` zfn(`}vhFq}6p<^qCYPX+uWZiFdRBp08Xf=F=%q)@&(zq&ourdp$-qvre@r-#%q)si z2gTBe?n^Sb=q?>hKV)oT5+_kC6~ZO^`YrEQ-(lLC!2lR`9(YIaowv-?LETZm&g^7*PR(pEk{>?7FPXqD}Q zG9VHYR4=7ULlPF@%xXiZ#1O!p5pim1l)#}sL>(dK7}&0WoN^$fO0znQ)Swc4y7%*! zx-=|m)In9r(jcT_T`0C(X;ea{7O~<2k9z}sgxk`sxhsAXX(gkaL$zvQR8zh-%(Wn( z4k%k4-o~;pJ~Eacpk!+U>T4Y1bZrZo4{+won*RW0XbK z8f7c_#JzJpE(vdmtDEU*|3xs=DT169sn&_ac<2seRrbvn#$f#9okIp>R7SPIf=Cjw zhkYljdY1Vc)Lv1gHsWV?N5##Ah?rt&_zB}#pd`xt1-1CuhrD^O^wtrEtJCbfAG)UE z*g<9#6kFe&PzRCS0Pi-cnlLZY_FH&Eb+WTLjVcuDD`8psirAwN9IomBO4cVpMp9>D zMD@T*R)?6cIKq0wwIbJ|*#V)zqMDHa*GI_(bg_snmajuVjNYr>z&;$0GG3h1voJ{W z5jxm3A{Geq21UdjRU(BVy(K?BIG<2v^|o8ao|w-^LuYRK?H3uJRA~rZVJaQp@M~Oy z={7*P{H=S^ZQRt(t~YVwh>Ra2O2N2W1{Ggy{aH^>ndrErEHAMPHYr#>wUH7WVT55Y zI;VVom9+UMg>&~q?3WcuU%6egp*;~-vnYcGD%{e@xF2Ct&(-nS40fZ15;xF2n3K*| zuM%J4?hZy_DXm(Lrj<{~o{C-+#E>DTsx!LS%n72JNRc5g!hVM~?ycWD3VZAepX9~; zhwj%6afENHjyktA6}#u}!gXVkAj_itptRZ*ud!@v6ARnh)fg1pLBUDm3(OP1*XQ^| z$A<)>?Gf!ZyBzM1Qvhc9tQgjvdQ484GLf0P=8Gpq@_F0o5Hk}+bMzTp?T;1qCp)bD zY(*q7Vb5G=F^*>sx22PlnBptPUP+NEFz4nhrN<*QI@XU|g{*hg(X(+ls(jiNZH_{h z8p-M#PvWUam;{my83pE zY3bvNhAu=b(wS6&)wtslscnlBb@IQhll);Cj$wI-^NBcq)RNa0TVhg@RAx>chB&Pi zVh63fqBml8IXCmHE|8Y;DK(6I3w~A(XJ*}AziVE{nQ6JtORDeL3|f=O-JQyI23{8- zqM~q`Ua_BEZX5_kW0P-G3oIBQc`=|B-cBGd z6WnIuWH5JQ!w()i?9%D?M@ z8GUnd&whcaItut@+gu89*xPXS3>KSyjWQREfwTJAF1lNg!QDdAL9hSxtd}tHjXGxp zS+>Rr{YK{3E`O3Iu^ai8-R`kzxH?~dd+2(RDY{vKnbW9rLG6*X;{lin;Q2to0S)4Q zm9S=uUvt}DbF{#(Z+Mscrq%eex7;?g+=B9Nfrr<;d@uX;ulmJr{Ct{M`ZCn&M&H73 zY|SrpJ-4wDx9BIHxOX~q`p>7Ny)F)5;!E*;#`iBG9=XFYlWz{0gR9t;n{XVtCyi@z zi!W)1MJxY|4f%}?dBsml?N93FUFSOlui8h;V^w1E55(p{Vn)p>i`Z@S`Ls1MdWkdl&Od>wK5 zpe$;<%5t51CDIsbzRF@}(V|I^S`!7*rehM5mM391IXQfp>C0eA-}p`Vq`Z^ctNp%m z9_(%vyFjAfP>+V(DuX2ou-y50Zi8YFO4jH;ab_}X2w}IGJpk^Cm<2gY&X^?)BbKzc%v0+z}E) z$Z%+Hf;*n5`2b0uNk_)=f6ecmsu0EJH9<)G&6hM;eGxo2^E!DI zT<=^YH^55-L{})miLgh7Wug5`qSBoFJoLqd&mHZ93QtmoWrXS`DsA-($CZlw7RF7f4EW)|2pLNg|HR6Hh6kJ zlqfyb56opU@VDO{;s7iiwl8!S@$@Fqx38=>PRnfSR z;|>_B2Y2;|`~VscM2!b{$i3XJL&A2X+aT+EJC1=Ti$Zj}F#aL^U(^8p zEi~4cGy9skhHQI>dOiqSZ}G6HV~f^t6>I&Pw#aInyd7gRFnB~$Lpi^x2P{TGpr?2D zYfwZa+REU>j}(QD#vy)v1#fW)f0K7#2aGQh4|g}@Z%Q~pS>BsXBp`Y=w-I9>1ykVlT0 zLx`ChF!9Mnc@U)-O0#L6ZL-K29OtwgF-ei0h=Em4Ri6m*GXPFy`$bU7NyNMjn6V}R2*O!nda!hnhs_b9FM^Y-jtuchC)?)0b9}go1#>Qn?RAg^ zcutZm7u~jXYL&G(dJw!)B0NGUn1>N?770oxKO5|x~g6k1ci z7JRkm0jry{;7+N!U1{spaii7WD2pGI) z7{~@$i9af24n6-g^R+)=rYoL5etO7u@^5Pty|X&cyz3-cYf+QG`Tl`)8i}2R@ekWw z0uP*&@jro%-=Dh*&JhKN54>#H39Ng8+u|tBqYm84fe6w*i>GrFY&{DQYRK}~hI}Rh z(iTMr5rKJ$;=n=JcIf&I96rqFA)y19ho=6M!nHE|_?>vluH@qyU^I(FQ?hX#A8N`G zXVQUhX2_|g;Nyikk$OlR6Lb%jEzgdC4`P5y*B=@$NBHi>lV()K5^36X%tLSJ4ly?* z8e&W612Q)TT`W>N3T+uWsnbT{BQgml>DHqmNewe)_?hUvZ)9CZ+MEVxOgUg+y*R)t zkfkK@5*9gyB03VKq9bRBiEAfNUh-6>QF0JDz?lNvob_n*fNR$0a=e0)j7|4Obgook zeTEayr?X}A2yLMlzKES)y`rQ-Pcf?>S|w|6Dv1Ht54VBSgo}{{fREw2`rf?Clh-Km zyrCE1;z!|&faCOy8J*Z3uCv|~PYJgfn@NM*h>8tRi-}r41$Z|E(w+vu&&0LYqH&8r z-vY}GIbsYq5Qe^H7<^6|hs3j*Cr%l?dK)T_ewytLBEZF;90<5-l^4cN4g`yg)LYESzuC zcW%+86CFJ-AfpiG1kO!3IK^*f40p=-OLTp*QC`uoPa0f?xdp&So@pfgP^fz%9Rj_e zsNuB&H6LKCkv7AmH#p44*L+BW%9;t2xAZniz#jprMpw?(*MQT zJ9lRi#cP{!(y?vZw$rg~Jh5%twmY_Mn;qM>lTI=@=RIp?&O3k1%vq~GL9M;3YX9oK z?(5V~>>yiO!)}v79YHTqM>?hwped4ZMiezc3wbsn0EQyvxbGcu2F@{_AwLMFi`>g# z$I7)OqCRZoh)%doIMX9Z{U))0oi|4Ig>@X&+h=sgp+UV}XnbnpnDCvb!<{xq6-3pR zvY8F@4^_9}Luu@dj;)qq%^7K3T*pXS$4=Qwplt`dH*zE5okPA~292S=mFUWbfA5V2 zTvr`v46CRLr70G`6$x`pW!n~ytAW?NLi2OZu^cdl&yE3*N+FbwAX}5&Q5QvG?828L zdK||2O0#hwb<05KMc6iN%KuRTC5fei0Qd)yo{?&9SHTw1lo~)ZL!Pb^MH6A2fl+oa z6*h33of%ro%-1^6k<27J1RG2BBwLB=$buH;RvamH`JM z5bh~5w^VD4+T#I60OIV*!vSIdvx zT4Z~ulD%aS!L!3-zg$_Eh-R-P)nKK_1Y_191ptC-P;)4}kw}6nm>7UQrEWS1K7p$t zbhR|0Gbu7fq38rHT0F-ys}dvW{K=tWOs$e$!4y+9qyQ(pVWgZ>j>u)-Q%*k>X_4;^ zS;ip7@g;fJuv_c4%|k#Pj-U>EzXyuj8#fZBv@e2nR}lDG=l@z4|7uTvx5v9b2(mx; zHu!p*^)1gIe9JsYev7;w?KAYXU?Ls9M%RDC522ol8&nBDs8gd)cMa+nQRUzOhw<^T z8{`&P8y#Z;j5XCICC@Nb{dlO8_F(vuWbYG2e3C?BVd)_0ufqpSzy(Zjh1V94qr=bI zH+)O@t^c9T1fwZeDmo4gj@QWFJUT=@nnItZ$Ae z+lVnNw1Yu4q7W}jMc|Ryco2*Oczw{BUXN0`N`9=zr6r+3Yep$f+j_Bzx0o?{a+j5+ zPnx(*j$iYl?hIS0$aH}=Y7E1oT$c1%AzZU*`U2Zb(8}A_?8|vr6+Vw|n+4^4t#UtPnZMNknq(rRuLE5TrGq0aQp5@Y}*f zh2y-0ir?su!jBqYTdqT4$z*CeQP8Q8Gzvxch>Kw?Q^2YeI|}sL16MzQR||A(+V4W* zshkM#@B7FSLu@_du1FwAVp90e}WO8a)TLk@FUPWOYvCaWI?;XbsuXPHK zaZa99NzG!wzXg5lMAqX#XxDkrxM6gpgWN>$-|z_*7-MA_Uas%yn(ClwFHxk;JaEGc zlBeCfA1Ur;G|sf%8Qq+s0G}O8`|MAiW?>&Zys_$-Q9~(Qx#E9ihaR zZ~o5i`ZbZ9WfPB8~RrPG;7E zh5{0jCB{|p9L7L^uSXHIB&Ed)52^Skfd`n3)*ViY$b6xC$ZO$N_FdEv>8l96mroLd z?2m$#7mx+4k(_EIqa}L&V#bcK1dlbcL!lG9ce7AYA?lGs)kX_WNXRJhpZ}h?+$-`X zNlB(<@P)Nw#88I^6n(rsCp2NFt|s)wY5&IE#i8kc1KTzq(6lBOSlUml9=(!GP!X&E z|2+(P%NzsKC8@PEZ675nS-kp>ye(rUC*-j!C3!FDv1^(Hui~E-W8Q2yh`VdM{v`(g zUH{S7H|T%=nN2sPy)XBZ_;LNQOZ?9yi2qg6Sgo@0AAv8vjbKhDTWshiL1`*P^Gv~< zxgwp&_)~BdDxIPQsHbVo+6A##ytjo&jIUo2Ao~FbyH4&uFRn&cQPOxR6vOd@jQOby zu7kDD`-dM3cdI=^811q1p}piN?Qu$5)d6(PvBD4=KyRfkzhEf-*Ncfl6*DbI`wAP~ z$}^uuR?AjQ5y?i$fs0H%!{*$kN&|o^VHG`gx8S5!>D}jS;ud&N$|CsRFuI86RwMYJ zsB*`?#a|nk?~o1|%pMiCI&Fu;v4HTFM_Fx*ra)jWQ;5r@> zIlm!Gvb5}MDe3i?9^uq3p$rfROGH90j>zydi>1Y0`W@_h3&xucZ(`*5;|?5Mcf&Is z_HfB09z(qCNd(jjul8VJ05dg&b2h61NvIZX>0-uw>mS#a$peTJ9kj@7F|m_`#N5pHwo17S_CZZ( zCv4m|isC+%#S5`EZ<;ge>b1YSEK)73BIJYfucK3bqmV!?orRH8Hb%KH4tcE{>!bO} z6p^}n$ms4EWAfjoln}3x!r2ARc%}B@1$(q5?@$aMP>n}!1_TLG{AVwm4&gFJ=Acu& zN~FC=6K+$x6487qhcERh>@iNkxdU&UHy~ml7Weu?77OI#X=3-N5&g0ze*Zh|655qP z^|MGtemeL6|7&0W&sgvOTA*3M=^gNJ#YH+O^uP;ELz2M1eho=e7Ak2W1`t01kJqb@ zr%rRy_&#daD zE#?|b7y{q$5nW^#U2^yhqcirJu(~TI^cLeTEDT@4t1C19?P)zn{GsM$NDqZu*KbU( zb6U(qSOa_Zlzl~YPc5f=XJ*wJx8(>oeks)P&;rHt(cEwE$t)R|wNJ(fZ(sQ$l`m-9 zD~}I+rfr2D*k*-6I{dhSz5z5yI2GL0bhlkh8*o{eO88~_B_WuaAa*HOTk#rLfbwEW zE_Me3gx6+V#NVkWO2u>kXRXvJShTA+_xRsmCM;X+@&$x?aNPDHXVqanj7md zBE_qx81)wv+$FF+0?@^WLL9jAr<23knRBoDvotE%@QjtI_~&C zqbB}q-TilJ!XKln0QCw^?Jg3tusogBf(_~D+u2PCqP27S8odxTd)&#kbG|wEQ&MUqxd6v42iOjZx z1iKdcIJBr&N*0LGb(Bc4b8B;5b0=7F6Tni?G71#h{^x;~b`D;`W+w_{$A@0{O`H>U z*k}-ea@7^UfvVPq_Jkr*xg7WRl1)OKW)`0+lN#&2yKng)$18cO&UJIOSv8ztWiLr3 zQma0jhx*MZ5F2%41xoJk=H5$z^A8ICBHUIV0DZ}Rsx5o?;{CwBO!bKfSsg3fe^8EyTX)` zAfIHkh76Pl{7=1<*Z-xL+CS9dH-OXq(Mxre{cS20`hbcs40lwARLZ1GeMU>FruwJq zT(0rW5P>(nlE`26#M2sO(Q&!p{5I^!MaJoGs+4y>mo$O^@vx>rCnnklv247 zo+3c>r3V?im>$U8`6r!lLxnv+W}o$M0Dn*;XnMRL>QwuL1Zn2d&F`K;jWltgwxQy%nS{tFlqksgq=dk9JyX2o2&NZV8G`23TJ=%Q!^@Ud z&DW7F>m70`70q5&FFrdQHdj5J&%JJX&rtR3Z^u0!Gh8p5PG3$x_CvnA0g^aibMy!2 z><}xESO3_hHZ}Ma$qAR?Y#)Wur#7L%7;ca@$-(JF_oz_qqX%}l)(gr(S#J0Pc&<&V zae{?A!%Z?Xw~IN47}U3Lc##Po)R)(lN0HCB6pC5qk)gZOJ6Qtg+R!J zw88MuFz4*(u;K*?XWaf#XWq!s!;8_Q+qOer;YeSl9$%@!QkUS{3?py!yu~{YZy|X3 zvArnA*F@YN+OXPJX!MknPL$as?4Al^w4Rc^3+x^uh1AnVlx5S&dgXb17CMEVMz1(7 zlTb%Z9(>|r+5EPZSh8vBnR+mG8N=FhIj;J-+^twZNTKO6ETEkJuNB3DgO!exwxQyt z%!u%w$#SHo+=iuP&2MK%*^voT&#bQpXAUm*~1hrJwiSve}l`%b&0hiCQV0Lo*R(UwqjwyGGw_C*aWquA4XEjfv zRPdC^U&*z31=h^5H3u(3RQX}y<@l7TQ0^-kZd>>`t?WoH)4*l-Bvaw?TvKSeaL&|> zW^SbjXKG_JI=G!elQtf-i{Hae5hff@8j_Ppj02MVJo~2P#`Ec&LC~-?$gPW5G|6je za953gm_X36m6zF47{}R`k zqr0*+?*Cb4x^8q9Aw(nyvYMuGiDVZQheIyQ3yI1!W-@FI1?LsYFc~!MAU*Bd}Q&C_Q98{M>cxP1PyUzW0V;Hoen zPqES`rh8!LLq*cyuWTszaG*(SR+hA^%+-MyJeyadyqeBT=yn8stC@+kvwDpUb;Xb2 zC0UT8HUg;3Q~QIj2T8!?1JRl6^fX69sTgLg~JMmjVH_fG*J zv1Vja&81q)%c8~ypk*=in#;t_I47y&o1WW4qwqjHLt`IH9OZ(QLa|~dI$Ynr4Ak3{ zH91Iu5BY;oQ8jowgW^uA1%IhN7ek{4(DiHP+V8E|6jZ?!?S}wZ|C)AOnt3uan22?3 zq%y~is{dkz=nW+!7c(O^DTSVR$rDwM5qAvE6SJkF2n0ZnI=kSb3pH&C8*!10PDZS zzTR6qxNoLg*j%<-++2>^Kofx=3^=u9M$v>@#Qkv#A@5*y6n>Fchuvoo`lEJFIqzeQ$Et>fyYA{%wdpz6|T>fTeHumezH7cjvx z^?fA_K4sc9onb4G+i$-;?9zQ(GpA(<_cE0@sux|V4|E);-2O>Bd88|!QS!nB!i}0& zaep*;<)La|W_$i5FkRlxJv9c#ITH(Z6}9h^)vA_T?XI+YD~*+3O6a<2v0qHBk>Mfu z$>QpnujeLto0+rLO3YOOjJ)*4=2A&DtTxs+n$ltqt~1Udw5AADWk%a_4>rdT#HwlX zOT+wS*h3{-yc$7cMaSJZte*G-YfGV$~}Ya8Tm3i7j12KnS?x`kUWJCQy|WI9%^sA|wl34f1)wftrE)hT$HUcr$=&h7)5o!TOqYxj|LF3aohBT@ZYwX#7*lYS+ADyb<;dttjaD=wipmCz6Sx4n3`bUsn zu?hSI5UM|*Kt4$*uPtDCp+AL^Zw;-jb>0c3;_{=jxvIBIwCwPYWTo%RZkt{!yaTf4 z>x(Ab2#(d>G9S_FiGKXVC5Nh-VqB5Os|HK*e8#YbX9Ve%0)yGL=N3k`l7@a9<(2M*U6?(V(gYIPyvR_0t zC(JXO|H5YYj-Fr(qj4+Oaf%;|pHL3V z3`ISLuiqZqOlma5%~#{{0#!$)VP$)@9RBAKRp$R9-HH#7!b!C{*XEzmU=Q|G{q;tt zl~S@5cz~^nZPEONeFG)OTgK#;i~Z87b>+#_%|CXk7m4x)yI*bmrVx6d%K!VPI;+b< z;Tf7OY1{qpPwU*ev~2A_NL#oSq$)ouUmPb!4r`(LOLsV~hO?zass}z1e3+t28pCxw^gaV?tkwFa^C45;rUpJeSZz5U` zOldyOuLe~rOFPZoSn*sw2|h-XTm0gQw0%suz- z6;>$*Qqj)8NQgL~B%!U02_cy=q3q&N3`%uFIvVVCh|pGKw8NC1=yIho$Ee*WT0lx}hGKb$~=8%Wq#iL$v zSspn8+m^`j`t?NwqBQ-aj_4iJvBL5*P3TS`YEQI3J5)XBE|RpW8CbHNU{ItBXt8vQ z(Y#bo{zxGc54g}$3VtcS`)pEc!R=qX&2q06xYhsfGqk^u|3dkRe{_JVKtRm@@6_CX zD`NSt8WwF2gC8P9;H4+SBWYY3Bml?|&Pz?0IEs`O0t!)5m=qW`pYun}D#L7OI#7si zwN>p>&|<54ft|LVss_PmJB(i4+_KzqEq!(V0~ytfz3{wrd$357JKV3`#K})@ZFm3R zAlY6wuR|Cw0-~bDnJgi`L>=w!{!x&8l=%DmhV=M*`)2?47%}1;UrsjP!7(J-evpmP z_E$0tdHXvW`rO0)6?|^u+=XE~-sIsYJ3dw=-eLXR<9#>$yXaTG)~~*5f%AP$gcyw3 z;>eR>m=5=O5tL!44)<}9q+uuCD}Zj-eu0JQiK=ebaRIN>d!F-MM_3Ql*JXk59R~f^ zgx6<`w%^IJ0OlJ#Cj47mf}Ww?@}kabf%&Et4uVVq4pPI0-iqL6cW!Z2n4 z9dTxy56+s$1yOYgv}q46?fwbVdkiox?PDTo^};&j^1FqBQNud4dq!09)h3>)LbQ(U z^y==X*tvc&Rr~v*aXRCyDU+7lO`#_%j`fjyD-H!66hbqOZ1UwN^`WTkp(|4DsiVtx zD@iVErv+XztScf7jzRPs%%{q)59K=63r7~b`$tBN+U!i0)3j`P+O~IN=Ca9^dcRXsG@<<70<5v_L7wxsP`drHf0(; z7MiLqMVJo0|+n^7r^8)K%2z z^nqm>Qq|}yHI%pJygJGj1V=ss#UFDGY7T4Ffh;!?XAI_6@N3|knqD*0-d$g_{NGp4 zO9Zv+Tf4b1Z#ghHI9i+EIWW{S^o1NY9(y8Ce5|uCp&vE(3y!)GFJJ;oVMtF zG2!$zn})(=4-g_`d)Or{xmJcD1$`;i87 zot^6h`&d*@U#YHkZ7fhq=;WC!eu6TDuXQU3K2-Wp2DvR-J2R)TST-&_&!&`PN`7U3 zmO60~H5n+khQ_c5n~EiWG-h$O7Y9efcY;;bY2bl2+xF@0923D<&sWeGE3Pdz&*0N> zktDmg%g~uRcu*iZ%0E++LmK!iSd5!Tj@H+Kf_<`pmRP|t0@DDq7CQLgL0~J@oYt)s zYRcZ(g_0RWadZD|mGpGA+IL*)8jz|M!A>si z0PThI2Z%qF${Xk1Jl^$sE`1xgSH;b$z%d4#>+%*>x6Ll;mRYt9V+54P{JR~;9~+;0 zA%RVsUCn{G(je36sfuuWnY7low#OkE3LJJ9T6S}@K2~!`+vj1lW~5~7#ofE$(sYN7 zb%W>XPLP8U-*!$J9u=$cYTmd{@O0HH<* zW6o4!VR#T`+#QMXixoDNYs-5Vjm$JPf@k&|y@M+S;gdEZJqgp|$(%7Wuy!=2rRb2o zs0}fN`TQ!)_(%Bwzt{&D)jCT%PDI+3Bl>j42g$3XlnoASro64`9);DvPBt(!rmhG! zwU#%hu*My;8)`EwAn8mUqtU84d#_YA(>dBLb;b!e=z|CB;M{8gZLJ2TIUHT8X`KnR zMA9OGB~iZ+bEnw_mo*cibz7;#Ayg8pid8hFC)UQg==?dXMG6tCsP5!Xg~V(KyI{-M zM}xyv5YCMY>)TKg!f^tndSP+AY;n2I=1{#WUz&-RUo)BJstQf438%B`O=uX8zJmE7 zW;$sVlO`%P?CG6C@|i%V!2GgM$FS{Aowvc2pg%Z~``@6oQnP}MXK@T-0|%9E>WD*7 zdU6ho9pc0`THLd|hw+36SO-^PO;vNYYEHl}tLk;#e;t&E?aMZ%6h@!cJ8Lq|jSRqRLTg^w3}!3(95d=hW_E3YK_?r8zpx5=NWe-;Tzld_ zy>6_$;PL<2c>5$ znjv_@Ifq86L%PyGg?3-gWzAr{^Z)){oe-uo19F$DaIj4{6sVP#4(M zyTxKWG^MOgVJB`r$Jo&GV$LNImeux|&Z+KUQ$4?c28ZQc?1xnYZ*t_Itx@VBn|zK@ zt=u2X{G%!B4BlcLpj9}3Ze?d!+JhuzAxTI7v+rKsGC4v(vjTgT1%XCO7s22F7&|nz zF4v5+!ti#C;r7QsDk}7B6N&*ok*&j}QNmRx;mcm1=zfe~&L-xKc)j~mPE0hgtNu;` zv=xn@qK~2R%c$m8KbX%I%-gKNOIDgTTU{oD;)5*QZ7auzYdt8GGelMuLtqIxxZvEF zfm!DS%AcM;+Ze1lD_vkjJQV^cPOggpB;M@S@^K7k z*$ghHlKgky`-QKC^#CL3OASJ|>F6uvfe%{zNB&=O-iqPHu=R#>h)LYdBhQp*O;+(< zq+|t$xAC1)Mq{|6W0!9XM2i>6TMYa@atDgmrcCVG-!QAISbcpj$n!pCjjJ@@Zjpaa zYzd*AXQnt+b8vRJa(OO#8+>5xJ!2yaWtpTy4=W*Q-tfcj zy0*=naNK`^5>%;D{JuiYA-t4LzA%5A$RfRnJgQk}n@-7wwBpn!LR`g}-H*B2zQpGP z0O9mj+{1a-8r!kAjxOtSF+Zl%GHc0qLth>faW#(KV|A_Of6sYAd}c+tDTXup7=>fg z)1#+3Iz$M~E+#?v{3gKK*YVDGJI#amjM`>DWqVH&@;lkr^qk30I|T&kCcySEP7&gX!8bEy}ei|)TQmHO3!uG_&o z8ov{N0`KZ_W`KcfHuv$kZJ<8Qd>YhWn`txlkWN$_)CO4egTN zrZazKuI+7RIi36k>G-;v&C+Zl^*gN-&Li$$0MSi~z)*x;T`L!7TDt6ReMt_B2ZWs- z%8~W8b-&yu?IhD-+~Et{n{oRrB1L#)KlLfD$d~AsO4ZJ#J(SspFZ{)1(3dGc$$irY+!`GL<6)3d2oV8xmvV;HSlAIs zXc>TIZG4!T9=+a|*33SshZmO#UjXNFWb~n8Fw{Gl&D~!5 zUGmm_*zn{ogyer_$szQqwZXZjlY^sEa*kNV)np-$+VEauqhSJ&@ajz!oZIfoa2tLb5 z`)GINR2#DA_WV;|KwC`m=vQIEYp;+3)nNHxBH;7vijpXf^cY-ax;_WWp#c&4#>z%* zy1x){cCKc?eCD{rZ2Sw2I$qbbqSCxpRkWS64_d3$a`t_=PxVE#RGzyER{eI)L-yC~ zTRk-EA82)D#9+-kczEs7D11Woc+0{#sA$(}kachQ&p;G!eQR5#eR9R5qV6-57eFY> zMxdFJc@68yqu}EHmS*|Rj=TC#8)&(Hc$R1qX?qKJO4*vubm#-zy1S10T=g93`Xr1{ z^%vcQul^7ZC6mO=z!-^U=e`!je3_I(DCN_GS604fRUCgSjue8`pI!i^Jj9~!p*YGJ zhI23}pLpD&`M(bu&qT1#OekZTc|YnJ0-4JE8BhwjP29KZ)7Z&z(Qt zVQpUocQ3SjrR%fCcq4oQ zE4PfqD~>1YpJ6u8PBM`9s7ueN%XzCyK{23^HY91Ti%K#esWc>`7!*#20ziVCLR zBs67f7md87WM|E+LMk_pBN=6~t~eW*6U(Gu_yxnvPc&+z3+oVoA!4KF4Kc%|xI!aU ziI7#qQoN=^<)Su3ps(y6e*CK=HS~`n`6&{0EnVc?zZzpZRDZ}^W{LivyR4@(N7M%G zUz;zbiUQWXa6ABOWc?kWX(!0HT|}#KYrcAEt#RwEaRaS!DWze@CfbIr_R-#e1=+(R zTJqC@m}K6BN#7tS>VnH~e&EiOsUu;5?4BQml&AEp#JI2QD2BzP6g{BTk`LXt#N@{@ zHq9w)xEwt47ssN71&E!ND38kw3AdXpPsLiy-9JZj~ z3!jQxoIq$oi}y!oP8B*Q57I{uLbnEk%9(j=i*lh16l9w$l=8U>2#I8zreSY_^hhZO zlWqjamP#!KO#wC&@(BweY%h3g8i`ARx20;U1#pFqver#=jF#vM3!@{I&^I*RkzN>8 z-jP1?3Wzdh4JmpM>XI~hgn*erl9M+%)<~a_(VdZ!-K=mLds0vmP4FD3!&*-2O zo)nt2;C8kRb>s)m3JBnY=tzjmDU8}bJDrVg0J~c$&}gX3T4pK7N&g!ob57+@B&SrA zYnhPV{_D_i#DY8pJp@dGd%MIQ`I*FEgtIux2%BEYJUU(EVAz3GXsQt0s76*ZvakxQ zt#l(9u9YgwpG|QBeLa~j`i~5S2Q?H~{xlO1zPK5hDW~q#EQ9wnfAT#`P0~;n( zid(SsCQR;UuCeuZol}Lt(SM!z?+oZI1aRdwBZakDUq4icLde}pL@(aIvr#_KG7Ski z2j;_$6!C~fiw5&ztU^3TIECud{AMGb9BSde zloA=TNhZ4%ygUum-2~^~78%|T+dUvaH6?jJ{WLPEE7=yIQBo=e-6Ct@(gzf1=VDH} z!y1<>a$MEh^YIAl$L(3J-#*k0VT>U%%q!e6#~{|uliqQE`wfS8=@0E`*dA!mr>?g| zL@xn_rqp4h3}zAXp@0ADH*}93Koq*hLE>)3h*V0XXJal(`YF+fiKk+fEKw?uDg6oT z$Q>PTP|m2^ikG2xVquA>-72?-TtZewm2sqx(H~O_pNJ_l_5Uu!G}_+iMxULv=FNzhA}0=MxjG--MbL6j1gE@V>ODjVn@MxCaVn?A1mD~D@5&Ifq z*EYuadQc-?LzE_kOknd}V*;6gur@_Ji7u{)Ep^~C-voNvfa;JdUUNjDODt7Pc~t7& zw65GI<*}NoYZTwbNux1MIG_YP&Y%gIR?C{w<&i1Gg-m-VIrXbt0qJ(+!P@T6hR?Mc z(KF-}Q~I!q+7>W1kj-4hG;eKJP+H&7f+Ymjk9M3f2mhO_gt15NECk~AA!~hhyt@i4 z2z*{t?LuqFz0pWGMf6c$5FR30N#%-l`*ikc$-Ro5dud3R*zU`V?Q4D*i_smuW<2YrlIloCdiM>s zWmkPnO&?qpUdwmJX+JtkCf6>AuwhkdS=7MFA8!C9hF7YGoxfw2i8_M6pU7#_z5dx_ z965Be`7nnW&gHyOJ#@1r7l$in9FG3`HdO#KYTkw@+py!pjj! z`8XREsTy?W1$--t73PRmjK{^rQV2Sn+S!GV{!h7@Z6#UwiVF+L+d?EiP9Sj^7hY1SSN9C%te-B*JV-#8xPb|i+ zAo$x8w&Qh$-|#lTAB`hgU+L8u{JSkKWfJ<30zhlguC$cQ)urLpW|z%H##=u}tedhJ z=%Q2p^9B@1X#V{dl?h4jFKRi*rTREp4I>YR&w#B;2dhib&$lLY| za7pSy_B#Re`qBr7F1O(;$`M__ft?BHyQ_`Aw`FLKs+**?Ar>zR;qK)u)7%!Z?(sKZ zN9QGtP_iR}!*I#vQ1p(@W};aJ&ko-M(feW7M6|@q2t?#8@!1OHl4k9)LS~t#c<8>B zQF$5;{{8fI)Ld85?bn4Ff9ZyhA3$d~l>E4YlZoAX2ebf!z<~sj!967@rM%La?0GKxOS22Ov74iOYl%tPfL6(FOZ3^Dja|gYm02!7 zB^AYSG)76<(Wa;&X2BUd8sy<{Sa>(gAlXo4bj6K5KhLgN-4L5$mXP6DF5@Pk&_x}GY6n~N-$2Uh^*zM11nf^gnS^W0=FzFyXq<4_ zl}mj%77m$2&h9qG4m2n&I}mqQUqnwS;^yS7W>ChG`3l94)Yst445)>pf^sDHkX)oo z3gIv4}hO<5NLWasd^Up^W#qz@R&mA#v= z9p?{n7G@X=^bxJ&25A)D<>sPuoqKv?Z&g}H+F4v9<VF;QTAn0jVaEcx`oeL zfLL3XRVry_d~so&KNOi8<0j|&d0Czfm;fD#76Pvcxd@paBI3mlxxqr(6%B`EguyR- zBAwUJ83FUMj+Re50NNX}Tj!drB~mMs7XyMG{jn|=HPmo#ZuH!8rJeD-dREG%cye*q zd8HO7EZW%fMyy~gYFfh4Eh+rZ(7^cqcvdShIgr>3dse3(j0iaduZEyR$)S|1T&lIg zFXo?{`E$v&?Rzun=*q^FuIaH#1I z`hgs?zG3EVULsIGtQ)gfCw5eja>rmDYZ&OZ|9xIsT0o*>Nr?b3-n++T(G3lt^7lLMVeV$UE-m|M>mIU-rka0 z(Ij56$nGCeI!At`vhioCus+fr!+X?3dPjU;2W_c!(OS)>N#m&SY2O;nZPB9nwS#kX zq1ED$H?&wTiDRzQPJ>IPe!`GELVgTD&U6BQx=LU=`Zd-UtGgj(YlFiIK#2J}Q>eJU z_RTZuo6ET~V8W2vV4P<=UePj^__vH$TLEzglk$6ZSIG!Op}hSk!?!dOHrb}I#{`o< zvVS;z=?p}&=*^b$W9of$b_ihBx7DSjZ(wCeIXpRJw?PuYh?1VN0(ka!sL@NTfBAQ& zGQ>L^3QTdLm2VhPER;MB5Yxd-Jesf6w{k+z-4wE(qO~^YQcGfiP%xnwm=fpDUV6OG zKMM!c6cVj3*$1rjJ?SEdz2k50JlXqktiJfH)pg+lw13&csa#||61zI~Z@+qK4CaEL z->)LUJc&3n$zmvr6L(Mlj_AArL^3p6TR=%ZRjC#u*RKzPe|liW{#x@4_QmH>BjjW0 zj?OqLBAk@0Pi)G)fx3I#k?C-SIWlim3UI|a{TXcK3+Q*E(0|j5eOpe^2!738FT>g3 z5cL%E#*IGnVzr!%H}N6#Zi&9B6w}iNTk4K@EAeJblq6Za;kai`F3rzBau6gfPDF&Z z4WsQ#N$W>de=t$(P>$HX{5`^!!>9)>RK7gwu7BOwuK9o_yiSd?;-ws%$=U;n z@@9&HHPv|36$x2O=P=(d;PxcQ$0CqNP>0&10vh1oEWl{^gnUdw#|~u(giG%Q_mC|t zV7CMj1lk(UVAoREZAaF$yFl#>L?1ch_1_e~FnAKR|0z4@gH+EaE@poI!k9cnmlz5F zi~2xvxfzB;xu#9jfHlNdI{JJBJC{a$$0vWt^Qw9$uH+xBc+q&uQ0v~R$g1L=tti!e zGO5fpUpknUVx700k5>4^yh=4xhvt(zat!n$r`8V71vcexqy2SZGBLs& z3Eb@QFtfz8phb)06gwxZGi#4n}}=OmYR z(3hU+h&QgFEiK?}%%SP(Ghmg}7pA-F?cr?RCv!2)VB5~C5YVBAP`mAlRWHJh23zEG zW*VdN@yyNIlRgM;DWEAg-$(X}X3OgJ+e3?`OEJ_re+xKlf1EAH{e)X##f8cD$s-vcjR<58rrI~5b8JeQMtJlB$$6bF!cQfgZS}(}~gaYtbpg%(8 zO?V(eJJ}W&SNN(C8i~eCCISj@uQKg3v9E&e_P_$iWX3m;rDOqA4JI1G1c`YQ4+hW- zib#$?uw#**Q%g&YPBu7bsz>3MW=gT`(lYDVC>S!suH;7;cDdeQb+Cq2;=jDO3g3CH zMU|<(wv6o2r^o#e;jVXw9Lnz)CC$s7~)+oZTvl5iWyoDqx|>FeSrtDrWYnT`vgdw=yXTH)rDu&wb{=Yv{Ia?&R|iDg;s*|ti)*Ab zMn1z<=U1Ao(R#wE8+7gB;3caf3oF86D-BZ@cEbOdsoCPyX zLM?RGj#~%_qFgrzSnv)F0^y-w-kGo9);I|vhXl9bsV(y=;?5=d??|4B=)#`>R{0P= zk;yH8wrAXG<%Hbd@|@|DfF{1dkX(EDqUdL4?nHodWc49?tO{#l>>sp-#+VcbTEr`4 zy)f_E(m6RjzgW{Vn6U+v?D!txyhk>OVmn4EpD3#Z<;}z%5joG$kA)Y;B7!<0e*RS7 zdy=PVAmlL%mI5S63mnZ4Jc}4 zB`K+mHeT|g%*0U>M1;U}BlBl=IF3Yv-vGAGB1dUAYY}yq?j}IGxDeY_A$3d0QTgP( z4)bQixSM`5-(bt8W`}^8MkvQDBU&{iVM{-vMw3hbTm~C$eTynJlvxgjOoOwTO-vTf zCR33f8w9B#1S2KZB=St*xUPCUpd$7);gIuauHN`mZ0{(9idFs;_Rf6S^)%BI8=zix z9jhK-zZ8j}`_xbUNT+u{-m|x%!`7~<@{nt+Atrp?G&FNo2m7bFjucFMPUL$$REl^8 z%xcz4$1Cdq(CaG1YXWt2MJItm6Iun>0Vl&svOs(QNVC|aEsXuLTU-uGS7Qv)1jjp7 zK^(~rlt`zL0gt;A?FU&9qNUbIoM7%it=cUQG#jIIk5vWNtYVf~eu^mr`>yBCAqcHIhPsYW7k60n8mIetn&jU6XkgmU3w5cLiTL*1q>WW_ZFClcrOU}s zSUt6T{wNz$xFBX#wDf4B^QTNvQKKYITyU4`BzpIk{2!#fQ;;ZOx29RPZQHhO+qP}n zcGWK1wr$(CjlHKP{?i@P=c>CWA|vuPBV*;4OYigU1E+H_xa3ab1Det3b{DDDFl8qV z2OZa+!fV;HxGj{wm!EDTItg(A^+|&D=~xrx zAhIj_JUz-EbW_C0?iBGnjt7L=V+lWVmsm|9*w|M6Eg&00LQi#8sZ;dD zAf!En`?)pnTM+9jbcNt{p(oCZXw{(tiotW9aXu>VaUMj-$1 zKP$HXBoMi3(&}5k-g08=X^2t;2;}Xu*hxYRqXfbbBw>rCAXr*Y7Wwl$n<^5<#6;4y zB*_V`|3q4MOmdS~CoJbC8l(S4{*BF}le&|59_Gv__9$3b$^Cl2HNHKM_xJM!HNfe* zIUhBL@?t34SKdaL>J9?+p)1dZLVE}eqF0nLlI%`5t)@6fSgd-NQfM=&h75HrxNnJp z#mr{vHIx9^O56Kmor~JR>#JSVKGGHifl!B9}iC{StPfZtH7bf~8S= zB%k~@=GSb3W=N$J6<8P>rh#+q7$#SA>R_s6676mEg*KaGM|5re26m@u_zjSm41hU6 zP}c?Lc4njV&iPQLWxvngCdo*Fs@5{~b@ihtz>q=Ng;$Burs~pp#veoEvbPmQDpXBq z+g2{}UWelLysOjfN=`F+p1xgA6#f(+lgC8}ZnH9ta?q2u1I;5%xwIzZ*7FC3*FD>A zbGtycb(}y`WZZ;Y2$Ogc9Vks+ze1t|O{3Lq=jpkJs6kTy1eTCF28Ud@c$T93@4sZ& zFlPHStGZA9j>eh%85WSPbCB-oo(s)JCnMVVSa4}CRu#Ce=hhf*rrSx3;=IydOm$5M z>)bwc8pGaJae2`o$IDW;@p`R%rq1C&tMz1i*`c;i8Br)R)$Cz!)LlWjn@xIq|TA!>;H_1|Jw5`~1kybi?oWz(m}kBd>mY&667gRrmH8 zhl~3}AB#*=xq!26sZ=YR`*RVq;mrbUus2Ha51-zX4wtfl*^GMQX0x*l@!I4LN;tZ??9b}DF?O`Vsv=JG?v$Ne+`LclX2(DaAE3Vpz{3As2IyDwact}{*T<0>l zSm4Ljsx8)DC9XlaWJsZoMcJ&T3bBuKlaQC|M{Fl;7;tm@(u-ph-C`UQAF4m%__}?( zE=M_FSC>df!FHyRc}?e}c3I_+pJ+Iw+~3d|ub{=|1=HTmWD;PD0K4Q`s4OTsL!J@b z)$r&<(AS)j?g)2|ujcjX7T!Sgg+|gHdy11Qi2gxeh}4SVI^s^A#a%scB?w=zxNZ6Z zw8KjcLPWV9tdwoCWXcmM-^ibfkt$Bz^LcS)Tl$A0xa91Ai1ZZvWE#u9{7)AjxH^f7 z%JLACV9!~=xe=ig(H)W!%=0CL&(j*;1+YfzQ-(|}QJ9m$U@c)fSpb!>#3OrcEur#U zP`wvjFl6Y-N z<4BA$2F5T3C=OL3nOKg8Z20-ci}FX$`6Cm>QjR42f{yDD%#j~_o#yF`l}1xys+ejg zD8@5NQx!#DA#0t6>3jpzGW!PpBDtwZB$8m-5wjOgH@%1KGD2N*@xegLtocH~`~&)5 z3!${A8rKLI002^~|1E#i|Bt9u!PLp@zx1q{|5MLOKSZ1fBZ*=H35A7Z0HTS4Fu^no zNeIboh?yT7pJ|SfGn0u50O zy@x?BXCA6b{7n^$pQ?D@IDPl~>F=pQ`ri9vRZh`+Z<;ZC zRqw%3{FAqTP`)(>^ijT5NA^^{bw~JBzLiJ%RJv;qVNva=-rX71j`$lrrP;(cAjZYU zOC{LgHaLEJXRrkj=|tN==AuEAOXIi8$hZ&13w9<)7wcfW3%18ti;*FStVb6q0cbKM zF*d?Nb0;x50-BA)xpEl~GRcCicBM@~&{~TdbpPU{o5Bv_?bez~`3vlskvRQ@-DhRU z*+q*b#uyS$mh0=|beyH9;C#z&@7RtY{iS)#Cm$TTsY|kVG=&Xb#~)DHa_5zG|JG;5 zmUX7p6~}C9>~+}r5}+U=2nI&NUZ~72E0>z0IEF3dH8G-GLr83jKGEHtTin^ey4cu2 z*Cg@)DO){k3fsa4I&iFS9@h&5m%09I&N>jFz=V*Bm(^xE3Y6Cpudh<=w`8V;9mI9Q z(P=Po%rWU*h8=7RI){ezD6+rg7$Utox57MW$Jq2uaNNhkfK{I9AmghNiP9>~|3Q@$!s10+3U@phN@CX#I`i=Cv$eEi8Ka>^LoG@!eXF!S(e zT?pG$WC~$0Gl{MCcZV0fdazba}zTai8~X3F%_3m!^7U^%@j(IhspMOZfx-y zEv(7Wt~NlnClU34aGzW^9iXVM{S1*yBDi>1#%OTE(U}k}$3cn`%fEn>?W6qyv-}g_V^-A;sd0xMEeL>W-pV3XzvF(y%PlQTb(} zAeTihfqs{HU{BT5t<~cV=O3>}s||_SAZ0M$HWf!sD1~Ysaf)L({~{r$fkaObVVr@qijjdSFw61ak<^ z%hz`;RaRF{8kjq?8j3hMPT)~g)D){PtkhbwHSxZ_9j#7%CZB{Vp)xsUq8W_wkyTx> z!`HEXNiwb?kflMMVr>v&L!UG&v7t+vq1srUDAi-|pXP4~FnrGw5tuy&&5|iYSs8TvYKlwZ(3av0#p6A@S7}zANvQ)lBE4|1(hA_{0*w#&SreO9NX+A#n>%lwh z?AWzlvr9vtuB5qp95ZlCT5qwO>e~3(rbtJEKDTxcq`E>(jYds)HdZ5L0K^@7J>6CC zN#d$F&yo}znwUag;FFPPdAx~fzTMQ##qkVe{7~&V;zeUNcNB*1LhpWt(o$0Du8eOqQC-y`ieg>t`5UJ|%kO_^gwUKo>t|G3_*tJ!Z zC=mBQE1G+ad;+YSLasHk*BnBDVjq7oDw2b523A=?N*d^zOPeTF&@p`@b15*5$yNzT z`nvANoJK!42Y|xthY(+*>M@n;LA53OW(xP@I-N0mAuW9|dSYE%FmQ=&VT$NUXM%!$ zC>w|C^SZZc%$?S|H;R>q&PDGhrR)=C6MB6xa!=x062Gqn@SufTyChnY^@q#aN?Y*@ zM;#$VS!abEaO#WQnqCGbTA}G^!ojH{hTWbMlr5(Ya6QUGqtNdLyE-cSQ9b1SUZyCh z`?OP6mSpGH z?#5bDUgq=R_qn7c(HdVz582v3;5l@b;7d*k%Y7++S6G6_7Q!J}f#->SGCUwi^%5-+TC#d*nB#seRP%P|B@i%XmF%V?cfrvdPa5q&@zouwg5)XMWe4y+?_l%b$FbJklojc7 z&kDL+M#cGOD}5oFe45@4(zH4+uGNyxbd=*RIk;J`m&smAX0^BuuEQDaBfC~jUry;& zskXiM(>bW2!S~};xTsN!7B}LZy&D8H=fgvh%uNrvhdAmWu-}XgL$^BMol8Qvw1j9S z%7u}{X;#o~h5`X;O25)HuwD%-?tIu!ShZ-Ab%U#e|*wZ?xcRYh7%9RM~ zwpJx-T(&Mfc07Hlx;>w^L1lY_M-{2+szhO%RBdj$mD zksgWj(a7hm>_}B^y!y$@P%Z2{Nxv=)ywo7;}9`Ne&I>&eXA`7q&!X z-56tV`Bh4m&*_DYO2fE%?^?r{dhcE>pTfOvZ!?PIz6qV+_2(|{4CtEN(Pb)!=#ArF z-;}WqUT%B{hg}#^h+C!^g=^o_Ma z)hF;lP%at1j3`2vMY$LmNx2YB5bCigRiR&1s79w%sztXhF`(C?|w zNB6h;o3f}Nax!;CCytFD<%2UE1iT7%x7819;h`f#Wf&hVuU z1*+b$^4}|)L1ZYI&|hF5JoNt&4^7h6!QScDsq;UjO^=$kJ+e5;U$%yxxF*Pi1Q2IR zTZ@V#gI1Q4G!QMJ_I6hs2_b@m&Ib0`pc~dJSAu4x5-3Wc%1@;Vp;Qk+(L@D&tO{4D zA~d{7QHdM>3uFAv=WN|Al7Un$)k>qeUuBTltb4EX?C{*b?*noG`=cTNJ<;j-K!8Jx z6h|Tecq1k`uzg_}7-2scXilP|lo&N&PfYa7_I8Re zX{Kg?&0*1(41}f-j3g!(LUPmQDT#@Ur2Ux~i;C`$)24>0&F0e578@ztR$Aih%f$VO z;pr3*{klZf>tG};A$o1sn8n-r%w^K*NK8#$y>XfI)d;r4(o#w@vgYSKZ&mVns1#&ZS1z=&_$xS>YFq52xqS{MsXa*A-d9my3?OfY^VpwtA6@SH{4^oukFan)MiW{34x zZI^>hJE$LAoPo|cD#M5332tAK>1x>sR3qrtL^XKLG&W@BM>gN)TA4AK+MJ{~D{wFU zFwgq%5>ffVCSW!O8?@?>TcnLfE==wbP$<1IQ^+g0k*{#Dq961xIZ@*uba62G@%BQ3 z*q9L76N(XD$0a^1w8jZ2U!+Yn;0t3+1{YAx45ZDKZ<*ayW~(4mQY+$u4LeVf_6UX^ zPf$)BQoxmq6ohIOBR2K=3z0RZlb+U-^R%1j{#}G%Qm{@B_;3`F5rgQ6>?)PEptbp0 z1MJPIFKSM8g??4grS{T&E;Ev9UEIzf?01ErPx>#T5!qnm^p_A_vHfzfQkthMZI~89 zMcpY}?(0s{O7k*On_nEZ3au4w`cji5WTL-IVT#(qQ0qk{Nj1D`kJnK2u0|Si8Do~d z4Xri0J(2C{i<19HPF%%G(u8VVmb9$=7{?Zs{kFqJMAX@!OA-ko3YYdsZ&X~x?xZWBD5q5{MuH94-}^B@?R{yfMt~@UMp}n9?>5n9%JZxKAn(wrCr64zKS?u z#K3o=%Q3Wq`z;1EO@SkzRT=}^HuH|&r_fz&I4`t%I-F;^e9Cjrps@V1h*M6FE?>br zsJTYV!%d7DobOdvXU*~X6HKnt-;quH`THJBqi;!(uIrqH%-&O#7wr~1`{O^v;BroiCTFEsrL#@cOiv1EKf@M0iF=X zxL}NkK@KZ2<+$I?Bz z#t1$B4Ba~3rd8<7Wf%UV%wJsr%q?AGcHtZim}Lxj`sXZwwCn!(Jn-K@5-shm3Om05 z36Wp>S&{!uki>trtwVyTTG0Udg2|%+ z0^SX!S1DAWP=Dy##2rGLy^|!X^}N9I4#1bvUV#p;#})n&evtjq+sn@fxNVeVoN|=H zurT1-;-6-*H-b>A{v>Kag?4jI1~sNT#cfSxN!(YyIAJrlC#w%RLgf;hMt&9I@V6ax zk8i!*@$$y3rj=?e!=X-iW-^;hV`y|a*R|CIJ*nXA<8H-NNxknMI$P^u38Bt+8_t~X zWTrBs6iLntm)oRzH_ck1_TUTPE(`(8JUT3-I7X>uz>)8qLNN`J;J9N_tRKMt+VE5U zhgLlN`|8|(earvvShmdnHMUTewf|+=`fi03Ysz3vihq;}1R#)HBkcnT38^MhDba;) zv%ga4Hb1C!$$fPtlt*#g1HYr@1n385v=G1a18PaAnm-{mymDg>>fTV?Sm9;fpWNdS@vf*grZK`A@I&oE>4R|F` z0|Z(y&rQ@|pYS>!|L)(pgYyV@>zgA}RbrR|{p`+j(MiSbzMy>l6cXJ#rWV=iDWx8Q zN_YPI^8w_vHLD|4=bB-1VlNoaSI#@4-H=!|>Xj?sTE5<(iQGoCJ(>$4OCx3hx*Qtg zfSxpW1LE9t?r-UrTCo$9)4$p90g9$9UYY-v4b-ByPF}H?(tQV)bZpU(Mxt?2wXv^F z#g)r|TLkK-o!~QAhRJ*28ZrKN2sq*|AsSyAC*l^^8Ni-nn5~LYB-<<)8_m&n&H?OK zfBo`5kO$0`$(4AE&GLqyfg69Ozp9J6)l#P}fVpF!WR(x;3T;8s*H_=fx`JgkG#&|$ zAO8(=?Dc(W+W-G6H2$Zj^M67k`4@9c276NEqf8(Gf#eqW`k&AMN*B7t_D-R5>P~fE z`b$TG5CO~|fFJrl!eZBxIq%KQ%>Dc7E&dNc-k~76xdX2VHoK6{h-}|BlmmA^SHaC| z3oc2B`O)&Uv8qYQf@Y5CQ|Rmq%j^uzsW6>R_t9G9BzpH4XW48DeaK%W^vxb38(8@P ze-^^#R@u>z)CRiuA0xh#$Kb3}Bu(6Gl@nxDt2_E&T75de5Q{d7_U+l!7MjGFWPASg z6fiGwuIz>?;~Ye>p$5v(l+(|CC!2U3T7x4l@$AB+$|!Qmiv#~t`3vkUeCnKu)wb_~7W+B-_9I@QsSD`y?kv_zTxk~tlreBX zmxbbnr0yN1G$%Zr5z<7iSBRr5kzgV2Rnd{p&O+JDR1OhO}zyLx*kU`vAX(R~3BnJ?9okr=# z`i(V9)KmR%2(Bi|%hmb{ij#XGG!tvV}P+ZC&;Q1E|mr(ds9X$c}fKR*r5 zbKds5-PUHk@6YgW{2%}$kKZgV2X8sw{S$stAPzxrQXu3>RcLb6IxUWpN5iv{SZ$o` z%kjv$RX?1V{SyLq(q|;XK%4Q-`w$1qBBj7#QG#%Zq+rf_2z0O~N^J66A;~y$VUAk` za2W()&Q2Ox4v~mCF~=q-q!IJ1&?uyp?X2xldmobBUYGG6hj22aqGc7^P z@x+l?7YPbSfvS9CM0gfrk_bHMDOaIkgD!4i6bVdKAqODMN$DI>VuU$IyEg1(lrnS} zuma=Wl2H%|1zvr$Q8i+O*EC2-HIj%V2iO8geNz|}Iem0$HQF#qr{>%nhMd*63ix8g z7GKj4?ds;3LKf#H5WL_Lql;sWe#PpT1Dq8@)~0JyAFUbL7Eop*S)Dcotbu z481klOxDJ+AJnK-*Jfpy4P0lspU%u+i*wHqoqTvdRQI|ux{1ZH71+-3{>KLm`=Yl{qnG=n>+PHE&6?tOiG5Vg9KQ>w1c49s3l zq&3ccdwUvo-O;gE*LyCRokRc9p;&F3gHy8Zc6FpS&OHx19BSJua$DpUXNT}F({JC@ z5uJT+=KOA9xX1E7Yki2v_T&^oS{R%BW_o33I${Url`qee-~D>u5!dY@fDF$iB7Asg z`{@^KJ~?sM@x@5@6DS?;+?c-hHw4!ib31wa4MpB7t1A96?q8zkH%xloN!(fW*UUeh zL(c~w_MY)v`Ubc1WBKh@rlylQ9WPPjIxdBTOXbHb-R{{B?40k=uD#~yzS04_2Q~hO zR*!J$J0{UP$47R&`(9n21hAV!+R~dlozH}1|Md}m)`vvlZ|QA&^na(N@i&Lav;3|R z|03qvpF4p05d0gW|5+XSXT1Xv_Do*At%msy^ke-go5hABJU0ytXtONX!-ev!fP%1$ zo=mUSS8gruL;;*a_@svc6Ebus=-#}sf0Y7w00;^OPo<;NR?>>iXI1IU?ha%aG&~t7 z?Ig0V*;~VSL*T0f`H(pNEIK?Y%|+S>^*WA#WxBgJZ_=4o4BFphiA0_vMb6kk@b zarS0Q@dzH-NGoEtU?eB2!+eG?9uqc92K;jmOgB<&?66o2b+-ySbKt-bu+OzGV+=Ag zBbU_`;YNBLD>o-~sl9?>i=V_p-p-0}GZDcQ=f`!g!cmkg)zjPgSns zcwcqPG9zbI4$Afl=M=u>hzh7fvx4v&%>1Kbg9NomDarIi~#{SW6$EG#-!bfD}gs(QcTD`#H zlV(tquBMT@HdG^FxuwJwM>>`93!Qc2moH3}QeMV1E#BRxgQ>K3Vqg8T5*DSl#Ev-e zqhFx4lFW9%MsHOP^vi-oGaLr=#9_@e`AX81OdBvc%fZ7uCMiwuhU~JegBiG$$18AS zrpy2;$5~59;k7=bmi38s*orrFu*V&W*M_#&;d-D&q>$W9=xzXaun{l5z3Aa?T4VFv zF}$O4+GUDY9mIoUQ|koQbLuMUWZsD#NT8p7Ogw$FgUM#bk-f&BmeLxRCc1+I&+crx zIJt$@b9?<^2bmCN#qf(-rMpy85^w>}9y*2`N2w&LBW&lq+2*yN82D$p)o##M1@KcV#7&?dZh0GZnh_}QJ7VD5A6i>Gv z$?7^ia&jyZ(Py*NH81Qpn%%6DJg~WHF}Ec80e7H-6mMAJI$Wh9S+63j*bhvF7 zKyB;WL;rQ3ym!ozPXkdcez`f7~0e5NAlgPlk^U9Zi*t< z*ppQeX^)UrEV8YQTy?%J5X{qXK2r&XM}rpq1s?LjOMKG*H#FiL{V^;ZoVPcYk3JJ3 zo8176)$^)-4UI`&-K53X%Dx7c4SX<5L%?S9{OPMJ&!-W_TaY~KShlG?lqmJ7{7A}a z&tUUDRBCu|0$0?tCy6wURtOw`wIKTL4h_kfAUsGWo*Dfm{B%IA%>wxK4aB!p1M^w2AJBcvFs)Gdc~0;nT!O1gR_At zlv;OAmp_i)OayO)@ponQia2h0zz^lQdALR9ALpvk?wg3?v;oeKk@gR9tGO;vP9#iO zYkbPvEdQ_r8hMS$;#>j#!c{c}8mU(vpW*M=BZ@FLsd>Iavc*!8l~kb0--vA3j@H;B z$)sE(agAHrv%iP`enwTbzN2DQdNfIAZXTt6v?=eGs6XYZ6TVwMe{)zIkR*i13kT(@McP}C zBo1{_Mz>oTPVaoz`_4Zy;t@js>QIp)?FOq`c_5Q(W$5++A{8v83r)#-mlCIsTPO)d zsy<3eMb2J!u{?ViQy^L@7Eb>u{vDLLZs=cj;`}CpnQ;ylZKZnYn2VBziHCotO!A`( zCu4g%&72yCC3MXu1zomz&Ixl@DVtn4HQ*!}JEtJS#NkCbt_i|5@X~6ggiYe&xF1|_ z=SG$g$=(iFm=fO(Y<1z1!`+5ZPaa)4b2@x5;sh~vd(aZxwHvDKK~S$2NyQaZdKGld zpJB&IaP7yqUAognWa5@jC^D<27G&P*Z+$fP5@c6TFmQ?{hFmkr$1XWQ%an$%Up+?I z!7g1g=_TF9CWIEpKJetnPuxJewRNF#reaWBIrBm*=*P>>Ibt6;V~)UzL@+FQQbTkO zyy~VnmOXeUD89mCWky^^&8+QM7-F^Xwijvf(DzQBL04FC%^9e=et&)j1lYa`COob7 z=cVq%Kc}%O|H=u=A8+o{5RjL5yd~M0Ot+`ZM{8XAukia)%`F=x(vgovWRtq(lajy0 zo?%*nm0dVh<%lDh^~xji1ScS_RJjLJdNaV8?om1Z#Cd%$t1gNYpqy)cyml31lV#hR zVI|&V&5?Of3b`EN(S3t}pk$ z7R7_0Uvb2%@96W+bxgbI&Ke*mjleU~y&xS;LVH>V1Ik<2=xYk6SpgbPM$`+iz;{ff)wn;3pHXymskkk-Uwbg>78yg;h^#**=b98dqF48k z!P!ZSu{@A>@E%9Y7fEM6q0ya9bN(FPvV<|Kr8}GW=+q%VPjalfx5MT4>!w3L!1V>; z$_=DjJwb$VfdpQ8*9HsP6c3o(PQ7f50i;t^!HTQ3zmFn6cgmz$8)om+nN2hd)d^IX zhnDE9P7!Y`(W*F7D!%t~iMJzleXxfst~6<(Q=eGr)FIC8)R~fu)hU}QI$r4H2_v3A zhYq)X9@bf0cz)j*kAlsXfMaQpPam6n>Ew~?bnOTX9@L8ScG(bE=!8+|CJAm-Ode|zw^}rnU~}S-oiW7Qo2JT^MUraH|V~Q-xp^{%Zkvl!SkT^V60EU8*R$HEehhgyq3o5SY{a6rE{NXV)cxp^Ph<3RL=_!a7K*srgz;5nT__b1>RD^lKUL;-9lXAC2joPnb4OY*xqodl6ZOTXS6f^woxJ340DygjT zFC&T%+l@#PCFQ7WtT->QCooN~@#AD?kH)d$Xfv3*axQ)S=(^BOd1@9mXN1A1j}eZC z;TkJNDoL6fUbTbEDyMKVx+j*}$6?)e!MRxZ+CyaSYYq9`5pbJF_F%78TVLgZZdEf? z5#Rdp)<7QN?kU&0GN-IDpy}nLFLt4A2e_B;<@5Ng{9~o0E{GdqUht$BJ6BnMV|S8B z28GhNPT9eD{8$)B7RCPKUltZzlq-6_*RE^erL=a$WKUwZpJ{BvU6Z%N$}SDv_qCtxY{a1} z0%yY<6Uk%guU7V4{w&GECz4ZVA={u&g!6yWAf06`9Fb}-+s1DrL>&W?2Wr>M@KA)8 zI|Wdt*0g}Bu#?!?RD(^cpTU5Q_2O@jT|s)i>LoB(6cLNd$zM^i-AN?KxIeS$VedeA zcWih`ND`R`bjCWgM+@JPD4sF(ZH9#Y=ZXCSFAgfNOA(Dx{VY^!<@Q2JoH$R6X$esd zwwH@k)iQ4Bij+ZjDfKb&oid$q4r_~g$`D-vMm6|vFG&6));gU|-Rk(;S@)4SH4|G1 z+lM!YBF01{k-#Zo9K1Y{DJasBGgt~&Z=Kml_r;hVr%!vLFcISCK1~uLE2Q8BS?=03 zeVKjz2F4AW=#Lflei}&2C3@G^R;Wt!q!ErtlfhyV^(>^(nhr*P^pw6yVETEJ@Id@{_NVX-fUc-hK zC0h1@WF_|?48@rZZ$ZL6+BER+GsMP8 z+XJip=4CLV=b)JK0;+ywqce#BKi3Blb20}JOVwzk^(IJ9UQqkzs9dAD3d6I?yfR8_ z%)yzH-9jVFx`ip0MmvQUnmh)Jh+%E=0k#~J#8Sj}>x7Y#ixxOI_2xhN@y<&7^-oYY z*sjP_14TKGlno$9Oqv^a(E#0~pECjWY{;P0hA`ZYTu$A zi>Dr2$N@G7U~RC&fj720QEXeLM0?+@9aZaq%>&|AWqQY=o_2J5%z^iYuX53m1YIa~ zTO|p^AKI>8s~#bZ>p5Xr8*J7sGk%&&OZ zomt{p*%L*h$=QM51}r=b77L68rqVxF;-94lPeLIk(Wb~3?qGexlY+=FQ-|8&PJfpX zkg0O)TP5q|D1dP+S%TT8?LbXfGZc9ZN$e-t@tJsjIjUGfv0V89Zqes38--mc%`V$n zF@(&i5V%Z5gT|(T6-cRTzYAU#>6U?*ryzLBvWdloEdw)96O>1eYNcb7rw$;z79O=~ zy~?a%Ma~q$BIIp1R?^%DqQ*QRH0nAGifw&FsA6DRz5u=4u$IwGE1BEuAr+w+khPRe z2=`W+ZSs}YL4=ydB47#Y#;S`qc1o(nMwYWBNSdiNuEK0ax_Ca3V*u~gt%USiJyY(mE zOMB!Mzk()BrdzE3*@t{*{!OdW0?t!L$y1T?WB3VExG>Kk+I5s%wDgSNZa8 zt}`pUoeh<>!?rEy*@u)lf~SHQdV|R4Cz0@^@6+FlB<;AZhmk90r`F?!k{?l$Q2!aw z;YqD76>ASWvQ%kY>U2B&1vHMMVcC|I*x80(o{XYtBeks$YU?Nq#?X;;wMG@2yUGSS z%VJmChPrH)Jd0G6Gh(eWvd2gDFECo3caJr-bCrzZf$L%H3<)unRdH~Z;>OW7oL12` zajY$+wG29j?6=!gEy)au^m8npQKGnt_2gMTJj{{-Zi@6|L(HF& z2xRB@0neAp1(fgh2LR~;fL5?e1YjitfaZ>22L$2H4cmBUfuxgOH2wqX_v@wV0+P?dGgP z{Xh!-6M^9FFwA*+{@RBTG;%Qmal;gztu*B2;|4l&3HqF>RdCHJbPubHch1X(Ne#;> z%g>WnII?O*U-$8qHtPNZ1QmcN;YcEtArsYzVXF}PGeH0!000+~%z!&P2*VK{@+pJ^ z(l&6JNF| z2L&@ba^eAs`HelXyeB927m(>r{)o&6l>JD*0g!(b;BSCnV%!)sdLGH>!GsY~9u?Z5 zWg(V()bBufJqVrRMc1-iW%)(HGF(YN8sY%cgRJ7tL9qx$WzdTe#a*#3RP6ySFVsAl zvoBH20WOq+%?Gr0L_f*Ndbq^7NXps~Q%sjfJ;9*HoE-c7J7c+durA`JPbvwch)~n4 zoz@r+R-@S`=V@)>N44e@t|?gls(WbJ0Z{rfv@~IOKm~=1o~d7_T5IWrt-TnPBd4BR z=mVCW!yh|PTJ@mO;^5KZAj;xk(&B)sX&z9W`F01fU5qwk0mHgCEozOcj@a}-2uD{o zGL&`H$TsxqS7xQtt|rOpiFbB~)b3y#hKqu}j<3z_9EfFSAZ15F=TcZ48bv#s4I`z6 zMA<;O!%Ar>DJUr|vu7ulUrZ_$8A_EloAPTgC^Jz5Mp^=Pr8{{pK_z`=d;HQqA4%!T zSgJ6019J#x>Vqb>)+q5Y%^is}c^lQg;|RzIiKK#<3t1TEjj`Dyqu?hRS;?CXH{I>7$`_*e%&^{H11DI;una2n7;hf0`M(nvd?#b1Uw>= zNGBx{R*oR2NXsNz!y%Ip`U_x%aBRdNr9(YOVO%eoE* zUk`ssK(Li`Ku}fRii@X{Y-=4Jo&*HgbaIgOZKI9NV>Al1frbiWfs;%Mf~6BHh85%M zhU7*K4noBn#+VGfk^*fTE{=M*$BlnP5hop)q+X_F0`k|ujkh*uRuAby&LB_0AkT%* zG|00NI)l8(p!~{G4+-$oSY2R_LSW7<^KbPNC-QTXq5#XeC-OWr6vEh>668Ng2MpUy zrgQSi0|Z4tP$N(oJZXGEURIvbsDvy$U$IpXnGwgw!xV~I4x0L7YKYFDz+i% zL3sNpuy+xVeQHFJuJy=`Ll+#<*?!7HnH*xbgTzCzZ2~O@8apC)H|ATUw*8Yr;@kfF z%5n##2?RO1pD+}q^2L(kB&5&8dtJHNNg=Dv>YOapw0$e5(o_-;^CN=pq*JTQoZ>M9 z9oa$49Cf#)JFMoBK0}ZlSYD z_q(`UN^pRG_R4sH1&4|pIy|MTiSqSfL2+vbFU!`UiSOLqg1PgLMrVUOu2{nyzLg~z zTJO)R@Wbu~EBB+mD2W&#ZS!}f7z6$U{W1=g-%$H2B%9=7mj1Dp{w)sh-Meu1BK!1* zSPo<@_d$7)^b$Kb5j`Xj^8FMKBr=Hdi2yvv+UpdB=VslZ_hm!P>w{9p(A28oTf?EL zmBtkf47kVc33AZc&GfCPZHsrU=L?B&oI*2=53!^9?XGSNcn>@pBhZlH3fYnB_iamH zAk}14ne6C}5V^vIb7#R2-JDzF|G2`5k8`H-_v)(zo0P_W!vKfiT0Cl{ zFQth&>>IAizHMJs$+lZ_+&sb`zh=5)Q+BUA_h?Ra;C|Ne5sTYy<1V&vFeFs^VBCHk z)T7rV{Pe;d4`C@#?S)F-sWFe+_BkHlmZRMZ6uZI6Bl-q14}CereE`s-;ElHgr(zoZ z5;@@>Mbci>Bi|J+I1aItUx`1>roGKae)8lS_YQxvbSCon&miO->2X9| zfu=VQsR8~p!rrjq9dB|N9@4vUc^jFVM&VmrOAoO%=>&-ZXf!KL@cvJJhIM2Jt=K&<`)F-#qN)K&cGdkTa2jGR%b&5-u^BP+d;4 zzNE;w^|^pztme-zWC-=pw%6W_IZZ9!#S7i^PgC_~xafZ0Q1zMsDZU!feV=%KaS**y z?>-l2COus|<|4i2qWTg|Fm3SuRW!Y_-8k3!BHR~*UNqOZ_KKHQJ_TY1DQo@uy$M>G#`ahJpMXlN-UsoX%F(Ppww6PGRax0oG5X0g zIm5IeJ_YzNGVZ53iV>RAHDs}v8F$dc4R(StAt4@{YM|;>Q4a&|m@1_2{3%5(s_{ZlBo|UH9_eotUT0f!{U#%-ZfVH z*PL*D=;H@nJXy@9j2%OC;*Cj1!kmERl3O^`y&NK|hnzIyQy0ToosetOvlqyJ)@iWs zNih3$Rruxc;?pO{x|PE2!8T&pCulv=x1(;Hu=%Lu`+FWmd<64C?8lYwi9NCW=-dza zJi%_$yFzsHNSRJHj8b{jt5eUSjZZ)e_4FtPAB`NtyH&Pn>CxdQsE6@SQV%&jQ$0lW zXm^v=sFobDO#}$1Rl!9er^a3RW8xDYb_7X_ZdNt!lpA*Iih9}sq3#v^?g%WN?%&=q zZic)l6!n&(P85Ru_uG&XCG(ygeSm0)WP0L{l9#*i^9qC?VVAvWSoW7x8tW^faP9Wk zV!hKmu>O_De31=-W;moPP6Ry2z>GG9{E59oXYk>lRyRb@G=NIYJMy^ru&_X}ExJGV zd_<+2Vn-poCQsfgg8a(g^Mfk(7uzq%AK47X>J>=DIlcwHVrMYG2gJ^L!-Xl7xYX9L z&SCBiP2KBfaC@LXlrAmmNc<-7E^7hxW=O`lftm!@1v_G5oI+X|6NW}wen7;T7LF$7 z2zj}$wfnrA94>fx@4uI2Pi*VO1e?^RVCo1?xyl29?_fJ^SgV|2=P3pqe#R;MkY~*L zXR+8FgM=i;V&=w8jzRxL->~%^z<+9 z(Dz(Q@=KoVUx-i=wr$(CZFJeT z>(;z8=SIv#+LNmxx!AMBsCQOoEKQr8cUof73U18_RB}>Hca|5) zcZxxy@4us4JV5pR74ueIo751FMDeSc;saJx{?h{u68pF>*74N~C*muMZ$Hv4_svBoVgz9Xo1Kfd! ze#tXj2g!6=@`umAdoHyu|12zgGg#37FBmMW|1?oGDrw6i@*{EAQ&%)VA^k!w4bv#3 z9!dI1kaVRHPa=$KPM0@J)&5TkOLI=BPw(Gte>k4>5DeURpidIRj{0>xiaeI*8pfxs zR)@olc89~e+3zGJ$Q7>vedfw_s*vDOw)oVEP^!&vSEOF#iry$c=dNx6u z!G@PgW3Tvjmj>x@f)8ZTJ!ynCzigPX{&-4qnk;6%BfBJJk5# zO2+#OGe5rZokrre124xrKkEyhd2OrLE_dn22@QVsX)#4idOpCc1hey}Mu?a@(rsRGch0KEs1H^P=^!*b& zdC1Ow(#&BALnhB4Itu2^DI6C?If3v!gZ{f%Qh1$~>A%g%kl$kA_&>5668($JmZ`j| z@DHtI3V{ac>lXY%ViG_|;ui)&)ly^P0AfPAnk+yBC1tcLQeS|p^DL0qz8omw z-99Ndr3_>{!gJmNI+VSeue3=miZrQd?2JI|;Ed*?L9=UelNpE2+^Z&4?uGtO?}&RA z_@Nd~ke{>lA|ETeAo>}gdmBl&I!t?%E`uYEzlE^8S&$JmGmMX5&8}L^5wudeYf?Jd zgScM8dlqg?i{^ZdAdDXfEmQInu40VFVAdO)nHG4MGPatWw)KPI`vZb}FW1aE}hor6DgaYu5#AS>uPG;oHJevQWDvi7?4U2^8rjV56j8(iqT)_fS@McstQtJ z@lb8u9e%PpiMgtUoAwi`{*_dB5dI*6Dm$0(iAzMh`e5aBg)-^IqBRWA5*Jqaa5IsL zM&(Y(KgR9^3x5so&-h5)+DFoIoiYxSCPsMFj2&@nCrAgL^87cMmiWKNX0gdOI66TDuWYqOF)V~Wc(u-8%;U7&F-C?b1sIbVx)7Xm9F7n6R^IH6T4m`naC zdy06CiS4`F1R<_hRY~B^h~Y@ilb}5r8`uO9C{Ub3}$XTSr=Vcdn@Ew?TNC;&pt#IunuKY6T`s1J{V#n7$ z=3M95loNb)P`fW%iwLqtL~+_#)L_%cU`}(XKRF0ZL4y^(B=0i&M|CJD{7r4|MS6x8 z^{aYKH_$#sfvU#M+RHZ;k0f<&o!eJn)4N(7{Z|4}<7B0=h;~dh6aE{=FhT)Z~ zx(m@K9lwZyQcO@m?jQ>?EL4uRSUryr(alWFssa4 zkx)bXEjE6duHTuy8i&`aLP*-mdG{`b>~*pkkR#DbLpo*gbqx^PTCqzVPo~)Kx=lyI z$qG^AU%X1!()=bVCf);U4yei6p=Kh{A1X49yOWo<9r1bP6js9Ttu zuJLRbd-KQL7A@t*lqlqj;W8>RaX-92sprnT;_dPVwuKF+uA!>{TNk1fk%HD=AR2Z< zM76C&PnL?Y&J?H^CE)0{Y`5?)9(t2Q8;#N=mRZcjk2j(I-fFu}R_=5fTy_yL_2e2^c-v!oT7+7Pp2rmk4$ z)kdRXx$i|=`@);1t%gj#=jhCDiHbjWeG^>?&|S{j+1M(LTe2o=L5LzNSECK` ztsHz1OQP^YlLB4D@IqDgK^%?1k-lsr2%Fm@On5L5yrO@b{?oQmTH@|<%ZO&QTv;XM z+@2>DD-U3M9!`8pEHSN3x8RBp(ve)tTyJG6^jdS_#P)gcUubw% z1Pu`#yWPVCjWpLtTKd@_HwWD9EP)THeG5#d4!8WhL{14Gy@V8fRYGA6j~UatbBIIR z<~{uvHri9PG5l9#Rh@DOks!~}VQY@qhIH^Ib@XF8g@QVx{Ex`^f)4lC;2Ec^XJ%Az zUky9**+LbCxTM>;#omdY3w-B6L7^E^bAjRiC@4mt}wupZGywbLR)7 z6a(?o_Zpn!4-wZ-jZ`D=zHP4EiC?`YUMf~z7RZ*ul^Do>4f3Qf%*$(>*4s9%7uyz> zPS?y`$}LJ{jX7&=L6Bet!9Uc}#OZWqdP|r&?V+55}*)e)mA1+Tlm=a>DnC z(nR!qkLAYoj0^AH_Bpum!QgS?@7E37W_0tUQF<$UU`6GH9od6|(QT9a!}EzZ;;&_d zU*1Xkk{xepgc>HxWX~oqg5i*h6`9djDa)fJ5PwFHdCN40iUZOC-s(^ymrsz&*cXV0 zd?Y6hn>hAl*QzAKUKb;pA4`KFoDW#L!HBfer^>{eA70D-tpAj68>*#X+8TtCj#(rb zGmI0LuhHugiPtX4Mby4lXpK=dRVtNNhE?XI&yGSoR4ni*!<#!Apxj?HO@or0DV}vi zJr$03Ps4}G-p`9aK{A_koTcJW#;Iq%ZFNIt)5+5SZSO+78AI^MSOFO8iT5 z^N^+&L5ug=98XeCtI)|_Hhl>}O{Vh)(Vw-ZblWh$`Z9avY8ElWnmi&aMTmAPp@ujG zJ})EWye-(;0jUF%22AiziXJnSl=ec_{>5 z+}w&)aN}P>cYxJxYMtP&Cf~8)ES~+Fn_|9DAkXN(g~Ip_VW`iv0Urs5xY3=)UuF~1 zbay{xTY!%yiv~|9GIK`wu7h?vya#G=W7?{*WeNqtxf{glt<&eBLpL*p;XXfM4%;L@ z_RVt==opglp|TzTjm5T<5YB2i8tAQCRD#BrAc7AMQu7iG<{i=kM}!r$_G3(li?ZF- zFY8r-E!;`_QcfgTLF^4k;UzW5C@z68Nq{2{FFAt~spE48j~!gU2paV;Wqwmf+|c~U zmC2t9X&&^$KL2|TON>sVff@eN9F8NUfna5E7D0b&1$-j8OomeQ`au`~vx+v!PC!>* zuad5}5aw+MTHSe5Ud{c>2jx#a3Vw4vA)cmnJ?~J7u5AQ$+9Iqv=ip-%U(l=5@K1n% zw^m^GU}o-&J+gztWE7}RdGF{N#-qz`5u=cX1>C>>2q(wFXq-}>@z{A-v3ySN z(<5<63b)ImDGX>aDQ%{j>9esBEf2#JO&#RBV8Yre9fW9lgG(s_r|7yBS%f<_Hb`WU z3^{cAWZ+|{4B zDSL>~1A=9Ct&+3@43qF8J&%Fx;|O%2*RTE{9+8`CT-<+KTke79YGmRE+ zsA`EQIxr09(>9b+eG5Y0qRCHKLc=n{^~c0qvf#Hpu*s`!?<}(Zy3mjHf;Mc{5UJNr zV<)X~X$WttS((vwC0gOyzT1WTz_`jjFvtg&P7%!Kl^(kc>g5FV)@nA5X&!UbvJsWa z_Br?rUkYPy|NYG?aj3QCP;Iu;J?Kb0KXVyHJ=$Oo!a86=1wpj z8)cSf8accaZC{g068}acUNH(DY7s}DIksh4<0)bDUkIz&M;QF-@8qAkgK#!EdFI|) zJP~OVFHXM!wdu_5ccRYrRxP~iiy(}?;Mf#sK6S+O(#&6XYV&tKZcwCZ<}c=CfoIpA@?)25pb=6IS1=HwoNvt-uXfRiigZ%a%2J0^!bZ zV0BR8og?sUyL?X=Wz4}%?D1u`Q8bU!LCd+93RGnQny_H5m{5GW5LxMe!LuMdalMf0 zd~}^vNkfDL(`AT1@nH^T-B-{*X5Bvn2>9p#LMBbl5lzkrz8OD1fW&nQCSyWdu}$+~ zFr$FS){Gwxth)^p)K)zBH6N16_`u@KUNi0+-HD7b7v2I?U)yLA(?uz3cLTtPX1%5a+LLv}2qfc?MmaX09X3 z#v*K}&kM!QIqr2Ilv^mZtjtbPMAon$kYAyIyNaazy%ic2Aw4c7jB8!@vP{XR+EXrF zrLAnK;w4$`Ox-T0)KWG_3C3iH59I7OM-_W6`6hAG(aiF}6uem;j2i{b&=%8LO*<7!>J6W1KY3qlVEQj0%`5pwOWsHwkhE%FN0Rog)QMCXKEMq39m@o?qFskp8V(^O$2D0t&F*r-(I$f5XugH1_dCE|z)HcQ$XfYt3uF9#rT_k95 zBoA_qQML>r5jc*fnoTL|<_W48ZpZQtCPfB!5Wujst1_W(J4SL^rZ_;_RC?N{tl@SC z4`AihW64Cm5LB$Mih~V6jCF}Tz%qS(c%<;siNApT-Q9zaJA|xE2-?Ct4iGd|n9?6_ z+cQ(X*D$dxjU+M==C0ZT0vObk@K*(sb9KD}Xwhscp8)oM=8{dt3vrIKIx zCW|S~GEfWC7XyM2EP8EoC2oTf>W<1EafOozk?+{z3+egjX=~$WXNy~O)zHgELYk^{ zb10|KXq3d@#^;$xE5yKrx;|WS%oD(dJH7NVsttqwN0|P`#fDYfRJrhbzXjAj*ixY4 z=J9wrpDk#p-ob2@R383h?I(d8m1X}fY{qbw;*ZcQBthO4Bcp+JG@!iuG1cEhRIPix zK6Evh=%IHK)R=q9z5*V(INd9=;%i{d22RVT$y12QuI+_Om+L^Qb-N4e-P#sAPMfJq z5H3B+fW!XIm6}~_>p?6X92=Kc6Nf%!5Quy!0!RskN5B1+uBNC*og)1#7L~Gj%N(nT?(p$Um&RMf+*f zbYMI5t*9W>+_to`U4RGjI`%XihiPs=@@f+jk3`e z^k%YB64Ju4UbKy{b+s~qgzfc>I`STgc|(_iVdvYG!XiyCUvZ4f{cfqDVh`^+NcQqo zRgoV{#pUhveI}&Y`DlanOlMLipO&hNI0-0im#vufmW-L_NW(9V(OHhkp>qnK3WwvA zcJ8;2J@vq8l~62agN&KlUN+Dfe9Oi%zfErUoecM(&<@6*_OArhX+`Lr^SxdUQ%Jip zhpQj<>!A&^1|h;6E2?`GYv6qFvfJy%;+OsHx&(@Fy4Th$bbHHrgC(HSG|tiQDA8*ufXJBN zf0iMGGq%we4y^i@cID|rSlzUU_N@Dxb^T0oiuh|Dviy-Ad9~Xh<;U zPMP@%ptSfeg2#zydZ%jG6+ zi6SAXpFi`PVrikk-eW_&EOt&o$t4|>kzUu7s%}e#KgK@+A@U?dwHkZKFj7N%z zo!VAYhrgS#XD+&_-ZI?n5)asI_tpNL3hjQZK@^1{TUEr~y)A}ly)sr9A(kHP{%;TR zuU3>~yNMP^Z7c}f9n(9u;dB%B&(2=@E5j&3pWaHNL96|q3nmyBdB)3pl?_>aF218Z z{5GWMnMl{L%K=75*0xe8?gq@x`(NICw0myu*tBz;uA#)A@SW3Fa5dArJ(NiX?-RSd z)6{!6WdI6qv((^aDks;?xe#5=yywURf(}3{hK;6mA7@gg>+8k)i6C6X$S<(NTt7WGQh_(^wS_KK%i zW3P;irD8Hw16u7!MD%ZXa-jCeo^zsvJjdQO?4@zXM#KihFH$2 z?`ri#s6%JehqrXM$A+Hf_WB?8NZPqhpzA8H8x=0Z&JGOJdPqg}%&FPRN59GQ-Im_w z7^`HbTNt?acJCu5Pta9FcI0suiPa105S!yxLp4V;MyVM&#m6UPV9)YhBaYU08h9c| zuZcX2yB@r`0*l$}Q8cawai$7okThL#)8!pXJ)1)Rk~DEwG>1Yq$E9(nFG;;tNj+(2 zTrv`S(~#GuB1@+_7Xe;*#s^+vlMJc(rzO9i$qX17(&)?CEy6SbG3en~g+XiTRV=)a()E3qeB|A9VK<^&1$%4OJ znSFY8M6 zhg&0$LTr_l(d{C8=ji+hss+{)`D<*Y`aompe0{{}j`rGS!v$UAM)IBWYPg-v-~+Py z#@h(0uzdRIhC`G~fqFF`#krp~p15NnQJ)P?kLAq_mv&E%=E34IBRORFYKfhEWxupva0>Na2OwxTAO9Xxt|u=`P;A24^ueo2hooGbA@ z!ev!?EKC{Q#H>;u47%`RIi2$DLvrL*ID4WO5$Vs+PF=_veH@D+gH^8*2NKHz^qpv|l zdcHUb%R7*d)Rg58F~HYg){(ZL&msnq8Z~Vw-i-Jb1h*|CvE?V>aan}Fl^w>DK_Xyc zF9(^)FPOI{_>iF^kqP9+`mnnc^CH8tlzz|CRzB&D-)d3Ea=(R6;E--9{lzs-{;vCv zo+0WXy}a}(o}H4c&%aegdl{+HQ-esth;dWyW}#G5R^c>`zw58993i9dWD2`Ntgg4bkGUzL5Hv zw(!Yk5A$^2x<9cau-bnAS}nyz3H|WS11$JN)fDLy&~BlgM$(m>-wqV9?^>SP ze|@8;=w@yB56nN2y14Z>64Uv+q-JIX`2NMr#z;lq(h1=34O6oO82$tD^UsUF%Kw3a zA!SI{5JhD9LSc?2QokmO8%SQRImVt&GKBjF0o#u36cB@l1Sq>nD-A} z$eVK|ekk$VkxmllNjH%3y*-)?tQN zRCUnArXM?QgkI}x-g;+H6mt#MV`ss#ryotmE<$Xt1MIUUi1$pO+Yj?bWh#ebEI_mL zMw$x|s!8{{z}q{n=4EQJTM9zrX*9Ae594Ien_x94P<9gDwAoRFNPuY*(U^h%qFPIl z(4kQm3a#9`Awfu-(yZt{;D*Hjt;r2cC%W{=F=$q5yv4}2UN|r}9N8m8$IXjt=uyYv z8PXfiJo$?shoLgWHhLF1a^us>^?(2iZ?M(`b@+0OVORpgaLv>hZsR*Ma~Z6>eYK&T z^0PI-f#v>g8aA{Z4ru&JieaALBVrv1>r+QXn{w+7n4*)Gnw`R>BlVJWmO<=E2kH+x zdA~fw;-Ke)xc z1fcJt&?o4RAB_L|1uN_fuy*`kl)ryf3KRb|u4<~OrjRBV57lOa^6@8CPR`FfHOM>= zh~M9!n03SUmUEVNO&Zxx%)iWjp(LL_eB$Y+R#osYa;@eVO{P;{jJsA-S8=<$yus)K zRjFHo613**(IW-=)sXgPH0gXt$x1g=Z}7svi}F)?1(BnbxK!xJe}GyoHU*~Fwj4o3 zZbx^kGFEGanl0xuDQAQ&XcjH?+3Le_m5rs?MDh&=~g|? zAagN^FJIkLryf|_Z_^UeKjKaXNzsPvgX*gt+P?vy{Ip1qltq9;r*ZsXuzl_6F{jb{ zF%;gUpnsUDpr8+g1e2WPTA23tVFlUtkbVhjPBjd=19)jStg@Pq3kZ+UHr95k$g?ei z^~GcPZHOkGiR2f4UZo;5hv)Ug$h;$}Ez&tuq3HdhX9016cETLxiylY1GR;I;FOXKL z)Z}5^s=Tr1Qw^;6LtVU!VX&HTzfOCL@plg_gCV&QCOpmxbvZ!S9+KjanXA_?NXF4D zBv)GytjgkeND+COiE5V}-(o~hY%nf}Dv3NFhM0+md`Cs8W2Po7S;-D2m2tr6yiNH1 zVflqyU(P;W)tJJ>y(=fkKx{Ev*?k#iU}(my#qVvm!Vg#~=qc1a?1NU;=lvIsy^NiI zdzS5y{n382XR^+Q)-8hVx(jwVBz{6nA)5lO18Cwz`TH>9=unxQ_Gpy}E2;yuzp8!F z&qDwDa~RT3*#pllDP|_)5+CBbPfA;KjTRS6%vC~tx_crTBe!~k^}1YeFkq%(=CsL5 z@Iiw*&L43Lc)kTmxxLjh-I&}3PX5kN_gEyWsS??L3Pogd3Wj8UOL*}=8|HY$FB#7T zF$8KhBtI8KueSidU>^(X!kop#2oSx^7n^EeegquC9ZPrx*+V{H60`@b+J}_{z<|_X z&oweck^n%{7{Yk~^tSYJfu*f6?cN;u^1{vjXn8q7Crm#d5xppO3%pptoqj!u1;z3q zSOIkjK$LzGDBocIdkXj#(U0#8@INo~?{`BRD_VVPBYPV&BU&RPD7Uf=?VN2=3+(C?={uh-*A||YyZp5`q#(n15^*%fdQ!m9YK(x4h>~3q$YA~MlMc% zC}qYPoSsI&TB3z`rzwCFTET0|zCth2m)k@vxLjpM*8r^o&070=q?OA*Mq0V|*Hr>0 zcXFFX`kj__J<2#YvT_o{$KZ<576c|Tb`b>4*O=J_JqGQc4f4AtK1^Qgq?4w?ERG-J zE;SAQG8O%z0xc|C=4PmR)nYZ{ykgtPZhFAm-p7t!?)ox<_w^}s`!wSJA^8du+>%rD zG6wZgShd=wIDeQK^XAML>D95^#b*vM>Xz)M-j8 zJJyzWsKF0?g5mx%6E~>#tNVA*hTS6Jyxv4VgHZ(`9yD-mT?p}(nC+xf1F za=qJK3lfynbGVVetf)ptgD>k7K=na~;OOr-$VWp;lSxHhHw!(pl9j*7c7f0^b~*!# zaEcSRF2Cd!XBGyul@nfoaKb>d%`hPJMN$cS z;hQO=@JW}aE@=uZ#2(&oh87W}8p06X@=KnZ-_f*1?5vRtxcfgUroj+nMgrd&Y5Lw@ zQUCvm>)$l;uepkeO8>-Vrl+K!u@)R)LKei#dGcWk*ns>h3*(cZ6q18W{N6Ju*Da`v zJ{H0A27D`m5g{ZDSQPdhiD(;F&Nvg#yCqmm>Zt=s56EDDs$v$2R6F!#)N;oVekj&Q>X&L7Pwm+ zc-TU{yvq85sTqNP*Niqiq;5E#6k_E74@dohYWR*XR@bXhZ2c zodmd<zTw*u}28|__i#(GGVmS?0O8a;T1mM#mD;NmZ44#q7BrW5X!ET=WwICWL zTATX;GNsU_BgacFm&2|J#;42uNq?Sd!S>M%*jL26ZMxU{RD8$tB)+xMQ&<( zEqc3Aq0t~57*&LLVNBrwX*_k9j&Gh2IrXTr+4x&6_h_0Qp9U@RSzXbuAW43;dgVn2 zRRVx$LnX>sqHr_Dsyo<)tb1J^mv2`QrR*znJ63R&ZoV+1r|pS;1wj}$bqpl##Mo(F z7)1KbU82&vz&bi@o4m8uwrMHGg4GG9{x(O`-U4RTk&316;G+}rU%0*l^%Kfy0mm!{ zFV-$RTdL(E=~^}p+L#n*ERLP5xU=%JNO$2thq_&I9x=JZtqy4IA!>jhBI0ox6uWXU zT6xBNl~6dp<*s5|;J7yy4^qkgeI)fJRAL=Ax+JIb*;v=1dlqIa;a_s8=G4RQ zVeU==ennAUkl%OyWMf-99qAln3WUCQoD#lcP0A|u6jWzo1!1}hh2#8$6=jq(^DFI( zH+D42Q0x&JBYRbW%Qr!RUR)ia6@i3Y8GY*{JoVS?Ax~j}6j)G@MlNvSBwA3CYE8D_ z!P2fMr`OcK)EememPBKb*>19-{9teMB8@2|%P^oaK6Nc2keqw!Xhhr2Vw*gFuhwZO z%O`Mi%4}*{HY1BRQ|Gc{j8Ql$3%Pi0|8R=uJnRkYcIM0mUnK~RgxmV zJ~^|llBmu#zJ$0xDiW3S(I)YC;DjKn2?x(U0{^M%pxMFNofqg+&?x*qe)Vg4m84vV zGUx;dGY^bDQpZ|4?ZMt94rwJCSOcd(o2vAyp zz6}Z(A`bYa7AE99n(p?EWhVA-x9=`V3KZPe9I zY^1bLA>RaY4Gm0*3PScjGA=FNB%^w&P$@6PV%&Y;AV||OIO9a$-lHl_Y12r+M9@&Q zT}A2X6j8SR6Pho#?Cku)!(3J6SfJDW$sc=QuZUN4nBS)+OeK4W7)A;D_ZYbo@skc& z1@pbf{LTET7SP)<^dWYH{^EwT*64!Q5nj)IE77!(z|TW$Ay<+5lcOL8A+YV@j~G)s z9fxL-OLb|F%pJkuj_RAwC|B_`;;6UG@Y#W->Kh}%Ym)h>{vVmKt}o%e?3 z8B8W(S%rP+IjikL*V28=1)u-n8dQG!F;(^LD8>0cB>wjq_h0PtNCj;RBzh$7lc@F! zwzbW&hjk4rvvxni({uufQZ=>knYkxR`_-W-65{xaPnur1>t5XLEBJfy4sq1`NjEY3 ztAh*&S?k-6r)8UOZu#d=hK&(BSXKwE{z_PD7hBOB>~BirqnMsy&PGduiie3~>4U2Q z1tS%8pnY`T!pok^oWer{`E(@nP9UF8J-*4A9NxNf+AkEap}8$1E+QYiU}V)KP?^zF zId{Z6b^7q8UwCro8j{}aaZb`&tL97g7Z8}8Y|QRL`q>l;zK#s%?7ORWUz(kI(UCw5UY6Yf8Noz1uI{!Ui(G7XSb?qWc|ARc^1j!wlWqEeIHa1)v#oYB_yj%^x>O)nsRTQ zV>j8w_$mI*wQ1cjJ&RTCMs@9ffW>{lrE>e}FZ=t0Hsvjb?#RuC5%k&D?}=cLohAo! zai)JxYKJ&my#5P*mr=v##jWgXzp6WK=(gf1RV0FDhoN4xC5~;pWz1wK`;!9&F}qt; zVD~>J@1Bo0*2I1vMb5sbR`UMW*-hEjNZ%141h90}S8%en);F*O{42|;UZ^4gkiNM4 z$7;qyz)Q>ub7ICRVE3UY#0bh88~#AVnacN10YgtsSu)oDc{Bj1zX$-#4N3t=Mn?i9}WC5VZAiR9e017V65U*qSac^|6fO7|aq#d`~`>;JI{y2B~ zfXbP}-Y>~zNN-(mh-1H9zf;r&K5&1!U(7^F{s#1dVtsM?--BLUL-Bdh4~mLMZP8E~ z9Bc-hVVyNJ#;_Z>L5YpkH_Ygnmk($Jy}0yAl_%%0*JqTqWs6|uj}@seOA*W3nZwf) zzuJm@bMac|=h`EPO!O*^7BQJN#x#N!TQ|j6_gOKLc9`k!105Dp^Xn@YH=%t|G)Z+) z2VlaRi{rgEPUc>vb`u6J0*sw4qVT5>C;os3=Q`R*EOT7~w~<<@uLifi+Mxm?DUM9|eSy+EjdO5Yp zGEkt8?Fz2d6cuszD~(3wyEV<@;obR{hOAHh;aIq`tx|YSqgXLd*G~%?M{FvLFPR&D zvkEpzg}&qDwr`V`eA#}}-j}bZ3*fkDh~I99_I>S3#i>H1Aye~jgfy>F=O^xiEQ61Y zRiTT3N6&|Z-J$Cb^LXx;W5SkAKp?U9RHv#Yb49L>WrU2{uJK#G0VVzd`mJ>!M)Q5ywmyLy6w3WfBlHqxg}u&PUb1`oTcpFZ-f9Kn|ReXkmUjwhd8tt;-F zdfpMUnrOL1u}&P4ZW17tdAXN>6;F6cM^vX^m*eN(*SW?|Y#;(zJhDuoSVzdhIkeLd zU7584N!)JC_vm>BOQQ5RSu28aH={zF^!lb1`u(>iYtbrSuV^>){pk2a%@_~s+??PR zUrg{VJ~A029ne543!guIbWs;V6DH5-!WZhBAk52hp6D;CSMM}vyFy=3jAW}AY8YyS z_tm)#B>r{fF5XC}$@=g%Q>QfK)7!aJt&VA0alLgAhe!|C1&4P0Skv3FR9~~_O{O;yZTL`>D z)8|y5g)hXlv6 zW2<%CZx<7ujC-kGZ%0=)KPqzBA#v|;hV0e_VICMz#!Oq(sP>|Q5t69d&hdM~f|V;; z)m&7 zMb+1H_@LUd=i04LL?f7rEk)l!;0&^y{k*ZYD5Z^sSTelC-SbgE9t2hKaIZ4dO+q0Rkq1 zGLhXt=s|}5NhpO}f^4`g(8s`NkCv-lN!)bT?cLmH(dV!>qmffasjS79^hbDMp+;W+ zu8ZQ6lk6HvYY+YUu8%94XSidm+i&z^ zEezXtTNTjLX&bQ(3^%CHS?T%8uVIE$@OTjE#QPH2h@7glPPx!rsUC0D0j1>C*SF0> zihcXbj=ouZIN{&oIOQ@ZmS&ar%P)TVN)JM5gwUI>Al6^aI#)4I&RdUqVJJ=Vh5Z&8 zBp(PX`vsZM9P?Q2MRNG}4C0s#O(Dx_@5mK57*o@&Q%s$b1;9t>#$u0^&~U2P_~)y3 z$jhKOI=#ZNUFDIwJ&>eiN@0~_BU89!+zyo1xeR?e`5hLlvUIPQ9TK$)S{%JAE+#^% zp^#HgWSqQu6YM?2!Fz4AVRc!)7`#-N}nNQ z_sbb20doab?kk6`yG+gxyZ0M#KAb1$0eXX;9SwTS9N`CX2NmjBG0u~vpy_=k zA-pJ$VFxc8hIFXf$sSNq2X7eyh=U4loF=VJswmD=P0i(f@MrAFV66b0^Q6v7rT;XS^B+cEZ{)^wsia(WZ#)vQ9#ywR9@~5~CatU{uKx zvBO-L!4+q6&0D4OQx4q3Q~R_L!g6pY+0mEUFw_bwiOUgastoytsX^(C6s2j{;0+XY zA{YzOY4qt-#+9oFuAszShEJ|jHhP38r&3LQ#alxA{i4he8Odhs_DGEBi}#vB-8N?J zlJTFihxNp=N@HFml}Uf!Lc+O)hGVC)Z0w6bUzmgzV}JDA{`?9BXF*3O1ywBs%XvW- z{l!!&-?3B5`T2^n`Nu{qqc&_Ke8LAh4{ldOv3wiB%Mf%QQ7_JWPKoRm!>BoySsJDD zH(IJ78TtV?frk;fX);fo++!}q)eL`=TU@9NQC}4Iai!B}&<8pDZIcdmBF@3JQlUaM zS9F)gfs0U1d-ALPZR?GwM3-M zoB{tRhb#T#ETN_@r0=Nj0C4=*p(63j0Z|!kC|jCkRV(SIuttGam>kSbotj*L0VNR< ziU5CDp|Bk5sF`}&`O;(&2<_LOeo4;|Ne%#;EL%^X$Ext9FkS8vI2%nPSqT z+pNECQ*S(^FMFf3-9Tu=N9^Z45kckbM7Gr7`W@q)XB!jt4ubt2Zbo#;{n8V66F*s- zTikd>Z<S@3hrDkkqSk6_ubo&b z`o)?0FewJK1o`sKt?<-*V1Dmhy?>OGyU7_lwDwTua>XGbleJNmq1Xr}CdSQPbnucz8-r)9*CBR8*71&0WA~|Y2Vl4wZ5k!+~+a7 z7pN%X8|jk&y?b5M=bpCN%bXb`*|Truz=(WUFJ4_9kLZnP>BfT5zkBJv7A(O}K!&%U zJo?EVs2^^Q%%Z;%(KS(@(&r?SkDtf+RUE9GHmZWKwB(O819UR_b$5v1-J%<76V3vGcnZMMhcgloZA?qYi?UUYVxq zF05_7VDGK-$BW(!F64c1!2$@(k!+_4k6+`nngyhR49A>ZMl3b(%j>eqHxy?YKPody(B3HI$HTVxu7a@F3k2s#=HIA=S13Sl+NUXW zu1vF`r`<$~>3Hyh=ruJ%I=YJY1cDumNWj7tp5+?60V8r$jDzU?hEWczoWi_ zOgGaZ001bJ1puJ>?@r?XXXI9MLuh9$-!a|Lcu$@{0SX$zCd6ghw*_qxpzVic^7G?^ z&4)pG6J%0Tuj?Qlpv=1OnswbYtvuTmP%Bg{TNXBls$M8GH(P2om#V?BWc*Bh^zi%z zVQcx^dO5*ivfIh@V0zg3IC#+GdjA-^EG#49t&Ze@jc0V}DCZoMnF_ zBl|h<{V_e|gZ7!3L(B3$>w|C?9<0TVixI<)3)i!9PjUpz;0KFRO%{YtiOX7JmmpM* zuY#Bc49fn46r_Vh0Xv4;M+4i5J_4eHG>AjLT_xQdiJ&ds80p+OHa8>=b`&XHgE>5I zE`$B=uVCW?dMElMNOqz_2Kvq4x2;zBsDl&g&a}Mna9DEG^o+Elzs1a8@mGJ34ILq= zQIy!zC&rmid^?1u;F&NO#E=><30DCtg>!U{$o$_gsE;E4$Y9_6ZDz(hKP47Nwlr)3 zvP^f42RaXDZJwGR(gw4p@0@lDdovV>P$x&_7&26t8b_2(acr0(NQp$rAb0qi8D8bg z-$-5@KrW@b&p3Lb$l+Eaj!-!S0o~4A&k4dbj47kcnWv-7(MPr7D8(Ygl9gqS)||EK zplC<+7DgWKV#tHWMt?6Ff~hz}>VPOjRvI>afELim$nt@}VW4=F*-sP+1 zzO319+d2`j7OVY3+W&Gp05Kf+Kf>*`z|74 zQ;Oxu04}GoRVi98ScHmYQrzgcX@$D4?eQKed9kIdoFkItvGu-U>Kfh ziDGH~wyIO`XZU1iSz9EYZSG;p^1qrfqpI@ms+?HId-TRnAz56vFtVXo>;s9rBvRkY zzp>^D`VeOP_W#xnKSaGr z$#m`UAAz{XJKj;rbRG2(Z=@cW+*#Fl1gGAqfXSlNI&5ELyvjhL?AYhg?i^*bV&6jD zY2o@d=zPXJJ=(~8?$GY+7rU?0?lea7tqtQ{9mu)f^^o$;4TXXiQt@y_d&*@h+}Pe@ zVqr~e3Apuv6Q0&QyAJA|yMX->1u$5f*cKp?-+yRrFL!V+?i{~edG+Dqp$Wk=5H7Ff zF>eAJWXX7CQmJ7H==A=+a=E&W=~Knak3LsZI(!C0=P}7HC4r9BUY$RH#WV?FJFU60 zwlixnfA#F`^yr*FfW)*J(#r0v9<*Ard&0U#-CzYTCu=)Due8F#tkh0UuHgjMmeEF3 z^Uv$#Y1_HFA|@PNUP@70VyU62-IdqrEU9nmXsF*&(bI^DQ)r*=Uc7qj*wo5)5$G-L zZbmoQ>bq{n!ct>hN=5prYKvweDYlxpxHvdifeY5c3*;&&Y3XQkF!DJJeFJaCO)5sIKgqxfD=dki%RX-BCS(aCYDBL?Bgr z|E!iRT)#L2N1p+3**LbaUdi!=~)l zwRFG^nkz^K&|An`FXyu_9ov zT2}{7`acTF-csH+wsM{mXifRN`r>RW+$f8Waf9MkRBwnfh{bB>u^;J2+ZoiG47O(Q zVJ?rdwZ*v6{mpj`n+0_u0@hdDrrg&cOMB>KF%c-30&HX142e@zSU)zhuAfnRXBQ8w zEi5h`-FakYT!~JbS~?c;EbgMCIO@Ndm8RQQFt3WFs$|}PRz~fbIXYdr*jSU|P{3bE z6DVvHmfD31RzQ+v&ZP3iDSW#vTtU@DmIcn@16I7Ebh!7Xc)>SY+A?AO$u67br{O@% z&#MY9tXssL5m_AZ+T3A`FnvsclfpBvCBFF>w@^AF)_FFD zoEDzmGIMSfxilfh&>ZnnZFl-)_q($$S88UV#LHIck}4StW?VMcl|qN;Tx_BUwA{_a zn4KGm9@vbcj%RdP04H|3uDWZuB7t7!!d8^>j81b0Xlc}q-_fK|5KjiJf9a^${4rh_974grFcHBfYE^~g4pl%FZ0e4su+T#%25_74T=X-;3N$-5C zT*F3Q+cz#dk}$Vs$yB*e6h&q4pjMRFbc9bOZcaw@a4pQrs#XrHZb@9w7i^zGicZzW z7N%pbR{8s~j|;VwME*&j0apHynm!vAWruRq4syTzNLza*|Krqy(r_y+G#14`seI!G zhO*f5_aC!nNfHeuD0G9qp^UOfTSmu(EUP8sX~{7{^|S#`gi60^Z>LdZDaW3+ie^=K z@4~phV2Ww&{_{(7z*)-FQo%~xEO9k9sys<*d)R3{h|RN$qm*T2{i4txwza4FI3)}5 z4X^o#q#5%v4z7Nd2;ov@mLih09lY53weI;6${R~oPU>1M^jbX$DWvpR){1JD7K#!> zg_BXC@=Z(kZq$Vi*ItCvytWi=G3o*m$k^GlikRwD9O?*TCk*(J(L%inOpna;ilGmN!baEfRxDAm_m)GYDcgZB+g*bD$bHt`s#v& zfd#vNc!Y@cV1zZK<-u|=!kfxKF((Igr<6+7MXr9OLaCe;#28$h+r|%OCqvl46`AV1 z!$4N1@fiGWyGnt}!Z0$%U>WAHG%0$-iTnrQYyLfEQO33NJD6LN{f&bAPv~5EyONFJ z=sn4(%iCMlz<(AYp`gZp;Ntr6uWju4P+(p})C`h7tx0n;>ED(WrV5M(weg0t2GcT; zau-@5oK&Pxnmh58T3Yn)vJuC5&*F<%w{@XLtn!N2JMu=3J7>(79IG- zt0LuwN;yH({Ap!pPK+^Vi;1q-M20$Eq3H_LjmI{@aZ(Q<8zw2mwW}iiBK%#C4 zXupYgSBKFKq=LeQ^z2X7LynJD$cLU5yJGitf<`G9>IL^JKH`JsPvrS;VMd`LvqDZ4 z5R2@+sdlsL7`@9QYll`l*d@b(W#u7dBy~276JjcFfvsRb*~*)6EZI!K-e~>FCyXgq zLnjpzU4ycO?c|-S2AVsh<6(nHexid(xKdwrLSioBx@70;^$bqi0NE@Z(_}ALv=hra zun88K(`zQ7ITTaV9g}FqCJ&uCL}QkZ*mYveK++#!LfIkHkTSidR2ZnPb#+X~Av&_i z$<7$HWLJ)mb!3wlojG)7>YlIULj`nZhCrOD!?6p0-HSP-J;VvZiN@Y9|cB zfk%^n?pKHG5I@V;BPQJDAr$QYG%Fr$x8){Tdk~o%=rk**r zYv)fQN_=Q6*&P`&xZy|cagSK>nieaIq1Bj{Nv9qkTo6!)FCgJy*(15t&K*@d&g$S3S#6|IuvZz51?6Hf;{{bgnx9 zE~oF%%gwL{vP*4q4nW|13S58?J(HFj^SNuPL^vTSG5p%WLT?vMC7aOXd?RfWFw&z( z@0Cq`a?GY(orLMw61Np$t<$68d?r^)%|u^RMSFBPbr0v37!4MWaBbL<7Sqwm5oBpl z$jP`3iQquIa3|tA%Vf7mw$))}@FFm+I#gDp&ZU^P;5!{s^ z*MF_w(l~fy<$BMkBVxv5hJOm{3VI4qFO@8jUxvOCmZ!`PPz?Pj;A^b{O z&kF?S&ImQ~4;q*&qNy_vdNI{n8_}ul6|rI$C7_NjnVcm1Wm@I$ zm$6he78vI2FWZJgU}VVWbYG@GP<@~-5JGeRjh?w#+`mX}aoK5!qZmU|!qj`(hn*F0 z(d!Mc(FWQ{45IdQbyJvSa29VHrrU*(PDwW@C=U_I=}eIgEbkqL4UBI6J@XM+LL7V8 zQ|lNk`DO1zYu(KG+ha+HWs-{8!k@;4ym+SF1lI>Cu*SJWoE~! z^sGZn#gZs`YQa!2>dQcc8|tBh$gT@U|9xvZL)@9tA&o~sHcq6_YXLDw!^$G2)5_yc z%ZTtY^Bj*U{&LUK{THv-eTS}M7Xe0?nKjyo_Cv#QWseOFz>B5GaGKI6u3Pqsd2HA7 zK9HMHE+Uzj&CtxZ>BYw-B30GZRW&sh)m7G&4%KXVJYOwr#AsDgrj&g0c8oy!6|hVX zDNPLx#g34FYW>4!947Orv`>mk*fg1?EWs&~opaj67anw8@XL)ke+Eakhy|?w4Ut%> zrO5i&7qn{X2gmxzd&*8BVXNI4+2ukinyy@DhiHuDLBRCQ0r`ZP9z9;~tmjSV{W0$f#E!QH7pQ>RAVgK^w`< zU|@=z+2rD0J`b#Q6sxV=8c~6rNf4O@iNy@hPGBGhg4M{5CBq$qYoFUCz8gg>h7_3h zj8OS)9zlWn*dWN@RrE@E95H9)yaD&_Al;U&9w2ZV{gMj8mWJTACp0%!JPPFIy{l|Z zbX4Wm8l4WduVBBzvU%K(+yeHm_F$56A9Q8+EzGd4&cVQPE8i$4`N^&%*AmHQe)br2 zp<9QoH+ubbP^EMV*iIpOBBoT8HBvux=!Cb7KDD6Ldaom-eFLU63QI()M2x4_g<{$h zJ}vQpncfYKA;r>HfZC__*%FUvjuQWP)AX~ko-;wB49j1TDoV-`XW?ZtQGJ5}Y^b{F zS3^eHTv1L=rK8+{FwduJWlB8&FJ%XN4y&1!6W3adCT`>49Q~*W^Uy=%J*FqA5^Nk1 zjyF}rS(0(BoYQdk^V-#`!udvR8~W^Y5YM=i4MSvsy?y56>&Ec_cRuIMR-N}2#hV=z znby+j4>haDSHfSYY?|CyA4)0?srJ?;ieUc>TX{(sjRJBf76qrD1d5gbCOJ4;TeZ7$?~aei zPmemFShmg&EY*B%`&S|(NguLlCyH0|49ja>($esy&t-bm+ZHOGdRFt{y_%4I7=cD; zEc~uACtTH2UM+egMf7wgg$cKL_K$9QAoK=;1867k!~44bu1SL)E;KRp+|Gxb)nE*^ z#CPeIjEvzkX!Zl?a1qWgwjV(#tCqOn!?HiJEJUY8Xp2U&RenLXy}t zaDYdtT+|s}R=&IFR-`gtTCsh1w#2+IQ?-{0f>#;TwUsRu$qZR)xB;<8B4W+N1#kti z+d_lco&)ilc>(v-8WOQl*wn8eri3rqF8ui_-PQe%$w|loT>Wd83~yOAQ2v?U<=6*Y z_Xan<@KI6~?+XEzA(PP45o)1^j~1tmy%^G$FC3{6sw}|YaeCMXdOUcjGGcw0eG4Y~>s*nnP~N*-;SS*h4T7l~(jfl^)>K3h(q} zx=|ELy8dKJIuc$ExQ3?dp3L_40d4?aF)0DuuUjRozGz%G%a@mp0vF zEc?Oxq(EL9t=4J5275&2YafvaNp^*0)3!XdEVd+O$VxCT*_)dh|k!3(c@=+ z&6?AYqAguaUS`JML8;WDF0;HHt5U zV!3{DcTCbgCVCv$L27qY$UTlUm{}^>hUs}~S@d)KyEug4`Zl01V#EGi#nj~5DDenj4zw0RgN zh@A9zqyP=bF&H2Z)H$d(80O&sc>aqx$f=GPK_*jd;EWC1t@mS#j@W-~r^&TYx2LxR(qmYo!pZNGd)+ z9vR_-6;=31A|9A(0R5SP`hcSOxvG!b1v|ONCJ$p5r`QEy>mK?3{o97d+rxe((gwI0 zgt-OW0>m5eePs&TLqc3Fy(RPpwHr)b@(hyH>sNTibYYK32Uj34RL>siQ$rt;MLhVh z=e{1&kqpx@AefL;oHRCc(WG6-O}d9qauWifnW+egLh|fehJkIY(?n4;4~UUgNBXnU zi41if+}tF*ye%)3{OJ_5Ck!QRP={t&E~LY@R+uNM?-RHJbkSkJ(~ni=X~b41O(|Gc z&Uk~Lg&dIhK#T<0f5MFg=n9$uK8LWaC#Xf�Gr>s0Zg9q#s&w1^Bb^rv^{- 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%0Ao1;}iOHhdwAx5eu8>3r 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

      t3(1hb3FHT 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 ArT_o{ literal 0 HcmV?d00001 diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/android_icon_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/android_icon_16.png new file mode 100644 index 0000000000000000000000000000000000000000..08ffda85b67647a3a9871f813e025a93fcbab9e1 GIT binary patch literal 219 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`i#%N%Lo5W3PB0pn4*FPD(XOkD>*u9SLsJ5b5 z+J2prnF*;8P9IiGY;3E1>oALd(S4oE=h%Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_0004iNklHEU?j7kddOXU^OdcV{bAntYoKTp_E2eSXf$#Vlyd1v9J*v zmTERq4DXpY_q}5=%*-=m6sK<8bMHC-`TOsGm5A`4@D(RFyYnuz&gi206+uK05ssI~ zOcQE4d=;r?(zT6@9(IF^BFn%K{87=}UCOp{KhO9p?j^8q-H1O5O29)EzETA%@H zD=<$cViJk3D<6Pu+ep^6($K5%0yF)qm>HL9h zo!jNvX^jOyaP3FUCI2xG<}1lDRW%%lXt#e6fL}>Y4bhb5GY~{4c+SrRIwO*;o`3|C zNxtHw4gmBiPeXMxQNTM$;GzbiSjYynj&ZjbPFxl4ulHh~#vR1VUa^9sZ!3x5EkzX} zfzLqK1c3fD0BK#?0#%|lK-mZhV-EKwK@CVEC)ipUi}vgnccF=-oX#{^00000NkvXX Hu0mjfO6O;W literal 0 HcmV?d00001 diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/broken_pkg_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/broken_pkg_16.png new file mode 100644 index 0000000000000000000000000000000000000000..6daa67b87aab855155e38be57d1318b4eef031a3 GIT binary patch literal 281 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qucLCF%=h?3y^w370~qEv=}#LT=BJwMkFg)(D3 zQ$179u0IceiaI@A978Ppmrg#-+hD-sY|ZsM{~*`h7S0BRnARE{?~qrGt7^+0864aD zE!TQ#Zu#ld{(j4sZ3pxlIwdq5L)Z&uDrhNu%d`w+*yt`VF2^9cbmhs$003=u z0048K008{m004tz00404008W0001yP000n>N2bPDNB8b~7$DE;UJt{NMlp0SZY(K~y+Tjgmi0 zf>9L4--sHFRx=b7ksP8$@Dun26dc>6xv5X!;#Uf62pj}yigap73DL>LDN=t&o~y6v z&HKQQ_nzlG=f3}L6sp$qLj_)-3rX2w((o^g;Z}CI7J5l~z~5ZIY4g7onZwNS%f+uy;R|9VP~{@CkPJ&9My<1DDW-4%B5IQ53BJL8?ve literal 0 HcmV?d00001 diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/device.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/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/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/doc_pkg_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/doc_pkg_16.png new file mode 100644 index 0000000000000000000000000000000000000000..a2be37af0f8c0c0f3eb82f7c5c96f9064101afa7 GIT binary patch literal 290 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#S9GG!XV7ZFl&wkP*5S+ zBgmJ5A-9x)p?CoU!=Jkh49zJF3vBEC$x;#$5>knCo;*om0|7N5o$jQBgcZkMGN~NsaD8LX%-pQZ%zQXb zL;jtRTZF`cJq4|ivWFSw?l%wI!ybHjenRtARtbh>j?dqpO-S0)@Fu6E*CN@l* fbEZXST5WJ6AArqvrOC+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/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/error_icon_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/error_icon_16.png new file mode 100644 index 0000000000000000000000000000000000000000..ccb4d0aa9deb0f8868975f5b76d319eaaa86b379 GIT binary patch literal 626 zcmV-&0*(ENP)Px#24YJ`L;(K)#sC1O1(pE-000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iXQ5 z1r#DXW@Zrp00HtzL_t(I%VpEMYZXBh$MMhXeI+4533h^534&r{ZDS)=B0lg@sc7K? zNsyquSokPJNMVseeDvZ!kj6r=6x6~_&_>Mey}Nh!-d&5Gn-mTl1`addbLMw`6PjC? z#{hkF5l}m7VpD0Na9r+_5k?rs$mp8G0_Gy5iv+cMO7nV1tl^9rQ&cF?n*nB!`8FxB z-_%(p3N%ieI$+}4?4n*CVJbQ?6%~J z1q?jW_gD`dmv!uw#3#l`geY1PsXaH$ThjGQ@CF|Q-_aMWY@d%S*%qc1r>}5ZiqdoE zY`E%`+9Ne;*VUHQXxd0jFo~+>&Kue*mCLN-5-zE{Huc-YqT0;}i+~bi^KUaH2qtk>iGrNYzqLq`0NAs!^b@ZY(vnN9CHT^+O97A+st) z>b=Yl*^W>~$ZaXSG{)GfQm+NHwmf9$gcO-)wvhe@Aoo;ZqO?!xf^r7PtjLi0EVUVR z*jZWhe(tB-J2?vX6+&oETGsbd&p^jT9dC(as^f&JDD)VGgK}SDSEoXA%tHeYqCLkF zP5g~5WCp0>zi1~m7q$D?HMC3RZ}fJMAP5>v1vM%`8TYn}JZf_IAN)Ge|D1L9YXATM M07*qoM6N<$f+N5YNB{r; literal 0 HcmV?d00001 diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/extra_pkg_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/extra_pkg_16.png new file mode 100644 index 0000000000000000000000000000000000000000..7ad8a669170bef10ab86adcc930e79c2338ad449 GIT binary patch literal 540 zcmV+%0^|LOP)(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRZ-qDe$SRCwB?lD%#eQ4B_peecX$z`!TI+%}r5)o?!fDD<2cl^MC z0Bm?;RFKEc$A{NLRq+VS1NUHDu!x{4Bo$NycW3$OyR3Hr$o~E%a#d5z=!#K_W9|SX zRdQCUoX9Crx^`3>Ye3G4x~_;;C>A)*t-CJ(IQj4d5hbNeU1tcp0KlzKx{kK(I9;`z zo^|UHe*anFO@$C73Eay6HUgL#NTKUGZr*uLcrcw^2H^JbD@28FAD>bs>1)pe!VBR_ zgbh1B7XcAA)Rk=jgD@25%i9O6&I*SQ-vjXZ^*#2Q%(ST>tgpQYa1=-abyA{AhzI~p zm8g@Fq(6)p+}Q+#3aCPrb+8Uj-rWaa+Vu8SSy2U*ai|Rd0;)nrQ4wPx#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_0007VNklY%R#<_D2o_{e z1VI`G9U_y0@=%CUq@*@=saekD=JwzAI?PsG>fnQSd5`zy`~C91N<>&q839nr0eGjH z_xhKr%Fv6f0BqTJN&Oc=L=ZR{%{PlU93puoHvNs6mI z*gZa6bv|4v8=D7aalT)q{6`M&PBczuJcw;rhnzfs8Xkek8JPS6?YEG+%kW_gb^j@R zoPg{s$jJty`rAZ_3R;5`I-23|h4kjl5bF4O2!tUk3(RIP9WxScFcPo`x7BoYaT zgq1)2fv<{q-rC;)%L&o|u@ID4KmmFJC4!on zC$FIiTUR&zks!)!g1l;WH+QOx1YOrbkHW*((D4`y8e%db$s}570jY08%y+etXSMRg z-C%gseapm1P<`W+V7Qv+qi0Q6ivB}SBT zRBkPY!7eI9Ln$CNRVy7=#DCFgSpStYiTe04wmw-aoX@zA{XuQeX%{eIZnAgRTLH z#L_I^4kMZkCEx(OD>u&(8UHgYz1L1>;%W)Zvi?&Ll@h*Il(MVso>Hrg^H1D49z>nH Ru=@Z2002ovPDHLkV1lmhI~o80 literal 0 HcmV?d00001 diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/log_off_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/log_off_16.png new file mode 100644 index 0000000000000000000000000000000000000000..ad2edff61586ff014db58cd556f8c78128de542c GIT binary patch literal 1218 zcmbVMTWr%-7)0PbcLaJ1b+ot28V$v+z z=bYdF_x*QIXL|OvZhw6{K@hE}{c;wsJG`g43BTWbu&Ust4JGoZ*BU@2#eqajvkDNT z3}p~zp`wi*`U!RtL}O9UJa*StQqZ1-$pf6baT; z(2z*3JC#poK-_X52nRy}m1Y>gaY3dd%yG=SfTbChqVdfIm<}P#32X#xTqL&UXaho4 zPHx!3Cy^{7WD68kDwTqzP|$J)DTe2HkAr0cSR>$$nn)=JOt-zxAVXJmbQ|fG2|Pxn zV2vP=#GbB)VA$#OCb8*mBnqdDDl0a{1Zm1Jytvl1U6h6Yy0N9Un;W$um4&V~;;6VD z1MPJ%&fW73d5YK@;XOykMNx)jOC2$wiBhsi;x9o>*94xUBauX0W_Xt7q6`zIqoF7p zPRcUJOA;HYb8Ll`q7oaEcVt*6r%p?go~7 zE>?&;P(hZHv#jBI2V{yCvfQF&gLp3o{AoqiO;7NSSD^KHW$5T1K`rT623X6mpl{&; zAN#M!*Rk6F$r^>7p}h8Zrd{eH+=JeBv-$C1b9|tQXU4(9@#wRuTX?W)DLIxa-!81I zu68y0CZ;PAU2FPgqB)sN&ds0W-OLLo$yN0{%blX>b2f(r4544@GoV|bR{4dpWXBTU` z{oC$^x}TiOViJ%>VZFhmEtBZ%@5F-ZC}Q xU6`-^v+vZ+<-;!*^TP{oO_EE|hoM)xCmM(s&*T^PjvC1OtE3V=@@1+2@Kdy8oCg2^ literal 0 HcmV?d00001 diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/log_on_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/log_on_16.png new file mode 100644 index 0000000000000000000000000000000000000000..ed27b5204cf6201a34dfeb1c1cb5430d093d2c97 GIT binary patch literal 468 zcmV;_0W1EAP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02y>eSaefwW^{L9 za%BKeVQFr3E>1;MAa*k@H7+$tiu~XJ00BfvL_t(IPtB0MN&`U@h4&GB2qDF$5y6)* zMyvyijlO|JBq`DuEG!m8H@I<8aQ%4!OT%gv(pUr~3-JXk6h*OdJ?AZB+Okf=;VRexJvQrr;aW|gS2RqY0j;y6%?}zE$8{56gn(Vt=E`MR*=L^% z1KRPdzyN(Caox#t;S-GD9jYg+dY%Mu<+;0$Crtgsa|YPJyYFAt-h#dDiJQY<>vNP; z#u7lGaRzknddm;#_2N7BFAM}X!>g6sQL^-i{S#|xQemwR>T2A4g%?MSSAu;@;hjXy z=QwIg;OS4j0T$Hww_xypY$&QZo{iLh2?c&hNgYMDmskIT9Onz+AklDQL70(Y)*K0-AbW|YuPgga4sKCl6Bnib)<9EuJzX3_EKXmY zZ0ODGDA2m!vR@-Td%n?B7RM&v4V>G;o}G`psTO*{?ah%x-L4+0D}&gMoO+XY?(><5 zHBNfdSryIvEZ_XD_?#9NGBsB#>GZGY11|z!omcjB?th=BBKu{j9Lt-xm%N3dHm?k6 zs;m-Ndo|;z;ohtJ*0fv9?o#7CV!fe$0h57b_byfm=C^(CSs59=U3H$roV@o#E}y%4 zYM}swLA{`^-F`(jsk5sj6!=)vbVOL6SU!<|sGCy))aNFsGd*t3^Lr(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRZ-A4x<(RCwB?kwHqsKp2I;jLD{<5wux&1UD8cJ%HA30=+hJWV0|Gj@4V+>krfKy-!MEr~mkN{a6$GB3obxd_P^|Z*^Q(=cl%h?9#~OJII5tiYbay~#1vXmi z_2+-~Ja6Rtoixvn!z!~<*Fn(T6-5yOIK~+MgZBXdSU^Zwr7)E900000NkvXXu0mjf D@N}W= literal 0 HcmV?d00001 diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/pkg_installed_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/pkg_installed_16.png new file mode 100644 index 0000000000000000000000000000000000000000..70295655505c76c48f1f42d1892f4516324a59b6 GIT binary patch literal 563 zcmV-30?hr1P)(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRZ-xk*GpRCwB?lg~?3Q545N@7}3*WgKwPe;|?~)-r+?gAi8Bf=s22R@lOI)*@#% z>Apon(IO%zTe_)?q{vNcp=qHTor;18$>+QCygyv{Mw}W{m@izooO{maa1ZAjc<*UN zf)jWJ{1dxQ{3DPI65 zYPH&E*K)K3yiSX^fIE9L@XlaOLLA3~-IeVs@Dv!SbZ5>BAugW$lFS0nfquXOL%`=f zfU}0ArinKH0%{I83%u+3YVyDgFb#|V9=HL_H>|RE4reXfNdhnq{02?~Z-62&ndaYH zXsbSJ;ss9-fRv)NnEUY!7yveaGr;{c_Yp87rFhWPOj1geQm4W&eEYur`wDO&g#eU+ ziH{w>ZY!lG|MNm!^#$3dbLq6N0Lj0Poux860{}T+womu-mMQ=M002ovPDHLkV1g~)xww({ zkb!{P!~TwWvs7<1UK8S8lEmUKRM`>|rx4V(=rha00}i`Xyc`dE=K0n6f4?ugEOXQH zz6n81!ci@(X)bP^hWl8IcRbjpkn#GohHq{g*K^U}=fBKrlpge0@0xm1{>AeqZ9VDl z<_oglthw7%&SH4XH^FxAefJ;S`_g5Cy$-vTwLE8daYXuU>@?*xpt~46UHx3vIVCg! E0GTpe-~a#s literal 0 HcmV?d00001 diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/pkg_update_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/pkg_update_16.png new file mode 100644 index 0000000000000000000000000000000000000000..4171ba639d32e06f83231660b77bd6262574c281 GIT binary patch literal 553 zcmV+^0@nSBP)(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRZ-uSrBfRCwB?l09e?K^TVL-I?9n;XHpq2_(dHsguG=5U~>NL`1N#@X&-v5K^Sd zMG{haV4*R+h_!;1SfzkG^lsuJvkD zK<8dQ`}w915GDxI0Dy9jC^D#?b+ESEFd{;lrey;Wh6oQyfe~0m6dS~G)UCQ#1eulr zDg>250~G>GUPQNUy>r>%SPIPicRSmzw@iy_u68#O3uY~>a;WChOU?0HMARw_1S-<~ zRTY%y6BGIP`XBduEVLLC5J(CD7@@^;8;AEu?#N> z9xNUTg0a{&7S~?A*#9wfZjCW;&f!9I@$sc!mp40|>=Tg(kw>SKeLgs7U06GaclGXO zZ^Ntu`9L>~e$h-ohF^@t1c@|!PK=XDoFHPWZs*;$PCqPs@RGad z`9IJ9_xrzJKH9mrp>BH}000f44z}A^ZRT5RHNLIC>722&sDVDUM@p($PUZoBLW=QV zD9sJ<-8`2VdHx%|9RMszp|?-%i-gIzly-2Yjzddl3^V|=Q(A_L5AiA(;|B!M2Q8j2 zK%kKDLHpbhB$8qHL7`(*=6gmvd*h=+aUuaxyTNviGzikX%7I!sB`T!mgI0J+V{abA z5V)eE4*8%}r}`pMkdb5_bUP4x9PuEa2X&xMm)Glk8pIG3fe{QwF+1udofwHA;MxNj z*5pKz>}G>&whYM!4XSE}gyC#9>&QACk~{#T1VNY_7-ly#?8=C!a++OK+NumJuf$~` zqY9D;nv7gb8diOf;pu7!=}aWDPAn>Gi84|KYg`6K9SEFGn{lmZD{43YuNxa`E4?Eb z9`5E9X;_XM^+>i=!A9=xZ^%?Mydk?}!6*usVx{w}CNM?y%D2#O;p>UAMr z4DsM7iem)M&@@Icv^z*MSe0W#tjo_jogTvPW>FLkV1&!K90LR0WTXsgR61VF3f}bQ8Y*s3`@9iXbqeAKRJU9XJE5A{?jbgBclV& z?Rx7Q!us&=qA@YDF&ZtGAEf|blNMtAz1r2--?~m&c8=!{(Zvrw@m46O(>YNpjSoj9fh%_SozQ2pPGHO zl`1-D>z?n>Bd^XUCo8%RTb|f?CUskHn%t4kO%$gpH_Dwq zx6D&Bg;R-zny$96{?^;yW-s@ZFBQM|YIAtx(=Gic`p;_nD;>9HTv4U|WbJ9-%Ux|V z*J>N)K8qgr-?TipHJobw{Mw*Zac?80rdsxlZvuL*oKC;8w4=-Xw}k?o>>2vN;eP;F C3*VCf literal 0 HcmV?d00001 diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/pkgcat_other_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/pkgcat_other_16.png new file mode 100644 index 0000000000000000000000000000000000000000..395a2403fe28aaa2576b530a2037ae15c5114858 GIT binary patch literal 335 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6;>1s;*b z3=G_YAk0{w5<>&kwUO-P)Tb7FbPbD)r9iEBhjaDG}zd16s2LqK9?UWuNc zYluRbv7V`(sb|-p2S7zTJzX3_Ec}xb7O3smwC|O@k=n^O&36<3Uu5*?(QwYo&;IlO z{aZPMf1hhE|3Cjd?BD-_x_kfA-IxEV-?3cwXsChsB8i$}@4x@+{#;W|NrBsJq~HG&52Y`da bOPh%yqCs3Y{6d^8&=U-vu6{1-oD!M<4Mv7} literal 0 HcmV?d00001 diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/platform_pkg_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/platform_pkg_16.png new file mode 100644 index 0000000000000000000000000000000000000000..56e10d0a0c1c85b7bfaf3a02acf93df135b80487 GIT binary patch literal 2981 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 IM6N<$g3iIZPyhe` literal 0 HcmV?d00001 diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/status_ok_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/status_ok_16.png new file mode 100644 index 0000000000000000000000000000000000000000..ae61f7df993c36e0c20b51d0d7c1ef7d9d2800d1 GIT binary patch literal 3277 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 LNkvXXu0mjf76}mt literal 0 HcmV?d00001 diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/stop_disabled_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/stop_disabled_16.png new file mode 100644 index 0000000000000000000000000000000000000000..721d18410b2e61af85381948ee9ad349f5879a82 GIT binary patch literal 809 zcmV+^1J?YBP)(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRZ;uSrBfRCwBSlTC{pRTPGw`_a`^T~*!b>4bcND8a3e!9*A;5+xznaUsUIFoMR7 zn{2Xj#qWuS$-e2R$l@Ivy z!kY1avFKv^xMOYteB6-eIgIo9iJV6cWOU zP2;3U;{=031p$OmmNX6=#CCfCG~CCSc6tzvJ9+2f~S6P zACw|GbOeCbbMu7B81atm`31(m{(@F8V004R= z004l4008;_004mL004C`008P>0026e000+nl3&F}00009a7bBm000XT000XT0n*)m z`~Uy|8FWQhbW?9;ba!ELWdKlNX>N2bPDNB8b~7$DE;UJt{NMlp0pm$TK~y+Tb&}0X zlu;PPZzVO_O!*a+B_Z^IVJY^36tf8;C*G4XRsV zc{#qvHo&PeMMl}A!Nl1H#lx9b6kJ<_sqtvwe`B-+_in)5iO49Mis|~t=k)du<+br_ zI=;^zz^_F(D((AMxG)eIWfOf`_)Pb2!ooK=u_^xl{)Uy6Xt1;d@87_q+c5JgbW>U1 zJ?s;Qb+*MH@Zw3mK_mXC$?7UxKOcTm*+jP%K887UIn2I+R`6Urqnck?Ut=+hf-ZiZ z&F&VM`&jpy$|g#>M(?60^IA2#Tj1r>y3ZM9)j|)cQoJna%6xn$CmP`G`!!m(*BNEw zHM&W=3gtow)5BZwjOV|=?AsWgHGAx>g=;99Y?jb2L&tw;&7S6P4MmeS8Meq+YNBh_ c)`e602QrOKrQOEs{r~^~07*qoM6N<$f*c_pp8x;= literal 0 HcmV?d00001 diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/sysimg_pkg_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/sysimg_pkg_16.png new file mode 100644 index 0000000000000000000000000000000000000000..942ce47a1bc4018bef51c2d444d526a1c7a3c84f GIT binary patch literal 1145 zcmbVMO=uKX93Nd9Bx(sL)>PE#h$86Dyf^zbGiFV8=ff;!W7sTo5f7Q2d7BK$d^q#s zPIA#G2-1r=`9-0WqUgbcA}E4UEEMq|dWuCbLK82gJ@isbbbXWTrac4?9hiCV|K9)i z`+v{*f&Rmr+P`k6C~8xoS1FOTBYM}jk?*d*8ai1zv0A}{ZVlIUA5l5ets=T$>%*vo zbaUdwQ`ALKv6@w`;7YMiGF+S0BONxhJ%XmFuI|v&jZuW@DjK$&Ec54s8HTpZEc0!u z2#TJJMy%dR9}Q0SmyO9$Lo}K0Lv&Xt5ds@woeu3WCy>G{v&1WreRRw*^pXl5%`(eQ zRf+?&?D~jKu^?`MG@#Rvg^6S)lQ}^100Iv19OUCLBPDnV0J`}w#F}r`q>`dFZ4oKU zj9~0Z99OT`*?NL?{b3G@q8M@Te4J>+g9!)gVcZFJw-^)@7{29U%XR38QLnn=ILi=E zmqW0CRY9|R+4?BW7jXc z?pUh>21Z=$1|zOV%Y!Mps2i3O$x(Thqg9Y^okXVQyEeU)U&&gfy7xTrb)(=4qc(t*)-rS*xhGCas36XTQ7*njnINXF_$ zLCKZFU#p9aMt5w_Y4peI`&&96{oM1-(A3M{{y97S#pSc|i&NF(RNKXe-z^T;YHfEK zA3r?%;g|R4rhfu+3vbcfR~_q&j_o@R9J{@7X2YdQrL*!%eDwR9Q;jv>Pu&#Qz3np_ zdahi6?A7V_``EufyyLb#nY}lrEo`1&`{R?j=WiNA8;u0m{AFxQtZ#NF+1?}P m&Fk9MwKpz!$F3z0#@2149{2Q@J5+T(`WY0|e&tqv=*%Z+ymy-b literal 0 HcmV?d00001 diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tag_android-tv_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tag_android-tv_16.png new file mode 100644 index 0000000000000000000000000000000000000000..8887bcd36302b18de4cc74053171acec0e584658 GIT binary patch literal 2948 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 Y08CUp0=SqWX8-^I07*qoM6N<$f<95e?f?J) literal 0 HcmV?d00001 diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tag_android-wear_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tag_android-wear_16.png new file mode 100644 index 0000000000000000000000000000000000000000..652c5efa9ed19578c4c4aec5107a3928a1b80859 GIT binary patch literal 3115 zcmV+`4Ak?9P)X+uL$Nkc;* zP;zf(X>4Tx07wm;mUmQB*%pV-y*Itk5+Wca^cs2zAksTX6$DXM^`x7XQc?|s+0 z08spb1j2M!0f022SQPH-!CVp(%f$Br7!UytSOLJ{W@ZFO_(THK{JlMynW#v{v-a*T zfMmPdEWc1DbJqWVks>!kBnAKqMb$PuekK>?0+ds;#ThdH1j_W4DKdsJG8Ul;qO2n0 z#IJ1jr{*iW$(WZWsE0n`c;fQ!l&-AnmjxZO1uWyz`0VP>&nP`#itsL#`S=Q!g`M=rU9)45( zJ;-|dRq-b5&z?byo>|{)?5r=n76A4nTALlSzLiw~v~31J<>9PP?;rs31pu_(obw)r zY+jPY;tVGXi|p)da{-@gE-UCa`=5eu%D;v=_nFJ?`&K)q7e9d`Nfk3?MdhZarb|T3 z%nS~f&t(1g5dY)AIcd$w!z`Siz!&j_=v7hZlnI21XuE|xfmo0(WD10T)!}~_HYW!e zew}L+XmwuzeT6wtxJd`dZ#@7*BLgIEKY9Xv>st^p3dp{^Xswa2bB{85{^$B13tWnB z;Y>jyQ|9&zk7RNsqAVGs--K+z0uqo1bf5|}fi5rtEMN^BfHQCd-XH*kfJhJnmIE$G z0%<@5vOzxB0181d*a3EfYH$G5fqKvcPJ%XY23!PJzzuK<41h;K3WmW;Fah3yX$XSw z5EY_9s*o0>51B&N5F1(uc|$=^I1~fLLy3?Ol0f;;Ca4%HgQ}rJP(Ab`bQ-z{U4#0d z2hboi2K@njgb|nm(_szR0JebHusa+GN5aeCM0gdP2N%HG;Yzp`J`T6S7vUT504#-H z!jlL<$Or?`Mpy_N@kBz9SR?@vA#0H$qyni$nvf2p8@Y{0k#Xb$28W?xm>3qu8RLgp zjNxKdVb)?wFx8l2m{v>|<~C*!GlBVnrDD~wrdTJeKXwT=5u1%I#8zOBU|X=4u>;s) z>^mF|$G{ol9B_WP7+f-LHLe7=57&&lfa}8z;U@8Tyei%l?}87(bMRt(A-)QK9Dg3) zj~~XrCy)tR1Z#p1A(kK{Y$Q|=8VKhI{e%(1G*N-5Pjn)N5P8I0VkxnX*g?EW941ba z6iJ387g8iCnY4jaNopcpCOsy-A(P2EWJhusSwLP-t|XrzUnLKcKTwn?CKOLf97RIe zPB}`sKzTrUL#0v;sBY9)s+hW+T2H-1eM)^VN0T#`^Oxhvt&^*fYnAJldnHel*Ozyf zUoM{~Um<@={-*r60#U(0!Bc^wuvVc);k3d%g-J!4qLpHZVwz%!VuRu}#Ze`^l7W)9 z5>Kf>>9Eozr6C$Z)1`URxU@~QI@)F0FdauXr2Es8>BaOP=)Lp_WhG@>R;lZ?BJkMlIuMhw8ApiF&yDYW2hFJ?fJhni{?u z85&g@mo&yT8JcdI$(rSw=QPK(Xj%)k1X|@<=e1rim6`6$RAwc!i#egKuI;BS(LSWz zt39n_sIypSqfWEV6J3%nTQ@-4i zi$R;gsG*9XzhRzXqv2yCs*$VFDx+GXJH|L;wsDH_KI2;^u!)^Xl1YupO;gy^-c(?^ z&$Q1BYvyPsG^;hc$D**@Sy`+`)}T4VJji^bd7Jqw3q6Zii=7tT7GEswEK@D(EFW1Z zSp`^awCb?>!`j4}Yh7b~$A)U-W3$et-R8BesV(1jzwLcHnq9En7Q0Tn&-M=XBKs!$ zF$X<|c!#|X_tWYh)GZit z(Q)Cp9CDE^WG;+fcyOWARoj*0TI>4EP1lX*cEoMO-Pk?Z{kZ!p4@(b`M~lalr<3Oz z&kJ6Nm#vN_+kA5{dW4@^Vjg_`q%qU1ULk& z3Fr!>1V#i_2R;ij2@(Z$1jE4r!MlPVFVbHmT+|iPIq0wy5aS{>yK?9ZAjVh%SOwMWgFjair&;wpi!{CU}&@N=Eg#~ zLQ&zpEzVmGY{hI9Z0+4-0xS$$Xe-OToc?Y*V;rTcf_ zb_jRe-RZjXSeas3UfIyD;9afd%<`i0x4T#DzE)vdabOQ=k7SRuGN`h>O0Q~1)u-yD z>VX=Mn&!Rgd$;YK+Q-}1zu#?t(*cbG#Ronf6db&N$oEidtwC+YVcg-Y!_VuY>bk#Y ze_ww@?MU&F&qswvrN_dLb=5o6*Egs)ls3YRlE$&)amR1{;Ppd$6RYV^Go!iq1UMl% z@#4q$AMc(FJlT1QeX8jv{h#)>&{~RGq1N2iiMFIRX?sk2-|2wUogK~{EkB$8eDsX= znVPf8XG_nK&J~=SIiGia@9y}|z3FhX{g&gcj=lwb=lWgyFW&aLedUh- zof`v-2Kw$UzI*>(+&$@i-u=-BsSjR1%z8NeX#HdC`Hh-Z(6xI-`hmHDqv!v)W&&nrf>M(RhcN6(D;jNN*%^u_SYjF;2ng}*8Ow)d6M ztDk;%`@Lsk$;9w$(d(H%O5UixIr`T2ZRcd@grzRgSFN5Dcb zO%PW>5DUpFYL^uG1+lSGFb}lT!vEkO(AC}|zaR!dD|I!6g-P|)5CjWB(I~j<&Rp*- z49;_3Cti5(p1Jp&!`ztxUKo_8A6YvYG}itIdl4{MNs3!x&EdlHf?JD`SpQvN&w}2D zYmajcB0^PwbrA296{~oNBEIvYFy|W7W4=I5ZXq&bNg@Vz2=Qqs^tkB^#>)SC9#GvE zK?jJ=8$^*VCEjoxk5;BX{PAL!)jS(DsV+M)6VLxjitpX$u#VAgY7eVfHjvrD*Y6Ua zUZyuhlpZ9-fskA(r$=hs-`dgsZhNBh*7q_yxVZbv>KF-h{%6ss5vXwWUvKm&G6vgI z-SyV1Rg^iEpbfZBQ`$dPX>$N7K z*x}3>_~!8Hai6ua1d?$juz+@nWbxoz?{ClXxoHQk!4yS8=8PB%Vpt!c8Dc85c~O{i z4XiOO0y`%LbAZuT4QTCWDD*hjfJp}c-FuSD%&bnY5RXZ=m%xlM>^=Yh002ovPDHLk FV1hRu3A_LR literal 0 HcmV?d00001 diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tag_android-wear_32.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tag_android-wear_32.png new file mode 100644 index 0000000000000000000000000000000000000000..33560163b2792717cea886c7abdf91d5e1b5dfc7 GIT binary patch literal 3143 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 literal 0 HcmV?d00001 diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tag_default_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tag_default_16.png new file mode 100644 index 0000000000000000000000000000000000000000..e3ad2e60b2615147e41889e8b8b03786c69f3960 GIT binary patch literal 3097 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 literal 0 HcmV?d00001 diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tag_default_32.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/tag_default_32.png new file mode 100644 index 0000000000000000000000000000000000000000..f4264e7b7dad591cdb1f5a3d3a378e7c064526ca GIT binary patch literal 3064 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 literal 0 HcmV?d00001 diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/warning_icon16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/warning_icon16.png new file mode 100644 index 0000000000000000000000000000000000000000..a2fcf7cff6d413cb0e598bd92fd546d7ed0be5b8 GIT binary patch literal 259 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qucL8%hgh?3y^w370~qEv=}#LT=BJwMkFg)(D3 zQ$0gN_s>q|KvhMaE{-7<{!4o=@*Ys&aJf4vbXTnUD;_Z)Rrb9z!tCOd*uUo}1vRxf zs7E%mIe1=J|JmSRkkjGzNeNTb&6e+Vc0Sr|-SokD-@{{-tX`fET6Ja^-{Cv7pvCFf yx0iMt8I7qIBDp2X*yn2eU=+=9JpA5cH{*_*T?X0PwCjMjGkCiCxvX mClientSpecificActions = null; - - /** Console for DDMS log message */ - private MessageConsole mDdmsConsole; - - private IDevice mCurrentDevice; - private Client mCurrentClient; - private boolean mListeningToUiSelection = false; - - private final ArrayList mListeners = new ArrayList(); - - private Color mRed; - - /** - * Classes which implement this interface provide methods that deals with - * {@link IDevice} and {@link Client} selectionchanges. - */ - public interface ISelectionListener { - - /** - * Sent when a new {@link Client} is selected. - * - * @param selectedClient - * The selected client. If null, no clients are selected. - */ - public void selectionChanged(Client selectedClient); - - /** - * Sent when a new {@link IDevice} is selected. - * - * @param selectedDevice - * the selected device. If null, no devices are selected. - */ - public void selectionChanged(IDevice selectedDevice); - } - - /** - * The constructor - */ - public DdmsPlugin() { - 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); - - final Display display = getDisplay(); - - // get the eclipse store - final IPreferenceStore eclipseStore = getPreferenceStore(); - - AndroidDebugBridge.addDeviceChangeListener(this); - - DdmUiPreferences.setStore(eclipseStore); - - // DdmUiPreferences.displayCharts(); - - // set the consoles. - mDdmsConsole = new MessageConsole("DDMS", null); //$NON-NLS-1$ - ConsolePlugin.getDefault().getConsoleManager().addConsoles(new IConsole[] { mDdmsConsole }); - - final MessageConsoleStream consoleStream = mDdmsConsole.newMessageStream(); - final MessageConsoleStream errorConsoleStream = mDdmsConsole.newMessageStream(); - mRed = new Color(display, 0xFF, 0x00, 0x00); - - // because this can be run, in some cases, by a non UI thread, and - // because - // changing the console properties update the UI, we need to make this - // change - // in the UI thread. - display.asyncExec(new Runnable() { - @Override - public void run() { - errorConsoleStream.setColor(mRed); - } - }); - - // set up the ddms log to use the ddms console. - Log.setLogOutput(new ILogOutput() { - @Override - public void printLog(LogLevel logLevel, String tag, String message) { - if (logLevel.getPriority() >= LogLevel.ERROR.getPriority()) { - printToStream(errorConsoleStream, tag, message); - showConsoleView(mDdmsConsole); - } else { - printToStream(consoleStream, tag, message); - } - } - - @Override - public void printAndPromptLog(final LogLevel logLevel, final String tag, final String message) { - printLog(logLevel, tag, message); - // dialog box only run in UI thread.. - display.asyncExec(new Runnable() { - @Override - public void run() { - Shell shell = display.getActiveShell(); - if (logLevel == LogLevel.ERROR) { - MessageDialog.openError(shell, tag, message); - } else { - MessageDialog.openWarning(shell, tag, message); - } - } - }); - } - - }); - - // set up the ddms console to use this objects - DdmConsole.setConsole(new IDdmConsole() { - @Override - public void printErrorToConsole(String message) { - printToStream(errorConsoleStream, null, message); - showConsoleView(mDdmsConsole); - } - - @Override - public void printErrorToConsole(String[] messages) { - for (String m : messages) { - printToStream(errorConsoleStream, null, m); - } - showConsoleView(mDdmsConsole); - } - - @Override - public void printToConsole(String message) { - printToStream(consoleStream, null, message); - } - - @Override - public void printToConsole(String[] messages) { - for (String m : messages) { - printToStream(consoleStream, null, m); - } - } - }); - - // set the listener for the preference change - eclipseStore.addPropertyChangeListener(new IPropertyChangeListener() { - @Override - public void propertyChange(PropertyChangeEvent event) { - // get the name of the property that changed. - String property = event.getProperty(); - - if (PreferenceInitializer.ATTR_DEBUG_PORT_BASE.equals(property)) { - DdmPreferences.setDebugPortBase(eclipseStore.getInt(PreferenceInitializer.ATTR_DEBUG_PORT_BASE)); - } else if (PreferenceInitializer.ATTR_SELECTED_DEBUG_PORT.equals(property)) { - DdmPreferences.setSelectedDebugPort(eclipseStore - .getInt(PreferenceInitializer.ATTR_SELECTED_DEBUG_PORT)); - } else if (PreferenceInitializer.ATTR_THREAD_INTERVAL.equals(property)) { - DdmUiPreferences.setThreadRefreshInterval(eclipseStore - .getInt(PreferenceInitializer.ATTR_THREAD_INTERVAL)); - } else if (PreferenceInitializer.ATTR_LOG_LEVEL.equals(property)) { - DdmPreferences.setLogLevel(eclipseStore.getString(PreferenceInitializer.ATTR_LOG_LEVEL)); - } else if (PreferenceInitializer.ATTR_TIME_OUT.equals(property)) { - DdmPreferences.setTimeOut(eclipseStore.getInt(PreferenceInitializer.ATTR_TIME_OUT)); - } else if (PreferenceInitializer.ATTR_USE_ADBHOST.equals(property)) { - DdmPreferences.setUseAdbHost(eclipseStore.getBoolean(PreferenceInitializer.ATTR_USE_ADBHOST)); - } else if (PreferenceInitializer.ATTR_ADBHOST_VALUE.equals(property)) { - DdmPreferences.setAdbHostValue(eclipseStore.getString(PreferenceInitializer.ATTR_ADBHOST_VALUE)); - } - } - }); - - // do some last initializations - - // set the preferences. - PreferenceInitializer.setupPreferences(); - - // this class is set as the main source revealer and will look at all - // the implementations - // of the extension point. see #reveal(String, String, int) - StackTracePanel.setSourceRevealer(this); - - /* - * Load the extension point implementations. The first step is to load - * the IConfigurationElement representing the implementations. The 2nd - * step is to use these objects to instantiate the implementation - * classes. - * - * Because the 2nd step will trigger loading the plug-ins providing the - * implementations, and those plug-ins could access DDMS classes (like - * ADT), this 2nd step should be done in a Job to ensure that DDMS is - * loaded, so that the other plug-ins can load. - * - * Both steps could be done in the 2nd step but some of DDMS UI rely on - * knowing if there is an implementation or not (DeviceView), so we do - * the first steps in start() and, in some case, record it. - */ - - // get the IConfigurationElement for the debuggerConnector right away. - final IConfigurationElement[] dcce = findConfigElements("org.eclipse.andmore.ddms.debuggerConnector"); //$NON-NLS-1$ - mHasDebuggerConnectors = dcce.length > 0; - - // get the other configElements and instantiante them in a Job. - new Job(Messages.DdmsPlugin_DDMS_Post_Create_Init) { - @Override - protected IStatus run(IProgressMonitor monitor) { - try { - // init the lib - AndroidDebugBridge.init(true /* debugger support */); - - // get the available adb locators - IConfigurationElement[] elements = findConfigElements("org.eclipse.andmore.ddms.toolsLocator"); //$NON-NLS-1$ - - IToolsLocator[] locators = instantiateToolsLocators(elements); - - for (IToolsLocator locator : locators) { - try { - String adbLocation = locator.getAdbLocation(); - String traceviewLocation = locator.getTraceViewLocation(); - String hprofConvLocation = locator.getHprofConvLocation(); - if (adbLocation != null && traceviewLocation != null && hprofConvLocation != null) { - // checks if the location is valid. - if (setToolsLocation(adbLocation, hprofConvLocation, traceviewLocation)) { - - AndroidDebugBridge.createBridge(sAdbLocation, true /* forceNewBridge */); - - // no need to look at the other locators. - break; - } - } - } catch (Throwable t) { - // ignore, we'll just not use this implementation. - } - } - - // get the available debugger connectors - mDebuggerConnectors = instantiateDebuggerConnectors(dcce); - - // get the available Traceview Launchers. - elements = findConfigElements("org.eclipse.andmore.ddms.traceviewLauncher"); //$NON-NLS-1$ - mTraceviewLaunchers = instantiateTraceviewLauncher(elements); - - return Status.OK_STATUS; - } catch (CoreException e) { - return e.getStatus(); - } - } - }.schedule(); - } - - private void showConsoleView(MessageConsole console) { - ConsolePlugin.getDefault().getConsoleManager().showConsoleView(console); - } - - /** - * Obtain a list of configuration elements that extend the given extension - * point. - */ - IConfigurationElement[] findConfigElements(String extensionPointId) { - // get the adb location from an implementation of the ADB Locator - // extension point. - IExtensionRegistry extensionRegistry = Platform.getExtensionRegistry(); - IExtensionPoint extensionPoint = extensionRegistry.getExtensionPoint(extensionPointId); - if (extensionPoint != null) { - return extensionPoint.getConfigurationElements(); - } - - // shouldn't happen or it means the plug-in is broken. - return new IConfigurationElement[0]; - } - - /** - * Finds if any other plug-in is extending the exposed Extension Point - * called adbLocator. - * - * @return an array of all locators found, or an empty array if none were - * found. - */ - private IToolsLocator[] instantiateToolsLocators(IConfigurationElement[] configElements) throws CoreException { - ArrayList list = new ArrayList(); - - if (configElements.length > 0) { - // only use the first one, ignore the others. - IConfigurationElement configElement = configElements[0]; - - // instantiate the class - Object obj = configElement.createExecutableExtension("class"); //$NON-NLS-1$ - if (obj instanceof IToolsLocator) { - list.add((IToolsLocator) obj); - } - } - - return list.toArray(new IToolsLocator[list.size()]); - } - - /** - * Finds if any other plug-in is extending the exposed Extension Point - * called debuggerConnector. - * - * @return an array of all locators found, or an empty array if none were - * found. - */ - private IDebuggerConnector[] instantiateDebuggerConnectors(IConfigurationElement[] configElements) - throws CoreException { - ArrayList list = new ArrayList(); - - if (configElements.length > 0) { - // only use the first one, ignore the others. - IConfigurationElement configElement = configElements[0]; - - // instantiate the class - Object obj = configElement.createExecutableExtension("class"); //$NON-NLS-1$ - if (obj instanceof IDebuggerConnector) { - list.add((IDebuggerConnector) obj); - } - } - - return list.toArray(new IDebuggerConnector[list.size()]); - } - - /** - * Finds if any other plug-in is extending the exposed Extension Point - * called traceviewLauncher. - * - * @return an array of all locators found, or an empty array if none were - * found. - */ - private ITraceviewLauncher[] instantiateTraceviewLauncher(IConfigurationElement[] configElements) - throws CoreException { - ArrayList list = new ArrayList(); - - if (configElements.length > 0) { - // only use the first one, ignore the others. - IConfigurationElement configElement = configElements[0]; - - // instantiate the class - Object obj = configElement.createExecutableExtension("class"); //$NON-NLS-1$ - if (obj instanceof ITraceviewLauncher) { - list.add((ITraceviewLauncher) obj); - } - } - - return list.toArray(new ITraceviewLauncher[list.size()]); - } - - /** - * Returns the classes that implement {@link IClientAction} in each of the - * extensions that extend clientAction extension point. - * - * @throws CoreException - */ - private List instantiateClientSpecificActions(IConfigurationElement[] elements) throws CoreException { - if (elements == null || elements.length == 0) { - return Collections.emptyList(); - } - - List extensions = new ArrayList(1); - - for (IConfigurationElement e : elements) { - Object o = e.createExecutableExtension("class"); //$NON-NLS-1$ - if (o instanceof IClientAction) { - extensions.add((IClientAction) o); - } - } - - return extensions; - } - - public static Display getDisplay() { - IWorkbench bench = sPlugin.getWorkbench(); - if (bench != null) { - return bench.getDisplay(); - } - return null; - } - - /* - * (non-Javadoc) - * - * @see - * org.eclipse.ui.plugin.AbstractUIPlugin#stop(org.osgi.framework.BundleContext - * ) - */ - @Override - public void stop(BundleContext context) throws Exception { - AndroidDebugBridge.removeDeviceChangeListener(this); - - AndroidDebugBridge.terminate(); - - mRed.dispose(); - - sPlugin = null; - super.stop(context); - } - - /** - * Returns the shared instance - * - * @return the shared instance - */ - public static DdmsPlugin getDefault() { - return sPlugin; - } - - public static String getAdb() { - return sAdbLocation; - } - - public static File getPlatformToolsFolder() { - return new File(sAdbLocation).getParentFile(); - } - - public static String getToolsFolder() { - return sToolsFolder; - } - - public static String getHprofConverter() { - return sHprofConverter; - } - - /** - * Stores the adb location. This returns true if the location is an existing - * file. - */ - private static boolean setToolsLocation(String adbLocation, String hprofConvLocation, String traceViewLocation) { - - File adb = new File(adbLocation); - File hprofConverter = new File(hprofConvLocation); - File traceview = new File(traceViewLocation); - - String missing = ""; - if (adb.isFile() == false) { - missing += adb.getAbsolutePath() + " "; - } - if (hprofConverter.isFile() == false) { - missing += hprofConverter.getAbsolutePath() + " "; - } - if (traceview.isFile() == false) { - missing += traceview.getAbsolutePath() + " "; - } - - if (missing.length() > 0) { - String msg = String.format("DDMS files not found: %1$s", missing); - Log.e("DDMS", msg); - Status status = new Status(IStatus.ERROR, PLUGIN_ID, msg, null /* exception */); - getDefault().getLog().log(status); - return false; - } - - sAdbLocation = adbLocation; - sHprofConverter = hprofConverter.getAbsolutePath(); - DdmUiPreferences.setTraceviewLocation(traceview.getAbsolutePath()); - - sToolsFolder = traceview.getParent(); - - return true; - } - - /** - * Set the location of the adb executable and optionally starts adb - * - * @param adb - * location of adb - * @param startAdb - * flag to start adb - */ - public static void setToolsLocation(String adbLocation, boolean startAdb, String hprofConvLocation, - String traceViewLocation) { - - if (setToolsLocation(adbLocation, hprofConvLocation, traceViewLocation)) { - // starts the server in a thread in case this is blocking. - if (startAdb) { - new Thread() { - @Override - public void run() { - // create and start the bridge - try { - AndroidDebugBridge.createBridge(sAdbLocation, false /* forceNewBridge */); - } catch (Throwable t) { - Status status = new Status(IStatus.ERROR, PLUGIN_ID, "Failed to create AndroidDebugBridge", - t); - getDefault().getLog().log(status); - } - } - }.start(); - } - } - } - - /** - * Returns whether there are implementations of the debuggerConnectors - * extension point. - *

      - * This is guaranteed to return the correct value as soon as the plug-in is - * loaded. - */ - public boolean hasDebuggerConnectors() { - return mHasDebuggerConnectors; - } - - /** - * Returns the implementations of {@link IDebuggerConnector}. - *

      - * There may be a small amount of time right after the plug-in load where - * this can return null even if there are implementation. - *

      - * Since the use of the implementation likely require user input, the UI can - * use {@link #hasDebuggerConnectors()} to know if there are implementations - * before they are loaded. - */ - public IDebuggerConnector[] getDebuggerConnectors() { - return mDebuggerConnectors; - } - - public synchronized void addSelectionListener(ISelectionListener listener) { - mListeners.add(listener); - - // notify the new listener of the current selection - listener.selectionChanged(mCurrentDevice); - listener.selectionChanged(mCurrentClient); - } - - public synchronized void removeSelectionListener(ISelectionListener listener) { - mListeners.remove(listener); - } - - public synchronized void setListeningState(boolean state) { - mListeningToUiSelection = state; - } - - /** - * 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) - */ - @Override - public void deviceConnected(IDevice device) { - // if we are listening to selection coming from the ui, then we do - // nothing, as - // any change in the devices/clients, will be handled by the UI, and - // we'll receive - // selection notification through our implementation of - // IUiSelectionListener. - if (mListeningToUiSelection == false) { - if (mCurrentDevice == null) { - handleDefaultSelection(device); - } - } - } - - /** - * Sent when the a device is disconnected to the {@link AndroidDebugBridge}. - *

      - * This is sent from a non UI thread. - * - * @param device - * the new device. - * - * @see IDeviceChangeListener#deviceDisconnected(IDevice) - */ - @Override - public void deviceDisconnected(IDevice device) { - // if we are listening to selection coming from the ui, then we do - // nothing, as - // any change in the devices/clients, will be handled by the UI, and - // we'll receive - // selection notification through our implementation of - // IUiSelectionListener. - if (mListeningToUiSelection == false) { - // test if the disconnected device was the default selection. - if (mCurrentDevice == device) { - // try to find a new device - AndroidDebugBridge bridge = AndroidDebugBridge.getBridge(); - if (bridge != null) { - // get the device list - IDevice[] devices = bridge.getDevices(); - - // check if we still have devices - if (devices.length == 0) { - handleDefaultSelection((IDevice) null); - } else { - handleDefaultSelection(devices[0]); - } - } else { - handleDefaultSelection((IDevice) null); - } - } - } - } - - /** - * 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) - */ - @Override - public void deviceChanged(IDevice device, int changeMask) { - // if we are listening to selection coming from the ui, then we do - // nothing, as - // any change in the devices/clients, will be handled by the UI, and - // we'll receive - // selection notification through our implementation of - // IUiSelectionListener. - if (mListeningToUiSelection == false) { - - // check if this is our device - if (device == mCurrentDevice) { - if (mCurrentClient == null) { - handleDefaultSelection(device); - } else { - // get the clients and make sure ours is still in there. - Client[] clients = device.getClients(); - boolean foundClient = false; - for (Client client : clients) { - if (client == mCurrentClient) { - foundClient = true; - break; - } - } - - // if we haven't found our client, lets look for a new one - if (foundClient == false) { - mCurrentClient = null; - handleDefaultSelection(device); - } - } - } - } - } - - /** - * 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. - */ - @Override - public synchronized void selectionChanged(IDevice selectedDevice, Client selectedClient) { - if (mCurrentDevice != selectedDevice) { - mCurrentDevice = selectedDevice; - - // notify of the new default device - for (ISelectionListener listener : mListeners) { - listener.selectionChanged(mCurrentDevice); - } - } - - if (mCurrentClient != selectedClient) { - mCurrentClient = selectedClient; - - // notify of the new default client - for (ISelectionListener listener : mListeners) { - listener.selectionChanged(mCurrentClient); - } - } - } - - /** - * Handles a default selection of a {@link IDevice} and {@link Client}. - * - * @param device - * the selected device - */ - private void handleDefaultSelection(final IDevice device) { - // because the listener expect to receive this from the UI thread, and - // this is called - // from the AndroidDebugBridge notifications, we need to run this in the - // UI thread. - try { - Display display = getDisplay(); - - display.asyncExec(new Runnable() { - @Override - public void run() { - // set the new device if different. - boolean newDevice = false; - if (mCurrentDevice != device) { - mCurrentDevice = device; - newDevice = true; - - // notify of the new default device - for (ISelectionListener listener : mListeners) { - listener.selectionChanged(mCurrentDevice); - } - } - - if (device != null) { - // if this is a device switch or the same device but we - // didn't find a valid - // client the last time, we go look for a client to use - // again. - if (newDevice || mCurrentClient == null) { - // now get the new client - Client[] clients = device.getClients(); - if (clients.length > 0) { - handleDefaultSelection(clients[0]); - } else { - handleDefaultSelection((Client) null); - } - } - } else { - handleDefaultSelection((Client) null); - } - } - }); - } catch (SWTException e) { - // display is disposed. Do nothing since we're quitting anyway. - } - } - - private void handleDefaultSelection(Client client) { - mCurrentClient = client; - - // notify of the new default client - for (ISelectionListener listener : mListeners) { - listener.selectionChanged(mCurrentClient); - } - } - - /** - * Prints a message, 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 message - * The message to print. - */ - private static synchronized void printToStream(MessageConsoleStream stream, String tag, String message) { - String dateTag = getMessageTag(tag); - - stream.print(dateTag); - if (!dateTag.endsWith(" ")) { - stream.print(" "); //$NON-NLS-1$ - } - stream.println(message); - } - - /** - * Creates a string containing the current date/time, and the tag - * - * @param tag - * The tag associated to the message. Can be null - * @return The dateTag - */ - private static String getMessageTag(String tag) { - Calendar c = Calendar.getInstance(); - - if (tag == null) { - return String.format(Messages.DdmsPlugin_Message_Tag_Mask_1, c); - } - - return String.format(Messages.DdmsPlugin_Message_Tag_Mask_2, c, tag); - } - - /** - * Implementation of com.android.ddmuilib.StackTracePanel.ISourceRevealer. - */ - @Override - public void reveal(String applicationName, String className, int line) { - JavaSourceRevealer.reveal(applicationName, className, line); - } - - public boolean launchTraceview(String osPath) { - if (mTraceviewLaunchers != null) { - for (ITraceviewLauncher launcher : mTraceviewLaunchers) { - try { - if (launcher.openFile(osPath)) { - return true; - } - } catch (Throwable t) { - // ignore, we'll just not use this implementation. - } - } - } - - return false; - } - - /** - * Returns the list of clients that extend the clientAction extension point. - */ - @NonNull - public synchronized List getClientSpecificActions() { - if (mClientSpecificActions == null) { - // get available client specific action extensions - IConfigurationElement[] elements = findConfigElements("org.eclipse.andmore.ddms.clientAction"); //$NON-NLS-1$ - try { - mClientSpecificActions = instantiateClientSpecificActions(elements); - } catch (CoreException e) { - mClientSpecificActions = Collections.emptyList(); - } - } - - return mClientSpecificActions; - } - - private LogCatMonitor mLogCatMonitor; - - public void startLogCatMonitor(IDevice device) { - if (mLogCatMonitor == null) { - mLogCatMonitor = new LogCatMonitor(getDebuggerConnectors(), getPreferenceStore()); - } - - mLogCatMonitor.monitorDevice(device); - } - - /** - * Returns an image descriptor for the image file at the given plug-in - * relative path - */ - public static ImageDescriptor getImageDescriptor(String path) { - return imageDescriptorFromPlugin(PLUGIN_ID, path); - } -} +/* + * 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 org.eclipse.andmore.ddms; + +import com.android.annotations.NonNull; +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; +import com.android.ddmlib.Client; +import com.android.ddmlib.DdmPreferences; +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.ddmuilib.DdmUiPreferences; +import com.android.ddmuilib.DevicePanel.IUiSelectionListener; +import com.android.ddmuilib.StackTracePanel; +import com.android.ddmuilib.console.DdmConsole; +import com.android.ddmuilib.console.IDdmConsole; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.andmore.base.resources.JFaceImageLoader; +import org.eclipse.andmore.ddms.i18n.Messages; +import org.eclipse.andmore.ddms.preferences.PreferenceInitializer; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IConfigurationElement; +import org.eclipse.core.runtime.IExtensionPoint; +import org.eclipse.core.runtime.IExtensionRegistry; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Platform; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.util.IPropertyChangeListener; +import org.eclipse.jface.util.PropertyChangeEvent; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.console.ConsolePlugin; +import org.eclipse.ui.console.IConsole; +import org.eclipse.ui.console.MessageConsole; +import org.eclipse.ui.console.MessageConsoleStream; +import org.eclipse.ui.plugin.AbstractUIPlugin; +import org.osgi.framework.BundleContext; + +import java.io.File; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.List; + +/** + * The activator class controls the plug-in life cycle + */ +public final class DdmsPlugin extends AbstractUIPlugin implements IDeviceChangeListener, IUiSelectionListener, + com.android.ddmuilib.StackTracePanel.ISourceRevealer { + + // The plug-in ID + public static final String PLUGIN_ID = "org.eclipse.andmore.ddms"; //$NON-NLS-1$ + + public static final String NEWLINE = System.getProperty("line.separator"); + + /** The singleton instance */ + private static DdmsPlugin sPlugin; + + /** Location of the adb command line executable */ + private static String sAdbLocation; + private static String sToolsFolder; + private static String sHprofConverter; + + private boolean mHasDebuggerConnectors; + /** + * debugger connectors for already running apps. Initialized from an + * extension point. + */ + private IDebuggerConnector[] mDebuggerConnectors; + private ITraceviewLauncher[] mTraceviewLaunchers; + private List mClientSpecificActions = null; + + /** Console for DDMS log message */ + private MessageConsole mDdmsConsole; + + private IDevice mCurrentDevice; + private Client mCurrentClient; + private boolean mListeningToUiSelection = false; + + private final ArrayList mListeners = new ArrayList(); + + private Color mRed; + private ImageFactory mImageFactory; + + /** + * Classes which implement this interface provide methods that deals with + * {@link IDevice} and {@link Client} selectionchanges. + */ + public interface ISelectionListener { + + /** + * Sent when a new {@link Client} is selected. + * + * @param selectedClient + * The selected client. If null, no clients are selected. + */ + public void selectionChanged(Client selectedClient); + + /** + * Sent when a new {@link IDevice} is selected. + * + * @param selectedDevice + * the selected device. If null, no devices are selected. + */ + public void selectionChanged(IDevice selectedDevice); + } + + /** + * The constructor + */ + public DdmsPlugin() { + sPlugin = this; + } + + public ImageFactory getImageFactory() { + return mImageFactory; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.ui.plugin.AbstractUIPlugin#start(org.osgi.framework.BundleContext + * ) + */ + @Override + public void start(BundleContext context) throws Exception { + super.start(context); + + final Display display = getDisplay(); + + // get the eclipse store + final IPreferenceStore eclipseStore = getPreferenceStore(); + + AndroidDebugBridge.addDeviceChangeListener(this); + + DdmUiPreferences.setStore(eclipseStore); + + // DdmUiPreferences.displayCharts(); + + // set the consoles. + mDdmsConsole = new MessageConsole("DDMS", null); //$NON-NLS-1$ + ConsolePlugin.getDefault().getConsoleManager().addConsoles(new IConsole[] { mDdmsConsole }); + + final MessageConsoleStream consoleStream = mDdmsConsole.newMessageStream(); + final MessageConsoleStream errorConsoleStream = mDdmsConsole.newMessageStream(); + mRed = new Color(display, 0xFF, 0x00, 0x00); + + // because this can be run, in some cases, by a non UI thread, and + // because + // changing the console properties update the UI, we need to make this + // change + // in the UI thread. + display.asyncExec(new Runnable() { + @Override + public void run() { + errorConsoleStream.setColor(mRed); + } + }); + + // set up the ddms log to use the ddms console. + Log.addLogger(new ILogOutput() { + @Override + public void printLog(LogLevel logLevel, String tag, String message) { + if (logLevel.getPriority() >= LogLevel.ERROR.getPriority()) { + printToStream(errorConsoleStream, tag, message); + showConsoleView(mDdmsConsole); + } else { + printToStream(consoleStream, tag, message); + } + } + + @Override + public void printAndPromptLog(final LogLevel logLevel, final String tag, final String message) { + printLog(logLevel, tag, message); + // dialog box only run in UI thread.. + display.asyncExec(new Runnable() { + @Override + public void run() { + Shell shell = display.getActiveShell(); + if (logLevel == LogLevel.ERROR) { + MessageDialog.openError(shell, tag, message); + } else { + MessageDialog.openWarning(shell, tag, message); + } + } + }); + } + + }); + + // set up the ddms console to use this objects + DdmConsole.setConsole(new IDdmConsole() { + @Override + public void printErrorToConsole(String message) { + printToStream(errorConsoleStream, null, message); + showConsoleView(mDdmsConsole); + } + + @Override + public void printErrorToConsole(String[] messages) { + for (String m : messages) { + printToStream(errorConsoleStream, null, m); + } + showConsoleView(mDdmsConsole); + } + + @Override + public void printToConsole(String message) { + printToStream(consoleStream, null, message); + } + + @Override + public void printToConsole(String[] messages) { + for (String m : messages) { + printToStream(consoleStream, null, m); + } + } + }); + + // set the listener for the preference change + eclipseStore.addPropertyChangeListener(new IPropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent event) { + // get the name of the property that changed. + String property = event.getProperty(); + + if (PreferenceInitializer.ATTR_DEBUG_PORT_BASE.equals(property)) { + DdmPreferences.setDebugPortBase(eclipseStore.getInt(PreferenceInitializer.ATTR_DEBUG_PORT_BASE)); + } else if (PreferenceInitializer.ATTR_SELECTED_DEBUG_PORT.equals(property)) { + DdmPreferences.setSelectedDebugPort(eclipseStore + .getInt(PreferenceInitializer.ATTR_SELECTED_DEBUG_PORT)); + } else if (PreferenceInitializer.ATTR_THREAD_INTERVAL.equals(property)) { + DdmUiPreferences.setThreadRefreshInterval(eclipseStore + .getInt(PreferenceInitializer.ATTR_THREAD_INTERVAL)); + } else if (PreferenceInitializer.ATTR_LOG_LEVEL.equals(property)) { + DdmPreferences.setLogLevel(eclipseStore.getString(PreferenceInitializer.ATTR_LOG_LEVEL)); + } else if (PreferenceInitializer.ATTR_TIME_OUT.equals(property)) { + DdmPreferences.setTimeOut(eclipseStore.getInt(PreferenceInitializer.ATTR_TIME_OUT)); + } else if (PreferenceInitializer.ATTR_USE_ADBHOST.equals(property)) { + DdmPreferences.setUseAdbHost(eclipseStore.getBoolean(PreferenceInitializer.ATTR_USE_ADBHOST)); + } else if (PreferenceInitializer.ATTR_ADBHOST_VALUE.equals(property)) { + DdmPreferences.setAdbHostValue(eclipseStore.getString(PreferenceInitializer.ATTR_ADBHOST_VALUE)); + } + } + }); + + // do some last initializations + + mImageFactory = new JFaceImageLoader(new DdmResourceProvider()); + // set the preferences. + PreferenceInitializer.setupPreferences(); + + // this class is set as the main source revealer and will look at all + // the implementations + // of the extension point. see #reveal(String, String, int) + StackTracePanel.setSourceRevealer(this); + + /* + * Load the extension point implementations. The first step is to load + * the IConfigurationElement representing the implementations. The 2nd + * step is to use these objects to instantiate the implementation + * classes. + * + * Because the 2nd step will trigger loading the plug-ins providing the + * implementations, and those plug-ins could access DDMS classes (like + * ADT), this 2nd step should be done in a Job to ensure that DDMS is + * loaded, so that the other plug-ins can load. + * + * Both steps could be done in the 2nd step but some of DDMS UI rely on + * knowing if there is an implementation or not (DeviceView), so we do + * the first steps in start() and, in some case, record it. + */ + + // get the IConfigurationElement for the debuggerConnector right away. + final IConfigurationElement[] dcce = findConfigElements("org.eclipse.andmore.ddms.debuggerConnector"); //$NON-NLS-1$ + mHasDebuggerConnectors = dcce.length > 0; + + // get the other configElements and instantiante them in a Job. + new Job(Messages.DdmsPlugin_DDMS_Post_Create_Init) { + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + // init the lib + AndroidDebugBridge.init(true /* debugger support */); + + // get the available adb locators + IConfigurationElement[] elements = findConfigElements("org.eclipse.andmore.ddms.toolsLocator"); //$NON-NLS-1$ + + IToolsLocator[] locators = instantiateToolsLocators(elements); + + for (IToolsLocator locator : locators) { + try { + String adbLocation = locator.getAdbLocation(); + String traceviewLocation = locator.getTraceViewLocation(); + String hprofConvLocation = locator.getHprofConvLocation(); + if (adbLocation != null && traceviewLocation != null && hprofConvLocation != null) { + // checks if the location is valid. + if (setToolsLocation(adbLocation, hprofConvLocation, traceviewLocation)) { + + AndroidDebugBridge.createBridge(sAdbLocation, true /* forceNewBridge */); + + // no need to look at the other locators. + break; + } + } + } catch (Throwable t) { + // ignore, we'll just not use this implementation. + } + } + + // get the available debugger connectors + mDebuggerConnectors = instantiateDebuggerConnectors(dcce); + + // get the available Traceview Launchers. + elements = findConfigElements("org.eclipse.andmore.ddms.traceviewLauncher"); //$NON-NLS-1$ + mTraceviewLaunchers = instantiateTraceviewLauncher(elements); + + return Status.OK_STATUS; + } catch (CoreException e) { + return e.getStatus(); + } + } + }.schedule(); + } + + private void showConsoleView(MessageConsole console) { + ConsolePlugin.getDefault().getConsoleManager().showConsoleView(console); + } + + /** + * Obtain a list of configuration elements that extend the given extension + * point. + */ + IConfigurationElement[] findConfigElements(String extensionPointId) { + // get the adb location from an implementation of the ADB Locator + // extension point. + IExtensionRegistry extensionRegistry = Platform.getExtensionRegistry(); + // NPE occurred during testing + if (extensionRegistry != null) + { + IExtensionPoint extensionPoint = extensionRegistry.getExtensionPoint(extensionPointId); + if (extensionPoint != null) { + return extensionPoint.getConfigurationElements(); + } + } + // shouldn't happen or it means the plug-in is broken. + return new IConfigurationElement[0]; + } + + /** + * Finds if any other plug-in is extending the exposed Extension Point + * called adbLocator. + * + * @return an array of all locators found, or an empty array if none were + * found. + */ + private IToolsLocator[] instantiateToolsLocators(IConfigurationElement[] configElements) throws CoreException { + ArrayList list = new ArrayList(); + + if (configElements.length > 0) { + // only use the first one, ignore the others. + IConfigurationElement configElement = configElements[0]; + + // instantiate the class + Object obj = configElement.createExecutableExtension("class"); //$NON-NLS-1$ + if (obj instanceof IToolsLocator) { + list.add((IToolsLocator) obj); + } + } + + return list.toArray(new IToolsLocator[list.size()]); + } + + /** + * Finds if any other plug-in is extending the exposed Extension Point + * called debuggerConnector. + * + * @return an array of all locators found, or an empty array if none were + * found. + */ + private IDebuggerConnector[] instantiateDebuggerConnectors(IConfigurationElement[] configElements) + throws CoreException { + ArrayList list = new ArrayList(); + + if (configElements.length > 0) { + // only use the first one, ignore the others. + IConfigurationElement configElement = configElements[0]; + + // instantiate the class + Object obj = configElement.createExecutableExtension("class"); //$NON-NLS-1$ + if (obj instanceof IDebuggerConnector) { + list.add((IDebuggerConnector) obj); + } + } + + return list.toArray(new IDebuggerConnector[list.size()]); + } + + /** + * Finds if any other plug-in is extending the exposed Extension Point + * called traceviewLauncher. + * + * @return an array of all locators found, or an empty array if none were + * found. + */ + private ITraceviewLauncher[] instantiateTraceviewLauncher(IConfigurationElement[] configElements) + throws CoreException { + ArrayList list = new ArrayList(); + + if (configElements.length > 0) { + // only use the first one, ignore the others. + IConfigurationElement configElement = configElements[0]; + + // instantiate the class + Object obj = configElement.createExecutableExtension("class"); //$NON-NLS-1$ + if (obj instanceof ITraceviewLauncher) { + list.add((ITraceviewLauncher) obj); + } + } + + return list.toArray(new ITraceviewLauncher[list.size()]); + } + + /** + * Returns the classes that implement {@link IClientAction} in each of the + * extensions that extend clientAction extension point. + * + * @throws CoreException + */ + private List instantiateClientSpecificActions(IConfigurationElement[] elements) throws CoreException { + if (elements == null || elements.length == 0) { + return Collections.emptyList(); + } + + List extensions = new ArrayList(1); + + for (IConfigurationElement e : elements) { + Object o = e.createExecutableExtension("class"); //$NON-NLS-1$ + if (o instanceof IClientAction) { + extensions.add((IClientAction) o); + } + } + + return extensions; + } + + public static Display getDisplay() { + IWorkbench bench = sPlugin.getWorkbench(); + if (bench != null) { + return bench.getDisplay(); + } + return null; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.ui.plugin.AbstractUIPlugin#stop(org.osgi.framework.BundleContext + * ) + */ + @Override + public void stop(BundleContext context) throws Exception { + AndroidDebugBridge.removeDeviceChangeListener(this); + + AndroidDebugBridge.terminate(); + + mRed.dispose(); + mImageFactory.dispose(); + sPlugin = null; + super.stop(context); + } + + /** + * Returns the shared instance + * + * @return the shared instance + */ + public static DdmsPlugin getDefault() { + return sPlugin; + } + + public static String getAdb() { + return sAdbLocation; + } + + public static File getPlatformToolsFolder() { + return new File(sAdbLocation).getParentFile(); + } + + public static String getToolsFolder() { + return sToolsFolder; + } + + public static String getHprofConverter() { + return sHprofConverter; + } + + /** + * Stores the adb location. This returns true if the location is an existing + * file. + */ + private static boolean setToolsLocation(String adbLocation, String hprofConvLocation, String traceViewLocation) { + + File adb = new File(adbLocation); + File hprofConverter = new File(hprofConvLocation); + File traceview = new File(traceViewLocation); + + String missing = ""; + if (adb.isFile() == false) { + missing += adb.getAbsolutePath() + " "; + } + if (hprofConverter.isFile() == false) { + missing += hprofConverter.getAbsolutePath() + " "; + } + if (traceview.isFile() == false) { + missing += traceview.getAbsolutePath() + " "; + } + + if (missing.length() > 0) { + String msg = String.format("DDMS files not found: %1$s", missing); + Log.e("DDMS", msg); + Status status = new Status(IStatus.ERROR, PLUGIN_ID, msg, null /* exception */); + getDefault().getLog().log(status); + return false; + } + + sAdbLocation = adbLocation; + sHprofConverter = hprofConverter.getAbsolutePath(); + DdmUiPreferences.setTraceviewLocation(traceview.getAbsolutePath()); + + sToolsFolder = traceview.getParent(); + + return true; + } + + /** + * Set the location of the adb executable and optionally starts adb + * + * @param adb + * location of adb + * @param startAdb + * flag to start adb + */ + public static void setToolsLocation(String adbLocation, boolean startAdb, String hprofConvLocation, + String traceViewLocation) { + + if (setToolsLocation(adbLocation, hprofConvLocation, traceViewLocation)) { + // starts the server in a thread in case this is blocking. + if (startAdb) { + new Thread() { + @Override + public void run() { + // create and start the bridge + try { + AndroidDebugBridge.createBridge(sAdbLocation, false /* forceNewBridge */); + } catch (Throwable t) { + Status status = new Status(IStatus.ERROR, PLUGIN_ID, "Failed to create AndroidDebugBridge", + t); + getDefault().getLog().log(status); + } + } + }.start(); + } + } + } + + /** + * Returns whether there are implementations of the debuggerConnectors + * extension point. + *

      + * This is guaranteed to return the correct value as soon as the plug-in is + * loaded. + */ + public boolean hasDebuggerConnectors() { + return mHasDebuggerConnectors; + } + + /** + * Returns the implementations of {@link IDebuggerConnector}. + *

      + * There may be a small amount of time right after the plug-in load where + * this can return null even if there are implementation. + *

      + * Since the use of the implementation likely require user input, the UI can + * use {@link #hasDebuggerConnectors()} to know if there are implementations + * before they are loaded. + */ + public IDebuggerConnector[] getDebuggerConnectors() { + return mDebuggerConnectors; + } + + public synchronized void addSelectionListener(ISelectionListener listener) { + mListeners.add(listener); + + // notify the new listener of the current selection + listener.selectionChanged(mCurrentDevice); + listener.selectionChanged(mCurrentClient); + } + + public synchronized void removeSelectionListener(ISelectionListener listener) { + mListeners.remove(listener); + } + + public synchronized void setListeningState(boolean state) { + mListeningToUiSelection = state; + } + + /** + * 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) + */ + @Override + public void deviceConnected(IDevice device) { + // if we are listening to selection coming from the ui, then we do + // nothing, as + // any change in the devices/clients, will be handled by the UI, and + // we'll receive + // selection notification through our implementation of + // IUiSelectionListener. + if (mListeningToUiSelection == false) { + if (mCurrentDevice == null) { + handleDefaultSelection(device); + } + } + } + + /** + * Sent when the a device is disconnected to the {@link AndroidDebugBridge}. + *

      + * This is sent from a non UI thread. + * + * @param device + * the new device. + * + * @see IDeviceChangeListener#deviceDisconnected(IDevice) + */ + @Override + public void deviceDisconnected(IDevice device) { + // if we are listening to selection coming from the ui, then we do + // nothing, as + // any change in the devices/clients, will be handled by the UI, and + // we'll receive + // selection notification through our implementation of + // IUiSelectionListener. + if (mListeningToUiSelection == false) { + // test if the disconnected device was the default selection. + if (mCurrentDevice == device) { + // try to find a new device + AndroidDebugBridge bridge = AndroidDebugBridge.getBridge(); + if (bridge != null) { + // get the device list + IDevice[] devices = bridge.getDevices(); + + // check if we still have devices + if (devices.length == 0) { + handleDefaultSelection((IDevice) null); + } else { + handleDefaultSelection(devices[0]); + } + } else { + handleDefaultSelection((IDevice) null); + } + } + } + } + + /** + * 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) + */ + @Override + public void deviceChanged(IDevice device, int changeMask) { + // if we are listening to selection coming from the ui, then we do + // nothing, as + // any change in the devices/clients, will be handled by the UI, and + // we'll receive + // selection notification through our implementation of + // IUiSelectionListener. + if (mListeningToUiSelection == false) { + + // check if this is our device + if (device == mCurrentDevice) { + if (mCurrentClient == null) { + handleDefaultSelection(device); + } else { + // get the clients and make sure ours is still in there. + Client[] clients = device.getClients(); + boolean foundClient = false; + for (Client client : clients) { + if (client == mCurrentClient) { + foundClient = true; + break; + } + } + + // if we haven't found our client, lets look for a new one + if (foundClient == false) { + mCurrentClient = null; + handleDefaultSelection(device); + } + } + } + } + } + + /** + * 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. + */ + @Override + public synchronized void selectionChanged(IDevice selectedDevice, Client selectedClient) { + if (mCurrentDevice != selectedDevice) { + mCurrentDevice = selectedDevice; + + // notify of the new default device + for (ISelectionListener listener : mListeners) { + listener.selectionChanged(mCurrentDevice); + } + } + + if (mCurrentClient != selectedClient) { + mCurrentClient = selectedClient; + + // notify of the new default client + for (ISelectionListener listener : mListeners) { + listener.selectionChanged(mCurrentClient); + } + } + } + + /** + * Handles a default selection of a {@link IDevice} and {@link Client}. + * + * @param device + * the selected device + */ + private void handleDefaultSelection(final IDevice device) { + // because the listener expect to receive this from the UI thread, and + // this is called + // from the AndroidDebugBridge notifications, we need to run this in the + // UI thread. + try { + Display display = getDisplay(); + + display.asyncExec(new Runnable() { + @Override + public void run() { + // set the new device if different. + boolean newDevice = false; + if (mCurrentDevice != device) { + mCurrentDevice = device; + newDevice = true; + + // notify of the new default device + for (ISelectionListener listener : mListeners) { + listener.selectionChanged(mCurrentDevice); + } + } + + if (device != null) { + // if this is a device switch or the same device but we + // didn't find a valid + // client the last time, we go look for a client to use + // again. + if (newDevice || mCurrentClient == null) { + // now get the new client + Client[] clients = device.getClients(); + if (clients.length > 0) { + handleDefaultSelection(clients[0]); + } else { + handleDefaultSelection((Client) null); + } + } + } else { + handleDefaultSelection((Client) null); + } + } + }); + } catch (SWTException e) { + // display is disposed. Do nothing since we're quitting anyway. + } + } + + private void handleDefaultSelection(Client client) { + mCurrentClient = client; + + // notify of the new default client + for (ISelectionListener listener : mListeners) { + listener.selectionChanged(mCurrentClient); + } + } + + /** + * Prints a message, 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 message + * The message to print. + */ + private static synchronized void printToStream(MessageConsoleStream stream, String tag, String message) { + String dateTag = getMessageTag(tag); + + stream.print(dateTag); + if (!dateTag.endsWith(" ")) { + stream.print(" "); //$NON-NLS-1$ + } + stream.println(message); + } + + /** + * Creates a string containing the current date/time, and the tag + * + * @param tag + * The tag associated to the message. Can be null + * @return The dateTag + */ + private static String getMessageTag(String tag) { + Calendar c = Calendar.getInstance(); + + if (tag == null) { + return String.format(Messages.DdmsPlugin_Message_Tag_Mask_1, c); + } + + return String.format(Messages.DdmsPlugin_Message_Tag_Mask_2, c, tag); + } + + /** + * Implementation of com.android.ddmuilib.StackTracePanel.ISourceRevealer. + */ + @Override + public void reveal(String applicationName, String className, int line) { + JavaSourceRevealer.reveal(applicationName, className, line); + } + + public boolean launchTraceview(String osPath) { + if (mTraceviewLaunchers != null) { + for (ITraceviewLauncher launcher : mTraceviewLaunchers) { + try { + if (launcher.openFile(osPath)) { + return true; + } + } catch (Throwable t) { + // ignore, we'll just not use this implementation. + } + } + } + + return false; + } + + /** + * Returns the list of clients that extend the clientAction extension point. + */ + @NonNull + public synchronized List getClientSpecificActions() { + if (mClientSpecificActions == null) { + // get available client specific action extensions + IConfigurationElement[] elements = findConfigElements("org.eclipse.andmore.ddms.clientAction"); //$NON-NLS-1$ + try { + mClientSpecificActions = instantiateClientSpecificActions(elements); + } catch (CoreException e) { + mClientSpecificActions = Collections.emptyList(); + } + } + + return mClientSpecificActions; + } + + private LogCatMonitor mLogCatMonitor; + + public void startLogCatMonitor(IDevice device) { + if (mLogCatMonitor == null) { + mLogCatMonitor = new LogCatMonitor(getDebuggerConnectors(), getPreferenceStore()); + } + + mLogCatMonitor.monitorDevice(device); + } + + /** + * Returns an image descriptor for the image file at the given plug-in + * relative path + */ + public static ImageDescriptor getImageDescriptor(String path) { + return imageDescriptorFromPlugin(PLUGIN_ID, path); + } +} diff --git a/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/AllocTrackerView.java b/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/AllocTrackerView.java index 3c5b1950..f865dd51 100644 --- a/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/AllocTrackerView.java +++ b/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/AllocTrackerView.java @@ -1,47 +1,51 @@ -/* - * 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 org.eclipse.andmore.ddms.views; - -import com.android.ddmuilib.AllocationPanel; - -import org.eclipse.swt.widgets.Composite; - -public class AllocTrackerView extends TableView { - - public static final String ID = "org.eclipse.andmore.ddms.views.AllocTrackerView"; //$NON-NLS-1$ - private AllocationPanel mPanel; - - public AllocTrackerView() { - } - - @Override - public void createPartControl(Composite parent) { - mPanel = new AllocationPanel(); - mPanel.createPanel(parent); - - setSelectionDependentPanel(mPanel); - - // listen to focus changes for table(s) of the panel. - setupTableFocusListener(mPanel, parent); - } - - @Override - public void setFocus() { - mPanel.setFocus(); - } - -} +/* + * 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 org.eclipse.andmore.ddms.views; + +import com.android.ddmuilib.AllocationPanel; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.andmore.ddms.DdmsPlugin; +import org.eclipse.swt.widgets.Composite; + +public class AllocTrackerView extends TableView { + + public static final String ID = "org.eclipse.andmore.ddms.views.AllocTrackerView"; //$NON-NLS-1$ + private AllocationPanel mPanel; + private ImageFactory mImageFactory; + + public AllocTrackerView() { + super(); + mImageFactory = DdmsPlugin.getDefault().getImageFactory(); + } + + @Override + public void createPartControl(Composite parent) { + mPanel = new AllocationPanel(mImageFactory); + mPanel.createPanel(parent); + + setSelectionDependentPanel(mPanel); + + // listen to focus changes for table(s) of the panel. + setupTableFocusListener(mPanel, parent); + } + + @Override + public void setFocus() { + mPanel.setFocus(); + } +} diff --git a/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/DeviceView.java b/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/DeviceView.java index a467a1a0..2aa62d79 100644 --- a/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/DeviceView.java +++ b/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/DeviceView.java @@ -1,839 +1,839 @@ -/* - * 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 org.eclipse.andmore.ddms.views; - -import com.android.ddmlib.AndroidDebugBridge; -import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; -import com.android.ddmlib.Client; -import com.android.ddmlib.ClientData; -import com.android.ddmlib.ClientData.IHprofDumpHandler; -import com.android.ddmlib.ClientData.MethodProfilingStatus; -import com.android.ddmlib.CollectingOutputReceiver; -import com.android.ddmlib.DdmPreferences; -import com.android.ddmlib.IDevice; -import com.android.ddmlib.SyncException; -import com.android.ddmlib.SyncService; -import com.android.ddmlib.SyncService.ISyncProgressMonitor; -import com.android.ddmlib.TimeoutException; -import com.android.ddmuilib.DevicePanel; -import com.android.ddmuilib.DevicePanel.IUiSelectionListener; -import com.android.ddmuilib.ImageLoader; -import com.android.ddmuilib.ScreenShotDialog; -import com.android.ddmuilib.SyncProgressHelper; -import com.android.ddmuilib.SyncProgressHelper.SyncRunnable; -import com.android.ddmuilib.handler.BaseFileHandler; -import com.android.ddmuilib.handler.MethodProfilingHandler; -import com.android.uiautomator.UiAutomatorHelper; -import com.android.uiautomator.UiAutomatorHelper.UiAutomatorException; -import com.android.uiautomator.UiAutomatorHelper.UiAutomatorResult; -import com.google.common.io.Files; - -import org.eclipse.andmore.ddms.DdmsPlugin; -import org.eclipse.andmore.ddms.IClientAction; -import org.eclipse.andmore.ddms.IDebuggerConnector; -import org.eclipse.andmore.ddms.editors.UiAutomatorViewer; -import org.eclipse.andmore.ddms.i18n.Messages; -import org.eclipse.andmore.ddms.preferences.PreferenceInitializer; -import org.eclipse.andmore.ddms.systrace.ISystraceOptions; -import org.eclipse.andmore.ddms.systrace.ISystraceOptionsDialog; -import org.eclipse.andmore.ddms.systrace.SystraceOptionsDialogV1; -import org.eclipse.andmore.ddms.systrace.SystraceOptionsDialogV2; -import org.eclipse.andmore.ddms.systrace.SystraceOutputParser; -import org.eclipse.andmore.ddms.systrace.SystraceTask; -import org.eclipse.andmore.ddms.systrace.SystraceVersionDetector; -import org.eclipse.core.filesystem.EFS; -import org.eclipse.core.filesystem.IFileStore; -import org.eclipse.core.runtime.IAdaptable; -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.jface.action.Action; -import org.eclipse.jface.action.IAction; -import org.eclipse.jface.action.IMenuManager; -import org.eclipse.jface.action.IToolBarManager; -import org.eclipse.jface.action.Separator; -import org.eclipse.jface.dialogs.ErrorDialog; -import org.eclipse.jface.dialogs.MessageDialog; -import org.eclipse.jface.dialogs.ProgressMonitorDialog; -import org.eclipse.jface.operation.IRunnableWithProgress; -import org.eclipse.jface.preference.IPreferenceStore; -import org.eclipse.jface.resource.ImageDescriptor; -import org.eclipse.jface.window.Window; -import org.eclipse.swt.widgets.Composite; -import org.eclipse.swt.widgets.Display; -import org.eclipse.swt.widgets.Shell; -import org.eclipse.ui.IActionBars; -import org.eclipse.ui.ISharedImages; -import org.eclipse.ui.IWorkbench; -import org.eclipse.ui.IWorkbenchPage; -import org.eclipse.ui.IWorkbenchWindow; -import org.eclipse.ui.PartInitException; -import org.eclipse.ui.PlatformUI; -import org.eclipse.ui.WorkbenchException; -import org.eclipse.ui.ide.IDE; -import org.eclipse.ui.part.ViewPart; - -import java.io.File; -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -public class DeviceView extends ViewPart implements IUiSelectionListener, IClientChangeListener { - - private final static boolean USE_SELECTED_DEBUG_PORT = true; - - public static final String ID = "org.eclipse.andmore.ddms.views.DeviceView"; //$NON-NLS-1$ - - private static DeviceView sThis; - - private Shell mParentShell; - private DevicePanel mDeviceList; - - private Action mResetAdbAction; - private Action mCaptureAction; - private Action mViewUiAutomatorHierarchyAction; - private Action mSystraceAction; - private Action mUpdateThreadAction; - private Action mUpdateHeapAction; - private Action mGcAction; - private Action mKillAppAction; - private Action mDebugAction; - private Action mHprofAction; - private Action mTracingAction; - - private ImageDescriptor mTracingStartImage; - private ImageDescriptor mTracingStopImage; - - public class HProfHandler extends BaseFileHandler implements IHprofDumpHandler { - public final static String ACTION_SAVE = "hprof.save"; //$NON-NLS-1$ - public final static String ACTION_OPEN = "hprof.open"; //$NON-NLS-1$ - - public final static String DOT_HPROF = ".hprof"; //$NON-NLS-1$ - - HProfHandler(Shell parentShell) { - super(parentShell); - } - - @Override - protected String getDialogTitle() { - return Messages.DeviceView_HPROF_Error; - } - - @Override - public void onEndFailure(final Client client, final String message) { - mParentShell.getDisplay().asyncExec(new Runnable() { - @Override - public void run() { - try { - displayErrorFromUiThread(Messages.DeviceView_Unable_Create_HPROF_For_Application, client - .getClientData().getClientDescription(), message != null ? message + "\n\n" : ""); //$NON-NLS-1$ //$NON-NLS-2$ - } 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 - doSelectionChanged(mDeviceList.getSelectedClient()); - } - } - }); - } - - @Override - public void onSuccess(final String remoteFilePath, final Client client) { - mParentShell.getDisplay().asyncExec(new Runnable() { - @Override - 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) { - // get from the preference what action to take - IPreferenceStore store = DdmsPlugin.getDefault().getPreferenceStore(); - String value = store.getString(PreferenceInitializer.ATTR_HPROF_ACTION); - - if (ACTION_OPEN.equals(value)) { - File temp = File.createTempFile("android", DOT_HPROF); //$NON-NLS-1$ - final String tempPath = temp.getAbsolutePath(); - SyncProgressHelper.run(new SyncRunnable() { - - @Override - public void run(ISyncProgressMonitor monitor) throws SyncException, IOException, - TimeoutException { - sync.pullFile(remoteFilePath, tempPath, monitor); - } - - @Override - public void close() { - sync.close(); - } - }, String.format(Messages.DeviceView_Pulling_From_Device, remoteFilePath), mParentShell); - - open(tempPath); - } else { - // default action is ACTION_SAVE - promptAndPull(sync, client.getClientData().getClientDescription() + DOT_HPROF, - remoteFilePath, Messages.DeviceView_Save_HPROF_File); - - } - } else { - displayErrorFromUiThread( - Messages.DeviceView_Unable_Download_HPROF_From_Device_One_Param_First_Message, - device.getSerialNumber()); - } - } catch (SyncException e) { - if (e.wasCanceled() == false) { - displayErrorFromUiThread(Messages.DeviceView_Unable_Download_HPROF_From_Device_Two_Param, - device.getSerialNumber(), e.getMessage()); - } - } catch (Exception e) { - displayErrorFromUiThread( - Messages.DeviceView_Unable_Download_HPROF_From_Device_One_Param_Second_Message, - 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 - doSelectionChanged(mDeviceList.getSelectedClient()); - } - } - }); - } - - @Override - public void onSuccess(final byte[] data, final Client client) { - mParentShell.getDisplay().asyncExec(new Runnable() { - @Override - public void run() { - // get from the preference what action to take - IPreferenceStore store = DdmsPlugin.getDefault().getPreferenceStore(); - String value = store.getString(PreferenceInitializer.ATTR_HPROF_ACTION); - - if (ACTION_OPEN.equals(value)) { - try { - // no need to give an extension since we're going to - // convert the - // file anyway after. - File tempFile = saveTempFile(data, null /* extension */); - open(tempFile.getAbsolutePath()); - } catch (Exception e) { - String errorMsg = e.getMessage(); - displayErrorFromUiThread(Messages.DeviceView_Failed_To_Save_HPROF_Data, - errorMsg != null ? ":\n" + errorMsg : "."); //$NON-NLS-1$ //$NON-NLS-2$ - } - } else { - // default action is ACTION_SAVE - promptAndSave(client.getClientData().getClientDescription() + DOT_HPROF, data, - Messages.DeviceView_Save_HPROF_File); - } - } - }); - } - - private void open(String path) throws IOException, InterruptedException, PartInitException { - // make a temp file to convert the hprof into something - // readable by normal tools - File temp = File.createTempFile("android", DOT_HPROF); //$NON-NLS-1$ - String tempPath = temp.getAbsolutePath(); - - String[] command = new String[3]; - command[0] = DdmsPlugin.getHprofConverter(); - command[1] = path; - command[2] = tempPath; - - Process p = Runtime.getRuntime().exec(command); - p.waitFor(); - - IFileStore fileStore = EFS.getLocalFileSystem().getStore(new Path(tempPath)); - if (!fileStore.fetchInfo().isDirectory() && fileStore.fetchInfo().exists()) { - // before we open the file in an editor window, we make sure the - // current - // workbench page has an editor area (typically the ddms - // perspective doesn't). - IWorkbench workbench = PlatformUI.getWorkbench(); - IWorkbenchWindow window = workbench.getActiveWorkbenchWindow(); - IWorkbenchPage page = window.getActivePage(); - if (page == null) { - return; - } - - if (page.isEditorAreaVisible() == false) { - IAdaptable input; - input = page.getInput(); - try { - workbench.showPerspective("org.eclipse.debug.ui.DebugPerspective", //$NON-NLS-1$ - window, input); - } catch (WorkbenchException e) { - } - } - - IDE.openEditorOnFileStore(page, fileStore); - } - } - } - - public DeviceView() { - // the view is declared with allowMultiple="false" so we - // can safely do this. - sThis = this; - } - - public static DeviceView getInstance() { - return sThis; - } - - @Override - public void createPartControl(Composite parent) { - mParentShell = parent.getShell(); - - ImageLoader loader = ImageLoader.getDdmUiLibLoader(); - - mDeviceList = new DevicePanel(USE_SELECTED_DEBUG_PORT); - mDeviceList.createPanel(parent); - mDeviceList.addSelectionListener(this); - - DdmsPlugin plugin = DdmsPlugin.getDefault(); - mDeviceList.addSelectionListener(plugin); - plugin.setListeningState(true); - - mCaptureAction = new Action(Messages.DeviceView_Screen_Capture) { - @Override - public void run() { - ScreenShotDialog dlg = new ScreenShotDialog(DdmsPlugin.getDisplay().getActiveShell()); - dlg.open(mDeviceList.getSelectedDevice()); - } - }; - mCaptureAction.setToolTipText(Messages.DeviceView_Screen_Capture_Tooltip); - mCaptureAction.setImageDescriptor(loader.loadDescriptor("capture.png")); //$NON-NLS-1$ - - mViewUiAutomatorHierarchyAction = new Action("Dump View Hierarchy for UI Automator") { - @Override - public void run() { - takeUiAutomatorSnapshot(mDeviceList.getSelectedDevice(), DdmsPlugin.getDisplay().getActiveShell()); - } - }; - mViewUiAutomatorHierarchyAction.setToolTipText("Dump View Hierarchy for UI Automator"); - mViewUiAutomatorHierarchyAction.setImageDescriptor(DdmsPlugin.getImageDescriptor("icons/uiautomator.png")); //$NON-NLS-1$ - - mSystraceAction = new Action("Capture System Wide Trace") { - @Override - public void run() { - launchSystrace(mDeviceList.getSelectedDevice(), DdmsPlugin.getDisplay().getActiveShell()); - } - }; - mSystraceAction.setToolTipText("Capture system wide trace using Android systrace"); - mSystraceAction.setImageDescriptor(DdmsPlugin.getImageDescriptor("icons/systrace.png")); //$NON-NLS-1$ - mSystraceAction.setEnabled(true); - - mResetAdbAction = new Action(Messages.DeviceView_Reset_ADB) { - @Override - public void run() { - AndroidDebugBridge bridge = AndroidDebugBridge.getBridge(); - if (bridge != null) { - if (bridge.restart() == false) { - // get the current Display - final Display display = DdmsPlugin.getDisplay(); - - // dialog box only run in ui thread.. - display.asyncExec(new Runnable() { - @Override - public void run() { - Shell shell = display.getActiveShell(); - MessageDialog.openError(shell, Messages.DeviceView_ADB_Error, - Messages.DeviceView_ADB_Failed_Restart); - } - }); - } - } - } - }; - mResetAdbAction.setToolTipText(Messages.DeviceView_Reset_ADB_Host_Deamon); - mResetAdbAction.setImageDescriptor(PlatformUI.getWorkbench().getSharedImages() - .getImageDescriptor(ISharedImages.IMG_OBJS_WARN_TSK)); - - mKillAppAction = new Action() { - @Override - public void run() { - mDeviceList.killSelectedClient(); - } - }; - - mKillAppAction.setText(Messages.DeviceView_Stop_Process); - mKillAppAction.setToolTipText(Messages.DeviceView_Stop_Process_Tooltip); - mKillAppAction.setImageDescriptor(loader.loadDescriptor(DevicePanel.ICON_HALT)); - - mGcAction = new Action() { - @Override - public void run() { - mDeviceList.forceGcOnSelectedClient(); - } - }; - - mGcAction.setText(Messages.DeviceView_Cause_GC); - mGcAction.setToolTipText(Messages.DeviceView_Cause_GC_Tooltip); - mGcAction.setImageDescriptor(loader.loadDescriptor(DevicePanel.ICON_GC)); - - mHprofAction = new Action() { - @Override - public void run() { - mDeviceList.dumpHprof(); - doSelectionChanged(mDeviceList.getSelectedClient()); - } - }; - mHprofAction.setText(Messages.DeviceView_Dump_HPROF_File); - mHprofAction.setToolTipText(Messages.DeviceView_Dump_HPROF_File_Tooltip); - mHprofAction.setImageDescriptor(loader.loadDescriptor(DevicePanel.ICON_HPROF)); - - mUpdateHeapAction = new Action(Messages.DeviceView_Update_Heap, IAction.AS_CHECK_BOX) { - @Override - public void run() { - boolean enable = mUpdateHeapAction.isChecked(); - mDeviceList.setEnabledHeapOnSelectedClient(enable); - } - }; - mUpdateHeapAction.setToolTipText(Messages.DeviceView_Update_Heap_Tooltip); - mUpdateHeapAction.setImageDescriptor(loader.loadDescriptor(DevicePanel.ICON_HEAP)); - - mUpdateThreadAction = new Action(Messages.DeviceView_Threads, IAction.AS_CHECK_BOX) { - @Override - public void run() { - boolean enable = mUpdateThreadAction.isChecked(); - mDeviceList.setEnabledThreadOnSelectedClient(enable); - } - }; - mUpdateThreadAction.setToolTipText(Messages.DeviceView_Threads_Tooltip); - mUpdateThreadAction.setImageDescriptor(loader.loadDescriptor(DevicePanel.ICON_THREAD)); - - mTracingAction = new Action() { - @Override - public void run() { - mDeviceList.toggleMethodProfiling(); - } - }; - mTracingAction.setText(Messages.DeviceView_Start_Method_Profiling); - mTracingAction.setToolTipText(Messages.DeviceView_Start_Method_Profiling_Tooltip); - mTracingStartImage = loader.loadDescriptor(DevicePanel.ICON_TRACING_START); - mTracingStopImage = loader.loadDescriptor(DevicePanel.ICON_TRACING_STOP); - mTracingAction.setImageDescriptor(mTracingStartImage); - - mDebugAction = new Action(Messages.DeviceView_Debug_Process) { - @Override - public void run() { - if (DdmsPlugin.getDefault().hasDebuggerConnectors()) { - Client currentClient = mDeviceList.getSelectedClient(); - if (currentClient != null) { - ClientData clientData = currentClient.getClientData(); - - // make sure the client can be debugged - switch (clientData.getDebuggerConnectionStatus()) { - case ERROR: { - Display display = DdmsPlugin.getDisplay(); - Shell shell = display.getActiveShell(); - MessageDialog.openError(shell, Messages.DeviceView_Debug_Process_Title, - Messages.DeviceView_Process_Debug_Already_In_Use); - return; - } - case ATTACHED: { - Display display = DdmsPlugin.getDisplay(); - Shell shell = display.getActiveShell(); - MessageDialog.openError(shell, Messages.DeviceView_Debug_Process_Title, - Messages.DeviceView_Process_Already_Being_Debugged); - return; - } - } - - // get the name of the client - String packageName = clientData.getClientDescription(); - if (packageName != null) { - - // try all connectors till one returns true. - IDebuggerConnector[] connectors = DdmsPlugin.getDefault().getDebuggerConnectors(); - - if (connectors != null) { - for (IDebuggerConnector connector : connectors) { - try { - if (connector.connectDebugger(packageName, - currentClient.getDebuggerListenPort(), - DdmPreferences.getSelectedDebugPort())) { - return; - } - } catch (Throwable t) { - // ignore, we'll just not use this - // implementation - } - } - } - - // if we get to this point, then we failed to find a - // project - // that matched the application to debug - Display display = DdmsPlugin.getDisplay(); - Shell shell = display.getActiveShell(); - MessageDialog.openError(shell, Messages.DeviceView_Debug_Process_Title, - String.format(Messages.DeviceView_Debug_Session_Failed, packageName)); - } - } - } - } - }; - mDebugAction.setToolTipText(Messages.DeviceView_Debug_Process_Tooltip); - mDebugAction.setImageDescriptor(loader.loadDescriptor("debug-attach.png")); //$NON-NLS-1$ - mDebugAction.setEnabled(DdmsPlugin.getDefault().hasDebuggerConnectors()); - - placeActions(); - - // disabling all action buttons - selectionChanged(null, null); - - ClientData.setHprofDumpHandler(new HProfHandler(mParentShell)); - AndroidDebugBridge.addClientChangeListener(this); - ClientData.setMethodProfilingHandler(new MethodProfilingHandler(mParentShell) { - @Override - protected void open(String tempPath) { - if (DdmsPlugin.getDefault().launchTraceview(tempPath) == false) { - super.open(tempPath); - } - } - }); - } - - private void takeUiAutomatorSnapshot(final IDevice device, final Shell shell) { - ProgressMonitorDialog dialog = new ProgressMonitorDialog(shell); - try { - dialog.run(true, false, new IRunnableWithProgress() { - @Override - public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException { - UiAutomatorResult result = null; - try { - result = UiAutomatorHelper.takeSnapshot(device, monitor); - } catch (UiAutomatorException e) { - throw new InvocationTargetException(e); - } finally { - monitor.done(); - } - - UiAutomatorViewer.openEditor(result); - } - }); - } catch (Exception e) { - Throwable t = e; - if (e instanceof InvocationTargetException) { - t = ((InvocationTargetException) e).getTargetException(); - } - Status s = new Status(IStatus.ERROR, DdmsPlugin.PLUGIN_ID, "Error obtaining UI hierarchy", t); - ErrorDialog.openError(shell, "UI Automator", "Unexpected error while obtaining UI hierarchy", s); - } - }; - - private void launchSystrace(final IDevice device, final Shell parentShell) { - final File systraceAssets = new File(DdmsPlugin.getPlatformToolsFolder(), "systrace"); //$NON-NLS-1$ - if (!systraceAssets.isDirectory()) { - MessageDialog.openError(parentShell, "Systrace", - "Updated version of platform-tools (18.0.1 or greater) is required.\n" - + "Please update your platform-tools using SDK Manager."); - return; - } - - SystraceVersionDetector detector = new SystraceVersionDetector(device); - try { - new ProgressMonitorDialog(parentShell).run(true, false, detector); - } catch (InvocationTargetException e) { - MessageDialog.openError(parentShell, "Systrace", "Unexpected error while detecting atrace version: " + e); - return; - } catch (InterruptedException e) { - return; - } - - final ISystraceOptionsDialog dlg; - if (detector.getVersion() == SystraceVersionDetector.SYSTRACE_V1) { - dlg = new SystraceOptionsDialogV1(parentShell); - } else { - Client[] clients = device.getClients(); - List apps = new ArrayList(clients.length); - for (int i = 0; i < clients.length; i++) { - String name = clients[i].getClientData().getClientDescription(); - if (name != null && !name.isEmpty()) { - apps.add(name); - } - } - dlg = new SystraceOptionsDialogV2(parentShell, detector.getTags(), apps); - } - - if (dlg.open() != Window.OK) { - return; - } - - final ISystraceOptions options = dlg.getSystraceOptions(); - - // set trace tag if necessary: - // adb shell setprop debug.atrace.tags.enableflags - String tag = options.getTags(); - if (tag != null) { - CountDownLatch setTagLatch = new CountDownLatch(1); - CollectingOutputReceiver receiver = new CollectingOutputReceiver(setTagLatch); - try { - String cmd = "setprop debug.atrace.tags.enableflags " + tag; - device.executeShellCommand(cmd, receiver); - setTagLatch.await(5, TimeUnit.SECONDS); - } catch (Exception e) { - MessageDialog.openError(parentShell, "Systrace", "Unexpected error while setting trace tags: " + e); - return; - } - - String shellOutput = receiver.getOutput(); - if (shellOutput.contains("Error type")) { //$NON-NLS-1$ - throw new RuntimeException(receiver.getOutput()); - } - } - - // obtain the output of "adb shell atrace " and generate - // the html file - ProgressMonitorDialog d = new ProgressMonitorDialog(parentShell); - try { - d.run(true, true, new IRunnableWithProgress() { - @Override - public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException { - boolean COMPRESS_DATA = true; - - monitor.setTaskName("Collecting Trace Information"); - final String atraceOptions = options.getOptions() + (COMPRESS_DATA ? " -z" : ""); - SystraceTask task = new SystraceTask(device, atraceOptions); - Thread t = new Thread(task, "Systrace Output Receiver"); - t.start(); - - // check if the user has cancelled tracing every so often - while (true) { - t.join(1000); - - if (t.isAlive()) { - if (monitor.isCanceled()) { - task.cancel(); - return; - } - } else { - break; - } - } - - if (task.getError() != null) { - throw new RuntimeException(task.getError()); - } - - monitor.setTaskName("Saving trace information"); - SystraceOutputParser parser = new SystraceOutputParser(COMPRESS_DATA, SystraceOutputParser - .getJs(systraceAssets), SystraceOutputParser.getCss(systraceAssets), SystraceOutputParser - .getHtmlPrefix(systraceAssets), SystraceOutputParser.getHtmlSuffix(systraceAssets)); - - parser.parse(task.getAtraceOutput()); - - String html = parser.getSystraceHtml(); - try { - Files.write(html.getBytes(), new File(dlg.getTraceFilePath())); - } catch (IOException e) { - throw new InvocationTargetException(e); - } - } - }); - } catch (InvocationTargetException e) { - ErrorDialog.openError(parentShell, "Systrace", "Unable to collect system trace.", new Status(IStatus.ERROR, - DdmsPlugin.PLUGIN_ID, "Unexpected error while collecting system trace.", e.getCause())); - } catch (InterruptedException ignore) { - } - } - - @Override - public void setFocus() { - mDeviceList.setFocus(); - } - - /** - * 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. - */ - @Override - public void selectionChanged(IDevice selectedDevice, Client selectedClient) { - // update the buttons - doSelectionChanged(selectedClient); - doSelectionChanged(selectedDevice); - } - - private void doSelectionChanged(Client selectedClient) { - // update the buttons - if (selectedClient != null) { - if (USE_SELECTED_DEBUG_PORT) { - // set the client as the debug client - selectedClient.setAsSelectedClient(); - } - - mDebugAction.setEnabled(DdmsPlugin.getDefault().hasDebuggerConnectors()); - mKillAppAction.setEnabled(true); - mGcAction.setEnabled(true); - - mUpdateHeapAction.setEnabled(true); - mUpdateHeapAction.setChecked(selectedClient.isHeapUpdateEnabled()); - - mUpdateThreadAction.setEnabled(true); - mUpdateThreadAction.setChecked(selectedClient.isThreadUpdateEnabled()); - - ClientData data = selectedClient.getClientData(); - - if (data.hasFeature(ClientData.FEATURE_HPROF)) { - mHprofAction.setEnabled(data.hasPendingHprofDump() == false); - mHprofAction.setToolTipText(Messages.DeviceView_Dump_HPROF_File); - } else { - mHprofAction.setEnabled(false); - mHprofAction.setToolTipText(Messages.DeviceView_Dump_HPROF_File_Not_Supported_By_VM); - } - - if (data.hasFeature(ClientData.FEATURE_PROFILING)) { - mTracingAction.setEnabled(true); - if (data.getMethodProfilingStatus() == MethodProfilingStatus.TRACER_ON - || data.getMethodProfilingStatus() == MethodProfilingStatus.SAMPLER_ON) { - mTracingAction.setToolTipText(Messages.DeviceView_Stop_Method_Profiling_Tooltip); - mTracingAction.setText(Messages.DeviceView_Stop_Method_Profiling); - mTracingAction.setImageDescriptor(mTracingStopImage); - } else { - mTracingAction.setToolTipText(Messages.DeviceView_Start_Method_Profiling_Tooltip); - mTracingAction.setImageDescriptor(mTracingStartImage); - mTracingAction.setText(Messages.DeviceView_Start_Method_Profiling); - } - } else { - mTracingAction.setEnabled(false); - mTracingAction.setImageDescriptor(mTracingStartImage); - mTracingAction.setToolTipText(Messages.DeviceView_Start_Method_Profiling_Not_Suported_By_Vm); - mTracingAction.setText(Messages.DeviceView_Start_Method_Profiling); - } - } else { - if (USE_SELECTED_DEBUG_PORT) { - // set the client as the debug client - AndroidDebugBridge bridge = AndroidDebugBridge.getBridge(); - if (bridge != null) { - bridge.setSelectedClient(null); - } - } - - mDebugAction.setEnabled(false); - mKillAppAction.setEnabled(false); - mGcAction.setEnabled(false); - mUpdateHeapAction.setChecked(false); - mUpdateHeapAction.setEnabled(false); - mUpdateThreadAction.setEnabled(false); - mUpdateThreadAction.setChecked(false); - mHprofAction.setEnabled(false); - - mHprofAction.setEnabled(false); - mHprofAction.setToolTipText(Messages.DeviceView_Dump_HPROF_File); - - mTracingAction.setEnabled(false); - mTracingAction.setImageDescriptor(mTracingStartImage); - mTracingAction.setToolTipText(Messages.DeviceView_Start_Method_Profiling_Tooltip); - mTracingAction.setText(Messages.DeviceView_Start_Method_Profiling); - } - - for (IClientAction a : DdmsPlugin.getDefault().getClientSpecificActions()) { - a.selectedClientChanged(selectedClient); - } - } - - private void doSelectionChanged(IDevice selectedDevice) { - boolean validDevice = selectedDevice != null; - - mCaptureAction.setEnabled(validDevice); - mViewUiAutomatorHierarchyAction.setEnabled(validDevice); - mSystraceAction.setEnabled(validDevice); - } - - /** - * Place the actions in the ui. - */ - private final void placeActions() { - IActionBars actionBars = getViewSite().getActionBars(); - - // first in the menu - IMenuManager menuManager = actionBars.getMenuManager(); - menuManager.removeAll(); - menuManager.add(mDebugAction); - menuManager.add(new Separator()); - menuManager.add(mUpdateHeapAction); - menuManager.add(mHprofAction); - menuManager.add(mGcAction); - menuManager.add(new Separator()); - menuManager.add(mUpdateThreadAction); - menuManager.add(mTracingAction); - menuManager.add(new Separator()); - menuManager.add(mKillAppAction); - menuManager.add(new Separator()); - menuManager.add(mCaptureAction); - menuManager.add(new Separator()); - menuManager.add(mViewUiAutomatorHierarchyAction); - menuManager.add(new Separator()); - menuManager.add(mSystraceAction); - menuManager.add(new Separator()); - menuManager.add(mResetAdbAction); - for (IClientAction a : DdmsPlugin.getDefault().getClientSpecificActions()) { - menuManager.add(a.getAction()); - } - - // and then in the toolbar - IToolBarManager toolBarManager = actionBars.getToolBarManager(); - toolBarManager.removeAll(); - toolBarManager.add(mDebugAction); - toolBarManager.add(new Separator()); - toolBarManager.add(mUpdateHeapAction); - toolBarManager.add(mHprofAction); - toolBarManager.add(mGcAction); - toolBarManager.add(new Separator()); - toolBarManager.add(mUpdateThreadAction); - toolBarManager.add(mTracingAction); - toolBarManager.add(new Separator()); - toolBarManager.add(mKillAppAction); - toolBarManager.add(new Separator()); - toolBarManager.add(mCaptureAction); - toolBarManager.add(new Separator()); - toolBarManager.add(mViewUiAutomatorHierarchyAction); - toolBarManager.add(new Separator()); - toolBarManager.add(mSystraceAction); - for (IClientAction a : DdmsPlugin.getDefault().getClientSpecificActions()) { - toolBarManager.add(a.getAction()); - } - } - - @Override - public void clientChanged(final Client client, int changeMask) { - if ((changeMask & Client.CHANGE_METHOD_PROFILING_STATUS) == Client.CHANGE_METHOD_PROFILING_STATUS) { - if (mDeviceList.getSelectedClient() == client) { - mParentShell.getDisplay().asyncExec(new Runnable() { - @Override - public void run() { - // force refresh of the button enabled state. - doSelectionChanged(client); - } - }); - } - } - } -} +/* + * 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 org.eclipse.andmore.ddms.views; + +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; +import com.android.ddmlib.Client; +import com.android.ddmlib.ClientData; +import com.android.ddmlib.ClientData.IHprofDumpHandler; +import com.android.ddmlib.ClientData.MethodProfilingStatus; +import com.android.ddmlib.CollectingOutputReceiver; +import com.android.ddmlib.DdmPreferences; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.SyncException; +import com.android.ddmlib.SyncService; +import com.android.ddmlib.SyncService.ISyncProgressMonitor; +import com.android.ddmlib.TimeoutException; +import com.android.ddmuilib.DevicePanel; +import com.android.ddmuilib.DevicePanel.IUiSelectionListener; +import com.android.ddmuilib.ScreenShotDialog; +import com.android.ddmuilib.SyncProgressHelper; +import com.android.ddmuilib.SyncProgressHelper.SyncRunnable; +import com.android.ddmuilib.handler.BaseFileHandler; +import com.android.ddmuilib.handler.MethodProfilingHandler; +import com.android.uiautomator.UiAutomatorHelper; +import com.android.uiautomator.UiAutomatorHelper.UiAutomatorException; +import com.android.uiautomator.UiAutomatorHelper.UiAutomatorResult; +import com.google.common.io.Files; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.andmore.ddms.DdmsPlugin; +import org.eclipse.andmore.ddms.IClientAction; +import org.eclipse.andmore.ddms.IDebuggerConnector; +import org.eclipse.andmore.ddms.editors.UiAutomatorViewer; +import org.eclipse.andmore.ddms.i18n.Messages; +import org.eclipse.andmore.ddms.preferences.PreferenceInitializer; +import org.eclipse.andmore.ddms.systrace.ISystraceOptions; +import org.eclipse.andmore.ddms.systrace.ISystraceOptionsDialog; +import org.eclipse.andmore.ddms.systrace.SystraceOptionsDialogV1; +import org.eclipse.andmore.ddms.systrace.SystraceOptionsDialogV2; +import org.eclipse.andmore.ddms.systrace.SystraceOutputParser; +import org.eclipse.andmore.ddms.systrace.SystraceTask; +import org.eclipse.andmore.ddms.systrace.SystraceVersionDetector; +import org.eclipse.core.filesystem.EFS; +import org.eclipse.core.filesystem.IFileStore; +import org.eclipse.core.runtime.IAdaptable; +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.jface.action.Action; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.action.IMenuManager; +import org.eclipse.jface.action.IToolBarManager; +import org.eclipse.jface.action.Separator; +import org.eclipse.jface.dialogs.ErrorDialog; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.dialogs.ProgressMonitorDialog; +import org.eclipse.jface.operation.IRunnableWithProgress; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.IActionBars; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.WorkbenchException; +import org.eclipse.ui.ide.IDE; +import org.eclipse.ui.part.ViewPart; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class DeviceView extends ViewPart implements IUiSelectionListener, IClientChangeListener { + + private final static boolean USE_SELECTED_DEBUG_PORT = true; + + public static final String ID = "org.eclipse.andmore.ddms.views.DeviceView"; //$NON-NLS-1$ + + private static DeviceView sThis; + + private Shell mParentShell; + private DevicePanel mDeviceList; + + private Action mResetAdbAction; + private Action mCaptureAction; + private Action mViewUiAutomatorHierarchyAction; + private Action mSystraceAction; + private Action mUpdateThreadAction; + private Action mUpdateHeapAction; + private Action mGcAction; + private Action mKillAppAction; + private Action mDebugAction; + private Action mHprofAction; + private Action mTracingAction; + + private ImageDescriptor mTracingStartImage; + private ImageDescriptor mTracingStopImage; + + public class HProfHandler extends BaseFileHandler implements IHprofDumpHandler { + public final static String ACTION_SAVE = "hprof.save"; //$NON-NLS-1$ + public final static String ACTION_OPEN = "hprof.open"; //$NON-NLS-1$ + + public final static String DOT_HPROF = ".hprof"; //$NON-NLS-1$ + + HProfHandler(Shell parentShell) { + super(parentShell); + } + + @Override + protected String getDialogTitle() { + return Messages.DeviceView_HPROF_Error; + } + + @Override + public void onEndFailure(final Client client, final String message) { + mParentShell.getDisplay().asyncExec(new Runnable() { + @Override + public void run() { + try { + displayErrorFromUiThread(Messages.DeviceView_Unable_Create_HPROF_For_Application, client + .getClientData().getClientDescription(), message != null ? message + "\n\n" : ""); //$NON-NLS-1$ //$NON-NLS-2$ + } 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 + doSelectionChanged(mDeviceList.getSelectedClient()); + } + } + }); + } + + @Override + public void onSuccess(final String remoteFilePath, final Client client) { + mParentShell.getDisplay().asyncExec(new Runnable() { + @Override + 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) { + // get from the preference what action to take + IPreferenceStore store = DdmsPlugin.getDefault().getPreferenceStore(); + String value = store.getString(PreferenceInitializer.ATTR_HPROF_ACTION); + + if (ACTION_OPEN.equals(value)) { + File temp = File.createTempFile("android", DOT_HPROF); //$NON-NLS-1$ + final String tempPath = temp.getAbsolutePath(); + SyncProgressHelper.run(new SyncRunnable() { + + @Override + public void run(ISyncProgressMonitor monitor) throws SyncException, IOException, + TimeoutException { + sync.pullFile(remoteFilePath, tempPath, monitor); + } + + @Override + public void close() { + sync.close(); + } + }, String.format(Messages.DeviceView_Pulling_From_Device, remoteFilePath), mParentShell); + + open(tempPath); + } else { + // default action is ACTION_SAVE + promptAndPull(sync, client.getClientData().getClientDescription() + DOT_HPROF, + remoteFilePath, Messages.DeviceView_Save_HPROF_File); + + } + } else { + displayErrorFromUiThread( + Messages.DeviceView_Unable_Download_HPROF_From_Device_One_Param_First_Message, + device.getSerialNumber()); + } + } catch (SyncException e) { + if (e.wasCanceled() == false) { + displayErrorFromUiThread(Messages.DeviceView_Unable_Download_HPROF_From_Device_Two_Param, + device.getSerialNumber(), e.getMessage()); + } + } catch (Exception e) { + displayErrorFromUiThread( + Messages.DeviceView_Unable_Download_HPROF_From_Device_One_Param_Second_Message, + 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 + doSelectionChanged(mDeviceList.getSelectedClient()); + } + } + }); + } + + @Override + public void onSuccess(final byte[] data, final Client client) { + mParentShell.getDisplay().asyncExec(new Runnable() { + @Override + public void run() { + // get from the preference what action to take + IPreferenceStore store = DdmsPlugin.getDefault().getPreferenceStore(); + String value = store.getString(PreferenceInitializer.ATTR_HPROF_ACTION); + + if (ACTION_OPEN.equals(value)) { + try { + // no need to give an extension since we're going to + // convert the + // file anyway after. + File tempFile = saveTempFile(data, null /* extension */); + open(tempFile.getAbsolutePath()); + } catch (Exception e) { + String errorMsg = e.getMessage(); + displayErrorFromUiThread(Messages.DeviceView_Failed_To_Save_HPROF_Data, + errorMsg != null ? ":\n" + errorMsg : "."); //$NON-NLS-1$ //$NON-NLS-2$ + } + } else { + // default action is ACTION_SAVE + promptAndSave(client.getClientData().getClientDescription() + DOT_HPROF, data, + Messages.DeviceView_Save_HPROF_File); + } + } + }); + } + + private void open(String path) throws IOException, InterruptedException, PartInitException { + // make a temp file to convert the hprof into something + // readable by normal tools + File temp = File.createTempFile("android", DOT_HPROF); //$NON-NLS-1$ + String tempPath = temp.getAbsolutePath(); + + String[] command = new String[3]; + command[0] = DdmsPlugin.getHprofConverter(); + command[1] = path; + command[2] = tempPath; + + Process p = Runtime.getRuntime().exec(command); + p.waitFor(); + + IFileStore fileStore = EFS.getLocalFileSystem().getStore(new Path(tempPath)); + if (!fileStore.fetchInfo().isDirectory() && fileStore.fetchInfo().exists()) { + // before we open the file in an editor window, we make sure the + // current + // workbench page has an editor area (typically the ddms + // perspective doesn't). + IWorkbench workbench = PlatformUI.getWorkbench(); + IWorkbenchWindow window = workbench.getActiveWorkbenchWindow(); + IWorkbenchPage page = window.getActivePage(); + if (page == null) { + return; + } + + if (page.isEditorAreaVisible() == false) { + IAdaptable input; + input = page.getInput(); + try { + workbench.showPerspective("org.eclipse.debug.ui.DebugPerspective", //$NON-NLS-1$ + window, input); + } catch (WorkbenchException e) { + } + } + + IDE.openEditorOnFileStore(page, fileStore); + } + } + } + + public DeviceView() { + // the view is declared with allowMultiple="false" so we + // can safely do this. + sThis = this; + } + + public static DeviceView getInstance() { + return sThis; + } + + @Override + public void createPartControl(Composite parent) { + mParentShell = parent.getShell(); + + ImageFactory imageFactory = DdmsPlugin.getDefault().getImageFactory(); + + mDeviceList = new DevicePanel(USE_SELECTED_DEBUG_PORT, imageFactory); + mDeviceList.createPanel(parent); + mDeviceList.addSelectionListener(this); + + DdmsPlugin plugin = DdmsPlugin.getDefault(); + mDeviceList.addSelectionListener(plugin); + plugin.setListeningState(true); + + mCaptureAction = new Action(Messages.DeviceView_Screen_Capture) { + @Override + public void run() { + ScreenShotDialog dlg = new ScreenShotDialog(DdmsPlugin.getDisplay().getActiveShell()); + dlg.open(mDeviceList.getSelectedDevice()); + } + }; + mCaptureAction.setToolTipText(Messages.DeviceView_Screen_Capture_Tooltip); + mCaptureAction.setImageDescriptor(imageFactory.getDescriptorByName("capture.png")); //$NON-NLS-1$ + + mViewUiAutomatorHierarchyAction = new Action("Dump View Hierarchy for UI Automator") { + @Override + public void run() { + takeUiAutomatorSnapshot(mDeviceList.getSelectedDevice(), DdmsPlugin.getDisplay().getActiveShell()); + } + }; + mViewUiAutomatorHierarchyAction.setToolTipText("Dump View Hierarchy for UI Automator"); + mViewUiAutomatorHierarchyAction.setImageDescriptor(DdmsPlugin.getImageDescriptor("icons/uiautomator.png")); //$NON-NLS-1$ + + mSystraceAction = new Action("Capture System Wide Trace") { + @Override + public void run() { + launchSystrace(mDeviceList.getSelectedDevice(), DdmsPlugin.getDisplay().getActiveShell()); + } + }; + mSystraceAction.setToolTipText("Capture system wide trace using Android systrace"); + mSystraceAction.setImageDescriptor(DdmsPlugin.getImageDescriptor("icons/systrace.png")); //$NON-NLS-1$ + mSystraceAction.setEnabled(true); + + mResetAdbAction = new Action(Messages.DeviceView_Reset_ADB) { + @Override + public void run() { + AndroidDebugBridge bridge = AndroidDebugBridge.getBridge(); + if (bridge != null) { + if (bridge.restart() == false) { + // get the current Display + final Display display = DdmsPlugin.getDisplay(); + + // dialog box only run in ui thread.. + display.asyncExec(new Runnable() { + @Override + public void run() { + Shell shell = display.getActiveShell(); + MessageDialog.openError(shell, Messages.DeviceView_ADB_Error, + Messages.DeviceView_ADB_Failed_Restart); + } + }); + } + } + } + }; + mResetAdbAction.setToolTipText(Messages.DeviceView_Reset_ADB_Host_Deamon); + mResetAdbAction.setImageDescriptor(PlatformUI.getWorkbench().getSharedImages() + .getImageDescriptor(ISharedImages.IMG_OBJS_WARN_TSK)); + + mKillAppAction = new Action() { + @Override + public void run() { + mDeviceList.killSelectedClient(); + } + }; + + mKillAppAction.setText(Messages.DeviceView_Stop_Process); + mKillAppAction.setToolTipText(Messages.DeviceView_Stop_Process_Tooltip); + mKillAppAction.setImageDescriptor(imageFactory.getDescriptorByName(DevicePanel.ICON_HALT)); + + mGcAction = new Action() { + @Override + public void run() { + mDeviceList.forceGcOnSelectedClient(); + } + }; + + mGcAction.setText(Messages.DeviceView_Cause_GC); + mGcAction.setToolTipText(Messages.DeviceView_Cause_GC_Tooltip); + mGcAction.setImageDescriptor(imageFactory.getDescriptorByName(DevicePanel.ICON_GC)); + + mHprofAction = new Action() { + @Override + public void run() { + mDeviceList.dumpHprof(); + doSelectionChanged(mDeviceList.getSelectedClient()); + } + }; + mHprofAction.setText(Messages.DeviceView_Dump_HPROF_File); + mHprofAction.setToolTipText(Messages.DeviceView_Dump_HPROF_File_Tooltip); + mHprofAction.setImageDescriptor(imageFactory.getDescriptorByName(DevicePanel.ICON_HPROF)); + + mUpdateHeapAction = new Action(Messages.DeviceView_Update_Heap, IAction.AS_CHECK_BOX) { + @Override + public void run() { + boolean enable = mUpdateHeapAction.isChecked(); + mDeviceList.setEnabledHeapOnSelectedClient(enable); + } + }; + mUpdateHeapAction.setToolTipText(Messages.DeviceView_Update_Heap_Tooltip); + mUpdateHeapAction.setImageDescriptor(imageFactory.getDescriptorByName(DevicePanel.ICON_HEAP)); + + mUpdateThreadAction = new Action(Messages.DeviceView_Threads, IAction.AS_CHECK_BOX) { + @Override + public void run() { + boolean enable = mUpdateThreadAction.isChecked(); + mDeviceList.setEnabledThreadOnSelectedClient(enable); + } + }; + mUpdateThreadAction.setToolTipText(Messages.DeviceView_Threads_Tooltip); + mUpdateThreadAction.setImageDescriptor(imageFactory.getDescriptorByName(DevicePanel.ICON_THREAD)); + + mTracingAction = new Action() { + @Override + public void run() { + mDeviceList.toggleMethodProfiling(); + } + }; + mTracingAction.setText(Messages.DeviceView_Start_Method_Profiling); + mTracingAction.setToolTipText(Messages.DeviceView_Start_Method_Profiling_Tooltip); + mTracingStartImage = imageFactory.getDescriptorByName(DevicePanel.ICON_TRACING_START); + mTracingStopImage = imageFactory.getDescriptorByName(DevicePanel.ICON_TRACING_STOP); + mTracingAction.setImageDescriptor(mTracingStartImage); + + mDebugAction = new Action(Messages.DeviceView_Debug_Process) { + @Override + public void run() { + if (DdmsPlugin.getDefault().hasDebuggerConnectors()) { + Client currentClient = mDeviceList.getSelectedClient(); + if (currentClient != null) { + ClientData clientData = currentClient.getClientData(); + + // make sure the client can be debugged + switch (clientData.getDebuggerConnectionStatus()) { + case ERROR: { + Display display = DdmsPlugin.getDisplay(); + Shell shell = display.getActiveShell(); + MessageDialog.openError(shell, Messages.DeviceView_Debug_Process_Title, + Messages.DeviceView_Process_Debug_Already_In_Use); + return; + } + case ATTACHED: { + Display display = DdmsPlugin.getDisplay(); + Shell shell = display.getActiveShell(); + MessageDialog.openError(shell, Messages.DeviceView_Debug_Process_Title, + Messages.DeviceView_Process_Already_Being_Debugged); + return; + } + } + + // get the name of the client + String packageName = clientData.getClientDescription(); + if (packageName != null) { + + // try all connectors till one returns true. + IDebuggerConnector[] connectors = DdmsPlugin.getDefault().getDebuggerConnectors(); + + if (connectors != null) { + for (IDebuggerConnector connector : connectors) { + try { + if (connector.connectDebugger(packageName, + currentClient.getDebuggerListenPort(), + DdmPreferences.getSelectedDebugPort())) { + return; + } + } catch (Throwable t) { + // ignore, we'll just not use this + // implementation + } + } + } + + // if we get to this point, then we failed to find a + // project + // that matched the application to debug + Display display = DdmsPlugin.getDisplay(); + Shell shell = display.getActiveShell(); + MessageDialog.openError(shell, Messages.DeviceView_Debug_Process_Title, + String.format(Messages.DeviceView_Debug_Session_Failed, packageName)); + } + } + } + } + }; + mDebugAction.setToolTipText(Messages.DeviceView_Debug_Process_Tooltip); + mDebugAction.setImageDescriptor(imageFactory.getDescriptorByName("debug-attach.png")); //$NON-NLS-1$ + mDebugAction.setEnabled(DdmsPlugin.getDefault().hasDebuggerConnectors()); + + placeActions(); + + // disabling all action buttons + selectionChanged(null, null); + + ClientData.setHprofDumpHandler(new HProfHandler(mParentShell)); + AndroidDebugBridge.addClientChangeListener(this); + ClientData.setMethodProfilingHandler(new MethodProfilingHandler(mParentShell) { + @Override + protected void open(String tempPath) { + if (DdmsPlugin.getDefault().launchTraceview(tempPath) == false) { + super.open(tempPath); + } + } + }); + } + + private void takeUiAutomatorSnapshot(final IDevice device, final Shell shell) { + ProgressMonitorDialog dialog = new ProgressMonitorDialog(shell); + try { + dialog.run(true, false, new IRunnableWithProgress() { + @Override + public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException { + UiAutomatorResult result = null; + try { + result = UiAutomatorHelper.takeSnapshot(device, monitor); + } catch (UiAutomatorException e) { + throw new InvocationTargetException(e); + } finally { + monitor.done(); + } + + UiAutomatorViewer.openEditor(result); + } + }); + } catch (Exception e) { + Throwable t = e; + if (e instanceof InvocationTargetException) { + t = ((InvocationTargetException) e).getTargetException(); + } + Status s = new Status(IStatus.ERROR, DdmsPlugin.PLUGIN_ID, "Error obtaining UI hierarchy", t); + ErrorDialog.openError(shell, "UI Automator", "Unexpected error while obtaining UI hierarchy", s); + } + }; + + private void launchSystrace(final IDevice device, final Shell parentShell) { + final File systraceAssets = new File(DdmsPlugin.getPlatformToolsFolder(), "systrace"); //$NON-NLS-1$ + if (!systraceAssets.isDirectory()) { + MessageDialog.openError(parentShell, "Systrace", + "Updated version of platform-tools (18.0.1 or greater) is required.\n" + + "Please update your platform-tools using SDK Manager."); + return; + } + + SystraceVersionDetector detector = new SystraceVersionDetector(device); + try { + new ProgressMonitorDialog(parentShell).run(true, false, detector); + } catch (InvocationTargetException e) { + MessageDialog.openError(parentShell, "Systrace", "Unexpected error while detecting atrace version: " + e); + return; + } catch (InterruptedException e) { + return; + } + + final ISystraceOptionsDialog dlg; + if (detector.getVersion() == SystraceVersionDetector.SYSTRACE_V1) { + dlg = new SystraceOptionsDialogV1(parentShell); + } else { + Client[] clients = device.getClients(); + List apps = new ArrayList(clients.length); + for (int i = 0; i < clients.length; i++) { + String name = clients[i].getClientData().getClientDescription(); + if (name != null && !name.isEmpty()) { + apps.add(name); + } + } + dlg = new SystraceOptionsDialogV2(parentShell, detector.getTags(), apps); + } + + if (dlg.open() != Window.OK) { + return; + } + + final ISystraceOptions options = dlg.getSystraceOptions(); + + // set trace tag if necessary: + // adb shell setprop debug.atrace.tags.enableflags + String tag = options.getTags(); + if (tag != null) { + CountDownLatch setTagLatch = new CountDownLatch(1); + CollectingOutputReceiver receiver = new CollectingOutputReceiver(setTagLatch); + try { + String cmd = "setprop debug.atrace.tags.enableflags " + tag; + device.executeShellCommand(cmd, receiver); + setTagLatch.await(5, TimeUnit.SECONDS); + } catch (Exception e) { + MessageDialog.openError(parentShell, "Systrace", "Unexpected error while setting trace tags: " + e); + return; + } + + String shellOutput = receiver.getOutput(); + if (shellOutput.contains("Error type")) { //$NON-NLS-1$ + throw new RuntimeException(receiver.getOutput()); + } + } + + // obtain the output of "adb shell atrace " and generate + // the html file + ProgressMonitorDialog d = new ProgressMonitorDialog(parentShell); + try { + d.run(true, true, new IRunnableWithProgress() { + @Override + public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException { + boolean COMPRESS_DATA = true; + + monitor.setTaskName("Collecting Trace Information"); + final String atraceOptions = options.getOptions() + (COMPRESS_DATA ? " -z" : ""); + SystraceTask task = new SystraceTask(device, atraceOptions); + Thread t = new Thread(task, "Systrace Output Receiver"); + t.start(); + + // check if the user has cancelled tracing every so often + while (true) { + t.join(1000); + + if (t.isAlive()) { + if (monitor.isCanceled()) { + task.cancel(); + return; + } + } else { + break; + } + } + + if (task.getError() != null) { + throw new RuntimeException(task.getError()); + } + + monitor.setTaskName("Saving trace information"); + SystraceOutputParser parser = new SystraceOutputParser(COMPRESS_DATA, SystraceOutputParser + .getJs(systraceAssets), SystraceOutputParser.getCss(systraceAssets), SystraceOutputParser + .getHtmlPrefix(systraceAssets), SystraceOutputParser.getHtmlSuffix(systraceAssets)); + + parser.parse(task.getAtraceOutput()); + + String html = parser.getSystraceHtml(); + try { + Files.write(html.getBytes(), new File(dlg.getTraceFilePath())); + } catch (IOException e) { + throw new InvocationTargetException(e); + } + } + }); + } catch (InvocationTargetException e) { + ErrorDialog.openError(parentShell, "Systrace", "Unable to collect system trace.", new Status(IStatus.ERROR, + DdmsPlugin.PLUGIN_ID, "Unexpected error while collecting system trace.", e.getCause())); + } catch (InterruptedException ignore) { + } + } + + @Override + public void setFocus() { + mDeviceList.setFocus(); + } + + /** + * 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. + */ + @Override + public void selectionChanged(IDevice selectedDevice, Client selectedClient) { + // update the buttons + doSelectionChanged(selectedClient); + doSelectionChanged(selectedDevice); + } + + private void doSelectionChanged(Client selectedClient) { + // update the buttons + if (selectedClient != null) { + if (USE_SELECTED_DEBUG_PORT) { + // set the client as the debug client + selectedClient.setAsSelectedClient(); + } + + mDebugAction.setEnabled(DdmsPlugin.getDefault().hasDebuggerConnectors()); + mKillAppAction.setEnabled(true); + mGcAction.setEnabled(true); + + mUpdateHeapAction.setEnabled(true); + mUpdateHeapAction.setChecked(selectedClient.isHeapUpdateEnabled()); + + mUpdateThreadAction.setEnabled(true); + mUpdateThreadAction.setChecked(selectedClient.isThreadUpdateEnabled()); + + ClientData data = selectedClient.getClientData(); + + if (data.hasFeature(ClientData.FEATURE_HPROF)) { + mHprofAction.setEnabled(data.hasPendingHprofDump() == false); + mHprofAction.setToolTipText(Messages.DeviceView_Dump_HPROF_File); + } else { + mHprofAction.setEnabled(false); + mHprofAction.setToolTipText(Messages.DeviceView_Dump_HPROF_File_Not_Supported_By_VM); + } + + if (data.hasFeature(ClientData.FEATURE_PROFILING)) { + mTracingAction.setEnabled(true); + if (data.getMethodProfilingStatus() == MethodProfilingStatus.TRACER_ON + || data.getMethodProfilingStatus() == MethodProfilingStatus.SAMPLER_ON) { + mTracingAction.setToolTipText(Messages.DeviceView_Stop_Method_Profiling_Tooltip); + mTracingAction.setText(Messages.DeviceView_Stop_Method_Profiling); + mTracingAction.setImageDescriptor(mTracingStopImage); + } else { + mTracingAction.setToolTipText(Messages.DeviceView_Start_Method_Profiling_Tooltip); + mTracingAction.setImageDescriptor(mTracingStartImage); + mTracingAction.setText(Messages.DeviceView_Start_Method_Profiling); + } + } else { + mTracingAction.setEnabled(false); + mTracingAction.setImageDescriptor(mTracingStartImage); + mTracingAction.setToolTipText(Messages.DeviceView_Start_Method_Profiling_Not_Suported_By_Vm); + mTracingAction.setText(Messages.DeviceView_Start_Method_Profiling); + } + } else { + if (USE_SELECTED_DEBUG_PORT) { + // set the client as the debug client + AndroidDebugBridge bridge = AndroidDebugBridge.getBridge(); + if (bridge != null) { + bridge.setSelectedClient(null); + } + } + + mDebugAction.setEnabled(false); + mKillAppAction.setEnabled(false); + mGcAction.setEnabled(false); + mUpdateHeapAction.setChecked(false); + mUpdateHeapAction.setEnabled(false); + mUpdateThreadAction.setEnabled(false); + mUpdateThreadAction.setChecked(false); + mHprofAction.setEnabled(false); + + mHprofAction.setEnabled(false); + mHprofAction.setToolTipText(Messages.DeviceView_Dump_HPROF_File); + + mTracingAction.setEnabled(false); + mTracingAction.setImageDescriptor(mTracingStartImage); + mTracingAction.setToolTipText(Messages.DeviceView_Start_Method_Profiling_Tooltip); + mTracingAction.setText(Messages.DeviceView_Start_Method_Profiling); + } + + for (IClientAction a : DdmsPlugin.getDefault().getClientSpecificActions()) { + a.selectedClientChanged(selectedClient); + } + } + + private void doSelectionChanged(IDevice selectedDevice) { + boolean validDevice = selectedDevice != null; + + mCaptureAction.setEnabled(validDevice); + mViewUiAutomatorHierarchyAction.setEnabled(validDevice); + mSystraceAction.setEnabled(validDevice); + } + + /** + * Place the actions in the ui. + */ + private final void placeActions() { + IActionBars actionBars = getViewSite().getActionBars(); + + // first in the menu + IMenuManager menuManager = actionBars.getMenuManager(); + menuManager.removeAll(); + menuManager.add(mDebugAction); + menuManager.add(new Separator()); + menuManager.add(mUpdateHeapAction); + menuManager.add(mHprofAction); + menuManager.add(mGcAction); + menuManager.add(new Separator()); + menuManager.add(mUpdateThreadAction); + menuManager.add(mTracingAction); + menuManager.add(new Separator()); + menuManager.add(mKillAppAction); + menuManager.add(new Separator()); + menuManager.add(mCaptureAction); + menuManager.add(new Separator()); + menuManager.add(mViewUiAutomatorHierarchyAction); + menuManager.add(new Separator()); + menuManager.add(mSystraceAction); + menuManager.add(new Separator()); + menuManager.add(mResetAdbAction); + for (IClientAction a : DdmsPlugin.getDefault().getClientSpecificActions()) { + menuManager.add(a.getAction()); + } + + // and then in the toolbar + IToolBarManager toolBarManager = actionBars.getToolBarManager(); + toolBarManager.removeAll(); + toolBarManager.add(mDebugAction); + toolBarManager.add(new Separator()); + toolBarManager.add(mUpdateHeapAction); + toolBarManager.add(mHprofAction); + toolBarManager.add(mGcAction); + toolBarManager.add(new Separator()); + toolBarManager.add(mUpdateThreadAction); + toolBarManager.add(mTracingAction); + toolBarManager.add(new Separator()); + toolBarManager.add(mKillAppAction); + toolBarManager.add(new Separator()); + toolBarManager.add(mCaptureAction); + toolBarManager.add(new Separator()); + toolBarManager.add(mViewUiAutomatorHierarchyAction); + toolBarManager.add(new Separator()); + toolBarManager.add(mSystraceAction); + for (IClientAction a : DdmsPlugin.getDefault().getClientSpecificActions()) { + toolBarManager.add(a.getAction()); + } + } + + @Override + public void clientChanged(final Client client, int changeMask) { + if ((changeMask & Client.CHANGE_METHOD_PROFILING_STATUS) == Client.CHANGE_METHOD_PROFILING_STATUS) { + if (mDeviceList.getSelectedClient() == client) { + mParentShell.getDisplay().asyncExec(new Runnable() { + @Override + public void run() { + // force refresh of the button enabled state. + doSelectionChanged(client); + } + }); + } + } + } +} diff --git a/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/EmulatorControlView.java b/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/EmulatorControlView.java index 73d72625..098d491a 100644 --- a/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/EmulatorControlView.java +++ b/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/EmulatorControlView.java @@ -1,41 +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 org.eclipse.andmore.ddms.views; - -import com.android.ddmuilib.EmulatorControlPanel; - -import org.eclipse.swt.widgets.Composite; - -public class EmulatorControlView extends SelectionDependentViewPart { - - public static final String ID = "org.eclipse.andmore.ddms.views.EmulatorControlView"; //$NON-NLS-1$ - - private EmulatorControlPanel mPanel; - - @Override - public void createPartControl(Composite parent) { - mPanel = new EmulatorControlPanel(); - mPanel.createPanel(parent); - setSelectionDependentPanel(mPanel); - } - - @Override - public void setFocus() { - mPanel.setFocus(); - } - -} +/* + * 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 org.eclipse.andmore.ddms.views; + +import com.android.ddmuilib.EmulatorControlPanel; + +import org.eclipse.andmore.ddms.DdmsPlugin; +import org.eclipse.swt.widgets.Composite; + +public class EmulatorControlView extends SelectionDependentViewPart { + + public static final String ID = "org.eclipse.andmore.ddms.views.EmulatorControlView"; //$NON-NLS-1$ + + private EmulatorControlPanel mPanel; + + @Override + public void createPartControl(Composite parent) { + mPanel = new EmulatorControlPanel(DdmsPlugin.getDefault().getImageFactory()); + mPanel.createPanel(parent); + setSelectionDependentPanel(mPanel); + } + + @Override + public void setFocus() { + mPanel.setFocus(); + } + +} diff --git a/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/EventLogView.java b/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/EventLogView.java index f7260b11..c5166602 100644 --- a/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/EventLogView.java +++ b/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/EventLogView.java @@ -1,110 +1,111 @@ -/* - * 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 org.eclipse.andmore.ddms.views; - -import com.android.ddmuilib.ImageLoader; -import com.android.ddmuilib.log.event.EventLogPanel; - -import org.eclipse.andmore.ddms.CommonAction; -import org.eclipse.andmore.ddms.i18n.Messages; -import org.eclipse.jface.action.IAction; -import org.eclipse.jface.action.IMenuManager; -import org.eclipse.jface.action.IToolBarManager; -import org.eclipse.jface.action.Separator; -import org.eclipse.swt.widgets.Composite; -import org.eclipse.ui.IActionBars; - -public class EventLogView extends SelectionDependentViewPart { - - private EventLogPanel mLogPanel; - - @Override - public void createPartControl(Composite parent) { - ImageLoader loader = ImageLoader.getDdmUiLibLoader(); - - // create the external actions - CommonAction optionsAction = new CommonAction(Messages.EventLogView_Options); - optionsAction.setToolTipText(Messages.EventLogView_Opens_Options_Panel); - optionsAction.setImageDescriptor(loader.loadDescriptor("edit.png")); //$NON-NLS-1$ - - CommonAction clearLogAction = new CommonAction(Messages.EventLogView_Clear_Log); - clearLogAction.setToolTipText(Messages.EventLogView_Clears_Event_Log); - clearLogAction.setImageDescriptor(loader.loadDescriptor("clear.png")); //$NON-NLS-1$ - - CommonAction saveAction = new CommonAction(Messages.EventLogView_Save_Log); - saveAction.setToolTipText(Messages.EventLogView_Saves_Event_Log); - saveAction.setImageDescriptor(loader.loadDescriptor("save.png")); //$NON-NLS-1$ - - CommonAction loadAction = new CommonAction(Messages.EventLogView_Load_Log); - loadAction.setToolTipText(Messages.EventLogView_Loads_Event_Log); - loadAction.setImageDescriptor(loader.loadDescriptor("load.png")); //$NON-NLS-1$ - - CommonAction importBugAction = new CommonAction(Messages.EventLogView_Import_Bug_Report_Log); - importBugAction.setToolTipText(Messages.EventLogView_Imports_Bug_Report); - importBugAction.setImageDescriptor(loader.loadDescriptor("importBug.png")); //$NON-NLS-1$ - - placeActions(optionsAction, clearLogAction, saveAction, loadAction, importBugAction); - - mLogPanel = new EventLogPanel(); - mLogPanel.setActions(optionsAction, clearLogAction, saveAction, loadAction, importBugAction); - mLogPanel.createPanel(parent); - setSelectionDependentPanel(mLogPanel); - } - - @Override - public void setFocus() { - mLogPanel.setFocus(); - } - - @Override - public void dispose() { - if (mLogPanel != null) { - mLogPanel.stopEventLog(true); - } - } - - /** - * Places the actions in the toolbar and in the menu. - * - * @param importBugAction - */ - private void placeActions(IAction optionAction, IAction clearAction, IAction saveAction, IAction loadAction, - CommonAction importBugAction) { - IActionBars actionBars = getViewSite().getActionBars(); - - // first in the menu - IMenuManager menuManager = actionBars.getMenuManager(); - menuManager.add(clearAction); - menuManager.add(new Separator()); - menuManager.add(saveAction); - menuManager.add(loadAction); - menuManager.add(importBugAction); - menuManager.add(new Separator()); - menuManager.add(optionAction); - - // and then in the toolbar - IToolBarManager toolBarManager = actionBars.getToolBarManager(); - toolBarManager.add(clearAction); - toolBarManager.add(new Separator()); - toolBarManager.add(saveAction); - toolBarManager.add(loadAction); - toolBarManager.add(importBugAction); - toolBarManager.add(new Separator()); - toolBarManager.add(optionAction); - } - -} +/* + * 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 org.eclipse.andmore.ddms.views; + +import com.android.ddmuilib.log.event.EventLogPanel; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.andmore.ddms.CommonAction; +import org.eclipse.andmore.ddms.DdmsPlugin; +import org.eclipse.andmore.ddms.i18n.Messages; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.action.IMenuManager; +import org.eclipse.jface.action.IToolBarManager; +import org.eclipse.jface.action.Separator; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.IActionBars; + +public class EventLogView extends SelectionDependentViewPart { + + private EventLogPanel mLogPanel; + + @Override + public void createPartControl(Composite parent) { + ImageFactory imageFactory = DdmsPlugin.getDefault().getImageFactory(); + + // create the external actions + CommonAction optionsAction = new CommonAction(Messages.EventLogView_Options); + optionsAction.setToolTipText(Messages.EventLogView_Opens_Options_Panel); + optionsAction.setImageDescriptor(imageFactory.getDescriptorByName("edit.png")); //$NON-NLS-1$ + + CommonAction clearLogAction = new CommonAction(Messages.EventLogView_Clear_Log); + clearLogAction.setToolTipText(Messages.EventLogView_Clears_Event_Log); + clearLogAction.setImageDescriptor(imageFactory.getDescriptorByName("clear.png")); //$NON-NLS-1$ + + CommonAction saveAction = new CommonAction(Messages.EventLogView_Save_Log); + saveAction.setToolTipText(Messages.EventLogView_Saves_Event_Log); + saveAction.setImageDescriptor(imageFactory.getDescriptorByName("save.png")); //$NON-NLS-1$ + + CommonAction loadAction = new CommonAction(Messages.EventLogView_Load_Log); + loadAction.setToolTipText(Messages.EventLogView_Loads_Event_Log); + loadAction.setImageDescriptor(imageFactory.getDescriptorByName("load.png")); //$NON-NLS-1$ + + CommonAction importBugAction = new CommonAction(Messages.EventLogView_Import_Bug_Report_Log); + importBugAction.setToolTipText(Messages.EventLogView_Imports_Bug_Report); + importBugAction.setImageDescriptor(imageFactory.getDescriptorByName("importBug.png")); //$NON-NLS-1$ + + placeActions(optionsAction, clearLogAction, saveAction, loadAction, importBugAction); + + mLogPanel = new EventLogPanel(imageFactory); + mLogPanel.setActions(optionsAction, clearLogAction, saveAction, loadAction, importBugAction); + mLogPanel.createPanel(parent); + setSelectionDependentPanel(mLogPanel); + } + + @Override + public void setFocus() { + mLogPanel.setFocus(); + } + + @Override + public void dispose() { + if (mLogPanel != null) { + mLogPanel.stopEventLog(true); + } + } + + /** + * Places the actions in the toolbar and in the menu. + * + * @param importBugAction + */ + private void placeActions(IAction optionAction, IAction clearAction, IAction saveAction, IAction loadAction, + CommonAction importBugAction) { + IActionBars actionBars = getViewSite().getActionBars(); + + // first in the menu + IMenuManager menuManager = actionBars.getMenuManager(); + menuManager.add(clearAction); + menuManager.add(new Separator()); + menuManager.add(saveAction); + menuManager.add(loadAction); + menuManager.add(importBugAction); + menuManager.add(new Separator()); + menuManager.add(optionAction); + + // and then in the toolbar + IToolBarManager toolBarManager = actionBars.getToolBarManager(); + toolBarManager.add(clearAction); + toolBarManager.add(new Separator()); + toolBarManager.add(saveAction); + toolBarManager.add(loadAction); + toolBarManager.add(importBugAction); + toolBarManager.add(new Separator()); + toolBarManager.add(optionAction); + } + +} diff --git a/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/FileExplorerView.java b/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/FileExplorerView.java index 93cb09a3..afeb8884 100644 --- a/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/FileExplorerView.java +++ b/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/FileExplorerView.java @@ -1,177 +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 org.eclipse.andmore.ddms.views; - -import com.android.ddmlib.Client; -import com.android.ddmlib.IDevice; -import com.android.ddmuilib.ImageLoader; -import com.android.ddmuilib.explorer.DeviceExplorer; - -import org.eclipse.andmore.ddms.CommonAction; -import org.eclipse.andmore.ddms.DdmsPlugin; -import org.eclipse.andmore.ddms.DdmsPlugin.ISelectionListener; -import org.eclipse.andmore.ddms.i18n.Messages; -import org.eclipse.jface.action.IMenuManager; -import org.eclipse.jface.action.IToolBarManager; -import org.eclipse.jface.action.Separator; -import org.eclipse.swt.graphics.Device; -import org.eclipse.swt.widgets.Composite; -import org.eclipse.ui.IActionBars; -import org.eclipse.ui.ISharedImages; -import org.eclipse.ui.PlatformUI; -import org.eclipse.ui.part.ViewPart; - -public class FileExplorerView extends ViewPart implements ISelectionListener { - - public static final String ID = "org.eclipse.andmore.ddms.views.FileExplorerView"; //$NON-NLS-1$ - - private final static String COLUMN_NAME = DdmsPlugin.PLUGIN_ID + ".explorer.name"; //$NON-NLS-1S - private final static String COLUMN_SIZE = DdmsPlugin.PLUGIN_ID + ".explorer.size"; //$NON-NLS-1S - private final static String COLUMN_DATE = DdmsPlugin.PLUGIN_ID + ".explorer.data"; //$NON-NLS-1S - private final static String COLUMN_TIME = DdmsPlugin.PLUGIN_ID + ".explorer.time"; //$NON-NLS-1S - private final static String COLUMN_PERMISSIONS = DdmsPlugin.PLUGIN_ID + ".explorer.permissions"; //$NON-NLS-1S - private final static String COLUMN_INFO = DdmsPlugin.PLUGIN_ID + ".explorer.info"; //$NON-NLS-1$ - - private DeviceExplorer mExplorer; - - public FileExplorerView() { - } - - @Override - public void createPartControl(Composite parent) { - ImageLoader loader = ImageLoader.getDdmUiLibLoader(); - - DeviceExplorer.COLUMN_NAME = COLUMN_NAME; - DeviceExplorer.COLUMN_SIZE = COLUMN_SIZE; - DeviceExplorer.COLUMN_DATE = COLUMN_DATE; - DeviceExplorer.COLUMN_TIME = COLUMN_TIME; - DeviceExplorer.COLUMN_PERMISSIONS = COLUMN_PERMISSIONS; - DeviceExplorer.COLUMN_INFO = COLUMN_INFO; - - // device explorer - mExplorer = new DeviceExplorer(); - - mExplorer.setCustomImages(PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_OBJ_FILE), - PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_OBJ_FOLDER), null /* - * apk - * image - */, PlatformUI - .getWorkbench().getSharedImages().getImage(ISharedImages.IMG_OBJ_ELEMENT)); - - // creates the actions - CommonAction pushAction = new CommonAction(Messages.FileExplorerView_Push_File) { - @Override - public void run() { - mExplorer.pushIntoSelection(); - } - }; - pushAction.setToolTipText(Messages.FileExplorerView_Push_File_Onto_Device); - pushAction.setImageDescriptor(loader.loadDescriptor("push.png")); //$NON-NLS-1$ - pushAction.setEnabled(false); - - CommonAction pullAction = new CommonAction(Messages.FileExplorerView_Pull_File) { - @Override - public void run() { - mExplorer.pullSelection(); - } - }; - pullAction.setToolTipText(Messages.FileExplorerView_Pull_File_From_File); - pullAction.setImageDescriptor(loader.loadDescriptor("pull.png")); //$NON-NLS-1$ - pullAction.setEnabled(false); - - CommonAction deleteAction = new CommonAction(Messages.FileExplorerView_Delete) { - @Override - public void run() { - mExplorer.deleteSelection(); - } - }; - deleteAction.setToolTipText(Messages.FileExplorerView_Delete_The_Selection); - deleteAction.setImageDescriptor(loader.loadDescriptor("delete.png")); //$NON-NLS-1$ - deleteAction.setEnabled(false); - - CommonAction createNewFolderAction = new CommonAction("New Folder") { - @Override - public void run() { - mExplorer.createNewFolderInSelection(); - } - }; - createNewFolderAction.setToolTipText("New Folder"); - createNewFolderAction.setImageDescriptor(loader.loadDescriptor("add.png")); //$NON-NLS-1$ - createNewFolderAction.setEnabled(false); - - // set up the actions in the explorer - mExplorer.setActions(pushAction, pullAction, deleteAction, createNewFolderAction); - - // and in the ui - IActionBars actionBars = getViewSite().getActionBars(); - IMenuManager menuManager = actionBars.getMenuManager(); - IToolBarManager toolBarManager = actionBars.getToolBarManager(); - - menuManager.add(pullAction); - menuManager.add(pushAction); - menuManager.add(new Separator()); - menuManager.add(deleteAction); - menuManager.add(new Separator()); - menuManager.add(createNewFolderAction); - - toolBarManager.add(pullAction); - toolBarManager.add(pushAction); - toolBarManager.add(new Separator()); - toolBarManager.add(deleteAction); - toolBarManager.add(new Separator()); - toolBarManager.add(createNewFolderAction); - - mExplorer.createPanel(parent); - - DdmsPlugin.getDefault().addSelectionListener(this); - } - - @Override - public void setFocus() { - mExplorer.setFocus(); - } - - /** - * Sent when a new {@link Client} is selected. - * - * @param selectedClient - * The selected client. - */ - @Override - public void selectionChanged(Client selectedClient) { - // pass - } - - /** - * Sent when a new {@link Device} is selected. - * - * @param selectedDevice - * the selected device. - */ - @Override - public void selectionChanged(IDevice selectedDevice) { - mExplorer.switchDevice(selectedDevice); - } - - /** - * Sent when there is no current selection. - */ - public void selectionRemoved() { - - } - -} +/* + * 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 org.eclipse.andmore.ddms.views; + +import com.android.ddmlib.Client; +import com.android.ddmlib.IDevice; +import com.android.ddmuilib.explorer.DeviceExplorer; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.andmore.ddms.CommonAction; +import org.eclipse.andmore.ddms.DdmsPlugin; +import org.eclipse.andmore.ddms.DdmsPlugin.ISelectionListener; +import org.eclipse.andmore.ddms.i18n.Messages; +import org.eclipse.jface.action.IMenuManager; +import org.eclipse.jface.action.IToolBarManager; +import org.eclipse.jface.action.Separator; +import org.eclipse.swt.graphics.Device; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.IActionBars; +import org.eclipse.ui.ISharedImages; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.part.ViewPart; + +public class FileExplorerView extends ViewPart implements ISelectionListener { + + public static final String ID = "org.eclipse.andmore.ddms.views.FileExplorerView"; //$NON-NLS-1$ + + private final static String COLUMN_NAME = DdmsPlugin.PLUGIN_ID + ".explorer.name"; //$NON-NLS-1S + private final static String COLUMN_SIZE = DdmsPlugin.PLUGIN_ID + ".explorer.size"; //$NON-NLS-1S + private final static String COLUMN_DATE = DdmsPlugin.PLUGIN_ID + ".explorer.data"; //$NON-NLS-1S + private final static String COLUMN_TIME = DdmsPlugin.PLUGIN_ID + ".explorer.time"; //$NON-NLS-1S + private final static String COLUMN_PERMISSIONS = DdmsPlugin.PLUGIN_ID + ".explorer.permissions"; //$NON-NLS-1S + private final static String COLUMN_INFO = DdmsPlugin.PLUGIN_ID + ".explorer.info"; //$NON-NLS-1$ + + private DeviceExplorer mExplorer; + + public FileExplorerView() { + } + + @Override + public void createPartControl(Composite parent) { + ImageFactory imageFactory = DdmsPlugin.getDefault().getImageFactory(); + + DeviceExplorer.COLUMN_NAME = COLUMN_NAME; + DeviceExplorer.COLUMN_SIZE = COLUMN_SIZE; + DeviceExplorer.COLUMN_DATE = COLUMN_DATE; + DeviceExplorer.COLUMN_TIME = COLUMN_TIME; + DeviceExplorer.COLUMN_PERMISSIONS = COLUMN_PERMISSIONS; + DeviceExplorer.COLUMN_INFO = COLUMN_INFO; + + // device explorer + mExplorer = new DeviceExplorer(imageFactory); + + mExplorer.setCustomImages(PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_OBJ_FILE), + PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_OBJ_FOLDER), null /* + * apk + * image + */, PlatformUI + .getWorkbench().getSharedImages().getImage(ISharedImages.IMG_OBJ_ELEMENT)); + + // creates the actions + CommonAction pushAction = new CommonAction(Messages.FileExplorerView_Push_File) { + @Override + public void run() { + mExplorer.pushIntoSelection(); + } + }; + pushAction.setToolTipText(Messages.FileExplorerView_Push_File_Onto_Device); + pushAction.setImageDescriptor(imageFactory.getDescriptorByName("push.png")); //$NON-NLS-1$ + pushAction.setEnabled(false); + + CommonAction pullAction = new CommonAction(Messages.FileExplorerView_Pull_File) { + @Override + public void run() { + mExplorer.pullSelection(); + } + }; + pullAction.setToolTipText(Messages.FileExplorerView_Pull_File_From_File); + pullAction.setImageDescriptor(imageFactory.getDescriptorByName("pull.png")); //$NON-NLS-1$ + pullAction.setEnabled(false); + + CommonAction deleteAction = new CommonAction(Messages.FileExplorerView_Delete) { + @Override + public void run() { + mExplorer.deleteSelection(); + } + }; + deleteAction.setToolTipText(Messages.FileExplorerView_Delete_The_Selection); + deleteAction.setImageDescriptor(imageFactory.getDescriptorByName("delete.png")); //$NON-NLS-1$ + deleteAction.setEnabled(false); + + CommonAction createNewFolderAction = new CommonAction("New Folder") { + @Override + public void run() { + mExplorer.createNewFolderInSelection(); + } + }; + createNewFolderAction.setToolTipText("New Folder"); + createNewFolderAction.setImageDescriptor(imageFactory.getDescriptorByName("add.png")); //$NON-NLS-1$ + createNewFolderAction.setEnabled(false); + + // set up the actions in the explorer + mExplorer.setActions(pushAction, pullAction, deleteAction, createNewFolderAction); + + // and in the ui + IActionBars actionBars = getViewSite().getActionBars(); + IMenuManager menuManager = actionBars.getMenuManager(); + IToolBarManager toolBarManager = actionBars.getToolBarManager(); + + menuManager.add(pullAction); + menuManager.add(pushAction); + menuManager.add(new Separator()); + menuManager.add(deleteAction); + menuManager.add(new Separator()); + menuManager.add(createNewFolderAction); + + toolBarManager.add(pullAction); + toolBarManager.add(pushAction); + toolBarManager.add(new Separator()); + toolBarManager.add(deleteAction); + toolBarManager.add(new Separator()); + toolBarManager.add(createNewFolderAction); + + mExplorer.createPanel(parent); + + DdmsPlugin.getDefault().addSelectionListener(this); + } + + @Override + public void setFocus() { + mExplorer.setFocus(); + } + + /** + * Sent when a new {@link Client} is selected. + * + * @param selectedClient + * The selected client. + */ + @Override + public void selectionChanged(Client selectedClient) { + // pass + } + + /** + * Sent when a new {@link Device} is selected. + * + * @param selectedDevice + * the selected device. + */ + @Override + public void selectionChanged(IDevice selectedDevice) { + mExplorer.switchDevice(selectedDevice); + } + + /** + * Sent when there is no current selection. + */ + public void selectionRemoved() { + + } + +} diff --git a/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/LogCatView.java b/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/LogCatView.java index 78573463..9c374237 100644 --- a/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/LogCatView.java +++ b/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/LogCatView.java @@ -1,115 +1,115 @@ -/* - * Copyright (C) 2011 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 org.eclipse.andmore.ddms.views; - -import com.android.ddmlib.logcat.LogCatMessage; -import com.android.ddmuilib.logcat.ILogCatMessageSelectionListener; -import com.android.ddmuilib.logcat.LogCatPanel; -import com.android.ddmuilib.logcat.LogCatStackTraceParser; - -import org.eclipse.andmore.ddms.DdmsPlugin; -import org.eclipse.andmore.ddms.JavaSourceRevealer; -import org.eclipse.andmore.ddms.i18n.Messages; -import org.eclipse.andmore.ddms.preferences.PreferenceInitializer; -import org.eclipse.jface.action.Action; -import org.eclipse.jface.preference.IPreferenceStore; -import org.eclipse.swt.dnd.Clipboard; -import org.eclipse.swt.layout.FillLayout; -import org.eclipse.swt.widgets.Composite; -import org.eclipse.ui.IActionBars; -import org.eclipse.ui.actions.ActionFactory; - -public class LogCatView extends SelectionDependentViewPart { - /** LogCatView ID as defined in plugin.xml. */ - public static final String ID = "org.eclipse.andmore.ddms.views.LogCatView"; //$NON-NLS-1$ - - /** Switch perspective when a Java file is opened from logcat view. */ - public static final boolean DEFAULT_SWITCH_PERSPECTIVE = true; - - /** Target perspective to open when a Java file is opened from logcat view. */ - public static final String DEFAULT_PERSPECTIVE_ID = "org.eclipse.jdt.ui.JavaPerspective"; //$NON-NLS-1$ - - private LogCatPanel mLogCatPanel; - private LogCatStackTraceParser mStackTraceParser = new LogCatStackTraceParser(); - - private Clipboard mClipboard; - - @Override - public void createPartControl(Composite parent) { - parent.setLayout(new FillLayout()); - - IPreferenceStore prefStore = DdmsPlugin.getDefault().getPreferenceStore(); - mLogCatPanel = new LogCatPanel(prefStore); - mLogCatPanel.createPanel(parent); - setSelectionDependentPanel(mLogCatPanel); - - mLogCatPanel.addLogCatMessageSelectionListener(new ILogCatMessageSelectionListener() { - @Override - public void messageDoubleClicked(LogCatMessage m) { - onDoubleClick(m); - } - }); - - mClipboard = new Clipboard(parent.getDisplay()); - IActionBars actionBars = getViewSite().getActionBars(); - actionBars.setGlobalActionHandler(ActionFactory.COPY.getId(), new Action(Messages.LogCatView_Copy) { - @Override - public void run() { - mLogCatPanel.copySelectionToClipboard(mClipboard); - } - }); - - actionBars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(), new Action(Messages.LogCatView_Select_All) { - @Override - public void run() { - mLogCatPanel.selectAll(); - } - }); - - actionBars.setGlobalActionHandler(ActionFactory.FIND.getId(), new Action("Find") { - @Override - public void run() { - mLogCatPanel.showFindDialog(); - } - }); - } - - @Override - public void setFocus() { - } - - private void onDoubleClick(LogCatMessage m) { - String msg = m.getMessage(); - if (!mStackTraceParser.isValidExceptionTrace(msg)) { - return; - } - - IPreferenceStore store = DdmsPlugin.getDefault().getPreferenceStore(); - String perspectiveId = null; - if (store.getBoolean(PreferenceInitializer.ATTR_SWITCH_PERSPECTIVE)) { - perspectiveId = store.getString(PreferenceInitializer.ATTR_PERSPECTIVE_ID); - } - - String fileName = mStackTraceParser.getFileName(msg); - int lineNumber = mStackTraceParser.getLineNumber(msg); - String methodName = mStackTraceParser.getMethodName(msg); - JavaSourceRevealer.revealMethod(methodName, fileName, lineNumber, perspectiveId); - } - - public void selectTransientAppFilter(String appName) { - mLogCatPanel.selectTransientAppFilter(appName); - } -} +/* + * Copyright (C) 2011 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 org.eclipse.andmore.ddms.views; + +import com.android.ddmlib.logcat.LogCatMessage; +import com.android.ddmuilib.logcat.ILogCatMessageSelectionListener; +import com.android.ddmuilib.logcat.LogCatPanel; +import com.android.ddmuilib.logcat.LogCatStackTraceParser; + +import org.eclipse.andmore.ddms.DdmsPlugin; +import org.eclipse.andmore.ddms.JavaSourceRevealer; +import org.eclipse.andmore.ddms.i18n.Messages; +import org.eclipse.andmore.ddms.preferences.PreferenceInitializer; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.IActionBars; +import org.eclipse.ui.actions.ActionFactory; + +public class LogCatView extends SelectionDependentViewPart { + /** LogCatView ID as defined in plugin.xml. */ + public static final String ID = "org.eclipse.andmore.ddms.views.LogCatView"; //$NON-NLS-1$ + + /** Switch perspective when a Java file is opened from logcat view. */ + public static final boolean DEFAULT_SWITCH_PERSPECTIVE = true; + + /** Target perspective to open when a Java file is opened from logcat view. */ + public static final String DEFAULT_PERSPECTIVE_ID = "org.eclipse.jdt.ui.JavaPerspective"; //$NON-NLS-1$ + + private LogCatPanel mLogCatPanel; + private LogCatStackTraceParser mStackTraceParser = new LogCatStackTraceParser(); + + private Clipboard mClipboard; + + @Override + public void createPartControl(Composite parent) { + parent.setLayout(new FillLayout()); + + IPreferenceStore prefStore = DdmsPlugin.getDefault().getPreferenceStore(); + mLogCatPanel = new LogCatPanel(prefStore, DdmsPlugin.getDefault().getImageFactory()); + mLogCatPanel.createPanel(parent); + setSelectionDependentPanel(mLogCatPanel); + + mLogCatPanel.addLogCatMessageSelectionListener(new ILogCatMessageSelectionListener() { + @Override + public void messageDoubleClicked(LogCatMessage m) { + onDoubleClick(m); + } + }); + + mClipboard = new Clipboard(parent.getDisplay()); + IActionBars actionBars = getViewSite().getActionBars(); + actionBars.setGlobalActionHandler(ActionFactory.COPY.getId(), new Action(Messages.LogCatView_Copy) { + @Override + public void run() { + mLogCatPanel.copySelectionToClipboard(mClipboard); + } + }); + + actionBars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(), new Action(Messages.LogCatView_Select_All) { + @Override + public void run() { + mLogCatPanel.selectAll(); + } + }); + + actionBars.setGlobalActionHandler(ActionFactory.FIND.getId(), new Action("Find") { + @Override + public void run() { + mLogCatPanel.showFindDialog(); + } + }); + } + + @Override + public void setFocus() { + } + + private void onDoubleClick(LogCatMessage m) { + String msg = m.getMessage(); + if (!mStackTraceParser.isValidExceptionTrace(msg)) { + return; + } + + IPreferenceStore store = DdmsPlugin.getDefault().getPreferenceStore(); + String perspectiveId = null; + if (store.getBoolean(PreferenceInitializer.ATTR_SWITCH_PERSPECTIVE)) { + perspectiveId = store.getString(PreferenceInitializer.ATTR_PERSPECTIVE_ID); + } + + String fileName = mStackTraceParser.getFileName(msg); + int lineNumber = mStackTraceParser.getLineNumber(msg); + String methodName = mStackTraceParser.getMethodName(msg); + JavaSourceRevealer.revealMethod(methodName, fileName, lineNumber, perspectiveId); + } + + public void selectTransientAppFilter(String appName) { + mLogCatPanel.selectTransientAppFilter(appName); + } +} diff --git a/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/OldLogCatView.java b/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/OldLogCatView.java index 4a4b5c16..51a7bb54 100644 --- a/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/OldLogCatView.java +++ b/android-core/plugins/org.eclipse.andmore.ddms/src/org/eclipse/andmore/ddms/views/OldLogCatView.java @@ -1,371 +1,371 @@ -/* - * 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 org.eclipse.andmore.ddms.views; - -import com.android.ddmlib.Log.LogLevel; -import com.android.ddmuilib.ImageLoader; -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 com.android.ddmuilib.logcat.LogPanel.LogCatViewInterface; - -import org.eclipse.andmore.ddms.CommonAction; -import org.eclipse.andmore.ddms.DdmsPlugin; -import org.eclipse.andmore.ddms.i18n.Messages; -import org.eclipse.andmore.ddms.preferences.PreferenceInitializer; -import org.eclipse.core.resources.IFile; -import org.eclipse.core.resources.IMarker; -import org.eclipse.core.runtime.CoreException; -import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.Status; -import org.eclipse.jface.action.Action; -import org.eclipse.jface.action.IAction; -import org.eclipse.jface.action.IMenuManager; -import org.eclipse.jface.action.IToolBarManager; -import org.eclipse.jface.action.Separator; -import org.eclipse.jface.preference.IPreferenceStore; -import org.eclipse.swt.dnd.Clipboard; -import org.eclipse.swt.graphics.Color; -import org.eclipse.swt.graphics.Font; -import org.eclipse.swt.graphics.FontData; -import org.eclipse.swt.widgets.Composite; -import org.eclipse.swt.widgets.Display; -import org.eclipse.ui.IActionBars; -import org.eclipse.ui.IPerspectiveRegistry; -import org.eclipse.ui.IWorkbench; -import org.eclipse.ui.IWorkbenchPage; -import org.eclipse.ui.IWorkbenchWindow; -import org.eclipse.ui.PlatformUI; -import org.eclipse.ui.WorkbenchException; -import org.eclipse.ui.actions.ActionFactory; -import org.eclipse.ui.ide.IDE; - -import java.util.ArrayList; - -/** - * The log cat view displays log output from the current device selection. - */ -public final class OldLogCatView extends SelectionDependentViewPart implements LogCatViewInterface { - - public static final String ID = "org.eclipse.andmore.ddms.views.OldLogCatView"; //$NON-NLS-1$ - - private static final String PREFS_COL_TIME = DdmsPlugin.PLUGIN_ID + ".logcat.time"; //$NON-NLS-1$ - private static final String PREFS_COL_LEVEL = DdmsPlugin.PLUGIN_ID + ".logcat.level"; //$NON-NLS-1$ - private static final String PREFS_COL_PID = DdmsPlugin.PLUGIN_ID + ".logcat.pid"; //$NON-NLS-1$ - private static final String PREFS_COL_TAG = DdmsPlugin.PLUGIN_ID + ".logcat.tag"; //$NON-NLS-1$ - private static final String PREFS_COL_MESSAGE = DdmsPlugin.PLUGIN_ID + ".logcat.message"; //$NON-NLS-1$ - - private static final String PREFS_FILTERS = DdmsPlugin.PLUGIN_ID + ".logcat.filters"; //$NON-NLS-1$ - - public static final String CHOICE_METHOD_DECLARATION = DdmsPlugin.PLUGIN_ID + ".logcat.MethodDeclaration"; //$NON-NLS-1$ - public static final String CHOICE_ERROR_LINE = DdmsPlugin.PLUGIN_ID + ".logcat.ErrorLine"; //$NON-NLS-1$ - - /* Default values for the switch of perspective. */ - public static final boolean DEFAULT_SWITCH_PERSPECTIVE = true; - public static final String DEFAULT_PERSPECTIVE_ID = "org.eclipse.jdt.ui.JavaPerspective"; //$NON-NLS-1$ - private static OldLogCatView sThis; - private LogPanel mLogPanel; - - private CommonAction mCreateFilterAction; - private CommonAction mDeleteFilterAction; - private CommonAction mEditFilterAction; - private CommonAction mExportAction; - - private CommonAction[] 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 - }; - - private Action mClearAction; - - private Clipboard mClipboard; - - /** - * An implementation of {@link ILogFilterStorageManager} to bridge to the - * eclipse preference store, and saves the log filters. - */ - private final class FilterStorage implements ILogFilterStorageManager { - - @Override - public LogFilter[] getFilterFromStore() { - String filterPrefs = DdmsPlugin.getDefault().getPreferenceStore().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()]); - } - - @Override - public void saveFilters(LogFilter[] filters) { - StringBuilder sb = new StringBuilder(); - for (LogFilter f : filters) { - String filterString = f.toString(); - sb.append(filterString); - sb.append('|'); - } - - DdmsPlugin.getDefault().getPreferenceStore().setValue(PREFS_FILTERS, sb.toString()); - } - - @Override - public boolean requiresDefaultFilter() { - return true; - } - } - - public OldLogCatView() { - sThis = this; - 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; - } - - /** - * Returns the singleton instance. - */ - public static OldLogCatView getInstance() { - return sThis; - } - - /** - * Sets the display font. - * - * @param font - * The font. - */ - public static void setFont(Font font) { - if (sThis != null && sThis.mLogPanel != null) { - sThis.mLogPanel.setFont(font); - } - } - - @Override - public void createPartControl(Composite parent) { - Display d = parent.getDisplay(); - LogColors colors = new LogColors(); - - ImageLoader loader = ImageLoader.getDdmUiLibLoader(); - - colors.infoColor = new Color(d, 0, 127, 0); - colors.debugColor = new Color(d, 0, 0, 127); - colors.errorColor = new Color(d, 255, 0, 0); - colors.warningColor = new Color(d, 255, 127, 0); - colors.verboseColor = new Color(d, 0, 0, 0); - - mCreateFilterAction = new CommonAction(Messages.LogCatView_Create_Filter) { - @Override - public void run() { - mLogPanel.addFilter(); - } - }; - mCreateFilterAction.setToolTipText(Messages.LogCatView_Create_Filter_Tooltip); - mCreateFilterAction.setImageDescriptor(loader.loadDescriptor("add.png")); //$NON-NLS-1$ - - mEditFilterAction = new CommonAction(Messages.LogCatView_Edit_Filter) { - @Override - public void run() { - mLogPanel.editFilter(); - } - }; - mEditFilterAction.setToolTipText(Messages.LogCatView_Edit_Filter_Tooltip); - mEditFilterAction.setImageDescriptor(loader.loadDescriptor("edit.png")); //$NON-NLS-1$ - - mDeleteFilterAction = new CommonAction(Messages.LogCatView_Delete_Filter) { - @Override - public void run() { - mLogPanel.deleteFilter(); - } - }; - mDeleteFilterAction.setToolTipText(Messages.LogCatView_Delete_Filter_Tooltip); - mDeleteFilterAction.setImageDescriptor(loader.loadDescriptor("delete.png")); //$NON-NLS-1$ - - mExportAction = new CommonAction(Messages.LogCatView_Export_Selection_As_Text) { - @Override - public void run() { - mLogPanel.save(); - } - }; - mExportAction.setToolTipText(Messages.LogCatView_Export_Selection_As_Text_Tooltip); - mExportAction.setImageDescriptor(loader.loadDescriptor("save.png")); //$NON-NLS-1$ - - LogLevel[] levels = LogLevel.values(); - mLogLevelActions = new CommonAction[mLogLevelIcons.length]; - for (int i = 0; i < mLogLevelActions.length; i++) { - String name = levels[i].getStringValue(); - mLogLevelActions[i] = new CommonAction(name, IAction.AS_CHECK_BOX) { - @Override - public void run() { - // disable the other actions and record current index - for (int j = 0; j < mLogLevelActions.length; j++) { - Action a = mLogLevelActions[j]; - if (a == this) { - a.setChecked(true); - - // set the log level - mLogPanel.setCurrentFilterLogLevel(j + 2); - } else { - a.setChecked(false); - } - } - } - }; - - mLogLevelActions[i].setToolTipText(name); - mLogLevelActions[i].setImageDescriptor(loader.loadDescriptor(mLogLevelIcons[i])); - } - - mClearAction = new Action(Messages.LogCatView_Clear_Log) { - @Override - public void run() { - mLogPanel.clear(); - } - }; - mClearAction.setImageDescriptor(loader.loadDescriptor("clear.png")); //$NON-NLS-1$ - - // now create the log view - mLogPanel = new LogPanel(colors, new FilterStorage(), LogPanel.FILTER_MANUAL); - mLogPanel.setLogCatViewInterface(this); - mLogPanel.setActions(mDeleteFilterAction, mEditFilterAction, mLogLevelActions); - - // get the font - String fontStr = DdmsPlugin.getDefault().getPreferenceStore().getString(PreferenceInitializer.ATTR_LOGCAT_FONT); - if (fontStr != null) { - FontData data = new FontData(fontStr); - - if (fontStr != null) { - mLogPanel.setFont(new Font(parent.getDisplay(), data)); - } - } - - mLogPanel.createPanel(parent); - setSelectionDependentPanel(mLogPanel); - - // place the actions. - placeActions(); - - // setup the copy action - mClipboard = new Clipboard(d); - IActionBars actionBars = getViewSite().getActionBars(); - actionBars.setGlobalActionHandler(ActionFactory.COPY.getId(), new Action(Messages.LogCatView_Copy) { - @Override - public void run() { - mLogPanel.copy(mClipboard); - } - }); - - // setup the select all action - actionBars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(), new Action(Messages.LogCatView_Select_All) { - @Override - public void run() { - mLogPanel.selectAll(); - } - }); - } - - @Override - public void dispose() { - mLogPanel.stopLogCat(true); - mClipboard.dispose(); - } - - @Override - public void setFocus() { - mLogPanel.setFocus(); - } - - /** - * Place the actions in the ui. - */ - private void placeActions() { - IActionBars actionBars = getViewSite().getActionBars(); - - // first in the menu - IMenuManager menuManager = actionBars.getMenuManager(); - menuManager.add(mCreateFilterAction); - menuManager.add(mEditFilterAction); - menuManager.add(mDeleteFilterAction); - menuManager.add(new Separator()); - menuManager.add(mClearAction); - menuManager.add(new Separator()); - menuManager.add(mExportAction); - - // and then in the toolbar - IToolBarManager toolBarManager = actionBars.getToolBarManager(); - for (CommonAction a : mLogLevelActions) { - toolBarManager.add(a); - } - toolBarManager.add(new Separator()); - toolBarManager.add(mCreateFilterAction); - toolBarManager.add(mEditFilterAction); - toolBarManager.add(mDeleteFilterAction); - toolBarManager.add(new Separator()); - toolBarManager.add(mClearAction); - } - - void openFile(IFile file, IMarker marker) { - try { - IWorkbenchPage page = getViewSite().getWorkbenchWindow().getActivePage(); - if (page != null) { - IDE.openEditor(page, marker); - marker.delete(); - } - } catch (CoreException e) { - Status s = new Status(IStatus.ERROR, DdmsPlugin.PLUGIN_ID, e.getMessage(), e); - DdmsPlugin.getDefault().getLog().log(s); - } - } - - void switchPerspective() { - IPreferenceStore store = DdmsPlugin.getDefault().getPreferenceStore(); - if (store.getBoolean(PreferenceInitializer.ATTR_SWITCH_PERSPECTIVE)) { - IWorkbench workbench = PlatformUI.getWorkbench(); - IWorkbenchWindow window = workbench.getActiveWorkbenchWindow(); - IPerspectiveRegistry perspectiveRegistry = workbench.getPerspectiveRegistry(); - String perspectiveId = store.getString(PreferenceInitializer.ATTR_PERSPECTIVE_ID); - if (perspectiveId != null && perspectiveId.length() > 0 - && perspectiveRegistry.findPerspectiveWithId(perspectiveId) != null) { - try { - workbench.showPerspective(perspectiveId, window); - } catch (WorkbenchException e) { - e.printStackTrace(); - } - } - } - } - - @Override - public void onDoubleClick() { - } -} +/* + * 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 org.eclipse.andmore.ddms.views; + +import com.android.ddmlib.Log.LogLevel; +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 com.android.ddmuilib.logcat.LogPanel.LogCatViewInterface; + +import org.eclipse.andmore.base.resources.ImageFactory; +import org.eclipse.andmore.ddms.CommonAction; +import org.eclipse.andmore.ddms.DdmsPlugin; +import org.eclipse.andmore.ddms.i18n.Messages; +import org.eclipse.andmore.ddms.preferences.PreferenceInitializer; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.action.IMenuManager; +import org.eclipse.jface.action.IToolBarManager; +import org.eclipse.jface.action.Separator; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.IActionBars; +import org.eclipse.ui.IPerspectiveRegistry; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.WorkbenchException; +import org.eclipse.ui.actions.ActionFactory; +import org.eclipse.ui.ide.IDE; + +import java.util.ArrayList; + +/** + * The log cat view displays log output from the current device selection. + */ +public final class OldLogCatView extends SelectionDependentViewPart implements LogCatViewInterface { + + public static final String ID = "org.eclipse.andmore.ddms.views.OldLogCatView"; //$NON-NLS-1$ + + private static final String PREFS_COL_TIME = DdmsPlugin.PLUGIN_ID + ".logcat.time"; //$NON-NLS-1$ + private static final String PREFS_COL_LEVEL = DdmsPlugin.PLUGIN_ID + ".logcat.level"; //$NON-NLS-1$ + private static final String PREFS_COL_PID = DdmsPlugin.PLUGIN_ID + ".logcat.pid"; //$NON-NLS-1$ + private static final String PREFS_COL_TAG = DdmsPlugin.PLUGIN_ID + ".logcat.tag"; //$NON-NLS-1$ + private static final String PREFS_COL_MESSAGE = DdmsPlugin.PLUGIN_ID + ".logcat.message"; //$NON-NLS-1$ + + private static final String PREFS_FILTERS = DdmsPlugin.PLUGIN_ID + ".logcat.filters"; //$NON-NLS-1$ + + public static final String CHOICE_METHOD_DECLARATION = DdmsPlugin.PLUGIN_ID + ".logcat.MethodDeclaration"; //$NON-NLS-1$ + public static final String CHOICE_ERROR_LINE = DdmsPlugin.PLUGIN_ID + ".logcat.ErrorLine"; //$NON-NLS-1$ + + /* Default values for the switch of perspective. */ + public static final boolean DEFAULT_SWITCH_PERSPECTIVE = true; + public static final String DEFAULT_PERSPECTIVE_ID = "org.eclipse.jdt.ui.JavaPerspective"; //$NON-NLS-1$ + private static OldLogCatView sThis; + private LogPanel mLogPanel; + + private CommonAction mCreateFilterAction; + private CommonAction mDeleteFilterAction; + private CommonAction mEditFilterAction; + private CommonAction mExportAction; + + private CommonAction[] 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 + }; + + private Action mClearAction; + private ImageFactory mImageFactory; + private Clipboard mClipboard; + + /** + * An implementation of {@link ILogFilterStorageManager} to bridge to the + * eclipse preference store, and saves the log filters. + */ + private final class FilterStorage implements ILogFilterStorageManager { + + @Override + public LogFilter[] getFilterFromStore() { + String filterPrefs = DdmsPlugin.getDefault().getPreferenceStore().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()]); + } + + @Override + public void saveFilters(LogFilter[] filters) { + StringBuilder sb = new StringBuilder(); + for (LogFilter f : filters) { + String filterString = f.toString(); + sb.append(filterString); + sb.append('|'); + } + + DdmsPlugin.getDefault().getPreferenceStore().setValue(PREFS_FILTERS, sb.toString()); + } + + @Override + public boolean requiresDefaultFilter() { + return true; + } + } + + public OldLogCatView(ImageFactory imageFactory) { + sThis = this; + mImageFactory = imageFactory; + 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; + } + + /** + * Returns the singleton instance. + */ + public static OldLogCatView getInstance() { + return sThis; + } + + /** + * Sets the display font. + * + * @param font + * The font. + */ + public static void setFont(Font font) { + if (sThis != null && sThis.mLogPanel != null) { + sThis.mLogPanel.setFont(font); + } + } + + @Override + public void createPartControl(Composite parent) { + Display d = parent.getDisplay(); + LogColors colors = new LogColors(); + + ImageFactory imageFactory = DdmsPlugin.getDefault().getImageFactory(); + colors.infoColor = new Color(d, 0, 127, 0); + colors.debugColor = new Color(d, 0, 0, 127); + colors.errorColor = new Color(d, 255, 0, 0); + colors.warningColor = new Color(d, 255, 127, 0); + colors.verboseColor = new Color(d, 0, 0, 0); + + mCreateFilterAction = new CommonAction(Messages.LogCatView_Create_Filter) { + @Override + public void run() { + mLogPanel.addFilter(); + } + }; + mCreateFilterAction.setToolTipText(Messages.LogCatView_Create_Filter_Tooltip); + mCreateFilterAction.setImageDescriptor(imageFactory.getDescriptorByName("add.png")); //$NON-NLS-1$ + + mEditFilterAction = new CommonAction(Messages.LogCatView_Edit_Filter) { + @Override + public void run() { + mLogPanel.editFilter(); + } + }; + mEditFilterAction.setToolTipText(Messages.LogCatView_Edit_Filter_Tooltip); + mEditFilterAction.setImageDescriptor(imageFactory.getDescriptorByName("edit.png")); //$NON-NLS-1$ + + mDeleteFilterAction = new CommonAction(Messages.LogCatView_Delete_Filter) { + @Override + public void run() { + mLogPanel.deleteFilter(); + } + }; + mDeleteFilterAction.setToolTipText(Messages.LogCatView_Delete_Filter_Tooltip); + mDeleteFilterAction.setImageDescriptor(imageFactory.getDescriptorByName("delete.png")); //$NON-NLS-1$ + + mExportAction = new CommonAction(Messages.LogCatView_Export_Selection_As_Text) { + @Override + public void run() { + mLogPanel.save(); + } + }; + mExportAction.setToolTipText(Messages.LogCatView_Export_Selection_As_Text_Tooltip); + mExportAction.setImageDescriptor(imageFactory.getDescriptorByName("save.png")); //$NON-NLS-1$ + + LogLevel[] levels = LogLevel.values(); + mLogLevelActions = new CommonAction[mLogLevelIcons.length]; + for (int i = 0; i < mLogLevelActions.length; i++) { + String name = levels[i].getStringValue(); + mLogLevelActions[i] = new CommonAction(name, IAction.AS_CHECK_BOX) { + @Override + public void run() { + // disable the other actions and record current index + for (int j = 0; j < mLogLevelActions.length; j++) { + Action a = mLogLevelActions[j]; + if (a == this) { + a.setChecked(true); + + // set the log level + mLogPanel.setCurrentFilterLogLevel(j + 2); + } else { + a.setChecked(false); + } + } + } + }; + + mLogLevelActions[i].setToolTipText(name); + mLogLevelActions[i].setImageDescriptor(imageFactory.getDescriptorByName(mLogLevelIcons[i])); + } + + mClearAction = new Action(Messages.LogCatView_Clear_Log) { + @Override + public void run() { + mLogPanel.clear(); + } + }; + mClearAction.setImageDescriptor(imageFactory.getDescriptorByName("clear.png")); //$NON-NLS-1$ + + // now create the log view + mLogPanel = new LogPanel(colors, new FilterStorage(), LogPanel.FILTER_MANUAL, mImageFactory); + mLogPanel.setLogCatViewInterface(this); + mLogPanel.setActions(mDeleteFilterAction, mEditFilterAction, mLogLevelActions); + + // get the font + String fontStr = DdmsPlugin.getDefault().getPreferenceStore().getString(PreferenceInitializer.ATTR_LOGCAT_FONT); + if (fontStr != null) { + FontData data = new FontData(fontStr); + + if (fontStr != null) { + mLogPanel.setFont(new Font(parent.getDisplay(), data)); + } + } + + mLogPanel.createPanel(parent); + setSelectionDependentPanel(mLogPanel); + + // place the actions. + placeActions(); + + // setup the copy action + mClipboard = new Clipboard(d); + IActionBars actionBars = getViewSite().getActionBars(); + actionBars.setGlobalActionHandler(ActionFactory.COPY.getId(), new Action(Messages.LogCatView_Copy) { + @Override + public void run() { + mLogPanel.copy(mClipboard); + } + }); + + // setup the select all action + actionBars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(), new Action(Messages.LogCatView_Select_All) { + @Override + public void run() { + mLogPanel.selectAll(); + } + }); + } + + @Override + public void dispose() { + mLogPanel.stopLogCat(true); + mClipboard.dispose(); + } + + @Override + public void setFocus() { + mLogPanel.setFocus(); + } + + /** + * Place the actions in the ui. + */ + private void placeActions() { + IActionBars actionBars = getViewSite().getActionBars(); + + // first in the menu + IMenuManager menuManager = actionBars.getMenuManager(); + menuManager.add(mCreateFilterAction); + menuManager.add(mEditFilterAction); + menuManager.add(mDeleteFilterAction); + menuManager.add(new Separator()); + menuManager.add(mClearAction); + menuManager.add(new Separator()); + menuManager.add(mExportAction); + + // and then in the toolbar + IToolBarManager toolBarManager = actionBars.getToolBarManager(); + for (CommonAction a : mLogLevelActions) { + toolBarManager.add(a); + } + toolBarManager.add(new Separator()); + toolBarManager.add(mCreateFilterAction); + toolBarManager.add(mEditFilterAction); + toolBarManager.add(mDeleteFilterAction); + toolBarManager.add(new Separator()); + toolBarManager.add(mClearAction); + } + + void openFile(IFile file, IMarker marker) { + try { + IWorkbenchPage page = getViewSite().getWorkbenchWindow().getActivePage(); + if (page != null) { + IDE.openEditor(page, marker); + marker.delete(); + } + } catch (CoreException e) { + Status s = new Status(IStatus.ERROR, DdmsPlugin.PLUGIN_ID, e.getMessage(), e); + DdmsPlugin.getDefault().getLog().log(s); + } + } + + void switchPerspective() { + IPreferenceStore store = DdmsPlugin.getDefault().getPreferenceStore(); + if (store.getBoolean(PreferenceInitializer.ATTR_SWITCH_PERSPECTIVE)) { + IWorkbench workbench = PlatformUI.getWorkbench(); + IWorkbenchWindow window = workbench.getActiveWorkbenchWindow(); + IPerspectiveRegistry perspectiveRegistry = workbench.getPerspectiveRegistry(); + String perspectiveId = store.getString(PreferenceInitializer.ATTR_PERSPECTIVE_ID); + if (perspectiveId != null && perspectiveId.length() > 0 + && perspectiveRegistry.findPerspectiveWithId(perspectiveId) != null) { + try { + workbench.showPerspective(perspectiveId, window); + } catch (WorkbenchException e) { + e.printStackTrace(); + } + } + } + } + + @Override + public void onDoubleClick() { + } +} diff --git a/android-core/plugins/org.eclipse.andmore.gldebugger.tests/.classpath b/android-core/plugins/org.eclipse.andmore.gldebugger.tests/.classpath index 2abf4833..0c695ecb 100644 --- a/android-core/plugins/org.eclipse.andmore.gldebugger.tests/.classpath +++ b/android-core/plugins/org.eclipse.andmore.gldebugger.tests/.classpath @@ -1,11 +1,11 @@ - - - - - - - - - - - + + + + + + + + + + + diff --git a/android-core/plugins/org.eclipse.andmore.gldebugger/.classpath b/android-core/plugins/org.eclipse.andmore.gldebugger/.classpath index 2edb66f5..da24a626 100755 --- a/android-core/plugins/org.eclipse.andmore.gldebugger/.classpath +++ b/android-core/plugins/org.eclipse.andmore.gldebugger/.classpath @@ -1,9 +1,9 @@ - - - - - - - - - + + + + + + + + + diff --git a/android-core/plugins/org.eclipse.andmore.gldebugger/.settings/org.eclipse.jdt.core.prefs b/android-core/plugins/org.eclipse.andmore.gldebugger/.settings/org.eclipse.jdt.core.prefs index ea661960..c137e176 100644 --- a/android-core/plugins/org.eclipse.andmore.gldebugger/.settings/org.eclipse.jdt.core.prefs +++ b/android-core/plugins/org.eclipse.andmore.gldebugger/.settings/org.eclipse.jdt.core.prefs @@ -1,98 +1,98 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore -org.eclipse.jdt.core.compiler.annotation.nonnull=com.android.annotations.NonNull -org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=com.android.annotations.NonNullByDefault -org.eclipse.jdt.core.compiler.annotation.nonnullisdefault=disabled -org.eclipse.jdt.core.compiler.annotation.nullable=com.android.annotations.Nullable -org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.debug.lineNumber=generate -org.eclipse.jdt.core.compiler.debug.localVariable=generate -org.eclipse.jdt.core.compiler.debug.sourceFile=generate -org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.autoboxing=ignore -org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning -org.eclipse.jdt.core.compiler.problem.deadCode=warning -org.eclipse.jdt.core.compiler.problem.deprecation=warning -org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled -org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled -org.eclipse.jdt.core.compiler.problem.discouragedReference=warning -org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore -org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning -org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled -org.eclipse.jdt.core.compiler.problem.fieldHiding=warning -org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning -org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning -org.eclipse.jdt.core.compiler.problem.forbiddenReference=error -org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning -org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled -org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning -org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning -org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore -org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning -org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning -org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore -org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning -org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled -org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning -org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error -org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled -org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning -org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore -org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning -org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning -org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore -org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning -org.eclipse.jdt.core.compiler.problem.nullReference=warning -org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning -org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning -org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore -org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning -org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore -org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning -org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning -org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error -org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning -org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning -org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning -org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore -org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore -org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning -org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore -org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore -org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled -org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning -org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled -org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled -org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore -org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning -org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled -org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning -org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error -org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore -org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning -org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore -org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning -org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled -org.eclipse.jdt.core.compiler.problem.unusedImport=warning -org.eclipse.jdt.core.compiler.problem.unusedLabel=warning -org.eclipse.jdt.core.compiler.problem.unusedLocal=warning -org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning -org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore -org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled -org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled -org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled -org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning -org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning -org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore +org.eclipse.jdt.core.compiler.annotation.nonnull=com.android.annotations.NonNull +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=com.android.annotations.NonNullByDefault +org.eclipse.jdt.core.compiler.annotation.nonnullisdefault=disabled +org.eclipse.jdt.core.compiler.annotation.nullable=com.android.annotations.Nullable +org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=warning +org.eclipse.jdt.core.compiler.problem.deprecation=warning +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore +org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=warning +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning +org.eclipse.jdt.core.compiler.problem.forbiddenReference=error +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning +org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning +org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning +org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning +org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning +org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning +org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning +org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedImport=warning +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning +org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/android-core/plugins/org.eclipse.andmore.gldebugger/META-INF/MANIFEST.MF b/android-core/plugins/org.eclipse.andmore.gldebugger/META-INF/MANIFEST.MF index 1d210493..d766990e 100644 --- a/android-core/plugins/org.eclipse.andmore.gldebugger/META-INF/MANIFEST.MF +++ b/android-core/plugins/org.eclipse.andmore.gldebugger/META-INF/MANIFEST.MF @@ -1,23 +1,24 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: Tracer for OpenGL ES -Bundle-SymbolicName: org.eclipse.andmore.gldebugger;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.gltrace.GlTracePlugin -Require-Bundle: org.eclipse.ui, - org.eclipse.core.runtime, - org.eclipse.ui.ide, - org.eclipse.core.resources, - org.eclipse.core.filesystem, - org.eclipse.ui.console, - org.eclipse.andmore.ddms, - org.eclipse.andmore.base -Bundle-ActivationPolicy: lazy -Bundle-ClassPath: libs/host-libprotobuf-java-2.3.0-lite.jar, - libs/liblzf-1.0.jar, - . -Bundle-Vendor: Eclipse Andmore -Export-Package: org.eclipse.andmore.gltrace;x-friends:="com.android.ide.eclipse.gldebugger.tests", - org.eclipse.andmore.gltrace.format;x-friends:="com.android.ide.eclipse.gldebugger.tests", - org.eclipse.andmore.gltrace.model;x-friends:="com.android.ide.eclipse.gldebugger.tests" -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Tracer for OpenGL ES +Bundle-SymbolicName: org.eclipse.andmore.gldebugger;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.gltrace.GlTracePlugin +Require-Bundle: org.eclipse.ui, + org.eclipse.core.runtime, + org.eclipse.ui.ide, + org.eclipse.core.resources, + org.eclipse.core.filesystem, + org.eclipse.ui.console, + org.eclipse.andmore.swt, + org.eclipse.andmore.ddms, + org.eclipse.andmore.ddmuilib;bundle-version="0.5.2" +Bundle-ActivationPolicy: lazy +Bundle-ClassPath: libs/host-libprotobuf-java-2.3.0-lite.jar, + libs/liblzf-1.0.jar, + . +Bundle-Vendor: Eclipse Andmore +Export-Package: org.eclipse.andmore.gltrace;x-friends:="com.android.ide.eclipse.gldebugger.tests", + org.eclipse.andmore.gltrace.format;x-friends:="com.android.ide.eclipse.gldebugger.tests", + org.eclipse.andmore.gltrace.model;x-friends:="com.android.ide.eclipse.gldebugger.tests" +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/.classpath b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/.classpath index 3d094dff..7498423d 100644 --- a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/.classpath +++ b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/.classpath @@ -1,8 +1,7 @@ - - - - - - - - + + + + + + + diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/.settings/org.eclipse.jdt.core.prefs b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/.settings/org.eclipse.jdt.core.prefs index ea661960..c137e176 100644 --- a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/.settings/org.eclipse.jdt.core.prefs +++ b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/.settings/org.eclipse.jdt.core.prefs @@ -1,98 +1,98 @@ -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore -org.eclipse.jdt.core.compiler.annotation.nonnull=com.android.annotations.NonNull -org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=com.android.annotations.NonNullByDefault -org.eclipse.jdt.core.compiler.annotation.nonnullisdefault=disabled -org.eclipse.jdt.core.compiler.annotation.nullable=com.android.annotations.Nullable -org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 -org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=1.6 -org.eclipse.jdt.core.compiler.debug.lineNumber=generate -org.eclipse.jdt.core.compiler.debug.localVariable=generate -org.eclipse.jdt.core.compiler.debug.sourceFile=generate -org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.autoboxing=ignore -org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning -org.eclipse.jdt.core.compiler.problem.deadCode=warning -org.eclipse.jdt.core.compiler.problem.deprecation=warning -org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled -org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled -org.eclipse.jdt.core.compiler.problem.discouragedReference=warning -org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore -org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning -org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled -org.eclipse.jdt.core.compiler.problem.fieldHiding=warning -org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning -org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning -org.eclipse.jdt.core.compiler.problem.forbiddenReference=error -org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning -org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled -org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning -org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning -org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore -org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning -org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning -org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore -org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning -org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled -org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning -org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error -org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled -org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning -org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore -org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning -org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning -org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore -org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning -org.eclipse.jdt.core.compiler.problem.nullReference=warning -org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning -org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning -org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore -org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning -org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore -org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning -org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning -org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error -org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning -org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning -org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning -org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore -org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore -org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning -org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore -org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore -org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled -org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning -org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled -org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled -org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore -org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning -org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled -org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning -org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error -org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore -org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning -org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore -org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning -org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled -org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled -org.eclipse.jdt.core.compiler.problem.unusedImport=warning -org.eclipse.jdt.core.compiler.problem.unusedLabel=warning -org.eclipse.jdt.core.compiler.problem.unusedLocal=warning -org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning -org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore -org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled -org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled -org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled -org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning -org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning -org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning -org.eclipse.jdt.core.compiler.source=1.6 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore +org.eclipse.jdt.core.compiler.annotation.nonnull=com.android.annotations.NonNull +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=com.android.annotations.NonNullByDefault +org.eclipse.jdt.core.compiler.annotation.nonnullisdefault=disabled +org.eclipse.jdt.core.compiler.annotation.nullable=com.android.annotations.Nullable +org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=warning +org.eclipse.jdt.core.compiler.problem.deprecation=warning +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore +org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=warning +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning +org.eclipse.jdt.core.compiler.problem.forbiddenReference=error +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning +org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=warning +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=warning +org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning +org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning +org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning +org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning +org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning +org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=enabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedImport=warning +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning +org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/META-INF/MANIFEST.MF b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/META-INF/MANIFEST.MF index 8067225a..df622fd1 100644 --- a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/META-INF/MANIFEST.MF +++ b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/META-INF/MANIFEST.MF @@ -1,18 +1,19 @@ -Manifest-Version: 1.0 -Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName -Bundle-SymbolicName: org.eclipse.andmore.hierarchyviewer;singleton:=true -Bundle-Version: 0.5.2.qualifier -Bundle-Activator: org.eclipse.andmore.hierarchyviewer.HierarchyViewerPlugin -Bundle-Vendor: %providerName -Bundle-Localization: plugin -Bundle-ActivationPolicy: lazy -Require-Bundle: org.eclipse.ui, - org.eclipse.core.runtime, - org.eclipse.ui.console, - org.eclipse.andmore.ddms, - org.eclipse.andmore.base -Bundle-ClassPath: ., - libs/hierarchyviewer2lib.jar -Export-Package: org.eclipse.andmore.hierarchyviewer -Bundle-RequiredExecutionEnvironment: JavaSE-1.6 +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %pluginName +Bundle-SymbolicName: org.eclipse.andmore.hierarchyviewer;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Activator: org.eclipse.andmore.hierarchyviewer.HierarchyViewerPlugin +Bundle-Vendor: %providerName +Bundle-Localization: plugin +Bundle-ActivationPolicy: lazy +Require-Bundle: org.eclipse.ui, + org.eclipse.core.runtime, + org.eclipse.ui.console, + org.eclipse.andmore.swt, + org.eclipse.andmore.ddms, + org.eclipse.andmore.hierarchyviewer2lib, + org.eclipse.andmore.ddmuilib +Bundle-ClassPath: . +Export-Package: org.eclipse.andmore.hierarchyviewer +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/build.properties b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/build.properties index 9fb508f2..7e5f633d 100644 --- a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/build.properties +++ b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/build.properties @@ -1,11 +1,12 @@ -source.. = src/ -output.. = bin/ -bin.includes = META-INF/,\ - icons/,\ - plugin.xml,\ - .,\ - libs/,\ - about.html,\ - about.ini,\ - about.properties,\ - plugin.properties +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + icons/,\ + plugin.xml,\ + .,\ + libs/,\ + about.html,\ + about.ini,\ + about.properties,\ + plugin.properties,\ + hiarch/ diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/auto-refresh.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/auto-refresh.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/auto-refresh.png rename to android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/auto-refresh.png diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/capture-psd.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/capture-psd.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/capture-psd.png rename to android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/capture-psd.png diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/device-view-selected.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/device-view-selected.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/device-view-selected.png rename to android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/device-view-selected.png diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/device-view.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/device-view.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/device-view.png rename to android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/device-view.png diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/device.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/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/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/display.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/display.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/display.png rename to android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/display.png diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/emulator.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/emulator.png new file mode 100644 index 0000000000000000000000000000000000000000..a71804281a05161a4f96f718d2551603409d7435 GIT binary patch literal 287 zcmV+)0pR|LP)ST5WJ6AArqvrOC+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/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/file.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/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/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/green.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/green.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/green.png rename to android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/green.png diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/inspect-screenshot.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/inspect-screenshot.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/inspect-screenshot.png rename to android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/inspect-screenshot.png diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/invalidate.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/invalidate.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/invalidate.png rename to android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/invalidate.png diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/load-all-views.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/load-all-views.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/load-all-views.png rename to android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/load-all-views.png diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/load-overlay.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/load-overlay.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/load-overlay.png rename to android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/load-overlay.png diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/load-view-hierarchy.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/load-view-hierarchy.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/load-view-hierarchy.png rename to android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/load-view-hierarchy.png diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/not-selected.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/not-selected.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/not-selected.png rename to android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/not-selected.png diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/on-black.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/on-black.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/on-black.png rename to android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/on-black.png diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/on-white.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/on-white.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/on-white.png rename to android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/on-white.png diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/picker.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/picker.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/picker.png rename to android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/picker.png diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/pixel-perfect-view-selected.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/pixel-perfect-view-selected.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/pixel-perfect-view-selected.png rename to android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/pixel-perfect-view-selected.png diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/pixel-perfect-view.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/pixel-perfect-view.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/pixel-perfect-view.png rename to android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/pixel-perfect-view.png diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/profile.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/profile.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/profile.png rename to android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/profile.png diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/red.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/red.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/red.png rename to android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/red.png diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/refresh-windows.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/refresh-windows.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/refresh-windows.png rename to android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/refresh-windows.png diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/request-layout.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/request-layout.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/request-layout.png rename to android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/request-layout.png diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/save.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/save.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/save.png rename to android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/save.png diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/sdk-hierarchyviewer-128.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/sdk-hierarchyviewer-128.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/sdk-hierarchyviewer-128.png rename to android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/sdk-hierarchyviewer-128.png diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/sdk-hierarchyviewer-16.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/sdk-hierarchyviewer-16.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/sdk-hierarchyviewer-16.png rename to android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/sdk-hierarchyviewer-16.png diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/selected-filtered-small.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/selected-filtered-small.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/selected-filtered-small.png rename to android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/selected-filtered-small.png diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/selected-filtered.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/selected-filtered.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/selected-filtered.png rename to android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/selected-filtered.png diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/selected-small.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/selected-small.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/selected-small.png rename to android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/selected-small.png diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/selected.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/selected.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/selected.png rename to android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/selected.png diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/show-extras.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/show-extras.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/show-extras.png rename to android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/show-extras.png diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/show-overlay.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/show-overlay.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/show-overlay.png rename to android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/show-overlay.png diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/tree-view-selected.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/tree-view-selected.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/tree-view-selected.png rename to android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/tree-view-selected.png diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/tree-view.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/tree-view.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/tree-view.png rename to android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/tree-view.png diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/yellow.png b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/yellow.png similarity index 100% rename from andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/yellow.png rename to android-core/plugins/org.eclipse.andmore.hierarchyviewer/hiarch/icons/yellow.png diff --git a/android-core/plugins/org.eclipse.andmore.hierarchyviewer/libs/hierarchyviewer2lib.jar b/android-core/plugins/org.eclipse.andmore.hierarchyviewer/libs/hierarchyviewer2lib.jar deleted file mode 100644 index a339050e828411135cbbe2d1056868e22c3d1e3e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 406917 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 diff --git a/android-core/plugins/org.eclipse.andmore.ddms/libs/uiautomatorviewer.jar b/android-core/plugins/org.eclipse.andmore.ddms/libs/uiautomatorviewer.jar deleted file mode 100644 index ebef827d0f548e1cb65451ce763b1340fe2b9eeb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 67649 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 diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/accept_icon16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/accept_icon16.png new file mode 100644 index 0000000000000000000000000000000000000000..ae61f7df993c36e0c20b51d0d7c1ef7d9d2800d1 GIT binary patch literal 3277 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 LNkvXXu0mjf76}mt literal 0 HcmV?d00001 diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/addon_pkg_16.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/addon_pkg_16.png new file mode 100644 index 0000000000000000000000000000000000000000..addef8ef3d92dbe4859f0095419a99c16058cde8 GIT binary patch literal 529 zcmV+s0`C2ZP)(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRZ-mq|oHRCwBy(?4z#K^O(_->lcMBO(w5f(p@4&>;H!k$eUm0iuXF0ir-oL(L)z z0^$NG5TH$poFEc%Ebb-JvtYf8CyNpY?g70B4t7pj2RkRN>aaX39{$T)C<9Rjc~o1UYvP~iBb@U+!><7VeHMNd T!P8!+00000NkvXXu0mjfffw7D literal 0 HcmV?d00001 diff --git a/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/android_icon_128.png b/android-core/plugins/org.eclipse.andmore.ddms/sdk/icons/android_icon_128.png new file mode 100644 index 0000000000000000000000000000000000000000..830c04b0beff37224ef4e0b32d68fae5db4daad1 GIT binary patch literal 17715 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 l_ac^b)>bn);<2)^lH=j~0r0W^JDk*p?8D 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'W%_d2_b8}7rN-U#=x9YlS;KWIT4I1P-T;q)z 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�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 z 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

  • Ld@*mhS*XaXU)8GLFp7AQ9# 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 Z<|QG?5my`R6M_C@@O1TaS?83{1OTd@g5v-H literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/show-overlay.png b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/show-overlay.png new file mode 100644 index 0000000000000000000000000000000000000000..e39e90a1ec3e74dcb136443271de57d13b68654e GIT binary patch literal 958 zcmV;v13~Px#32;bRa{vGh*8l(w*8xH(n|J^K00(qQO+^RV2pAU`EA)9p^#A|^P)S5VR5;6R zlSxcmbpXbH@684?EHl`}0#hh;rUb1k2dt$BqedGKc<@jY6QhYSCdI@UJ$N-9iiT(p z9z3Xtafya%n`lj0T3Z*Y(p0f5g%lWunE{63E%V;I|NkxCj7z`Umwfr1exJfkGW_MY z$M)~fK9g)rv;S*-5B7z zl946b{bP1vJa={K#qpuR#s2|@FU`NQucPA)(-SeEAm}5dlKJ|P?Hv!>t8Xm{SunFj-i(k z{%X?MTNnWdMGhW0wyUfAxuO`uFWq3dK2LjDC2g#cGRE+{IsSBVEXxY2euTh^5f?p} zrKyw}um3dM)RBG64#G&;!b0T(lwQ`Pr=VJ=yB)^EmI6_6*CCu6iu~@uE z6|V0VYNKSLNn)19R;-_5G>s&LkS=qPLj>`A=nWOwG+swqMRao%*YWZFNK`dy_ELyG zw!{`f*tlvlGEmsi&rq%95W)eq8MW>f8s&Wij!R5rX>sl(q&w4UX>R(I8tx79#sE1t zM93{7JQuIJh^BvzZcU)bCCZ|Uy5KNKP(?j~={s^he?kE`et!7}C!Rc%G|rPXN0KV^pv_UWI{ zv!^givuje8h|Q|={LJX7kN#E}VByS%*Xwt;f0I?L-p)w0^*|`D#N7s@=F%*5 z48!mODYRsmh8=Zbx_j=JoV_-^t=#O+nWInM{XqYN#~bP*w>g#-Y;0^SUL7AlaaT6& g0Ykt5a29y_FCvG^l1f$H6951J07*qoM6N<$f-WY;$p8QV literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/tree-view-selected.png b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/tree-view-selected.png new file mode 100644 index 0000000000000000000000000000000000000000..175ad1f0d7170b0d041d51d9698a21e019b9895b GIT binary patch literal 276 zcmeAS@N?(olHy`uVBq!ia0vp^qChOf!3HF^2u;-kQjEnx?oJHr&dIz4a@dl*-CY>| zgW!U_%O?XxI14-?iy0WWg+Z8+Vb&Z8pdfpRr>`sfEe;`3dHMJ2l+OT#BuiW)N}Tg^ zb5rw57@Uhz6H8K46v{J8G8EiBeFMT9`NV;W@;zM~Lo7}wCrD^DObp>&etyo+e(!Tv zj`t+J%R3k9ENKxv*(M|$#sdTs1s@)f_R`r= z|DW;kfi_LwOPYp>t~ob0PY?=Yp5-@_&9!mSiWMK`PkLgugMmTEUo+;&q4&>#Rx^0I L`njxgN@xNA9#>wR literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/tree-view.png b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/main/java/images/tree-view.png new file mode 100644 index 0000000000000000000000000000000000000000..23aa42442fd38e8f56a2729e069ddead5f83e973 GIT binary patch literal 281 zcmeAS@N?(olHy`uVBq!ia0vp^qChOf!3HF^2u;-kQjEnx?oJHr&dIz4a@dl*-CY>| zgW!U_%O?XxI14-?iy0WWg+Z8+Vb&Z8pdfpRr>`sfEe;`3IpP1zam#>0k|nMYCC>S| zxv6<249-QVi6yBi3gww484B*6z5(HleBwYwC7v#hAr_~T6C|`6CWi2;9Xs~F@Z?;} z>S+@jfuJ?SdFITSl0Sa_G(2Ko_lG0h_~N18CWcd5r)*HV8IW|S3#caXXV+8<-r%61 z6b0=?Q>KVWvYYv2PV(#I)J{4)52#S;Ldhu;4X!EG-{1XTuCkLC=)*-bQL70(Y)*K0-AbW|YuPgg44iQlmj?yJ!?}0*+C9V-A&iT2y zsd*&~&PAz-C8;S2<(VZJ3hti10pX2&;y^`Fo-U3d8t0czKFE7Of#*n!So6gjJTuPx zUDRdgp>y~0#6>X=nlDXVxFc0#uKkp<2{t^L>KD7W#L7+obfes4LR^Da&o2$OR4&d- rpLlj{X3*AHe`oGxUhh)tU!nTbFY6Un$UNE$w1vUb)z4*}Q$iB}bvRVM literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/java/com/android/hierarchyviewerlib/models/EvaluateContrastModelTest.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/java/com/android/hierarchyviewerlib/models/EvaluateContrastModelTest.java new file mode 100644 index 00000000..727daf5c --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/java/com/android/hierarchyviewerlib/models/EvaluateContrastModelTest.java @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2014 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.hierarchyviewerlib.models; + +import com.android.ddmuilib.ImageLoader; +import com.android.hierarchyviewerlib.models.EvaluateContrastModel.ContrastResult; + +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.Display; + +import junit.framework.TestCase; + +public class EvaluateContrastModelTest extends TestCase { + + private static ImageLoader sImageLoader; + private static final int ARGB_BLACK = 0xFF000000; + private static final int ARGB_WHITE = 0XFFFFFFFF; + private static final int ARGB_DARK_GREEN = 0xFF0D6F57; + private static final int ARGB_LIGHT_GREEN = 0xFF1DE9B6; + + @Override + protected void setUp() throws Exception { + super.setUp(); + sImageLoader = ImageLoader.getLoader(EvaluateContrastModelTest.class); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + sImageLoader.dispose(); + } + + private static int argbToRgb(int argb) { + return 0xFFFFFF & argb; + } + + public void testFailsAllCases() { + Image allBlack = sImageLoader.loadImage("all_black.png", Display.getDefault()); + Rectangle bounds = allBlack.getBounds(); + + // Text color is irrelevant in these tests because it will be calculated. + + // no text size, not bold + EvaluateContrastModel model = new EvaluateContrastModel(allBlack, null, null, bounds.x, + bounds.y, bounds.width, bounds.height, false); + assertEquals(ContrastResult.FAIL, model.getContrastResult()); + assertFalse(model.isIndeterminate()); + + // text size is normal, bold + model = new EvaluateContrastModel(allBlack, null, + (double) EvaluateContrastModel.NORMAL_TEXT_SZ_PTS, bounds.x, bounds.y, bounds.width, + bounds.height, true); + assertEquals(ContrastResult.FAIL, model.getContrastResult()); + + // text size is normal for bold, bold + model = new EvaluateContrastModel(allBlack, null, + (double) EvaluateContrastModel.NORMAL_TEXT_BOLD_SZ_PTS, bounds.x, bounds.y, + bounds.width, bounds.height, true); + assertEquals(ContrastResult.FAIL, model.getContrastResult()); + + // text size is large, not bold + model = new EvaluateContrastModel(allBlack, null, + (double) EvaluateContrastModel.NORMAL_TEXT_SZ_PTS + 1, bounds.x, bounds.y, + bounds.width, bounds.height, false); + assertEquals(ContrastResult.FAIL, model.getContrastResult()); + } + + public void testPassesAllCases() { + Image[] images = { + sImageLoader.loadImage("black_on_white.png", Display.getDefault()), + sImageLoader.loadImage("white_on_black.png", Display.getDefault()), + }; + int[] textColors = {ARGB_BLACK, ARGB_WHITE}; + Image image; + Rectangle bounds; + + // Text color is irrelevant in these tests because it will be calculated. + for (int i = 0; i < images.length; ++i) { + image = images[i]; + bounds = image.getBounds(); + + // no text size, not bold + EvaluateContrastModel model = new EvaluateContrastModel(image, null, null, bounds.x, + bounds.y, bounds.width, bounds.height, false); + assertEquals(ContrastResult.PASS, model.getContrastResult()); + assertFalse(model.isIndeterminate()); + + // text size is normal, bold + model = new EvaluateContrastModel(image, null, + (double) EvaluateContrastModel.NORMAL_TEXT_SZ_PTS, bounds.x, bounds.y, + bounds.width, bounds.height, true); + assertEquals(ContrastResult.PASS, model.getContrastResult()); + + // text size is normal for bold, bold + model = new EvaluateContrastModel(image, null, + (double) EvaluateContrastModel.NORMAL_TEXT_BOLD_SZ_PTS, bounds.x, bounds.y, + bounds.width, bounds.height, true); + assertEquals(ContrastResult.PASS, model.getContrastResult()); + + // text size is large, not bold + model = new EvaluateContrastModel(image, null, + (double) EvaluateContrastModel.NORMAL_TEXT_SZ_PTS + 1, bounds.x, bounds.y, + bounds.width, bounds.height, false); + assertEquals(ContrastResult.PASS, model.getContrastResult()); + } + } + + public void testIndeterminateFailsNormalAndPassesLarge() { + Image greens = sImageLoader.loadImage("dark_on_light_greens.png", Display.getDefault()); + Rectangle bounds = greens.getBounds(); + + // Not providing a text size is the main cause for an indeterminate result. + // Text color is irrelevant because it will be calculated. + + // no text size, not bold + EvaluateContrastModel model = new EvaluateContrastModel(greens, null, null, bounds.x, + bounds.y, bounds.width, bounds.height, true); + assertEquals(ContrastResult.INDETERMINATE, model.getContrastResult()); + assertTrue(model.isIndeterminate()); + + // no text size, bold + model = new EvaluateContrastModel(greens, null, null, bounds.x, + bounds.y, bounds.width, bounds.height, true); + assertEquals(ContrastResult.INDETERMINATE, model.getContrastResult()); + assertTrue(model.isIndeterminate()); + + // text size normal, not bold + model = new EvaluateContrastModel(greens, null, + (double) EvaluateContrastModel.NORMAL_TEXT_SZ_PTS, bounds.x, bounds.y, + bounds.width, bounds.height, false); + assertEquals(ContrastResult.FAIL, model.getContrastResult()); + + // text size normal, bold + model = new EvaluateContrastModel(greens, null, + (double) EvaluateContrastModel.NORMAL_TEXT_SZ_PTS, bounds.x, bounds.y, + bounds.width, bounds.height, true); + assertEquals(ContrastResult.PASS, model.getContrastResult()); + + // text size normal for bold, not bold + model = new EvaluateContrastModel(greens, null, + (double) EvaluateContrastModel.NORMAL_TEXT_BOLD_SZ_PTS, bounds.x, bounds.y, + bounds.width, bounds.height, false); + assertEquals(ContrastResult.FAIL, model.getContrastResult()); + + // text size normal for bold, bold + model = new EvaluateContrastModel(greens, null, + (double) EvaluateContrastModel.NORMAL_TEXT_BOLD_SZ_PTS, bounds.x, bounds.y, + bounds.width, bounds.height, true); + assertEquals(ContrastResult.PASS, model.getContrastResult()); + + // text size large, not bold + model = new EvaluateContrastModel(greens, null, + (double) EvaluateContrastModel.NORMAL_TEXT_SZ_PTS + 1, bounds.x, bounds.y, + bounds.width, bounds.height, true); + assertEquals(ContrastResult.PASS, model.getContrastResult()); + + // text size large, bold + model = new EvaluateContrastModel(greens, null, + (double) EvaluateContrastModel.NORMAL_TEXT_SZ_PTS + 1, bounds.x, bounds.y, + bounds.width, bounds.height, true); + assertEquals(ContrastResult.PASS, model.getContrastResult()); + } + + public void testGetBackgroundColor() { + Image allBlack = sImageLoader.loadImage("all_black.png", Display.getDefault()); + Rectangle bounds = allBlack.getBounds(); + EvaluateContrastModel model = new EvaluateContrastModel(allBlack, null, null, bounds.x, + bounds.y, bounds.width, bounds.height, false); + assertEquals(argbToRgb(ARGB_BLACK), model.getBackgroundColor()); + + Image blackOnWhite = sImageLoader.loadImage("black_on_white.png", Display.getDefault()); + bounds = blackOnWhite.getBounds(); + model = new EvaluateContrastModel(blackOnWhite, null, null, bounds.x, bounds.y, + bounds.width, bounds.height, false); + assertEquals(argbToRgb(ARGB_WHITE), model.getBackgroundColor()); + + Image whiteOnBlack = sImageLoader.loadImage("white_on_black.png", Display.getDefault()); + bounds = whiteOnBlack.getBounds(); + model = new EvaluateContrastModel(whiteOnBlack, null, null, bounds.x, bounds.y, + bounds.width, bounds.height, false); + assertEquals(argbToRgb(ARGB_BLACK), model.getBackgroundColor()); + + Image greens = sImageLoader.loadImage("dark_on_light_greens.png", Display.getDefault()); + bounds = greens.getBounds(); + model = new EvaluateContrastModel(greens, null, null, bounds.x, bounds.y, bounds.width, + bounds.height, true); + assertEquals(argbToRgb(ARGB_LIGHT_GREEN), model.getBackgroundColor()); + } + + public void testGetTextColor() { + Image allBlack = sImageLoader.loadImage("all_black.png", Display.getDefault()); + Rectangle bounds = allBlack.getBounds(); + EvaluateContrastModel model = new EvaluateContrastModel(allBlack, null, null, bounds.x, + bounds.y, bounds.width, bounds.height, false); + assertEquals(argbToRgb(ARGB_BLACK), model.getTextColor()); + model = new EvaluateContrastModel(allBlack, ARGB_BLACK, null, bounds.x, + bounds.y, bounds.width, bounds.height, false); + assertEquals(ARGB_BLACK, model.getTextColor()); + + Image blackOnWhite = sImageLoader.loadImage("black_on_white.png", Display.getDefault()); + bounds = blackOnWhite.getBounds(); + model = new EvaluateContrastModel(blackOnWhite, null, null, bounds.x, bounds.y, + bounds.width, bounds.height, false); + assertEquals(argbToRgb(ARGB_BLACK), model.getTextColor()); + + Image whiteOnBlack = sImageLoader.loadImage("white_on_black.png", Display.getDefault()); + bounds = whiteOnBlack.getBounds(); + model = new EvaluateContrastModel(whiteOnBlack, null, null, bounds.x, bounds.y, + bounds.width, bounds.height, false); + assertEquals(argbToRgb(ARGB_WHITE), model.getTextColor()); + + Image greens = sImageLoader.loadImage("dark_on_light_greens.png", Display.getDefault()); + bounds = greens.getBounds(); + model = new EvaluateContrastModel(greens, null, null, bounds.x, + bounds.y, bounds.width, bounds.height, true); + assertEquals(argbToRgb(ARGB_DARK_GREEN), model.getTextColor()); + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/java/com/android/hierarchyviewerlib/ui/PropertyViewerTest.java b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/java/com/android/hierarchyviewerlib/ui/PropertyViewerTest.java new file mode 100644 index 00000000..67a632b1 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/java/com/android/hierarchyviewerlib/ui/PropertyViewerTest.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2014 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.hierarchyviewerlib.ui; + +import junit.framework.TestCase; + +public class PropertyViewerTest extends TestCase { + + public void testExistingCases() { + assertEquals("alpha", PropertyViewer.parseColumnTextName("drawing:getAlpha()")); + assertEquals("x", PropertyViewer.parseColumnTextName("drawing:getX()")); + } + + public void testEdgeCases() { + assertEquals("alpha", PropertyViewer.parseColumnTextName("foo:alpha")); + assertEquals("x", PropertyViewer.parseColumnTextName("foo:x")); + + assertEquals("get", PropertyViewer.parseColumnTextName("foo:get")); + assertEquals("", PropertyViewer.parseColumnTextName("foo:()")); + assertEquals("", PropertyViewer.parseColumnTextName("foo:")); + + assertEquals("getter", PropertyViewer.parseColumnTextName("foo:getter")); + assertEquals("together", PropertyViewer.parseColumnTextName("foo:together")); + assertEquals("together", PropertyViewer.parseColumnTextName("foo:getTogether")); + assertEquals("()get", PropertyViewer.parseColumnTextName("foo:()get")); + } +} diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/resources/images/all_black.png b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/resources/images/all_black.png new file mode 100644 index 0000000000000000000000000000000000000000..56acc0c33234af79d67e9aea545bc6a4f628e477 GIT binary patch literal 146 zcmeAS@N?(olHy`uVBq!ia0vp^(Lij%!2~3;zFl?%Qq09po*^6@9Je3(KLBziOI#yL zg7ec#$`gxH8PfCeb5nJTlZ#SQ^NKU_OWrf@w*x9t^>lFz$!L6g(2x#uV7$w_$V$667+QSb6Mw<&;$UFdnQ=` literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/resources/images/black_on_white.png b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/resources/images/black_on_white.png new file mode 100644 index 0000000000000000000000000000000000000000..d3006471087d1deec7090c1d8c41d89387c22c82 GIT binary patch literal 10523 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 Q>x~#FIaS#Y(x##R2kxfEpa1{> literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/resources/images/dark_on_light_greens.png b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/resources/images/dark_on_light_greens.png new file mode 100644 index 0000000000000000000000000000000000000000..7dc531fd1965b8706dbae63c22f319da72fb3d67 GIT binary patch literal 1837 zcmah~Sv1>;8~rKOsHzyW=)~Sm8#)qWsihH$GDw3&ulkbjrT_r>Hr4{UxGTH}_Po|MUa%ur6dEq2EXBiC%e+ z4PqkCM)8l9viVg4RQ1NAI8f z54rDhSbN!aU>tUE?9gsjY$5Ik0GPx#FUNhe|8Sg+Jtzxba1g|K)uUaiAY@PHU?HGi zoViaswDbC{rq<&v0)0vZAZQ!rj<1I{EKmxGMRI?R)K?C|XWZU+wG50e>lH=LfsI}45O$L1NlM#prWaN zz`E30ojRW|mzD?y0I`1m41$4X_OCzbl&O2l`*bc?PnC)&`|!$M5>a|Fg3LlF;-YUk2bINQ3(+_U(h24{AF*Vl5+i08%O+Gj*&4w z2CCyOx??cEDaHE|^dh#`IU@jt@!b~3r|!A{07%FTqF$I}X2w0i!)uXPHr~B-FbScsaV^#CvD0uC5Zueemrf!aK=Bp&?EYyIEEAV*=w`X`4 z$I42+*C&}utLIs{K}}Jj%x1}De2m6bZgcRtf&02wWUf0w>`zq}oX#q@T3{rvONl zXl-ht8A^0`PZ{V+(>h55NfjqJ@>%H25z3`@y&h?go}iF>C^ZTM8%_fQ$fzo3J6q<( zifADq$pPalrHVLxPN}@ih&!{m@n}Awksd}JgnU;gMwP+6QUo>CDXZ^1ksew^1KxsR zZG5rcoE#JL#sK`@YWAk%(CG?@n+(8gI?r(QpOYeJ+qA=^NZk8Dvw_yUVb78<=xrIJ zj=na(7!VM_xGCtSZI~*SXBM>9_*4r`ED0NshY~^Nqu8`QUh@F;7fGerNCt*K!yRdd z^JcG9KnQT=LKtc|QH?;h^wZdx%Kyfy613SocG%COl+a`UQ@p-o-DVZ zzKSoZwO5klkz!PoSX$k_x1$m(bt8JJwPKU;u5f%l3@?Api&H@-E%rteeS2Hu42Q{# zO-fZ06*tpOnj)yYY1DigFthb(@}n@H{hWU0YjzE_?M|XxM*4JUCHIe_%-B88gPHIJ zx|;HNyz4W;qSLGT(UHLDo}!;^%|N;fNW*V^1|;YS(Z7}ba0QPf;-9Oo2}Q?MfU~e( z_I-W|z+Ra|k=ELs_R}v&Mvui^pKdr|vv0kHyR)=k$OC{0xO#i&HnZm03r)6fajeSI znxJ~EO@8>T*Ye>0f(x{4Q#qCHLjs$Wo+nc%a^w;AwAn*khA#xjaIfSVhn{PGZq$%^ zm@b6$8%)aIElt(PtbE;rcLsCXL)o#ADz{xyCz1qBe~W(D&tJ2g;V4e=p(*`?L8`;Q?q1JM$Vdx1@go8gYQN literal 0 HcmV?d00001 diff --git a/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/resources/images/white_on_black.png b/andmore-swt/org.eclipse.andmore.hierarchyviewer2lib/src/test/resources/images/white_on_black.png new file mode 100644 index 0000000000000000000000000000000000000000..7dc11dfcba8f9281a8936f1d963fc55ba1daf414 GIT binary patch literal 10783 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 + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkstats/.gitignore b/andmore-swt/org.eclipse.andmore.sdkstats/.gitignore new file mode 100644 index 00000000..0f630157 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/.gitignore @@ -0,0 +1,2 @@ +/target/ +/bin/ diff --git a/andmore-swt/org.eclipse.andmore.sdkstats/.project b/andmore-swt/org.eclipse.andmore.sdkstats/.project new file mode 100644 index 00000000..8fa5963c --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/.project @@ -0,0 +1,34 @@ + + + org.eclipse.andmore.sdkstats + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.m2e.core.maven2Nature + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/andmore-swt/org.eclipse.andmore.sdkstats/.settings/README.txt b/andmore-swt/org.eclipse.andmore.sdkstats/.settings/README.txt new file mode 100644 index 00000000..2945bee0 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/.settings/README.txt @@ -0,0 +1,2 @@ +Copy this in eclipse project as a .settings folder at the root. +This ensure proper compilation compliance and warning/error levels. \ No newline at end of file diff --git a/andmore-swt/org.eclipse.andmore.sdkstats/.settings/org.eclipse.core.resources.prefs b/andmore-swt/org.eclipse.andmore.sdkstats/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000..4824b802 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/andmore-swt/org.eclipse.andmore.sdkstats/.settings/org.eclipse.jdt.core.prefs b/andmore-swt/org.eclipse.andmore.sdkstats/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..295926d9 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,7 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 +org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/andmore-swt/org.eclipse.andmore.sdkstats/.settings/org.eclipse.m2e.core.prefs b/andmore-swt/org.eclipse.andmore.sdkstats/.settings/org.eclipse.m2e.core.prefs new file mode 100644 index 00000000..14b697b7 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/.settings/org.eclipse.m2e.core.prefs @@ -0,0 +1,4 @@ +activeProfiles= +eclipse.preferences.version=1 +resolveWorkspaceProjects=true +version=1 diff --git a/andmore-swt/org.eclipse.andmore.sdkstats/META-INF/MANIFEST.MF b/andmore-swt/org.eclipse.andmore.sdkstats/META-INF/MANIFEST.MF new file mode 100644 index 00000000..95b2a485 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/META-INF/MANIFEST.MF @@ -0,0 +1,14 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Localization: plugin +Bundle-Name: %Bundle-Name +Bundle-SymbolicName: org.eclipse.andmore.sdkstats;singleton:=true +Bundle-Version: 0.5.2.qualifier +Bundle-Vendor: %Bundle-Vendor +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Require-Bundle: org.eclipse.core.commands;bundle-version="3.8.1", + org.eclipse.equinox.common;bundle-version="3.8.0", + org.eclipse.jface;bundle-version="3.12.2", + org.eclipse.andmore.swt +Export-Package: com.android.sdkstats +Bundle-ClassPath: . diff --git a/andmore-swt/org.eclipse.andmore.sdkstats/NOTICE b/andmore-swt/org.eclipse.andmore.sdkstats/NOTICE new file mode 100644 index 00000000..70c54220 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/NOTICE @@ -0,0 +1,190 @@ + + Copyright (c) 2005-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. + + 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. + + + 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 + diff --git a/andmore-swt/org.eclipse.andmore.sdkstats/README b/andmore-swt/org.eclipse.andmore.sdkstats/README new file mode 100644 index 00000000..bda8ef84 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/README @@ -0,0 +1,11 @@ +How to use the Eclipse projects for SdkStats. + +SdkStats 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. diff --git a/andmore-swt/org.eclipse.andmore.sdkstats/build.gradle b/andmore-swt/org.eclipse.andmore.sdkstats/build.gradle new file mode 100644 index 00000000..1f9261c2 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/build.gradle @@ -0,0 +1,15 @@ +group = 'com.android.tools' +archivesBaseName = 'sdkstats' + +dependencies { + compile project(':base:common') + compile project(':swt:swtmenubar') + + testCompile 'junit:junit:3.8.1' +} + +sourceSets { + main.resources.srcDir 'src/main/java' + test.resources.srcDir 'src/test/java' +} + diff --git a/andmore-swt/org.eclipse.andmore.sdkstats/build.properties b/andmore-swt/org.eclipse.andmore.sdkstats/build.properties new file mode 100644 index 00000000..ecc09051 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/build.properties @@ -0,0 +1,4 @@ +source.. = src/main/java/ +output.. = bin/ +bin.includes = META-INF/,\ + . diff --git a/andmore-swt/org.eclipse.andmore.sdkstats/plugin.properties b/andmore-swt/org.eclipse.andmore.sdkstats/plugin.properties new file mode 100644 index 00000000..a4ad3878 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/plugin.properties @@ -0,0 +1,4 @@ +#Properties file for com.android.ide.eclipse.uiautomatorviewer +Bundle-Vendor = Eclipse Andmore +Bundle-Name = UI SDK Statistics +category.name = Android diff --git a/andmore-swt/org.eclipse.andmore.sdkstats/pom.xml b/andmore-swt/org.eclipse.andmore.sdkstats/pom.xml new file mode 100644 index 00000000..861f9c3b --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + ../pom.xml + org.eclipse.andmore + swt-droid-parent + 0.5.2-SNAPSHOT + + org.eclipse.andmore.sdkstats + eclipse-plugin + sdkstats + + + + org.eclipse.tycho + tycho-maven-plugin + true + + + org.eclipse.tycho + tycho-source-plugin + ${tycho-version} + + + plugin-source + + plugin-source + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkstats/sdkstats.iml b/andmore-swt/org.eclipse.andmore.sdkstats/sdkstats.iml new file mode 100644 index 00000000..cb715fd8 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/sdkstats.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/andmore-swt/org.eclipse.andmore.sdkstats/src/main/java/com/android/sdkstats/DdmsPreferenceStore.java b/andmore-swt/org.eclipse.andmore.sdkstats/src/main/java/com/android/sdkstats/DdmsPreferenceStore.java new file mode 100644 index 00000000..7f12f0e1 --- /dev/null +++ b/andmore-swt/org.eclipse.andmore.sdkstats/src/main/java/com/android/sdkstats/DdmsPreferenceStore.java @@ -0,0 +1,332 @@ +/* + * 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.sdkstats; + +import com.android.prefs.AndroidLocation; +import com.android.prefs.AndroidLocation.AndroidLocationException; + +import org.eclipse.jface.preference.PreferenceStore; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Random; + +/** + * Manages persistence settings for DDMS. + * + * For convenience, this also stores persistence settings related to the "server stats" ping + * as well as some ADT settings that are SDK specific but not workspace specific. + */ +public class DdmsPreferenceStore { + + public final static String PING_OPT_IN = "pingOptIn"; //$NON-NLS-1$ + private final static String PING_TIME = "pingTime"; //$NON-NLS-1$ + private final static String PING_ID = "pingId"; //$NON-NLS-1$ + + private final static String ADT_USED = "adtUsed"; //$NON-NLS-1$ + private final static String LAST_SDK_PATH = "lastSdkPath"; //$NON-NLS-1$ + + /** + * PreferenceStore for DDMS. + * Creation and usage must be synchronized on {@code DdmsPreferenceStore.class}. + * Don't use it directly, instead retrieve it via {@link #getPreferenceStore()}. + */ + private static volatile PreferenceStore sPrefStore; + + public DdmsPreferenceStore() { + } + + /** + * Returns the DDMS {@link PreferenceStore}. + * This keeps a static reference on the store, so consequent calls will + * return always the same store. + */ + public PreferenceStore getPreferenceStore() { + synchronized (DdmsPreferenceStore.class) { + if (sPrefStore == null) { + // get the location of the preferences + String homeDir = null; + try { + homeDir = AndroidLocation.getFolder(); + } catch (AndroidLocationException e1) { + // pass, we'll do a dummy store since homeDir is null + } + + if (homeDir == null) { + sPrefStore = new PreferenceStore(); + return sPrefStore; + } + + assert homeDir != null; + + String rcFileName = homeDir + "ddms.cfg"; //$NON-NLS-1$ + + // also look for an old pref file in the previous location + String oldPrefPath = System.getProperty("user.home") //$NON-NLS-1$ + + File.separator + ".ddmsrc"; //$NON-NLS-1$ + File oldPrefFile = new File(oldPrefPath); + if (oldPrefFile.isFile()) { + FileOutputStream fileOutputStream = null; + try { + PreferenceStore oldStore = new PreferenceStore(oldPrefPath); + oldStore.load(); + + fileOutputStream = new FileOutputStream(rcFileName); + oldStore.save(fileOutputStream, ""); //$NON-NLS-1$ + oldPrefFile.delete(); + + PreferenceStore newStore = new PreferenceStore(rcFileName); + newStore.load(); + sPrefStore = newStore; + } catch (IOException e) { + // create a new empty store. + sPrefStore = new PreferenceStore(rcFileName); + } finally { + if (fileOutputStream != null) { + try { + fileOutputStream.close(); + } catch (IOException e) { + // pass + } + } + } + } else { + sPrefStore = new PreferenceStore(rcFileName); + + try { + sPrefStore.load(); + } catch (IOException e) { + System.err.println("Error Loading DDMS Preferences"); + } + } + } + + assert sPrefStore != null; + return sPrefStore; + } + } + + /** + * Save the prefs to the config file. + */ + public void save() { + PreferenceStore prefs = getPreferenceStore(); + synchronized (DdmsPreferenceStore.class) { + try { + prefs.save(); + } + catch (IOException ioe) { + // FIXME com.android.dmmlib.Log.w("ddms", "Failed saving prefs file: " + ioe.getMessage()); + } + } + } + + // ---- Utility methods to access some specific prefs ---- + + /** + * Indicates whether the ping ID is set. + * This should be true when {@link #isPingOptIn()} is true. + * + * @return true if a ping ID is set, which means the user gave permission + * to use the ping service. + */ + public boolean hasPingId() { + PreferenceStore prefs = getPreferenceStore(); + synchronized (DdmsPreferenceStore.class) { + return prefs != null && prefs.contains(PING_ID); + } + } + + /** + * Retrieves the current ping ID, if set. + * To know if the ping ID is set, use {@link #hasPingId()}. + *