aboutsummaryrefslogtreecommitdiffstats
path: root/eclipse/plugins
diff options
context:
space:
mode:
authorThe Android Open Source Project <initial-contribution@android.com>2009-03-03 19:29:09 -0800
committerThe Android Open Source Project <initial-contribution@android.com>2009-03-03 19:29:09 -0800
commit55a2c71f27d3e0b8344597c7f281e687cb7aeb1b (patch)
treeecd18b995aea8eeeb8b3823266280d41245bf0f7 /eclipse/plugins
parent82ea7a177797b844b252effea5c7c7c5d63ea4ac (diff)
downloadsdk-55a2c71f27d3e0b8344597c7f281e687cb7aeb1b.zip
sdk-55a2c71f27d3e0b8344597c7f281e687cb7aeb1b.tar.gz
sdk-55a2c71f27d3e0b8344597c7f281e687cb7aeb1b.tar.bz2
auto import from //depot/cupcake/@135843
Diffstat (limited to 'eclipse/plugins')
-rw-r--r--eclipse/plugins/README.txt114
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/.classpath16
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/.project30
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF79
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/MODULE_LICENSE_EPL0
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/NOTICE224
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/about.ini1
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/build.properties17
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/icons/add.pngbin0 -> 146 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/icons/android.pngbin0 -> 3609 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/icons/android_32X32.jpgbin0 -> 4204 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/icons/android_large.pngbin0 -> 6094 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/icons/android_project.pngbin0 -> 146 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/icons/az_sort.pngbin0 -> 363 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/icons/delete.pngbin0 -> 107 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/icons/dimension.pngbin0 -> 320 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/icons/down.pngbin0 -> 157 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/icons/dpi.pngbin0 -> 302 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/icons/error.pngbin0 -> 194 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/icons/keyboard.pngbin0 -> 307 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/icons/language.pngbin0 -> 287 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/icons/mainLaunchTab.pngbin0 -> 308 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/icons/match.pngbin0 -> 138 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/icons/mcc.pngbin0 -> 463 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/icons/mnc.pngbin0 -> 265 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/icons/navpad.pngbin0 -> 308 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/icons/new_adt_project.pngbin0 -> 664 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/icons/new_xml.pngbin0 -> 3577 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/icons/orientation.pngbin0 -> 325 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/icons/region.pngbin0 -> 445 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/icons/text_input.pngbin0 -> 321 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/icons/touch.pngbin0 -> 344 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/icons/up.pngbin0 -> 137 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/icons/warning.pngbin0 -> 147 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml502
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtConstants.java59
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java1372
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/Messages.java48
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/VersionCheck.java115
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ApkBuilder.java1154
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ApkDeltaVisitor.java280
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/BaseBuilder.java929
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/DexWrapper.java168
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/Messages.java137
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerBuilder.java1150
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerDeltaVisitor.java432
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ResourceManagerBuilder.java130
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/build_messages.properties61
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/AndroidLaunch.java57
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/AndroidLaunchController.java1838
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/DeviceChooserDialog.java705
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/LaunchConfigDelegate.java431
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/LaunchShortcut.java87
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/ui/EmulatorConfigTab.java459
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/ui/LaunchConfigTabGroup.java40
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/ui/MainLaunchConfigTab.java489
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/messages.properties16
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/AndroidPreferencePage.java209
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/BuildPreferencePage.java217
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/LaunchPreferencePage.java51
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/Messages.java43
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/PreferenceInitializer.java57
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/messages.properties14
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/AndroidNature.java291
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ConvertToAndroidAction.java153
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/CreateAidlImportAction.java210
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ExportAction.java65
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ExportWizardAction.java57
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FixLaunchConfig.java156
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FixProjectAction.java136
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FolderDecorator.java105
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/NewXmlFileWizardAction.java57
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ProjectHelper.java668
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/ExportWizard.java501
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeyCheckPage.java291
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeyCreationPage.java332
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeySelectionPage.java266
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeystoreSelectionPage.java260
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/ProjectCheckPage.java322
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/internal/AndroidClasspathContainer.java60
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/internal/AndroidClasspathContainerInitializer.java627
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/properties/AndroidPropertyPage.java123
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/AndroidJarLoader.java431
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/AndroidTargetData.java321
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/AndroidTargetParser.java704
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/FrameworkResourceRepository.java76
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/IAndroidClassLoader.java81
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/LayoutParamsParser.java372
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/LoadStatus.java24
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/Sdk.java492
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/WidgetClassLoader.java334
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/NewProjectAction.java34
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/NewXmlFileAction.java34
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/OpenWizardAction.java139
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newproject/NewProjectCreationPage.java1319
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newproject/NewProjectWizard.java737
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/AndroidConstants.java219
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/EclipseUiHelper.java64
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/Messages.java21
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/SdkStatsHelper.java39
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/StreamHelper.java63
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/messages.properties2
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/preferences/UsagePreferencePage.java123
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidManifestHelper.java241
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidManifestParser.java661
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidXPathFactory.java89
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/BaseProjectHelper.java452
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/ExportHelper.java188
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/ProjectChooserHelper.java110
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/XmlErrorHandler.java115
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/AttrsXmlParser.java505
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/DeclareStyleableInfo.java174
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/IIdResourceItem.java28
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/IPathChangedListener.java29
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/IResourceRepository.java47
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/ResourceItem.java48
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/ResourceType.java111
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/ViewClassInfo.java159
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/AndroidContentAssist.java791
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/AndroidEditor.java828
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/AndroidSourceViewerConfig.java116
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/FirstElementParser.java164
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/IconFactory.java255
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/AttributeDescriptor.java104
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/AttributeDescriptorLabelProvider.java81
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/BooleanAttributeDescriptor.java33
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/DescriptorsUtils.java845
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/DocumentDescriptor.java57
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/ElementDescriptor.java318
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/EnumAttributeDescriptor.java41
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/FlagAttributeDescriptor.java85
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/IDescriptorProvider.java24
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/ListAttributeDescriptor.java71
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/ReferenceAttributeDescriptor.java84
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/SeparatorAttributeDescriptor.java45
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/TextAttributeDescriptor.java137
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/TextValueDescriptor.java48
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/XmlnsAttributeDescriptor.java81
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/BasePullParser.java219
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/GraphicalLayoutEditor.java2402
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutConstants.java65
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutContentAssist.java33
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutCreatorDialog.java140
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutEditor.java419
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutReloadMonitor.java214
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutSourceViewerConfig.java30
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/MatchingStrategy.java64
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/PaletteFactory.java93
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/ProjectCallback.java161
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/UiContentOutlinePage.java615
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/UiElementPullParser.java244
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/UiPropertySheetPage.java139
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/WidgetPullParser.java143
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/descriptors/CustomViewDescriptorService.java284
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/descriptors/LayoutDescriptors.java204
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/descriptors/ViewElementDescriptor.java139
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/DropFeedback.java761
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/ElementCreateCommand.java98
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/ElementFigure.java75
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/LayoutFigure.java151
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiDocumentEditPart.java212
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiDocumentTreeEditPart.java37
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiElementEditPart.java345
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiElementTreeEditPart.java74
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiElementTreeEditPartFactory.java47
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiElementsEditPartFactory.java55
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiLayoutEditPart.java115
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiLayoutTreeEditPart.java39
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiViewEditPart.java55
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiViewTreeEditPart.java30
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/uimodel/UiViewElementNode.java130
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/ManifestContentAssist.java33
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/ManifestEditor.java387
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/ManifestEditorContributor.java100
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/ManifestSourceViewerConfig.java30
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/AndroidManifestDescriptors.java562
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/ApplicationAttributeDescriptor.java44
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/ClassAttributeDescriptor.java88
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/InstrumentationAttributeDescriptor.java46
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/ManifestElementDescriptor.java96
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/PackageAttributeDescriptor.java41
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/PostActivityCreationAction.java88
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/PostReceiverCreationAction.java88
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/ThemeAttributeDescriptor.java42
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/model/UiClassAttributeNode.java624
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/model/UiManifestElementNode.java92
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/model/UiPackageAttributeNode.java319
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/ApplicationAttributesPart.java174
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/ApplicationPage.java126
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/ApplicationToggle.java313
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/InstrumentationPage.java92
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/OverviewExportPart.java87
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/OverviewInfoPart.java222
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/OverviewLinksPart.java125
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/OverviewPage.java90
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/PermissionPage.java101
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/MenuContentAssist.java33
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/MenuEditor.java184
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/MenuSourceViewerConfig.java30
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/MenuTreePage.java62
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/descriptors/MenuDescriptors.java196
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/ResourcesContentAssist.java33
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/ResourcesEditor.java164
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/ResourcesSourceViewerConfig.java30
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/ResourcesTreePage.java85
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/CountryCodeQualifier.java144
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/FolderConfiguration.java499
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/KeyboardStateQualifier.java180
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/LanguageQualifier.java145
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/NavigationMethodQualifier.java183
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/NetworkCodeQualifier.java156
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/PixelDensityQualifier.java144
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/RegionQualifier.java145
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/ResourceQualifier.java86
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/ScreenDimensionQualifier.java148
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/ScreenOrientationQualifier.java178
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/TextInputMethodQualifier.java182
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/TouchScreenQualifier.java180
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/descriptors/ColorValueDescriptor.java41
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/descriptors/ItemElementDescriptor.java55
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/descriptors/ResourcesDescriptors.java283
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/explorer/ResourceExplorerView.java340
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/CompiledResourcesMonitor.java211
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ConfigurableResourceItem.java82
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/FolderTypeRelationship.java164
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/IdResourceItem.java54
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/IntArrayWrapper.java50
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/MultiResourceFile.java174
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ProjectClassLoader.java251
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ProjectResourceItem.java91
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ProjectResources.java804
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/Resource.java46
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceFile.java100
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceFolder.java251
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceFolderType.java73
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceManager.java493
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceMonitor.java377
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/SingleResourceFile.java145
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/FileWrapper.java86
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/FolderWrapper.java73
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IAbstractFile.java44
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IAbstractFolder.java38
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IAbstractResource.java34
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IFileWrapper.java68
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IFolderWrapper.java74
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/uimodel/UiColorValueNode.java80
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/uimodel/UiItemElementNode.java58
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/EditableDialogCellEditor.java458
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/ErrorImageComposite.java47
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/FlagValueCellEditor.java58
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/ListValueCellEditor.java76
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/ResourceValueCellEditor.java59
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/SectionHelper.java348
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/TextValueCellEditor.java43
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/UiElementPart.java283
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/CopyCutAction.java220
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/ICommitXml.java28
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/NewItemSelectionDialog.java229
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/PasteAction.java124
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiActions.java385
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiElementDetail.java486
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiModelTreeContentProvider.java115
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiModelTreeLabelProvider.java100
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiTreeBlock.java882
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/IUiSettableAttributeNode.java32
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/IUiUpdateListener.java47
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiAbstractTextAttributeNode.java119
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiAttributeNode.java155
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiDocumentNode.java135
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiElementNode.java1500
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiFlagAttributeNode.java308
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiListAttributeNode.java200
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiResourceAttributeNode.java161
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiSeparatorAttributeNode.java142
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiTextAttributeNode.java190
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiTextValueNode.java118
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ConfigurationSelector.java1278
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/NewXmlFileCreationPage.java1159
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/NewXmlFileWizard.java226
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ReferenceChooserDialog.java266
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ResourceChooser.java193
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ResourceContentProvider.java110
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ResourceLabelProvider.java138
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/XmlContentAssist.java33
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/XmlEditor.java202
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/XmlSourceViewerConfig.java30
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/XmlTreePage.java62
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/descriptors/XmlDescriptors.java364
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/templates/AndroidManifest.template10
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/templates/activity.template7
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/templates/icon.pngbin0 -> 3180 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/templates/java_file.template13
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/templates/launcher_intent_filter.template1
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/templates/layout.template13
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/templates/preference_intent_filter.template1
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/templates/string.template1
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/templates/strings.template5
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/templates/uses-sdk.template1
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.ddms/.classpath10
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.ddms/.project28
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.ddms/META-INF/MANIFEST.MF23
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.ddms/MODULE_LICENSE_APACHE20
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.ddms/build.properties10
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.ddms/icons/android.pngbin0 -> 3609 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.ddms/icons/capture.pngbin0 -> 696 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.ddms/plugin.xml102
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/CommonAction.java69
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/DdmsPlugin.java565
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/ImageLoader.java67
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/Perspective.java80
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/preferences/LogCatPreferencePage.java74
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/preferences/PreferenceInitializer.java102
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/preferences/PreferencePage.java82
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/DeviceView.java329
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/EmulatorControlView.java43
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/EventLogView.java114
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/FileExplorerView.java165
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/HeapView.java47
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/LogCatView.java328
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/NativeHeapView.java47
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/SelectionDependentViewPart.java68
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/TableView.java96
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/ThreadView.java47
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/.classpath10
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/.project29
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/META-INF/MANIFEST.MF19
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/MODULE_LICENSE_EPL0
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/NOTICE224
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/README.txt72
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/build.properties10
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/not_source_folder/jar/example/Class1.java35
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/not_source_folder/jar/example/Class2.java21
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/prefs.template3
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/wizards/newproject/StubSampleProjectCreationPage.java71
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/wizards/newproject/StubSampleProjectWizard.java102
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/AdtTestData.java75
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/AllTests.java40
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/AndroidTestPlugin.java69
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/EclipseTestCollector.java113
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/FuncTestCase.java51
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/FuncTests.java46
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/UnitTests.java58
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/functests/sampleProjects/SampleProjectTest.java189
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/test.xml76
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittest.xml54
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/build/BaseBuilderTest.java37
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/project/ProjectHelperTest.java67
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/sdk/AndroidJarLoaderTest.java164
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/sdk/LayoutParamsParserTest.java184
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/common/project/AndroidManifestHelperTest.java97
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/common/resources/AttrsXmlParserTest.java132
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/descriptors/DescriptorsUtilsTest.java124
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/layout/UiElementPullParserTest.java240
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/manifest/model/UiElementNodeTest.java211
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/mock/MockNamedNodeMap.java106
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/mock/MockNodeList.java60
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/mock/MockXmlNode.java286
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/CountryCodeQualifierTest.java55
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/KeyboardStateQualifierTest.java61
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/LanguageQualifierTest.java53
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/NavigationMethodQualifierTest.java69
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/NetworkCodeQualifierTest.java55
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/PixelDensityQualifierTest.java55
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/RegionQualifierTest.java54
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/ScreenDimensionQualifierTest.java57
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/ScreenOrientationQualifierTest.java71
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/TextInputMethodQualifierTest.java71
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/TouchScreenQualifierTest.java70
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/manager/ConfigMatchTest.java255
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/manager/QualifierListTest.java77
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/ClasspathEntryMock.java89
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FileMock.java447
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FolderMock.java451
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/JavaProjectMock.java414
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/ProjectMock.java499
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/jar_example.jarbin0 -> 1829 bytes
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/jar_example.jardesc16
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/mock_attrs.xml340
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/mock_android/view/View.java21
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/mock_android/view/ViewGroup.java29
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/mock_android/widget/LinearLayout.java27
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.tests/unittests/mock_android/widget/TableLayout.java27
382 files changed, 68969 insertions, 0 deletions
diff --git a/eclipse/plugins/README.txt b/eclipse/plugins/README.txt
new file mode 100644
index 0000000..184d731
--- /dev/null
+++ b/eclipse/plugins/README.txt
@@ -0,0 +1,114 @@
+Compiling and deploying the Android Development Toolkit (ADT) feature.
+
+The ADT feature is composed of four plugins:
+- com.android.ide.eclipse.adt:
+ The ADT plugin, which provides support for compiling and debugging android
+ applications.
+- com.android.ide.eclipse.common:
+ A common plugin providing utility services to the other plugins.
+- com.android.ide.eclipse.editors:
+ A plugin providing optional XML editors.
+- com.android.ide.eclipse.ddms:
+ A plugin version of the tool DDMS
+
+Because the DDMS plugin source code is not yet released, compiling the
+ADT/Common/Editors plugins requires to install the DDMS plugin in eclipse.
+
+Basic requirements:
+- Eclipse 3.3 or 3.4 with JDT and PDE.
+- DDMS plugin installed and running.
+
+
+--------------------------
+1- Install the DDMS plugin
+--------------------------
+
+The easiest way to setup the DDMS plugin in your Eclipse environment is to
+install the ADT features (see SDK documentation for details) and then remove
+the following features and plugins:
+
+- <eclipse-directory>/features/com.android.ide.eclipse.adt_x.x.x.jar
+- <eclipse-directory>/plugins/com.android.ide.eclipse.adt_x.x.x.jar
+- <eclipse-directory>/plugins/com.android.ide.eclipse.common_x.x.x.jar
+- <eclipse-directory>/plugins/com.android.ide.eclipse.editors_x.x.x.jar
+
+This will leave you with only the DDMS plugin installed in your Eclipse
+distribution.
+
+
+-------------------------------------
+2- Setting up the ADT/Common project
+-------------------------------------
+
+- Download the ADT/Common/Editors source.
+
+- From the SDK, copy the following jars:
+ * androidprefs.jar => com.android.ide.eclipse.adt folder.
+ * jarutils.jar => com.android.ide.eclipse.adt folder.
+ * ping.jar => com.android.ide.eclipse.common folder.
+ * androidprefs.jar => com.android.ide.eclipse.common folder.
+
+- Create a java project from existing source for both the ADT plugin and the
+ common plugin.
+
+- In the Package Explorer, right click the projects and choose
+ PDE Tools > Convert Projects to Plug-in Project...
+
+- Select your projects in the dialog box and click OK.
+
+- In the Package Explorer, for ADT and common, right click the jar files mentioned above
+ and choose Build Path > Add to Build Path
+
+At this point the projects will compile.
+
+To launch the projects, open the Run/Debug Dialog and create an "Eclipse
+Application" launch configuration.
+
+Additionnaly, another feature containing the Android Editors Plugin
+(com.android.ide.eclipse.editors) is available.
+
+- Make sure the common project is present in your workspace as the Editors
+ plugin depends on this plugin. Alternatively, you can have the offical ADT
+ feature installed in your Eclipse distribution.
+- Create a java project from existing source for the Editors project.
+- In the Package Explorer, right click the project and choose
+ PDE Tools > Convert Projects to Plug-in Project...
+- Select your project in the dialog box and click OK.
+
+Create an "Eclipse Application" launch configuration to test the plugin.
+
+-------------------------------------
+3- Setting up the Editors project
+-------------------------------------
+
+The "editors" plugin is optional. You can use ADT to develop Android
+applications without the XML editor support. When this plugin is present, it
+offers several customized form-based XML editors and one graphical layout
+editor.
+
+At the time of this release (Android 0.9 SDK), some of the supporting libraries
+still need some cleanup and are currently only provided as JAR files.
+
+- Download the ADT/Common/Editors source.
+
+- From the source archives, copy the following jars:
+ * ninepatch.jar => com.android.ide.eclipse.editors folder.
+ * layoutlib_utils.jar => com.android.ide.eclipse.editors folder.
+ * layoutlib_api.jar => com.android.ide.eclipse.editors folder.
+
+- From http://kxml.sourceforge.net/ download:
+ * kXML2-2.3.0.jar => com.android.ide.eclipse.editors folder.
+
+- Create a java project from existing source for both the editors plugin.
+
+- In the Package Explorer, right click the project and choose
+ PDE Tools > Convert Projects to Plug-in Project...
+
+- Select your project in the dialog box and click OK.
+
+- In the Package Explorer for editors, right click the jar files mentioned
+ above and choose Build Path > Add to Build Path
+
+To launch the projects, reuse the "Eclipse Application" launch configuration
+created for ADT.
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/.classpath b/eclipse/plugins/com.android.ide.eclipse.adt/.classpath
new file mode 100644
index 0000000..c3c8c10
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/.classpath
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry excluding="Makefile|resources/" kind="src" path="src"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+ <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+ <classpathentry kind="lib" path="jarutils.jar"/>
+ <classpathentry kind="lib" path="androidprefs.jar"/>
+ <classpathentry kind="lib" path="sdkstats.jar"/>
+ <classpathentry kind="lib" path="kxml2-2.3.0.jar"/>
+ <classpathentry kind="lib" path="layoutlib_api.jar"/>
+ <classpathentry kind="lib" path="layoutlib_utils.jar"/>
+ <classpathentry kind="lib" path="ninepatch.jar"/>
+ <classpathentry combineaccessrules="false" kind="src" path="/SdkLib"/>
+ <classpathentry combineaccessrules="false" kind="src" path="/SdkUiLib"/>
+ <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/.project b/eclipse/plugins/com.android.ide.eclipse.adt/.project
new file mode 100644
index 0000000..c7b1ad4
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/.project
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>adt</name>
+ <comment></comment>
+ <projects>
+ <project>SdkLib</project>
+ <project>SdkUiLib</project>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.pde.ManifestBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.pde.SchemaBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.eclipse.pde.PluginNature</nature>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+</projectDescription>
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF b/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF
new file mode 100644
index 0000000..a464d5c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/META-INF/MANIFEST.MF
@@ -0,0 +1,79 @@
+Manifest-Version: 1.0
+Bundle-ManifestVersion: 2
+Bundle-Name: Android Development Toolkit
+Bundle-SymbolicName: com.android.ide.eclipse.adt;singleton:=true
+Bundle-Version: 0.9.0.qualifier
+Bundle-ClassPath: .,
+ jarutils.jar,
+ androidprefs.jar,
+ sdkstats.jar,
+ kxml2-2.3.0.jar,
+ layoutlib_api.jar,
+ ninepatch.jar,
+ layoutlib_utils.jar,
+ sdklib.jar,
+ sdkuilib.jar
+Bundle-Activator: com.android.ide.eclipse.adt.AdtPlugin
+Bundle-Vendor: The Android Open Source Project
+Require-Bundle: com.android.ide.eclipse.ddms,
+ org.eclipse.core.runtime,
+ org.eclipse.core.resources,
+ org.eclipse.debug.core,
+ org.eclipse.debug.ui,
+ org.eclipse.jdt,
+ org.eclipse.ant.core,
+ org.eclipse.jdt.core,
+ org.eclipse.jdt.ui,
+ org.eclipse.jdt.launching,
+ org.eclipse.jface.text,
+ org.eclipse.ui.editors,
+ org.eclipse.ui.workbench.texteditor,
+ org.eclipse.ui.console,
+ org.eclipse.core.filesystem,
+ org.eclipse.ui,
+ org.eclipse.ui.ide,
+ org.eclipse.ui.forms,
+ org.eclipse.gef,
+ org.eclipse.ui.browser,
+ org.eclipse.ui.views,
+ org.eclipse.wst.sse.core,
+ org.eclipse.wst.sse.ui,
+ org.eclipse.wst.xml.core,
+ org.eclipse.wst.xml.ui
+Eclipse-LazyStart: true
+Export-Package: com.android.ide.eclipse.adt,
+ com.android.ide.eclipse.adt.build;x-friends:="com.android.ide.eclipse.tests",
+ com.android.ide.eclipse.adt.project;x-friends:="com.android.ide.eclipse.tests",
+ com.android.ide.eclipse.adt.project.internal;x-friends:="com.android.ide.eclipse.tests",
+ com.android.ide.eclipse.adt.sdk;x-friends:="com.android.ide.eclipse.tests",
+ com.android.ide.eclipse.adt.wizards.newproject;x-friends:="com.android.ide.eclipse.tests",
+ com.android.ide.eclipse.common,
+ com.android.ide.eclipse.common.project,
+ com.android.ide.eclipse.common.resources,
+ com.android.ide.eclipse.editors;x-friends:="com.android.ide.eclipse.tests",
+ com.android.ide.eclipse.editors.descriptors;x-friends:="com.android.ide.eclipse.tests",
+ com.android.ide.eclipse.editors.layout;x-friends:="com.android.ide.eclipse.tests",
+ com.android.ide.eclipse.editors.layout.descriptors;x-friends:="com.android.ide.eclipse.tests",
+ com.android.ide.eclipse.editors.layout.parts;x-friends:="com.android.ide.eclipse.tests",
+ com.android.ide.eclipse.editors.layout.uimodel;x-friends:="com.android.ide.eclipse.tests",
+ com.android.ide.eclipse.editors.manifest;x-friends:="com.android.ide.eclipse.tests",
+ com.android.ide.eclipse.editors.manifest.descriptors;x-friends:="com.android.ide.eclipse.tests",
+ com.android.ide.eclipse.editors.manifest.model;x-friends:="com.android.ide.eclipse.tests",
+ com.android.ide.eclipse.editors.manifest.pages;x-friends:="com.android.ide.eclipse.tests",
+ com.android.ide.eclipse.editors.menu;x-friends:="com.android.ide.eclipse.tests",
+ com.android.ide.eclipse.editors.menu.descriptors;x-friends:="com.android.ide.eclipse.tests",
+ com.android.ide.eclipse.editors.resources;x-friends:="com.android.ide.eclipse.tests",
+ com.android.ide.eclipse.editors.resources.configurations;x-friends:="com.android.ide.eclipse.tests",
+ com.android.ide.eclipse.editors.resources.descriptors;x-friends:="com.android.ide.eclipse.tests",
+ com.android.ide.eclipse.editors.resources.explorer;x-friends:="com.android.ide.eclipse.tests",
+ com.android.ide.eclipse.editors.resources.manager;x-friends:="com.android.ide.eclipse.tests",
+ com.android.ide.eclipse.editors.resources.manager.files;x-friends:="com.android.ide.eclipse.tests",
+ com.android.ide.eclipse.editors.resources.uimodel;x-friends:="com.android.ide.eclipse.tests",
+ com.android.ide.eclipse.editors.ui;x-friends:="com.android.ide.eclipse.tests",
+ com.android.ide.eclipse.editors.ui.tree;x-friends:="com.android.ide.eclipse.tests",
+ com.android.ide.eclipse.editors.uimodel;x-friends:="com.android.ide.eclipse.tests",
+ com.android.ide.eclipse.editors.wizards;x-friends:="com.android.ide.eclipse.tests",
+ com.android.ide.eclipse.editors.xml;x-friends:="com.android.ide.eclipse.tests",
+ com.android.ide.eclipse.editors.xml.descriptors;x-friends:="com.android.ide.eclipse.tests"
+
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/MODULE_LICENSE_EPL b/eclipse/plugins/com.android.ide.eclipse.adt/MODULE_LICENSE_EPL
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/MODULE_LICENSE_EPL
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/NOTICE b/eclipse/plugins/com.android.ide.eclipse.adt/NOTICE
new file mode 100644
index 0000000..49c101d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/NOTICE
@@ -0,0 +1,224 @@
+*Eclipse Public License - v 1.0*
+
+THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
+PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF
+THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
+
+*1. DEFINITIONS*
+
+"Contribution" means:
+
+a) in the case of the initial Contributor, the initial code and
+documentation distributed under this Agreement, and
+b) in the case of each subsequent Contributor:
+
+i) changes to the Program, and
+
+ii) additions to the Program;
+
+where such changes and/or additions to the Program originate from and
+are distributed by that particular Contributor. A Contribution
+'originates' from a Contributor if it was added to the Program by such
+Contributor itself or anyone acting on such Contributor's behalf.
+Contributions do not include additions to the Program which: (i) are
+separate modules of software distributed in conjunction with the Program
+under their own license agreement, and (ii) are not derivative works of
+the Program.
+
+"Contributor" means any person or entity that distributes the Program.
+
+"Licensed Patents " mean patent claims licensable by a Contributor which
+are necessarily infringed by the use or sale of its Contribution alone
+or when combined with the Program.
+
+"Program" means the Contributions distributed in accordance with this
+Agreement.
+
+"Recipient" means anyone who receives the Program under this Agreement,
+including all Contributors.
+
+*2. GRANT OF RIGHTS*
+
+a) Subject to the terms of this Agreement, each Contributor hereby
+grants Recipient a non-exclusive, worldwide, royalty-free copyright
+license to reproduce, prepare derivative works of, publicly display,
+publicly perform, distribute and sublicense the Contribution of such
+Contributor, if any, and such derivative works, in source code and
+object code form.
+
+b) Subject to the terms of this Agreement, each Contributor hereby
+grants Recipient a non-exclusive, worldwide, royalty-free patent license
+under Licensed Patents to make, use, sell, offer to sell, import and
+otherwise transfer the Contribution of such Contributor, if any, in
+source code and object code form. This patent license shall apply to the
+combination of the Contribution and the Program if, at the time the
+Contribution is added by the Contributor, such addition of the
+Contribution causes such combination to be covered by the Licensed
+Patents. The patent license shall not apply to any other combinations
+which include the Contribution. No hardware per se is licensed hereunder.
+
+c) Recipient understands that although each Contributor grants the
+licenses to its Contributions set forth herein, no assurances are
+provided by any Contributor that the Program does not infringe the
+patent or other intellectual property rights of any other entity. Each
+Contributor disclaims any liability to Recipient for claims brought by
+any other entity based on infringement of intellectual property rights
+or otherwise. As a condition to exercising the rights and licenses
+granted hereunder, each Recipient hereby assumes sole responsibility to
+secure any other intellectual property rights needed, if any. For
+example, if a third party patent license is required to allow Recipient
+to distribute the Program, it is Recipient's responsibility to acquire
+that license before distributing the Program.
+
+d) Each Contributor represents that to its knowledge it has sufficient
+copyright rights in its Contribution, if any, to grant the copyright
+license set forth in this Agreement.
+
+*3. REQUIREMENTS*
+
+A Contributor may choose to distribute the Program in object code form
+under its own license agreement, provided that:
+
+a) it complies with the terms and conditions of this Agreement; and
+
+b) its license agreement:
+
+i) effectively disclaims on behalf of all Contributors all warranties
+and conditions, express and implied, including warranties or conditions
+of title and non-infringement, and implied warranties or conditions of
+merchantability and fitness for a particular purpose;
+
+ii) effectively excludes on behalf of all Contributors all liability for
+damages, including direct, indirect, special, incidental and
+consequential damages, such as lost profits;
+
+iii) states that any provisions which differ from this Agreement are
+offered by that Contributor alone and not by any other party; and
+
+iv) states that source code for the Program is available from such
+Contributor, and informs licensees how to obtain it in a reasonable
+manner on or through a medium customarily used for software exchange.
+
+When the Program is made available in source code form:
+
+a) it must be made available under this Agreement; and
+
+b) a copy of this Agreement must be included with each copy of the Program.
+
+Contributors may not remove or alter any copyright notices contained
+within the Program.
+
+Each Contributor must identify itself as the originator of its
+Contribution, if any, in a manner that reasonably allows subsequent
+Recipients to identify the originator of the Contribution.
+
+*4. COMMERCIAL DISTRIBUTION*
+
+Commercial distributors of software may accept certain responsibilities
+with respect to end users, business partners and the like. While this
+license is intended to facilitate the commercial use of the Program, the
+Contributor who includes the Program in a commercial product offering
+should do so in a manner which does not create potential liability for
+other Contributors. Therefore, if a Contributor includes the Program in
+a commercial product offering, such Contributor ("Commercial
+Contributor") hereby agrees to defend and indemnify every other
+Contributor ("Indemnified Contributor") against any losses, damages and
+costs (collectively "Losses") arising from claims, lawsuits and other
+legal actions brought by a third party against the Indemnified
+Contributor to the extent caused by the acts or omissions of such
+Commercial Contributor in connection with its distribution of the
+Program in a commercial product offering. The obligations in this
+section do not apply to any claims or Losses relating to any actual or
+alleged intellectual property infringement. In order to qualify, an
+Indemnified Contributor must: a) promptly notify the Commercial
+Contributor in writing of such claim, and b) allow the Commercial
+Contributor to control, and cooperate with the Commercial Contributor
+in, the defense and any related settlement negotiations. The Indemnified
+Contributor may participate in any such claim at its own expense.
+
+For example, a Contributor might include the Program in a commercial
+product offering, Product X. That Contributor is then a Commercial
+Contributor. If that Commercial Contributor then makes performance
+claims, or offers warranties related to Product X, those performance
+claims and warranties are such Commercial Contributor's responsibility
+alone. Under this section, the Commercial Contributor would have to
+defend claims against the other Contributors related to those
+performance claims and warranties, and if a court requires any other
+Contributor to pay any damages as a result, the Commercial Contributor
+must pay those damages.
+
+*5. NO WARRANTY*
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED
+ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES
+OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR
+A PARTICULAR PURPOSE. Each Recipient is solely responsible for
+determining the appropriateness of using and distributing the Program
+and assumes all risks associated with its exercise of rights under this
+Agreement , including but not limited to the risks and costs of program
+errors, compliance with applicable laws, damage to or loss of data,
+programs or equipment, and unavailability or interruption of operations.
+
+*6. DISCLAIMER OF LIABILITY*
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR
+ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING
+WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR
+DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED
+HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+*7. GENERAL*
+
+If any provision of this Agreement is invalid or unenforceable under
+applicable law, it shall not affect the validity or enforceability of
+the remainder of the terms of this Agreement, and without further action
+by the parties hereto, such provision shall be reformed to the minimum
+extent necessary to make such provision valid and enforceable.
+
+If Recipient institutes patent litigation against any entity (including
+a cross-claim or counterclaim in a lawsuit) alleging that the Program
+itself (excluding combinations of the Program with other software or
+hardware) infringes such Recipient's patent(s), then such Recipient's
+rights granted under Section 2(b) shall terminate as of the date such
+litigation is filed.
+
+All Recipient's rights under this Agreement shall terminate if it fails
+to comply with any of the material terms or conditions of this Agreement
+and does not cure such failure in a reasonable period of time after
+becoming aware of such noncompliance. If all Recipient's rights under
+this Agreement terminate, Recipient agrees to cease use and distribution
+of the Program as soon as reasonably practicable. However, Recipient's
+obligations under this Agreement and any licenses granted by Recipient
+relating to the Program shall continue and survive.
+
+Everyone is permitted to copy and distribute copies of this Agreement,
+but in order to avoid inconsistency the Agreement is copyrighted and may
+only be modified in the following manner. The Agreement Steward reserves
+the right to publish new versions (including revisions) of this
+Agreement from time to time. No one other than the Agreement Steward has
+the right to modify this Agreement. The Eclipse Foundation is the
+initial Agreement Steward. The Eclipse Foundation may assign the
+responsibility to serve as the Agreement Steward to a suitable separate
+entity. Each new version of the Agreement will be given a distinguishing
+version number. The Program (including Contributions) may always be
+distributed subject to the version of the Agreement under which it was
+received. In addition, after a new version of the Agreement is
+published, Contributor may elect to distribute the Program (including
+its Contributions) under the new version. Except as expressly stated in
+Sections 2(a) and 2(b) above, Recipient receives no rights or licenses
+to the intellectual property of any Contributor under this Agreement,
+whether expressly, by implication, estoppel or otherwise. All rights in
+the Program not expressly granted under this Agreement are reserved.
+
+This Agreement is governed by the laws of the State of New York and the
+intellectual property laws of the United States of America. No party to
+this Agreement will bring a legal action under this Agreement more than
+one year after the cause of action arose. Each party waives its rights
+to a jury trial in any resulting litigation.
+
+
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/about.ini b/eclipse/plugins/com.android.ide.eclipse.adt/about.ini
new file mode 100644
index 0000000..fddaef0
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/about.ini
@@ -0,0 +1 @@
+featureImage=icons/android_32X32.jpg
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/build.properties b/eclipse/plugins/com.android.ide.eclipse.adt/build.properties
new file mode 100644
index 0000000..c7eb749
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/build.properties
@@ -0,0 +1,17 @@
+bin.includes = plugin.xml,\
+ META-INF/,\
+ icons/,\
+ .,\
+ templates/,\
+ about.ini,\
+ jarutils.jar,\
+ androidprefs.jar,\
+ sdkstats.jar,\
+ kxml2-2.3.0.jar,\
+ layoutlib_api.jar,\
+ layoutlib_utils.jar,\
+ ninepatch.jar,\
+ sdklib.jar,\
+ sdkuilib.jar
+source.. = src/
+output.. = bin/
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/add.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/add.png
new file mode 100644
index 0000000..eefc2ca
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/add.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/android.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/android.png
new file mode 100644
index 0000000..3779d4d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/android.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/android_32X32.jpg b/eclipse/plugins/com.android.ide.eclipse.adt/icons/android_32X32.jpg
new file mode 100644
index 0000000..823670b
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/android_32X32.jpg
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/android_large.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/android_large.png
new file mode 100644
index 0000000..64e3601
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/android_large.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/android_project.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/android_project.png
new file mode 100644
index 0000000..5334568
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/android_project.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/az_sort.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/az_sort.png
new file mode 100644
index 0000000..5d92f76
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/az_sort.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/delete.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/delete.png
new file mode 100644
index 0000000..db5fab8
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/delete.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/dimension.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/dimension.png
new file mode 100644
index 0000000..10057c8
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/dimension.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/down.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/down.png
new file mode 100644
index 0000000..36cd223
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/down.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/dpi.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/dpi.png
new file mode 100644
index 0000000..fae5e96
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/dpi.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/error.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/error.png
new file mode 100644
index 0000000..1eecf2c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/error.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/keyboard.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/keyboard.png
new file mode 100644
index 0000000..7911a85
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/keyboard.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/language.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/language.png
new file mode 100644
index 0000000..a727dd5
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/language.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/mainLaunchTab.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/mainLaunchTab.png
new file mode 100644
index 0000000..2540fbb
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/mainLaunchTab.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/match.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/match.png
new file mode 100644
index 0000000..7e939c2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/match.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/mcc.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/mcc.png
new file mode 100644
index 0000000..4dc95d7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/mcc.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/mnc.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/mnc.png
new file mode 100644
index 0000000..aefffe4
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/mnc.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/navpad.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/navpad.png
new file mode 100644
index 0000000..c2bb79a
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/navpad.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/new_adt_project.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/new_adt_project.png
new file mode 100644
index 0000000..0f0e883
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/new_adt_project.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/new_xml.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/new_xml.png
new file mode 100644
index 0000000..8273185
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/new_xml.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/orientation.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/orientation.png
new file mode 100644
index 0000000..423c3cd
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/orientation.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/region.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/region.png
new file mode 100644
index 0000000..9608cd6
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/region.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/text_input.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/text_input.png
new file mode 100644
index 0000000..b4ddc87
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/text_input.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/touch.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/touch.png
new file mode 100644
index 0000000..6536576
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/touch.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/up.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/up.png
new file mode 100644
index 0000000..35b9a46
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/up.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/icons/warning.png b/eclipse/plugins/com.android.ide.eclipse.adt/icons/warning.png
new file mode 100644
index 0000000..ca3b6ed
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/icons/warning.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml b/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml
new file mode 100644
index 0000000..d6c9ac1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/plugin.xml
@@ -0,0 +1,502 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?eclipse version="3.2"?>
+<plugin>
+ <extension
+ id="com.android.ide.eclipse.common.xmlProblem"
+ name="Android XML Format Problem"
+ point="org.eclipse.core.resources.markers">
+ <super type="org.eclipse.core.resources.problemmarker"/>
+ <super type="org.eclipse.core.resources.textmarker"/>
+ <persistent value="true"/>
+ </extension>
+ <extension
+ id="com.android.ide.eclipse.common.aaptProblem"
+ name="Android AAPT Problem"
+ point="org.eclipse.core.resources.markers">
+ <super type="org.eclipse.core.resources.problemmarker"/>
+ <super type="org.eclipse.core.resources.textmarker"/>
+ <persistent value="true"/>
+ </extension>
+ <extension
+ id="com.android.ide.eclipse.common.aapt2Problem"
+ name="Android AAPT Problem"
+ point="org.eclipse.core.resources.markers">
+ <super type="org.eclipse.core.resources.problemmarker"/>
+ <super type="org.eclipse.core.resources.textmarker"/>
+ <persistent value="true"/>
+ </extension>
+ <extension
+ id="com.android.ide.eclipse.common.aidlProblem"
+ name="Android AIDL Problem"
+ point="org.eclipse.core.resources.markers">
+ <super type="org.eclipse.core.resources.problemmarker"/>
+ <super type="org.eclipse.core.resources.textmarker"/>
+ <persistent value="true"/>
+ </extension>
+ <extension
+ id="com.android.ide.eclipse.common.androidProblem"
+ name="Android XML Content Problem"
+ point="org.eclipse.core.resources.markers">
+ <super type="org.eclipse.core.resources.problemmarker"/>
+ <super type="org.eclipse.core.resources.textmarker"/>
+ <persistent value="true"/>
+ </extension>
+ <extension
+ id="ResourceManagerBuilder"
+ name="Android Resource Manager"
+ point="org.eclipse.core.resources.builders">
+ <builder
+ hasNature="true">
+ <run class="com.android.ide.eclipse.adt.build.ResourceManagerBuilder"/>
+ </builder>
+ </extension>
+ <extension
+ id="PreCompilerBuilder"
+ name="Android Pre Compiler"
+ point="org.eclipse.core.resources.builders">
+ <builder
+ hasNature="true">
+ <run class="com.android.ide.eclipse.adt.build.PreCompilerBuilder"/>
+ </builder>
+ </extension>
+ <extension
+ id="ApkBuilder"
+ name="Android Package Builder"
+ point="org.eclipse.core.resources.builders">
+ <builder
+ hasNature="true">
+ <run class="com.android.ide.eclipse.adt.build.ApkBuilder"/>
+ </builder>
+ </extension>
+ <extension
+ id="AndroidNature"
+ name="AndroidNature"
+ point="org.eclipse.core.resources.natures">
+ <runtime>
+ <run class="com.android.ide.eclipse.adt.project.AndroidNature"/>
+ </runtime>
+ <builder id="com.android.ide.eclipse.adt.ResourceManagerBuilder"/>
+ <builder id="com.android.ide.eclipse.adt.PreCompilerBuilder"/>
+ <builder id="com.android.ide.eclipse.adt.ApkBuilder"/>
+ </extension>
+ <extension
+ point="org.eclipse.ui.newWizards">
+ <category
+ id="com.android.ide.eclipse.wizards.category"
+ name="Android"/>
+ <wizard
+ canFinishEarly="false"
+ category="com.android.ide.eclipse.wizards.category"
+ class="com.android.ide.eclipse.adt.wizards.newproject.NewProjectWizard"
+ finalPerspective="org.eclipse.jdt.ui.JavaPerspective"
+ hasPages="true"
+ icon="icons/android.png"
+ id="com.android.ide.eclipse.adt.project.NewProjectWizard"
+ name="Android Project"
+ preferredPerspectives="org.eclipse.jdt.ui.JavaPerspective"
+ project="true"/>
+ <wizard
+ canFinishEarly="false"
+ category="com.android.ide.eclipse.wizards.category"
+ class="com.android.ide.eclipse.editors.wizards.NewXmlFileWizard"
+ finalPerspective="org.eclipse.jdt.ui.JavaPerspective"
+ hasPages="true"
+ icon="icons/android.png"
+ id="com.android.ide.eclipse.editors.wizards.NewXmlFileWizard"
+ name="Android XML File"
+ preferredPerspectives="org.eclipse.jdt.ui.JavaPerspective"
+ project="false">
+ </wizard>
+ </extension>
+ <extension
+ point="org.eclipse.debug.core.launchConfigurationTypes">
+ <launchConfigurationType
+ delegate="com.android.ide.eclipse.adt.debug.launching.LaunchConfigDelegate"
+ delegateDescription="The Android Application Launcher supports running and debugging remote Android applications on devices or emulators."
+ delegateName="Android Launcher"
+ id="com.android.ide.eclipse.adt.debug.LaunchConfigType"
+ modes="debug, run"
+ name="Android Application"
+ public="true"
+ sourceLocatorId="org.eclipse.jdt.launching.sourceLocator.JavaSourceLookupDirector"
+ sourcePathComputerId="org.eclipse.jdt.launching.sourceLookup.javaSourcePathComputer">
+ </launchConfigurationType>
+ </extension>
+ <extension
+ point="org.eclipse.debug.ui.launchConfigurationTypeImages">
+ <launchConfigurationTypeImage
+ configTypeID="com.android.ide.eclipse.adt.debug.LaunchConfigType"
+ icon="icons/android.png"
+ id="com.android.ide.eclipse.adt.debug.LaunchConfigTypeImage"/>
+ </extension>
+ <extension
+ point="org.eclipse.debug.ui.launchConfigurationTabGroups">
+ <launchConfigurationTabGroup
+ class="com.android.ide.eclipse.adt.debug.ui.LaunchConfigTabGroup"
+ description="Android Application"
+ id="com.android.ide.eclipse.adt.debug.LaunchConfigTabGroup"
+ type="com.android.ide.eclipse.adt.debug.LaunchConfigType"/>
+ </extension>
+ <extension
+ point="org.eclipse.debug.ui.launchShortcuts">
+ <shortcut
+ category="com.android.ide.eclipse.adt.debug.LaunchConfigType"
+ class="com.android.ide.eclipse.adt.debug.launching.LaunchShortcut"
+ icon="icons/android.png"
+ id="com.android.ide.eclipse.adt.debug.launching.LaunchShortcut"
+ label="Android Application"
+ modes="debug, run">
+ <contextualLaunch>
+ <enablement>
+ <with variable="selection">
+ <count value="1"/>
+ <iterate>
+ <and>
+ <test property="org.eclipse.jdt.launching.isContainer"/>
+ <test property="org.eclipse.jdt.launching.hasProjectNature" args="com.android.ide.eclipse.adt.AndroidNature"/>
+ </and>
+ </iterate>
+ </with>
+ </enablement>
+ </contextualLaunch>
+ <perspective id="org.eclipse.jdt.ui.JavaPerspective"/>
+ <perspective id="org.eclipse.debug.ui.DebugPerspective"/>
+ <description
+ description="Runs an Android Application"
+ mode="run">
+ </description>
+ <description
+ description="Debugs an Android Application"
+ mode="debug">
+ </description>
+ </shortcut>
+ </extension>
+ <extension
+ point="org.eclipse.ui.popupMenus">
+ <objectContribution
+ id="com.android.ide.eclipse.adt.contribution1"
+ nameFilter="*"
+ objectClass="org.eclipse.core.resources.IProject"
+ adaptable="true">
+ <menu
+ id="com.android.ide.eclipse.adt.AndroidTools"
+ label="Android Tools"
+ path="additions">
+ <separator name="group1"/>
+ </menu>
+ <visibility>
+ <not>
+ <or>
+ <objectState
+ name="projectNature"
+ value="com.android.ide.eclipse.adt.AndroidNature"/>
+ <objectState
+ name="open"
+ value="false"/>
+ </or>
+ </not>
+ </visibility>
+ <action
+ class="com.android.ide.eclipse.adt.project.ConvertToAndroidAction"
+ enablesFor="1"
+ id="com.android.ide.eclipse.adt.ConvertToAndroidAction"
+ label="Convert To Android Project"
+ menubarPath="com.android.ide.eclipse.adt.AndroidTools/group1"/>
+ </objectContribution>
+ <objectContribution
+ id="com.android.ide.eclipse.adt.contribution2"
+ nameFilter="*"
+ objectClass="org.eclipse.core.resources.IProject"
+ adaptable="true">
+ <menu
+ id="com.android.ide.eclipse.adt.AndroidTools"
+ label="Android Tools"
+ path="additions">
+ <separator name="group1"/>
+ <separator name="group2"/>
+ </menu>
+ <filter
+ name="projectNature"
+ value="com.android.ide.eclipse.adt.AndroidNature">
+ </filter>
+ <action
+ class="com.android.ide.eclipse.adt.project.CreateAidlImportAction"
+ enablesFor="1"
+ id="com.android.ide.eclipse.adt.project.CreateAidlImportAction"
+ label="Create Aidl preprocess file for Parcelable classes"
+ menubarPath="com.android.ide.eclipse.adt.AndroidTools/group1"/>
+ <action
+ class="com.android.ide.eclipse.adt.project.NewXmlFileWizardAction"
+ enablesFor="1"
+ id="com.android.ide.eclipse.adt.project.NewXmlFileWizardAction"
+ label="New Resource File..."
+ menubarPath="com.android.ide.eclipse.adt.AndroidTools/group1">
+ </action>
+ <action
+ class="com.android.ide.eclipse.adt.project.ExportAction"
+ enablesFor="1"
+ id="com.android.ide.eclipse.adt.project.ExportAction"
+ label="Export Unsigned Application Package..."
+ menubarPath="com.android.ide.eclipse.adt.AndroidTools/group2"/>
+ <action
+ class="com.android.ide.eclipse.adt.project.ExportWizardAction"
+ enablesFor="1"
+ id="com.android.ide.eclipse.adt.project.ExportWizardAction"
+ label="Export Signed Application Package..."
+ menubarPath="com.android.ide.eclipse.adt.AndroidTools/group2"/>
+ <action
+ class="com.android.ide.eclipse.adt.project.FixProjectAction"
+ enablesFor="1"
+ id="com.android.ide.eclipse.adt.project.FixProjectAction"
+ label="Fix Project Properties"
+ menubarPath="com.android.ide.eclipse.adt.AndroidTools/group3"/>
+ </objectContribution>
+ </extension>
+ <extension
+ point="org.eclipse.ui.preferencePages">
+ <page
+ class="com.android.ide.eclipse.adt.preferences.AndroidPreferencePage"
+ id="com.android.ide.eclipse.preferences.main"
+ name="Android"/>
+ <page
+ category="com.android.ide.eclipse.preferences.main"
+ class="com.android.ide.eclipse.adt.preferences.BuildPreferencePage"
+ id="com.android.ide.eclipse.adt.preferences.BuildPreferencePage"
+ name="Build"/>
+ <page
+ category="com.android.ide.eclipse.preferences.main"
+ class="com.android.ide.eclipse.adt.preferences.LaunchPreferencePage"
+ id="com.android.ide.eclipse.adt.preferences.LaunchPreferencePage"
+ name="Launch"/>
+ <page
+ category="com.android.ide.eclipse.preferences.main"
+ class="com.android.ide.eclipse.common.preferences.UsagePreferencePage"
+ id="com.android.ide.eclipse.common.preferences.UsagePreferencePage"
+ name="Usage Stats">
+ </page>
+ </extension>
+ <extension
+ point="org.eclipse.core.runtime.preferences">
+ <initializer class="com.android.ide.eclipse.adt.preferences.PreferenceInitializer"/>
+ </extension>
+ <extension
+ id="com.android.ide.eclipse.adt.adtProblem"
+ name="Android ADT Problem"
+ point="org.eclipse.core.resources.markers">
+ <super type="org.eclipse.core.resources.problemmarker"/>
+ <persistent value="true"/>
+ </extension>
+ <extension
+ id="com.android.ide.eclipse.adt.targetProblem"
+ name="Android Target Problem"
+ point="org.eclipse.core.resources.markers">
+ <super type="org.eclipse.core.resources.problemmarker"/>
+ <persistent value="false"/>
+ </extension>
+ <extension
+ point="org.eclipse.ui.perspectiveExtensions">
+ <perspectiveExtension targetID="org.eclipse.jdt.ui.JavaPerspective">
+ <newWizardShortcut id="com.android.ide.eclipse.adt.project.NewProjectWizard" />
+ <newWizardShortcut
+ id="com.android.ide.eclipse.editors.wizards.NewXmlFileWizard">
+ </newWizardShortcut>
+ </perspectiveExtension>
+ <perspectiveExtension targetID="org.eclipse.debug.ui.DebugPerspective">
+ <viewShortcut id="com.android.ide.eclipse.ddms.views.LogCatView"/>
+ <viewShortcut id="com.android.ide.eclipse.ddms.views.DeviceView"/>
+ </perspectiveExtension>
+ </extension>
+ <extension
+ point="org.eclipse.ui.ide.projectNatureImages">
+ <image
+ icon="icons/android_project.png"
+ id="com.android.ide.eclipse.adt.AndroidNature.image"
+ natureId="com.android.ide.eclipse.adt.AndroidNature">
+ </image>
+ </extension>
+ <extension
+ point="org.eclipse.jdt.core.classpathContainerInitializer">
+ <classpathContainerInitializer
+ class="com.android.ide.eclipse.adt.project.internal.AndroidClasspathContainerInitializer"
+ id="com.android.ide.eclipse.adt.project.AndroidClasspathContainerInitializer">
+ </classpathContainerInitializer>
+ <classpathContainerInitializer
+ class="com.android.ide.eclipse.adt.project.internal.AndroidClasspathContainerInitializer"
+ id="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK">
+ </classpathContainerInitializer>
+ </extension>
+ <extension
+ point="org.eclipse.ui.exportWizards">
+ <category
+ id="com.android.ide.eclipse.wizards.category"
+ name="Android">
+ </category>
+ <wizard
+ category="com.android.ide.eclipse.wizards.category"
+ class="com.android.ide.eclipse.adt.project.export.ExportWizard"
+ icon="icons/android.png"
+ id="com.android.ide.eclipse.adt.project.ExportWizard"
+ name="Export Android Application">
+ </wizard>
+ </extension>
+ <extension
+ point="org.eclipse.ui.commands">
+ <command
+ name="Debug Android Application"
+ description="Debug Android Application"
+ categoryId="org.eclipse.debug.ui.category.run"
+ id="com.android.ide.eclipse.adt.debug.launching.LaunchShortcut.debug">
+ </command>
+ <command
+ name="Run Android Application"
+ description="Run Android Application"
+ categoryId="org.eclipse.debug.ui.category.run"
+ id="com.android.ide.eclipse.adt.debug.launching.LaunchShortcut.run">
+ </command>
+ <keyBinding
+ keySequence="M3+M2+A D"
+ contextId="org.eclipse.ui.globalScope"
+ commandId="com.android.ide.eclipse.adt.debug.launching.LaunchShortcut.debug"
+ keyConfigurationId="org.eclipse.ui.defaultAcceleratorConfiguration">
+ </keyBinding>
+ <keyBinding
+ keySequence="M3+M2+A R"
+ contextId="org.eclipse.ui.globalScope"
+ commandId="com.android.ide.eclipse.adt.debug.launching.LaunchShortcut.run"
+ keyConfigurationId="org.eclipse.ui.defaultAcceleratorConfiguration">
+ </keyBinding>
+ </extension>
+ <extension
+ point="org.eclipse.ui.decorators">
+ <decorator
+ adaptable="true"
+ class="com.android.ide.eclipse.adt.project.FolderDecorator"
+ id="com.android.ide.eclipse.adt.project.FolderDecorator"
+ label="Android Decorator"
+ lightweight="true"
+ location="TOP_RIGHT"
+ objectClass="org.eclipse.core.resources.IFolder"
+ state="true">
+ </decorator>
+ </extension>
+ <extension
+ point="org.eclipse.ui.editors">
+ <editor
+ class="com.android.ide.eclipse.editors.manifest.ManifestEditor"
+ default="true"
+ filenames="AndroidManifest.xml"
+ icon="icons/android.png"
+ id="com.android.ide.eclipse.editors.manifest.ManifestEditor"
+ name="Android Manifest Editor">
+ </editor>
+ <editor
+ class="com.android.ide.eclipse.editors.resources.ResourcesEditor"
+ default="false"
+ extensions="xml"
+ icon="icons/android.png"
+ id="com.android.ide.eclipse.editors.resources.ResourcesEditor"
+ name="Android Resource Editor">
+ </editor>
+ <editor
+ class="com.android.ide.eclipse.editors.layout.LayoutEditor"
+ default="false"
+ extensions="xml"
+ icon="icons/android.png"
+ id="com.android.ide.eclipse.editors.layout.LayoutEditor"
+ matchingStrategy="com.android.ide.eclipse.editors.layout.MatchingStrategy"
+ name="Android Layout Editor">
+ </editor>
+ <editor
+ class="com.android.ide.eclipse.editors.menu.MenuEditor"
+ default="false"
+ extensions="xml"
+ icon="icons/android.png"
+ id="com.android.ide.eclipse.editors.menu.MenuEditor"
+ name="Android Menu Editor">
+ </editor>
+ <editor
+ class="com.android.ide.eclipse.editors.xml.XmlEditor"
+ default="false"
+ extensions="xml"
+ icon="icons/android.png"
+ id="com.android.ide.eclipse.editors.xml.XmlEditor"
+ name="Android Xml Resources Editor">
+ </editor>
+ </extension>
+ <extension
+ point="org.eclipse.ui.views">
+ <view
+ allowMultiple="false"
+ category="com.android.ide.eclipse.ddms.views.category"
+ class="com.android.ide.eclipse.editors.resources.explorer.ResourceExplorerView"
+ icon="icons/android.png"
+ id="com.android.ide.eclipse.editors.resources.explorer.ResourceExplorerView"
+ name="Resource Explorer">
+ </view>
+ </extension>
+ <extension
+ point="org.eclipse.wst.sse.ui.editorConfiguration">
+ <sourceViewerConfiguration
+ class="com.android.ide.eclipse.editors.manifest.ManifestSourceViewerConfig"
+ target="com.android.ide.eclipse.editors.manifest.ManifestEditor">
+ </sourceViewerConfiguration>
+ <sourceViewerConfiguration
+ class="com.android.ide.eclipse.editors.resources.ResourcesSourceViewerConfig"
+ target="com.android.ide.eclipse.editors.resources.ResourcesEditor">
+ </sourceViewerConfiguration>
+ <sourceViewerConfiguration
+ class="com.android.ide.eclipse.editors.layout.LayoutSourceViewerConfig"
+ target="com.android.ide.eclipse.editors.layout.LayoutEditor">
+ </sourceViewerConfiguration>
+ <sourceViewerConfiguration
+ class="com.android.ide.eclipse.editors.menu.MenuSourceViewerConfig"
+ target="com.android.ide.eclipse.editors.menu.MenuEditor">
+ </sourceViewerConfiguration>
+ <sourceViewerConfiguration
+ class="com.android.ide.eclipse.editors.xml.XmlSourceViewerConfig"
+ target="com.android.ide.eclipse.editors.xml.XmlEditor">
+ </sourceViewerConfiguration>
+ </extension>
+ <extension
+ point="org.eclipse.ui.propertyPages">
+ <page
+ adaptable="true"
+ class="com.android.ide.eclipse.adt.project.properties.AndroidPropertyPage"
+ id="com.android.ide.eclipse.adt.project.properties.AndroidPropertyPage"
+ name="Android"
+ nameFilter="*"
+ objectClass="org.eclipse.core.resources.IProject">
+ <enabledWhen>
+ <test property="org.eclipse.jdt.launching.hasProjectNature"
+ args="com.android.ide.eclipse.adt.AndroidNature"/>
+ </enabledWhen>
+ </page>
+ </extension>
+ <extension
+ point="org.eclipse.ui.actionSets">
+ <actionSet
+ description="Android Wizards"
+ id="adt.actionSet1"
+ label="Android Wizards"
+ visible="true">
+ <action
+ class="com.android.ide.eclipse.adt.wizards.actions.NewProjectAction"
+ icon="icons/new_adt_project.png"
+ id="com.android.ide.eclipse.adt.wizards.actions.NewProjectAction"
+ label="New Android Project"
+ style="push"
+ toolbarPath="android_project"
+ tooltip="Opens a wizard to help create a new Android project">
+ </action>
+ <action
+ class="com.android.ide.eclipse.adt.wizards.actions.NewXmlFileAction"
+ icon="icons/new_xml.png"
+ id="com.android.ide.eclipse.adt.wizards.actions.NewXmlFileAction"
+ label="New Android XML File"
+ style="push"
+ toolbarPath="android_project"
+ tooltip="Opens a wizard to help create a new Android XML file">
+ </action>
+ </actionSet>
+ </extension>
+</plugin>
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtConstants.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtConstants.java
new file mode 100644
index 0000000..7304e5e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtConstants.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt;
+
+import com.android.ide.eclipse.adt.project.internal.AndroidClasspathContainerInitializer;
+
+
+/**
+ * Constant definition class.<br>
+ * <br>
+ * Most constants have a prefix defining the content.
+ * <ul>
+ * <li><code>WS_</code> Workspace path constant. Those are absolute paths,
+ * from the project root.</li>
+ * <li><code>OS_</code> OS path constant. These paths are different depending on the platform.</li>
+ * <li><code>FN_</code> File name constant.</li>
+ * <li><code>FD_</code> Folder name constant.</li>
+ * <li><code>MARKER_</code> Resource Marker Ids constant.</li>
+ * <li><code>EXT_</code> File extension constant. This does NOT include a dot.</li>
+ * <li><code>DOT_</code> File extension constant. This start with a dot.</li>
+ * <li><code>RE_</code> Regexp constant.</li>
+ * <li><code>BUILD_</code> Build verbosity level constant. To use with
+ * <code>AdtPlugin.printBuildToConsole()</code></li>
+ * </ul>
+ */
+public class AdtConstants {
+ /** Generic marker for ADT errors. */
+ public final static String MARKER_ADT = AdtPlugin.PLUGIN_ID + ".adtProblem"; //$NON-NLS-1$
+
+ /** Marker for Android Target errors.
+ * This is not cleared on each like other markers. Instead, it's cleared
+ * when an {@link AndroidClasspathContainerInitializer} has succeeded in creating an
+ * AndroidClasspathContainer */
+ public final static String MARKER_TARGET = AdtPlugin.PLUGIN_ID + ".targetProblem"; //$NON-NLS-1$
+
+ /** Build verbosity "Always". Those messages are always displayed. */
+ public final static int BUILD_ALWAYS = 0;
+
+ /** Build verbosity level "Normal" */
+ public final static int BUILD_NORMAL = 1;
+
+ /** Build verbosity level "Verbose". Those messages are only displayed in verbose mode */
+ public final static int BUILD_VERBOSE = 2;
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java
new file mode 100644
index 0000000..61be3e5
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/AdtPlugin.java
@@ -0,0 +1,1372 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt;
+
+import com.android.ddmuilib.StackTracePanel;
+import com.android.ddmuilib.StackTracePanel.ISourceRevealer;
+import com.android.ddmuilib.console.DdmConsole;
+import com.android.ddmuilib.console.IDdmConsole;
+import com.android.ide.eclipse.adt.debug.launching.AndroidLaunchController;
+import com.android.ide.eclipse.adt.preferences.BuildPreferencePage;
+import com.android.ide.eclipse.adt.project.ProjectHelper;
+import com.android.ide.eclipse.adt.project.export.ExportWizard;
+import com.android.ide.eclipse.adt.project.internal.AndroidClasspathContainerInitializer;
+import com.android.ide.eclipse.adt.sdk.AndroidTargetParser;
+import com.android.ide.eclipse.adt.sdk.LoadStatus;
+import com.android.ide.eclipse.adt.sdk.Sdk;
+import com.android.ide.eclipse.adt.sdk.Sdk.ITargetChangeListener;
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.common.EclipseUiHelper;
+import com.android.ide.eclipse.common.SdkStatsHelper;
+import com.android.ide.eclipse.common.StreamHelper;
+import com.android.ide.eclipse.common.project.BaseProjectHelper;
+import com.android.ide.eclipse.common.project.ExportHelper;
+import com.android.ide.eclipse.common.project.ExportHelper.IExportCallback;
+import com.android.ide.eclipse.ddms.DdmsPlugin;
+import com.android.ide.eclipse.ddms.ImageLoader;
+import com.android.ide.eclipse.editors.IconFactory;
+import com.android.ide.eclipse.editors.layout.LayoutEditor;
+import com.android.ide.eclipse.editors.menu.MenuEditor;
+import com.android.ide.eclipse.editors.resources.ResourcesEditor;
+import com.android.ide.eclipse.editors.resources.manager.ProjectResources;
+import com.android.ide.eclipse.editors.resources.manager.ResourceFolder;
+import com.android.ide.eclipse.editors.resources.manager.ResourceFolderType;
+import com.android.ide.eclipse.editors.resources.manager.ResourceManager;
+import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor;
+import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IFileListener;
+import com.android.ide.eclipse.editors.xml.XmlEditor;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.SdkConstants;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IMarkerDelta;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResourceDelta;
+import org.eclipse.core.resources.IWorkspace;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Preferences;
+import org.eclipse.core.runtime.QualifiedName;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.SubMonitor;
+import org.eclipse.core.runtime.Preferences.IPropertyChangeListener;
+import org.eclipse.core.runtime.Preferences.PropertyChangeEvent;
+import org.eclipse.core.runtime.jobs.IJobChangeEvent;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.core.runtime.jobs.JobChangeAdapter;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.jface.wizard.WizardDialog;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.IEditorDescriptor;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.console.ConsolePlugin;
+import org.eclipse.ui.console.IConsole;
+import org.eclipse.ui.console.IConsoleConstants;
+import org.eclipse.ui.console.MessageConsole;
+import org.eclipse.ui.console.MessageConsoleStream;
+import org.eclipse.ui.ide.IDE;
+import org.eclipse.ui.part.FileEditorInput;
+import org.eclipse.ui.plugin.AbstractUIPlugin;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.Version;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * The activator class controls the plug-in life cycle
+ */
+public class AdtPlugin extends AbstractUIPlugin {
+ /** The plug-in ID */
+ public static final String PLUGIN_ID = "com.android.ide.eclipse.adt"; //$NON-NLS-1$
+
+ public final static String PREFS_SDK_DIR = PLUGIN_ID + ".sdk"; //$NON-NLS-1$
+
+ public final static String PREFS_RES_AUTO_REFRESH = PLUGIN_ID + ".resAutoRefresh"; //$NON-NLS-1$
+
+ public final static String PREFS_BUILD_VERBOSITY = PLUGIN_ID + ".buildVerbosity"; //$NON-NLS-1$
+
+ public final static String PREFS_DEFAULT_DEBUG_KEYSTORE = PLUGIN_ID + ".defaultDebugKeyStore"; //$NON-NLS-1$
+
+ public final static String PREFS_CUSTOM_DEBUG_KEYSTORE = PLUGIN_ID + ".customDebugKeyStore"; //$NON-NLS-1$
+
+ public final static String PREFS_HOME_PACKAGE = PLUGIN_ID + ".homePackage"; //$NON-NLS-1$
+
+ public final static String PREFS_EMU_OPTIONS = PLUGIN_ID + ".emuOptions"; //$NON-NLS-1$
+
+ /** singleton instance */
+ private static AdtPlugin sPlugin;
+
+ private static Image sAndroidLogo;
+ private static ImageDescriptor sAndroidLogoDesc;
+
+ /** default store, provided by eclipse */
+ private IPreferenceStore mStore;
+
+ /** cached location for the sdk folder */
+ private String mOsSdkLocation;
+
+ /** The global android console */
+ private MessageConsole mAndroidConsole;
+
+ /** Stream to write in the android console */
+ private MessageConsoleStream mAndroidConsoleStream;
+
+ /** Stream to write error messages to the android console */
+ private MessageConsoleStream mAndroidConsoleErrorStream;
+
+ /** Image loader object */
+ private ImageLoader mLoader;
+
+ /** Verbosity of the build */
+ private int mBuildVerbosity = AdtConstants.BUILD_NORMAL;
+
+ /** Color used in the error console */
+ private Color mRed;
+
+ /** Load status of the SDK. Any access MUST be in a synchronized(mPostLoadProjects) block */
+ private LoadStatus mSdkIsLoaded = LoadStatus.LOADING;
+ /** Project to update once the SDK is loaded.
+ * Any access MUST be in a synchronized(mPostLoadProjectsToResolve) block */
+ private final ArrayList<IJavaProject> mPostLoadProjectsToResolve =
+ new ArrayList<IJavaProject>();
+ /** Project to check validity of cache vs actual once the SDK is loaded.
+ * Any access MUST be in a synchronized(mPostLoadProjectsToResolve) block */
+ private final ArrayList<IJavaProject> mPostLoadProjectsToCheck = new ArrayList<IJavaProject>();
+
+ private ResourceMonitor mResourceMonitor;
+ private ArrayList<ITargetChangeListener> mTargetChangeListeners =
+ new ArrayList<ITargetChangeListener>();
+
+ /**
+ * Custom PrintStream for Dx output. This class overrides the method
+ * <code>println()</code> and adds the standard output tag with the
+ * date and the project name in front of every messages.
+ */
+ private static final class AndroidPrintStream extends PrintStream {
+ private IProject mProject;
+ private String mPrefix;
+
+ /**
+ * Default constructor with project and output stream.
+ * The project is used to get the project name for the output tag.
+ *
+ * @param project The Project
+ * @param prefix A prefix to be printed before the actual message. Can be null
+ * @param stream The Stream
+ */
+ public AndroidPrintStream(IProject project, String prefix, OutputStream stream) {
+ super(stream);
+ mProject = project;
+ }
+
+ @Override
+ public void println(String message) {
+ // write the date/project tag first.
+ String tag = StreamHelper.getMessageTag(mProject != null ? mProject.getName() : null);
+
+ print(tag);
+ if (mPrefix != null) {
+ print(mPrefix);
+ }
+
+ // then write the regular message
+ super.println(message);
+ }
+ }
+
+ /**
+ * An error handler for checkSdkLocationAndId() that will handle the generated error
+ * or warning message. Each method must return a boolean that will in turn be returned by
+ * checkSdkLocationAndId.
+ */
+ public static abstract class CheckSdkErrorHandler {
+ /** Handle an error message during sdk location check. Returns whatever
+ * checkSdkLocationAndId() should returns.
+ */
+ public abstract boolean handleError(String message);
+
+ /** Handle a warning message during sdk location check. Returns whatever
+ * checkSdkLocationAndId() should returns.
+ */
+ public abstract boolean handleWarning(String message);
+ }
+
+ /**
+ * The constructor
+ */
+ public AdtPlugin() {
+ sPlugin = this;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.eclipse.ui.plugin.AbstractUIPlugin#start(org.osgi.framework.BundleContext)
+ */
+ @Override
+ public void start(BundleContext context) throws Exception {
+ super.start(context);
+
+ Display display = getDisplay();
+
+ // set the default android console.
+ mAndroidConsole = new MessageConsole("Android", null); //$NON-NLS-1$
+ ConsolePlugin.getDefault().getConsoleManager().addConsoles(
+ new IConsole[] { mAndroidConsole });
+
+ // get the stream to write in the android console.
+ mAndroidConsoleStream = mAndroidConsole.newMessageStream();
+ mAndroidConsoleErrorStream = mAndroidConsole.newMessageStream();
+ mRed = new Color(display, 0xFF, 0x00, 0x00);
+
+ // because this can be run, in some cases, by a non ui thread, and beccause
+ // changing the console properties update the ui, we need to make this change
+ // in the ui thread.
+ display.asyncExec(new Runnable() {
+ public void run() {
+ mAndroidConsoleErrorStream.setColor(mRed);
+ }
+ });
+
+ // set up the ddms console to use this objects
+ DdmConsole.setConsole(new IDdmConsole() {
+ public void printErrorToConsole(String message) {
+ AdtPlugin.printErrorToConsole((String)null, message);
+ }
+ public void printErrorToConsole(String[] messages) {
+ AdtPlugin.printErrorToConsole((String)null, (Object[])messages);
+ }
+ public void printToConsole(String message) {
+ AdtPlugin.printToConsole((String)null, message);
+ }
+ public void printToConsole(String[] messages) {
+ AdtPlugin.printToConsole((String)null, (Object[])messages);
+ }
+ });
+
+ // get the eclipse store
+ mStore = getPreferenceStore();
+
+ // set the listener for the preference change
+ Preferences prefs = getPluginPreferences();
+ prefs.addPropertyChangeListener(new IPropertyChangeListener() {
+ public void propertyChange(PropertyChangeEvent event) {
+ // get the name of the property that changed.
+ String property = event.getProperty();
+
+ // if the SDK changed, we update the cached version
+ if (PREFS_SDK_DIR.equals(property)) {
+
+ // get the new one from the preferences
+ mOsSdkLocation = (String)event.getNewValue();
+
+ // make sure it ends with a separator
+ if (mOsSdkLocation.endsWith(File.separator) == false) {
+ mOsSdkLocation = mOsSdkLocation + File.separator;
+ }
+
+ // finally restart adb, in case it's a different version
+ DdmsPlugin.setAdb(getOsAbsoluteAdb(), true /* startAdb */);
+
+ // get the SDK location and build id.
+ if (checkSdkLocationAndId()) {
+ // if sdk if valid, reparse it
+
+ // add all the opened Android projects to the list of projects to be updated
+ // after the SDK is reloaded
+ synchronized (getSdkLockObject()) {
+ // get the project to refresh.
+ IJavaProject[] androidProjects = BaseProjectHelper.getAndroidProjects();
+ mPostLoadProjectsToResolve.addAll(Arrays.asList(androidProjects));
+ }
+
+ // parse the SDK resources at the new location
+ parseSdkContent();
+ }
+ } else if (PREFS_BUILD_VERBOSITY.equals(property)) {
+ mBuildVerbosity = BuildPreferencePage.getBuildLevel(
+ mStore.getString(PREFS_BUILD_VERBOSITY));
+ }
+ }
+ });
+
+ mOsSdkLocation = mStore.getString(PREFS_SDK_DIR);
+
+ // make sure it ends with a separator. Normally this is done when the preference
+ // is set. But to make sure older version still work, we fix it here as well.
+ if (mOsSdkLocation.length() > 0 && mOsSdkLocation.endsWith(File.separator) == false) {
+ mOsSdkLocation = mOsSdkLocation + File.separator;
+ }
+
+ // check the location of SDK
+ final boolean isSdkLocationValid = checkSdkLocationAndId();
+
+ mBuildVerbosity = BuildPreferencePage.getBuildLevel(
+ mStore.getString(PREFS_BUILD_VERBOSITY));
+
+ // create the loader that's able to load the images
+ mLoader = new ImageLoader(this);
+
+ // start the DdmsPlugin by setting the adb location, only if it is set already.
+ if (mOsSdkLocation.length() > 0) {
+ DdmsPlugin.setAdb(getOsAbsoluteAdb(), true);
+ }
+
+ // and give it the debug launcher for android projects
+ DdmsPlugin.setRunningAppDebugLauncher(new DdmsPlugin.IDebugLauncher() {
+ public boolean debug(String appName, int port) {
+ // search for an android project matching the process name
+ IProject project = ProjectHelper.findAndroidProjectByAppName(appName);
+ if (project != null) {
+ AndroidLaunchController.debugRunningApp(project, port);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ });
+
+ StackTracePanel.setSourceRevealer(new ISourceRevealer() {
+ public void reveal(String applicationName, String className, int line) {
+ IProject project = ProjectHelper.findAndroidProjectByAppName(applicationName);
+ if (project != null) {
+ BaseProjectHelper.revealSource(project, className, line);
+ }
+ }
+ });
+
+ // setup export callback for editors
+ ExportHelper.setCallback(new IExportCallback() {
+ public void startExportWizard(IProject project) {
+ StructuredSelection selection = new StructuredSelection(project);
+
+ ExportWizard wizard = new ExportWizard();
+ wizard.init(PlatformUI.getWorkbench(), selection);
+ WizardDialog dialog = new WizardDialog(getDisplay().getActiveShell(),
+ wizard);
+ dialog.open();
+ }
+ });
+
+ // initialize editors
+ startEditors();
+
+ // Ping the usage server and parse the SDK content.
+ // This is deferred in separate jobs to avoid blocking the bundle start.
+ // We also serialize them to avoid too many parallel jobs when Eclipse starts.
+ Job pingJob = createPingUsageServerJob();
+ pingJob.addJobChangeListener(new JobChangeAdapter() {
+ @Override
+ public void done(IJobChangeEvent event) {
+ super.done(event);
+
+ // Once the ping job is finished, start the SDK parser
+ if (isSdkLocationValid) {
+ // parse the SDK resources.
+ parseSdkContent();
+ }
+ }
+ });
+ // build jobs are run after other interactive jobs
+ pingJob.setPriority(Job.BUILD);
+ // Wait 2 seconds before starting the ping job. This leaves some time to the
+ // other bundles to initialize.
+ pingJob.schedule(2000 /*milliseconds*/);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.eclipse.ui.plugin.AbstractUIPlugin#stop(org.osgi.framework.BundleContext)
+ */
+ @Override
+ public void stop(BundleContext context) throws Exception {
+ super.stop(context);
+
+ stopEditors();
+
+ mRed.dispose();
+ synchronized (AdtPlugin.class) {
+ sPlugin = null;
+ }
+ }
+
+ /** Return the image loader for the plugin */
+ public static synchronized ImageLoader getImageLoader() {
+ if (sPlugin != null) {
+ return sPlugin.mLoader;
+ }
+ return null;
+ }
+
+ /**
+ * Returns the shared instance
+ *
+ * @return the shared instance
+ */
+ public static synchronized AdtPlugin getDefault() {
+ return sPlugin;
+ }
+
+ public static Display getDisplay() {
+ IWorkbench bench = null;
+ synchronized (AdtPlugin.class) {
+ bench = sPlugin.getWorkbench();
+ }
+
+ if (bench != null) {
+ return bench.getDisplay();
+ }
+ return null;
+ }
+
+ /** Returns the adb path relative to the sdk folder */
+ public static String getOsRelativeAdb() {
+ return SdkConstants.OS_SDK_TOOLS_FOLDER + AndroidConstants.FN_ADB;
+ }
+
+ /** Returns the emulator path relative to the sdk folder */
+ public static String getOsRelativeEmulator() {
+ return SdkConstants.OS_SDK_TOOLS_FOLDER + AndroidConstants.FN_EMULATOR;
+ }
+
+ /** Returns the absolute adb path */
+ public static String getOsAbsoluteAdb() {
+ return getOsSdkFolder() + getOsRelativeAdb();
+ }
+
+ /** Returns the absolute traceview path */
+ public static String getOsAbsoluteTraceview() {
+ return getOsSdkFolder() + SdkConstants.OS_SDK_TOOLS_FOLDER +
+ AndroidConstants.FN_TRACEVIEW;
+ }
+
+ /** Returns the absolute emulator path */
+ public static String getOsAbsoluteEmulator() {
+ return getOsSdkFolder() + getOsRelativeEmulator();
+ }
+
+ /**
+ * Returns a Url file path to the javaDoc folder.
+ */
+ public static String getUrlDoc() {
+ return ProjectHelper.getJavaDocPath(
+ getOsSdkFolder() + AndroidConstants.WS_JAVADOC_FOLDER_LEAF);
+ }
+
+ /**
+ * Returns the SDK folder.
+ * Guaranteed to be terminated by a platform-specific path separator.
+ */
+ public static synchronized String getOsSdkFolder() {
+ if (sPlugin == null) {
+ return null;
+ }
+
+ if (sPlugin.mOsSdkLocation == null) {
+ sPlugin.mOsSdkLocation = sPlugin.mStore.getString(PREFS_SDK_DIR);
+ }
+ return sPlugin.mOsSdkLocation;
+ }
+
+ public static String getOsSdkToolsFolder() {
+ return getOsSdkFolder() + SdkConstants.OS_SDK_TOOLS_FOLDER;
+ }
+
+ public static synchronized boolean getAutoResRefresh() {
+ if (sPlugin == null) {
+ return false;
+ }
+ return sPlugin.mStore.getBoolean(PREFS_RES_AUTO_REFRESH);
+ }
+
+ public static synchronized int getBuildVerbosity() {
+ if (sPlugin != null) {
+ return sPlugin.mBuildVerbosity;
+ }
+
+ return 0;
+ }
+
+ /**
+ * Returns an image descriptor for the image file at the given
+ * plug-in relative path
+ *
+ * @param path the path
+ * @return the image descriptor
+ */
+ public static ImageDescriptor getImageDescriptor(String path) {
+ return imageDescriptorFromPlugin(PLUGIN_ID, path);
+ }
+
+ /**
+ * Reads and returns the content of a text file embedded in the plugin jar
+ * file.
+ * @param filepath the file path to the text file
+ * @return null if the file could not be read
+ */
+ public static String readEmbeddedTextFile(String filepath) {
+ Bundle bundle = null;
+ synchronized (AdtPlugin.class) {
+ if (sPlugin != null) {
+ bundle = sPlugin.getBundle();
+ } else {
+ return null;
+ }
+ }
+
+ // attempt to get a file to one of the template.
+ try {
+ URL url = bundle.getEntry(AndroidConstants.WS_SEP + filepath);
+ if (url != null) {
+ BufferedReader reader = new BufferedReader(
+ new InputStreamReader(url.openStream()));
+
+ String line;
+ StringBuilder total = new StringBuilder(reader.readLine());
+ while ((line = reader.readLine()) != null) {
+ total.append('\n');
+ total.append(line);
+ }
+
+ return total.toString();
+ }
+ } catch (MalformedURLException e) {
+ // we'll just return null.
+ } catch (IOException e) {
+ // we'll just return null.
+ }
+
+ return null;
+ }
+
+ /**
+ * Reads and returns the content of a binary file embedded in the plugin jar
+ * file.
+ * @param filepath the file path to the text file
+ * @return null if the file could not be read
+ */
+ public static byte[] readEmbeddedFile(String filepath) {
+ Bundle bundle = null;
+ synchronized (AdtPlugin.class) {
+ if (sPlugin != null) {
+ bundle = sPlugin.getBundle();
+ } else {
+ return null;
+ }
+ }
+
+ // attempt to get a file to one of the template.
+ try {
+ URL url = bundle.getEntry(AndroidConstants.WS_SEP + filepath);
+ if (url != null) {
+ // create a buffered reader to facilitate reading.
+ BufferedInputStream stream = new BufferedInputStream(
+ url.openStream());
+
+ // get the size to read.
+ int avail = stream.available();
+
+ // create the buffer and reads it.
+ byte[] buffer = new byte[avail];
+ stream.read(buffer);
+
+ // and return.
+ return buffer;
+ }
+ } catch (MalformedURLException e) {
+ // we'll just return null.
+ } catch (IOException e) {
+ // we'll just return null;.
+ }
+
+ return null;
+ }
+
+ /**
+ * Displays an error dialog box. This dialog box is ran asynchronously in the ui thread,
+ * therefore this method can be called from any thread.
+ * @param title The title of the dialog box
+ * @param message The error message
+ */
+ public final static void displayError(final String title, final String message) {
+ // get the current Display
+ final Display display = getDisplay();
+
+ // dialog box only run in ui thread..
+ display.asyncExec(new Runnable() {
+ public void run() {
+ Shell shell = display.getActiveShell();
+ MessageDialog.openError(shell, title, message);
+ }
+ });
+ }
+
+ /**
+ * Displays a warning dialog box. This dialog box is ran asynchronously in the ui thread,
+ * therefore this method can be called from any thread.
+ * @param title The title of the dialog box
+ * @param message The warning message
+ */
+ public final static void displayWarning(final String title, final String message) {
+ // get the current Display
+ final Display display = getDisplay();
+
+ // dialog box only run in ui thread..
+ display.asyncExec(new Runnable() {
+ public void run() {
+ Shell shell = display.getActiveShell();
+ MessageDialog.openWarning(shell, title, message);
+ }
+ });
+ }
+
+ /**
+ * Display a yes/no question dialog box. This dialog is opened synchronously in the ui thread,
+ * therefore this message can be called from any thread.
+ * @param title The title of the dialog box
+ * @param message The error message
+ * @return true if OK was clicked.
+ */
+ public final static boolean displayPrompt(final String title, final String message) {
+ // get the current Display and Shell
+ final Display display = getDisplay();
+
+ // we need to ask the user what he wants to do.
+ final boolean[] result = new boolean[1];
+ display.syncExec(new Runnable() {
+ public void run() {
+ Shell shell = display.getActiveShell();
+ result[0] = MessageDialog.openQuestion(shell, title, message);
+ }
+ });
+ return result[0];
+ }
+
+ /**
+ * Logs a message to the default Eclipse log.
+ *
+ * @param severity The severity code. Valid values are: {@link IStatus#OK},
+ * {@link IStatus#ERROR}, {@link IStatus#INFO}, {@link IStatus#WARNING} or
+ * {@link IStatus#CANCEL}.
+ * @param format The format string, like for {@link String#format(String, Object...)}.
+ * @param args The arguments for the format string, like for
+ * {@link String#format(String, Object...)}.
+ */
+ public static void log(int severity, String format, Object ... args) {
+ String message = String.format(format, args);
+ Status status = new Status(severity, PLUGIN_ID, message);
+ getDefault().getLog().log(status);
+ }
+
+ /**
+ * Logs an exception to the default Eclipse log.
+ * <p/>
+ * The status severity is always set to ERROR.
+ *
+ * @param exception the exception to log.
+ * @param format The format string, like for {@link String#format(String, Object...)}.
+ * @param args The arguments for the format string, like for
+ * {@link String#format(String, Object...)}.
+ */
+ public static void log(Throwable exception, String format, Object ... args) {
+ String message = String.format(format, args);
+ Status status = new Status(IStatus.ERROR, PLUGIN_ID, message, exception);
+ getDefault().getLog().log(status);
+ }
+
+ /**
+ * This is a mix between log(Throwable) and printErrorToConsole.
+ * <p/>
+ * This logs the exception with an ERROR severity and the given printf-like format message.
+ * The same message is then printed on the Android error console with the associated tag.
+ *
+ * @param exception the exception to log.
+ * @param format The format string, like for {@link String#format(String, Object...)}.
+ * @param args The arguments for the format string, like for
+ * {@link String#format(String, Object...)}.
+ */
+ public static synchronized void logAndPrintError(Throwable exception, String tag,
+ String format, Object ... args) {
+ if (sPlugin != null) {
+ String message = String.format(format, args);
+ Status status = new Status(IStatus.ERROR, PLUGIN_ID, message, exception);
+ getDefault().getLog().log(status);
+ StreamHelper.printToStream(sPlugin.mAndroidConsoleErrorStream, tag, message);
+ showAndroidConsole();
+ }
+ }
+
+ /**
+ * Prints one or more error message to the android console.
+ * @param tag A tag to be associated with the message. Can be null.
+ * @param objects the objects to print through their <code>toString</code> method.
+ */
+ public static synchronized void printErrorToConsole(String tag, Object... objects) {
+ if (sPlugin != null) {
+ StreamHelper.printToStream(sPlugin.mAndroidConsoleErrorStream, tag, objects);
+
+ showAndroidConsole();
+ }
+ }
+
+ /**
+ * Prints one or more error message to the android console.
+ * @param objects the objects to print through their <code>toString</code> method.
+ */
+ public static void printErrorToConsole(Object... objects) {
+ printErrorToConsole((String)null, objects);
+ }
+
+ /**
+ * Prints one or more error message to the android console.
+ * @param project The project to which the message is associated. Can be null.
+ * @param objects the objects to print through their <code>toString</code> method.
+ */
+ public static void printErrorToConsole(IProject project, Object... objects) {
+ String tag = project != null ? project.getName() : null;
+ printErrorToConsole(tag, objects);
+ }
+
+ /**
+ * Prints one or more build messages to the android console, filtered by Build output verbosity.
+ * @param level Verbosity level of the message.
+ * @param project The project to which the message is associated. Can be null.
+ * @param objects the objects to print through their <code>toString</code> method.
+ * @see AdtConstants#BUILD_ALWAYS
+ * @see AdtConstants#BUILD_NORMAL
+ * @see AdtConstants#BUILD_VERBOSE
+ */
+ public static synchronized void printBuildToConsole(int level, IProject project,
+ Object... objects) {
+ if (sPlugin != null) {
+ if (level <= sPlugin.mBuildVerbosity) {
+ String tag = project != null ? project.getName() : null;
+ StreamHelper.printToStream(sPlugin.mAndroidConsoleStream, tag, objects);
+ }
+ }
+ }
+
+ /**
+ * Prints one or more message to the android console.
+ * @param tag The tag to be associated with the message. Can be null.
+ * @param objects the objects to print through their <code>toString</code> method.
+ */
+ public static synchronized void printToConsole(String tag, Object... objects) {
+ if (sPlugin != null) {
+ StreamHelper.printToStream(sPlugin.mAndroidConsoleStream, tag, objects);
+ }
+ }
+
+ /**
+ * Prints one or more message to the android console.
+ * @param project The project to which the message is associated. Can be null.
+ * @param objects the objects to print through their <code>toString</code> method.
+ */
+ public static void printToConsole(IProject project, Object... objects) {
+ String tag = project != null ? project.getName() : null;
+ printToConsole(tag, objects);
+ }
+
+ /** Force the display of the android console */
+ public static void showAndroidConsole() {
+ // first make sure the console is in the workbench
+ EclipseUiHelper.showView(IConsoleConstants.ID_CONSOLE_VIEW, true);
+
+ // now make sure it's not docked.
+ ConsolePlugin.getDefault().getConsoleManager().showConsoleView(
+ AdtPlugin.getDefault().getAndroidConsole());
+ }
+
+ /**
+ * Returns an standard PrintStream object for a specific project.<br>
+ * This PrintStream will add a date/project at the beginning of every
+ * <code>println()</code> output.
+ *
+ * @param project The project object
+ * @param prefix The prefix to be added to the message. Can be null.
+ * @return a new PrintStream
+ */
+ public static synchronized PrintStream getOutPrintStream(IProject project, String prefix) {
+ if (sPlugin != null) {
+ return new AndroidPrintStream(project, prefix, sPlugin.mAndroidConsoleStream);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns an error PrintStream object for a specific project.<br>
+ * This PrintStream will add a date/project at the beginning of every
+ * <code>println()</code> output.
+ *
+ * @param project The project object
+ * @param prefix The prefix to be added to the message. Can be null.
+ * @return a new PrintStream
+ */
+ public static synchronized PrintStream getErrPrintStream(IProject project, String prefix) {
+ if (sPlugin != null) {
+ return new AndroidPrintStream(project, prefix, sPlugin.mAndroidConsoleErrorStream);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns whether the Sdk has been loaded.
+ */
+ public final LoadStatus getSdkLoadStatus() {
+ synchronized (getSdkLockObject()) {
+ return mSdkIsLoaded;
+ }
+ }
+
+ /**
+ * Returns the lock object for SDK loading. If you wish to do things while the SDK is loading,
+ * you must synchronize on this object.
+ */
+ public final Object getSdkLockObject() {
+ return mPostLoadProjectsToResolve;
+ }
+
+ /**
+ * Sets the given {@link IJavaProject} to have its target resolved again once the SDK finishes
+ * to load.
+ */
+ public final void setProjectToResolve(IJavaProject javaProject) {
+ synchronized (getSdkLockObject()) {
+ mPostLoadProjectsToResolve.add(javaProject);
+ }
+ }
+
+ /**
+ * Sets the given {@link IJavaProject} to have its target checked for consistency
+ * once the SDK finishes to load. This is used if the target is resolved using cached
+ * information while the SDK is loading.
+ */
+ public final void setProjectToCheck(IJavaProject javaProject) {
+ // only lock on
+ synchronized (getSdkLockObject()) {
+ mPostLoadProjectsToCheck.add(javaProject);
+ }
+ }
+
+ /**
+ * Checks the location of the SDK is valid and if it is, grab the SDK API version
+ * from the SDK.
+ * @return false if the location is not correct.
+ */
+ private boolean checkSdkLocationAndId() {
+ if (mOsSdkLocation == null || mOsSdkLocation.length() == 0) {
+ displayError(Messages.Dialog_Title_SDK_Location, Messages.SDK_Not_Setup);
+ return false;
+ }
+
+ return checkSdkLocationAndId(mOsSdkLocation, new CheckSdkErrorHandler() {
+ @Override
+ public boolean handleError(String message) {
+ AdtPlugin.displayError(Messages.Dialog_Title_SDK_Location,
+ String.format(Messages.Error_Check_Prefs, message));
+ return false;
+ }
+
+ @Override
+ public boolean handleWarning(String message) {
+ AdtPlugin.displayWarning(Messages.Dialog_Title_SDK_Location, message);
+ return true;
+ }
+ });
+ }
+
+ /**
+ * Internal helper to perform the actual sdk location and id check.
+ *
+ * @param osSdkLocation The sdk directory, an OS path.
+ * @param errorHandler An checkSdkErrorHandler that can display a warning or an error.
+ * @return False if there was an error or the result from the errorHandler invocation.
+ */
+ public boolean checkSdkLocationAndId(String osSdkLocation, CheckSdkErrorHandler errorHandler) {
+ if (osSdkLocation.endsWith(File.separator) == false) {
+ osSdkLocation = osSdkLocation + File.separator;
+ }
+
+ File osSdkFolder = new File(osSdkLocation);
+ if (osSdkFolder.isDirectory() == false) {
+ return errorHandler.handleError(
+ String.format(Messages.Could_Not_Find_Folder, osSdkLocation));
+ }
+
+ String osTools = osSdkLocation + SdkConstants.OS_SDK_TOOLS_FOLDER;
+ File toolsFolder = new File(osTools);
+ if (toolsFolder.isDirectory() == false) {
+ return errorHandler.handleError(
+ String.format(Messages.Could_Not_Find_Folder_In_SDK,
+ SdkConstants.FD_TOOLS, osSdkLocation));
+ }
+
+ // check the path to various tools we use
+ String[] filesToCheck = new String[] {
+ osSdkLocation + getOsRelativeAdb(),
+ osSdkLocation + getOsRelativeEmulator()
+ };
+ for (String file : filesToCheck) {
+ if (checkFile(file) == false) {
+ return errorHandler.handleError(String.format(Messages.Could_Not_Find, file));
+ }
+ }
+
+ // check the SDK build id/version and the plugin version.
+ return VersionCheck.checkVersion(osSdkLocation, errorHandler);
+ }
+
+ /**
+ * Checks if a path reference a valid existing file.
+ * @param osPath the os path to check.
+ * @return true if the file exists and is, in fact, a file.
+ */
+ private boolean checkFile(String osPath) {
+ File file = new File(osPath);
+ if (file.isFile() == false) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Creates a job than can ping the usage server.
+ */
+ private Job createPingUsageServerJob() {
+ // In order to not block the plugin loading, so we spawn another thread.
+ Job job = new Job("Android SDK Ping") { // Job name, visible in progress view
+ @Override
+ protected IStatus run(IProgressMonitor monitor) {
+ try {
+
+ // get the version of the plugin
+ String versionString = (String) getBundle().getHeaders().get(
+ Constants.BUNDLE_VERSION);
+ Version version = new Version(versionString);
+
+ SdkStatsHelper.pingUsageServer("adt", version); //$NON-NLS-1$
+
+ return Status.OK_STATUS;
+ } catch (Throwable t) {
+ log(t, "pingUsageServer failed"); //$NON-NLS-1$
+ return new Status(IStatus.ERROR, PLUGIN_ID,
+ "pingUsageServer failed", t);
+ }
+ }
+ };
+ return job;
+ }
+
+ /**
+ * Parses the SDK resources.
+ */
+ private void parseSdkContent() {
+ // Perform the update in a thread (here an Eclipse runtime job)
+ // since this should never block the caller (especially the start method)
+ Job job = new Job(Messages.AdtPlugin_Android_SDK_Content_Loader) {
+ @SuppressWarnings("unchecked")
+ @Override
+ protected IStatus run(IProgressMonitor monitor) {
+ try {
+ SubMonitor progress = SubMonitor.convert(monitor,
+ "Initialize SDK Manager", 100);
+
+ Sdk sdk = Sdk.loadSdk(mOsSdkLocation);
+
+ if (sdk != null) {
+
+ progress.setTaskName(Messages.AdtPlugin_Parsing_Resources);
+
+ int n = sdk.getTargets().length;
+ if (n > 0) {
+ int w = 60 / n;
+ for (IAndroidTarget target : sdk.getTargets()) {
+ SubMonitor p2 = progress.newChild(w);
+ IStatus status = new AndroidTargetParser(target).run(p2);
+ if (status.getCode() != IStatus.OK) {
+ synchronized (getSdkLockObject()) {
+ mSdkIsLoaded = LoadStatus.FAILED;
+ mPostLoadProjectsToResolve.clear();
+ }
+ return status;
+ }
+ }
+ }
+
+ synchronized (getSdkLockObject()) {
+ mSdkIsLoaded = LoadStatus.LOADED;
+
+ progress.setTaskName("Check Projects");
+
+ // check the projects that need checking.
+ // The method modifies the list (it removes the project that
+ // do not need to be resolved again).
+ AndroidClasspathContainerInitializer.checkProjectsCache(
+ mPostLoadProjectsToCheck);
+
+ mPostLoadProjectsToResolve.addAll(mPostLoadProjectsToCheck);
+
+ // update the project that needs recompiling.
+ if (mPostLoadProjectsToResolve.size() > 0) {
+ IJavaProject[] array = mPostLoadProjectsToResolve.toArray(
+ new IJavaProject[mPostLoadProjectsToResolve.size()]);
+ AndroidClasspathContainerInitializer.updateProjects(array);
+ mPostLoadProjectsToResolve.clear();
+ }
+
+ progress.worked(10);
+ }
+ }
+
+ // Notify resource changed listeners
+ progress.setTaskName("Refresh UI");
+ progress.setWorkRemaining(mTargetChangeListeners.size());
+
+ // Clone the list before iterating, to avoid Concurrent Modification
+ // exceptions
+ final List<ITargetChangeListener> listeners =
+ (List<ITargetChangeListener>)mTargetChangeListeners.clone();
+ final SubMonitor progress2 = progress;
+ AdtPlugin.getDisplay().syncExec(new Runnable() {
+ public void run() {
+ for (ITargetChangeListener listener : listeners) {
+ try {
+ listener.onTargetsLoaded();
+ } catch (Exception e) {
+ AdtPlugin.log(e, "Failed to update a TargetChangeListener."); //$NON-NLS-1$
+ } finally {
+ progress2.worked(1);
+ }
+ }
+ }
+ });
+ } finally {
+ if (monitor != null) {
+ monitor.done();
+ }
+ }
+
+ return Status.OK_STATUS;
+ }
+ };
+ job.setPriority(Job.BUILD); // build jobs are run after other interactive jobs
+ job.schedule();
+ }
+
+ /** Returns the global android console */
+ public MessageConsole getAndroidConsole() {
+ return mAndroidConsole;
+ }
+
+ // ----- Methods for Editors -------
+
+ public void startEditors() {
+ sAndroidLogoDesc = imageDescriptorFromPlugin(AdtPlugin.PLUGIN_ID,
+ "/icons/android.png"); //$NON-NLS-1$
+ sAndroidLogo = sAndroidLogoDesc.createImage();
+
+ // get the stream to write in the android console.
+ MessageConsole androidConsole = AdtPlugin.getDefault().getAndroidConsole();
+ mAndroidConsoleStream = androidConsole.newMessageStream();
+
+ mAndroidConsoleErrorStream = androidConsole.newMessageStream();
+ mRed = new Color(getDisplay(), 0xFF, 0x00, 0x00);
+
+ // because this can be run, in some cases, by a non ui thread, and beccause
+ // changing the console properties update the ui, we need to make this change
+ // in the ui thread.
+ getDisplay().asyncExec(new Runnable() {
+ public void run() {
+ mAndroidConsoleErrorStream.setColor(mRed);
+ }
+ });
+
+ // Add a resource listener to handle compiled resources.
+ IWorkspace ws = ResourcesPlugin.getWorkspace();
+ mResourceMonitor = ResourceMonitor.startMonitoring(ws);
+
+ if (mResourceMonitor != null) {
+ try {
+ setupDefaultEditor(mResourceMonitor);
+ ResourceManager.setup(mResourceMonitor);
+ } catch (Throwable t) {
+ log(t, "ResourceManager.setup failed"); //$NON-NLS-1$
+ }
+ }
+ }
+
+ /**
+ * The <code>AbstractUIPlugin</code> implementation of this <code>Plugin</code>
+ * method saves this plug-in's preference and dialog stores and shuts down
+ * its image registry (if they are in use). Subclasses may extend this
+ * method, but must send super <b>last</b>. A try-finally statement should
+ * be used where necessary to ensure that <code>super.shutdown()</code> is
+ * always done.
+ *
+ * @see org.eclipse.ui.plugin.AbstractUIPlugin#stop(org.osgi.framework.BundleContext)
+ */
+ public void stopEditors() {
+ sAndroidLogo.dispose();
+
+ IconFactory.getInstance().Dispose();
+
+ // Remove the resource listener that handles compiled resources.
+ IWorkspace ws = ResourcesPlugin.getWorkspace();
+ ResourceMonitor.stopMonitoring(ws);
+
+ mRed.dispose();
+ }
+
+ /**
+ * Returns an Image for the small Android logo.
+ *
+ * Callers should not dispose it.
+ */
+ public static Image getAndroidLogo() {
+ return sAndroidLogo;
+ }
+
+ /**
+ * Returns an {@link ImageDescriptor} for the small Android logo.
+ *
+ * Callers should not dispose it.
+ */
+ public static ImageDescriptor getAndroidLogoDesc() {
+ return sAndroidLogoDesc;
+ }
+
+ /**
+ * Returns the ResourceMonitor object.
+ */
+ public ResourceMonitor getResourceMonitor() {
+ return mResourceMonitor;
+ }
+
+ /**
+ * Sets up the editor to register default editors for resource files when needed.
+ *
+ * This is called by the {@link AdtPlugin} during initialization.
+ *
+ * @param monitor The main Resource Monitor object.
+ */
+ public void setupDefaultEditor(ResourceMonitor monitor) {
+ monitor.addFileListener(new IFileListener() {
+
+ private static final String UNKNOWN_EDITOR = "unknown-editor"; //$NON-NLS-1$
+
+ /* (non-Javadoc)
+ * Sent when a file changed.
+ * @param file The file that changed.
+ * @param markerDeltas The marker deltas for the file.
+ * @param kind The change kind. This is equivalent to
+ * {@link IResourceDelta#accept(IResourceDeltaVisitor)}
+ *
+ * @see IFileListener#fileChanged
+ */
+ public void fileChanged(IFile file, IMarkerDelta[] markerDeltas, int kind) {
+ if (AndroidConstants.EXT_XML.equals(file.getFileExtension())) {
+ // The resources files must have a file path similar to
+ // project/res/.../*.xml
+ // There is no support for sub folders, so the segment count must be 4
+ if (file.getFullPath().segmentCount() == 4) {
+ // check if we are inside the res folder.
+ String segment = file.getFullPath().segment(1);
+ if (segment.equalsIgnoreCase(SdkConstants.FD_RESOURCES)) {
+ // we are inside a res/ folder, get the actual ResourceFolder
+ ProjectResources resources = ResourceManager.getInstance().
+ getProjectResources(file.getProject());
+
+ // This happens when importing old Android projects in Eclipse
+ // that lack the container (probably because resources fail to build
+ // properly.)
+ if (resources == null) {
+ log(IStatus.INFO,
+ "getProjectResources failed for path %1$s in project %2$s", //$NON-NLS-1$
+ file.getFullPath().toOSString(),
+ file.getProject().getName());
+ return;
+ }
+
+ ResourceFolder resFolder = resources.getResourceFolder(
+ (IFolder)file.getParent());
+
+ if (resFolder != null) {
+ if (kind == IResourceDelta.ADDED) {
+ resourceAdded(file, resFolder.getType());
+ } else if (kind == IResourceDelta.CHANGED) {
+ resourceChanged(file, resFolder.getType());
+ }
+ } else {
+ // if the res folder is null, this means the name is invalid,
+ // in this case we remove whatever android editors that was set
+ // as the default editor.
+ IEditorDescriptor desc = IDE.getDefaultEditor(file);
+ String editorId = desc.getId();
+ if (editorId.startsWith(AndroidConstants.EDITORS_NAMESPACE)) {
+ // reset the default editor.
+ IDE.setDefaultEditor(file, null);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private void resourceAdded(IFile file, ResourceFolderType type) {
+ // set the default editor based on the type.
+ if (type == ResourceFolderType.LAYOUT) {
+ IDE.setDefaultEditor(file, LayoutEditor.ID);
+ } else if (type == ResourceFolderType.DRAWABLE
+ || type == ResourceFolderType.VALUES) {
+ IDE.setDefaultEditor(file, ResourcesEditor.ID);
+ } else if (type == ResourceFolderType.MENU) {
+ IDE.setDefaultEditor(file, MenuEditor.ID);
+ } else if (type == ResourceFolderType.XML) {
+ if (XmlEditor.canHandleFile(file)) {
+ IDE.setDefaultEditor(file, XmlEditor.ID);
+ } else {
+ // set a property to determine later if the XML can be handled
+ QualifiedName qname = new QualifiedName(
+ AdtPlugin.PLUGIN_ID,
+ UNKNOWN_EDITOR);
+ try {
+ file.setPersistentProperty(qname, "1");
+ } catch (CoreException e) {
+ // pass
+ }
+ }
+ }
+ }
+
+ private void resourceChanged(IFile file, ResourceFolderType type) {
+ if (type == ResourceFolderType.XML) {
+ IEditorDescriptor ed = IDE.getDefaultEditor(file);
+ if (ed == null || ed.getId() != XmlEditor.ID) {
+ QualifiedName qname = new QualifiedName(
+ AdtPlugin.PLUGIN_ID,
+ UNKNOWN_EDITOR);
+ String prop = null;
+ try {
+ prop = file.getPersistentProperty(qname);
+ } catch (CoreException e) {
+ // pass
+ }
+ if (prop != null && XmlEditor.canHandleFile(file)) {
+ try {
+ // remove the property & set editor
+ file.setPersistentProperty(qname, null);
+ IWorkbenchPage page = PlatformUI.getWorkbench().
+ getActiveWorkbenchWindow().getActivePage();
+
+ IEditorPart oldEditor = page.findEditor(new FileEditorInput(file));
+ if (oldEditor != null &&
+ AdtPlugin.displayPrompt("Android XML Editor",
+ String.format("The file you just saved as been recognized as a file that could be better handled using the Android XML Editor. Do you want to edit '%1$s' using the Android XML editor instead?",
+ file.getFullPath()))) {
+ IDE.setDefaultEditor(file, XmlEditor.ID);
+ IEditorPart newEditor = page.openEditor(
+ new FileEditorInput(file),
+ XmlEditor.ID,
+ true, /* activate */
+ IWorkbenchPage.MATCH_NONE);
+
+ if (newEditor != null) {
+ page.closeEditor(oldEditor, true /* save */);
+ }
+ }
+ } catch (CoreException e) {
+ // setPersistentProperty or page.openEditor may have failed
+ }
+ }
+ }
+ }
+ }
+
+ }, IResourceDelta.ADDED | IResourceDelta.CHANGED);
+ }
+
+ /**
+ * Adds a new {@link ITargetChangeListener} to be notified when a new SDK is loaded, or when
+ * a project has its target changed.
+ */
+ public void addTargetListener(ITargetChangeListener listener) {
+ mTargetChangeListeners.add(listener);
+ }
+
+ /**
+ * Removes an existing {@link ITargetChangeListener}.
+ * @see #addTargetListener(ITargetChangeListener)
+ */
+ public void removeTargetListener(ITargetChangeListener listener) {
+ mTargetChangeListeners.remove(listener);
+ }
+
+ /**
+ * Updates all the {@link ITargetChangeListener} that a target has changed for a given project.
+ * <p/>Only editors related to that project should reload.
+ */
+ @SuppressWarnings("unchecked")
+ public void updateTargetListener(final IProject project) {
+ final List<ITargetChangeListener> listeners =
+ (List<ITargetChangeListener>)mTargetChangeListeners.clone();
+
+ AdtPlugin.getDisplay().asyncExec(new Runnable() {
+ public void run() {
+ for (ITargetChangeListener listener : listeners) {
+ try {
+ listener.onProjectTargetChange(project);
+ } catch (Exception e) {
+ AdtPlugin.log(e, "Failed to update a TargetChangeListener."); //$NON-NLS-1$
+ }
+ }
+ }
+ });
+ }
+
+ public static synchronized OutputStream getErrorStream() {
+ return sPlugin.mAndroidConsoleErrorStream;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/Messages.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/Messages.java
new file mode 100644
index 0000000..a638810
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/Messages.java
@@ -0,0 +1,48 @@
+
+package com.android.ide.eclipse.adt;
+
+import org.eclipse.osgi.util.NLS;
+
+public class Messages extends NLS {
+ private static final String BUNDLE_NAME = "com.android.ide.eclipse.adt.messages"; //$NON-NLS-1$
+
+ public static String AdtPlugin_Android_SDK_Content_Loader;
+
+ public static String AdtPlugin_Android_SDK_Resource_Parser;
+
+ public static String AdtPlugin_Failed_To_Parse_s;
+
+ public static String AdtPlugin_Failed_To_Start_s;
+
+ public static String AdtPlugin_Parsing_Resources;
+
+ public static String Could_Not_Find;
+
+ public static String Could_Not_Find_Folder;
+
+ public static String Could_Not_Find_Folder_In_SDK;
+
+ public static String Dialog_Title_SDK_Location;
+
+ public static String Error_Check_Prefs;
+
+ public static String SDK_Not_Setup;
+
+ public static String VersionCheck_Plugin_Too_Old;
+
+ public static String VersionCheck_Plugin_Version_Failed;
+
+ public static String VersionCheck_SDK_Build_Too_Low;
+
+ public static String VersionCheck_SDK_Milestone_Too_Low;
+
+ public static String VersionCheck_Unable_To_Parse_Version_s;
+
+ static {
+ // initialize resource bundle
+ NLS.initializeMessages(BUNDLE_NAME, Messages.class);
+ }
+
+ private Messages() {
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/VersionCheck.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/VersionCheck.java
new file mode 100644
index 0000000..6d85af3
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/VersionCheck.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt;
+
+import com.android.ide.eclipse.adt.AdtPlugin.CheckSdkErrorHandler;
+import com.android.sdklib.SdkConstants;
+
+import org.osgi.framework.Constants;
+import org.osgi.framework.Version;
+
+import java.io.BufferedReader;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Class handling the version check for the plugin vs. the SDK.<br>
+ * The plugin must be able to support all version of the SDK.
+ *
+ * <p/>An SDK can require a new version of the plugin.
+ * <p/>The SDK contains a file with the minimum version for the plugin. This file is inside the
+ * <code>tools/lib</code> directory, and is called <code>plugin.prop</code>.<br>
+ * Inside that text file, there is a line in the format "plugin.version=#.#.#". This is checked
+ * against the current plugin version.<br>
+ *
+ */
+final class VersionCheck {
+ /**
+ * Pattern to get the minimum plugin version supported by the SDK. This is read from
+ * the file <code>$SDK/tools/lib/plugin.prop</code>.
+ */
+ private final static Pattern sPluginVersionPattern = Pattern.compile(
+ "^plugin.version=(\\d+)\\.(\\d+)\\.(\\d+).*$"); //$NON-NLS-1$
+
+ /**
+ * Checks the plugin and the SDK have compatible versions.
+ * @param osSdkPath The path to the SDK
+ * @return true if compatible.
+ */
+ public static boolean checkVersion(String osSdkPath, CheckSdkErrorHandler errorHandler) {
+ AdtPlugin plugin = AdtPlugin.getDefault();
+ String osLibs = osSdkPath + SdkConstants.OS_SDK_TOOLS_LIB_FOLDER;
+
+ // get the plugin property file, and grab the minimum plugin version required
+ // to work with the sdk
+ int minMajorVersion = -1;
+ int minMinorVersion = -1;
+ int minMicroVersion = -1;
+ try {
+ FileReader reader = new FileReader(osLibs + SdkConstants.FN_PLUGIN_PROP);
+ BufferedReader bReader = new BufferedReader(reader);
+ String line;
+ while ((line = bReader.readLine()) != null) {
+ Matcher m = sPluginVersionPattern.matcher(line);
+ if (m.matches()) {
+ minMajorVersion = Integer.parseInt(m.group(1));
+ minMinorVersion = Integer.parseInt(m.group(2));
+ minMicroVersion = Integer.parseInt(m.group(3));
+ break;
+ }
+ }
+ } catch (FileNotFoundException e) {
+ // the build id will be null, and this is handled by the builders.
+ } catch (IOException e) {
+ // the build id will be null, and this is handled by the builders.
+ }
+
+ // Failed to get the min plugin version number?
+ if (minMajorVersion == -1 || minMinorVersion == -1 || minMicroVersion ==-1) {
+ return errorHandler.handleWarning(Messages.VersionCheck_Plugin_Version_Failed);
+ }
+
+ // test the plugin number
+ String versionString = (String) plugin.getBundle().getHeaders().get(
+ Constants.BUNDLE_VERSION);
+ Version version = new Version(versionString);
+
+ boolean valid = true;
+ if (version.getMajor() < minMajorVersion) {
+ valid = false;
+ } else if (version.getMajor() == minMajorVersion) {
+ if (version.getMinor() < minMinorVersion) {
+ valid = false;
+ } else if (version.getMinor() == minMinorVersion) {
+ if (version.getMicro() < minMicroVersion) {
+ valid = false;
+ }
+ }
+ }
+
+ if (valid == false) {
+ return errorHandler.handleWarning(
+ String.format(Messages.VersionCheck_Plugin_Too_Old,
+ minMajorVersion, minMinorVersion, minMicroVersion, versionString));
+ }
+
+ return true; // no error!
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ApkBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ApkBuilder.java
new file mode 100644
index 0000000..e71ae47
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ApkBuilder.java
@@ -0,0 +1,1154 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.build;
+
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.project.ProjectHelper;
+import com.android.ide.eclipse.adt.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.sdk.Sdk;
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.common.project.BaseProjectHelper;
+import com.android.jarutils.DebugKeyProvider;
+import com.android.jarutils.JavaResourceFilter;
+import com.android.jarutils.SignedJarBuilder;
+import com.android.jarutils.DebugKeyProvider.IKeyGenOutput;
+import com.android.jarutils.DebugKeyProvider.KeytoolException;
+import com.android.jarutils.SignedJarBuilder.IZipEntryFilter;
+import com.android.prefs.AndroidLocation.AndroidLocationException;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.SdkConstants;
+
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IResourceDelta;
+import org.eclipse.core.resources.IResourceDeltaVisitor;
+import org.eclipse.core.resources.IWorkspace;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jface.preference.IPreferenceStore;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.security.GeneralSecurityException;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.text.DateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Map;
+import java.util.Set;
+import java.util.Map.Entry;
+
+public class ApkBuilder extends BaseBuilder {
+
+ public static final String ID = "com.android.ide.eclipse.adt.ApkBuilder"; //$NON-NLS-1$
+
+ private static final String PROPERTY_CONVERT_TO_DEX = "convertToDex"; //$NON-NLS-1$
+ private static final String PROPERTY_PACKAGE_RESOURCES = "packageResources"; //$NON-NLS-1$
+ private static final String PROPERTY_BUILD_APK = "buildApk"; //$NON-NLS-1$
+
+ private static final String DX_PREFIX = "Dx"; //$NON-NLS-1$
+
+ /**
+ * Dex conversion flag. This is set to true if one of the changed/added/removed
+ * file is a .class file. Upon visiting all the delta resource, if this
+ * flag is true, then we know we'll have to make the "classes.dex" file.
+ */
+ private boolean mConvertToDex = false;
+
+ /**
+ * Package resources flag. This is set to true if one of the changed/added/removed
+ * file is a resource file. Upon visiting all the delta resource, if
+ * this flag is true, then we know we'll have to repackage the resources.
+ */
+ private boolean mPackageResources = false;
+
+ /**
+ * Final package build flag.
+ */
+ private boolean mBuildFinalPackage = false;
+
+ private PrintStream mOutStream = null;
+ private PrintStream mErrStream = null;
+
+ /**
+ * Basic Resource Delta Visitor class to check if a referenced project had a change in its
+ * compiled java files.
+ */
+ private static class ReferencedProjectDeltaVisitor implements IResourceDeltaVisitor {
+
+ private boolean mConvertToDex = false;
+ private boolean mMakeFinalPackage;
+
+ private IPath mOutputFolder;
+ private ArrayList<IPath> mSourceFolders;
+
+ private ReferencedProjectDeltaVisitor(IJavaProject javaProject) {
+ try {
+ mOutputFolder = javaProject.getOutputLocation();
+ mSourceFolders = BaseProjectHelper.getSourceClasspaths(javaProject);
+ } catch (JavaModelException e) {
+ } finally {
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws CoreException
+ */
+ public boolean visit(IResourceDelta delta) throws CoreException {
+ // no need to keep looking if we already know we need to convert
+ // to dex and make the final package.
+ if (mConvertToDex && mMakeFinalPackage) {
+ return false;
+ }
+
+ // get the resource and the path segments.
+ IResource resource = delta.getResource();
+ IPath resourceFullPath = resource.getFullPath();
+
+ if (mOutputFolder.isPrefixOf(resourceFullPath)) {
+ int type = resource.getType();
+ if (type == IResource.FILE) {
+ String ext = resource.getFileExtension();
+ if (AndroidConstants.EXT_CLASS.equals(ext)) {
+ mConvertToDex = true;
+ }
+ }
+ return true;
+ } else {
+ for (IPath sourceFullPath : mSourceFolders) {
+ if (sourceFullPath.isPrefixOf(resourceFullPath)) {
+ int type = resource.getType();
+ if (type == IResource.FILE) {
+ // check if the file is a valid file that would be
+ // included during the final packaging.
+ if (checkFileForPackaging((IFile)resource)) {
+ mMakeFinalPackage = true;
+ }
+
+ return false;
+ } else if (type == IResource.FOLDER) {
+ // if this is a folder, we check if this is a valid folder as well.
+ // If this is a folder that needs to be ignored, we must return false,
+ // so that we ignore its content.
+ return checkFolderForPackaging((IFolder)resource);
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns if one of the .class file was modified.
+ */
+ boolean needDexConvertion() {
+ return mConvertToDex;
+ }
+
+ boolean needMakeFinalPackage() {
+ return mMakeFinalPackage;
+ }
+ }
+
+ /**
+ * {@link IZipEntryFilter} to filter out everything that is not a standard java resources.
+ * <p/>Used in {@link SignedJarBuilder#writeZip(java.io.InputStream, IZipEntryFilter)} when
+ * we only want the java resources from external jars.
+ */
+ private final IZipEntryFilter mJavaResourcesFilter = new JavaResourceFilter();
+
+ public ApkBuilder() {
+ super();
+ }
+
+ // build() returns a list of project from which this project depends for future compilation.
+ @SuppressWarnings("unchecked") //$NON-NLS-1$
+ @Override
+ protected IProject[] build(int kind, Map args, IProgressMonitor monitor)
+ throws CoreException {
+ // get a project object
+ IProject project = getProject();
+
+ // Top level check to make sure the build can move forward.
+ abortOnBadSetup(project);
+
+ // get the list of referenced projects.
+ IProject[] referencedProjects = ProjectHelper.getReferencedProjects(project);
+ IJavaProject[] referencedJavaProjects = getJavaProjects(referencedProjects);
+
+ // get the output folder, this method returns the path with a trailing
+ // separator
+ IJavaProject javaProject = JavaCore.create(project);
+ IFolder outputFolder = BaseProjectHelper.getOutputFolder(project);
+
+ // now we need to get the classpath list
+ ArrayList<IPath> sourceList = BaseProjectHelper.getSourceClasspaths(javaProject);
+
+ // First thing we do is go through the resource delta to not
+ // lose it if we have to abort the build for any reason.
+ ApkDeltaVisitor dv = null;
+ if (kind == FULL_BUILD) {
+ AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project,
+ Messages.Start_Full_Apk_Build);
+
+ mPackageResources = true;
+ mConvertToDex = true;
+ mBuildFinalPackage = true;
+ } else {
+ AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project,
+ Messages.Start_Inc_Apk_Build);
+
+ // go through the resources and see if something changed.
+ IResourceDelta delta = getDelta(project);
+ if (delta == null) {
+ mPackageResources = true;
+ mConvertToDex = true;
+ mBuildFinalPackage = true;
+ } else {
+ dv = new ApkDeltaVisitor(this, sourceList, outputFolder);
+ delta.accept(dv);
+
+ // save the state
+ mPackageResources |= dv.getPackageResources();
+ mConvertToDex |= dv.getConvertToDex();
+ mBuildFinalPackage |= dv.getMakeFinalPackage();
+ }
+
+ // also go through the delta for all the referenced projects, until we are forced to
+ // compile anyway
+ for (int i = 0 ; i < referencedJavaProjects.length &&
+ (mBuildFinalPackage == false || mConvertToDex == false); i++) {
+ IJavaProject referencedJavaProject = referencedJavaProjects[i];
+ delta = getDelta(referencedJavaProject.getProject());
+ if (delta != null) {
+ ReferencedProjectDeltaVisitor refProjectDv = new ReferencedProjectDeltaVisitor(
+ referencedJavaProject);
+ delta.accept(refProjectDv);
+
+ // save the state
+ mConvertToDex |= refProjectDv.needDexConvertion();
+ mBuildFinalPackage |= refProjectDv.needMakeFinalPackage();
+ }
+ }
+ }
+
+ // store the build status in the persistent storage
+ saveProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX , mConvertToDex);
+ saveProjectBooleanProperty(PROPERTY_PACKAGE_RESOURCES, mPackageResources);
+ saveProjectBooleanProperty(PROPERTY_BUILD_APK, mBuildFinalPackage);
+
+ if (dv != null && dv.mXmlError) {
+ AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project,
+ Messages.Xml_Error);
+
+ // if there was some XML errors, we just return w/o doing
+ // anything since we've put some markers in the files anyway
+ return referencedProjects;
+ }
+
+ if (outputFolder == null) {
+ // mark project and exit
+ markProject(AdtConstants.MARKER_ADT, Messages.Failed_To_Get_Output,
+ IMarker.SEVERITY_ERROR);
+ return referencedProjects;
+ }
+
+ // first thing we do is check that the SDK directory has been setup.
+ String osSdkFolder = AdtPlugin.getOsSdkFolder();
+
+ if (osSdkFolder.length() == 0) {
+ // this has already been checked in the precompiler. Therefore,
+ // while we do have to cancel the build, we don't have to return
+ // any error or throw anything.
+ return referencedProjects;
+ }
+
+ // get the extra configs for the project.
+ // The map contains (name, filter) where 'name' is a name to be used in the apk filename,
+ // and filter is the resource filter to be used in the aapt -c parameters to restrict
+ // which resource configurations to package in the apk.
+ Map<String, String> configs = Sdk.getCurrent().getProjectApkConfigs(project);
+
+ // do some extra check, in case the output files are not present. This
+ // will force to recreate them.
+ IResource tmp = null;
+
+ if (mPackageResources == false) {
+ // check the full resource package
+ tmp = outputFolder.findMember(AndroidConstants.FN_RESOURCES_AP_);
+ if (tmp == null || tmp.exists() == false) {
+ mPackageResources = true;
+ mBuildFinalPackage = true;
+ } else {
+ // if the full package is present, we check the filtered resource packages as well
+ if (configs != null) {
+ Set<Entry<String, String>> entrySet = configs.entrySet();
+
+ for (Entry<String, String> entry : entrySet) {
+ String filename = String.format(AndroidConstants.FN_RESOURCES_S_AP_,
+ entry.getKey());
+
+ tmp = outputFolder.findMember(filename);
+ if (tmp == null || (tmp instanceof IFile &&
+ tmp.exists() == false)) {
+ String msg = String.format(Messages.s_Missing_Repackaging, filename);
+ AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, msg);
+ mPackageResources = true;
+ mBuildFinalPackage = true;
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ // check classes.dex is present. If not we force to recreate it.
+ if (mConvertToDex == false) {
+ tmp = outputFolder.findMember(AndroidConstants.FN_CLASSES_DEX);
+ if (tmp == null || tmp.exists() == false) {
+ mConvertToDex = true;
+ mBuildFinalPackage = true;
+ }
+ }
+
+ // also check the final file(s)!
+ String finalPackageName = getFileName(project, null /*config*/);
+ if (mBuildFinalPackage == false) {
+ tmp = outputFolder.findMember(finalPackageName);
+ if (tmp == null || (tmp instanceof IFile &&
+ tmp.exists() == false)) {
+ String msg = String.format(Messages.s_Missing_Repackaging, finalPackageName);
+ AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, msg);
+ mBuildFinalPackage = true;
+ } else if (configs != null) {
+ // if the full apk is present, we check the filtered apk as well
+ Set<Entry<String, String>> entrySet = configs.entrySet();
+
+ for (Entry<String, String> entry : entrySet) {
+ String filename = getFileName(project, entry.getKey());
+
+ tmp = outputFolder.findMember(filename);
+ if (tmp == null || (tmp instanceof IFile &&
+ tmp.exists() == false)) {
+ String msg = String.format(Messages.s_Missing_Repackaging, filename);
+ AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, msg);
+ mBuildFinalPackage = true;
+ break;
+ }
+ }
+ }
+ }
+
+ // at this point we know if we need to recreate the temporary apk
+ // or the dex file, but we don't know if we simply need to recreate them
+ // because they are missing
+
+ // refresh the output directory first
+ IContainer ic = outputFolder.getParent();
+ if (ic != null) {
+ ic.refreshLocal(IResource.DEPTH_ONE, monitor);
+ }
+
+ // we need to test all three, as we may need to make the final package
+ // but not the intermediary ones.
+ if (mPackageResources || mConvertToDex || mBuildFinalPackage) {
+ IPath binLocation = outputFolder.getLocation();
+ if (binLocation == null) {
+ markProject(AdtConstants.MARKER_ADT, Messages.Output_Missing,
+ IMarker.SEVERITY_ERROR);
+ return referencedProjects;
+ }
+ String osBinPath = binLocation.toOSString();
+
+ // Remove the old .apk.
+ // This make sure that if the apk is corrupted, then dx (which would attempt
+ // to open it), will not fail.
+ String osFinalPackagePath = osBinPath + File.separator + finalPackageName;
+ File finalPackage = new File(osFinalPackagePath);
+
+ // if delete failed, this is not really a problem, as the final package generation
+ // handle already present .apk, and if that one failed as well, the user will be
+ // notified.
+ finalPackage.delete();
+
+ if (configs != null) {
+ Set<Entry<String, String>> entrySet = configs.entrySet();
+ for (Entry<String, String> entry : entrySet) {
+ String packageFilepath = osBinPath + File.separator +
+ getFileName(project, entry.getKey());
+
+ finalPackage = new File(packageFilepath);
+ finalPackage.delete();
+ }
+ }
+
+ // first we check if we need to package the resources.
+ if (mPackageResources) {
+ // remove some aapt_package only markers.
+ removeMarkersFromContainer(project, AndroidConstants.MARKER_AAPT_PACKAGE);
+
+ // need to figure out some path before we can execute aapt;
+
+ // resource to the AndroidManifest.xml file
+ IResource manifestResource = project .findMember(
+ AndroidConstants.WS_SEP + AndroidConstants.FN_ANDROID_MANIFEST);
+
+ if (manifestResource == null
+ || manifestResource.exists() == false) {
+ // mark project and exit
+ String msg = String.format(Messages.s_File_Missing,
+ AndroidConstants.FN_ANDROID_MANIFEST);
+ markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
+ return referencedProjects;
+ }
+
+ // get the resource folder
+ IFolder resFolder = project.getFolder(
+ AndroidConstants.WS_RESOURCES);
+
+ // and the assets folder
+ IFolder assetsFolder = project.getFolder(
+ AndroidConstants.WS_ASSETS);
+
+ // we need to make sure this one exists.
+ if (assetsFolder.exists() == false) {
+ assetsFolder = null;
+ }
+
+ IPath resLocation = resFolder.getLocation();
+ IPath manifestLocation = manifestResource.getLocation();
+
+ if (resLocation != null && manifestLocation != null) {
+ String osResPath = resLocation.toOSString();
+ String osManifestPath = manifestLocation.toOSString();
+
+ String osAssetsPath = null;
+ if (assetsFolder != null) {
+ osAssetsPath = assetsFolder.getLocation().toOSString();
+ }
+
+ // build the default resource package
+ if (executeAapt(project, osManifestPath, osResPath,
+ osAssetsPath, osBinPath + File.separator +
+ AndroidConstants.FN_RESOURCES_AP_, null /*configFilter*/) == false) {
+ // aapt failed. Whatever files that needed to be marked
+ // have already been marked. We just return.
+ return referencedProjects;
+ }
+
+ // now do the same thing for all the configured resource packages.
+ if (configs != null) {
+ Set<Entry<String, String>> entrySet = configs.entrySet();
+ for (Entry<String, String> entry : entrySet) {
+ String outPathFormat = osBinPath + File.separator +
+ AndroidConstants.FN_RESOURCES_S_AP_;
+ String outPath = String.format(outPathFormat, entry.getKey());
+ if (executeAapt(project, osManifestPath, osResPath,
+ osAssetsPath, outPath, entry.getValue()) == false) {
+ // aapt failed. Whatever files that needed to be marked
+ // have already been marked. We just return.
+ return referencedProjects;
+ }
+ }
+ }
+
+ // build has been done. reset the state of the builder
+ mPackageResources = false;
+
+ // and store it
+ saveProjectBooleanProperty(PROPERTY_PACKAGE_RESOURCES, mPackageResources);
+ }
+ }
+
+ // then we check if we need to package the .class into classes.dex
+ if (mConvertToDex) {
+ if (executeDx(javaProject, osBinPath, osBinPath + File.separator +
+ AndroidConstants.FN_CLASSES_DEX, referencedJavaProjects) == false) {
+ // dx failed, we return
+ return referencedProjects;
+ }
+
+ // build has been done. reset the state of the builder
+ mConvertToDex = false;
+
+ // and store it
+ saveProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX, mConvertToDex);
+ }
+
+ // now we need to make the final package from the intermediary apk
+ // and classes.dex.
+ // This is the default package with all the resources.
+
+ String classesDexPath = osBinPath + File.separator + AndroidConstants.FN_CLASSES_DEX;
+ if (finalPackage(osBinPath + File.separator + AndroidConstants.FN_RESOURCES_AP_,
+ classesDexPath,osFinalPackagePath, javaProject,
+ referencedJavaProjects) == false) {
+ return referencedProjects;
+ }
+
+ // now do the same thing for all the configured resource packages.
+ if (configs != null) {
+ String resPathFormat = osBinPath + File.separator +
+ AndroidConstants.FN_RESOURCES_S_AP_;
+
+ Set<Entry<String, String>> entrySet = configs.entrySet();
+ for (Entry<String, String> entry : entrySet) {
+ // make the filename for the resource package.
+ String resPath = String.format(resPathFormat, entry.getKey());
+
+ // make the filename for the apk to generate
+ String apkOsFilePath = osBinPath + File.separator +
+ getFileName(project, entry.getKey());
+ if (finalPackage(resPath, classesDexPath, apkOsFilePath, javaProject,
+ referencedJavaProjects) == false) {
+ return referencedProjects;
+ }
+ }
+ }
+
+ // we are done.
+
+ // get the resource to bin
+ outputFolder.refreshLocal(IResource.DEPTH_ONE, monitor);
+
+ // build has been done. reset the state of the builder
+ mBuildFinalPackage = false;
+
+ // and store it
+ saveProjectBooleanProperty(PROPERTY_BUILD_APK, mBuildFinalPackage);
+
+ AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, getProject(),
+ "Build Success!");
+ }
+ return referencedProjects;
+ }
+
+
+ @Override
+ protected void startupOnInitialize() {
+ super.startupOnInitialize();
+
+ // load the build status. We pass true as the default value to
+ // force a recompile in case the property was not found
+ mConvertToDex = loadProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX , true);
+ mPackageResources = loadProjectBooleanProperty(PROPERTY_PACKAGE_RESOURCES, true);
+ mBuildFinalPackage = loadProjectBooleanProperty(PROPERTY_BUILD_APK, true);
+ }
+
+ /**
+ * Executes aapt. If any error happen, files or the project will be marked.
+ * @param project The Project
+ * @param osManifestPath The path to the manifest file
+ * @param osResPath The path to the res folder
+ * @param osAssetsPath The path to the assets folder. This can be null.
+ * @param osOutFilePath The path to the temporary resource file to create.
+ * @param configFilter The configuration filter for the resources to include
+ * (used with -c option, for example "port,en,fr" to include portrait, English and French
+ * resources.)
+ * @return true if success, false otherwise.
+ */
+ private boolean executeAapt(IProject project, String osManifestPath,
+ String osResPath, String osAssetsPath, String osOutFilePath, String configFilter) {
+ IAndroidTarget target = Sdk.getCurrent().getTarget(project);
+
+ // Create the command line.
+ ArrayList<String> commandArray = new ArrayList<String>();
+ commandArray.add(target.getPath(IAndroidTarget.AAPT));
+ commandArray.add("package"); //$NON-NLS-1$
+ commandArray.add("-f");//$NON-NLS-1$
+ if (AdtPlugin.getBuildVerbosity() == AdtConstants.BUILD_VERBOSE) {
+ commandArray.add("-v"); //$NON-NLS-1$
+ }
+ if (configFilter != null) {
+ commandArray.add("-c"); //$NON-NLS-1$
+ commandArray.add(configFilter);
+ }
+ commandArray.add("-M"); //$NON-NLS-1$
+ commandArray.add(osManifestPath);
+ commandArray.add("-S"); //$NON-NLS-1$
+ commandArray.add(osResPath);
+ if (osAssetsPath != null) {
+ commandArray.add("-A"); //$NON-NLS-1$
+ commandArray.add(osAssetsPath);
+ }
+ commandArray.add("-I"); //$NON-NLS-1$
+ commandArray.add(target.getPath(IAndroidTarget.ANDROID_JAR));
+ commandArray.add("-F"); //$NON-NLS-1$
+ commandArray.add(osOutFilePath);
+
+ String command[] = commandArray.toArray(
+ new String[commandArray.size()]);
+
+ if (AdtPlugin.getBuildVerbosity() == AdtConstants.BUILD_VERBOSE) {
+ StringBuilder sb = new StringBuilder();
+ for (String c : command) {
+ sb.append(c);
+ sb.append(' ');
+ }
+ AdtPlugin.printToConsole(project, sb.toString());
+ }
+
+ // launch
+ int execError = 1;
+ try {
+ // launch the command line process
+ Process process = Runtime.getRuntime().exec(command);
+
+ // list to store each line of stderr
+ ArrayList<String> results = new ArrayList<String>();
+
+ // get the output and return code from the process
+ execError = grabProcessOutput(process, results);
+
+ // attempt to parse the error output
+ boolean parsingError = parseAaptOutput(results, project);
+
+ // if we couldn't parse the output we display it in the console.
+ if (parsingError) {
+ if (execError != 0) {
+ AdtPlugin.printErrorToConsole(project, results.toArray());
+ } else {
+ AdtPlugin.printBuildToConsole(AdtConstants.BUILD_ALWAYS, project,
+ results.toArray());
+ }
+ }
+
+ // We need to abort if the exec failed.
+ if (execError != 0) {
+ // if the exec failed, and we couldn't parse the error output (and therefore
+ // not all files that should have been marked, were marked), we put a generic
+ // marker on the project and abort.
+ if (parsingError) {
+ markProject(AdtConstants.MARKER_ADT, Messages.Unparsed_AAPT_Errors,
+ IMarker.SEVERITY_ERROR);
+ }
+
+ // abort if exec failed.
+ return false;
+ }
+ } catch (IOException e1) {
+ String msg = String.format(Messages.AAPT_Exec_Error, command[0]);
+ markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
+ return false;
+ } catch (InterruptedException e) {
+ String msg = String.format(Messages.AAPT_Exec_Error, command[0]);
+ markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Execute the Dx tool for dalvik code conversion.
+ * @param javaProject The java project
+ * @param osBinPath the path to the output folder of the project
+ * @param osOutFilePath the path of the dex file to create.
+ * @param referencedJavaProjects the list of referenced projects for this project.
+ *
+ * @throws CoreException
+ */
+ private boolean executeDx(IJavaProject javaProject, String osBinPath, String osOutFilePath,
+ IJavaProject[] referencedJavaProjects) throws CoreException {
+ IAndroidTarget target = Sdk.getCurrent().getTarget(javaProject.getProject());
+ AndroidTargetData targetData = Sdk.getCurrent().getTargetData(target);
+ if (targetData == null) {
+ throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ Messages.ApkBuilder_UnableBuild_Dex_Not_loaded));
+ }
+
+ // get the dex wrapper
+ DexWrapper wrapper = targetData.getDexWrapper();
+
+ if (wrapper == null) {
+ throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ Messages.ApkBuilder_UnableBuild_Dex_Not_loaded));
+ }
+
+ // make sure dx use the proper output streams.
+ // first make sure we actually have the streams available.
+ if (mOutStream == null) {
+ IProject project = getProject();
+ mOutStream = AdtPlugin.getOutPrintStream(project, DX_PREFIX);
+ mErrStream = AdtPlugin.getErrPrintStream(project, DX_PREFIX);
+ }
+
+ try {
+ // get the list of libraries to include with the source code
+ String[] libraries = getExternalJars();
+
+ // get the list of referenced projects output to add
+ String[] projectOutputs = getProjectOutputs(referencedJavaProjects);
+
+ String[] fileNames = new String[1 + projectOutputs.length + libraries.length];
+
+ // first this project output
+ fileNames[0] = osBinPath;
+
+ // then other project output
+ System.arraycopy(projectOutputs, 0, fileNames, 1, projectOutputs.length);
+
+ // then external jars.
+ System.arraycopy(libraries, 0, fileNames, 1 + projectOutputs.length, libraries.length);
+
+ int res = wrapper.run(osOutFilePath, fileNames,
+ AdtPlugin.getBuildVerbosity() == AdtConstants.BUILD_VERBOSE,
+ mOutStream, mErrStream);
+
+ if (res != 0) {
+ // output error message and marker the project.
+ String message = String.format(Messages.Dalvik_Error_d,
+ res);
+ AdtPlugin.printErrorToConsole(getProject(), message);
+ markProject(AdtConstants.MARKER_ADT, message, IMarker.SEVERITY_ERROR);
+ return false;
+ }
+ } catch (Throwable ex) {
+ String message = ex.getMessage();
+ if (message == null) {
+ message = ex.getClass().getCanonicalName();
+ }
+ message = String.format(Messages.Dalvik_Error_s, message);
+ AdtPlugin.printErrorToConsole(getProject(), message);
+ markProject(AdtConstants.MARKER_ADT, message, IMarker.SEVERITY_ERROR);
+ if ((ex instanceof NoClassDefFoundError)
+ || (ex instanceof NoSuchMethodError)) {
+ AdtPlugin.printErrorToConsole(getProject(), Messages.Incompatible_VM_Warning,
+ Messages.Requires_1_5_Error);
+ }
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Makes the final package. Package the dex files, the temporary resource file into the final
+ * package file.
+ * @param intermediateApk The path to the temporary resource file.
+ * @param dex The path to the dex file.
+ * @param output The path to the final package file to create.
+ * @param javaProject
+ * @param referencedJavaProjects
+ * @return true if success, false otherwise.
+ */
+ private boolean finalPackage(String intermediateApk, String dex, String output,
+ final IJavaProject javaProject, IJavaProject[] referencedJavaProjects) {
+ FileOutputStream fos = null;
+ try {
+ IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore();
+ String osKeyPath = store.getString(AdtPlugin.PREFS_CUSTOM_DEBUG_KEYSTORE);
+ if (osKeyPath == null || new File(osKeyPath).exists() == false) {
+ osKeyPath = DebugKeyProvider.getDefaultKeyStoreOsPath();
+ AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, getProject(),
+ Messages.ApkBuilder_Using_Default_Key);
+ } else {
+ AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, getProject(),
+ String.format(Messages.ApkBuilder_Using_s_To_Sign, osKeyPath));
+ }
+
+ // TODO: get the store type from somewhere else.
+ DebugKeyProvider provider = new DebugKeyProvider(osKeyPath, null /* storeType */,
+ new IKeyGenOutput() {
+ public void err(String message) {
+ AdtPlugin.printErrorToConsole(javaProject.getProject(),
+ Messages.ApkBuilder_Signing_Key_Creation_s + message);
+ }
+
+ public void out(String message) {
+ AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE,
+ javaProject.getProject(),
+ Messages.ApkBuilder_Signing_Key_Creation_s + message);
+ }
+ });
+ PrivateKey key = provider.getDebugKey();
+ X509Certificate certificate = (X509Certificate)provider.getCertificate();
+
+ if (key == null) {
+ String msg = String.format(Messages.Final_Archive_Error_s,
+ Messages.ApkBuilder_Unable_To_Gey_Key);
+ AdtPlugin.printErrorToConsole(javaProject.getProject(), msg);
+ markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
+ return false;
+ }
+
+ // compare the certificate expiration date
+ if (certificate != null && certificate.getNotAfter().compareTo(new Date()) < 0) {
+ // TODO, regenerate a new one.
+ String msg = String.format(Messages.Final_Archive_Error_s,
+ String.format(Messages.ApkBuilder_Certificate_Expired_on_s,
+ DateFormat.getInstance().format(certificate.getNotAfter())));
+ AdtPlugin.printErrorToConsole(javaProject.getProject(), msg);
+ markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
+ return false;
+ }
+
+ // create the jar builder.
+ fos = new FileOutputStream(output);
+ SignedJarBuilder builder = new SignedJarBuilder(fos, key, certificate);
+
+ // add the intermediate file containing the compiled resources.
+ AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, getProject(),
+ String.format(Messages.ApkBuilder_Packaging_s, intermediateApk));
+ FileInputStream fis = new FileInputStream(intermediateApk);
+ try {
+ builder.writeZip(fis, null /* filter */);
+ } finally {
+ fis.close();
+ }
+
+ // Now we add the new file to the zip archive for the classes.dex file.
+ AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, getProject(),
+ String.format(Messages.ApkBuilder_Packaging_s, AndroidConstants.FN_CLASSES_DEX));
+ File entryFile = new File(dex);
+ builder.writeFile(entryFile, AndroidConstants.FN_CLASSES_DEX);
+
+ // Now we write the standard resources from the project and the referenced projects.
+ writeStandardResources(builder, javaProject, referencedJavaProjects);
+
+ // Now we write the standard resources from the external libraries
+ for (String libraryOsPath : getExternalJars()) {
+ AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, getProject(),
+ String.format(Messages.ApkBuilder_Packaging_s, libraryOsPath));
+ try {
+ fis = new FileInputStream(libraryOsPath);
+ builder.writeZip(fis, mJavaResourcesFilter);
+ } finally {
+ fis.close();
+ }
+ }
+
+ // now write the native libraries.
+ // First look if the lib folder is there.
+ IResource libFolder = javaProject.getProject().findMember(SdkConstants.FD_NATIVE_LIBS);
+ if (libFolder != null && libFolder.exists() &&
+ libFolder.getType() == IResource.FOLDER) {
+ // look inside and put .so in lib/* by keeping the relative folder path.
+ writeNativeLibraries(libFolder.getFullPath().segmentCount(), builder, libFolder);
+ }
+
+ // close the jar file and write the manifest and sign it.
+ builder.close();
+ } catch (GeneralSecurityException e1) {
+ // mark project and return
+ String msg = String.format(Messages.Final_Archive_Error_s, e1.getMessage());
+ AdtPlugin.printErrorToConsole(javaProject.getProject(), msg);
+ markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
+ return false;
+ } catch (IOException e1) {
+ // mark project and return
+ String msg = String.format(Messages.Final_Archive_Error_s, e1.getMessage());
+ AdtPlugin.printErrorToConsole(javaProject.getProject(), msg);
+ markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
+ return false;
+ } catch (KeytoolException e) {
+ String eMessage = e.getMessage();
+
+ // mark the project with the standard message
+ String msg = String.format(Messages.Final_Archive_Error_s, eMessage);
+ markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
+
+ // output more info in the console
+ AdtPlugin.printErrorToConsole(javaProject.getProject(),
+ msg,
+ String.format(Messages.ApkBuilder_JAVA_HOME_is_s, e.getJavaHome()),
+ Messages.ApkBuilder_Update_or_Execute_manually_s,
+ e.getCommandLine());
+ } catch (AndroidLocationException e) {
+ String eMessage = e.getMessage();
+
+ // mark the project with the standard message
+ String msg = String.format(Messages.Final_Archive_Error_s, eMessage);
+ markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
+
+ // and also output it in the console
+ AdtPlugin.printErrorToConsole(javaProject.getProject(), msg);
+ } catch (CoreException e) {
+ // mark project and return
+ String msg = String.format(Messages.Final_Archive_Error_s, e.getMessage());
+ AdtPlugin.printErrorToConsole(javaProject.getProject(), msg);
+ markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
+ return false;
+ } finally {
+ if (fos != null) {
+ try {
+ fos.close();
+ } catch (IOException e) {
+ // pass.
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Writes native libraries into a {@link SignedJarBuilder}.
+ * <p/>This recursively go through folder and writes .so files.
+ * The path in the archive is based on the root folder containing the libraries in the project.
+ * Its segment count is passed to the method to compute the resources path relative to the root
+ * folder.
+ * Native libraries in the archive must be in a "lib" folder. Everything in the project native
+ * lib folder directly goes in this "lib" folder in the archive.
+ *
+ *
+ * @param rootSegmentCount The number of segment of the path of the folder containing the
+ * libraries. This is used to compute the path in the archive.
+ * @param jarBuilder the {@link SignedJarBuilder} used to create the archive.
+ * @param resource the IResource to write.
+ * @throws CoreException
+ * @throws IOException
+ */
+ private void writeNativeLibraries(int rootSegmentCount, SignedJarBuilder jarBuilder,
+ IResource resource) throws CoreException, IOException {
+ if (resource.getType() == IResource.FILE) {
+ IPath path = resource.getFullPath();
+
+ // check the extension.
+ if (path.getFileExtension().equalsIgnoreCase(AndroidConstants.EXT_NATIVE_LIB)) {
+ // remove the first segment to build the path inside the archive.
+ path = path.removeFirstSegments(rootSegmentCount);
+
+ // add it to the archive.
+ IPath apkPath = new Path(SdkConstants.FD_APK_NATIVE_LIBS);
+ apkPath = apkPath.append(path);
+
+ // writes the file in the apk.
+ jarBuilder.writeFile(resource.getLocation().toFile(), apkPath.toString());
+ }
+ } else if (resource.getType() == IResource.FOLDER) {
+ IResource[] members = ((IFolder)resource).members();
+ for (IResource member : members) {
+ writeNativeLibraries(rootSegmentCount, jarBuilder, member);
+ }
+ }
+ }
+
+ /**
+ * Writes the standard resources of a project and its referenced projects
+ * into a {@link SignedJarBuilder}.
+ * Standard resources are non java/aidl files placed in the java package folders.
+ * @param jarBuilder the {@link SignedJarBuilder}.
+ * @param javaProject the javaProject object.
+ * @param referencedJavaProjects the java projects that this project references.
+ * @throws IOException
+ */
+ private void writeStandardResources(SignedJarBuilder jarBuilder, IJavaProject javaProject,
+ IJavaProject[] referencedJavaProjects) throws IOException {
+ IWorkspace ws = ResourcesPlugin.getWorkspace();
+ IWorkspaceRoot wsRoot = ws.getRoot();
+
+ // create a list of path already put into the archive, in order to detect conflict
+ ArrayList<String> list = new ArrayList<String>();
+
+ writeStandardProjectResources(jarBuilder, javaProject, wsRoot, list);
+
+ for (IJavaProject referencedJavaProject : referencedJavaProjects) {
+ writeStandardProjectResources(jarBuilder, referencedJavaProject, wsRoot, list);
+ }
+ }
+
+ /**
+ * Writes the standard resources of a {@link IJavaProject} into a {@link SignedJarBuilder}.
+ * Standard resources are non java/aidl files placed in the java package folders.
+ * @param jarBuilder the {@link SignedJarBuilder}.
+ * @param javaProject the javaProject object.
+ * @param wsRoot the {@link IWorkspaceRoot}.
+ * @param list a list of files already added to the archive, to detect conflicts.
+ * @throws IOException
+ */
+ private void writeStandardProjectResources(SignedJarBuilder jarBuilder,
+ IJavaProject javaProject, IWorkspaceRoot wsRoot, ArrayList<String> list)
+ throws IOException {
+ // get the source pathes
+ ArrayList<IPath> sourceFolders = BaseProjectHelper.getSourceClasspaths(javaProject);
+
+ // loop on them and then recursively go through the content looking for matching files.
+ for (IPath sourcePath : sourceFolders) {
+ IResource sourceResource = wsRoot.findMember(sourcePath);
+ if (sourceResource != null && sourceResource.getType() == IResource.FOLDER) {
+ writeStandardSourceFolderResources(jarBuilder, sourcePath, (IFolder)sourceResource,
+ list);
+ }
+ }
+ }
+
+ /**
+ * Recursively writes the standard resources of a source folder into a {@link SignedJarBuilder}.
+ * Standard resources are non java/aidl files placed in the java package folders.
+ * @param jarBuilder the {@link SignedJarBuilder}.
+ * @param sourceFolder the {@link IPath} of the source folder.
+ * @param currentFolder The current folder we're recursively processing.
+ * @param list a list of files already added to the archive, to detect conflicts.
+ * @throws IOException
+ */
+ private void writeStandardSourceFolderResources(SignedJarBuilder jarBuilder, IPath sourceFolder,
+ IFolder currentFolder, ArrayList<String> list) throws IOException {
+ try {
+ IResource[] members = currentFolder.members();
+
+ for (IResource member : members) {
+ int type = member.getType();
+ if (type == IResource.FILE && member.exists()) {
+ if (checkFileForPackaging((IFile)member)) {
+ // this files must be added to the archive.
+ IPath fullPath = member.getFullPath();
+
+ // We need to create its path inside the archive.
+ // This path is relative to the source folder.
+ IPath relativePath = fullPath.removeFirstSegments(
+ sourceFolder.segmentCount());
+ String zipPath = relativePath.toString();
+
+ // lets check it's not already in the list of path added to the archive
+ if (list.indexOf(zipPath) != -1) {
+ AdtPlugin.printErrorToConsole(getProject(),
+ String.format(
+ Messages.ApkBuilder_s_Conflict_with_file_s,
+ fullPath, zipPath));
+ } else {
+ // get the File object
+ File entryFile = member.getLocation().toFile();
+
+ AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, getProject(),
+ String.format(Messages.ApkBuilder_Packaging_s_into_s, fullPath, zipPath));
+
+ // write it in the zip archive
+ jarBuilder.writeFile(entryFile, zipPath);
+
+ // and add it to the list of entries
+ list.add(zipPath);
+ }
+ }
+ } else if (type == IResource.FOLDER) {
+ if (checkFolderForPackaging((IFolder)member)) {
+ writeStandardSourceFolderResources(jarBuilder, sourceFolder,
+ (IFolder)member, list);
+ }
+ }
+ }
+ } catch (CoreException e) {
+ // if we can't get the members of the folder, we just don't do anything.
+ }
+ }
+
+ /**
+ * Returns the list of the output folders for the specified {@link IJavaProject} objects.
+ * @param referencedJavaProjects the java projects.
+ * @return an array, always. Can be empty.
+ * @throws CoreException
+ */
+ private String[] getProjectOutputs(IJavaProject[] referencedJavaProjects) throws CoreException {
+ ArrayList<String> list = new ArrayList<String>();
+
+ IWorkspace ws = ResourcesPlugin.getWorkspace();
+ IWorkspaceRoot wsRoot = ws.getRoot();
+
+ for (IJavaProject javaProject : referencedJavaProjects) {
+ // get the output folder
+ IPath path = null;
+ try {
+ path = javaProject.getOutputLocation();
+ } catch (JavaModelException e) {
+ continue;
+ }
+
+ IResource outputResource = wsRoot.findMember(path);
+ if (outputResource != null && outputResource.getType() == IResource.FOLDER) {
+ String outputOsPath = outputResource.getLocation().toOSString();
+
+ list.add(outputOsPath);
+ }
+ }
+
+ return list.toArray(new String[list.size()]);
+ }
+
+ /**
+ * Returns an array of {@link IJavaProject} matching the provided {@link IProject} objects.
+ * @param projects the IProject objects.
+ * @return an array, always. Can be empty.
+ * @throws CoreException
+ */
+ private IJavaProject[] getJavaProjects(IProject[] projects) throws CoreException {
+ ArrayList<IJavaProject> list = new ArrayList<IJavaProject>();
+
+ for (IProject p : projects) {
+ if (p.isOpen() && p.hasNature(JavaCore.NATURE_ID)) {
+
+ list.add(JavaCore.create(p));
+ }
+ }
+
+ return list.toArray(new IJavaProject[list.size()]);
+ }
+
+ /**
+ * Returns the apk filename for the given project
+ * @param project The project.
+ * @param config An optional config name. Can be null.
+ */
+ private static String getFileName(IProject project, String config) {
+ if (config != null) {
+ return project.getName() + "-" + config + AndroidConstants.DOT_ANDROID_PACKAGE; //$NON-NLS-1$
+ }
+
+ return project.getName() + AndroidConstants.DOT_ANDROID_PACKAGE;
+ }
+
+ /**
+ * Checks a {@link IFile} to make sure it should be packaged as standard resources.
+ * @param file the IFile representing the file.
+ * @return true if the file should be packaged as standard java resources.
+ */
+ static boolean checkFileForPackaging(IFile file) {
+ String name = file.getName();
+
+ String ext = file.getFileExtension();
+ return JavaResourceFilter.checkFileForPackaging(name, ext);
+ }
+
+ /**
+ * Checks whether an {@link IFolder} and its content is valid for packaging into the .apk as
+ * standard Java resource.
+ * @param folder the {@link IFolder} to check.
+ */
+ static boolean checkFolderForPackaging(IFolder folder) {
+ String name = folder.getName();
+ return JavaResourceFilter.checkFolderForPackaging(name);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ApkDeltaVisitor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ApkDeltaVisitor.java
new file mode 100644
index 0000000..5d6793a
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ApkDeltaVisitor.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.build;
+
+import com.android.ide.eclipse.adt.build.BaseBuilder.BaseDeltaVisitor;
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.sdklib.SdkConstants;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IResourceDelta;
+import org.eclipse.core.resources.IResourceDeltaVisitor;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+
+import java.util.ArrayList;
+
+/**
+ * Delta resource visitor looking for changes that will trigger a new packaging of an Android
+ * application.
+ * <p/>
+ * This looks for the following changes:
+ * <ul>
+ * <li>Any change to the AndroidManifest.xml file</li>
+ * <li>Any change inside the assets/ folder</li>
+ * <li>Any file change inside the res/ folder</li>
+ * <li>Any .class file change inside the output folder</li>
+ * <li>Any change to the classes.dex inside the output folder</li>
+ * <li>Any change to the packaged resources file inside the output folder</li>
+ * <li>Any change to a non java/aidl file inside the source folders</li>
+ * <li>Any change to .so file inside the lib (native library) folder</li>
+ * </ul>
+ */
+public class ApkDeltaVisitor extends BaseDeltaVisitor
+ implements IResourceDeltaVisitor {
+
+ /**
+ * compile flag. This is set to true if one of the changed/added/removed
+ * file is a .class file. Upon visiting all the delta resources, if this
+ * flag is true, then we know we'll have to make the "classes.dex" file.
+ */
+ private boolean mConvertToDex = false;
+
+ /**
+ * compile flag. This is set to true if one of the changed/added/removed
+ * file is a resource file. Upon visiting all the delta resources, if
+ * this flag is true, then we know we'll have to make the intermediate
+ * apk file.
+ */
+ private boolean mPackageResources = false;
+
+ /**
+ * Final package flag. This is set to true if one of the changed/added/removed
+ * file is a non java file (or aidl) in the resource folder. Upon visiting all the
+ * delta resources, if this flag is true, then we know we'll have to make the final
+ * package.
+ */
+ private boolean mMakeFinalPackage = false;
+
+ /** List of source folders. */
+ private ArrayList<IPath> mSourceFolders;
+
+ private IPath mOutputPath;
+
+ private IPath mAssetPath;
+
+ private IPath mResPath;
+
+ private IPath mLibFolder;
+
+ /**
+ * Builds the object with a specified output folder.
+ * @param builder the xml builder using this object to visit the
+ * resource delta.
+ * @param sourceFolders the list of source folders for the project, relative to the workspace.
+ * @param outputfolder the output folder of the project.
+ */
+ public ApkDeltaVisitor(BaseBuilder builder, ArrayList<IPath> sourceFolders,
+ IFolder outputfolder) {
+ super(builder);
+ mSourceFolders = sourceFolders;
+
+ if (outputfolder != null) {
+ mOutputPath = outputfolder.getFullPath();
+ }
+
+ IResource assetFolder = builder.getProject().findMember(SdkConstants.FD_ASSETS);
+ if (assetFolder != null) {
+ mAssetPath = assetFolder.getFullPath();
+ }
+
+ IResource resFolder = builder.getProject().findMember(SdkConstants.FD_RESOURCES);
+ if (resFolder != null) {
+ mResPath = resFolder.getFullPath();
+ }
+
+ IResource libFolder = builder.getProject().findMember(SdkConstants.FD_NATIVE_LIBS);
+ if (libFolder != null) {
+ mLibFolder = libFolder.getFullPath();
+ }
+ }
+
+ public boolean getConvertToDex() {
+ return mConvertToDex;
+ }
+
+ public boolean getPackageResources() {
+ return mPackageResources;
+ }
+
+ public boolean getMakeFinalPackage() {
+ return mMakeFinalPackage;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws CoreException
+ *
+ * @see org.eclipse.core.resources.IResourceDeltaVisitor
+ * #visit(org.eclipse.core.resources.IResourceDelta)
+ */
+ public boolean visit(IResourceDelta delta) throws CoreException {
+ // if all flags are true, we can stop going through the resource delta.
+ if (mConvertToDex && mPackageResources && mMakeFinalPackage) {
+ return false;
+ }
+
+ // we are only going to look for changes in res/, src/ and in
+ // AndroidManifest.xml since the delta visitor goes through the main
+ // folder before its childre we can check when the path segment
+ // count is 2 (format will be /$Project/folder) and make sure we are
+ // processing res/, src/ or AndroidManifest.xml
+ IResource resource = delta.getResource();
+ IPath path = resource.getFullPath();
+ String[] pathSegments = path.segments();
+ int type = resource.getType();
+
+ // since the delta visitor also visits the root we return true if
+ // segments.length = 1
+ if (pathSegments.length == 1) {
+ return true;
+ }
+
+ // check the manifest.
+ if (pathSegments.length == 2 &&
+ AndroidConstants.FN_ANDROID_MANIFEST.equalsIgnoreCase(pathSegments[1])) {
+ // if the manifest changed we have to repackage the
+ // resources.
+ mPackageResources = true;
+ mMakeFinalPackage = true;
+
+ // we don't want to go to the children, not like they are
+ // any for this resource anyway.
+ return false;
+ }
+
+ // check the other folders.
+ if (mOutputPath != null && mOutputPath.isPrefixOf(path)) {
+ // a resource changed inside the output folder.
+ if (type == IResource.FILE) {
+ // just check this is a .class file. Any modification will
+ // trigger a change in the classes.dex file
+ String ext = resource.getFileExtension();
+ if (AndroidConstants.EXT_CLASS.equalsIgnoreCase(ext)) {
+ mConvertToDex = true;
+ mMakeFinalPackage = true;
+
+ // no need to check the children, as we are in a package
+ // and there can only be subpackage children containing
+ // only .class files
+ return false;
+ }
+
+ // check for a few files directly in the output folder and force
+ // rebuild if they have been deleted.
+ if (delta.getKind() == IResourceDelta.REMOVED) {
+ IPath parentPath = path.removeLastSegments(1);
+ if (mOutputPath.equals(parentPath)) {
+ String resourceName = resource.getName();
+ // check if classes.dex was removed
+ if (resourceName.equalsIgnoreCase(AndroidConstants.FN_CLASSES_DEX)) {
+ mConvertToDex = true;
+ mMakeFinalPackage = true;
+ } else if (resourceName.equalsIgnoreCase(
+ AndroidConstants.FN_RESOURCES_AP_) ||
+ AndroidConstants.PATTERN_RESOURCES_S_AP_.matcher(
+ resourceName).matches()) {
+ // or if the default resources.ap_ or a configured version
+ // (resources-###.ap_) was removed.
+ mPackageResources = true;
+ mMakeFinalPackage = true;
+ }
+ }
+ }
+ }
+
+ // if this is a folder, we only go visit it if we don't already know
+ // that we need to convert to dex already.
+ return mConvertToDex == false;
+ } else if (mResPath != null && mResPath.isPrefixOf(path)) {
+ // in the res folder we are looking for any file modification
+ // (we don't care about folder being added/removed, only content
+ // is important)
+ if (type == IResource.FILE) {
+ mPackageResources = true;
+ mMakeFinalPackage = true;
+ return false;
+ }
+
+ // for folders, return true only if we don't already know we have to
+ // package the resources.
+ return mPackageResources == false;
+ } else if (mAssetPath != null && mAssetPath.isPrefixOf(path)) {
+ // this is the assets folder that was modified.
+ // we don't care what content was changed. All we care
+ // about is that something changed inside. No need to visit
+ // the children even.
+ mPackageResources = true;
+ mMakeFinalPackage = true;
+ return false;
+ } else if (mLibFolder != null && mLibFolder.isPrefixOf(path)) {
+ // inside the native library folder. Test if the changed resource is a .so file.
+ if (type == IResource.FILE &&
+ path.getFileExtension().equalsIgnoreCase(AndroidConstants.EXT_NATIVE_LIB)) {
+ mMakeFinalPackage = true;
+ return false; // return false for file.
+ }
+
+ // for folders, return true only if we don't already know we have to make the
+ // final package.
+ return mMakeFinalPackage == false;
+ } else {
+ // we are in a folder that is neither the resource folders, nor the output.
+ // check against all the source folders, unless we already know we need to do
+ // the final package.
+ // This could be a source folder or a folder leading to a source folder.
+ // However we only check this if we don't already know that we need to build the
+ // package anyway
+ if (mMakeFinalPackage == false) {
+ for (IPath sourcePath : mSourceFolders) {
+ if (sourcePath.isPrefixOf(path)) {
+ // In the source folders, we are looking for any kind of
+ // modification related to file that are not java files.
+ // Also excluded are aidl files, and package.html files
+ if (type == IResource.FOLDER) {
+ // always visit the subfolders, unless the folder is not to be included
+ return ApkBuilder.checkFolderForPackaging((IFolder)resource);
+ } else if (type == IResource.FILE) {
+ if (ApkBuilder.checkFileForPackaging((IFile)resource)) {
+ mMakeFinalPackage = true;
+ }
+
+ return false;
+ }
+
+ }
+ }
+ }
+ }
+
+ // if the folder is not inside one of the folders we are interested in (res, assets, output,
+ // source folders), it could be a folder leading to them, so we return true.
+ return true;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/BaseBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/BaseBuilder.java
new file mode 100644
index 0000000..e2e9728
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/BaseBuilder.java
@@ -0,0 +1,929 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.build;
+
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.project.ProjectHelper;
+import com.android.ide.eclipse.adt.sdk.LoadStatus;
+import com.android.ide.eclipse.adt.sdk.Sdk;
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.common.project.BaseProjectHelper;
+import com.android.ide.eclipse.common.project.XmlErrorHandler;
+import com.android.ide.eclipse.common.project.XmlErrorHandler.XmlErrorListener;
+import com.android.sdklib.IAndroidTarget;
+
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.IncrementalProjectBuilder;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jdt.core.IClasspathEntry;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+import org.xml.sax.SAXException;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+/**
+ * Base builder for XML files. This class allows for basic XML parsing with
+ * error checking and marking the files for errors/warnings.
+ */
+abstract class BaseBuilder extends IncrementalProjectBuilder {
+
+ // TODO: rename the pattern to something that makes sense + javadoc comments.
+
+ /**
+ * Single line aapt warning for skipping files.<br>
+ * " (skipping hidden file '&lt;file path&gt;'"
+ */
+ private final static Pattern sPattern0Line1 = Pattern.compile(
+ "^\\s+\\(skipping hidden file\\s'(.*)'\\)$"); //$NON-NLS-1$
+
+ /**
+ * First line of dual line aapt error.<br>
+ * "ERROR at line &lt;line&gt;: &lt;error&gt;"<br>
+ * " (Occurred while parsing &lt;path&gt;)"
+ */
+ private final static Pattern sPattern1Line1 = Pattern.compile(
+ "^ERROR\\s+at\\s+line\\s+(\\d+):\\s+(.*)$"); //$NON-NLS-1$
+ /**
+ * Second line of dual line aapt error.<br>
+ * "ERROR at line &lt;line&gt;: &lt;error&gt;"<br>
+ * " (Occurred while parsing &lt;path&gt;)"<br>
+ * @see #sPattern1Line1
+ */
+ private final static Pattern sPattern1Line2 = Pattern.compile(
+ "^\\s+\\(Occurred while parsing\\s+(.*)\\)$"); //$NON-NLS-1$
+ /**
+ * First line of dual line aapt error.<br>
+ * "ERROR: &lt;error&gt;"<br>
+ * "Defined at file &lt;path&gt; line &lt;line&gt;"
+ */
+ private final static Pattern sPattern2Line1 = Pattern.compile(
+ "^ERROR:\\s+(.+)$"); //$NON-NLS-1$
+ /**
+ * Second line of dual line aapt error.<br>
+ * "ERROR: &lt;error&gt;"<br>
+ * "Defined at file &lt;path&gt; line &lt;line&gt;"<br>
+ * @see #sPattern2Line1
+ */
+ private final static Pattern sPattern2Line2 = Pattern.compile(
+ "Defined\\s+at\\s+file\\s+(.+)\\s+line\\s+(\\d+)"); //$NON-NLS-1$
+ /**
+ * Single line aapt error<br>
+ * "&lt;path&gt; line &lt;line&gt;: &lt;error&gt;"
+ */
+ private final static Pattern sPattern3Line1 = Pattern.compile(
+ "^(.+)\\sline\\s(\\d+):\\s(.+)$"); //$NON-NLS-1$
+ /**
+ * First line of dual line aapt error.<br>
+ * "ERROR parsing XML file &lt;path&gt;"<br>
+ * "&lt;error&gt; at line &lt;line&gt;"
+ */
+ private final static Pattern sPattern4Line1 = Pattern.compile(
+ "^Error\\s+parsing\\s+XML\\s+file\\s(.+)$"); //$NON-NLS-1$
+ /**
+ * Second line of dual line aapt error.<br>
+ * "ERROR parsing XML file &lt;path&gt;"<br>
+ * "&lt;error&gt; at line &lt;line&gt;"<br>
+ * @see #sPattern4Line1
+ */
+ private final static Pattern sPattern4Line2 = Pattern.compile(
+ "^(.+)\\s+at\\s+line\\s+(\\d+)$"); //$NON-NLS-1$
+
+ /**
+ * Single line aapt warning<br>
+ * "&lt;path&gt;:&lt;line&gt;: &lt;error&gt;"
+ */
+ private final static Pattern sPattern5Line1 = Pattern.compile(
+ "^(.+?):(\\d+):\\s+WARNING:(.+)$"); //$NON-NLS-1$
+
+ /**
+ * Single line aapt error<br>
+ * "&lt;path&gt;:&lt;line&gt;: &lt;error&gt;"
+ */
+ private final static Pattern sPattern6Line1 = Pattern.compile(
+ "^(.+?):(\\d+):\\s+(.+)$"); //$NON-NLS-1$
+
+ /**
+ * 4 line aapt error<br>
+ * "ERROR: 9-path image &lt;path&gt; malformed"<br>
+ * Line 2 and 3 are taken as-is while line 4 is ignored (it repeats with<br>
+ * 'ERROR: failure processing &lt;path&gt;)
+ */
+ private final static Pattern sPattern7Line1 = Pattern.compile(
+ "^ERROR:\\s+9-patch\\s+image\\s+(.+)\\s+malformed\\.$"); //$NON-NLS-1$
+
+ private final static Pattern sPattern8Line1 = Pattern.compile(
+ "^(invalid resource directory name): (.*)$"); //$NON-NLS-1$
+
+ /**
+ * 2 line aapt error<br>
+ * "ERROR: Invalid configuration: foo"<br>
+ * " ^^^"<br>
+ * There's no need to parse the 2nd line.
+ */
+ private final static Pattern sPattern9Line1 = Pattern.compile(
+ "^Invalid configuration: (.+)$"); //$NON-NLS-1$
+
+ /** SAX Parser factory. */
+ private SAXParserFactory mParserFactory;
+
+ /**
+ * Base Resource Delta Visitor to handle XML error
+ */
+ protected static class BaseDeltaVisitor implements XmlErrorListener {
+
+ /** The Xml builder used to validate XML correctness. */
+ protected BaseBuilder mBuilder;
+
+ /**
+ * XML error flag. if true, we keep parsing the ResourceDelta but the
+ * compilation will not happen (we're putting markers)
+ */
+ public boolean mXmlError = false;
+
+ public BaseDeltaVisitor(BaseBuilder builder) {
+ mBuilder = builder;
+ }
+
+ /**
+ * Finds a matching Source folder for the current path. This checkds if the current path
+ * leads to, or is a source folder.
+ * @param sourceFolders The list of source folders
+ * @param pathSegments The segments of the current path
+ * @return The segments of the source folder, or null if no match was found
+ */
+ protected static String[] findMatchingSourceFolder(ArrayList<IPath> sourceFolders,
+ String[] pathSegments) {
+
+ for (IPath p : sourceFolders) {
+ // check if we are inside one of those source class path
+
+ // get the segments
+ String[] srcSegments = p.segments();
+
+ // compare segments. We want the path of the resource
+ // we're visiting to be
+ boolean valid = true;
+ int segmentCount = pathSegments.length;
+
+ for (int i = 0 ; i < segmentCount; i++) {
+ String s1 = pathSegments[i];
+ String s2 = srcSegments[i];
+
+ if (s1.equalsIgnoreCase(s2) == false) {
+ valid = false;
+ break;
+ }
+ }
+
+ if (valid) {
+ // this folder, or one of this children is a source
+ // folder!
+ // we return its segments
+ return srcSegments;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Sent when an XML error is detected.
+ * @see XmlErrorListener
+ */
+ public void errorFound() {
+ mXmlError = true;
+ }
+ }
+
+ public BaseBuilder() {
+ super();
+ mParserFactory = SAXParserFactory.newInstance();
+
+ // FIXME when the compiled XML support for namespace is in, set this to true.
+ mParserFactory.setNamespaceAware(false);
+ }
+
+ /**
+ * Checks an Xml file for validity. Errors/warnings will be marked on the
+ * file
+ * @param resource the resource to check
+ * @param visitor a valid resource delta visitor
+ */
+ protected final void checkXML(IResource resource, BaseDeltaVisitor visitor) {
+
+ // first make sure this is an xml file
+ if (resource instanceof IFile) {
+ IFile file = (IFile)resource;
+
+ // remove previous markers
+ removeMarkersFromFile(file, AndroidConstants.MARKER_XML);
+
+ // create the error handler
+ XmlErrorHandler reporter = new XmlErrorHandler(file, visitor);
+ try {
+ // parse
+ getParser().parse(file.getContents(), reporter);
+ } catch (Exception e1) {
+ }
+ }
+ }
+
+ /**
+ * Returns the SAXParserFactory, instantiating it first if it's not already
+ * created.
+ * @return the SAXParserFactory object
+ * @throws ParserConfigurationException
+ * @throws SAXException
+ */
+ protected final SAXParser getParser() throws ParserConfigurationException,
+ SAXException {
+ return mParserFactory.newSAXParser();
+ }
+
+ /**
+ * Adds a marker to the current project.
+ *
+ * @param markerId The id of the marker to add.
+ * @param message the message associated with the mark
+ * @param severity the severity of the marker.
+ */
+ protected final void markProject(String markerId, String message, int severity) {
+ BaseProjectHelper.addMarker(getProject(), markerId, message, severity);
+ }
+
+
+ /**
+ * Removes markers from a file.
+ * @param file The file from which to delete the markers.
+ * @param markerId The id of the markers to remove. If null, all marker of
+ * type <code>IMarker.PROBLEM</code> will be removed.
+ */
+ protected final void removeMarkersFromFile(IFile file, String markerId) {
+ try {
+ if (file.exists()) {
+ file.deleteMarkers(markerId, true, IResource.DEPTH_ZERO);
+ }
+ } catch (CoreException ce) {
+ String msg = String.format(Messages.Marker_Delete_Error, markerId, file.toString());
+ AdtPlugin.printErrorToConsole(getProject(), msg);
+ }
+ }
+
+ /**
+ * Removes markers from a container and its children.
+ * @param folder The container from which to delete the markers.
+ * @param markerId The id of the markers to remove. If null, all marker of
+ * type <code>IMarker.PROBLEM</code> will be removed.
+ */
+ protected final void removeMarkersFromContainer(IContainer folder, String markerId) {
+ try {
+ if (folder.exists()) {
+ folder.deleteMarkers(markerId, true, IResource.DEPTH_INFINITE);
+ }
+ } catch (CoreException ce) {
+ String msg = String.format(Messages.Marker_Delete_Error, markerId, folder.toString());
+ AdtPlugin.printErrorToConsole(getProject(), msg);
+ }
+ }
+
+ /**
+ * Removes markers from a project and its children.
+ * @param project The project from which to delete the markers
+ * @param markerId The id of the markers to remove. If null, all marker of
+ * type <code>IMarker.PROBLEM</code> will be removed.
+ */
+ protected final static void removeMarkersFromProject(IProject project,
+ String markerId) {
+ try {
+ if (project.exists()) {
+ project.deleteMarkers(markerId, true, IResource.DEPTH_INFINITE);
+ }
+ } catch (CoreException ce) {
+ String msg = String.format(Messages.Marker_Delete_Error, markerId, project.getName());
+ AdtPlugin.printErrorToConsole(project, msg);
+ }
+ }
+
+ /**
+ * Get the stderr output of a process and return when the process is done.
+ * @param process The process to get the ouput from
+ * @param results The array to store the stderr output
+ * @return the process return code.
+ * @throws InterruptedException
+ */
+ protected final int grabProcessOutput(final Process process,
+ final ArrayList<String> results)
+ throws InterruptedException {
+ // Due to the limited buffer size on windows for the standard io (stderr, stdout), we
+ // *need* to read both stdout and stderr all the time. If we don't and a process output
+ // a large amount, this could deadlock the process.
+
+ // read the lines as they come. if null is returned, it's
+ // because the process finished
+ new Thread("") { //$NON-NLS-1$
+ @Override
+ public void run() {
+ // create a buffer to read the stderr output
+ InputStreamReader is = new InputStreamReader(process.getErrorStream());
+ BufferedReader errReader = new BufferedReader(is);
+
+ try {
+ while (true) {
+ String line = errReader.readLine();
+ if (line != null) {
+ results.add(line);
+ } else {
+ break;
+ }
+ }
+ } catch (IOException e) {
+ // do nothing.
+ }
+ }
+ }.start();
+
+ new Thread("") { //$NON-NLS-1$
+ @Override
+ public void run() {
+ InputStreamReader is = new InputStreamReader(process.getInputStream());
+ BufferedReader outReader = new BufferedReader(is);
+
+ IProject project = getProject();
+
+ try {
+ while (true) {
+ String line = outReader.readLine();
+ if (line != null) {
+ AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE,
+ project, line);
+ } else {
+ break;
+ }
+ }
+ } catch (IOException e) {
+ // do nothing.
+ }
+ }
+
+ }.start();
+
+ // get the return code from the process
+ return process.waitFor();
+ }
+
+ /**
+ * Parse the output of aapt and mark the incorrect file with error markers
+ *
+ * @param results the output of aapt
+ * @param project the project containing the file to mark
+ * @return true if the parsing failed, false if success.
+ */
+ protected final boolean parseAaptOutput(ArrayList<String> results,
+ IProject project) {
+ // nothing to parse? just return false;
+ if (results.size() == 0) {
+ return false;
+ }
+
+ // get the root of the project so that we can make IFile from full
+ // file path
+ String osRoot = project.getLocation().toOSString();
+
+ Matcher m;
+
+ for (int i = 0; i < results.size(); i++) {
+ String p = results.get(i);
+
+ m = sPattern0Line1.matcher(p);
+ if (m.matches()) {
+ // we ignore those (as this is an ignore message from aapt)
+ continue;
+ }
+
+ m = sPattern1Line1.matcher(p);
+ if (m.matches()) {
+ String lineStr = m.group(1);
+ String msg = m.group(2);
+
+ // get the matcher for the next line.
+ m = getNextLineMatcher(results, ++i, sPattern1Line2);
+ if (m == null) {
+ return true;
+ }
+
+ String location = m.group(1);
+
+ // check the values and attempt to mark the file.
+ if (checkAndMark(location, lineStr, msg, osRoot, project,
+ AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) {
+ return true;
+ }
+ continue;
+ }
+
+ // this needs to be tested before Pattern2 since they both start with 'ERROR:'
+ m = sPattern7Line1.matcher(p);
+ if (m.matches()) {
+ String location = m.group(1);
+ String msg = p; // default msg is the line in case we don't find anything else
+
+ if (++i < results.size()) {
+ msg = results.get(i).trim();
+ if (++i < results.size()) {
+ msg = msg + " - " + results.get(i).trim(); //$NON-NLS-1$
+
+ // skip the next line
+ i++;
+ }
+ }
+
+ // display the error
+ if (checkAndMark(location, null, msg, osRoot, project,
+ AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) {
+ return true;
+ }
+
+ // success, go to the next line
+ continue;
+ }
+
+ m = sPattern2Line1.matcher(p);
+ if (m.matches()) {
+ // get the msg
+ String msg = m.group(1);
+
+ // get the matcher for the next line.
+ m = getNextLineMatcher(results, ++i, sPattern2Line2);
+ if (m == null) {
+ return true;
+ }
+
+ String location = m.group(1);
+ String lineStr = m.group(2);
+
+ // check the values and attempt to mark the file.
+ if (checkAndMark(location, lineStr, msg, osRoot, project,
+ AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) {
+ return true;
+ }
+ continue;
+ }
+
+ m = sPattern3Line1.matcher(p);
+ if (m.matches()) {
+ String location = m.group(1);
+ String lineStr = m.group(2);
+ String msg = m.group(3);
+
+ // check the values and attempt to mark the file.
+ if (checkAndMark(location, lineStr, msg, osRoot, project,
+ AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) {
+ return true;
+ }
+
+ // success, go to the next line
+ continue;
+ }
+
+ m = sPattern4Line1.matcher(p);
+ if (m.matches()) {
+ // get the filename.
+ String location = m.group(1);
+
+ // get the matcher for the next line.
+ m = getNextLineMatcher(results, ++i, sPattern4Line2);
+ if (m == null) {
+ return true;
+ }
+
+ String msg = m.group(1);
+ String lineStr = m.group(2);
+
+ // check the values and attempt to mark the file.
+ if (checkAndMark(location, lineStr, msg, osRoot, project,
+ AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) {
+ return true;
+ }
+
+ // success, go to the next line
+ continue;
+ }
+
+ m = sPattern5Line1.matcher(p);
+ if (m.matches()) {
+ String location = m.group(1);
+ String lineStr = m.group(2);
+ String msg = m.group(3);
+
+ // check the values and attempt to mark the file.
+ if (checkAndMark(location, lineStr, msg, osRoot, project,
+ AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_WARNING) == false) {
+ return true;
+ }
+
+ // success, go to the next line
+ continue;
+ }
+
+ m = sPattern6Line1.matcher(p);
+ if (m.matches()) {
+ String location = m.group(1);
+ String lineStr = m.group(2);
+ String msg = m.group(3);
+
+ // check the values and attempt to mark the file.
+ if (checkAndMark(location, lineStr, msg, osRoot, project,
+ AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) {
+ return true;
+ }
+
+ // success, go to the next line
+ continue;
+ }
+
+ m = sPattern8Line1.matcher(p);
+ if (m.matches()) {
+ String location = m.group(2);
+ String msg = m.group(1);
+
+ // check the values and attempt to mark the file.
+ if (checkAndMark(location, null, msg, osRoot, project,
+ AndroidConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) {
+ return true;
+ }
+
+ // success, go to the next line
+ continue;
+ }
+
+ m = sPattern9Line1.matcher(p);
+ if (m.matches()) {
+ String badConfig = m.group(1);
+ String msg = String.format("APK Configuration filter '%1$s' is invalid", badConfig);
+
+ // skip the next line
+ i++;
+
+ // check the values and attempt to mark the file.
+ if (checkAndMark(null /*location*/, null, msg, osRoot, project,
+ AndroidConstants.MARKER_AAPT_PACKAGE, IMarker.SEVERITY_ERROR) == false) {
+ return true;
+ }
+
+ // success, go to the next line
+ continue;
+ }
+
+ // invalid line format, flag as error, and bail
+ return true;
+ }
+
+ return false;
+ }
+
+
+
+ /**
+ * Saves a String property into the persistent storage of the project.
+ * @param propertyName the name of the property. The id of the plugin is added to this string.
+ * @param value the value to save
+ * @return true if the save succeeded.
+ */
+ protected boolean saveProjectStringProperty(String propertyName, String value) {
+ IProject project = getProject();
+ return ProjectHelper.saveStringProperty(project, propertyName, value);
+ }
+
+
+ /**
+ * Loads a String property from the persistent storage of the project.
+ * @param propertyName the name of the property. The id of the plugin is added to this string.
+ * @return the property value or null if it was not found.
+ */
+ protected String loadProjectStringProperty(String propertyName) {
+ IProject project = getProject();
+ return ProjectHelper.loadStringProperty(project, propertyName);
+ }
+
+ /**
+ * Saves a property into the persistent storage of the project.
+ * @param propertyName the name of the property. The id of the plugin is added to this string.
+ * @param value the value to save
+ * @return true if the save succeeded.
+ */
+ protected boolean saveProjectBooleanProperty(String propertyName, boolean value) {
+ IProject project = getProject();
+ return ProjectHelper.saveStringProperty(project, propertyName, Boolean.toString(value));
+ }
+
+ /**
+ * Loads a boolean property from the persistent storage of the project.
+ * @param propertyName the name of the property. The id of the plugin is added to this string.
+ * @param defaultValue The default value to return if the property was not found.
+ * @return the property value or the default value if the property was not found.
+ */
+ protected boolean loadProjectBooleanProperty(String propertyName, boolean defaultValue) {
+ IProject project = getProject();
+ return ProjectHelper.loadBooleanProperty(project, propertyName, defaultValue);
+ }
+
+ /**
+ * Saves the path of a resource into the persistent storate of the project.
+ * @param propertyName the name of the property. The id of the plugin is added to this string.
+ * @param resource the resource which path is saved.
+ * @return true if the save succeeded
+ */
+ protected boolean saveProjectResourceProperty(String propertyName, IResource resource) {
+ return ProjectHelper.saveResourceProperty(getProject(), propertyName, resource);
+ }
+
+ /**
+ * Loads the path of a resource from the persistent storage of the project, and returns the
+ * corresponding IResource object.
+ * @param propertyName the name of the property. The id of the plugin is added to this string.
+ * @return The corresponding IResource object (or children interface) or null
+ */
+ protected IResource loadProjectResourceProperty(String propertyName) {
+ IProject project = getProject();
+ return ProjectHelper.loadResourceProperty(project, propertyName);
+ }
+
+ /**
+ * Check if the parameters gotten from the error output are valid, and mark
+ * the file with an AAPT marker.
+ * @param location the full OS path of the error file. If null, the project is marked
+ * @param lineStr
+ * @param message
+ * @param root The root directory of the project, in OS specific format.
+ * @param project
+ * @param markerId The marker id to put.
+ * @param severity The severity of the marker to put (IMarker.SEVERITY_*)
+ * @return true if the parameters were valid and the file was marked successfully.
+ *
+ * @see IMarker
+ */
+ private final boolean checkAndMark(String location, String lineStr,
+ String message, String root, IProject project, String markerId, int severity) {
+ // check this is in fact a file
+ if (location != null) {
+ File f = new File(location);
+ if (f.exists() == false) {
+ return false;
+ }
+ }
+
+ // get the line number
+ int line = -1; // default value for error with no line.
+
+ if (lineStr != null) {
+ try {
+ line = Integer.parseInt(lineStr);
+ } catch (NumberFormatException e) {
+ // looks like the string we extracted wasn't a valid
+ // file number. Parsing failed and we return true
+ return false;
+ }
+ }
+
+ // add the marker
+ IResource f2 = project;
+ if (location != null) {
+ f2 = getResourceFromFullPath(location, root, project);
+ if (f2 == null) {
+ return false;
+ }
+ }
+
+ // check if there's a similar marker already, since aapt is launched twice
+ boolean markerAlreadyExists = false;
+ try {
+ IMarker[] markers = f2.findMarkers(markerId, true, IResource.DEPTH_ZERO);
+
+ for (IMarker marker : markers) {
+ int tmpLine = marker.getAttribute(IMarker.LINE_NUMBER, -1);
+ if (tmpLine != line) {
+ break;
+ }
+
+ int tmpSeverity = marker.getAttribute(IMarker.SEVERITY, -1);
+ if (tmpSeverity != severity) {
+ break;
+ }
+
+ String tmpMsg = marker.getAttribute(IMarker.MESSAGE, null);
+ if (tmpMsg == null || tmpMsg.equals(message) == false) {
+ break;
+ }
+
+ // if we're here, all the marker attributes are equals, we found it
+ // and exit
+ markerAlreadyExists = true;
+ break;
+ }
+
+ } catch (CoreException e) {
+ // if we couldn't get the markers, then we just mark the file again
+ // (since markerAlreadyExists is initialized to false, we do nothing)
+ }
+
+ if (markerAlreadyExists == false) {
+ if (line != -1) {
+ BaseProjectHelper.addMarker(f2, markerId, message, line,
+ severity);
+ } else {
+ BaseProjectHelper.addMarker(f2, markerId, message, severity);
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns a matching matcher for the next line
+ * @param lines The array of lines
+ * @param nextIndex The index of the next line
+ * @param pattern The pattern to match
+ * @return null if error or no match, the matcher otherwise.
+ */
+ private final Matcher getNextLineMatcher(ArrayList<String> lines,
+ int nextIndex, Pattern pattern) {
+ // unless we can't, because we reached the last line
+ if (nextIndex == lines.size()) {
+ // we expected a 2nd line, so we flag as error
+ // and we bail
+ return null;
+ }
+
+ Matcher m = pattern.matcher(lines.get(nextIndex));
+ if (m.matches()) {
+ return m;
+ }
+
+ return null;
+ }
+
+ private IResource getResourceFromFullPath(String filename, String root,
+ IProject project) {
+ if (filename.startsWith(root)) {
+ String file = filename.substring(root.length());
+
+ // get the resource
+ IResource r = project.findMember(file);
+
+ // if the resource is valid, we add the marker
+ if (r.exists()) {
+ return r;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns an array of external jar files used by the project.
+ * @return an array of OS-specific absolute file paths
+ */
+ protected final String[] getExternalJars() {
+ // get the current project
+ IProject project = getProject();
+
+ // get a java project from it
+ IJavaProject javaProject = JavaCore.create(project);
+
+ IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot();
+
+ ArrayList<String> oslibraryList = new ArrayList<String>();
+ IClasspathEntry[] classpaths = javaProject.readRawClasspath();
+ if (classpaths != null) {
+ for (IClasspathEntry e : classpaths) {
+ if (e.getEntryKind() == IClasspathEntry.CPE_LIBRARY ||
+ e.getEntryKind() == IClasspathEntry.CPE_VARIABLE) {
+ // if this is a classpath variable reference, we resolve it.
+ if (e.getEntryKind() == IClasspathEntry.CPE_VARIABLE) {
+ e = JavaCore.getResolvedClasspathEntry(e);
+ }
+
+ // get the IPath
+ IPath path = e.getPath();
+
+ // check the name ends with .jar
+ if (AndroidConstants.EXT_JAR.equalsIgnoreCase(path.getFileExtension())) {
+ boolean local = false;
+ IResource resource = wsRoot.findMember(path);
+ if (resource != null && resource.exists() &&
+ resource.getType() == IResource.FILE) {
+ local = true;
+ oslibraryList.add(resource.getLocation().toOSString());
+ }
+
+ if (local == false) {
+ // if the jar path doesn't match a workspace resource,
+ // then we get an OSString and check if this links to a valid file.
+ String osFullPath = path.toOSString();
+
+ File f = new File(osFullPath);
+ if (f.exists()) {
+ oslibraryList.add(osFullPath);
+ } else {
+ String message = String.format( Messages.Couldnt_Locate_s_Error,
+ path);
+ AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE,
+ project, message);
+
+ // Also put a warning marker on the project
+ markProject(AdtConstants.MARKER_ADT, message,
+ IMarker.SEVERITY_WARNING);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return oslibraryList.toArray(new String[oslibraryList.size()]);
+ }
+
+ /**
+ * Aborts the build if the SDK/project setups are broken. This does not
+ * display any errors.
+ *
+ * @param project The {@link IJavaProject} being compiled.
+ * @throws CoreException
+ */
+ protected final void abortOnBadSetup(IProject project) throws CoreException {
+ // check if we have finished loading the SDK.
+ if (AdtPlugin.getDefault().getSdkLoadStatus() != LoadStatus.LOADED) {
+ // we exit silently
+ stopBuild("SDK is not loaded yet");
+ }
+
+ // check the compiler compliance level.
+ if (ProjectHelper.checkCompilerCompliance(project) !=
+ ProjectHelper.COMPILER_COMPLIANCE_OK) {
+ // we exit silently
+ stopBuild(Messages.Compiler_Compliance_Error);
+ }
+
+ // Check that the SDK directory has been setup.
+ String osSdkFolder = AdtPlugin.getOsSdkFolder();
+
+ if (osSdkFolder == null || osSdkFolder.length() == 0) {
+ stopBuild(Messages.No_SDK_Setup_Error);
+ }
+
+ IAndroidTarget projectTarget = Sdk.getCurrent().getTarget(project);
+ if (projectTarget == null) {
+ // no target. error has been output by the container initializer:
+ // exit silently.
+ stopBuild("Project has no target");
+ }
+ }
+
+ /**
+ * Throws an exception to cancel the build.
+ *
+ * @param error the error message
+ * @param args the printf-style arguments to the error message.
+ * @throws CoreException
+ */
+ protected final void stopBuild(String error, Object... args) throws CoreException {
+ throw new CoreException(new Status(IStatus.CANCEL, AdtPlugin.PLUGIN_ID,
+ String.format(error, args)));
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/DexWrapper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/DexWrapper.java
new file mode 100644
index 0000000..65ad4f5
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/DexWrapper.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.build;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+
+import java.io.File;
+import java.io.PrintStream;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+
+/**
+ * Wrapper to access dex.jar through reflection.
+ * <p/>Since there is no proper api to call the method in the dex library, this wrapper is going
+ * to access it through reflection.
+ */
+public final class DexWrapper {
+
+ private final static String DEX_MAIN = "com.android.dx.command.dexer.Main"; //$NON-NLS-1$
+ private final static String DEX_CONSOLE = "com.android.dx.command.DxConsole"; //$NON-NLS-1$
+ private final static String DEX_ARGS = "com.android.dx.command.dexer.Main$Arguments"; //$NON-NLS-1$
+
+ private final static String MAIN_RUN = "run"; //$NON-NLS-1$
+
+ private Method mRunMethod;
+
+ private Constructor<?> mArgConstructor;
+ private Field mArgOutName;
+ private Field mArgVerbose;
+ private Field mArgJarOutput;
+ private Field mArgFileNames;
+
+ private Field mConsoleOut;
+ private Field mConsoleErr;
+
+ /**
+ * Loads the dex library from a file path.
+ *
+ * The loaded library can be used via
+ * {@link DexWrapper#run(String, String[], boolean, PrintStream, PrintStream)}.
+ *
+ * @param osFilepath the location of the dex.jar file.
+ * @return an IStatus indicating the result of the load.
+ */
+ public synchronized IStatus loadDex(String osFilepath) {
+ try {
+ File f = new File(osFilepath);
+ if (f.isFile() == false) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, String.format(
+ Messages.DexWrapper_s_does_not_exists, osFilepath));
+ }
+ URL url = f.toURL();
+
+ URLClassLoader loader = new URLClassLoader(new URL[] { url },
+ DexWrapper.class.getClassLoader());
+
+ // get the classes.
+ Class<?> mainClass = loader.loadClass(DEX_MAIN);
+ Class<?> consoleClass = loader.loadClass(DEX_CONSOLE);
+ Class<?> argClass = loader.loadClass(DEX_ARGS);
+
+ try {
+ // now get the fields/methods we need
+ mRunMethod = mainClass.getMethod(MAIN_RUN, argClass);
+
+ mArgConstructor = argClass.getConstructor();
+ mArgOutName = argClass.getField("outName"); //$NON-NLS-1$
+ mArgJarOutput = argClass.getField("jarOutput"); //$NON-NLS-1$
+ mArgFileNames = argClass.getField("fileNames"); //$NON-NLS-1$
+ mArgVerbose = argClass.getField("verbose"); //$NON-NLS-1$
+
+ mConsoleOut = consoleClass.getField("out"); //$NON-NLS-1$
+ mConsoleErr = consoleClass.getField("err"); //$NON-NLS-1$
+
+ } catch (SecurityException e) {
+ return createErrorStatus(Messages.DexWrapper_SecuryEx_Unable_To_Find_API, e);
+ } catch (NoSuchMethodException e) {
+ return createErrorStatus(Messages.DexWrapper_SecuryEx_Unable_To_Find_Method, e);
+ } catch (NoSuchFieldException e) {
+ return createErrorStatus(Messages.DexWrapper_SecuryEx_Unable_To_Find_Field, e);
+ }
+
+ return Status.OK_STATUS;
+ } catch (MalformedURLException e) {
+ // really this should not happen.
+ return createErrorStatus(
+ String.format(Messages.DexWrapper_Failed_to_load_s, osFilepath), e);
+ } catch (ClassNotFoundException e) {
+ return createErrorStatus(
+ String.format(Messages.DexWrapper_Failed_to_load_s, osFilepath), e);
+ }
+ }
+
+ /**
+ * Runs the dex command.
+ * @param osOutFilePath the OS path to the outputfile (classes.dex
+ * @param osFilenames list of input source files (.class and .jar files)
+ * @param verbose verbose mode.
+ * @param outStream the stdout console
+ * @param errStream the stderr console
+ * @return the integer return code of com.android.dx.command.dexer.Main.run()
+ * @throws CoreException
+ */
+ public synchronized int run(String osOutFilePath, String[] osFilenames,
+ boolean verbose, PrintStream outStream, PrintStream errStream) throws CoreException {
+
+ try {
+ // set the stream
+ mConsoleErr.set(null /* obj: static field */, errStream);
+ mConsoleOut.set(null /* obj: static field */, outStream);
+
+ // create the Arguments object.
+ Object args = mArgConstructor.newInstance();
+ mArgOutName.set(args, osOutFilePath);
+ mArgFileNames.set(args, osFilenames);
+ mArgJarOutput.set(args, false);
+ mArgVerbose.set(args, verbose);
+
+ // call the run method
+ Object res = mRunMethod.invoke(null /* obj: static method */, args);
+
+ if (res instanceof Integer) {
+ return ((Integer)res).intValue();
+ }
+
+ return -1;
+ } catch (IllegalAccessException e) {
+ throw new CoreException(createErrorStatus(
+ String.format(Messages.DexWrapper_Unable_To_Execute_Dex_s, e.getMessage()), e));
+ } catch (InstantiationException e) {
+ throw new CoreException(createErrorStatus(
+ String.format(Messages.DexWrapper_Unable_To_Execute_Dex_s, e.getMessage()), e));
+ } catch (InvocationTargetException e) {
+ throw new CoreException(createErrorStatus(
+ String.format(Messages.DexWrapper_Unable_To_Execute_Dex_s, e.getMessage()), e));
+ }
+ }
+
+ private static IStatus createErrorStatus(String message, Exception e) {
+ AdtPlugin.log(e, message);
+ AdtPlugin.printErrorToConsole(Messages.DexWrapper_Dex_Loader, message);
+
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, message, e);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/Messages.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/Messages.java
new file mode 100644
index 0000000..0100049
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/Messages.java
@@ -0,0 +1,137 @@
+
+package com.android.ide.eclipse.adt.build;
+
+import org.eclipse.osgi.util.NLS;
+
+public class Messages extends NLS {
+ private static final String BUNDLE_NAME = "com.android.ide.eclipse.adt.build.build_messages"; //$NON-NLS-1$
+
+ public static String AAPT_Error;
+
+ public static String AAPT_Exec_Error;
+
+ public static String Added_s_s_Needs_Updating;
+
+ public static String AIDL_Exec_Error;
+
+ public static String AIDL_Java_Conflict;
+
+ public static String ApkBuilder_Certificate_Expired_on_s;
+
+ public static String ApkBuilder_JAVA_HOME_is_s;
+
+ public static String ApkBuilder_Packaging_s;
+
+ public static String ApkBuilder_Packaging_s_into_s;
+
+ public static String ApkBuilder_s_Conflict_with_file_s;
+
+ public static String ApkBuilder_Signing_Key_Creation_s;
+
+ public static String ApkBuilder_Unable_To_Gey_Key;
+
+ public static String ApkBuilder_UnableBuild_Dex_Not_loaded;
+
+ public static String ApkBuilder_Update_or_Execute_manually_s;
+
+ public static String ApkBuilder_Using_Default_Key;
+
+ public static String ApkBuilder_Using_s_To_Sign;
+
+ public static String Checking_Package_Change;
+
+ public static String Compiler_Compliance_Error;
+
+ public static String Couldnt_Locate_s_Error;
+
+ public static String Dalvik_Error_d;
+
+ public static String Dalvik_Error_s;
+
+ public static String Delete_Obsolete_Error;
+
+ public static String DexWrapper_Dex_Loader;
+
+ public static String DexWrapper_Failed_to_load_s;
+
+ public static String DexWrapper_s_does_not_exists;
+
+ public static String DexWrapper_SecuryEx_Unable_To_Find_API;
+
+ public static String DexWrapper_SecuryEx_Unable_To_Find_Field;
+
+ public static String DexWrapper_SecuryEx_Unable_To_Find_Method;
+
+ public static String DexWrapper_Unable_To_Execute_Dex_s;
+
+ public static String DX_Jar_Error;
+
+ public static String Failed_To_Get_Output;
+
+ public static String Final_Archive_Error_s;
+
+ public static String Incompatible_VM_Warning;
+
+ public static String Marker_Delete_Error;
+
+ public static String No_SDK_Setup_Error;
+
+ public static String Nothing_To_Compile;
+
+ public static String Output_Missing;
+
+ public static String Package_s_Doesnt_Exist_Error;
+
+ public static String Preparing_Generated_Files;
+
+ public static String Project_Has_Errors;
+
+ public static String Refreshing_Res;
+
+ public static String Removing_Generated_Classes;
+
+ public static String Requires_1_5_Error;
+
+ public static String Requires_Class_Compatibility_5;
+
+ public static String Requires_Compiler_Compliance_5;
+
+ public static String Requires_Source_Compatibility_5;
+
+ public static String s_Contains_Xml_Error;
+
+ public static String s_Doesnt_Declare_Package_Error;
+
+ public static String s_File_Missing;
+
+ public static String s_Missing_Repackaging;
+
+ public static String s_Modified_Manually_Recreating_s;
+
+ public static String s_Modified_Recreating_s;
+
+ public static String s_Removed_Recreating_s;
+
+ public static String s_Removed_s_Needs_Updating;
+
+ public static String Start_Full_Apk_Build;
+
+ public static String Start_Full_Pre_Compiler;
+
+ public static String Start_Inc_Apk_Build;
+
+ public static String Start_Inc_Pre_Compiler;
+
+ public static String Unparsed_AAPT_Errors;
+
+ public static String Unparsed_AIDL_Errors;
+
+ public static String Xml_Error;
+ static {
+ // initialize resource bundle
+ NLS.initializeMessages(BUNDLE_NAME, Messages.class);
+ }
+
+ private Messages() {
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerBuilder.java
new file mode 100644
index 0000000..a0e446c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerBuilder.java
@@ -0,0 +1,1150 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.build;
+
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.project.FixLaunchConfig;
+import com.android.ide.eclipse.adt.project.ProjectHelper;
+import com.android.ide.eclipse.adt.sdk.Sdk;
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.common.project.AndroidManifestHelper;
+import com.android.ide.eclipse.common.project.AndroidManifestParser;
+import com.android.ide.eclipse.common.project.BaseProjectHelper;
+import com.android.ide.eclipse.common.project.XmlErrorHandler.BasicXmlErrorListener;
+import com.android.sdklib.IAndroidTarget;
+
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IResourceDelta;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourceAttributes;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Pre Java Compiler.
+ * This incremental builder performs 2 tasks:
+ * <ul>
+ * <li>compiles the resources located in the res/ folder, along with the
+ * AndroidManifest.xml file into the R.java class.</li>
+ * <li>compiles any .aidl files into a corresponding java file.</li>
+ * </ul>
+ *
+ */
+public class PreCompilerBuilder extends BaseBuilder {
+
+ public static final String ID = "com.android.ide.eclipse.adt.PreCompilerBuilder"; //$NON-NLS-1$
+
+ private static final String PROPERTY_PACKAGE = "manifestPackage"; //$NON-NLS-1$
+
+ private static final String PROPERTY_SOURCE_FOLDER =
+ "manifestPackageSourceFolder"; //$NON-NLS-1$
+
+ private static final String PROPERTY_COMPILE_RESOURCES = "compileResources"; //$NON-NLS-1$
+
+ static final String PROPERTY_ANDROID_GENERATED = "androidGenerated"; //$NON-NLS-1$
+ static final String PROPERTY_ANDROID_CONFLICT = "androidConflict"; //$NON-NLS-1$
+
+ /**
+ * Single line aidl error<br>
+ * "&lt;path&gt;:&lt;line&gt;: &lt;error&gt;"
+ */
+ private static Pattern sAidlPattern1 = Pattern.compile("^(.+?):(\\d+):\\s(.+)$"); //$NON-NLS-1$
+
+ /**
+ * Compile flag. This is set to true if one of the changed/added/removed
+ * file is a resource file. Upon visiting all the delta resources, if
+ * this flag is true, then we know we'll have to compile the resources
+ * into R.java
+ */
+ private boolean mCompileResources = false;
+
+ /** List of .aidl files found that are modified or new. */
+ private final ArrayList<IFile> mAidlToCompile = new ArrayList<IFile>();
+
+ /** List of .aidl files that have been removed. */
+ private final ArrayList<IFile> mAidlToRemove = new ArrayList<IFile>();
+
+ /** cache of the java package defined in the manifest */
+ private String mManifestPackage;
+
+ /** Source folder containing the java package defined in the manifest. */
+ private IFolder mManifestPackageSourceFolder;
+
+ /**
+ * Progress monitor waiting the end of the process to set a persistent value
+ * in a file. This is typically used in conjunction with <code>IResource.refresh()</code>,
+ * since this call is asysnchronous, and we need to wait for it to finish for the file
+ * to be known by eclipse, before we can call <code>resource.setPersistentProperty</code> on
+ * a new file.
+ */
+ private static class RefreshProgressMonitor implements IProgressMonitor {
+ private boolean mCancelled = false;
+ private IFile mNewFile;
+ private IFile mSource;
+ private boolean mDoneExecuted = false;
+ public RefreshProgressMonitor(IFile newFile, IFile source) {
+ mNewFile = newFile;
+ mSource = source;
+ }
+
+ public void beginTask(String name, int totalWork) {
+ }
+
+ public void done() {
+ if (mDoneExecuted == false) {
+ mDoneExecuted = true;
+ if (mNewFile.exists()) {
+ ProjectHelper.saveResourceProperty(mNewFile, PROPERTY_ANDROID_GENERATED,
+ mSource);
+ try {
+ mNewFile.setDerived(true);
+ } catch (CoreException e) {
+ // This really shouldn't happen since we check that the resource exist.
+ // Worst case scenario, the resource isn't marked as derived.
+ }
+ }
+ }
+ }
+
+ public void internalWorked(double work) {
+ }
+
+ public boolean isCanceled() {
+ return mCancelled;
+ }
+
+ public void setCanceled(boolean value) {
+ mCancelled = value;
+ }
+
+ public void setTaskName(String name) {
+ }
+
+ public void subTask(String name) {
+ }
+
+ public void worked(int work) {
+ }
+ }
+
+ /**
+ * Progress Monitor setting up to two files as derived once their parent is refreshed.
+ * This is used as ProgressMonitor to refresh the R.java/Manifest.java parent (to display
+ * the newly created files in the package explorer).
+ */
+ private static class DerivedProgressMonitor implements IProgressMonitor {
+ private boolean mCancelled = false;
+ private IFile mFile1;
+ private IFile mFile2;
+ private boolean mDoneExecuted = false;
+ public DerivedProgressMonitor(IFile file1, IFile file2) {
+ mFile1 = file1;
+ mFile2 = file2;
+ }
+
+ public void beginTask(String name, int totalWork) {
+ }
+
+ public void done() {
+ if (mDoneExecuted == false) {
+ if (mFile1 != null && mFile1.exists()) {
+ mDoneExecuted = true;
+ try {
+ mFile1.setDerived(true);
+ } catch (CoreException e) {
+ // This really shouldn't happen since we check that the resource edit.
+ // Worst case scenario, the resource isn't marked as derived.
+ }
+ }
+ if (mFile2 != null && mFile2.exists()) {
+ try {
+ mFile2.setDerived(true);
+ } catch (CoreException e) {
+ // This really shouldn't happen since we check that the resource edit.
+ // Worst case scenario, the resource isn't marked as derived.
+ }
+ }
+ }
+ }
+
+ public void internalWorked(double work) {
+ }
+
+ public boolean isCanceled() {
+ return mCancelled;
+ }
+
+ public void setCanceled(boolean value) {
+ mCancelled = value;
+ }
+
+ public void setTaskName(String name) {
+ }
+
+ public void subTask(String name) {
+ }
+
+ public void worked(int work) {
+ }
+ }
+
+ public PreCompilerBuilder() {
+ super();
+ }
+
+ // build() returns a list of project from which this project depends for future compilation.
+ @SuppressWarnings("unchecked") //$NON-NLS-1$
+ @Override
+ protected IProject[] build(int kind, Map args, IProgressMonitor monitor)
+ throws CoreException {
+ // First thing we do is go through the resource delta to not
+ // lose it if we have to abort the build for any reason.
+
+ // get the project objects
+ IProject project = getProject();
+
+ // Top level check to make sure the build can move forward.
+ abortOnBadSetup(project);
+
+ IJavaProject javaProject = JavaCore.create(project);
+ IAndroidTarget projectTarget = Sdk.getCurrent().getTarget(project);
+
+ // now we need to get the classpath list
+ ArrayList<IPath> sourceList = BaseProjectHelper.getSourceClasspaths(javaProject);
+
+ PreCompilerDeltaVisitor dv = null;
+ String javaPackage = null;
+
+ if (kind == FULL_BUILD) {
+ AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project,
+ Messages.Start_Full_Pre_Compiler);
+ mCompileResources = true;
+ buildAidlCompilationList(project, sourceList);
+ } else {
+ AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project,
+ Messages.Start_Inc_Pre_Compiler);
+
+ // Go through the resources and see if something changed.
+ // Even if the mCompileResources flag is true from a previously aborted
+ // build, we need to go through the Resource delta to get a possible
+ // list of aidl files to compile/remove.
+ IResourceDelta delta = getDelta(project);
+ if (delta == null) {
+ mCompileResources = true;
+ buildAidlCompilationList(project, sourceList);
+ } else {
+ dv = new PreCompilerDeltaVisitor(this, sourceList);
+ delta.accept(dv);
+
+ // record the state
+ mCompileResources |= dv.getCompileResources();
+
+ // handle aidl modification
+ if (dv.getFullAidlRecompilation()) {
+ buildAidlCompilationList(project, sourceList);
+ } else {
+ mergeAidlFileModifications(dv.getAidlToCompile(),
+ dv.getAidlToRemove());
+ }
+
+ // get the java package from the visitor
+ javaPackage = dv.getManifestPackage();
+ }
+ }
+
+ // store the build status in the persistent storage
+ saveProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES , mCompileResources);
+ // TODO also needs to store the list of aidl to compile/remove
+
+ // if there was some XML errors, we just return w/o doing
+ // anything since we've put some markers in the files anyway.
+ if (dv != null && dv.mXmlError) {
+ AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project,
+ Messages.Xml_Error);
+
+ // This interrupts the build. The next builders will not run.
+ stopBuild(Messages.Xml_Error);
+ }
+
+
+ // get the manifest file
+ IFile manifest = AndroidManifestHelper.getManifest(project);
+
+ if (manifest == null) {
+ String msg = String.format(Messages.s_File_Missing,
+ AndroidConstants.FN_ANDROID_MANIFEST);
+ AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, msg);
+ markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
+
+ // This interrupts the build. The next builders will not run.
+ stopBuild(msg);
+ }
+
+ // lets check the XML of the manifest first, if that hasn't been done by the
+ // resource delta visitor yet.
+ if (dv == null || dv.getCheckedManifestXml() == false) {
+ BasicXmlErrorListener errorListener = new BasicXmlErrorListener();
+ AndroidManifestParser parser = BaseProjectHelper.parseManifestForError(manifest,
+ errorListener);
+
+ if (errorListener.mHasXmlError == true) {
+ // there was an error in the manifest, its file has been marked,
+ // by the XmlErrorHandler.
+ // We return;
+ String msg = String.format(Messages.s_Contains_Xml_Error,
+ AndroidConstants.FN_ANDROID_MANIFEST);
+ AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, msg);
+
+ // This interrupts the build. The next builders will not run.
+ stopBuild(msg);
+ }
+
+ // get the java package from the parser
+ javaPackage = parser.getPackage();
+ }
+
+ if (javaPackage == null || javaPackage.length() == 0) {
+ // looks like the AndroidManifest file isn't valid.
+ String msg = String.format(Messages.s_Doesnt_Declare_Package_Error,
+ AndroidConstants.FN_ANDROID_MANIFEST);
+ AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project,
+ msg);
+
+ // This interrupts the build. The next builders will not run.
+ stopBuild(msg);
+ }
+
+ // at this point we have the java package. We need to make sure it's not a different package
+ // than the previous one that were built.
+ if (javaPackage.equals(mManifestPackage) == false) {
+ // The manifest package has changed, the user may want to update
+ // the launch configuration
+ if (mManifestPackage != null) {
+ AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project,
+ Messages.Checking_Package_Change);
+
+ FixLaunchConfig flc = new FixLaunchConfig(project, mManifestPackage, javaPackage);
+ flc.start();
+ }
+
+ // now we delete the generated classes from their previous location
+ deleteObsoleteGeneratedClass(AndroidConstants.FN_RESOURCE_CLASS,
+ mManifestPackageSourceFolder, mManifestPackage);
+ deleteObsoleteGeneratedClass(AndroidConstants.FN_MANIFEST_CLASS,
+ mManifestPackageSourceFolder, mManifestPackage);
+
+ // record the new manifest package, and save it.
+ mManifestPackage = javaPackage;
+ saveProjectStringProperty(PROPERTY_PACKAGE, mManifestPackage);
+ }
+
+ if (mCompileResources) {
+ // we need to figure out where to store the R class.
+ // get the parent folder for R.java and update mManifestPackageSourceFolder
+ IFolder packageFolder = getManifestPackageFolder(project, sourceList);
+
+ // at this point, either we have found the package or not.
+ // if we haven't well it's time to tell the user and abort
+ if (mManifestPackageSourceFolder == null) {
+ // mark the manifest file
+ String message = String.format(Messages.Package_s_Doesnt_Exist_Error,
+ mManifestPackage);
+ BaseProjectHelper.addMarker(manifest, AndroidConstants.MARKER_AAPT_COMPILE, message,
+ IMarker.SEVERITY_ERROR);
+ AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project, message);
+
+ // abort
+ // This interrupts the build. The next builders will not run.
+ stopBuild(message);
+ }
+
+
+ // found the folder in which to write the stuff
+
+ // get the resource folder
+ IFolder resFolder = project.getFolder(AndroidConstants.WS_RESOURCES);
+
+ // get the file system path
+ IPath outputLocation = mManifestPackageSourceFolder.getLocation();
+ IPath resLocation = resFolder.getLocation();
+ IPath manifestLocation = manifest.getLocation();
+
+ // those locations have to exist for us to do something!
+ if (outputLocation != null && resLocation != null
+ && manifestLocation != null) {
+ String osOutputPath = outputLocation.toOSString();
+ String osResPath = resLocation.toOSString();
+ String osManifestPath = manifestLocation.toOSString();
+
+ // remove the aapt markers
+ removeMarkersFromFile(manifest, AndroidConstants.MARKER_AAPT_COMPILE);
+ removeMarkersFromContainer(resFolder, AndroidConstants.MARKER_AAPT_COMPILE);
+
+ AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project,
+ Messages.Preparing_Generated_Files);
+
+ // since the R.java file may be already existing in read-only
+ // mode we need to make it readable so that aapt can overwrite
+ // it
+ IFile rJavaFile = packageFolder.getFile(AndroidConstants.FN_RESOURCE_CLASS);
+ prepareFileForExternalModification(rJavaFile);
+
+ // do the same for the Manifest.java class
+ IFile manifestJavaFile = packageFolder.getFile(AndroidConstants.FN_MANIFEST_CLASS);
+ prepareFileForExternalModification(manifestJavaFile);
+
+ // we actually need to delete the manifest.java as it may become empty and in this
+ // case aapt doesn't generate an empty one, but instead doesn't touch it.
+ manifestJavaFile.delete(true, null);
+
+ // launch aapt: create the command line
+ ArrayList<String> array = new ArrayList<String>();
+ array.add(projectTarget.getPath(IAndroidTarget.AAPT));
+ array.add("package"); //$NON-NLS-1$
+ array.add("-m"); //$NON-NLS-1$
+ if (AdtPlugin.getBuildVerbosity() == AdtConstants.BUILD_VERBOSE) {
+ array.add("-v"); //$NON-NLS-1$
+ }
+ array.add("-J"); //$NON-NLS-1$
+ array.add(osOutputPath);
+ array.add("-M"); //$NON-NLS-1$
+ array.add(osManifestPath);
+ array.add("-S"); //$NON-NLS-1$
+ array.add(osResPath);
+ array.add("-I"); //$NON-NLS-1$
+ array.add(projectTarget.getPath(IAndroidTarget.ANDROID_JAR));
+
+ if (AdtPlugin.getBuildVerbosity() == AdtConstants.BUILD_VERBOSE) {
+ StringBuilder sb = new StringBuilder();
+ for (String c : array) {
+ sb.append(c);
+ sb.append(' ');
+ }
+ String cmd_line = sb.toString();
+ AdtPlugin.printToConsole(project, cmd_line);
+ }
+
+ // launch
+ int execError = 1;
+ try {
+ // launch the command line process
+ Process process = Runtime.getRuntime().exec(
+ array.toArray(new String[array.size()]));
+
+ // list to store each line of stderr
+ ArrayList<String> results = new ArrayList<String>();
+
+ // get the output and return code from the process
+ execError = grabProcessOutput(process, results);
+
+ // attempt to parse the error output
+ boolean parsingError = parseAaptOutput(results, project);
+
+ // if we couldn't parse the output we display it in the console.
+ if (parsingError) {
+ if (execError != 0) {
+ AdtPlugin.printErrorToConsole(project, results.toArray());
+ } else {
+ AdtPlugin.printBuildToConsole(AdtConstants.BUILD_NORMAL,
+ project, results.toArray());
+ }
+ }
+
+ if (execError != 0) {
+ // if the exec failed, and we couldn't parse the error output (and therefore
+ // not all files that should have been marked, were marked), we put a
+ // generic marker on the project and abort.
+ if (parsingError) {
+ markProject(AdtConstants.MARKER_ADT, Messages.Unparsed_AAPT_Errors,
+ IMarker.SEVERITY_ERROR);
+ }
+
+ AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project,
+ Messages.AAPT_Error);
+
+ // abort if exec failed.
+ // This interrupts the build. The next builders will not run.
+ stopBuild(Messages.AAPT_Error);
+ }
+ } catch (IOException e1) {
+ // something happen while executing the process,
+ // mark the project and exit
+ String msg = String.format(Messages.AAPT_Exec_Error, array.get(0));
+ markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
+
+ // This interrupts the build. The next builders will not run.
+ stopBuild(msg);
+ } catch (InterruptedException e) {
+ // we got interrupted waiting for the process to end...
+ // mark the project and exit
+ String msg = String.format(Messages.AAPT_Exec_Error, array.get(0));
+ markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
+
+ // This interrupts the build. The next builders will not run.
+ stopBuild(msg);
+ }
+
+ // if the return code was OK, we refresh the folder that
+ // contains R.java to force a java recompile.
+ if (execError == 0) {
+ // now set the R.java/Manifest.java file as read only.
+ finishJavaFilesAfterExternalModification(rJavaFile, manifestJavaFile);
+
+ // build has been done. reset the state of the builder
+ mCompileResources = false;
+
+ // and store it
+ saveProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES, mCompileResources);
+ }
+ }
+ } else {
+ // nothing to do
+ }
+
+ // now handle the aidl stuff.
+ // look for a preprocessed aidl file
+ IResource projectAidl = project.findMember("project.aidl"); //$NON-NLS-1$
+ String folderAidlPath = null;
+ if (projectAidl != null && projectAidl.exists()) {
+ folderAidlPath = projectAidl.getLocation().toOSString();
+ }
+ boolean aidlStatus = handleAidl(projectTarget, sourceList, folderAidlPath, monitor);
+
+ if (aidlStatus == false && mCompileResources == false) {
+ AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project,
+ Messages.Nothing_To_Compile);
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void clean(IProgressMonitor monitor) throws CoreException {
+ super.clean(monitor);
+
+ AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, getProject(),
+ Messages.Removing_Generated_Classes);
+
+ // check if we have the R.java info already.
+ if (mManifestPackageSourceFolder != null && mManifestPackage != null) {
+ deleteObsoleteGeneratedClass(AndroidConstants.FN_RESOURCE_CLASS,
+ mManifestPackageSourceFolder, mManifestPackage);
+ deleteObsoleteGeneratedClass(AndroidConstants.FN_MANIFEST_CLASS,
+ mManifestPackageSourceFolder, mManifestPackage);
+ }
+
+ // FIXME: delete all java generated from aidl.
+ }
+
+ @Override
+ protected void startupOnInitialize() {
+ super.startupOnInitialize();
+
+ // load the previous IFolder and java package.
+ mManifestPackage = loadProjectStringProperty(PROPERTY_PACKAGE);
+ IResource resource = loadProjectResourceProperty(PROPERTY_SOURCE_FOLDER);
+ if (resource instanceof IFolder) {
+ mManifestPackageSourceFolder = (IFolder)resource;
+ }
+
+ // Load the current compile flag. We ask for true if not found to force a
+ // recompile.
+ mCompileResources = loadProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES, true);
+ }
+
+ /**
+ * Delete the a generated java class associated with the specified java package.
+ * @param filename Name of the generated file to remove.
+ * @param sourceFolder The source Folder containing the old java package.
+ * @param javaPackage the old java package
+ */
+ private void deleteObsoleteGeneratedClass(String filename, IFolder sourceFolder,
+ String javaPackage) {
+ if (sourceFolder == null || javaPackage == null) {
+ return;
+ }
+
+ // convert the java package into path
+ String[] segments = javaPackage.split(AndroidConstants.RE_DOT);
+
+ StringBuilder path = new StringBuilder();
+ for (String s : segments) {
+ path.append(AndroidConstants.WS_SEP_CHAR);
+ path.append(s);
+ }
+
+ // appends the name of the generated file
+ path.append(AndroidConstants.WS_SEP_CHAR);
+ path.append(filename);
+
+ Path iPath = new Path(path.toString());
+
+ // Find a matching resource object.
+ IResource javaFile = sourceFolder.findMember(iPath);
+ if (javaFile != null && javaFile.exists() && javaFile.getType() == IResource.FILE) {
+ try {
+ // remove the read-only tag
+ prepareFileForExternalModification((IFile)javaFile);
+
+ // delete
+ javaFile.delete(true, null);
+
+ // refresh parent
+ javaFile.getParent().refreshLocal(IResource.DEPTH_ONE, new NullProgressMonitor());
+
+ } catch (CoreException e) {
+ // failed to delete it, the user will have to delete it manually.
+ String message = String.format(Messages.Delete_Obsolete_Error, path);
+ IProject project = getProject();
+ AdtPlugin.printErrorToConsole(project, message);
+ AdtPlugin.printErrorToConsole(project, e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * Looks for the folder containing the package defined in the manifest. It looks in the
+ * list of source folders for the one containing folders matching the package defined in the
+ * manifest (from the field <code>mManifestPackage</code>). It returns the final folder, which
+ * will contain the R class, and update the field <code>mManifestPackageSourceFolder</code>
+ * to be the source folder containing the full package.
+ * @param project The project.
+ * @param sourceList The list of source folders for the project.
+ * @return the package that will contain the R class or null if the folder was not found.
+ * @throws CoreException
+ */
+ private IFolder getManifestPackageFolder(IProject project, ArrayList<IPath> sourceList)
+ throws CoreException {
+ // split the package in segments
+ String[] packageSegments = mManifestPackage.split(AndroidConstants.RE_DOT);
+
+ // we look for 2 folders.
+ // 1. The source folder that contains the full java package.
+ // we will store the folder in the field mJavaSourceFolder, for reuse during
+ IFolder manifestPackageSourceFolder = null;
+ // subsequent builds. This is the folder we will give to aapt.
+ // 2. The folder actually containing the R.java files. We need this one to do a refresh
+ IFolder packageFolder = null;
+
+ for (IPath iPath : sourceList) {
+ int packageSegmentIndex = 0;
+
+ // the path is relative to the workspace. We ignore the first segment,
+ // when getting the resource from the IProject object.
+ IResource classpathEntry = project.getFolder(iPath.removeFirstSegments(1));
+
+ if (classpathEntry instanceof IFolder) {
+ IFolder classpathFolder = (IFolder)classpathEntry;
+ IFolder folder = classpathFolder;
+
+ boolean failed = false;
+ while (failed == false
+ && packageSegmentIndex < packageSegments.length) {
+
+ // loop on that folder content looking for folders
+ // that match the package
+ // defined in AndroidManifest.xml
+
+ // get the folder content
+ IResource[] content = folder.members();
+
+ // this is the segment we look for
+ String segment = packageSegments[packageSegmentIndex];
+
+ // did we find it at this level
+ boolean found = false;
+
+ for (IResource r : content) {
+ // look for the java package segment
+ if (r instanceof IFolder) {
+ if (r.getName().equals(segment)) {
+ // we need to skip to the next one
+ folder = (IFolder)r;
+ packageSegmentIndex++;
+ found = true;
+ break;
+ }
+ }
+ }
+
+ // if we didn't find it at this level we just fail.
+ if (found == false) {
+ failed = true;
+ }
+ }
+
+ // if we didn't fail then we found it. no point in
+ // looping through the rest
+ // or the classpathEntry
+ if (failed == false) {
+ // save the target folder reference
+ manifestPackageSourceFolder = classpathFolder;
+ packageFolder = folder;
+ break;
+ }
+ }
+ }
+
+ // save the location of the folder into the persistent storage
+ if (manifestPackageSourceFolder != mManifestPackageSourceFolder) {
+ mManifestPackageSourceFolder = manifestPackageSourceFolder;
+ saveProjectResourceProperty(PROPERTY_SOURCE_FOLDER, mManifestPackageSourceFolder);
+ }
+ return packageFolder;
+ }
+
+ /**
+ * Compiles aidl files into java. This will also removes old java files
+ * created from aidl files that are now gone.
+ * @param projectTarget Target of the project
+ * @param sourceFolders the list of source folders, relative to the workspace.
+ * @param folderAidlPath
+ * @param monitor the projess monitor
+ * @returns true if it did something
+ * @throws CoreException
+ */
+ private boolean handleAidl(IAndroidTarget projectTarget, ArrayList<IPath> sourceFolders,
+ String folderAidlPath, IProgressMonitor monitor) throws CoreException {
+ if (mAidlToCompile.size() == 0 && mAidlToRemove.size() == 0) {
+ return false;
+ }
+
+
+ // create the command line
+ String[] command = new String[4 + sourceFolders.size() + (folderAidlPath != null ? 1 : 0)];
+ int index = 0;
+ int aidlIndex;
+ command[index++] = projectTarget.getPath(IAndroidTarget.AIDL);
+ command[aidlIndex = index++] = "-p"; //$NON-NLS-1$
+ if (folderAidlPath != null) {
+ command[index++] = "-p" + folderAidlPath; //$NON-NLS-1$
+ }
+
+ // since the path are relative to the workspace and not the project itself, we need
+ // the workspace root.
+ IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot();
+ for (IPath p : sourceFolders) {
+ IFolder f = wsRoot.getFolder(p);
+ command[index++] = "-I" + f.getLocation().toOSString(); //$NON-NLS-1$
+ }
+
+ // list of files that have failed compilation.
+ ArrayList<IFile> stillNeedCompilation = new ArrayList<IFile>();
+
+ // if an aidl file is being removed before we managed to compile it, it'll be in
+ // both list. We *need* to remove it from the compile list or it'll never go away.
+ for (IFile aidlFile : mAidlToRemove) {
+ int pos = mAidlToCompile.indexOf(aidlFile);
+ if (pos != -1) {
+ mAidlToCompile.remove(pos);
+ }
+ }
+
+ // loop until we've compile them all
+ for (IFile aidlFile : mAidlToCompile) {
+ // Remove the AIDL error markers from the aidl file
+ removeMarkersFromFile(aidlFile, AndroidConstants.MARKER_AIDL);
+
+ // get the path
+ IPath iPath = aidlFile.getLocation();
+ String osPath = iPath.toOSString();
+
+ // get the parent container
+ IContainer parentContainer = aidlFile.getParent();
+
+ // replace the extension in both the full path and the
+ // last segment
+ String osJavaPath = osPath.replaceAll(AndroidConstants.RE_AIDL_EXT,
+ AndroidConstants.DOT_JAVA);
+ String javaName = aidlFile.getName().replaceAll(AndroidConstants.RE_AIDL_EXT,
+ AndroidConstants.DOT_JAVA);
+
+ // check if we can compile it, or if there is a conflict with a java file
+ boolean conflict = ProjectHelper.loadBooleanProperty(aidlFile,
+ PROPERTY_ANDROID_CONFLICT, false);
+ if (conflict) {
+ String msg = String.format(Messages.AIDL_Java_Conflict, javaName,
+ aidlFile.getName());
+
+ // put a marker
+ BaseProjectHelper.addMarker(aidlFile, AndroidConstants.MARKER_AIDL, msg,
+ IMarker.SEVERITY_ERROR);
+
+ // output an error
+ AdtPlugin.printErrorToConsole(getProject(), msg);
+
+ stillNeedCompilation.add(aidlFile);
+
+ // move on to next file
+ continue;
+ }
+
+ // get the resource for the java file.
+ Path javaIPath = new Path(javaName);
+ IFile javaFile = parentContainer.getFile(javaIPath);
+
+ // if the file was read-only, this will make it readable.
+ prepareFileForExternalModification(javaFile);
+
+ // finish to set the command line.
+ command[aidlIndex] = "-p" + Sdk.getCurrent().getTarget(aidlFile.getProject()).getPath(
+ IAndroidTarget.ANDROID_AIDL); //$NON-NLS-1$
+ command[index] = osPath;
+ command[index + 1] = osJavaPath;
+
+ // launch the process
+ if (execAidl(command, aidlFile) == false) {
+ // aidl failed. File should be marked. We add the file to the list
+ // of file that will need compilation again.
+ stillNeedCompilation.add(aidlFile);
+
+ // and we move on to the next one.
+ continue;
+ } else {
+ // since the exec worked, we refresh the parent, and set the
+ // file as read only.
+ finishFileAfterExternalModification(javaFile, aidlFile);
+ }
+ }
+
+ // change the list to only contains the file that have failed compilation
+ mAidlToCompile.clear();
+ mAidlToCompile.addAll(stillNeedCompilation);
+
+ // Remove the java files created from aidl files that have been removed.
+ for (IFile aidlFile : mAidlToRemove) {
+ // make the java filename
+ String javaName = aidlFile.getName().replaceAll(
+ AndroidConstants.RE_AIDL_EXT,
+ AndroidConstants.DOT_JAVA);
+
+ // get the parent container
+ IContainer ic = aidlFile.getParent();
+
+ // and get the IFile corresponding to the java file.
+ IFile javaFile = ic.getFile(new Path(javaName));
+ if (javaFile != null && javaFile.exists() ) {
+ // check if this java file has a persistent data marking it as generated by
+ // the builder.
+ // While we put the aidl path as a resource, internally it's all string anyway.
+ // We use loadStringProperty, because loadResourceProperty tries to match
+ // the string value (a path in this case) with an existing resource, but
+ // the aidl file was deleted, so it would return null, even though the property
+ // existed.
+ String aidlPath = ProjectHelper.loadStringProperty(javaFile,
+ PROPERTY_ANDROID_GENERATED);
+
+ if (aidlPath != null) {
+ // This confirms the java file was generated by the builder,
+ // we can delete the aidlFile.
+ javaFile.delete(true, null);
+
+ // Refresh parent.
+ ic.refreshLocal(IResource.DEPTH_ONE, monitor);
+ }
+ }
+ }
+ mAidlToRemove.clear();
+
+ return true;
+ }
+
+ /**
+ * Execute the aidl command line, parse the output, and mark the aidl file
+ * with any reported errors.
+ * @param command the String array containing the command line to execute.
+ * @param file The IFile object representing the aidl file being
+ * compiled.
+ * @return false if the exec failed, and build needs to be aborted.
+ */
+ private boolean execAidl(String[] command, IFile file) {
+ // do the exec
+ try {
+ Process p = Runtime.getRuntime().exec(command);
+
+ // list to store each line of stderr
+ ArrayList<String> results = new ArrayList<String>();
+
+ // get the output and return code from the process
+ int result = grabProcessOutput(p, results);
+
+ // attempt to parse the error output
+ boolean error = parseAidlOutput(results, file);
+
+ // If the process failed and we couldn't parse the output
+ // we pring a message, mark the project and exit
+ if (result != 0 && error == true) {
+ // display the message in the console.
+ AdtPlugin.printErrorToConsole(getProject(), results.toArray());
+
+ // mark the project and exit
+ markProject(AdtConstants.MARKER_ADT, Messages.Unparsed_AIDL_Errors,
+ IMarker.SEVERITY_ERROR);
+ return false;
+ }
+ } catch (IOException e) {
+ // mark the project and exit
+ String msg = String.format(Messages.AIDL_Exec_Error, command[0]);
+ markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
+ return false;
+ } catch (InterruptedException e) {
+ // mark the project and exit
+ String msg = String.format(Messages.AIDL_Exec_Error, command[0]);
+ markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Goes through the build paths and fills the list of aidl files to compile
+ * ({@link #mAidlToCompile}).
+ * @param project The project.
+ * @param buildPaths The list of build paths.
+ */
+ private void buildAidlCompilationList(IProject project,
+ ArrayList<IPath> buildPaths) {
+ for (IPath p : buildPaths) {
+ // Because the path contains the name of the project as well, we
+ // need to remove it, to access the final folder.
+ String[] segments = p.segments();
+ IContainer folder = project;
+ for (int i = 1; i < segments.length; i++) {
+ IResource r = folder.findMember(segments[i]);
+ if (r != null && r.exists() &&
+ r.getType() == IResource.FOLDER) {
+ folder = (IContainer)r;
+ } else {
+ // hmm looks like the build path is corrupted/wrong.
+ // reset and break
+ folder = project;
+ break;
+ }
+ }
+
+ // did we ge a folder?
+ if (folder != project) {
+ // then we scan!
+ scanContainerForAidl(folder);
+ }
+ }
+ }
+
+ /**
+ * Scans a container and fills the list of aidl files to compile.
+ * @param container The container to scan.
+ */
+ private void scanContainerForAidl(IContainer container) {
+ try {
+ IResource[] members = container.members();
+ for (IResource r : members) {
+ // get the type of the resource
+ switch (r.getType()) {
+ case IResource.FILE:
+ // if this a file, check that the file actually exist
+ // and that it's an aidl file
+ if (r.exists() &&
+ AndroidConstants.EXT_AIDL.equalsIgnoreCase(r.getFileExtension())) {
+ mAidlToCompile.add((IFile)r);
+ }
+ break;
+ case IResource.FOLDER:
+ // recursively go through children
+ scanContainerForAidl((IFolder)r);
+ break;
+ default:
+ // this would mean it's a project or the workspace root
+ // which is unlikely to happen. we do nothing
+ break;
+ }
+ }
+ } catch (CoreException e) {
+ // Couldn't get the members list for some reason. Just return.
+ }
+ }
+
+
+ /**
+ * Parse the output of aidl and mark the file with any errors.
+ * @param lines The output to parse.
+ * @param file The file to mark with error.
+ * @return true if the parsing failed, false if success.
+ */
+ private boolean parseAidlOutput(ArrayList<String> lines, IFile file) {
+ // nothing to parse? just return false;
+ if (lines.size() == 0) {
+ return false;
+ }
+
+ Matcher m;
+
+ for (int i = 0; i < lines.size(); i++) {
+ String p = lines.get(i);
+
+ m = sAidlPattern1.matcher(p);
+ if (m.matches()) {
+ // we can ignore group 1 which is the location since we already
+ // have a IFile object representing the aidl file.
+ String lineStr = m.group(2);
+ String msg = m.group(3);
+
+ // get the line number
+ int line = 0;
+ try {
+ line = Integer.parseInt(lineStr);
+ } catch (NumberFormatException e) {
+ // looks like the string we extracted wasn't a valid
+ // file number. Parsing failed and we return true
+ return true;
+ }
+
+ // mark the file
+ BaseProjectHelper.addMarker(file, AndroidConstants.MARKER_AIDL, msg, line,
+ IMarker.SEVERITY_ERROR);
+
+ // success, go to the next line
+ continue;
+ }
+
+ // invalid line format, flag as error, and bail
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Merge the current list of aidl file to compile/remove with the new one.
+ * @param toCompile List of file to compile
+ * @param toRemove List of file to remove
+ */
+ private void mergeAidlFileModifications(ArrayList<IFile> toCompile,
+ ArrayList<IFile> toRemove) {
+
+ // loop through the new toRemove list, and add it to the old one,
+ // plus remove any file that was still to compile and that are now
+ // removed
+ for (IFile r : toRemove) {
+ if (mAidlToRemove.indexOf(r) == -1) {
+ mAidlToRemove.add(r);
+ }
+
+ int index = mAidlToCompile.indexOf(r);
+ if (index != -1) {
+ mAidlToCompile.remove(index);
+ }
+ }
+
+ // now loop through the new files to compile and add it to the list.
+ // Also look for them in the remove list, this would mean that they
+ // were removed, then added back, and we shouldn't remove them, just
+ // recompile them.
+ for (IFile r : toCompile) {
+ if (mAidlToCompile.indexOf(r) == -1) {
+ mAidlToCompile.add(r);
+ }
+
+ int index = mAidlToRemove.indexOf(r);
+ if (index != -1) {
+ mAidlToRemove.remove(index);
+ }
+ }
+ }
+
+ /**
+ * Prepare an already existing file for modification. File generated from
+ * command line processed are marked as read-only. This method prepares
+ * them (mark them as read-write) before the command line process is
+ * started. A check is made to be sure the file exists.
+ * @param file The IResource object for the file to prepare.
+ * @throws CoreException
+ */
+ private void prepareFileForExternalModification(IFile file)
+ throws CoreException {
+ // file may not exist yet, so we check that.
+ if (file != null && file.exists()) {
+ // get the attributes.
+ ResourceAttributes ra = file.getResourceAttributes();
+ if (ra != null) {
+ // change the attributes
+ ra.setReadOnly(false);
+
+ // set the new attributes in the file.
+ file.setResourceAttributes(ra);
+ }
+ }
+ }
+
+ /**
+ * Finish a file created/modified by an outside command line process.
+ * The file is marked as modified by Android, and the parent folder is refreshed, so that,
+ * in case the file didn't exist beforehand, the file appears in the package explorer.
+ * @param rFile The R file to "finish".
+ * @param manifestFile The manifest file to "finish".
+ * @throws CoreException
+ */
+ private void finishJavaFilesAfterExternalModification(IFile rFile, IFile manifestFile)
+ throws CoreException {
+ IContainer parent = rFile.getParent();
+
+ IProgressMonitor monitor = new DerivedProgressMonitor(rFile, manifestFile);
+
+ // refresh the parent node in the package explorer. Once this is done the custom progress
+ // monitor will mark them as derived.
+ parent.refreshLocal(IResource.DEPTH_ONE, monitor);
+ }
+
+ /**
+ * Finish a file created/modified by an outside command line process.
+ * The file is marked as modified by Android, and the parent folder is refreshed, so that,
+ * in case the file didn't exist beforehand, the file appears in the package explorer.
+ * @param file The file to "finish".
+ * @param aidlFile The AIDL file to "finish".
+ * @throws CoreException
+ */
+ private void finishFileAfterExternalModification(IFile file, IFile aidlFile)
+ throws CoreException {
+ IContainer parent = file.getParent();
+
+ // we need to add a link to the aidl file.
+ // We need to wait for the refresh of the parent to be done, so we'll do
+ // it in the monitor. This will also set the file as derived.
+ IProgressMonitor monitor = new RefreshProgressMonitor(file, aidlFile);
+
+ // refresh the parent node in the package explorer.
+ parent.refreshLocal(IResource.DEPTH_ONE, monitor);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerDeltaVisitor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerDeltaVisitor.java
new file mode 100644
index 0000000..f4778d7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/PreCompilerDeltaVisitor.java
@@ -0,0 +1,432 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.build;
+
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.build.BaseBuilder.BaseDeltaVisitor;
+import com.android.ide.eclipse.adt.project.ProjectHelper;
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.common.project.AndroidManifestParser;
+import com.android.ide.eclipse.common.project.BaseProjectHelper;
+import com.android.sdklib.SdkConstants;
+
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IResourceDelta;
+import org.eclipse.core.resources.IResourceDeltaVisitor;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.Path;
+
+import java.util.ArrayList;
+
+/**
+ * Resource Delta visitor for the pre-compiler.
+ */
+class PreCompilerDeltaVisitor extends BaseDeltaVisitor implements
+ IResourceDeltaVisitor {
+
+ // Result fields.
+ /**
+ * Compile flag. This is set to true if one of the changed/added/removed
+ * file is a resource file. Upon visiting all the delta resources, if
+ * this flag is true, then we know we'll have to compile the resources
+ * into R.java
+ */
+ private boolean mCompileResources = false;
+
+ /** List of .aidl files found that are modified or new. */
+ private final ArrayList<IFile> mAidlToCompile = new ArrayList<IFile>();
+
+ /** List of .aidl files that have been removed. */
+ private final ArrayList<IFile> mAidlToRemove = new ArrayList<IFile>();
+
+ /** Aidl forced recompilation flag. This is set to true if project.aidl is modified. */
+ private boolean mFullAidlCompilation = false;
+
+ /** Manifest check/parsing flag. */
+ private boolean mCheckedManifestXml = false;
+
+ /** Application Pacakge, gathered from the parsing of the manifest */
+ private String mJavaPackage = null;
+
+ // Internal usage fields.
+ /**
+ * In Resource folder flag. This allows us to know if we're in the
+ * resource folder.
+ */
+ private boolean mInRes = false;
+
+ /**
+ * In Source folder flag. This allows us to know if we're in a source
+ * folder.
+ */
+ private boolean mInSrc = false;
+
+ /** List of source folders. */
+ private ArrayList<IPath> mSourceFolders;
+
+
+ public PreCompilerDeltaVisitor(BaseBuilder builder, ArrayList<IPath> sourceFolders) {
+ super(builder);
+ mSourceFolders = sourceFolders;
+ }
+
+ public boolean getCompileResources() {
+ return mCompileResources;
+ }
+
+ public ArrayList<IFile> getAidlToCompile() {
+ return mAidlToCompile;
+ }
+
+ public ArrayList<IFile> getAidlToRemove() {
+ return mAidlToRemove;
+ }
+
+ public boolean getFullAidlRecompilation() {
+ return mFullAidlCompilation;
+ }
+
+ /**
+ * Returns whether the manifest file was parsed/checked for error during the resource delta
+ * visiting.
+ */
+ public boolean getCheckedManifestXml() {
+ return mCheckedManifestXml;
+ }
+
+ /**
+ * Returns the manifest package if the manifest was checked/parsed.
+ * <p/>
+ * This can return null in two cases:
+ * <ul>
+ * <li>The manifest was not part of the resource change delta, and the manifest was
+ * not checked/parsed ({@link #getCheckedManifestXml()} returns <code>false</code>)</li>
+ * <li>The manifest was parsed ({@link #getCheckedManifestXml()} returns <code>true</code>),
+ * but the package declaration is missing</li>
+ * </ul>
+ * @return the manifest package or null.
+ */
+ public String getManifestPackage() {
+ return mJavaPackage;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.eclipse.core.resources.IResourceDeltaVisitor
+ * #visit(org.eclipse.core.resources.IResourceDelta)
+ */
+ public boolean visit(IResourceDelta delta) throws CoreException {
+ // we are only going to look for changes in res/, source folders and in
+ // AndroidManifest.xml since the delta visitor goes through the main
+ // folder before its children we can check when the path segment
+ // count is 2 (format will be /$Project/folder) and make sure we are
+ // processing res/, source folders or AndroidManifest.xml
+
+ IResource resource = delta.getResource();
+ IPath path = resource.getFullPath();
+ String[] segments = path.segments();
+
+ // since the delta visitor also visits the root we return true if
+ // segments.length = 1
+ if (segments.length == 1) {
+ return true;
+ } else if (segments.length == 2) {
+ // if we are at an item directly under the root directory,
+ // then we are not yet in a source or resource folder
+ mInRes = mInSrc = false;
+
+ if (SdkConstants.FD_RESOURCES.equalsIgnoreCase(segments[1])) {
+ // this is the resource folder that was modified. we want to
+ // see its content.
+
+ // since we're going to visit its children next, we set the
+ // flag
+ mInRes = true;
+ mInSrc = false;
+ return true;
+ } else if (AndroidConstants.FN_ANDROID_MANIFEST.equalsIgnoreCase(segments[1])) {
+ // any change in the manifest could trigger a new R.java
+ // class, so we don't need to check the delta kind
+ if (delta.getKind() != IResourceDelta.REMOVED) {
+ // parse the manifest for errors
+ AndroidManifestParser parser = BaseProjectHelper.parseManifestForError(
+ (IFile)resource, this);
+
+ if (parser != null) {
+ mJavaPackage = parser.getPackage();
+ }
+
+ mCheckedManifestXml = true;
+ }
+ mCompileResources = true;
+
+ // we don't want to go to the children, not like they are
+ // any for this resource anyway.
+ return false;
+ } else if (AndroidConstants.FN_PROJECT_AIDL.equalsIgnoreCase(segments[1])) {
+ // need to force recompilation of all the aidl files
+ mFullAidlCompilation = true;
+ }
+ }
+
+ // at this point we can either be in the source folder or in the
+ // resource folder or in a different folder that contains a source
+ // folder.
+ // This is due to not all source folder being src/. Some could be
+ // something/somethingelse/src/
+
+ // so first we test if we already know we are in a source or
+ // resource folder.
+
+ if (mInSrc) {
+ // if we are in the res folder, we are looking for the following changes:
+ // - added/removed/modified aidl files.
+ // - missing R.java file
+
+ // if the resource is a folder, we just go straight to the children
+ if (resource.getType() == IResource.FOLDER) {
+ return true;
+ }
+
+ if (resource.getType() != IResource.FILE) {
+ return false;
+ }
+ IFile file = (IFile)resource;
+
+ // get the modification kind
+ int kind = delta.getKind();
+
+ if (kind == IResourceDelta.ADDED) {
+ // we only care about added files (inside the source folders), if they
+ // are aidl files.
+
+ // get the extension of the resource
+ String ext = resource.getFileExtension();
+
+ if (AndroidConstants.EXT_AIDL.equalsIgnoreCase(ext)) {
+ // look for an already existing matching java file
+ String javaName = resource.getName().replaceAll(
+ AndroidConstants.RE_AIDL_EXT,
+ AndroidConstants.DOT_JAVA);
+
+ // get the parent container
+ IContainer ic = resource.getParent();
+
+ IFile javaFile = ic.getFile(new Path(javaName));
+ if (javaFile != null && javaFile.exists()) {
+ // check if that file was generated by the plugin. Normally those files are
+ // deleted automatically, but it's better to check.
+ String aidlPath = ProjectHelper.loadStringProperty(javaFile,
+ PreCompilerBuilder.PROPERTY_ANDROID_GENERATED);
+ if (aidlPath == null) {
+ // mark the aidl file that it cannot be compile just yet
+ ProjectHelper.saveBooleanProperty(file,
+ PreCompilerBuilder.PROPERTY_ANDROID_CONFLICT, true);
+ }
+
+ // we add it anyway so that we can try to compile it at every compilation
+ // until the conflict is fixed.
+ mAidlToCompile.add(file);
+
+ } else {
+ // the java file doesn't exist, we can safely add the file to the list
+ // of files to compile.
+ mAidlToCompile.add(file);
+ }
+ }
+
+ return false;
+ }
+
+ // get the filename
+ String fileName = segments[segments.length - 1];
+
+ boolean outputMessage = false;
+
+ // Special case of R.java/Manifest.java.
+ // FIXME: This does not check the package. Any modification of R.java/Manifest.java in another project will trigger a new recompilation of the resources.
+ if (AndroidConstants.FN_RESOURCE_CLASS.equals(fileName) ||
+ AndroidConstants.FN_MANIFEST_CLASS.equals(fileName)) {
+ // if it was removed, there's a possibility that it was removed due to a
+ // package change, or an aidl that was removed, but the only thing
+ // that will happen is that we'll have an extra build. Not much of a problem.
+ mCompileResources = true;
+
+ // we want a warning
+ outputMessage = true;
+ } else {
+
+ // get the extension of the resource
+ String ext = resource.getFileExtension();
+
+ if (AndroidConstants.EXT_AIDL.equalsIgnoreCase(ext)) {
+ if (kind == IResourceDelta.REMOVED) {
+ mAidlToRemove.add(file);
+ } else {
+ mAidlToCompile.add(file);
+ }
+ } else {
+ if (kind == IResourceDelta.REMOVED) {
+ // the file has been removed. we need to check it's a java file and that
+ // there's a matching aidl file. We can't check its persistent storage
+ // anymore.
+ if (AndroidConstants.EXT_JAVA.equalsIgnoreCase(ext)) {
+ String aidlFile = resource.getName().replaceAll(
+ AndroidConstants.RE_JAVA_EXT,
+ AndroidConstants.DOT_AIDL);
+
+ // get the parent container
+ IContainer ic = resource.getParent();
+
+ IFile f = ic.getFile(new Path(aidlFile));
+ if (f != null && f.exists() ) {
+ // make sure that the aidl file is not in conflict anymore, in
+ // case the java file was not generated by us.
+ if (ProjectHelper.loadBooleanProperty(f,
+ PreCompilerBuilder.PROPERTY_ANDROID_CONFLICT, false)) {
+ ProjectHelper.saveBooleanProperty(f,
+ PreCompilerBuilder.PROPERTY_ANDROID_CONFLICT, false);
+ } else {
+ outputMessage = true;
+ }
+ mAidlToCompile.add(f);
+ }
+ }
+ } else {
+ // check if it's an android generated java file.
+ IResource aidlSource = ProjectHelper.loadResourceProperty(
+ file, PreCompilerBuilder.PROPERTY_ANDROID_GENERATED);
+
+ if (aidlSource != null && aidlSource.exists() &&
+ aidlSource.getType() == IResource.FILE) {
+ // it looks like this was a java file created from an aidl file.
+ // we need to add the aidl file to the list of aidl file to compile
+ mAidlToCompile.add((IFile)aidlSource);
+ outputMessage = true;
+ }
+ }
+ }
+ }
+
+ if (outputMessage) {
+ if (kind == IResourceDelta.REMOVED) {
+ // We pring an error just so that it's red, but it's just a warning really.
+ String msg = String.format(Messages.s_Removed_Recreating_s, fileName);
+ AdtPlugin.printErrorToConsole(mBuilder.getProject(), msg);
+ } else if (kind == IResourceDelta.CHANGED) {
+ // the file was modified manually! we can't allow it.
+ String msg = String.format(Messages.s_Modified_Manually_Recreating_s, fileName);
+ AdtPlugin.printErrorToConsole(mBuilder.getProject(), msg);
+ }
+ }
+
+ // no children.
+ return false;
+ } else if (mInRes) {
+ // if we are in the res folder, we are looking for the following
+ // changes:
+ // - added/removed/modified xml files.
+ // - added/removed files of any other type
+
+ // if the resource is a folder, we just go straight to the
+ // children
+ if (resource.getType() == IResource.FOLDER) {
+ return true;
+ }
+
+ // get the extension of the resource
+ String ext = resource.getFileExtension();
+ int kind = delta.getKind();
+
+ String p = resource.getProjectRelativePath().toString();
+ String message = null;
+ switch (kind) {
+ case IResourceDelta.CHANGED:
+ // display verbose message
+ message = String.format(Messages.s_Modified_Recreating_s, p,
+ AndroidConstants.FN_RESOURCE_CLASS);
+ break;
+ case IResourceDelta.ADDED:
+ // display verbose message
+ message = String.format(Messages.Added_s_s_Needs_Updating, p,
+ AndroidConstants.FN_RESOURCE_CLASS);
+ break;
+ case IResourceDelta.REMOVED:
+ // display verbose message
+ message = String.format(Messages.s_Removed_s_Needs_Updating, p,
+ AndroidConstants.FN_RESOURCE_CLASS);
+ break;
+ }
+ if (message != null) {
+ AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE,
+ mBuilder.getProject(), message);
+ }
+
+ if (AndroidConstants.EXT_XML.equalsIgnoreCase(ext)) {
+ if (kind != IResourceDelta.REMOVED) {
+ // check xml Validity
+ mBuilder.checkXML(resource, this);
+ }
+
+ // if we are going through this resource, it was modified
+ // somehow.
+ // we don't care if it was an added/removed/changed event
+ mCompileResources = true;
+ return false;
+ } else {
+ // this is a non xml resource.
+ if (kind == IResourceDelta.ADDED
+ || kind == IResourceDelta.REMOVED) {
+ mCompileResources = true;
+ return false;
+ }
+ }
+ } else if (resource instanceof IFolder) {
+ // in this case we may be inside a folder that contains a source
+ // folder.
+ String[] sourceFolderSegments = findMatchingSourceFolder(mSourceFolders, segments);
+ if (sourceFolderSegments != null) {
+ // we have a match!
+ mInRes = false;
+
+ // Check if the current folder is actually a source folder
+ if (sourceFolderSegments.length == segments.length) {
+ mInSrc = true;
+ }
+
+ // and return true to visit the content, no matter what
+ return true;
+ }
+
+ // if we're here, we are visiting another folder
+ // like /$Project/bin/ for instance (we get notified for changes
+ // in .class!)
+ // This could also be another source folder and we have found
+ // R.java in a previous source folder
+ // We don't want to visit its children
+ return false;
+ }
+
+ return false;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ResourceManagerBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ResourceManagerBuilder.java
new file mode 100644
index 0000000..19d7185
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/ResourceManagerBuilder.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.build;
+
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.project.ProjectHelper;
+import com.android.ide.eclipse.adt.sdk.LoadStatus;
+import com.android.ide.eclipse.adt.sdk.Sdk;
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.common.project.BaseProjectHelper;
+import com.android.sdklib.IAndroidTarget;
+
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+
+import java.util.Map;
+
+/**
+ * Resource manager builder whose only purpose is to refresh the resource folder
+ * so that the other builder use an up to date version.
+ */
+public class ResourceManagerBuilder extends BaseBuilder {
+
+ public static final String ID = "com.android.ide.eclipse.adt.ResourceManagerBuilder"; //$NON-NLS-1$
+
+ public ResourceManagerBuilder() {
+ super();
+ }
+
+ // build() returns a list of project from which this project depends for future compilation.
+ @SuppressWarnings("unchecked") //$NON-NLS-1$
+ @Override
+ protected IProject[] build(int kind, Map args, IProgressMonitor monitor)
+ throws CoreException {
+ // Get the project.
+ IProject project = getProject();
+
+ // Clear the project of the generic markers
+ BaseBuilder.removeMarkersFromProject(project, AdtConstants.MARKER_ADT);
+
+ // Check the compiler compliance level, displaying the error message
+ // since this is the first builder.
+ int res = ProjectHelper.checkCompilerCompliance(project);
+ String errorMessage = null;
+ switch (res) {
+ case ProjectHelper.COMPILER_COMPLIANCE_LEVEL:
+ errorMessage = Messages.Requires_Compiler_Compliance_5;
+ case ProjectHelper.COMPILER_COMPLIANCE_SOURCE:
+ errorMessage = Messages.Requires_Source_Compatibility_5;
+ case ProjectHelper.COMPILER_COMPLIANCE_CODEGEN_TARGET:
+ errorMessage = Messages.Requires_Class_Compatibility_5;
+ }
+
+ if (errorMessage != null) {
+ BaseProjectHelper.addMarker(project, AdtConstants.MARKER_ADT, errorMessage,
+ IMarker.SEVERITY_ERROR);
+ AdtPlugin.printErrorToConsole(project, errorMessage);
+
+ // interrupt the build. The next builders will not run.
+ stopBuild(errorMessage);
+ }
+
+ // Check that the SDK directory has been setup.
+ String osSdkFolder = AdtPlugin.getOsSdkFolder();
+
+ if (osSdkFolder == null || osSdkFolder.length() == 0) {
+ AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project,
+ Messages.No_SDK_Setup_Error);
+ markProject(AdtConstants.MARKER_ADT, Messages.No_SDK_Setup_Error,
+ IMarker.SEVERITY_ERROR);
+
+ // This interrupts the build. The next builders will not run.
+ stopBuild(Messages.No_SDK_Setup_Error);
+ }
+
+ // check if we have finished loading the SDK.
+ if (AdtPlugin.getDefault().getSdkLoadStatus() != LoadStatus.LOADED) {
+ // we exit silently
+ // This interrupts the build. The next builders will not run.
+ stopBuild("SDK is not loaded yet");
+ }
+
+ // check the project has a target
+ IAndroidTarget projectTarget = Sdk.getCurrent().getTarget(project);
+ if (projectTarget == null) {
+ // no target. marker has been set by the container initializer: exit silently.
+ // This interrupts the build. The next builders will not run.
+ stopBuild("Project has no target");
+ }
+
+ // Check the preference to be sure we are supposed to refresh
+ // the folders.
+ if (AdtPlugin.getAutoResRefresh()) {
+ AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, project,
+ Messages.Refreshing_Res);
+
+ // refresh the res folder.
+ IFolder resFolder = project.getFolder(
+ AndroidConstants.WS_RESOURCES);
+ resFolder.refreshLocal(IResource.DEPTH_INFINITE, monitor);
+
+ // Also refresh the assets folder to make sure the ApkBuilder
+ // will now it's changed and will force a new resource packaging.
+ IFolder assetsFolder = project.getFolder(
+ AndroidConstants.WS_ASSETS);
+ assetsFolder.refreshLocal(IResource.DEPTH_INFINITE, monitor);
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/build_messages.properties b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/build_messages.properties
new file mode 100644
index 0000000..8ba43d4
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/build/build_messages.properties
@@ -0,0 +1,61 @@
+Start_Full_Apk_Build=Starting full Package build.
+Start_Full_Pre_Compiler=Starting full Pre Compiler.
+Start_Inc_Apk_Build=Starting incremental Package build: Checking resource changes.
+Start_Inc_Pre_Compiler=Starting incremental Pre Compiler: Checking resource changes.
+Xml_Error=Error in an XML file: aborting build.
+s_Missing_Repackaging=%1$s missing. Repackaging.
+Project_Has_Errors=Project contains error(s). Package Builder aborted.
+Failed_To_Get_Output=Failed to get project output folder\!
+Output_Missing=Output folder missing\! Make sure your project is configured properly.
+s_File_Missing=%1$s file missing\!
+Unparsed_AAPT_Errors=Unparsed aapt error(s)\! Check the console for output.
+Unparsed_AIDL_Errors=Unparsed aidl error\! Check the console for output.
+AAPT_Exec_Error=Error executing aapt. Please check aapt is present at %1$s
+Dalvik_Error_d=Conversion to Dalvik format failed with error %1$d
+DX_Jar_Error=Dx.jar is not found inside the plugin. Reinstall ADT\!
+Dalvik_Error_s=Conversion to Dalvik format failed: %1$s
+Incompatible_VM_Warning=Note: You may be using an incompatible virtual machine or class library.
+Requires_1_5_Error=This program requires JDK 1.5 compatibility.
+Final_Archive_Error_s=Error generating final archive: %1$s
+Marker_Delete_Error=Failed to delete marker '%1$s' for %2$s
+Couldnt_Locate_s_Error=Could not locate '%1$s'. This will not be added to the package.
+Compiler_Compliance_Error=Compiler compliance level not compatible: Build aborted.
+No_SDK_Setup_Error=SDK directory has not been setup. Please go to the Android preferences and enter the location of the SDK.
+s_Contains_Xml_Error=%1$s contains XML error: Build aborted.
+s_Doesnt_Declare_Package_Error=%1$s does not declare a Java package: Build aborted.
+Checking_Package_Change=Checking Java package value did not change...
+Package_s_Doesnt_Exist_Error=Package '%1$s' does not exist\!
+Preparing_Generated_Files=Preparing generated java files for update/creation.
+AAPT_Error='aapt' error. Pre Compiler Build aborted.
+Nothing_To_Compile=Nothing to pre compile\!
+Removing_Generated_Classes=Removing generated java classes.
+Delete_Obsolete_Error=Failed to delete obsolete %1$s, please delete it manually
+DexWrapper_Dex_Loader=Dex Loader
+AIDL_Java_Conflict=%1$s is in the way of %2$s, remove it or rename of one the files.
+AIDL_Exec_Error=Error executing aidl. Please check aidl is present at %1$s
+s_Removed_Recreating_s=%1$s was removed\! Recreating %1$s\!
+s_Modified_Manually_Recreating_s=%1$s was modified manually\! Reverting to generated version\!
+s_Modified_Recreating_s='%1$s' was modified, %2$s needs to be updated.
+Added_s_s_Needs_Updating=New resource file: '%1$s', %2$s needs to be updated.
+s_Removed_s_Needs_Updating='%1$s' was removed, %2$s needs to be updated.
+Requires_Compiler_Compliance_5=Android requires compiler compliance level 5.0. Please fix project properties.
+Requires_Source_Compatibility_5=Android requires source compatibility set to 5.0. Please fix project properties.
+Requires_Class_Compatibility_5=Android requires .class compatibility set to 5.0. Please fix project properties.
+Refreshing_Res=Refreshing resource folders.
+DexWrapper_s_does_not_exists=%1$s does not exist or is not a file
+DexWrapper_Failed_to_load_s=Failed to load %1$s
+DexWrapper_Unable_To_Execute_Dex_s=Unable to execute dex: %1$s
+DexWrapper_SecuryEx_Unable_To_Find_API=SecurityException: Unable to find API for dex.jar
+DexWrapper_SecuryEx_Unable_To_Find_Method=SecurityException: Unable to find method for dex.jar
+DexWrapper_SecuryEx_Unable_To_Find_Field=SecurityException: Unable to find field for dex.jar
+ApkBuilder_UnableBuild_Dex_Not_loaded=Unable to build: the file dex.jar was not loaded from the SDK folder\!
+ApkBuilder_Using_Default_Key=Using default debug key to sign package
+ApkBuilder_Using_s_To_Sign=Using '%1$s' to sign package
+ApkBuilder_Signing_Key_Creation_s=Signing Key Creation:
+ApkBuilder_Unable_To_Gey_Key=Unable to get debug signature key
+ApkBuilder_Certificate_Expired_on_s=Debug certificate expired on %1$s\!
+ApkBuilder_Packaging_s=Packaging %1$s
+ApkBuilder_JAVA_HOME_is_s=The Java VM Home used is: %1$s
+ApkBuilder_Update_or_Execute_manually_s=Update it if necessary, or manually execute the following command:
+ApkBuilder_s_Conflict_with_file_s=%1$s conflicts with another file already put at %2$s
+ApkBuilder_Packaging_s_into_s=Packaging %1$s into %2$s
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/AndroidLaunch.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/AndroidLaunch.java
new file mode 100644
index 0000000..3d60401
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/AndroidLaunch.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.debug.launching;
+
+import org.eclipse.debug.core.DebugException;
+import org.eclipse.debug.core.ILaunchConfiguration;
+import org.eclipse.debug.core.ILaunchManager;
+import org.eclipse.debug.core.Launch;
+import org.eclipse.debug.core.model.ISourceLocator;
+
+/**
+ * Custom implementation of Launch to allow access to the LaunchManager
+ *
+ */
+class AndroidLaunch extends Launch {
+
+ /**
+ * Basic constructor does nothing special
+ * @param launchConfiguration
+ * @param mode
+ * @param locator
+ */
+ public AndroidLaunch(ILaunchConfiguration launchConfiguration, String mode,
+ ISourceLocator locator) {
+ super(launchConfiguration, mode, locator);
+ }
+
+ /** Stops the launch, and removes it from the launch manager */
+ public void stopLaunch() {
+ ILaunchManager mgr = getLaunchManager();
+
+ if (canTerminate()) {
+ try {
+ terminate();
+ } catch (DebugException e) {
+ // well looks like we couldn't stop it. nothing else to be
+ // done really
+ }
+ }
+ // remove the launch
+ mgr.removeLaunch(this);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/AndroidLaunchController.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/AndroidLaunchController.java
new file mode 100644
index 0000000..ac003df
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/AndroidLaunchController.java
@@ -0,0 +1,1838 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.debug.launching;
+
+import com.android.ddmlib.AndroidDebugBridge;
+import com.android.ddmlib.Client;
+import com.android.ddmlib.ClientData;
+import com.android.ddmlib.Device;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.MultiLineReceiver;
+import com.android.ddmlib.SyncService;
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+import com.android.ddmlib.AndroidDebugBridge.IDebugBridgeChangeListener;
+import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener;
+import com.android.ddmlib.SyncService.SyncResult;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.debug.launching.DeviceChooserDialog.DeviceChooserResponse;
+import com.android.ide.eclipse.adt.debug.ui.EmulatorConfigTab;
+import com.android.ide.eclipse.adt.project.ProjectHelper;
+import com.android.ide.eclipse.adt.sdk.Sdk;
+import com.android.ide.eclipse.common.project.AndroidManifestHelper;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.SdkManager;
+import com.android.sdklib.avd.AvdManager;
+import com.android.sdklib.avd.AvdManager.AvdInfo;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.debug.core.DebugPlugin;
+import org.eclipse.debug.core.ILaunchConfiguration;
+import org.eclipse.debug.core.ILaunchConfigurationType;
+import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
+import org.eclipse.debug.core.ILaunchManager;
+import org.eclipse.debug.core.model.IDebugTarget;
+import org.eclipse.debug.ui.DebugUITools;
+import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
+import org.eclipse.jdt.launching.IVMConnector;
+import org.eclipse.jdt.launching.JavaRuntime;
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.preference.IPreferenceStore;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map.Entry;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Controls the launch of Android application either on a device or on the
+ * emulator. If an emulator is already running, this class will attempt to reuse
+ * it.
+ */
+public final class AndroidLaunchController implements IDebugBridgeChangeListener,
+ IDeviceChangeListener, IClientChangeListener {
+
+ private static final String FLAG_AVD = "-avd"; //$NON-NLS-1$
+ private static final String FLAG_NETDELAY = "-netdelay"; //$NON-NLS-1$
+ private static final String FLAG_NETSPEED = "-netspeed"; //$NON-NLS-1$
+ private static final String FLAG_WIPE_DATA = "-wipe-data"; //$NON-NLS-1$
+ private static final String FLAG_NO_BOOT_ANIM = "-no-boot-anim"; //$NON-NLS-1$
+
+ private static final int MAX_ATTEMPT_COUNT = 5;
+
+ private final static Pattern sAmErrorType = Pattern.compile("Error type (\\d+)"); //$NON-NLS-1$
+
+ /**
+ * A delayed launch waiting for a device to be present or ready before the
+ * application is launched.
+ */
+ static final class DelayedLaunchInfo {
+ /** The device on which to launch the app */
+ Device mDevice = null;
+
+ /** The eclipse project */
+ IProject mProject;
+
+ /** Package name */
+ String mPackageName;
+
+ /** fully qualified name of the activity */
+ String mActivity;
+
+ /** IFile to the package (.apk) file */
+ IFile mPackageFile;
+
+ /** Debuggable attribute of the manifest file. */
+ Boolean mDebuggable = null;
+
+ /** Required ApiVersionNumber by the app. 0 means no requirements */
+ int mRequiredApiVersionNumber = 0;
+
+ InstallRetryMode mRetryMode = InstallRetryMode.NEVER;
+
+ /**
+ * Launch action. See {@link LaunchConfigDelegate#ACTION_DEFAULT},
+ * {@link LaunchConfigDelegate#ACTION_ACTIVITY},
+ * {@link LaunchConfigDelegate#ACTION_DO_NOTHING}
+ */
+ int mLaunchAction;
+
+ /** the launch object */
+ AndroidLaunch mLaunch;
+
+ /** the monitor object */
+ IProgressMonitor mMonitor;
+
+ /** debug mode flag */
+ boolean mDebugMode;
+
+ int mAttemptCount = 0;
+
+ boolean mCancelled = false;
+
+ /** Basic constructor with activity and package info. */
+ private DelayedLaunchInfo(IProject project, String packageName, String activity,
+ IFile pack, Boolean debuggable, int requiredApiVersionNumber, int launchAction,
+ AndroidLaunch launch, IProgressMonitor monitor) {
+ mProject = project;
+ mPackageName = packageName;
+ mActivity = activity;
+ mPackageFile = pack;
+ mLaunchAction = launchAction;
+ mLaunch = launch;
+ mMonitor = monitor;
+ mDebuggable = debuggable;
+ mRequiredApiVersionNumber = requiredApiVersionNumber;
+ }
+ }
+
+ /**
+ * Map to store {@link ILaunchConfiguration} objects that must be launched as simple connection
+ * to running application. The integer is the port on which to connect.
+ * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b>
+ */
+ private final static HashMap<ILaunchConfiguration, Integer> sRunningAppMap =
+ new HashMap<ILaunchConfiguration, Integer>();
+
+ private final static Object sListLock = sRunningAppMap;
+
+ /**
+ * List of {@link DelayedLaunchInfo} waiting for an emulator to connect.
+ * <p>Once an emulator has connected, {@link DelayedLaunchInfo#mDevice} is set and the
+ * DelayedLaunchInfo object is moved to {@link AndroidLaunchController#mWaitingForReadyEmulatorList}.
+ * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b>
+ */
+ private final ArrayList<DelayedLaunchInfo> mWaitingForEmulatorLaunches =
+ new ArrayList<DelayedLaunchInfo>();
+
+ /**
+ * List of application waiting to be launched on a device/emulator.<br>
+ * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b>
+ * */
+ private final ArrayList<DelayedLaunchInfo> mWaitingForReadyEmulatorList =
+ new ArrayList<DelayedLaunchInfo>();
+
+ /**
+ * Application waiting to show up as waiting for debugger.
+ * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b>
+ */
+ private final ArrayList<DelayedLaunchInfo> mWaitingForDebuggerApplications =
+ new ArrayList<DelayedLaunchInfo>();
+
+ /**
+ * List of clients that have appeared as waiting for debugger before their name was available.
+ * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b>
+ */
+ private final ArrayList<Client> mUnknownClientsWaitingForDebugger = new ArrayList<Client>();
+
+ /** static instance for singleton */
+ private static AndroidLaunchController sThis = new AndroidLaunchController();
+
+ enum InstallRetryMode {
+ NEVER, ALWAYS, PROMPT;
+ }
+
+ /**
+ * Launch configuration data. This stores the result of querying the
+ * {@link ILaunchConfiguration} so that it's only done once.
+ */
+ static final class AndroidLaunchConfiguration {
+
+ /**
+ * Launch action. See {@link LaunchConfigDelegate#ACTION_DEFAULT},
+ * {@link LaunchConfigDelegate#ACTION_ACTIVITY},
+ * {@link LaunchConfigDelegate#ACTION_DO_NOTHING}
+ */
+ public int mLaunchAction = LaunchConfigDelegate.DEFAULT_LAUNCH_ACTION;
+
+ public static final boolean AUTO_TARGET_MODE = true;
+
+ /**
+ * Target selection mode.
+ * <ul>
+ * <li><code>true</code>: automatic mode, see {@link #AUTO_TARGET_MODE}</li>
+ * <li><code>false</code>: manual mode</li>
+ * </ul>
+ */
+ public boolean mTargetMode = LaunchConfigDelegate.DEFAULT_TARGET_MODE;
+
+ /**
+ * Indicates whether the emulator should be called with -wipe-data
+ */
+ public boolean mWipeData = LaunchConfigDelegate.DEFAULT_WIPE_DATA;
+
+ /**
+ * Indicates whether the emulator should be called with -no-boot-anim
+ */
+ public boolean mNoBootAnim = LaunchConfigDelegate.DEFAULT_NO_BOOT_ANIM;
+
+ /**
+ * AVD Name.
+ */
+ public String mAvdName = null;
+
+ public String mNetworkSpeed = EmulatorConfigTab.getSpeed(
+ LaunchConfigDelegate.DEFAULT_SPEED);
+ public String mNetworkDelay = EmulatorConfigTab.getDelay(
+ LaunchConfigDelegate.DEFAULT_DELAY);
+
+ /**
+ * Optional custom command line parameter to launch the emulator
+ */
+ public String mEmulatorCommandLine;
+
+ /**
+ * Initialized the structure from an ILaunchConfiguration object.
+ * @param config
+ */
+ public void set(ILaunchConfiguration config) {
+ try {
+ mLaunchAction = config.getAttribute(LaunchConfigDelegate.ATTR_LAUNCH_ACTION,
+ mLaunchAction);
+ } catch (CoreException e1) {
+ // nothing to be done here, we'll use the default value
+ }
+
+ try {
+ mTargetMode = config.getAttribute(LaunchConfigDelegate.ATTR_TARGET_MODE,
+ mTargetMode);
+ } catch (CoreException e) {
+ // nothing to be done here, we'll use the default value
+ }
+
+ try {
+ mAvdName = config.getAttribute(LaunchConfigDelegate.ATTR_AVD_NAME, mAvdName);
+ } catch (CoreException e) {
+ }
+
+ int index = LaunchConfigDelegate.DEFAULT_SPEED;
+ try {
+ index = config.getAttribute(LaunchConfigDelegate.ATTR_SPEED, index);
+ } catch (CoreException e) {
+ // nothing to be done here, we'll use the default value
+ }
+ mNetworkSpeed = EmulatorConfigTab.getSpeed(index);
+
+ index = LaunchConfigDelegate.DEFAULT_DELAY;
+ try {
+ index = config.getAttribute(LaunchConfigDelegate.ATTR_DELAY, index);
+ } catch (CoreException e) {
+ // nothing to be done here, we'll use the default value
+ }
+ mNetworkDelay = EmulatorConfigTab.getDelay(index);
+
+ try {
+ mEmulatorCommandLine = config.getAttribute(
+ LaunchConfigDelegate.ATTR_COMMANDLINE, ""); //$NON-NLS-1$
+ } catch (CoreException e) {
+ // lets not do anything here, we'll use the default value
+ }
+
+ try {
+ mWipeData = config.getAttribute(LaunchConfigDelegate.ATTR_WIPE_DATA, mWipeData);
+ } catch (CoreException e) {
+ // nothing to be done here, we'll use the default value
+ }
+
+ try {
+ mNoBootAnim = config.getAttribute(LaunchConfigDelegate.ATTR_NO_BOOT_ANIM,
+ mNoBootAnim);
+ } catch (CoreException e) {
+ // nothing to be done here, we'll use the default value
+ }
+ }
+ }
+
+ /**
+ * Output receiver for am process (activity Manager);
+ */
+ private final class AMReceiver extends MultiLineReceiver {
+ private DelayedLaunchInfo mLaunchInfo;
+ private Device mDevice;
+
+ /**
+ * Basic constructor.
+ * @param launchInfo The launch info associated with the am process.
+ * @param device The device on which the launch is done.
+ */
+ public AMReceiver(DelayedLaunchInfo launchInfo, Device device) {
+ mLaunchInfo = launchInfo;
+ mDevice = device;
+ }
+
+ @Override
+ public void processNewLines(String[] lines) {
+ // first we check if one starts with error
+ ArrayList<String> array = new ArrayList<String>();
+ boolean error = false;
+ boolean warning = false;
+ for (String s : lines) {
+ // ignore empty lines.
+ if (s.length() == 0) {
+ continue;
+ }
+
+ // check for errors that output an error type, if the attempt count is still
+ // valid. If not the whole text will be output in the console
+ if (mLaunchInfo.mAttemptCount < MAX_ATTEMPT_COUNT &&
+ mLaunchInfo.mCancelled == false) {
+ Matcher m = sAmErrorType.matcher(s);
+ if (m.matches()) {
+ // get the error type
+ int type = Integer.parseInt(m.group(1));
+
+ final int waitTime = 3;
+ String msg;
+
+ switch (type) {
+ case 1:
+ /* Intended fall through */
+ case 2:
+ msg = String.format(
+ "Device not ready. Waiting %1$d seconds before next attempt.",
+ waitTime);
+ break;
+ case 3:
+ msg = String.format(
+ "New package not yet registered with the system. Waiting %1$d seconds before next attempt.",
+ waitTime);
+ break;
+ default:
+ msg = String.format(
+ "Device not ready (%2$d). Waiting %1$d seconds before next attempt.",
+ waitTime, type);
+ break;
+
+ }
+
+ AdtPlugin.printToConsole(mLaunchInfo.mProject, msg);
+
+ // launch another thread, that waits a bit and attempts another launch
+ new Thread("Delayed Launch attempt") {
+ @Override
+ public void run() {
+ try {
+ sleep(waitTime * 1000);
+ } catch (InterruptedException e) {
+ }
+
+ launchApp(mLaunchInfo, mDevice);
+ }
+ }.start();
+
+ // no need to parse the rest
+ return;
+ }
+ }
+
+ // check for error if needed
+ if (error == false && s.startsWith("Error:")) { //$NON-NLS-1$
+ error = true;
+ }
+ if (warning == false && s.startsWith("Warning:")) { //$NON-NLS-1$
+ warning = true;
+ }
+
+ // add the line to the list
+ array.add("ActivityManager: " + s); //$NON-NLS-1$
+ }
+
+ // then we display them in the console
+ if (warning || error) {
+ AdtPlugin.printErrorToConsole(mLaunchInfo.mProject, array.toArray());
+ } else {
+ AdtPlugin.printToConsole(mLaunchInfo.mProject, array.toArray());
+ }
+
+ // if error then we cancel the launch, and remove the delayed info
+ if (error) {
+ mLaunchInfo.mLaunch.stopLaunch();
+ synchronized (sListLock) {
+ mWaitingForReadyEmulatorList.remove(mLaunchInfo);
+ }
+ }
+ }
+
+ public boolean isCancelled() {
+ return false;
+ }
+ }
+
+ /**
+ * Output receiver for "pm install package.apk" command line.
+ */
+ private final static class InstallReceiver extends MultiLineReceiver {
+
+ private final static String SUCCESS_OUTPUT = "Success"; //$NON-NLS-1$
+ private final static Pattern FAILURE_PATTERN = Pattern.compile("Failure\\s+\\[(.*)\\]"); //$NON-NLS-1$
+
+ private String mSuccess = null;
+
+ public InstallReceiver() {
+ }
+
+ @Override
+ public void processNewLines(String[] lines) {
+ for (String line : lines) {
+ if (line.length() > 0) {
+ if (line.startsWith(SUCCESS_OUTPUT)) {
+ mSuccess = null;
+ } else {
+ Matcher m = FAILURE_PATTERN.matcher(line);
+ if (m.matches()) {
+ mSuccess = m.group(1);
+ }
+ }
+ }
+ }
+ }
+
+ public boolean isCancelled() {
+ return false;
+ }
+
+ public String getSuccess() {
+ return mSuccess;
+ }
+ }
+
+
+ /** private constructor to enforce singleton */
+ private AndroidLaunchController() {
+ AndroidDebugBridge.addDebugBridgeChangeListener(this);
+ AndroidDebugBridge.addDeviceChangeListener(this);
+ AndroidDebugBridge.addClientChangeListener(this);
+ }
+
+ /**
+ * Returns the singleton reference.
+ */
+ public static AndroidLaunchController getInstance() {
+ return sThis;
+ }
+
+
+ /**
+ * Launches a remote java debugging session on an already running application
+ * @param project The project of the application to debug.
+ * @param debugPort The port to connect the debugger to.
+ */
+ public static void debugRunningApp(IProject project, int debugPort) {
+ // get an existing or new launch configuration
+ ILaunchConfiguration config = AndroidLaunchController.getLaunchConfig(project);
+
+ if (config != null) {
+ setPortLaunchConfigAssociation(config, debugPort);
+
+ // and launch
+ DebugUITools.launch(config, ILaunchManager.DEBUG_MODE);
+ }
+ }
+
+ /**
+ * Returns an {@link ILaunchConfiguration} for the specified {@link IProject}.
+ * @param project the project
+ * @return a new or already existing <code>ILaunchConfiguration</code> or null if there was
+ * an error when creating a new one.
+ */
+ public static ILaunchConfiguration getLaunchConfig(IProject project) {
+ // get the launch manager
+ ILaunchManager manager = DebugPlugin.getDefault().getLaunchManager();
+
+ // now get the config type for our particular android type.
+ ILaunchConfigurationType configType = manager.getLaunchConfigurationType(
+ LaunchConfigDelegate.ANDROID_LAUNCH_TYPE_ID);
+
+ String name = project.getName();
+
+ // search for an existing launch configuration
+ ILaunchConfiguration config = findConfig(manager, configType, name);
+
+ // test if we found one or not
+ if (config == null) {
+ // Didn't find a matching config, so we make one.
+ // It'll be made in the "working copy" object first.
+ ILaunchConfigurationWorkingCopy wc = null;
+
+ try {
+ // make the working copy object
+ wc = configType.newInstance(null,
+ manager.generateUniqueLaunchConfigurationNameFrom(name));
+
+ // set the project name
+ wc.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, name);
+
+ // set the launch mode to default.
+ wc.setAttribute(LaunchConfigDelegate.ATTR_LAUNCH_ACTION,
+ LaunchConfigDelegate.DEFAULT_LAUNCH_ACTION);
+
+ // set default target mode
+ wc.setAttribute(LaunchConfigDelegate.ATTR_TARGET_MODE,
+ LaunchConfigDelegate.DEFAULT_TARGET_MODE);
+
+ // default AVD: None
+ wc.setAttribute(LaunchConfigDelegate.ATTR_AVD_NAME, (String)null);
+
+ // set the default network speed
+ wc.setAttribute(LaunchConfigDelegate.ATTR_SPEED,
+ LaunchConfigDelegate.DEFAULT_SPEED);
+
+ // and delay
+ wc.setAttribute(LaunchConfigDelegate.ATTR_DELAY,
+ LaunchConfigDelegate.DEFAULT_DELAY);
+
+ // default wipe data mode
+ wc.setAttribute(LaunchConfigDelegate.ATTR_WIPE_DATA,
+ LaunchConfigDelegate.DEFAULT_WIPE_DATA);
+
+ // default disable boot animation option
+ wc.setAttribute(LaunchConfigDelegate.ATTR_NO_BOOT_ANIM,
+ LaunchConfigDelegate.DEFAULT_NO_BOOT_ANIM);
+
+ // set default emulator options
+ IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore();
+ String emuOptions = store.getString(AdtPlugin.PREFS_EMU_OPTIONS);
+ wc.setAttribute(LaunchConfigDelegate.ATTR_COMMANDLINE, emuOptions);
+
+ // map the config and the project
+ wc.setMappedResources(getResourcesToMap(project));
+
+ // save the working copy to get the launch config object which we return.
+ return wc.doSave();
+
+ } catch (CoreException e) {
+ String msg = String.format(
+ "Failed to create a Launch config for project '%1$s': %2$s",
+ project.getName(), e.getMessage());
+ AdtPlugin.printErrorToConsole(project, msg);
+
+ // no launch!
+ return null;
+ }
+ }
+
+ return config;
+ }
+
+ /**
+ * Returns the list of resources to map to a Launch Configuration.
+ * @param project the project associated to the launch configuration.
+ */
+ public static IResource[] getResourcesToMap(IProject project) {
+ ArrayList<IResource> array = new ArrayList<IResource>(2);
+ array.add(project);
+
+ AndroidManifestHelper helper = new AndroidManifestHelper(project);
+ IFile manifest = helper.getManifestIFile();
+ if (manifest != null) {
+ array.add(manifest);
+ }
+
+ return array.toArray(new IResource[array.size()]);
+ }
+
+ /**
+ * Launches an android app on the device or emulator
+ *
+ * @param project The project we're launching
+ * @param mode the mode in which to launch, one of the mode constants
+ * defined by <code>ILaunchManager</code> - <code>RUN_MODE</code> or
+ * <code>DEBUG_MODE</code>.
+ * @param apk the resource to the apk to launch.
+ * @param debuggable the debuggable value of the app, or null if not set.
+ * @param requiredApiVersionNumber the api version required by the app, or -1 if none.
+ * @param activity the class to provide to am to launch
+ * @param config the launch configuration
+ * @param launch the launch object
+ */
+ public void launch(final IProject project, String mode, IFile apk,
+ String packageName, Boolean debuggable, int requiredApiVersionNumber, String activity,
+ final AndroidLaunchConfiguration config, final AndroidLaunch launch,
+ IProgressMonitor monitor) {
+
+ String message;
+ if (config.mLaunchAction == LaunchConfigDelegate.ACTION_DO_NOTHING) {
+ message = String.format("Only Syncing Application Package");
+ } else {
+ message = String.format("Launching: %1$s", activity);
+ }
+ AdtPlugin.printToConsole(project, message);
+
+ // create the launch info
+ final DelayedLaunchInfo launchInfo = new DelayedLaunchInfo(project, packageName,
+ activity, apk, debuggable, requiredApiVersionNumber, config.mLaunchAction,
+ launch, monitor);
+
+ // set the debug mode
+ launchInfo.mDebugMode = mode.equals(ILaunchManager.DEBUG_MODE);
+
+ // get the SDK
+ Sdk currentSdk = Sdk.getCurrent();
+ AvdManager avdManager = currentSdk.getAvdManager();
+
+ // get the project target
+ final IAndroidTarget projectTarget = currentSdk.getTarget(project);
+
+ // FIXME: check errors on missing sdk, AVD manager, or project target.
+
+ // device chooser response object.
+ final DeviceChooserResponse response = new DeviceChooserResponse();
+
+ /*
+ * Launch logic:
+ * - Manually Mode
+ * Always display a UI that lets a user see the current running emulators/devices.
+ * The UI must show which devices are compatibles, and allow launching new emulators
+ * with compatible (and not yet running) AVD.
+ * - Automatic Way
+ * * Preferred AVD set.
+ * If Preferred AVD is not running: launch it.
+ * Launch the application on the preferred AVD.
+ * * No preferred AVD.
+ * Count the number of compatible emulators/devices.
+ * If != 1, display a UI similar to manual mode.
+ * If == 1, launch the application on this AVD/device.
+ */
+
+ if (config.mTargetMode == AndroidLaunchConfiguration.AUTO_TARGET_MODE) {
+ // if we are in automatic target mode, we need to find the current devices
+ Device[] devices = AndroidDebugBridge.getBridge().getDevices();
+
+ // first check if we have a preferred AVD name, and if it actually exists, and is valid
+ // (ie able to run the project).
+ // We need to check this in case the AVD was recreated with a different target that is
+ // not compatible.
+ AvdInfo preferredAvd = null;
+ if (config.mAvdName != null) {
+ preferredAvd = avdManager.getAvd(config.mAvdName);
+ if (projectTarget.isCompatibleBaseFor(preferredAvd.getTarget()) == false) {
+ preferredAvd = null;
+
+ AdtPlugin.printErrorToConsole(project, String.format(
+ "Preferred AVD '%1$s' is not compatible with the project target '%2$s'. Looking for a compatible AVD...",
+ config.mAvdName, projectTarget.getName()));
+ }
+ }
+
+ if (preferredAvd != null) {
+ // look for a matching device
+ for (Device d : devices) {
+ String deviceAvd = d.getAvdName();
+ if (deviceAvd != null && deviceAvd.equals(config.mAvdName)) {
+ response.setDeviceToUse(d);
+
+ AdtPlugin.printToConsole(project, String.format(
+ "Automatic Target Mode: Preferred AVD '%1$s' is available on emulator '%2$s'",
+ config.mAvdName, d));
+
+ continueLaunch(response, project, launch, launchInfo, config);
+ return;
+ }
+ }
+
+ // at this point we have a valid preferred AVD that is not running.
+ // We need to start it.
+ response.setAvdToLaunch(preferredAvd);
+
+ AdtPlugin.printToConsole(project, String.format(
+ "Automatic Target Mode: Preferred AVD '%1$s' is not available. Launching new emulator.",
+ config.mAvdName));
+
+ continueLaunch(response, project, launch, launchInfo, config);
+ return;
+ }
+
+ // no (valid) preferred AVD? look for one.
+ HashMap<Device, AvdInfo> compatibleRunningAvds = new HashMap<Device, AvdInfo>();
+ boolean hasDevice = false; // if there's 1+ device running, we may force manual mode,
+ // as we cannot always detect proper compatibility with
+ // devices. This is the case if the project target is not
+ // a standard platform
+ for (Device d : devices) {
+ String deviceAvd = d.getAvdName();
+ if (deviceAvd != null) { // physical devices return null.
+ AvdInfo info = avdManager.getAvd(deviceAvd);
+ if (info != null && projectTarget.isCompatibleBaseFor(info.getTarget())) {
+ compatibleRunningAvds.put(d, info);
+ }
+ } else {
+ if (projectTarget.isPlatform()) { // means this can run on any device as long
+ // as api level is high enough
+ String apiString = d.getProperty(SdkManager.PROP_VERSION_SDK);
+ try {
+ int apiNumber = Integer.parseInt(apiString);
+ if (apiNumber >= projectTarget.getApiVersionNumber()) {
+ // device is compatible with project
+ compatibleRunningAvds.put(d, null);
+ continue;
+ }
+ } catch (NumberFormatException e) {
+ // do nothing, we'll consider it a non compatible device below.
+ }
+ }
+ hasDevice = true;
+ }
+ }
+
+ // depending on the number of devices, we'll simulate an automatic choice
+ // from the device chooser or simply show up the device chooser.
+ if (hasDevice == false && compatibleRunningAvds.size() == 0) {
+ // if zero emulators/devices, we launch an emulator.
+ // We need to figure out which AVD first.
+
+ // we are going to take the closest AVD. ie a compatible AVD that has the API level
+ // closest to the project target.
+ AvdInfo[] avds = avdManager.getAvds();
+ AvdInfo defaultAvd = null;
+ for (AvdInfo avd : avds) {
+ if (projectTarget.isCompatibleBaseFor(avd.getTarget())) {
+ if (defaultAvd == null ||
+ avd.getTarget().getApiVersionNumber() <
+ defaultAvd.getTarget().getApiVersionNumber()) {
+ defaultAvd = avd;
+ }
+ }
+ }
+
+ if (defaultAvd != null) {
+ response.setAvdToLaunch(defaultAvd);
+
+ AdtPlugin.printToConsole(project, String.format(
+ "Automatic Target Mode: launching new emulator with compatible AVD '%1$s'",
+ defaultAvd.getName()));
+
+ continueLaunch(response, project, launch, launchInfo, config);
+ return;
+ } else {
+ // FIXME: ask the user if he wants to create a AVD.
+ // we found no compatible AVD.
+ AdtPlugin.printErrorToConsole(project, String.format(
+ "Failed to find a AVD compatible with target '%1$s'. Launch aborted.",
+ projectTarget.getName()));
+ launch.stopLaunch();
+ return;
+ }
+ } else if (hasDevice == false && compatibleRunningAvds.size() == 1) {
+ Entry<Device, AvdInfo> e = compatibleRunningAvds.entrySet().iterator().next();
+ response.setDeviceToUse(e.getKey());
+
+ // get the AvdInfo, if null, the device is a physical device.
+ AvdInfo avdInfo = e.getValue();
+ if (avdInfo != null) {
+ message = String.format("Automatic Target Mode: using existing emulator '%1$s' running compatible AVD '%2$s'",
+ response.getDeviceToUse(), e.getValue().getName());
+ } else {
+ message = String.format("Automatic Target Mode: using device '%1$s'",
+ response.getDeviceToUse());
+ }
+ AdtPlugin.printToConsole(project, message);
+
+ continueLaunch(response, project, launch, launchInfo, config);
+ return;
+ }
+
+ // if more than one device, we'll bring up the DeviceChooser dialog below.
+ if (compatibleRunningAvds.size() >= 2) {
+ message = "Automatic Target Mode: Several compatible targets. Please select a target device.";
+ } else if (hasDevice) {
+ message = "Automatic Target Mode: Unable to detect device compatibility. Please select a target device.";
+ }
+
+ AdtPlugin.printToConsole(project, message);
+ }
+
+ // bring up the device chooser.
+ AdtPlugin.getDisplay().asyncExec(new Runnable() {
+ public void run() {
+ try {
+ // open the chooser dialog. It'll fill 'response' with the device to use
+ // or the AVD to launch.
+ DeviceChooserDialog dialog = new DeviceChooserDialog(
+ AdtPlugin.getDisplay().getActiveShell(),
+ response, launchInfo.mPackageName, projectTarget);
+ if (dialog.open() == Dialog.OK) {
+ AndroidLaunchController.this.continueLaunch(response, project, launch,
+ launchInfo, config);
+ } else {
+ AdtPlugin.printErrorToConsole(project, "Launch canceled!");
+ launch.stopLaunch();
+ return;
+ }
+ } catch (Exception e) {
+ // there seems to be some case where the shell will be null. (might be
+ // an OS X bug). Because of this the creation of the dialog will throw
+ // and IllegalArg exception interrupting the launch with no user feedback.
+ // So we trap all the exception and display something.
+ String msg = e.getMessage();
+ if (msg == null) {
+ msg = e.getClass().getCanonicalName();
+ }
+ AdtPlugin.printErrorToConsole(project,
+ String.format("Error during launch: %s", msg));
+ launch.stopLaunch();
+ }
+ }
+ });
+ }
+
+ /**
+ * Continues the launch based on the DeviceChooser response.
+ * @param response the device chooser response
+ * @param project The project being launched
+ * @param launch The eclipse launch info
+ * @param launchInfo The {@link DelayedLaunchInfo}
+ * @param config The config needed to start a new emulator.
+ */
+ void continueLaunch(final DeviceChooserResponse response, final IProject project,
+ final AndroidLaunch launch, final DelayedLaunchInfo launchInfo,
+ final AndroidLaunchConfiguration config) {
+
+ // Since this is called from the UI thread we spawn a new thread
+ // to finish the launch.
+ new Thread() {
+ @Override
+ public void run() {
+ if (response.getAvdToLaunch() != null) {
+ // there was no selected device, we start a new emulator.
+ synchronized (sListLock) {
+ AvdInfo info = response.getAvdToLaunch();
+ mWaitingForEmulatorLaunches.add(launchInfo);
+ AdtPlugin.printToConsole(project, String.format(
+ "Launching a new emulator with Virtual Device '%1$s'",
+ info.getName()));
+ boolean status = launchEmulator(config, info);
+
+ if (status == false) {
+ // launching the emulator failed!
+ AdtPlugin.displayError("Emulator Launch",
+ "Couldn't launch the emulator! Make sure the SDK directory is properly setup and the emulator is not missing.");
+
+ // stop the launch and return
+ mWaitingForEmulatorLaunches.remove(launchInfo);
+ AdtPlugin.printErrorToConsole(project, "Launch canceled!");
+ launch.stopLaunch();
+ return;
+ }
+
+ return;
+ }
+ } else if (response.getDeviceToUse() != null) {
+ launchInfo.mDevice = response.getDeviceToUse();
+ simpleLaunch(launchInfo, launchInfo.mDevice);
+ }
+ }
+ }.start();
+ }
+
+ /**
+ * Queries for a debugger port for a specific {@link ILaunchConfiguration}.
+ * <p/>
+ * If the configuration and a debugger port where added through
+ * {@link #setPortLaunchConfigAssociation(ILaunchConfiguration, int)}, then this method
+ * will return the debugger port, and remove the configuration from the list.
+ * @param launchConfig the {@link ILaunchConfiguration}
+ * @return the debugger port or {@link LaunchConfigDelegate#INVALID_DEBUG_PORT} if the
+ * configuration was not setup.
+ */
+ static int getPortForConfig(ILaunchConfiguration launchConfig) {
+ synchronized (sListLock) {
+ Integer port = sRunningAppMap.get(launchConfig);
+ if (port != null) {
+ sRunningAppMap.remove(launchConfig);
+ return port;
+ }
+ }
+
+ return LaunchConfigDelegate.INVALID_DEBUG_PORT;
+ }
+
+ /**
+ * Set a {@link ILaunchConfiguration} and its associated debug port, in the list of
+ * launch config to connect directly to a running app instead of doing full launch (sync,
+ * launch, and connect to).
+ * @param launchConfig the {@link ILaunchConfiguration} object.
+ * @param port The debugger port to connect to.
+ */
+ private static void setPortLaunchConfigAssociation(ILaunchConfiguration launchConfig,
+ int port) {
+ synchronized (sListLock) {
+ sRunningAppMap.put(launchConfig, port);
+ }
+ }
+
+ /**
+ * Checks the build information, and returns whether the launch should continue.
+ * <p/>The value tested are:
+ * <ul>
+ * <li>Minimum API version requested by the application. If the target device does not match,
+ * the launch is canceled.</li>
+ * <li>Debuggable attribute of the application and whether or not the device requires it. If
+ * the device requires it and it is not set in the manifest, the launch will be forced to
+ * "release" mode instead of "debug"</li>
+ * <ul>
+ */
+ private boolean checkBuildInfo(DelayedLaunchInfo launchInfo, Device device) {
+ if (device != null) {
+ // check the app required API level versus the target device API level
+
+ String deviceApiVersionName = device.getProperty(Device.PROP_BUILD_VERSION);
+ String value = device.getProperty(Device.PROP_BUILD_VERSION_NUMBER);
+ int deviceApiVersionNumber = 0;
+ try {
+ deviceApiVersionNumber = Integer.parseInt(value);
+ } catch (NumberFormatException e) {
+ // pass, we'll keep the deviceVersionNumber value at 0.
+ }
+
+ if (launchInfo.mRequiredApiVersionNumber == 0) {
+ // warn the API level requirement is not set.
+ AdtPlugin.printErrorToConsole(launchInfo.mProject,
+ "WARNING: Application does not specify an API level requirement!");
+
+ // and display the target device API level (if known)
+ if (deviceApiVersionName == null || deviceApiVersionNumber == 0) {
+ AdtPlugin.printErrorToConsole(launchInfo.mProject,
+ "WARNING: Unknown device API version!");
+ } else {
+ AdtPlugin.printErrorToConsole(launchInfo.mProject, String.format(
+ "Device API version is %1$d (Android %2$s)", deviceApiVersionNumber,
+ deviceApiVersionName));
+ }
+ } else { // app requires a specific API level
+ if (deviceApiVersionName == null || deviceApiVersionNumber == 0) {
+ AdtPlugin.printToConsole(launchInfo.mProject,
+ "WARNING: Unknown device API version!");
+ } else if (deviceApiVersionNumber < launchInfo.mRequiredApiVersionNumber) {
+ String msg = String.format(
+ "ERROR: Application requires API version %1$d. Device API version is %2$d (Android %3$s).",
+ launchInfo.mRequiredApiVersionNumber, deviceApiVersionNumber,
+ deviceApiVersionName);
+ AdtPlugin.printErrorToConsole(launchInfo.mProject, msg);
+
+ // abort the launch
+ return false;
+ }
+ }
+
+ // now checks that the device/app can be debugged (if needed)
+ if (device.isEmulator() == false && launchInfo.mDebugMode) {
+ String debuggableDevice = device.getProperty(Device.PROP_DEBUGGABLE);
+ if (debuggableDevice != null && debuggableDevice.equals("0")) { //$NON-NLS-1$
+ // the device is "secure" and requires apps to declare themselves as debuggable!
+ if (launchInfo.mDebuggable == null) {
+ String message1 = String.format(
+ "Device '%1$s' requires that applications explicitely declare themselves as debuggable in their manifest.",
+ device.getSerialNumber());
+ String message2 = String.format("Application '%1$s' does not have the attribute 'debuggable' set to TRUE in its manifest and cannot be debugged.",
+ launchInfo.mPackageName);
+ AdtPlugin.printErrorToConsole(launchInfo.mProject, message1, message2);
+
+ // because am -D does not check for ro.debuggable and the
+ // 'debuggable' attribute, it is important we do not use the -D option
+ // in this case or the app will wait for a debugger forever and never
+ // really launch.
+ launchInfo.mDebugMode = false;
+ } else if (launchInfo.mDebuggable == Boolean.FALSE) {
+ String message = String.format("Application '%1$s' has its 'debuggable' attribute set to FALSE and cannot be debugged.",
+ launchInfo.mPackageName);
+ AdtPlugin.printErrorToConsole(launchInfo.mProject, message);
+
+ // because am -D does not check for ro.debuggable and the
+ // 'debuggable' attribute, it is important we do not use the -D option
+ // in this case or the app will wait for a debugger forever and never
+ // really launch.
+ launchInfo.mDebugMode = false;
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Do a simple launch on the specified device, attempting to sync the new
+ * package, and then launching the application. Failed sync/launch will
+ * stop the current AndroidLaunch and return false;
+ * @param launchInfo
+ * @param device
+ * @return true if succeed
+ */
+ private boolean simpleLaunch(DelayedLaunchInfo launchInfo, Device device) {
+ // API level check
+ if (checkBuildInfo(launchInfo, device) == false) {
+ AdtPlugin.printErrorToConsole(launchInfo.mProject, "Launch canceled!");
+ launchInfo.mLaunch.stopLaunch();
+ return false;
+ }
+
+ // sync the app
+ if (syncApp(launchInfo, device) == false) {
+ AdtPlugin.printErrorToConsole(launchInfo.mProject, "Launch canceled!");
+ launchInfo.mLaunch.stopLaunch();
+ return false;
+ }
+
+ // launch the app
+ launchApp(launchInfo, device);
+
+ return true;
+ }
+
+
+ /**
+ * Syncs the application on the device/emulator.
+ *
+ * @param launchInfo The Launch information object.
+ * @param device the device on which to sync the application
+ * @return true if the install succeeded.
+ */
+ private boolean syncApp(DelayedLaunchInfo launchInfo, Device device) {
+ SyncService sync = device.getSyncService();
+ if (sync != null) {
+ IPath path = launchInfo.mPackageFile.getLocation();
+ String message = String.format("Uploading %1$s onto device '%2$s'",
+ path.lastSegment(), device.getSerialNumber());
+ AdtPlugin.printToConsole(launchInfo.mProject, message);
+
+ String osLocalPath = path.toOSString();
+ String apkName = launchInfo.mPackageFile.getName();
+ String remotePath = "/data/local/tmp/" + apkName; //$NON-NLS-1$
+
+ SyncResult result = sync.pushFile(osLocalPath, remotePath,
+ SyncService.getNullProgressMonitor());
+
+ if (result.getCode() != SyncService.RESULT_OK) {
+ String msg = String.format("Failed to upload %1$s on '%2$s': %3$s",
+ apkName, device.getSerialNumber(), result.getMessage());
+ AdtPlugin.printErrorToConsole(launchInfo.mProject, msg);
+ return false;
+ }
+
+ // Now that the package is uploaded, we can install it properly.
+ // This will check that there isn't another apk declaring the same package, or
+ // that another install used a different key.
+ boolean installResult = installPackage(launchInfo, remotePath, device);
+
+ // now we delete the app we sync'ed
+ try {
+ device.executeShellCommand("rm " + remotePath, new MultiLineReceiver() { //$NON-NLS-1$
+ @Override
+ public void processNewLines(String[] lines) {
+ // pass
+ }
+ public boolean isCancelled() {
+ return false;
+ }
+ });
+ } catch (IOException e) {
+ AdtPlugin.printErrorToConsole(launchInfo.mProject, String.format(
+ "Failed to delete temporary package: %1$s", e.getMessage()));
+ return false;
+ }
+
+ return installResult;
+ }
+
+ String msg = String.format(
+ "Failed to upload %1$s on device '%2$s': Unable to open sync connection!",
+ launchInfo.mPackageFile.getName(), device.getSerialNumber());
+ AdtPlugin.printErrorToConsole(launchInfo.mProject, msg);
+
+ return false;
+ }
+
+ /**
+ * Installs the application package that was pushed to a temporary location on the device.
+ * @param launchInfo The launch information
+ * @param remotePath The remote path of the package.
+ * @param device The device on which the launch is done.
+ */
+ private boolean installPackage(DelayedLaunchInfo launchInfo, final String remotePath,
+ final Device device) {
+
+ String message = String.format("Installing %1$s...", launchInfo.mPackageFile.getName());
+ AdtPlugin.printToConsole(launchInfo.mProject, message);
+
+ try {
+ String result = doInstall(launchInfo, remotePath, device, false /* reinstall */);
+
+ /* For now we force to retry the install (after uninstalling) because there's no
+ * other way around it: adb install does not want to update a package w/o uninstalling
+ * the old one first!
+ */
+ return checkInstallResult(result, device, launchInfo, remotePath,
+ InstallRetryMode.ALWAYS);
+ } catch (IOException e) {
+ // do nothing, we'll return false
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks the result of an installation, and takes optional actions based on it.
+ * @param result the result string from the installation
+ * @param device the device on which the installation occured.
+ * @param launchInfo the {@link DelayedLaunchInfo}
+ * @param remotePath the temporary path of the package on the device
+ * @param retryMode indicates what to do in case, a package already exists.
+ * @return <code>true<code> if success, <code>false</code> otherwise.
+ * @throws IOException
+ */
+ private boolean checkInstallResult(String result, Device device, DelayedLaunchInfo launchInfo,
+ String remotePath, InstallRetryMode retryMode) throws IOException {
+ if (result == null) {
+ AdtPlugin.printToConsole(launchInfo.mProject, "Success!");
+ return true;
+ } else if (result.equals("INSTALL_FAILED_ALREADY_EXISTS")) { //$NON-NLS-1$
+ if (retryMode == InstallRetryMode.PROMPT) {
+ boolean prompt = AdtPlugin.displayPrompt("Application Install",
+ "A previous installation needs to be uninstalled before the new package can be installed.\nDo you want to uninstall?");
+ if (prompt) {
+ retryMode = InstallRetryMode.ALWAYS;
+ } else {
+ AdtPlugin.printErrorToConsole(launchInfo.mProject,
+ "Installation error! The package already exists.");
+ return false;
+ }
+ }
+
+ if (retryMode == InstallRetryMode.ALWAYS) {
+ /*
+ * TODO: create a UI that gives the dev the choice to:
+ * - clean uninstall on launch
+ * - full uninstall if application exists.
+ * - soft uninstall if application exists (keeps the app data around).
+ * - always ask (choice of soft-reinstall, full reinstall)
+ AdtPlugin.printErrorToConsole(launchInfo.mProject,
+ "Application already exists, uninstalling...");
+ String res = doUninstall(device, launchInfo);
+ if (res == null) {
+ AdtPlugin.printToConsole(launchInfo.mProject, "Success!");
+ } else {
+ AdtPlugin.printErrorToConsole(launchInfo.mProject,
+ String.format("Failed to uninstall: %1$s", res));
+ return false;
+ }
+ */
+
+ AdtPlugin.printToConsole(launchInfo.mProject,
+ "Application already exists. Attempting to re-install instead...");
+ String res = doInstall(launchInfo, remotePath, device, true /* reinstall */);
+ return checkInstallResult(res, device, launchInfo, remotePath,
+ InstallRetryMode.NEVER);
+ }
+
+ AdtPlugin.printErrorToConsole(launchInfo.mProject,
+ "Installation error! The package already exists.");
+ } else if (result.equals("INSTALL_FAILED_INVALID_APK")) { //$NON-NLS-1$
+ AdtPlugin.printErrorToConsole(launchInfo.mProject,
+ "Installation failed due to invalid APK file!",
+ "Please check logcat output for more details.");
+ } else if (result.equals("INSTALL_FAILED_INVALID_URI")) { //$NON-NLS-1$
+ AdtPlugin.printErrorToConsole(launchInfo.mProject,
+ "Installation failed due to invalid URI!",
+ "Please check logcat output for more details.");
+ } else if (result.equals("INSTALL_FAILED_COULDNT_COPY")) { //$NON-NLS-1$
+ AdtPlugin.printErrorToConsole(launchInfo.mProject,
+ String.format("Installation failed: Could not copy %1$s to its final location!",
+ launchInfo.mPackageFile.getName()),
+ "Please check logcat output for more details.");
+ } else if (result.equals("INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES")) {
+ AdtPlugin.printErrorToConsole(launchInfo.mProject,
+ "Re-installation failed due to different application signatures.",
+ "You must perform a full uninstall of the application. WARNING: This will remove the application data!",
+ String.format("Please execute 'adb uninstall %1$s' in a shell.", launchInfo.mPackageName));
+ } else {
+ AdtPlugin.printErrorToConsole(launchInfo.mProject,
+ String.format("Installation error: %1$s", result),
+ "Please check logcat output for more details.");
+ }
+
+ return false;
+ }
+
+ /**
+ * Performs the uninstallation of an application.
+ * @param device the device on which to install the application.
+ * @param launchInfo the {@link DelayedLaunchInfo}.
+ * @return a {@link String} with an error code, or <code>null</code> if success.
+ * @throws IOException
+ */
+ @SuppressWarnings("unused")
+ private String doUninstall(Device device, DelayedLaunchInfo launchInfo) throws IOException {
+ InstallReceiver receiver = new InstallReceiver();
+ try {
+ device.executeShellCommand("pm uninstall " + launchInfo.mPackageName, //$NON-NLS-1$
+ receiver);
+ } catch (IOException e) {
+ String msg = String.format(
+ "Failed to uninstall %1$s: %2$s", launchInfo.mPackageName, e.getMessage());
+ AdtPlugin.printErrorToConsole(launchInfo.mProject, msg);
+ throw e;
+ }
+
+ return receiver.getSuccess();
+ }
+
+ /**
+ * Performs the installation of an application whose package has been uploaded on the device.
+ * <p/>Before doing it, if the application is already running on the device, it is killed.
+ * @param launchInfo the {@link DelayedLaunchInfo}.
+ * @param remotePath the path of the application package in the device tmp folder.
+ * @param device the device on which to install the application.
+ * @param reinstall
+ * @return a {@link String} with an error code, or <code>null</code> if success.
+ * @throws IOException
+ */
+ private String doInstall(DelayedLaunchInfo launchInfo, final String remotePath,
+ final Device device, boolean reinstall) throws IOException {
+ // kill running application
+ Client application = device.getClient(launchInfo.mPackageName);
+ if (application != null) {
+ application.kill();
+ }
+
+ InstallReceiver receiver = new InstallReceiver();
+ try {
+ String cmd = String.format(
+ reinstall ? "pm install -r \"%1$s\"" : "pm install \"%1$s\"", //$NON-NLS-1$ //$NON-NLS-2$
+ remotePath); //$NON-NLS-1$ //$NON-NLS-2$
+ device.executeShellCommand(cmd, receiver);
+ } catch (IOException e) {
+ String msg = String.format(
+ "Failed to install %1$s on device '%2$s': %3$s",
+ launchInfo.mPackageFile.getName(), device.getSerialNumber(), e.getMessage());
+ AdtPlugin.printErrorToConsole(launchInfo.mProject, msg);
+ throw e;
+ }
+
+ return receiver.getSuccess();
+ }
+
+ /**
+ * launches an application on a device or emulator
+ *
+ * @param info the {@link DelayedLaunchInfo} that indicates the activity to launch
+ * @param device the device or emulator to launch the application on
+ */
+ private void launchApp(final DelayedLaunchInfo info, Device device) {
+ // if we're not supposed to do anything, just stop the Launch item and return;
+ if (info.mLaunchAction == LaunchConfigDelegate.ACTION_DO_NOTHING) {
+ String msg = String.format("%1$s installed on device",
+ info.mPackageFile.getFullPath().toOSString());
+ AdtPlugin.printToConsole(info.mProject, msg, "Done!");
+ info.mLaunch.stopLaunch();
+ return;
+ }
+ try {
+ String msg = String.format("Starting activity %1$s on device ", info.mActivity,
+ info.mDevice);
+ AdtPlugin.printToConsole(info.mProject, msg);
+
+ // In debug mode, we need to add the info to the list of application monitoring
+ // client changes.
+ if (info.mDebugMode) {
+ synchronized (sListLock) {
+ if (mWaitingForDebuggerApplications.contains(info) == false) {
+ mWaitingForDebuggerApplications.add(info);
+ }
+ }
+ }
+
+ // increment launch attempt count, to handle retries and timeouts
+ info.mAttemptCount++;
+
+ // now we actually launch the app.
+ device.executeShellCommand("am start" //$NON-NLS-1$
+ + (info.mDebugMode ? " -D" //$NON-NLS-1$
+ : "") //$NON-NLS-1$
+ + " -n " //$NON-NLS-1$
+ + info.mPackageName + "/" //$NON-NLS-1$
+ + info.mActivity.replaceAll("\\$", "\\\\\\$"), //$NON-NLS-1$ //$NON-NLS-2$
+ new AMReceiver(info, device));
+
+ // if the app is not a debug app, we need to do some clean up, as
+ // the process is done!
+ if (info.mDebugMode == false) {
+ // stop the launch object, since there's no debug, and it can't
+ // provide any control over the app
+ info.mLaunch.stopLaunch();
+ }
+ } catch (IOException e) {
+ // something went wrong trying to launch the app.
+ // lets stop the Launch
+ AdtPlugin.printErrorToConsole(info.mProject,
+ String.format("Launch error: %s", e.getMessage()));
+ info.mLaunch.stopLaunch();
+
+ // and remove it from the list of app waiting for debuggers
+ synchronized (sListLock) {
+ mWaitingForDebuggerApplications.remove(info);
+ }
+ }
+ }
+
+ private boolean launchEmulator(AndroidLaunchConfiguration config, AvdInfo avdToLaunch) {
+
+ // split the custom command line in segments
+ ArrayList<String> customArgs = new ArrayList<String>();
+ boolean has_wipe_data = false;
+ if (config.mEmulatorCommandLine != null && config.mEmulatorCommandLine.length() > 0) {
+ String[] segments = config.mEmulatorCommandLine.split("\\s+"); //$NON-NLS-1$
+
+ // we need to remove the empty strings
+ for (String s : segments) {
+ if (s.length() > 0) {
+ customArgs.add(s);
+ if (!has_wipe_data && s.equals(FLAG_WIPE_DATA)) {
+ has_wipe_data = true;
+ }
+ }
+ }
+ }
+
+ boolean needs_wipe_data = config.mWipeData && !has_wipe_data;
+ if (needs_wipe_data) {
+ if (!AdtPlugin.displayPrompt("Android Launch", "Are you sure you want to wipe all user data when starting this emulator?")) {
+ needs_wipe_data = false;
+ }
+ }
+
+ // build the command line based on the available parameters.
+ ArrayList<String> list = new ArrayList<String>();
+
+ list.add(AdtPlugin.getOsAbsoluteEmulator());
+ list.add(FLAG_AVD);
+ list.add(avdToLaunch.getName());
+
+ if (config.mNetworkSpeed != null) {
+ list.add(FLAG_NETSPEED);
+ list.add(config.mNetworkSpeed);
+ }
+
+ if (config.mNetworkDelay != null) {
+ list.add(FLAG_NETDELAY);
+ list.add(config.mNetworkDelay);
+ }
+
+ if (needs_wipe_data) {
+ list.add(FLAG_WIPE_DATA);
+ }
+
+ if (config.mNoBootAnim) {
+ list.add(FLAG_NO_BOOT_ANIM);
+ }
+
+ list.addAll(customArgs);
+
+ // convert the list into an array for the call to exec.
+ String[] command = list.toArray(new String[list.size()]);
+
+ // launch the emulator
+ try {
+ Process process = Runtime.getRuntime().exec(command);
+ grabEmulatorOutput(process);
+ } catch (IOException e) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Looks for and returns an existing {@link ILaunchConfiguration} object for a
+ * specified project.
+ * @param manager The {@link ILaunchManager}.
+ * @param type The {@link ILaunchConfigurationType}.
+ * @param projectName The name of the project
+ * @return an existing <code>ILaunchConfiguration</code> object matching the project, or
+ * <code>null</code>.
+ */
+ private static ILaunchConfiguration findConfig(ILaunchManager manager,
+ ILaunchConfigurationType type, String projectName) {
+ try {
+ ILaunchConfiguration[] configs = manager.getLaunchConfigurations(type);
+
+ for (ILaunchConfiguration config : configs) {
+ if (config.getAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME,
+ "").equals(projectName)) { //$NON-NLS-1$
+ return config;
+ }
+ }
+ } catch (CoreException e) {
+ MessageDialog.openError(AdtPlugin.getDisplay().getActiveShell(),
+ "Launch Error", e.getStatus().getMessage());
+ }
+
+ // didn't find anything that matches. Return null
+ return null;
+
+ }
+
+
+ /**
+ * Connects a remote debugger on the specified port.
+ * @param debugPort The port to connect the debugger to
+ * @param launch The associated AndroidLaunch object.
+ * @param monitor A Progress monitor
+ * @return false if cancelled by the monitor
+ * @throws CoreException
+ */
+ public static boolean connectRemoteDebugger(int debugPort,
+ AndroidLaunch launch, IProgressMonitor monitor)
+ throws CoreException {
+ // get some default parameters.
+ int connectTimeout = JavaRuntime.getPreferences().getInt(JavaRuntime.PREF_CONNECT_TIMEOUT);
+
+ HashMap<String, String> newMap = new HashMap<String, String>();
+
+ newMap.put("hostname", "localhost"); //$NON-NLS-1$ //$NON-NLS-2$
+
+ newMap.put("port", Integer.toString(debugPort)); //$NON-NLS-1$
+
+ newMap.put("timeout", Integer.toString(connectTimeout));
+
+ // get the default VM connector
+ IVMConnector connector = JavaRuntime.getDefaultVMConnector();
+
+ // connect to remote VM
+ connector.connect(newMap, monitor, launch);
+
+ // check for cancellation
+ if (monitor.isCanceled()) {
+ IDebugTarget[] debugTargets = launch.getDebugTargets();
+ for (IDebugTarget target : debugTargets) {
+ if (target.canDisconnect()) {
+ target.disconnect();
+ }
+ }
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Launch a new thread that connects a remote debugger on the specified port.
+ * @param debugPort The port to connect the debugger to
+ * @param androidLaunch The associated AndroidLaunch object.
+ * @param monitor A Progress monitor
+ * @see #connectRemoteDebugger(int, AndroidLaunch, IProgressMonitor)
+ */
+ public static void launchRemoteDebugger( final int debugPort, final AndroidLaunch androidLaunch,
+ final IProgressMonitor monitor) {
+ new Thread("Debugger connection") { //$NON-NLS-1$
+ @Override
+ public void run() {
+ try {
+ connectRemoteDebugger(debugPort, androidLaunch, monitor);
+ } catch (CoreException e) {
+ androidLaunch.stopLaunch();
+ }
+ monitor.done();
+ }
+ }.start();
+ }
+
+ /**
+ * Sent when a new {@link AndroidDebugBridge} is started.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param bridge the new {@link AndroidDebugBridge} object.
+ *
+ * @see IDebugBridgeChangeListener#bridgeChanged(AndroidDebugBridge)
+ */
+ public void bridgeChanged(AndroidDebugBridge bridge) {
+ // The adb server has changed. We cancel any pending launches.
+ String message1 = "adb server change: cancelling '%1$s' launch!";
+ String message2 = "adb server change: cancelling sync!";
+ synchronized (sListLock) {
+ for (DelayedLaunchInfo launchInfo : mWaitingForReadyEmulatorList) {
+ if (launchInfo.mLaunchAction == LaunchConfigDelegate.ACTION_DO_NOTHING) {
+ AdtPlugin.printErrorToConsole(launchInfo.mProject, message2);
+ } else {
+ AdtPlugin.printErrorToConsole(launchInfo.mProject,
+ String.format(message1, launchInfo.mActivity));
+ }
+ launchInfo.mLaunch.stopLaunch();
+ }
+ for (DelayedLaunchInfo launchInfo : mWaitingForDebuggerApplications) {
+ if (launchInfo.mLaunchAction == LaunchConfigDelegate.ACTION_DO_NOTHING) {
+ AdtPlugin.printErrorToConsole(launchInfo.mProject, message2);
+ } else {
+ AdtPlugin.printErrorToConsole(launchInfo.mProject,
+ String.format(message1, launchInfo.mActivity));
+ }
+ launchInfo.mLaunch.stopLaunch();
+ }
+
+ mWaitingForReadyEmulatorList.clear();
+ mWaitingForDebuggerApplications.clear();
+ }
+ }
+
+ /**
+ * Sent when the a device is connected to the {@link AndroidDebugBridge}.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param device the new device.
+ *
+ * @see IDeviceChangeListener#deviceConnected(Device)
+ */
+ public void deviceConnected(Device device) {
+ synchronized (sListLock) {
+ // look if there's an app waiting for a device
+ if (mWaitingForEmulatorLaunches.size() > 0) {
+ // get/remove first launch item from the list
+ // FIXME: what if we have multiple launches waiting?
+ DelayedLaunchInfo launchInfo = mWaitingForEmulatorLaunches.get(0);
+ mWaitingForEmulatorLaunches.remove(0);
+
+ // give the launch item its device for later use.
+ launchInfo.mDevice = device;
+
+ // and move it to the other list
+ mWaitingForReadyEmulatorList.add(launchInfo);
+
+ // and tell the user about it
+ AdtPlugin.printToConsole(launchInfo.mProject,
+ String.format("New emulator found: %1$s", device.getSerialNumber()));
+ AdtPlugin.printToConsole(launchInfo.mProject,
+ String.format("Waiting for HOME ('%1$s') to be launched...",
+ AdtPlugin.getDefault().getPreferenceStore().getString(
+ AdtPlugin.PREFS_HOME_PACKAGE)));
+ }
+ }
+ }
+
+ /**
+ * Sent when the a device is connected to the {@link AndroidDebugBridge}.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param device the new device.
+ *
+ * @see IDeviceChangeListener#deviceDisconnected(Device)
+ */
+ @SuppressWarnings("unchecked")
+ public void deviceDisconnected(Device device) {
+ // any pending launch on this device must be canceled.
+ String message = "%1$s disconnected! Cancelling '%2$s' launch!";
+ synchronized (sListLock) {
+ ArrayList<DelayedLaunchInfo> copyList =
+ (ArrayList<DelayedLaunchInfo>)mWaitingForReadyEmulatorList.clone();
+ for (DelayedLaunchInfo launchInfo : copyList) {
+ if (launchInfo.mDevice == device) {
+ AdtPlugin.printErrorToConsole(launchInfo.mProject,
+ String.format(message, device.getSerialNumber(), launchInfo.mActivity));
+ launchInfo.mLaunch.stopLaunch();
+ mWaitingForReadyEmulatorList.remove(launchInfo);
+ }
+ }
+ copyList = (ArrayList<DelayedLaunchInfo>)mWaitingForDebuggerApplications.clone();
+ for (DelayedLaunchInfo launchInfo : copyList) {
+ if (launchInfo.mDevice == device) {
+ AdtPlugin.printErrorToConsole(launchInfo.mProject,
+ String.format(message, device.getSerialNumber(), launchInfo.mActivity));
+ launchInfo.mLaunch.stopLaunch();
+ mWaitingForDebuggerApplications.remove(launchInfo);
+ }
+ }
+ }
+ }
+
+ /**
+ * Sent when a device data changed, or when clients are started/terminated on the device.
+ * <p/>
+ * 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(Device, int)
+ */
+ public void deviceChanged(Device device, int changeMask) {
+ // We could check if any starting device we care about is now ready, but we can wait for
+ // its home app to show up, so...
+ }
+
+ /**
+ * Sent when an existing client information changed.
+ * <p/>
+ * 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_INTEREST}, {@link Client#CHANGE_THREAD_MODE},
+ * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE},
+ * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA}
+ *
+ * @see IClientChangeListener#clientChanged(Client, int)
+ */
+ public void clientChanged(final Client client, int changeMask) {
+ boolean connectDebugger = false;
+ if ((changeMask & Client.CHANGE_NAME) == Client.CHANGE_NAME) {
+ String applicationName = client.getClientData().getClientDescription();
+ if (applicationName != null) {
+ IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore();
+ String home = store.getString(AdtPlugin.PREFS_HOME_PACKAGE);
+
+ if (home.equals(applicationName)) {
+
+ // looks like home is up, get its device
+ Device device = client.getDevice();
+
+ // look for application waiting for home
+ synchronized (sListLock) {
+ for (int i = 0 ; i < mWaitingForReadyEmulatorList.size() ;) {
+ DelayedLaunchInfo launchInfo = mWaitingForReadyEmulatorList.get(i);
+ if (launchInfo.mDevice == device) {
+ // it's match, remove from the list
+ mWaitingForReadyEmulatorList.remove(i);
+
+ // We couldn't check earlier the API level of the device
+ // (it's asynchronous when the device boot, and usually
+ // deviceConnected is called before it's queried for its build info)
+ // so we check now
+ if (checkBuildInfo(launchInfo, device) == false) {
+ // device is not the proper API!
+ AdtPlugin.printErrorToConsole(launchInfo.mProject,
+ "Launch canceled!");
+ launchInfo.mLaunch.stopLaunch();
+ return;
+ }
+
+ AdtPlugin.printToConsole(launchInfo.mProject,
+ String.format("HOME is up on device '%1$s'",
+ device.getSerialNumber()));
+
+ // attempt to sync the new package onto the device.
+ if (syncApp(launchInfo, device)) {
+ // application package is sync'ed, lets attempt to launch it.
+ launchApp(launchInfo, device);
+ } else {
+ // failure! Cancel and return
+ AdtPlugin.printErrorToConsole(launchInfo.mProject,
+ "Launch canceled!");
+ launchInfo.mLaunch.stopLaunch();
+ }
+
+ break;
+ } else {
+ i++;
+ }
+ }
+ }
+ }
+
+ // check if it's already waiting for a debugger, and if so we connect to it.
+ if (client.getClientData().getDebuggerConnectionStatus() == ClientData.DEBUGGER_WAITING) {
+ // search for this client in the list;
+ synchronized (sListLock) {
+ int index = mUnknownClientsWaitingForDebugger.indexOf(client);
+ if (index != -1) {
+ connectDebugger = true;
+ mUnknownClientsWaitingForDebugger.remove(client);
+ }
+ }
+ }
+ }
+ }
+
+ // if it's not home, it could be an app that is now in debugger mode that we're waiting for
+ // lets check it
+
+ if ((changeMask & Client.CHANGE_DEBUGGER_INTEREST) == Client.CHANGE_DEBUGGER_INTEREST) {
+ ClientData clientData = client.getClientData();
+ String applicationName = client.getClientData().getClientDescription();
+ if (clientData.getDebuggerConnectionStatus() == ClientData.DEBUGGER_WAITING) {
+ // Get the application name, and make sure its valid.
+ if (applicationName == null) {
+ // looks like we don't have the client yet, so we keep it around for when its
+ // name becomes available.
+ synchronized (sListLock) {
+ mUnknownClientsWaitingForDebugger.add(client);
+ }
+ return;
+ } else {
+ connectDebugger = true;
+ }
+ }
+ }
+
+ if (connectDebugger) {
+ Log.d("adt", "Debugging " + client);
+ // now check it against the apps waiting for a debugger
+ String applicationName = client.getClientData().getClientDescription();
+ Log.d("adt", "App Name: " + applicationName);
+ synchronized (sListLock) {
+ for (int i = 0 ; i < mWaitingForDebuggerApplications.size() ;) {
+ final DelayedLaunchInfo launchInfo = mWaitingForDebuggerApplications.get(i);
+ if (client.getDevice() == launchInfo.mDevice &&
+ applicationName.equals(launchInfo.mPackageName)) {
+ // this is a match. We remove the launch info from the list
+ mWaitingForDebuggerApplications.remove(i);
+
+ // and connect the debugger.
+ String msg = String.format(
+ "Attempting to connect debugger to '%1$s' on port %2$d",
+ launchInfo.mPackageName, client.getDebuggerListenPort());
+ AdtPlugin.printToConsole(launchInfo.mProject, msg);
+
+ new Thread("Debugger Connection") { //$NON-NLS-1$
+ @Override
+ public void run() {
+ try {
+ if (connectRemoteDebugger(
+ client.getDebuggerListenPort(),
+ launchInfo.mLaunch, launchInfo.mMonitor) == false) {
+ return;
+ }
+ } catch (CoreException e) {
+ // well something went wrong.
+ AdtPlugin.printErrorToConsole(launchInfo.mProject,
+ String.format("Launch error: %s", e.getMessage()));
+ // stop the launch
+ launchInfo.mLaunch.stopLaunch();
+ }
+
+ launchInfo.mMonitor.done();
+ }
+ }.start();
+
+ // we're done processing this client.
+ return;
+
+ } else {
+ i++;
+ }
+ }
+ }
+
+ // if we get here, we haven't found an app that we were launching, so we look
+ // for opened android projects that contains the app asking for a debugger.
+ // If we find one, we automatically connect to it.
+ IProject project = ProjectHelper.findAndroidProjectByAppName(applicationName);
+
+ if (project != null) {
+ debugRunningApp(project, client.getDebuggerListenPort());
+ }
+ }
+ }
+
+ /**
+ * Get the stderr/stdout outputs of a process and return when the process is done.
+ * Both <b>must</b> be read or the process will block on windows.
+ * @param process The process to get the ouput from
+ */
+ private void grabEmulatorOutput(final Process process) {
+ // read the lines as they come. if null is returned, it's
+ // because the process finished
+ new Thread("") { //$NON-NLS-1$
+ @Override
+ public void run() {
+ // create a buffer to read the stderr output
+ InputStreamReader is = new InputStreamReader(process.getErrorStream());
+ BufferedReader errReader = new BufferedReader(is);
+
+ try {
+ while (true) {
+ String line = errReader.readLine();
+ if (line != null) {
+ AdtPlugin.printErrorToConsole("Emulator", line);
+ } else {
+ break;
+ }
+ }
+ } catch (IOException e) {
+ // do nothing.
+ }
+ }
+ }.start();
+
+ new Thread("") { //$NON-NLS-1$
+ @Override
+ public void run() {
+ InputStreamReader is = new InputStreamReader(process.getInputStream());
+ BufferedReader outReader = new BufferedReader(is);
+
+ try {
+ while (true) {
+ String line = outReader.readLine();
+ if (line != null) {
+ AdtPlugin.printToConsole("Emulator", line);
+ } else {
+ break;
+ }
+ }
+ } catch (IOException e) {
+ // do nothing.
+ }
+ }
+ }.start();
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/DeviceChooserDialog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/DeviceChooserDialog.java
new file mode 100644
index 0000000..a260350
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/DeviceChooserDialog.java
@@ -0,0 +1,705 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.debug.launching;
+
+import com.android.ddmlib.AndroidDebugBridge;
+import com.android.ddmlib.Client;
+import com.android.ddmlib.Device;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener;
+import com.android.ddmlib.Device.DeviceState;
+import com.android.ddmuilib.IImageLoader;
+import com.android.ddmuilib.ImageHelper;
+import com.android.ddmuilib.TableHelper;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.sdk.Sdk;
+import com.android.ide.eclipse.ddms.DdmsPlugin;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.avd.AvdManager;
+import com.android.sdklib.avd.AvdManager.AvdInfo;
+import com.android.sdkuilib.AvdSelector;
+
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.dialogs.IDialogConstants;
+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.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.Shell;
+import org.eclipse.swt.widgets.Table;
+
+import java.util.ArrayList;
+
+public class DeviceChooserDialog extends Dialog implements IDeviceChangeListener {
+
+ private final static int ICON_WIDTH = 16;
+
+ private final static String PREFS_COL_SERIAL = "deviceChooser.serial"; //$NON-NLS-1$
+ private final static String PREFS_COL_STATE = "deviceChooser.state"; //$NON-NLS-1$
+ private final static String PREFS_COL_AVD = "deviceChooser.avd"; //$NON-NLS-1$
+ private final static String PREFS_COL_TARGET = "deviceChooser.target"; //$NON-NLS-1$
+ private final static String PREFS_COL_DEBUG = "deviceChooser.debug"; //$NON-NLS-1$
+
+ 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 Sdk mSdk;
+
+ private final AvdInfo[] mFullAvdList;
+
+ private Button mDeviceRadioButton;
+
+ private boolean mDisableAvdSelectionChange = false;
+
+ /**
+ * Basic Content Provider for a table full of {@link Device} objects. The input is
+ * a {@link AndroidDebugBridge}.
+ */
+ private class ContentProvider implements IStructuredContentProvider {
+ public Object[] getElements(Object inputElement) {
+ if (inputElement instanceof AndroidDebugBridge) {
+ return ((AndroidDebugBridge)inputElement).getDevices();
+ }
+
+ return new Object[0];
+ }
+
+ public void dispose() {
+ // pass
+ }
+
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ // pass
+ }
+ }
+
+
+ /**
+ * A Label Provider for the {@link TableViewer} in {@link DeviceChooserDialog}.
+ * It provides labels and images for {@link Device} objects.
+ */
+ private class LabelProvider implements ITableLabelProvider {
+
+ public Image getColumnImage(Object element, int columnIndex) {
+ if (element instanceof Device) {
+ Device device = (Device)element;
+ switch (columnIndex) {
+ case 0:
+ return device.isEmulator() ? mEmulatorImage : mDeviceImage;
+
+ case 2:
+ // check for compatibility.
+ if (device.isEmulator() == false) { // physical device
+ // get the api level of the device
+ try {
+ String apiValue = device.getProperty(
+ IDevice.PROP_BUILD_VERSION_NUMBER);
+ if (apiValue != null) {
+ int api = Integer.parseInt(apiValue);
+ if (api >= mProjectTarget.getApiVersionNumber()) {
+ // if the project is compiling against an add-on, the optional
+ // API may be missing from the device.
+ return mProjectTarget.isPlatform() ?
+ mMatchImage : mWarningImage;
+ } else {
+ return mNoMatchImage;
+ }
+ } else {
+ return mWarningImage;
+ }
+ } catch (NumberFormatException e) {
+ // lets consider the device non compatible
+ return mNoMatchImage;
+ }
+ } else {
+ // get the AvdInfo
+ AvdInfo info = mSdk.getAvdManager().getAvd(device.getAvdName());
+ if (info == null) {
+ return mWarningImage;
+ }
+ return mProjectTarget.isCompatibleBaseFor(info.getTarget()) ?
+ mMatchImage : mNoMatchImage;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public String getColumnText(Object element, int columnIndex) {
+ if (element instanceof Device) {
+ Device device = (Device)element;
+ switch (columnIndex) {
+ case 0:
+ return device.getSerialNumber();
+ 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());
+ 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;
+ }
+
+ public void addListener(ILabelProviderListener listener) {
+ // pass
+ }
+
+ public void dispose() {
+ // pass
+ }
+
+ public boolean isLabelProperty(Object element, String property) {
+ // pass
+ return false;
+ }
+
+ public void removeListener(ILabelProviderListener listener) {
+ // pass
+ }
+ }
+
+ public static class DeviceChooserResponse {
+ private AvdInfo mAvdToLaunch;
+ private Device mDeviceToUse;
+
+ public void setDeviceToUse(Device d) {
+ mDeviceToUse = d;
+ mAvdToLaunch = null;
+ }
+
+ public void setAvdToLaunch(AvdInfo avd) {
+ mAvdToLaunch = avd;
+ mDeviceToUse = null;
+ }
+
+ public Device getDeviceToUse() {
+ return mDeviceToUse;
+ }
+
+ public AvdInfo getAvdToLaunch() {
+ return mAvdToLaunch;
+ }
+ }
+
+ public DeviceChooserDialog(Shell parent, DeviceChooserResponse response, String packageName,
+ IAndroidTarget projectTarget) {
+ super(parent);
+ mResponse = response;
+ mPackageName = packageName;
+ mProjectTarget = projectTarget;
+ mSdk = Sdk.getCurrent();
+
+ // get the full list of Android Virtual Devices
+ AvdManager avdManager = mSdk.getAvdManager();
+ if (avdManager != null) {
+ mFullAvdList = avdManager.getAvds();
+ } else {
+ mFullAvdList = null;
+ }
+
+ loadImages();
+ }
+
+ private void cleanup() {
+ // done listening.
+ AndroidDebugBridge.removeDeviceChangeListener(this);
+
+ mEmulatorImage.dispose();
+ mDeviceImage.dispose();
+ mMatchImage.dispose();
+ mNoMatchImage.dispose();
+ mWarningImage.dispose();
+ }
+
+ @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;
+ }
+
+
+ @Override
+ protected Control createDialogArea(Composite parent) {
+ Composite top = new Composite(parent, SWT.NONE);
+ top.setLayout(new GridLayout(1, true));
+
+ 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.getFirstSelected());
+ }
+
+ 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);
+
+ IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore();
+ mDeviceTable = new Table(offsetComp, SWT.SINGLE | SWT.FULL_SELECTION);
+ 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$
+ PREFS_COL_SERIAL, store);
+
+ TableHelper.createTableColumn(mDeviceTable, "AVD Name",
+ SWT.LEFT, "engineering", //$NON-NLS-1$
+ PREFS_COL_AVD, store);
+
+ TableHelper.createTableColumn(mDeviceTable, "Target",
+ SWT.LEFT, "AAA+Android 9.9.9", //$NON-NLS-1$
+ PREFS_COL_TARGET, store);
+
+ TableHelper.createTableColumn(mDeviceTable, "Debug",
+ SWT.LEFT, "Debug", //$NON-NLS-1$
+ PREFS_COL_DEBUG, store);
+
+ TableHelper.createTableColumn(mDeviceTable, "State",
+ SWT.LEFT, "bootloader", //$NON-NLS-1$
+ PREFS_COL_STATE, store);
+
+ // create the viewer for it
+ mViewer = new TableViewer(mDeviceTable);
+ mViewer.setContentProvider(new ContentProvider());
+ mViewer.setLabelProvider(new LabelProvider());
+ mViewer.setInput(AndroidDebugBridge.getBridge());
+ mViewer.addDoubleClickListener(new IDoubleClickListener() {
+ public void doubleClick(DoubleClickEvent event) {
+ ISelection selection = event.getSelection();
+ if (selection instanceof IStructuredSelection) {
+ IStructuredSelection structuredSelection = (IStructuredSelection)selection;
+ Object object = structuredSelection.getFirstElement();
+ if (object instanceof Device) {
+ mResponse.setDeviceToUse((Device)object);
+ }
+ }
+ }
+ });
+
+ 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, getNonRunningAvds(), mProjectTarget,
+ false /*allowMultipleSelection*/);
+ mPreferredAvdSelector.setTableHeightHint(100);
+ mPreferredAvdSelector.setEnabled(false);
+ mDeviceTable.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ handleDeviceSelection();
+ }
+ });
+
+ mPreferredAvdSelector.setSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mDisableAvdSelectionChange == false) {
+ mResponse.setAvdToLaunch(mPreferredAvdSelector.getFirstSelected());
+ enableOkButton();
+ }
+ }
+ });
+
+ AndroidDebugBridge.addDeviceChangeListener(this);
+
+ return top;
+ }
+
+ private void loadImages() {
+ IImageLoader ddmsLoader = DdmsPlugin.getImageLoader();
+ Display display = DdmsPlugin.getDisplay();
+ IImageLoader adtLoader = AdtPlugin.getImageLoader();
+
+ if (mDeviceImage == null) {
+ mDeviceImage = ImageHelper.loadImage(ddmsLoader, display,
+ "device.png", //$NON-NLS-1$
+ ICON_WIDTH, ICON_WIDTH,
+ display.getSystemColor(SWT.COLOR_RED));
+ }
+ if (mEmulatorImage == null) {
+ mEmulatorImage = ImageHelper.loadImage(ddmsLoader, display,
+ "emulator.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$
+ display.getSystemColor(SWT.COLOR_BLUE));
+ }
+
+ if (mMatchImage == null) {
+ mMatchImage = ImageHelper.loadImage(adtLoader, display,
+ "match.png", //$NON-NLS-1$
+ ICON_WIDTH, ICON_WIDTH,
+ display.getSystemColor(SWT.COLOR_GREEN));
+ }
+
+ if (mNoMatchImage == null) {
+ mNoMatchImage = ImageHelper.loadImage(adtLoader, display,
+ "error.png", //$NON-NLS-1$
+ ICON_WIDTH, ICON_WIDTH,
+ display.getSystemColor(SWT.COLOR_RED));
+ }
+
+ if (mWarningImage == null) {
+ mWarningImage = ImageHelper.loadImage(adtLoader, display,
+ "warning.png", //$NON-NLS-1$
+ ICON_WIDTH, ICON_WIDTH,
+ display.getSystemColor(SWT.COLOR_YELLOW));
+ }
+
+ }
+
+ /**
+ * Returns a display string representing the state of the device.
+ * @param d the device
+ */
+ private static String getStateString(Device 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}.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param device the new device.
+ *
+ * @see IDeviceChangeListener#deviceConnected(Device)
+ */
+ public void deviceConnected(Device device) {
+ final DeviceChooserDialog dialog = this;
+ exec(new Runnable() {
+ 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();
+ } 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}.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param device the new device.
+ *
+ * @see IDeviceChangeListener#deviceDisconnected(Device)
+ */
+ public void deviceDisconnected(Device device) {
+ deviceConnected(device);
+ }
+
+ /**
+ * Sent when a device data changed, or when clients are started/terminated on the device.
+ * <p/>
+ * 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(Device, int)
+ */
+ public void deviceChanged(final Device device, int changeMask) {
+ if ((changeMask & (Device.CHANGE_STATE | Device.CHANGE_BUILD_INFO)) != 0) {
+ final DeviceChooserDialog dialog = this;
+ exec(new Runnable() {
+ 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();
+
+ // 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);
+ }
+ }
+
+ /**
+ * 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 Device) {
+ handleSelection((Device)data);
+ } else {
+ handleSelection(null);
+ }
+ }
+ }
+
+ private void handleSelection(Device 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.
+ * <p/>
+ * 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();
+
+ Device[] devices = bridge.getDevices();
+
+ for (Device 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();
+ }
+
+ /**
+ * Returns the list of {@link AvdInfo} that are not already running in an emulator.
+ */
+ private AvdInfo[] getNonRunningAvds() {
+ ArrayList<AvdInfo> list = new ArrayList<AvdInfo>();
+
+ Device[] devices = AndroidDebugBridge.getBridge().getDevices();
+
+ // loop through all the Avd and put the one that are not running in the list.
+ avdLoop: for (AvdInfo info : mFullAvdList) {
+ for (Device d : devices) {
+ if (info.getName().equals(d.getAvdName())) {
+ continue avdLoop;
+ }
+ }
+ list.add(info);
+ }
+
+ return list.toArray(new AvdInfo[list.size()]);
+ }
+
+ /**
+ * Refills the AVD list keeping the current selection.
+ */
+ private void refillAvdList() {
+ AvdInfo[] array = getNonRunningAvds();
+
+ // save the current selection
+ AvdInfo selected = mPreferredAvdSelector.getFirstSelected();
+
+ // disable selection change.
+ mDisableAvdSelectionChange = true;
+
+ // set the new list in the selector
+ mPreferredAvdSelector.setAvds(array, mProjectTarget);
+
+ // 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;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/LaunchConfigDelegate.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/LaunchConfigDelegate.java
new file mode 100644
index 0000000..bbd320b
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/LaunchConfigDelegate.java
@@ -0,0 +1,431 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.debug.launching;
+
+import com.android.ddmlib.AndroidDebugBridge;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.debug.launching.AndroidLaunchController.AndroidLaunchConfiguration;
+import com.android.ide.eclipse.adt.project.ProjectHelper;
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.common.project.AndroidManifestParser;
+import com.android.ide.eclipse.common.project.BaseProjectHelper;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IWorkspace;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.debug.core.ILaunch;
+import org.eclipse.debug.core.ILaunchConfiguration;
+import org.eclipse.debug.core.model.LaunchConfigurationDelegate;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
+
+/**
+ * Implementation of an eclipse LauncConfigurationDelegate to launch android
+ * application in debug.
+ */
+public class LaunchConfigDelegate extends LaunchConfigurationDelegate {
+ final static int INVALID_DEBUG_PORT = -1;
+
+ public final static String ANDROID_LAUNCH_TYPE_ID =
+ "com.android.ide.eclipse.adt.debug.LaunchConfigType"; //$NON-NLS-1$
+
+ /** Target mode parameters: true is automatic, false is manual */
+ public static final String ATTR_TARGET_MODE = AdtPlugin.PLUGIN_ID + ".target"; //$NON-NLS-1$
+ public static final boolean DEFAULT_TARGET_MODE = true; //automatic mode
+
+ /**
+ * Launch action:
+ * <ul>
+ * <li>0: launch default activity</li>
+ * <li>1: launch specified activity. See {@link #ATTR_ACTIVITY}</li>
+ * <li>2: Do Nothing</li>
+ * </ul>
+ */
+ public final static String ATTR_LAUNCH_ACTION = AdtPlugin.PLUGIN_ID + ".action"; //$NON-NLS-1$
+
+ /** Default launch action. This launches the activity that is setup to be found in the HOME
+ * screen.
+ */
+ public final static int ACTION_DEFAULT = 0;
+ /** Launch action starting a specific activity. */
+ public final static int ACTION_ACTIVITY = 1;
+ /** Launch action that does nothing. */
+ public final static int ACTION_DO_NOTHING = 2;
+ /** Default launch action value. */
+ public final static int DEFAULT_LAUNCH_ACTION = ACTION_DEFAULT;
+
+ /**
+ * Activity to be launched if {@link #ATTR_LAUNCH_ACTION} is 1
+ */
+ public static final String ATTR_ACTIVITY = AdtPlugin.PLUGIN_ID + ".activity"; //$NON-NLS-1$
+
+ public static final String ATTR_AVD_NAME = AdtPlugin.PLUGIN_ID + ".avd"; //$NON-NLS-1$
+
+ public static final String ATTR_SPEED = AdtPlugin.PLUGIN_ID + ".speed"; //$NON-NLS-1$
+
+ /**
+ * Index of the default network speed setting for the emulator.<br>
+ * Get the emulator option with <code>EmulatorConfigTab.getSpeed(index)</code>
+ */
+ public static final int DEFAULT_SPEED = 0;
+
+ public static final String ATTR_DELAY = AdtPlugin.PLUGIN_ID + ".delay"; //$NON-NLS-1$
+
+ /**
+ * Index of the default network latency setting for the emulator.<br>
+ * Get the emulator option with <code>EmulatorConfigTab.getDelay(index)</code>
+ */
+ public static final int DEFAULT_DELAY = 0;
+
+ public static final String ATTR_COMMANDLINE = AdtPlugin.PLUGIN_ID + ".commandline"; //$NON-NLS-1$
+
+ public static final String ATTR_WIPE_DATA = AdtPlugin.PLUGIN_ID + ".wipedata"; //$NON-NLS-1$
+ public static final boolean DEFAULT_WIPE_DATA = false;
+
+ public static final String ATTR_NO_BOOT_ANIM = AdtPlugin.PLUGIN_ID + ".nobootanim"; //$NON-NLS-1$
+ public static final boolean DEFAULT_NO_BOOT_ANIM = false;
+
+ public static final String ATTR_DEBUG_PORT =
+ AdtPlugin.PLUGIN_ID + ".debugPort"; //$NON-NLS-1$
+
+ public void launch(ILaunchConfiguration configuration, String mode,
+ ILaunch launch, IProgressMonitor monitor) throws CoreException {
+ // We need to check if it's a standard launch or if it's a launch
+ // to debug an application already running.
+ int debugPort = AndroidLaunchController.getPortForConfig(configuration);
+
+ // get the project
+ IProject project = getProject(configuration);
+
+ // first we make sure the launch is of the proper type
+ AndroidLaunch androidLaunch = null;
+ if (launch instanceof AndroidLaunch) {
+ androidLaunch = (AndroidLaunch)launch;
+ } else {
+ // wrong type, not sure how we got there, but we don't do
+ // anything else
+ AdtPlugin.printErrorToConsole(project, "Wrong Launch Type!");
+ return;
+ }
+
+ // if we have a valid debug port, this means we're debugging an app
+ // that's already launched.
+ if (debugPort != INVALID_DEBUG_PORT) {
+ AndroidLaunchController.launchRemoteDebugger(debugPort, androidLaunch, monitor);
+ return;
+ }
+
+ if (project == null) {
+ AdtPlugin.printErrorToConsole("Couldn't get project object!");
+ androidLaunch.stopLaunch();
+ return;
+ }
+
+ // check if the project has errors, and abort in this case.
+ if (ProjectHelper.hasError(project, true)) {
+ AdtPlugin.displayError("Android Launch",
+ "Your project contains error(s), please fix them before running your application.");
+ return;
+ }
+
+ AdtPlugin.printToConsole(project, "------------------------------"); //$NON-NLS-1$
+ AdtPlugin.printToConsole(project, "Android Launch!");
+
+ // check if the project is using the proper sdk.
+ // if that throws an exception, we simply let it propage to the caller.
+ if (checkAndroidProject(project) == false) {
+ AdtPlugin.printErrorToConsole(project, "Project is not an Android Project. Aborting!");
+ androidLaunch.stopLaunch();
+ return;
+ }
+
+ // Check adb status and abort if needed.
+ AndroidDebugBridge bridge = AndroidDebugBridge.getBridge();
+ if (bridge == null || bridge.isConnected() == false) {
+ try {
+ int connections = -1;
+ int restarts = -1;
+ if (bridge != null) {
+ connections = bridge.getConnectionAttemptCount();
+ restarts = bridge.getRestartAttemptCount();
+ }
+
+ // if we get -1, the device monitor is not even setup (anymore?).
+ // We need to ask the user to restart eclipse.
+ // This shouldn't happen, but it's better to let the user know in case it does.
+ if (connections == -1 || restarts == -1) {
+ AdtPlugin.printErrorToConsole(project,
+ "The connection to adb is down, and a severe error has occured.",
+ "You must restart adb and Eclipse.",
+ String.format(
+ "Please ensure that adb is correctly located at '%1$s' and can be executed.",
+ AdtPlugin.getOsAbsoluteAdb()));
+ return;
+ }
+
+ if (restarts == 0) {
+ AdtPlugin.printErrorToConsole(project,
+ "Connection with adb was interrupted.",
+ String.format("%1$s attempts have been made to reconnect.", connections),
+ "You may want to manually restart adb from the Devices view.");
+ } else {
+ AdtPlugin.printErrorToConsole(project,
+ "Connection with adb was interrupted, and attempts to reconnect have failed.",
+ String.format("%1$s attempts have been made to restart adb.", restarts),
+ "You may want to manually restart adb from the Devices view.");
+
+ }
+ return;
+ } finally {
+ androidLaunch.stopLaunch();
+ }
+ }
+
+ // since adb is working, we let the user know
+ // TODO have a verbose mode for launch with more info (or some of the less useful info we now have).
+ AdtPlugin.printToConsole(project, "adb is running normally.");
+
+ // make a config class
+ AndroidLaunchConfiguration config = new AndroidLaunchConfiguration();
+
+ // fill it with the config coming from the ILaunchConfiguration object
+ config.set(configuration);
+
+ // get the launch controller singleton
+ AndroidLaunchController controller = AndroidLaunchController.getInstance();
+
+ // get the application package
+ IFile applicationPackage = getApplicationPackage(project);
+ if (applicationPackage == null) {
+ androidLaunch.stopLaunch();
+ return;
+ }
+
+ // we need some information from the manifest
+ AndroidManifestParser manifestParser = AndroidManifestParser.parse(
+ BaseProjectHelper.getJavaProject(project), null /* errorListener */,
+ true /* gatherData */, false /* markErrors */);
+
+ if (manifestParser == null) {
+ AdtPlugin.printErrorToConsole(project, "Failed to parse AndroidManifest: aborting!");
+ androidLaunch.stopLaunch();
+ return;
+ }
+
+ String activityName = null;
+
+ if (config.mLaunchAction == ACTION_ACTIVITY) {
+ // Get the activity name defined in the config
+ activityName = getActivityName(configuration);
+
+ // Get the full activity list and make sure the one we got matches.
+ String[] activities = manifestParser.getActivities();
+
+ // first we check that there are, in fact, activities.
+ if (activities.length == 0) {
+ // if the activities list is null, then the manifest is empty
+ // and we can't launch the app. We'll revert to a sync-only launch
+ AdtPlugin.printErrorToConsole(project,
+ "The Manifest defines no activity!",
+ "The launch will only sync the application package on the device!");
+ config.mLaunchAction = ACTION_DO_NOTHING;
+ } else if (activityName == null) {
+ // if the activity we got is null, we look for the default one.
+ AdtPlugin.printErrorToConsole(project,
+ "No activity specified! Getting the launcher activity.");
+ activityName = manifestParser.getLauncherActivity();
+
+ // if there's no default activity. We revert to a sync-only launch.
+ if (activityName == null) {
+ revertToNoActionLaunch(project, config);
+ }
+ } else {
+
+ // check the one we got from the config matches any from the list
+ boolean match = false;
+ for (String a : activities) {
+ if (a != null && a.equals(activityName)) {
+ match = true;
+ break;
+ }
+ }
+
+ // if we didn't find a match, we revert to the default activity if any.
+ if (match == false) {
+ AdtPlugin.printErrorToConsole(project,
+ "The specified activity does not exist! Getting the launcher activity.");
+ activityName = manifestParser.getLauncherActivity();
+
+ // if there's no default activity. We revert to a sync-only launch.
+ if (activityName == null) {
+ revertToNoActionLaunch(project, config);
+ }
+ }
+ }
+ } else if (config.mLaunchAction == ACTION_DEFAULT) {
+ activityName = manifestParser.getLauncherActivity();
+
+ // if there's no default activity. We revert to a sync-only launch.
+ if (activityName == null) {
+ revertToNoActionLaunch(project, config);
+ }
+ }
+
+ // everything seems fine, we ask the launch controller to handle
+ // the rest
+ controller.launch(project, mode, applicationPackage, manifestParser.getPackage(),
+ manifestParser.getDebuggable(), manifestParser.getApiLevelRequirement(),
+ activityName, config, androidLaunch, monitor);
+ }
+
+ @Override
+ public boolean buildForLaunch(ILaunchConfiguration configuration,
+ String mode, IProgressMonitor monitor) throws CoreException {
+
+ // need to check we have everything
+ IProject project = getProject(configuration);
+
+ if (project != null) {
+ // force an incremental build to be sure the resources will
+ // be updated if they were not saved before the launch was launched.
+ return true;
+ }
+
+ throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ 1 /* code, unused */, "Can't find the project!", null /* exception */));
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws CoreException
+ */
+ @Override
+ public ILaunch getLaunch(ILaunchConfiguration configuration, String mode)
+ throws CoreException {
+ return new AndroidLaunch(configuration, mode, null);
+ }
+
+ /**
+ * Returns the IProject object matching the name found in the configuration
+ * object under the name
+ * <code>IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME</code>
+ * @param configuration
+ * @return The IProject object or null
+ */
+ private IProject getProject(ILaunchConfiguration configuration){
+ // get the project name from the config
+ String projectName;
+ try {
+ projectName = configuration.getAttribute(
+ IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, "");
+ } catch (CoreException e) {
+ return null;
+ }
+
+ // get the current workspace
+ IWorkspace workspace = ResourcesPlugin.getWorkspace();
+
+ // and return the project with the name from the config
+ return workspace.getRoot().getProject(projectName);
+ }
+
+ /**
+ * Checks the project is an android project.
+ * @param project The project to check
+ * @return true if the project is an android SDK.
+ * @throws CoreException
+ */
+ private boolean checkAndroidProject(IProject project) throws CoreException {
+ // check if the project is a java and an android project.
+ if (project.hasNature(JavaCore.NATURE_ID) == false) {
+ String msg = String.format("%1$s is not a Java project!", project.getName());
+ AdtPlugin.displayError("Android Launch", msg);
+ return false;
+ }
+
+ if (project.hasNature(AndroidConstants.NATURE) == false) {
+ String msg = String.format("%1$s is not an Android project!", project.getName());
+ AdtPlugin.displayError("Android Launch", msg);
+ return false;
+ }
+
+ return true;
+ }
+
+
+ /**
+ * Returns the android package file as an IFile object for the specified
+ * project.
+ * @param project The project
+ * @return The android package as an IFile object or null if not found.
+ */
+ private IFile getApplicationPackage(IProject project) {
+ // get the output folder
+ IFolder outputLocation = BaseProjectHelper.getOutputFolder(project);
+
+ if (outputLocation == null) {
+ AdtPlugin.printErrorToConsole(project,
+ "Failed to get the output location of the project. Check build path properties"
+ );
+ return null;
+ }
+
+
+ // get the package path
+ String packageName = project.getName() + AndroidConstants.DOT_ANDROID_PACKAGE;
+ IResource r = outputLocation.findMember(packageName);
+
+ // check the package is present
+ if (r instanceof IFile && r.exists()) {
+ return (IFile)r;
+ }
+
+ String msg = String.format("Could not find %1$s!", packageName);
+ AdtPlugin.printErrorToConsole(project, msg);
+
+ return null;
+ }
+
+ /**
+ * Returns the name of the activity.
+ */
+ private String getActivityName(ILaunchConfiguration configuration) {
+ String empty = "";
+ String activityName;
+ try {
+ activityName = configuration.getAttribute(ATTR_ACTIVITY, empty);
+ } catch (CoreException e) {
+ return null;
+ }
+
+ return (activityName != empty) ? activityName : null;
+ }
+
+ private final void revertToNoActionLaunch(IProject project, AndroidLaunchConfiguration config) {
+ AdtPlugin.printErrorToConsole(project,
+ "No Launcher activity found!",
+ "The launch will only sync the application package on the device!");
+ config.mLaunchAction = ACTION_DO_NOTHING;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/LaunchShortcut.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/LaunchShortcut.java
new file mode 100644
index 0000000..92677f1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/launching/LaunchShortcut.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.debug.launching;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.IAdaptable;
+import org.eclipse.debug.core.ILaunchConfiguration;
+import org.eclipse.debug.ui.DebugUITools;
+import org.eclipse.debug.ui.ILaunchShortcut;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.ui.IEditorPart;
+
+/**
+ * Launch shortcut to launch debug/run configuration directly.
+ */
+public class LaunchShortcut implements ILaunchShortcut {
+
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.ILaunchShortcut#launch(
+ * org.eclipse.jface.viewers.ISelection, java.lang.String)
+ */
+ public void launch(ISelection selection, String mode) {
+ if (selection instanceof IStructuredSelection) {
+
+ // get the object and the project from it
+ IStructuredSelection structSelect = (IStructuredSelection)selection;
+ Object o = structSelect.getFirstElement();
+
+ // get the first (and normally only) element
+ if (o instanceof IAdaptable) {
+ IResource r = (IResource)((IAdaptable)o).getAdapter(IResource.class);
+
+ // get the project from the resource
+ if (r != null) {
+ IProject project = r.getProject();
+
+ if (project != null) {
+ // and launch
+ launch(project, mode);
+ }
+ }
+ }
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.ILaunchShortcut#launch(
+ * org.eclipse.ui.IEditorPart, java.lang.String)
+ */
+ public void launch(IEditorPart editor, String mode) {
+ // since we force the shortcut to only work on selection in the
+ // package explorer, this will never be called.
+ }
+
+
+ /**
+ * Launch a config for the specified project.
+ * @param project The project to launch
+ * @param mode The launch mode ("debug", "run" or "profile")
+ */
+ private void launch(IProject project, String mode) {
+ // get an existing or new launch configuration
+ ILaunchConfiguration config = AndroidLaunchController.getLaunchConfig(project);
+
+ if (config != null) {
+ // and launch!
+ DebugUITools.launch(config, mode);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/ui/EmulatorConfigTab.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/ui/EmulatorConfigTab.java
new file mode 100644
index 0000000..d919c1f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/ui/EmulatorConfigTab.java
@@ -0,0 +1,459 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.debug.ui;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.debug.launching.LaunchConfigDelegate;
+import com.android.ide.eclipse.adt.sdk.Sdk;
+import com.android.ide.eclipse.common.project.BaseProjectHelper;
+import com.android.ide.eclipse.ddms.DdmsPlugin;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.avd.AvdManager;
+import com.android.sdklib.avd.AvdManager.AvdInfo;
+import com.android.sdkuilib.AvdSelector;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.debug.core.ILaunchConfiguration;
+import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
+import org.eclipse.debug.ui.AbstractLaunchConfigurationTab;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
+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.Font;
+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.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+/**
+ * Launch configuration tab to control the parameters of the Emulator
+ */
+public class EmulatorConfigTab extends AbstractLaunchConfigurationTab {
+
+ private final static String[][] NETWORK_SPEEDS = new String[][] {
+ { "Full", "full" }, //$NON-NLS-2$
+ { "GSM", "gsm" }, //$NON-NLS-2$
+ { "HSCSD", "hscsd" }, //$NON-NLS-2$
+ { "GPRS", "gprs" }, //$NON-NLS-2$
+ { "EDGE", "edge" }, //$NON-NLS-2$
+ { "UMTS", "umts" }, //$NON-NLS-2$
+ { "HSPDA", "hsdpa" }, //$NON-NLS-2$
+ };
+
+ private final static String[][] NETWORK_LATENCIES = new String[][] {
+ { "None", "none" }, //$NON-NLS-2$
+ { "GPRS", "gprs" }, //$NON-NLS-2$
+ { "EDGE", "edge" }, //$NON-NLS-2$
+ { "UMTS", "umts" }, //$NON-NLS-2$
+ };
+
+ private Button mAutoTargetButton;
+ private Button mManualTargetButton;
+
+ private AvdSelector mPreferredAvdSelector;
+
+ private Combo mSpeedCombo;
+
+ private Combo mDelayCombo;
+
+ private Group mEmulatorOptionsGroup;
+
+ private Text mEmulatorCLOptions;
+
+ private Button mWipeDataButton;
+
+ private Button mNoBootAnimButton;
+
+ private Label mPreferredAvdLabel;
+
+ /**
+ * Returns the emulator ready speed option value.
+ * @param value The index of the combo selection.
+ */
+ public static String getSpeed(int value) {
+ try {
+ return NETWORK_SPEEDS[value][1];
+ } catch (ArrayIndexOutOfBoundsException e) {
+ return NETWORK_SPEEDS[LaunchConfigDelegate.DEFAULT_SPEED][1];
+ }
+ }
+
+ /**
+ * Returns the emulator ready network latency value.
+ * @param value The index of the combo selection.
+ */
+ public static String getDelay(int value) {
+ try {
+ return NETWORK_LATENCIES[value][1];
+ } catch (ArrayIndexOutOfBoundsException e) {
+ return NETWORK_LATENCIES[LaunchConfigDelegate.DEFAULT_DELAY][1];
+ }
+ }
+
+ /**
+ *
+ */
+ public EmulatorConfigTab() {
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.ILaunchConfigurationTab#createControl(org.eclipse.swt.widgets.Composite)
+ */
+ public void createControl(Composite parent) {
+ Font font = parent.getFont();
+
+ Composite topComp = new Composite(parent, SWT.NONE);
+ setControl(topComp);
+ GridLayout topLayout = new GridLayout();
+ topLayout.numColumns = 1;
+ topLayout.verticalSpacing = 0;
+ topComp.setLayout(topLayout);
+ topComp.setFont(font);
+
+ GridData gd;
+ GridLayout layout;
+
+ // radio button for the target mode
+ Group targetModeGroup = new Group(topComp, SWT.NONE);
+ targetModeGroup.setText("Device Target Selection Mode");
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ targetModeGroup.setLayoutData(gd);
+ layout = new GridLayout();
+ layout.numColumns = 1;
+ targetModeGroup.setLayout(layout);
+ targetModeGroup.setFont(font);
+
+ mManualTargetButton = new Button(targetModeGroup, SWT.RADIO);
+ mManualTargetButton.setText("Manual");
+ // Since there are only 2 radio buttons, we can put a listener on only one (they
+ // are both called on select and unselect event.
+
+ // add the radio button
+ mAutoTargetButton = new Button(targetModeGroup, SWT.RADIO);
+ mAutoTargetButton.setText("Automatic");
+ mAutoTargetButton.setSelection(true);
+ mAutoTargetButton.addSelectionListener(new SelectionAdapter() {
+ // called when selection changes
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ updateLaunchConfigurationDialog();
+
+ boolean auto = mAutoTargetButton.getSelection();
+ mPreferredAvdSelector.setEnabled(auto);
+ mPreferredAvdLabel.setEnabled(auto);
+ }
+ });
+
+ Composite offsetComp = new Composite(targetModeGroup, 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);
+
+ mPreferredAvdLabel = new Label(offsetComp, SWT.NONE);
+ mPreferredAvdLabel.setText("Select a preferred Android Virtual Device:");
+ AvdInfo[] avds = new AvdInfo[0];
+ mPreferredAvdSelector = new AvdSelector(offsetComp, avds,
+ false /*allowMultipleSelection*/);
+ mPreferredAvdSelector.setTableHeightHint(100);
+ mPreferredAvdSelector.setSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ updateLaunchConfigurationDialog();
+ }
+ });
+
+ // emulator size
+ mEmulatorOptionsGroup = new Group(topComp, SWT.NONE);
+ mEmulatorOptionsGroup.setText("Emulator launch parameters:");
+ mEmulatorOptionsGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ layout = new GridLayout();
+ layout.numColumns = 2;
+ mEmulatorOptionsGroup.setLayout(layout);
+ mEmulatorOptionsGroup.setFont(font);
+
+ // network options
+ new Label(mEmulatorOptionsGroup, SWT.NONE).setText("Network Speed:");
+
+ mSpeedCombo = new Combo(mEmulatorOptionsGroup, SWT.READ_ONLY);
+ for (String[] speed : NETWORK_SPEEDS) {
+ mSpeedCombo.add(speed[0]);
+ }
+ mSpeedCombo.addSelectionListener(new SelectionAdapter() {
+ // called when selection changes
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ updateLaunchConfigurationDialog();
+ }
+ });
+ mSpeedCombo.pack();
+
+ new Label(mEmulatorOptionsGroup, SWT.NONE).setText("Network Latency:");
+
+ mDelayCombo = new Combo(mEmulatorOptionsGroup, SWT.READ_ONLY);
+
+ for (String[] delay : NETWORK_LATENCIES) {
+ mDelayCombo.add(delay[0]);
+ }
+ mDelayCombo.addSelectionListener(new SelectionAdapter() {
+ // called when selection changes
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ updateLaunchConfigurationDialog();
+ }
+ });
+ mDelayCombo.pack();
+
+ // wipe data option
+ mWipeDataButton = new Button(mEmulatorOptionsGroup, SWT.CHECK);
+ mWipeDataButton.setText("Wipe User Data");
+ mWipeDataButton.setToolTipText("Check this if you want to wipe your user data each time you start the emulator. You will be prompted for confirmation when the emulator starts.");
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalSpan = 2;
+ mWipeDataButton.setLayoutData(gd);
+ mWipeDataButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ updateLaunchConfigurationDialog();
+ }
+ });
+
+ // no boot anim option
+ mNoBootAnimButton = new Button(mEmulatorOptionsGroup, SWT.CHECK);
+ mNoBootAnimButton.setText("Disable Boot Animation");
+ mNoBootAnimButton.setToolTipText("Check this if you want to disable the boot animation. This can help the emulator start faster on slow machines.");
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalSpan = 2;
+ mNoBootAnimButton.setLayoutData(gd);
+ mNoBootAnimButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ updateLaunchConfigurationDialog();
+ }
+ });
+
+ // custom command line option for emulator
+ Label l = new Label(mEmulatorOptionsGroup, SWT.NONE);
+ l.setText("Additional Emulator Command Line Options");
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalSpan = 2;
+ l.setLayoutData(gd);
+
+ mEmulatorCLOptions = new Text(mEmulatorOptionsGroup, SWT.BORDER);
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalSpan = 2;
+ mEmulatorCLOptions.setLayoutData(gd);
+ mEmulatorCLOptions.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ updateLaunchConfigurationDialog();
+ }
+ });
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.ILaunchConfigurationTab#getName()
+ */
+ public String getName() {
+ return "Target";
+ }
+
+ @Override
+ public Image getImage() {
+ return DdmsPlugin.getImageLoader().loadImage("emulator.png", null); //$NON-NLS-1$
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.ILaunchConfigurationTab#initializeFrom(org.eclipse.debug.core.ILaunchConfiguration)
+ */
+ public void initializeFrom(ILaunchConfiguration configuration) {
+ AvdManager avdManager = Sdk.getCurrent().getAvdManager();
+
+ boolean value = LaunchConfigDelegate.DEFAULT_TARGET_MODE; // true == automatic
+ try {
+ value = configuration.getAttribute(LaunchConfigDelegate.ATTR_TARGET_MODE, value);
+ } catch (CoreException e) {
+ // let's not do anything here, we'll use the default value
+ }
+ mAutoTargetButton.setSelection(value);
+ mManualTargetButton.setSelection(!value);
+
+ // look for the project name to get its target.
+ String stringValue = "";
+ try {
+ stringValue = configuration.getAttribute(
+ IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, stringValue);
+ } catch (CoreException ce) {
+ // let's not do anything here, we'll use the default value
+ }
+
+ IProject project = null;
+
+ // get the list of existing Android projects from the workspace.
+ IJavaProject[] projects = BaseProjectHelper.getAndroidProjects();
+ if (projects != null) {
+ // look for the project whose name we read from the configuration.
+ for (IJavaProject p : projects) {
+ if (p.getElementName().equals(stringValue)) {
+ project = p.getProject();
+ break;
+ }
+ }
+ }
+
+ // update the AVD list
+ AvdInfo[] avds = null;
+ if (avdManager != null) {
+ avds = avdManager.getAvds();
+ }
+
+ IAndroidTarget projectTarget = null;
+ if (project != null) {
+ projectTarget = Sdk.getCurrent().getTarget(project);
+ } else {
+ avds = null; // no project? we don't want to display any "compatible" AVDs.
+ }
+
+ mPreferredAvdSelector.setAvds(avds, projectTarget);
+
+ stringValue = "";
+ try {
+ stringValue = configuration.getAttribute(LaunchConfigDelegate.ATTR_AVD_NAME,
+ stringValue);
+ } catch (CoreException e) {
+ // let's not do anything here, we'll use the default value
+ }
+
+ if (stringValue != null && stringValue.length() > 0 && avdManager != null) {
+ AvdInfo targetAvd = avdManager.getAvd(stringValue);
+ mPreferredAvdSelector.setSelection(targetAvd);
+ } else {
+ mPreferredAvdSelector.setSelection(null);
+ }
+
+ value = LaunchConfigDelegate.DEFAULT_WIPE_DATA;
+ try {
+ value = configuration.getAttribute(LaunchConfigDelegate.ATTR_WIPE_DATA, value);
+ } catch (CoreException e) {
+ // let's not do anything here, we'll use the default value
+ }
+ mWipeDataButton.setSelection(value);
+
+ value = LaunchConfigDelegate.DEFAULT_NO_BOOT_ANIM;
+ try {
+ value = configuration.getAttribute(LaunchConfigDelegate.ATTR_NO_BOOT_ANIM, value);
+ } catch (CoreException e) {
+ // let's not do anything here, we'll use the default value
+ }
+ mNoBootAnimButton.setSelection(value);
+
+ int index = -1;
+
+ index = LaunchConfigDelegate.DEFAULT_SPEED;
+ try {
+ index = configuration.getAttribute(LaunchConfigDelegate.ATTR_SPEED,
+ index);
+ } catch (CoreException e) {
+ // let's not do anything here, we'll use the default value
+ }
+ if (index == -1) {
+ mSpeedCombo.clearSelection();
+ } else {
+ mSpeedCombo.select(index);
+ }
+
+ index = LaunchConfigDelegate.DEFAULT_DELAY;
+ try {
+ index = configuration.getAttribute(LaunchConfigDelegate.ATTR_DELAY,
+ index);
+ } catch (CoreException e) {
+ // let's not do anything here, we'll put a proper value in
+ // performApply anyway
+ }
+ if (index == -1) {
+ mDelayCombo.clearSelection();
+ } else {
+ mDelayCombo.select(index);
+ }
+
+ String commandLine = null;
+ try {
+ commandLine = configuration.getAttribute(
+ LaunchConfigDelegate.ATTR_COMMANDLINE, ""); //$NON-NLS-1$
+ } catch (CoreException e) {
+ // let's not do anything here, we'll use the default value
+ }
+ if (commandLine != null) {
+ mEmulatorCLOptions.setText(commandLine);
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.ILaunchConfigurationTab#performApply(org.eclipse.debug.core.ILaunchConfigurationWorkingCopy)
+ */
+ public void performApply(ILaunchConfigurationWorkingCopy configuration) {
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_TARGET_MODE,
+ mAutoTargetButton.getSelection());
+ AvdInfo avd = mPreferredAvdSelector.getFirstSelected();
+ if (avd != null) {
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_AVD_NAME, avd.getName());
+ } else {
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_AVD_NAME, (String)null);
+ }
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_SPEED,
+ mSpeedCombo.getSelectionIndex());
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_DELAY,
+ mDelayCombo.getSelectionIndex());
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_COMMANDLINE,
+ mEmulatorCLOptions.getText());
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_WIPE_DATA,
+ mWipeDataButton.getSelection());
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_NO_BOOT_ANIM,
+ mNoBootAnimButton.getSelection());
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.ILaunchConfigurationTab#setDefaults(org.eclipse.debug.core.ILaunchConfigurationWorkingCopy)
+ */
+ public void setDefaults(ILaunchConfigurationWorkingCopy configuration) {
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_TARGET_MODE,
+ LaunchConfigDelegate.DEFAULT_TARGET_MODE);
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_SPEED,
+ LaunchConfigDelegate.DEFAULT_SPEED);
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_DELAY,
+ LaunchConfigDelegate.DEFAULT_DELAY);
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_WIPE_DATA,
+ LaunchConfigDelegate.DEFAULT_WIPE_DATA);
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_NO_BOOT_ANIM,
+ LaunchConfigDelegate.DEFAULT_NO_BOOT_ANIM);
+
+ IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore();
+ String emuOptions = store.getString(AdtPlugin.PREFS_EMU_OPTIONS);
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_COMMANDLINE, emuOptions);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/ui/LaunchConfigTabGroup.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/ui/LaunchConfigTabGroup.java
new file mode 100644
index 0000000..c0dbd54
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/ui/LaunchConfigTabGroup.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.debug.ui;
+
+import org.eclipse.debug.ui.AbstractLaunchConfigurationTabGroup;
+import org.eclipse.debug.ui.CommonTab;
+import org.eclipse.debug.ui.ILaunchConfigurationDialog;
+import org.eclipse.debug.ui.ILaunchConfigurationTab;
+
+/**
+ * Tab group object for Android Launch Config type.
+ */
+public class LaunchConfigTabGroup extends AbstractLaunchConfigurationTabGroup {
+
+ public LaunchConfigTabGroup() {
+ }
+
+ public void createTabs(ILaunchConfigurationDialog dialog, String mode) {
+ ILaunchConfigurationTab[] tabs = new ILaunchConfigurationTab[] {
+ new MainLaunchConfigTab(),
+ new EmulatorConfigTab(),
+ new CommonTab()
+ };
+ setTabs(tabs);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/ui/MainLaunchConfigTab.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/ui/MainLaunchConfigTab.java
new file mode 100644
index 0000000..6a40ed0
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/debug/ui/MainLaunchConfigTab.java
@@ -0,0 +1,489 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.debug.ui;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.debug.launching.AndroidLaunchController;
+import com.android.ide.eclipse.adt.debug.launching.LaunchConfigDelegate;
+import com.android.ide.eclipse.common.project.AndroidManifestParser;
+import com.android.ide.eclipse.common.project.BaseProjectHelper;
+import com.android.ide.eclipse.common.project.ProjectChooserHelper;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.debug.core.ILaunchConfiguration;
+import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
+import org.eclipse.debug.ui.AbstractLaunchConfigurationTab;
+import org.eclipse.debug.ui.ILaunchConfigurationTab;
+import org.eclipse.jdt.core.IJavaModel;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
+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.graphics.Font;
+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.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Text;
+
+/**
+ * Class for the main launch configuration tab.
+ */
+public class MainLaunchConfigTab extends AbstractLaunchConfigurationTab {
+
+ protected static final String EMPTY_STRING = ""; //$NON-NLS-1$
+
+ protected Text mProjText;
+ private Button mProjButton;
+
+ private Combo mActivityCombo;
+ private String[] mActivities;
+
+ private WidgetListener mListener = new WidgetListener();
+
+ private Button mDefaultActionButton;
+ private Button mActivityActionButton;
+ private Button mDoNothingActionButton;
+ private int mLaunchAction = LaunchConfigDelegate.DEFAULT_LAUNCH_ACTION;
+
+ private ProjectChooserHelper mProjectChooserHelper;
+
+ /**
+ * A listener which handles widget change events for the controls in this
+ * tab.
+ */
+ private class WidgetListener implements ModifyListener, SelectionListener {
+
+ public void modifyText(ModifyEvent e) {
+ IProject project = checkParameters();
+ loadActivities(project);
+ setDirty(true);
+ }
+
+ public void widgetDefaultSelected(SelectionEvent e) {/* do nothing */
+ }
+
+ public void widgetSelected(SelectionEvent e) {
+ Object source = e.getSource();
+ if (source == mProjButton) {
+ handleProjectButtonSelected();
+ } else {
+ checkParameters();
+ }
+ }
+ }
+
+ public MainLaunchConfigTab() {
+ }
+
+ public void createControl(Composite parent) {
+ mProjectChooserHelper = new ProjectChooserHelper(parent.getShell());
+
+ Font font = parent.getFont();
+ Composite comp = new Composite(parent, SWT.NONE);
+ setControl(comp);
+ GridLayout topLayout = new GridLayout();
+ topLayout.verticalSpacing = 0;
+ comp.setLayout(topLayout);
+ comp.setFont(font);
+ createProjectEditor(comp);
+ createVerticalSpacer(comp, 1);
+
+ // create the combo for the activity chooser
+ Group group = new Group(comp, SWT.NONE);
+ group.setText("Launch Action:");
+ GridData gd = new GridData(GridData.FILL_HORIZONTAL);
+ group.setLayoutData(gd);
+ GridLayout layout = new GridLayout();
+ layout.numColumns = 2;
+ group.setLayout(layout);
+ group.setFont(font);
+
+ mDefaultActionButton = new Button(group, SWT.RADIO);
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalSpan = 2;
+ mDefaultActionButton.setLayoutData(gd);
+ mDefaultActionButton.setText("Launch Default Activity");
+ mDefaultActionButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // event are received for both selection and deselection, so we only process
+ // the selection event to avoid doing it twice.
+ if (mDefaultActionButton.getSelection() == true) {
+ mLaunchAction = LaunchConfigDelegate.ACTION_DEFAULT;
+ mActivityCombo.setEnabled(false);
+ checkParameters();
+ }
+ }
+ });
+
+ mActivityActionButton = new Button(group, SWT.RADIO);
+ mActivityActionButton.setText("Launch:");
+ mActivityActionButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // event are received for both selection and deselection, so we only process
+ // the selection event to avoid doing it twice.
+ if (mActivityActionButton.getSelection() == true) {
+ mLaunchAction = LaunchConfigDelegate.ACTION_ACTIVITY;
+ mActivityCombo.setEnabled(true);
+ checkParameters();
+ }
+ }
+ });
+
+ mActivityCombo = new Combo(group, SWT.DROP_DOWN | SWT.READ_ONLY);
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ mActivityCombo.setLayoutData(gd);
+ mActivityCombo.clearSelection();
+ mActivityCombo.setEnabled(false);
+ mActivityCombo.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ checkParameters();
+ }
+ });
+
+ mDoNothingActionButton = new Button(group, SWT.RADIO);
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalSpan = 2;
+ mDoNothingActionButton.setLayoutData(gd);
+ mDoNothingActionButton.setText("Do Nothing");
+ mDoNothingActionButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // event are received for both selection and deselection, so we only process
+ // the selection event to avoid doing it twice.
+ if (mDoNothingActionButton.getSelection() == true) {
+ mLaunchAction = LaunchConfigDelegate.ACTION_DO_NOTHING;
+ mActivityCombo.setEnabled(false);
+ checkParameters();
+ }
+ }
+ });
+
+ }
+
+ public String getName() {
+ return "Android";
+ }
+
+ @Override
+ public Image getImage() {
+ return AdtPlugin.getImageLoader().loadImage("mainLaunchTab.png", null);
+ }
+
+
+ public void performApply(ILaunchConfigurationWorkingCopy configuration) {
+ configuration.setAttribute(
+ IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, mProjText.getText());
+ configuration.setAttribute(
+ IJavaLaunchConfigurationConstants.ATTR_ALLOW_TERMINATE, true);
+
+ // add the launch mode
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_LAUNCH_ACTION, mLaunchAction);
+
+ // add the activity
+ int selection = mActivityCombo.getSelectionIndex();
+ if (mActivities != null && selection >=0 && selection < mActivities.length) {
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_ACTIVITY, mActivities[selection]);
+ }
+
+ // link the project and the launch config.
+ mapResources(configuration);
+ }
+
+ public void setDefaults(ILaunchConfigurationWorkingCopy configuration) {
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_LAUNCH_ACTION,
+ LaunchConfigDelegate.DEFAULT_LAUNCH_ACTION);
+ }
+
+ /**
+ * Creates the widgets for specifying a main type.
+ *
+ * @param parent the parent composite
+ */
+ protected void createProjectEditor(Composite parent) {
+ Font font = parent.getFont();
+ Group group = new Group(parent, SWT.NONE);
+ group.setText("Project:");
+ GridData gd = new GridData(GridData.FILL_HORIZONTAL);
+ group.setLayoutData(gd);
+ GridLayout layout = new GridLayout();
+ layout.numColumns = 2;
+ group.setLayout(layout);
+ group.setFont(font);
+ mProjText = new Text(group, SWT.SINGLE | SWT.BORDER);
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ mProjText.setLayoutData(gd);
+ mProjText.setFont(font);
+ mProjText.addModifyListener(mListener);
+ mProjButton = createPushButton(group, "Browse...", null);
+ mProjButton.addSelectionListener(mListener);
+ }
+
+ /**
+ * returns the default listener from this class. For all subclasses this
+ * listener will only provide the functi Jaonality of updating the current
+ * tab
+ *
+ * @return a widget listener
+ */
+ protected WidgetListener getDefaultListener() {
+ return mListener;
+ }
+
+ /**
+ * Return the {@link IJavaProject} corresponding to the project name in the project
+ * name text field, or null if the text does not match a project name.
+ * @param javaModel the Java Model object corresponding for the current workspace root.
+ * @return a IJavaProject object or null.
+ */
+ protected IJavaProject getJavaProject(IJavaModel javaModel) {
+ String projectName = mProjText.getText().trim();
+ if (projectName.length() < 1) {
+ return null;
+ }
+ return javaModel.getJavaProject(projectName);
+ }
+
+ /**
+ * Show a dialog that lets the user select a project. This in turn provides
+ * context for the main type, allowing the user to key a main type name, or
+ * constraining the search for main types to the specified project.
+ */
+ protected void handleProjectButtonSelected() {
+ IJavaProject javaProject = mProjectChooserHelper.chooseJavaProject(
+ mProjText.getText().trim());
+ if (javaProject == null) {
+ return;
+ }// end if
+ String projectName = javaProject.getElementName();
+ mProjText.setText(projectName);
+
+ // get the list of activities and fill the combo
+ IProject project = javaProject.getProject();
+ loadActivities(project);
+ }// end handle selected
+
+ /**
+ * Initializes this tab's controls with values from the given
+ * launch configuration. This method is called when
+ * a configuration is selected to view or edit, after this
+ * tab's control has been created.
+ *
+ * @param config launch configuration
+ *
+ * @see ILaunchConfigurationTab
+ */
+ public void initializeFrom(ILaunchConfiguration config) {
+ String projectName = EMPTY_STRING;
+ try {
+ projectName = config.getAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME,
+ EMPTY_STRING);
+ }// end try
+ catch (CoreException ce) {
+ }
+ mProjText.setText(projectName);
+
+ // get the list of projects
+ IJavaProject[] projects = mProjectChooserHelper.getAndroidProjects(null);
+
+ if (projects != null) {
+ // look for the currently selected project
+ IProject proj = null;
+ for (IJavaProject p : projects) {
+ if (p.getElementName().equals(projectName)) {
+ proj = p.getProject();
+ break;
+ }
+ }
+
+ loadActivities(proj);
+ }
+
+ // load the launch action.
+ mLaunchAction = LaunchConfigDelegate.DEFAULT_LAUNCH_ACTION;
+ try {
+ mLaunchAction = config.getAttribute(LaunchConfigDelegate.ATTR_LAUNCH_ACTION,
+ mLaunchAction);
+ } catch (CoreException e) {
+ // nothing to be done really. launchAction will keep its default value.
+ }
+
+ mDefaultActionButton.setSelection(mLaunchAction == LaunchConfigDelegate.ACTION_DEFAULT);
+ mActivityActionButton.setSelection(mLaunchAction == LaunchConfigDelegate.ACTION_ACTIVITY);
+ mDoNothingActionButton.setSelection(
+ mLaunchAction == LaunchConfigDelegate.ACTION_DO_NOTHING);
+
+ // now look for the activity and load it if present, otherwise, revert
+ // to the current one.
+ String activityName = EMPTY_STRING;
+ try {
+ activityName = config.getAttribute(LaunchConfigDelegate.ATTR_ACTIVITY, EMPTY_STRING);
+ }// end try
+ catch (CoreException ce) {
+ // nothing to be done really. activityName will stay empty
+ }
+
+ if (mLaunchAction != LaunchConfigDelegate.ACTION_ACTIVITY) {
+ mActivityCombo.setEnabled(false);
+ mActivityCombo.clearSelection();
+ } else {
+ mActivityCombo.setEnabled(true);
+ if (activityName == null || activityName.equals(EMPTY_STRING)) {
+ mActivityCombo.clearSelection();
+ } else if (mActivities != null && mActivities.length > 0) {
+ // look for the name of the activity in the combo.
+ boolean found = false;
+ for (int i = 0 ; i < mActivities.length ; i++) {
+ if (activityName.equals(mActivities[i])) {
+ found = true;
+ mActivityCombo.select(i);
+ break;
+ }
+ }
+
+ // if we haven't found a matching activity we clear the combo selection
+ if (found == false) {
+ mActivityCombo.clearSelection();
+ }
+ }
+ }
+ }
+
+ /**
+ * Associates the launch config and the project. This allows Eclipse to delete the launch
+ * config when the project is deleted.
+ *
+ * @param config the launch config working copy.
+ */
+ protected void mapResources(ILaunchConfigurationWorkingCopy config) {
+ // get the java model
+ IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot();
+ IJavaModel javaModel = JavaCore.create(workspaceRoot);
+
+ // get the IJavaProject described by the text field.
+ IJavaProject javaProject = getJavaProject(javaModel);
+ IResource[] resources = null;
+ if (javaProject != null) {
+ resources = AndroidLaunchController.getResourcesToMap(javaProject.getProject());
+ }
+ config.setMappedResources(resources);
+ }
+
+ /**
+ * Loads the ui with the activities of the specified project, and stores the
+ * activities in <code>mActivities</code>.
+ * <p/>
+ * First activity is selected by default if present.
+ *
+ * @param project The project to load the activities from.
+ */
+ private void loadActivities(IProject project) {
+ if (project != null) {
+ try {
+ // parse the manifest for the list of activities.
+ AndroidManifestParser manifestParser = AndroidManifestParser.parse(
+ BaseProjectHelper.getJavaProject(project), null /* errorListener */,
+ true /* gatherData */, false /* markErrors */);
+ if (manifestParser != null) {
+ mActivities = manifestParser.getActivities();
+
+ mActivityCombo.removeAll();
+
+ if (mActivities.length > 0) {
+ if (mLaunchAction == LaunchConfigDelegate.ACTION_ACTIVITY) {
+ mActivityCombo.setEnabled(true);
+ }
+ for (String s : mActivities) {
+ mActivityCombo.add(s);
+ }
+ } else {
+ mActivityCombo.setEnabled(false);
+ }
+
+ // the selection will be set when we update the ui from the current
+ // config object.
+ mActivityCombo.clearSelection();
+
+ return;
+ }
+
+ } catch (CoreException e) {
+ // The AndroidManifest parsing failed. The builders must have reported the errors
+ // already so there's nothing to do.
+ }
+ }
+
+ // if we reach this point, either project is null, or we got an exception during
+ // the parsing. In either case, we empty the activity list.
+ mActivityCombo.removeAll();
+ mActivities = null;
+ }
+
+ /**
+ * Checks the parameters for correctness, and update the error message and buttons.
+ * @return the current IProject of this launch config.
+ */
+ private IProject checkParameters() {
+ try {
+ //test the project name first!
+ String text = mProjText.getText();
+ if (text.length() == 0) {
+ setErrorMessage("Project Name is required!");
+ } else if (text.matches("[a-zA-Z0-9_ \\.-]+") == false) {
+ setErrorMessage("Project name contains unsupported characters!");
+ } else {
+ IJavaProject[] projects = mProjectChooserHelper.getAndroidProjects(null);
+ IProject found = null;
+ for (IJavaProject javaProject : projects) {
+ if (javaProject.getProject().getName().equals(text)) {
+ found = javaProject.getProject();
+ break;
+ }
+
+ }
+
+ if (found != null) {
+ setErrorMessage(null);
+ } else {
+ setErrorMessage(String.format("There is no android project named '%1$s'",
+ text));
+ }
+
+ return found;
+ }
+ } finally {
+ updateLaunchConfigurationDialog();
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/messages.properties b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/messages.properties
new file mode 100644
index 0000000..dfb0eb2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/messages.properties
@@ -0,0 +1,16 @@
+Dialog_Title_SDK_Location=Android SDK Location
+SDK_Not_Setup=The location of the Android SDK has not been setup. Please go to Preferences > Android and set it up
+Error_Check_Prefs=%1$s\nPlease check the Android plugin's preferences.
+Could_Not_Find_Folder=Could not find SDK folder '%1$s'.
+Could_Not_Find_Folder_In_SDK=Could not find folder '%1$s' inside SDK '%2$s'.
+Could_Not_Find=Could not find %1$s\!
+VersionCheck_SDK_Milestone_Too_Low=This version of ADT requires a SDK in version M%1$d or above.\n\nCurrent version is %2$s.\n\nPlease update your SDK to the latest version.
+VersionCheck_SDK_Build_Too_Low=This version of ADT requires a SDK in version %1$d (M%2$d) or above.\n\nCurrent version is %3$s.\n\nPlease update your SDK to the latest version.
+VersionCheck_Plugin_Version_Failed=Failed to get the required ADT version number from the SDK.\n\nThe Android Developer Toolkit may not work properly.
+VersionCheck_Unable_To_Parse_Version_s=Unable to parse sdk build version: %1$s
+VersionCheck_Plugin_Too_Old=This Android SDK requires Android Developer Toolkit version %1$d.%2$d.%3$d or above.\n\nCurrent version is %4$s.\n\nPlease update ADT to the latest version.
+AdtPlugin_Failed_To_Start_s=Failed to start %1$s
+AdtPlugin_Android_SDK_Content_Loader=Android SDK Content Loader
+AdtPlugin_Parsing_Resources=Parsing Resources
+AdtPlugin_Android_SDK_Resource_Parser=Android SDK Resource Parser
+AdtPlugin_Failed_To_Parse_s=Failed to parse:
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/AndroidPreferencePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/AndroidPreferencePage.java
new file mode 100644
index 0000000..458f78e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/AndroidPreferencePage.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.preferences;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.sdk.Sdk;
+import com.android.ide.eclipse.adt.sdk.Sdk.ITargetChangeListener;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdkuilib.SdkTargetSelector;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.jface.preference.DirectoryFieldEditor;
+import org.eclipse.jface.preference.FieldEditorPreferencePage;
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.IWorkbenchPreferencePage;
+
+import java.io.File;
+
+/**
+ * This class represents a preference page that is contributed to the
+ * Preferences dialog. By subclassing <samp>FieldEditorPreferencePage</samp>,
+ * we can use the field support built into JFace that allows us to create a page
+ * that is small and knows how to save, restore and apply itself.
+ * <p>
+ * This page is used to modify preferences only. They are stored in the
+ * preference store that belongs to the main plug-in class. That way,
+ * preferences can be accessed directly via the preference store.
+ */
+
+public class AndroidPreferencePage extends FieldEditorPreferencePage implements
+ IWorkbenchPreferencePage {
+
+ public AndroidPreferencePage() {
+ super(GRID);
+ setPreferenceStore(AdtPlugin.getDefault().getPreferenceStore());
+ setDescription(Messages.AndroidPreferencePage_Title);
+ }
+
+ /**
+ * Creates the field editors. Field editors are abstractions of the common
+ * GUI blocks needed to manipulate various types of preferences. Each field
+ * editor knows how to save and restore itself.
+ */
+ @Override
+ public void createFieldEditors() {
+
+ addField(new SdkDirectoryFieldEditor(AdtPlugin.PREFS_SDK_DIR,
+ Messages.AndroidPreferencePage_SDK_Location_, getFieldEditorParent()));
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.eclipse.ui.IWorkbenchPreferencePage#init(org.eclipse.ui.IWorkbench)
+ */
+ public void init(IWorkbench workbench) {
+ }
+
+ /**
+ * Custom version of DirectoryFieldEditor which validates that the directory really
+ * contains an SDK.
+ *
+ * There's a known issue here, which is really a rare edge-case: if the pref dialog is open
+ * which a given sdk directory and the *content* of the directory changes such that the sdk
+ * state changed (i.e. from valid to invalid or vice versa), the pref panel will display or
+ * hide the error as appropriate but the pref panel will fail to validate the apply/ok buttons
+ * appropriately. The easy workaround is to cancel the pref panel and enter it again.
+ */
+ private static class SdkDirectoryFieldEditor extends DirectoryFieldEditor {
+
+ private SdkTargetSelector mTargetSelector;
+ private TargetChangedListener mTargetChangeListener;
+
+ public SdkDirectoryFieldEditor(String name, String labelText, Composite parent) {
+ super(name, labelText, parent);
+ setEmptyStringAllowed(false);
+ }
+
+ /**
+ * Method declared on StringFieldEditor and overridden in DirectoryFieldEditor.
+ * Checks whether the text input field contains a valid directory.
+ *
+ * @return True if the apply/ok button should be enabled in the pref panel
+ */
+ @Override
+ protected boolean doCheckState() {
+ String fileName = getTextControl().getText();
+ fileName = fileName.trim();
+
+ if (fileName.indexOf(',') >= 0 || fileName.indexOf(';') >= 0) {
+ setErrorMessage(Messages.AndroidPreferencePage_ERROR_Reserved_Char);
+ return false; // Apply/OK must be disabled
+ }
+
+ File file = new File(fileName);
+ if (!file.isDirectory()) {
+ setErrorMessage(JFaceResources.getString(
+ "DirectoryFieldEditor.errorMessage")); //$NON-NLS-1$
+ return false;
+ }
+
+ boolean ok = AdtPlugin.getDefault().checkSdkLocationAndId(fileName,
+ new AdtPlugin.CheckSdkErrorHandler() {
+ @Override
+ public boolean handleError(String message) {
+ setErrorMessage(message.replaceAll("\n", " ")); //$NON-NLS-1$ //$NON-NLS-2$
+ return false; // Apply/OK must be disabled
+ }
+
+ @Override
+ public boolean handleWarning(String message) {
+ showMessage(message.replaceAll("\n", " ")); //$NON-NLS-1$ //$NON-NLS-2$
+ return true; // Apply/OK must be enabled
+ }
+ });
+ if (ok) clearMessage();
+ return ok;
+ }
+
+ @Override
+ public Text getTextControl(Composite parent) {
+ setValidateStrategy(VALIDATE_ON_KEY_STROKE);
+ return super.getTextControl(parent);
+ }
+
+ /* (non-Javadoc)
+ * Method declared on StringFieldEditor (and FieldEditor).
+ */
+ @Override
+ protected void doFillIntoGrid(Composite parent, int numColumns) {
+ super.doFillIntoGrid(parent, numColumns);
+
+ GridData gd;
+ Label l = new Label(parent, SWT.NONE);
+ l.setText("Note: The list of SDK Targets below is only reloaded once you hit 'Apply' or 'OK'.");
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalSpan = numColumns;
+ l.setLayoutData(gd);
+
+ try {
+ // We may not have an sdk if the sdk path pref is empty or not valid.
+ Sdk sdk = Sdk.getCurrent();
+ IAndroidTarget[] targets = sdk != null ? sdk.getTargets() : null;
+
+ mTargetSelector = new SdkTargetSelector(parent,
+ targets,
+ false, /*allowSelection*/
+ false /*multipleSelection*/);
+ gd = (GridData) mTargetSelector.getLayoutData();
+ gd.horizontalSpan = numColumns;
+
+ if (mTargetChangeListener == null) {
+ mTargetChangeListener = new TargetChangedListener();
+ AdtPlugin.getDefault().addTargetListener(mTargetChangeListener);
+ }
+ } catch (Exception e) {
+ // We need to catch *any* exception that arises here, otherwise it disables
+ // the whole pref panel. We can live without the Sdk target selector but
+ // not being able to actually set an sdk path.
+ AdtPlugin.log(e, "SdkTargetSelector failed");
+ }
+ }
+
+ @Override
+ public void dispose() {
+ super.dispose();
+ if (mTargetChangeListener != null) {
+ AdtPlugin.getDefault().removeTargetListener(mTargetChangeListener);
+ mTargetChangeListener = null;
+ }
+ }
+
+ private class TargetChangedListener implements ITargetChangeListener {
+ public void onProjectTargetChange(IProject changedProject) {
+ // do nothing.
+ }
+
+ public void onTargetsLoaded() {
+ if (mTargetSelector != null) {
+ // We may not have an sdk if the sdk path pref is empty or not valid.
+ Sdk sdk = Sdk.getCurrent();
+ IAndroidTarget[] targets = sdk != null ? sdk.getTargets() : null;
+
+ mTargetSelector.setTargets(targets);
+ }
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/BuildPreferencePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/BuildPreferencePage.java
new file mode 100644
index 0000000..e64c2f4
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/BuildPreferencePage.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.preferences;
+
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.jarutils.DebugKeyProvider;
+import com.android.jarutils.DebugKeyProvider.KeytoolException;
+import com.android.prefs.AndroidLocation.AndroidLocationException;
+
+import org.eclipse.jface.preference.BooleanFieldEditor;
+import org.eclipse.jface.preference.FieldEditorPreferencePage;
+import org.eclipse.jface.preference.FileFieldEditor;
+import org.eclipse.jface.preference.RadioGroupFieldEditor;
+import org.eclipse.jface.preference.StringFieldEditor;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.IWorkbenchPreferencePage;
+
+import java.io.File;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.Date;
+
+/**
+ * Preference page for build options.
+ *
+ */
+public class BuildPreferencePage extends FieldEditorPreferencePage implements
+ IWorkbenchPreferencePage {
+
+ final static String BUILD_STR_SILENT = "silent"; //$NON-NLS-1$
+ final static String BUILD_STR_NORMAL = "normal"; //$NON-NLS-1$
+ final static String BUILD_STR_VERBOSE = "verbose"; //$NON-NLS-1$
+
+ public BuildPreferencePage() {
+ super(GRID);
+ setPreferenceStore(AdtPlugin.getDefault().getPreferenceStore());
+ setDescription(Messages.BuildPreferencePage_Title);
+ }
+
+ public static int getBuildLevel(String buildPrefValue) {
+ if (BUILD_STR_SILENT.equals(buildPrefValue)) {
+ return AdtConstants.BUILD_ALWAYS;
+ } else if (BUILD_STR_VERBOSE.equals(buildPrefValue)) {
+ return AdtConstants.BUILD_VERBOSE;
+ }
+
+ return AdtConstants.BUILD_NORMAL;
+ }
+
+ @Override
+ protected void createFieldEditors() {
+ addField(new BooleanFieldEditor(AdtPlugin.PREFS_RES_AUTO_REFRESH,
+ Messages.BuildPreferencePage_Auto_Refresh_Resources_on_Build,
+ getFieldEditorParent()));
+
+ RadioGroupFieldEditor rgfe = new RadioGroupFieldEditor(
+ AdtPlugin.PREFS_BUILD_VERBOSITY,
+ Messages.BuildPreferencePage_Build_Output, 1, new String[][] {
+ { Messages.BuildPreferencePage_Silent, BUILD_STR_SILENT },
+ { Messages.BuildPreferencePage_Normal, BUILD_STR_NORMAL },
+ { Messages.BuildPreferencePage_Verbose, BUILD_STR_VERBOSE }
+ },
+ getFieldEditorParent(), true);
+ addField(rgfe);
+
+ addField(new ReadOnlyFieldEditor(AdtPlugin.PREFS_DEFAULT_DEBUG_KEYSTORE,
+ Messages.BuildPreferencePage_Default_KeyStore, getFieldEditorParent()));
+
+ addField(new KeystoreFieldEditor(AdtPlugin.PREFS_CUSTOM_DEBUG_KEYSTORE,
+ Messages.BuildPreferencePage_Custom_Keystore, getFieldEditorParent()));
+
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.eclipse.ui.IWorkbenchPreferencePage#init(org.eclipse.ui.IWorkbench)
+ */
+ public void init(IWorkbench workbench) {
+ }
+
+ /**
+ * A read-only string field editor.
+ */
+ private static class ReadOnlyFieldEditor extends StringFieldEditor {
+
+ public ReadOnlyFieldEditor(String name, String labelText, Composite parent) {
+ super(name, labelText, parent);
+ }
+
+ @Override
+ protected void createControl(Composite parent) {
+ super.createControl(parent);
+
+ Text control = getTextControl();
+ control.setEditable(false);
+ }
+ }
+
+ /**
+ * Custom {@link FileFieldEditor} that checks that the keystore is valid.
+ */
+ private static class KeystoreFieldEditor extends FileFieldEditor {
+ public KeystoreFieldEditor(String name, String label, Composite parent) {
+ super(name, label, parent);
+ setValidateStrategy(VALIDATE_ON_KEY_STROKE);
+ }
+
+ @Override
+ protected boolean checkState() {
+ String fileName = getTextControl().getText();
+ fileName = fileName.trim();
+
+ // empty values are considered ok.
+ if (fileName.length() > 0) {
+ File file = new File(fileName);
+ if (file.isFile()) {
+ // attempt to load the debug key.
+ try {
+ DebugKeyProvider provider = new DebugKeyProvider(fileName,
+ null /* storeType */, null /* key gen output */);
+ PrivateKey key = provider.getDebugKey();
+ X509Certificate certificate = (X509Certificate)provider.getCertificate();
+
+ if (key == null || certificate == null) {
+ showErrorMessage("Unable to find debug key in keystore!");
+ return false;
+ }
+
+ Date today = new Date();
+ if (certificate.getNotAfter().compareTo(today) < 0) {
+ showErrorMessage("Certificate is expired!");
+ return false;
+ }
+
+ if (certificate.getNotBefore().compareTo(today) > 0) {
+ showErrorMessage("Certificate validity is in the future!");
+ return false;
+ }
+
+ // we're good!
+ clearErrorMessage();
+ return true;
+ } catch (GeneralSecurityException e) {
+ handleException(e);
+ return false;
+ } catch (IOException e) {
+ handleException(e);
+ return false;
+ } catch (KeytoolException e) {
+ handleException(e);
+ return false;
+ } catch (AndroidLocationException e) {
+ handleException(e);
+ return false;
+ }
+
+
+ } else {
+ // file does not exist.
+ showErrorMessage("Not a valid keystore path.");
+ return false; // Apply/OK must be disabled
+ }
+ }
+
+ clearErrorMessage();
+ return true;
+ }
+
+ @Override
+ public Text getTextControl(Composite parent) {
+ setValidateStrategy(VALIDATE_ON_KEY_STROKE);
+ return super.getTextControl(parent);
+ }
+
+ /**
+ * Set the error message from a {@link Throwable}. If the exception has no message, try
+ * to get the message from the cause.
+ * @param t the Throwable.
+ */
+ private void handleException(Throwable t) {
+ String msg = t.getMessage();
+ if (msg == null) {
+ Throwable cause = t.getCause();
+ if (cause != null) {
+ handleException(cause);
+ } else {
+ setErrorMessage("Uknown error when getting the debug key!");
+ }
+
+ return;
+ }
+
+ // valid text, display it.
+ showErrorMessage(msg);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/LaunchPreferencePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/LaunchPreferencePage.java
new file mode 100644
index 0000000..8fd72c1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/LaunchPreferencePage.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.preferences;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+
+import org.eclipse.jface.preference.FieldEditorPreferencePage;
+import org.eclipse.jface.preference.StringFieldEditor;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.IWorkbenchPreferencePage;
+
+/**
+ * Settings page for launch related preferences.
+ */
+public class LaunchPreferencePage extends FieldEditorPreferencePage implements
+ IWorkbenchPreferencePage {
+
+ public LaunchPreferencePage() {
+ super(GRID);
+ setPreferenceStore(AdtPlugin.getDefault().getPreferenceStore());
+ setDescription(Messages.LaunchPreferencePage_Title);
+ }
+
+ @Override
+ protected void createFieldEditors() {
+ addField(new StringFieldEditor(AdtPlugin.PREFS_EMU_OPTIONS,
+ Messages.LaunchPreferencePage_Default_Emu_Options, getFieldEditorParent()));
+
+ addField(new StringFieldEditor(AdtPlugin.PREFS_HOME_PACKAGE,
+ Messages.LaunchPreferencePage_Default_HOME_Package, getFieldEditorParent()));
+ }
+
+ public void init(IWorkbench workbench) {
+ // pass
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/Messages.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/Messages.java
new file mode 100644
index 0000000..e0197b9
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/Messages.java
@@ -0,0 +1,43 @@
+
+package com.android.ide.eclipse.adt.preferences;
+
+import org.eclipse.osgi.util.NLS;
+
+public class Messages extends NLS {
+ private static final String BUNDLE_NAME = "com.android.ide.eclipse.adt.preferences.messages"; //$NON-NLS-1$
+
+ public static String AndroidPreferencePage_ERROR_Reserved_Char;
+
+ public static String AndroidPreferencePage_SDK_Location_;
+
+ public static String AndroidPreferencePage_Title;
+
+ public static String BuildPreferencePage_Auto_Refresh_Resources_on_Build;
+
+ public static String BuildPreferencePage_Build_Output;
+
+ public static String BuildPreferencePage_Custom_Keystore;
+
+ public static String BuildPreferencePage_Default_KeyStore;
+
+ public static String BuildPreferencePage_Normal;
+
+ public static String BuildPreferencePage_Silent;
+
+ public static String BuildPreferencePage_Title;
+
+ public static String BuildPreferencePage_Verbose;
+
+ public static String LaunchPreferencePage_Default_Emu_Options;
+
+ public static String LaunchPreferencePage_Default_HOME_Package;
+
+ public static String LaunchPreferencePage_Title;
+ static {
+ // initialize resource bundle
+ NLS.initializeMessages(BUNDLE_NAME, Messages.class);
+ }
+
+ private Messages() {
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/PreferenceInitializer.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/PreferenceInitializer.java
new file mode 100644
index 0000000..2b1fb66
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/PreferenceInitializer.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.preferences;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.jarutils.DebugKeyProvider;
+import com.android.jarutils.DebugKeyProvider.KeytoolException;
+import com.android.prefs.AndroidLocation.AndroidLocationException;
+
+import org.eclipse.core.runtime.preferences.AbstractPreferenceInitializer;
+import org.eclipse.jface.preference.IPreferenceStore;
+
+/**
+ * Class used to initialize default preference values.
+ */
+public class PreferenceInitializer extends AbstractPreferenceInitializer {
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.eclipse.core.runtime.preferences.AbstractPreferenceInitializer
+ * #initializeDefaultPreferences()
+ */
+ @Override
+ public void initializeDefaultPreferences() {
+ IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore();
+
+ store.setDefault(AdtPlugin.PREFS_RES_AUTO_REFRESH, true);
+
+ store.setDefault(AdtPlugin.PREFS_BUILD_VERBOSITY, BuildPreferencePage.BUILD_STR_NORMAL);
+
+ store.setDefault(AdtPlugin.PREFS_HOME_PACKAGE, "android.process.acore"); //$NON-NLS-1$
+
+ try {
+ store.setDefault(AdtPlugin.PREFS_DEFAULT_DEBUG_KEYSTORE,
+ DebugKeyProvider.getDefaultKeyStoreOsPath());
+ } catch (KeytoolException e) {
+ AdtPlugin.log(e, "Get default debug keystore path failed"); //$NON-NLS-1$
+ } catch (AndroidLocationException e) {
+ AdtPlugin.log(e, "Get default debug keystore path failed"); //$NON-NLS-1$
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/messages.properties b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/messages.properties
new file mode 100644
index 0000000..865ac19
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/preferences/messages.properties
@@ -0,0 +1,14 @@
+BuildPreferencePage_Title=Build Settings:
+BuildPreferencePage_Auto_Refresh_Resources_on_Build=Automatically refresh Resources and Assets folder on build
+BuildPreferencePage_Build_Output=Build output
+BuildPreferencePage_Silent=Silent
+BuildPreferencePage_Normal=Normal
+BuildPreferencePage_Verbose=Verbose
+BuildPreferencePage_Default_KeyStore=Default debug keystore:
+BuildPreferencePage_Custom_Keystore=Custom debug keystore:
+LaunchPreferencePage_Title=Launch Settings:
+LaunchPreferencePage_Default_Emu_Options=Default emulator options:
+LaunchPreferencePage_Default_HOME_Package=Default HOME package:
+AndroidPreferencePage_Title=Android Preferences
+AndroidPreferencePage_SDK_Location_=SDK Location:
+AndroidPreferencePage_ERROR_Reserved_Char=Reserved characters ',' and ';' cannot be used in the SDK Location.
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/AndroidNature.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/AndroidNature.java
new file mode 100644
index 0000000..9bcadaf
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/AndroidNature.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.project;
+
+import com.android.ide.eclipse.adt.build.ApkBuilder;
+import com.android.ide.eclipse.adt.build.PreCompilerBuilder;
+import com.android.ide.eclipse.adt.build.ResourceManagerBuilder;
+import com.android.ide.eclipse.common.AndroidConstants;
+
+import org.eclipse.core.resources.ICommand;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IProjectDescription;
+import org.eclipse.core.resources.IProjectNature;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.core.runtime.SubProgressMonitor;
+import org.eclipse.jdt.core.JavaCore;
+
+/**
+ * Project nature for the Android Projects.
+ */
+public class AndroidNature implements IProjectNature {
+
+ /** the project this nature object is associated with */
+ private IProject mProject;
+
+ /**
+ * Configures this nature for its project. This is called by the workspace
+ * when natures are added to the project using
+ * <code>IProject.setDescription</code> and should not be called directly
+ * by clients. The nature extension id is added to the list of natures
+ * before this method is called, and need not be added here.
+ *
+ * Exceptions thrown by this method will be propagated back to the caller of
+ * <code>IProject.setDescription</code>, but the nature will remain in
+ * the project description.
+ *
+ * The Android nature adds the pre-builder and the APK builder if necessary.
+ *
+ * @see org.eclipse.core.resources.IProjectNature#configure()
+ * @throws CoreException if configuration fails.
+ */
+ public void configure() throws CoreException {
+ configureResourceManagerBuilder(mProject);
+ configurePreBuilder(mProject);
+ configureApkBuilder(mProject);
+ }
+
+ /**
+ * De-configures this nature for its project. This is called by the
+ * workspace when natures are removed from the project using
+ * <code>IProject.setDescription</code> and should not be called directly
+ * by clients. The nature extension id is removed from the list of natures
+ * before this method is called, and need not be removed here.
+ *
+ * Exceptions thrown by this method will be propagated back to the caller of
+ * <code>IProject.setDescription</code>, but the nature will still be
+ * removed from the project description.
+ *
+ * The Android nature removes the custom pre builder and APK builder.
+ *
+ * @see org.eclipse.core.resources.IProjectNature#deconfigure()
+ * @throws CoreException if configuration fails.
+ */
+ public void deconfigure() throws CoreException {
+ // remove the android builders
+ removeBuilder(mProject, ResourceManagerBuilder.ID);
+ removeBuilder(mProject, PreCompilerBuilder.ID);
+ removeBuilder(mProject, ApkBuilder.ID);
+ }
+
+ /**
+ * Returns the project to which this project nature applies.
+ *
+ * @return the project handle
+ * @see org.eclipse.core.resources.IProjectNature#getProject()
+ */
+ public IProject getProject() {
+ return mProject;
+ }
+
+ /**
+ * Sets the project to which this nature applies. Used when instantiating
+ * this project nature runtime. This is called by
+ * <code>IProject.create()</code> or
+ * <code>IProject.setDescription()</code> and should not be called
+ * directly by clients.
+ *
+ * @param project the project to which this nature applies
+ * @see org.eclipse.core.resources.IProjectNature#setProject(org.eclipse.core.resources.IProject)
+ */
+ public void setProject(IProject project) {
+ mProject = project;
+ }
+
+ /**
+ * Adds the Android Nature and the Java Nature to the project if it doesn't
+ * already have them.
+ *
+ * @param project An existing or new project to update
+ * @param monitor An optional progress monitor. Can be null.
+ * @throws CoreException if fails to change the nature.
+ */
+ public static synchronized void setupProjectNatures(IProject project,
+ IProgressMonitor monitor) throws CoreException {
+ if (project == null || !project.isOpen()) return;
+ if (monitor == null) monitor = new NullProgressMonitor();
+
+ // Add the natures. We need to add the Java nature first, so it adds its builder to the
+ // project first. This way, when the android nature is added, we can control where to put
+ // the android builders in relation to the java builder.
+ // Adding the java nature after the android one, would place the java builder before the
+ // android builders.
+ addNatureToProjectDescription(project, JavaCore.NATURE_ID, monitor);
+ addNatureToProjectDescription(project, AndroidConstants.NATURE, monitor);
+ }
+
+ /**
+ * Add the specified nature to the specified project. The nature is only
+ * added if not already present.
+ * <p/>
+ * Android Natures are always inserted at the beginning of the list of natures in order to
+ * have the jdt views/dialogs display the proper icon.
+ *
+ * @param project The project to modify.
+ * @param natureId The Id of the nature to add.
+ * @param monitor An existing progress monitor.
+ * @throws CoreException if fails to change the nature.
+ */
+ private static void addNatureToProjectDescription(IProject project,
+ String natureId, IProgressMonitor monitor) throws CoreException {
+ if (!project.hasNature(natureId)) {
+
+ IProjectDescription description = project.getDescription();
+ String[] natures = description.getNatureIds();
+ String[] newNatures = new String[natures.length + 1];
+
+ // Android natures always come first.
+ if (natureId.equals(AndroidConstants.NATURE)) {
+ System.arraycopy(natures, 0, newNatures, 1, natures.length);
+ newNatures[0] = natureId;
+ } else {
+ System.arraycopy(natures, 0, newNatures, 0, natures.length);
+ newNatures[natures.length] = natureId;
+ }
+
+ description.setNatureIds(newNatures);
+ project.setDescription(description, new SubProgressMonitor(monitor, 10));
+ }
+ }
+
+ /**
+ * Adds the ResourceManagerBuilder, if its not already there. It'll insert
+ * itself as the first builder.
+ * @throws CoreException
+ *
+ */
+ public static void configureResourceManagerBuilder(IProject project)
+ throws CoreException {
+ // get the builder list
+ IProjectDescription desc = project.getDescription();
+ ICommand[] commands = desc.getBuildSpec();
+
+ // look for the builder in case it's already there.
+ for (int i = 0; i < commands.length; ++i) {
+ if (ResourceManagerBuilder.ID.equals(commands[i].getBuilderName())) {
+ return;
+ }
+ }
+
+ // it's not there, lets add it at the beginning of the builders
+ ICommand[] newCommands = new ICommand[commands.length + 1];
+ System.arraycopy(commands, 0, newCommands, 1, commands.length);
+ ICommand command = desc.newCommand();
+ command.setBuilderName(ResourceManagerBuilder.ID);
+ newCommands[0] = command;
+ desc.setBuildSpec(newCommands);
+ project.setDescription(desc, null);
+ }
+
+ /**
+ * Adds the PreCompilerBuilder if its not already there. It'll check for
+ * presence of the ResourceManager and insert itself right after.
+ * @param project
+ * @throws CoreException
+ */
+ public static void configurePreBuilder(IProject project)
+ throws CoreException {
+ // get the builder list
+ IProjectDescription desc = project.getDescription();
+ ICommand[] commands = desc.getBuildSpec();
+
+ // look for the builder in case it's already there.
+ for (int i = 0; i < commands.length; ++i) {
+ if (PreCompilerBuilder.ID.equals(commands[i].getBuilderName())) {
+ return;
+ }
+ }
+
+ // we need to add it after the resource manager builder.
+ // Let's look for it
+ int index = -1;
+ for (int i = 0; i < commands.length; ++i) {
+ if (ResourceManagerBuilder.ID.equals(commands[i].getBuilderName())) {
+ index = i;
+ break;
+ }
+ }
+
+ // we're inserting after
+ index++;
+
+ // do the insertion
+
+ // copy the builders before.
+ ICommand[] newCommands = new ICommand[commands.length + 1];
+ System.arraycopy(commands, 0, newCommands, 0, index);
+
+ // insert the new builder
+ ICommand command = desc.newCommand();
+ command.setBuilderName(PreCompilerBuilder.ID);
+ newCommands[index] = command;
+
+ // copy the builder after
+ System.arraycopy(commands, index, newCommands, index + 1, commands.length-index);
+
+ // set the new builders in the project
+ desc.setBuildSpec(newCommands);
+ project.setDescription(desc, null);
+ }
+
+ public static void configureApkBuilder(IProject project)
+ throws CoreException {
+ // Add the .apk builder at the end if it's not already there
+ IProjectDescription desc = project.getDescription();
+ ICommand[] commands = desc.getBuildSpec();
+
+ for (int i = 0; i < commands.length; ++i) {
+ if (ApkBuilder.ID.equals(commands[i].getBuilderName())) {
+ return;
+ }
+ }
+
+ ICommand[] newCommands = new ICommand[commands.length + 1];
+ System.arraycopy(commands, 0, newCommands, 0, commands.length);
+ ICommand command = desc.newCommand();
+ command.setBuilderName(ApkBuilder.ID);
+ newCommands[commands.length] = command;
+ desc.setBuildSpec(newCommands);
+ project.setDescription(desc, null);
+ }
+
+ /**
+ * Removes a builder from the project.
+ * @param project The project to remove the builder from.
+ * @param id The String ID of the builder to remove.
+ * @return true if the builder was found and removed.
+ * @throws CoreException
+ */
+ public static boolean removeBuilder(IProject project, String id) throws CoreException {
+ IProjectDescription description = project.getDescription();
+ ICommand[] commands = description.getBuildSpec();
+ for (int i = 0; i < commands.length; ++i) {
+ if (id.equals(commands[i].getBuilderName())) {
+ ICommand[] newCommands = new ICommand[commands.length - 1];
+ System.arraycopy(commands, 0, newCommands, 0, i);
+ System.arraycopy(commands, i + 1, newCommands, i, commands.length - i - 1);
+ description.setBuildSpec(newCommands);
+ project.setDescription(description, null);
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ConvertToAndroidAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ConvertToAndroidAction.java
new file mode 100644
index 0000000..ffb2535
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ConvertToAndroidAction.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.project;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.common.AndroidConstants;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IProjectDescription;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IAdaptable;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.ui.IObjectActionDelegate;
+import org.eclipse.ui.IWorkbenchPart;
+
+import java.util.Iterator;
+
+/**
+ * Converts a project created with the activity creator into an
+ * Android project.
+ */
+public class ConvertToAndroidAction implements IObjectActionDelegate {
+
+ private ISelection mSelection;
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see IObjectActionDelegate#setActivePart(IAction, IWorkbenchPart)
+ */
+ public void setActivePart(IAction action, IWorkbenchPart targetPart) {
+ // pass
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see IActionDelegate#run(IAction)
+ */
+ public void run(IAction action) {
+ if (mSelection instanceof IStructuredSelection) {
+ for (Iterator<?> it = ((IStructuredSelection)mSelection).iterator(); it.hasNext();) {
+ Object element = it.next();
+ IProject project = null;
+ if (element instanceof IProject) {
+ project = (IProject)element;
+ } else if (element instanceof IAdaptable) {
+ project = (IProject)((IAdaptable)element).getAdapter(IProject.class);
+ }
+ if (project != null) {
+ convertProject(project);
+ }
+ }
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see IActionDelegate#selectionChanged(IAction, ISelection)
+ */
+ public void selectionChanged(IAction action, ISelection selection) {
+ this.mSelection = selection;
+ }
+
+ /**
+ * Toggles sample nature on a project
+ *
+ * @param project to have sample nature added or removed
+ */
+ private void convertProject(final IProject project) {
+ new Job("Convert Project") {
+ @Override
+ protected IStatus run(IProgressMonitor monitor) {
+ try {
+ if (monitor != null) {
+ monitor.beginTask(String.format(
+ "Convert %1$s to Android", project.getName()), 5);
+ }
+
+ IProjectDescription description = project.getDescription();
+ String[] natures = description.getNatureIds();
+
+ // check if the project already has the android nature.
+ for (int i = 0; i < natures.length; ++i) {
+ if (AndroidConstants.NATURE.equals(natures[i])) {
+ // we shouldn't be here as the visibility of the item
+ // is dependent on the project.
+ return new Status(Status.WARNING, AdtPlugin.PLUGIN_ID,
+ "Project is already an Android project");
+ }
+ }
+
+ if (monitor != null) {
+ monitor.worked(1);
+ }
+
+ String[] newNatures = new String[natures.length + 1];
+ System.arraycopy(natures, 0, newNatures, 1, natures.length);
+ newNatures[0] = AndroidConstants.NATURE;
+
+ // set the new nature list in the project
+ description.setNatureIds(newNatures);
+ project.setDescription(description, null);
+ if (monitor != null) {
+ monitor.worked(1);
+ }
+
+ // Fix the classpath entries.
+ // get a java project
+ IJavaProject javaProject = JavaCore.create(project);
+ ProjectHelper.fixProjectClasspathEntries(javaProject);
+ if (monitor != null) {
+ monitor.worked(1);
+ }
+
+ return Status.OK_STATUS;
+ } catch (JavaModelException e) {
+ return e.getJavaModelStatus();
+ } catch (CoreException e) {
+ return e.getStatus();
+ } finally {
+ if (monitor != null) {
+ monitor.done();
+ }
+ }
+ }
+ }.schedule();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/CreateAidlImportAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/CreateAidlImportAction.java
new file mode 100644
index 0000000..a1b3c38
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/CreateAidlImportAction.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.project;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.common.AndroidConstants;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IAdaptable;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.jdt.core.ICompilationUnit;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IPackageFragment;
+import org.eclipse.jdt.core.IPackageFragmentRoot;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.ITypeHierarchy;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.ui.IObjectActionDelegate;
+import org.eclipse.ui.IWorkbenchPart;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+
+/**
+ * Action going through all the source of a project and creating a pre-processed aidl file
+ * with all the custom parcelable classes.
+ */
+public class CreateAidlImportAction implements IObjectActionDelegate {
+
+ private ISelection mSelection;
+
+ public CreateAidlImportAction() {
+ // pass
+ }
+
+ public void setActivePart(IAction action, IWorkbenchPart targetPart) {
+ // pass
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.eclipse.ui.IActionDelegate#run(org.eclipse.jface.action.IAction)
+ */
+ public void run(IAction action) {
+ if (mSelection instanceof IStructuredSelection) {
+ for (Iterator<?> it = ((IStructuredSelection)mSelection).iterator(); it.hasNext();) {
+ Object element = it.next();
+ IProject project = null;
+ if (element instanceof IProject) {
+ project = (IProject)element;
+ } else if (element instanceof IAdaptable) {
+ project = (IProject)((IAdaptable)element).getAdapter(IProject.class);
+ }
+ if (project != null) {
+ final IProject fproject = project;
+ new Job("Aidl preprocess") {
+ @Override
+ protected IStatus run(IProgressMonitor monitor) {
+ return createImportFile(fproject, monitor);
+ }
+ }.schedule();
+ }
+ }
+ }
+ }
+
+ public void selectionChanged(IAction action, ISelection selection) {
+ mSelection = selection;
+ }
+
+ private IStatus createImportFile(IProject project, IProgressMonitor monitor) {
+ try {
+ if (monitor != null) {
+ monitor.beginTask(String.format(
+ "Creating aid preprocess file for %1$s", project.getName()), 1);
+ }
+
+ ArrayList<String> parcelables = new ArrayList<String>();
+
+ IJavaProject javaProject = JavaCore.create(project);
+
+ IPackageFragmentRoot[] roots = javaProject.getPackageFragmentRoots();
+
+ for (IPackageFragmentRoot root : roots) {
+ if (root.isArchive() == false && root.isExternal() == false) {
+ parsePackageFragmentRoot(root, parcelables, monitor);
+ }
+ }
+
+ // create the file with the parcelables
+ if (parcelables.size() > 0) {
+ IPath path = project.getLocation();
+ path = path.append(AndroidConstants.FN_PROJECT_AIDL);
+
+ File f = new File(path.toOSString());
+ if (f.exists() == false) {
+ if (f.createNewFile() == false) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "Failed to create /project.aidl");
+ }
+ }
+
+ FileWriter fw = new FileWriter(f);
+
+ fw.write("// This file is auto-generated by the\n");
+ fw.write("// 'Create Aidl preprocess file for Parcelable classes'\n");
+ fw.write("// action. Do not modify!\n\n");
+
+ for (String parcelable : parcelables) {
+ fw.write("parcelable "); //$NON-NLS-1$
+ fw.write(parcelable);
+ fw.append(";\n"); //$NON-NLS-1$
+ }
+
+ fw.close();
+
+ // need to refresh the level just below the project to make sure it's being picked
+ // up by eclipse.
+ project.refreshLocal(IResource.DEPTH_ONE, monitor);
+ }
+
+ if (monitor != null) {
+ monitor.worked(1);
+ monitor.done();
+ }
+
+ return Status.OK_STATUS;
+ } catch (JavaModelException e) {
+ return e.getJavaModelStatus();
+ } catch (IOException e) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "Failed to create /project.aidl", e);
+ } catch (CoreException e) {
+ return e.getStatus();
+ } finally {
+ if (monitor != null) {
+ monitor.done();
+ }
+ }
+ }
+
+ private void parsePackageFragmentRoot(IPackageFragmentRoot root,
+ ArrayList<String> parcelables, IProgressMonitor monitor) throws JavaModelException {
+
+ IJavaElement[] elements = root.getChildren();
+
+ for (IJavaElement element : elements) {
+ if (element instanceof IPackageFragment) {
+ ICompilationUnit[] compilationUnits =
+ ((IPackageFragment)element).getCompilationUnits();
+
+ for (ICompilationUnit unit : compilationUnits) {
+ IType[] types = unit.getTypes();
+
+ for (IType type : types) {
+ parseType(type, parcelables, monitor);
+ }
+ }
+ }
+ }
+ }
+
+ private void parseType(IType type, ArrayList<String> parcelables, IProgressMonitor monitor)
+ throws JavaModelException {
+ // first look in this type if it somehow extends parcelable.
+ ITypeHierarchy typeHierarchy = type.newSupertypeHierarchy(monitor);
+
+ IType[] superInterfaces = typeHierarchy.getAllSuperInterfaces(type);
+ for (IType superInterface : superInterfaces) {
+ if (AndroidConstants.CLASS_PARCELABLE.equals(superInterface.getFullyQualifiedName())) {
+ parcelables.add(type.getFullyQualifiedName());
+ }
+ }
+
+ // then look in inner types.
+ IType[] innerTypes = type.getTypes();
+
+ for (IType innerType : innerTypes) {
+ parseType(innerType, parcelables, monitor);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ExportAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ExportAction.java
new file mode 100644
index 0000000..3d7e929
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ExportAction.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.project;
+
+import com.android.ide.eclipse.common.project.ExportHelper;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.IAdaptable;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.ui.IObjectActionDelegate;
+import org.eclipse.ui.IWorkbenchPart;
+
+public class ExportAction implements IObjectActionDelegate {
+
+ private ISelection mSelection;
+
+ /**
+ * @see IObjectActionDelegate#setActivePart(IAction, IWorkbenchPart)
+ */
+ public void setActivePart(IAction action, IWorkbenchPart targetPart) {
+ }
+
+ public void run(IAction action) {
+ if (mSelection instanceof IStructuredSelection) {
+ IStructuredSelection selection = (IStructuredSelection)mSelection;
+ // get the unique selected item.
+ if (selection.size() == 1) {
+ Object element = selection.getFirstElement();
+
+ // get the project object from it.
+ IProject project = null;
+ if (element instanceof IProject) {
+ project = (IProject) element;
+ } else if (element instanceof IAdaptable) {
+ project = (IProject) ((IAdaptable) element).getAdapter(IProject.class);
+ }
+
+ // and finally do the action
+ if (project != null) {
+ ExportHelper.exportProject(project);
+ }
+ }
+ }
+ }
+
+ public void selectionChanged(IAction action, ISelection selection) {
+ this.mSelection = selection;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ExportWizardAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ExportWizardAction.java
new file mode 100644
index 0000000..9ade490
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ExportWizardAction.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.project;
+
+import com.android.ide.eclipse.adt.project.export.ExportWizard;
+
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.wizard.WizardDialog;
+import org.eclipse.ui.IObjectActionDelegate;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.IWorkbenchPart;
+
+public class ExportWizardAction implements IObjectActionDelegate {
+
+ private ISelection mSelection;
+ private IWorkbench mWorkbench;
+
+ /**
+ * @see IObjectActionDelegate#setActivePart(IAction, IWorkbenchPart)
+ */
+ public void setActivePart(IAction action, IWorkbenchPart targetPart) {
+ mWorkbench = targetPart.getSite().getWorkbenchWindow().getWorkbench();
+ }
+
+ public void run(IAction action) {
+ if (mSelection instanceof IStructuredSelection) {
+ IStructuredSelection selection = (IStructuredSelection)mSelection;
+
+ // call the export wizard on the current selection.
+ ExportWizard wizard = new ExportWizard();
+ wizard.init(mWorkbench, selection);
+ WizardDialog dialog = new WizardDialog(mWorkbench.getDisplay().getActiveShell(),
+ wizard);
+ dialog.open();
+ }
+ }
+
+ public void selectionChanged(IAction action, ISelection selection) {
+ this.mSelection = selection;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FixLaunchConfig.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FixLaunchConfig.java
new file mode 100644
index 0000000..b8a0b0c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FixLaunchConfig.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.project;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.debug.launching.LaunchConfigDelegate;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.debug.core.DebugPlugin;
+import org.eclipse.debug.core.ILaunchConfiguration;
+import org.eclipse.debug.core.ILaunchConfigurationType;
+import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
+import org.eclipse.debug.core.ILaunchManager;
+import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
+
+import java.util.ArrayList;
+
+/**
+ * Class to fix the launch configuration of a project if the java package
+ * defined in the manifest has been changed.<br>
+ * This fix can be done synchronously, or asynchronously.<br>
+ * <code>start()</code> will start a thread that will do the fix.<br>
+ * <code>run()</code> will do the fix in the current thread.<br><br>
+ * By default, the fix first display a dialog to the user asking if he/she wants to
+ * do the fix. This can be overriden by calling <code>setDisplayPrompt(false)</code>.
+ *
+ */
+public class FixLaunchConfig extends Thread {
+
+ private IProject mProject;
+ private String mOldPackage;
+ private String mNewPackage;
+
+ private boolean mDisplayPrompt = true;
+
+ public FixLaunchConfig(IProject project, String oldPackage, String newPackage) {
+ super();
+
+ mProject = project;
+ mOldPackage = oldPackage;
+ mNewPackage = newPackage;
+ }
+
+ /**
+ * Set the display prompt. If true run()/start() first ask the user if he/she wants
+ * to fix the Launch Config
+ * @param displayPrompt
+ */
+ public void setDisplayPrompt(boolean displayPrompt) {
+ mDisplayPrompt = displayPrompt;
+ }
+
+ /**
+ * Fix the Launch configurations.
+ */
+ @Override
+ public void run() {
+
+ if (mDisplayPrompt) {
+ // ask the user if he really wants to fix the launch config
+ boolean res = AdtPlugin.displayPrompt(
+ "Launch Configuration Update",
+ "The package definition in the manifest changed.\nDo you want to update your Launch Configuration(s)?");
+
+ if (res == false) {
+ return;
+ }
+ }
+
+ // get the list of config for the project
+ String projectName = mProject.getName();
+ ILaunchConfiguration[] configs = findConfigs(mProject.getName());
+
+ // loop through all the config and update the package
+ for (ILaunchConfiguration config : configs) {
+ try {
+ // get the working copy so that we can make changes.
+ ILaunchConfigurationWorkingCopy copy = config.getWorkingCopy();
+
+ // get the attributes for the activity
+ String activity = config.getAttribute(LaunchConfigDelegate.ATTR_ACTIVITY,
+ ""); //$NON-NLS-1$
+
+ // manifests can define activities that are not in the defined package,
+ // so we need to make sure the activity is inside the old package.
+ if (activity.startsWith(mOldPackage)) {
+ // create the new activity
+ activity = mNewPackage + activity.substring(mOldPackage.length());
+
+ // put it in the copy
+ copy.setAttribute(LaunchConfigDelegate.ATTR_ACTIVITY, activity);
+
+ // save the config
+ copy.doSave();
+ }
+ } catch (CoreException e) {
+ // couldn't get the working copy. we output the error in the console
+ String msg = String.format("Failed to modify %1$s: %2$s", projectName,
+ e.getMessage());
+ AdtPlugin.printErrorToConsole(mProject, msg);
+ }
+ }
+
+ }
+
+ /**
+ * Looks for and returns all existing Launch Configuration object for a
+ * specified project.
+ * @param projectName The name of the project
+ * @return all the ILaunchConfiguration object. If none are present, an empty array is
+ * returned.
+ */
+ private static ILaunchConfiguration[] findConfigs(String projectName) {
+ // get the launch manager
+ ILaunchManager manager = DebugPlugin.getDefault().getLaunchManager();
+
+ // now get the config type for our particular android type.
+ ILaunchConfigurationType configType = manager.
+ getLaunchConfigurationType(LaunchConfigDelegate.ANDROID_LAUNCH_TYPE_ID);
+
+ // create a temp list to hold all the valid configs
+ ArrayList<ILaunchConfiguration> list = new ArrayList<ILaunchConfiguration>();
+
+ try {
+ ILaunchConfiguration[] configs = manager.getLaunchConfigurations(configType);
+
+ for (ILaunchConfiguration config : configs) {
+ if (config.getAttribute(
+ IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME,
+ "").equals(projectName)) { //$NON-NLS-1$
+ list.add(config);
+ }
+ }
+ } catch (CoreException e) {
+ }
+
+ return list.toArray(new ILaunchConfiguration[list.size()]);
+
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FixProjectAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FixProjectAction.java
new file mode 100644
index 0000000..3043818
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FixProjectAction.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.project;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IAdaptable;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.ui.IObjectActionDelegate;
+import org.eclipse.ui.IWorkbenchPart;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.IWorkbenchWindowActionDelegate;
+
+import java.util.Iterator;
+
+/**
+ * Action to fix the project properties:
+ * <ul>
+ * <li>Make sure the framework archive is present with the link to the java
+ * doc</li>
+ * </ul>
+ */
+public class FixProjectAction implements IObjectActionDelegate {
+
+ private ISelection mSelection;
+
+ /**
+ * @see IObjectActionDelegate#setActivePart(IAction, IWorkbenchPart)
+ */
+ public void setActivePart(IAction action, IWorkbenchPart targetPart) {
+ }
+
+ public void run(IAction action) {
+ if (mSelection instanceof IStructuredSelection) {
+
+ for (Iterator<?> it = ((IStructuredSelection) mSelection).iterator();
+ it.hasNext();) {
+ Object element = it.next();
+ IProject project = null;
+ if (element instanceof IProject) {
+ project = (IProject) element;
+ } else if (element instanceof IAdaptable) {
+ project = (IProject) ((IAdaptable) element)
+ .getAdapter(IProject.class);
+ }
+ if (project != null) {
+ fixProject(project);
+ }
+ }
+ }
+ }
+
+ public void selectionChanged(IAction action, ISelection selection) {
+ this.mSelection = selection;
+ }
+
+ private void fixProject(final IProject project) {
+ new Job("Fix Project Properties") {
+
+ @Override
+ protected IStatus run(IProgressMonitor monitor) {
+ try {
+ if (monitor != null) {
+ monitor.beginTask("Fix Project Properties", 6);
+ }
+
+ ProjectHelper.fixProject(project);
+ if (monitor != null) {
+ monitor.worked(1);
+ }
+
+ // fix the nature order to have the proper project icon
+ ProjectHelper.fixProjectNatureOrder(project);
+ if (monitor != null) {
+ monitor.worked(1);
+ }
+
+ // now we fix the builders
+ AndroidNature.configureResourceManagerBuilder(project);
+ if (monitor != null) {
+ monitor.worked(1);
+ }
+
+ AndroidNature.configurePreBuilder(project);
+ if (monitor != null) {
+ monitor.worked(1);
+ }
+
+ AndroidNature.configureApkBuilder(project);
+ if (monitor != null) {
+ monitor.worked(1);
+ }
+
+ return Status.OK_STATUS;
+ } catch (JavaModelException e) {
+ return e.getJavaModelStatus();
+ } catch (CoreException e) {
+ return e.getStatus();
+ } finally {
+ if (monitor != null) {
+ monitor.done();
+ }
+ }
+ }
+ }.schedule();
+ }
+
+ /**
+ * @see IWorkbenchWindowActionDelegate#init
+ */
+ public void init(IWorkbenchWindow window) {
+ // pass
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FolderDecorator.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FolderDecorator.java
new file mode 100644
index 0000000..7fc3318
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/FolderDecorator.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.project;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.sdklib.SdkConstants;
+
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.viewers.IDecoration;
+import org.eclipse.jface.viewers.ILabelDecorator;
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.viewers.ILightweightLabelDecorator;
+
+/**
+ * A {@link ILabelDecorator} associated with an org.eclipse.ui.decorators extension.
+ * This is used to add android icons in some special folders in the package explorer.
+ */
+public class FolderDecorator implements ILightweightLabelDecorator {
+
+ private ImageDescriptor mDescriptor;
+
+ public FolderDecorator() {
+ mDescriptor = AdtPlugin.getImageDescriptor("/icons/android_project.png");
+ }
+
+ public void decorate(Object element, IDecoration decoration) {
+ if (element instanceof IFolder) {
+ IFolder folder = (IFolder)element;
+
+ // get the project and make sure this is an android project
+ IProject project = folder.getProject();
+
+ try {
+ if (project.hasNature(AndroidConstants.NATURE)) {
+ // check the folder is directly under the project.
+ if (folder.getParent().getType() == IResource.PROJECT) {
+ String name = folder.getName();
+ if (name.equals(SdkConstants.FD_ASSETS)) {
+ decorate(decoration, " [Android assets]");
+ decoration.addOverlay(mDescriptor, IDecoration.TOP_RIGHT);
+ } else if (name.equals(SdkConstants.FD_RESOURCES)) {
+ decorate(decoration, " [Android resources]");
+ decoration.addOverlay(mDescriptor, IDecoration.TOP_RIGHT);
+ } else if (name.equals(SdkConstants.FD_NATIVE_LIBS)) {
+ decorate(decoration, " [Native Libraries]");
+ }
+ }
+ }
+ } catch (CoreException e) {
+ // log the error
+ AdtPlugin.log(e, "Unable to get nature of project '%s'.", project.getName());
+ }
+ }
+ }
+
+ public void decorate(IDecoration decoration, String suffix) {
+ decoration.addOverlay(mDescriptor, IDecoration.TOP_RIGHT);
+
+ // this is broken as it changes the color of the whole text, not only of the decoration.
+ // TODO: figure out how to change the color of the decoration only.
+// decoration.addSuffix(suffix);
+// ITheme theme = PlatformUI.getWorkbench().getThemeManager().getCurrentTheme();
+// ColorRegistry registry = theme.getColorRegistry();
+// decoration.setForegroundColor(registry.get("org.eclipse.jdt.ui.ColoredLabels.decorations"));
+
+ }
+
+ public boolean isLabelProperty(Object element, String property) {
+ // at this time return false.
+ return false;
+ }
+
+ public void addListener(ILabelProviderListener listener) {
+ // No state change will affect the rendering.
+ }
+
+
+
+ public void removeListener(ILabelProviderListener listener) {
+ // No state change will affect the rendering.
+ }
+
+ public void dispose() {
+ // nothind to dispose
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/NewXmlFileWizardAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/NewXmlFileWizardAction.java
new file mode 100644
index 0000000..c117b4e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/NewXmlFileWizardAction.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.project;
+
+import com.android.ide.eclipse.editors.wizards.NewXmlFileWizard;
+
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.wizard.WizardDialog;
+import org.eclipse.ui.IObjectActionDelegate;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.IWorkbenchPart;
+
+public class NewXmlFileWizardAction implements IObjectActionDelegate {
+
+ private ISelection mSelection;
+ private IWorkbench mWorkbench;
+
+ /**
+ * @see IObjectActionDelegate#setActivePart(IAction, IWorkbenchPart)
+ */
+ public void setActivePart(IAction action, IWorkbenchPart targetPart) {
+ mWorkbench = targetPart.getSite().getWorkbenchWindow().getWorkbench();
+ }
+
+ public void run(IAction action) {
+ if (mSelection instanceof IStructuredSelection) {
+ IStructuredSelection selection = (IStructuredSelection)mSelection;
+
+ // call the new xml file wizard on the current selection.
+ NewXmlFileWizard wizard = new NewXmlFileWizard();
+ wizard.init(mWorkbench, selection);
+ WizardDialog dialog = new WizardDialog(mWorkbench.getDisplay().getActiveShell(),
+ wizard);
+ dialog.open();
+ }
+ }
+
+ public void selectionChanged(IAction action, ISelection selection) {
+ this.mSelection = selection;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ProjectHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ProjectHelper.java
new file mode 100644
index 0000000..cbeddd7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/ProjectHelper.java
@@ -0,0 +1,668 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.project;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.project.internal.AndroidClasspathContainerInitializer;
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.common.project.AndroidManifestHelper;
+import com.android.ide.eclipse.common.project.AndroidManifestParser;
+
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IProjectDescription;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IWorkspace;
+import org.eclipse.core.resources.IncrementalProjectBuilder;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.core.runtime.QualifiedName;
+import org.eclipse.jdt.core.IClasspathEntry;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.launching.JavaRuntime;
+
+import java.util.ArrayList;
+
+/**
+ * Utility class to manipulate Project parameters/properties.
+ */
+public final class ProjectHelper {
+ public final static int COMPILER_COMPLIANCE_OK = 0;
+ public final static int COMPILER_COMPLIANCE_LEVEL = 1;
+ public final static int COMPILER_COMPLIANCE_SOURCE = 2;
+ public final static int COMPILER_COMPLIANCE_CODEGEN_TARGET = 3;
+
+ /**
+ * Adds the corresponding source folder to the class path entries.
+ *
+ * @param entries The class path entries to read. A copy will be returned.
+ * @param new_entry The parent source folder to remove.
+ * @return A new class path entries array.
+ */
+ public static IClasspathEntry[] addEntryToClasspath(
+ IClasspathEntry[] entries, IClasspathEntry new_entry) {
+ int n = entries.length;
+ IClasspathEntry[] newEntries = new IClasspathEntry[n + 1];
+ System.arraycopy(entries, 0, newEntries, 0, n);
+ newEntries[n] = new_entry;
+ return newEntries;
+ }
+
+ /**
+ * Remove a classpath entry from the array.
+ * @param entries The class path entries to read. A copy will be returned
+ * @param index The index to remove.
+ * @return A new class path entries array.
+ */
+ public static IClasspathEntry[] removeEntryFromClasspath(
+ IClasspathEntry[] entries, int index) {
+ int n = entries.length;
+ IClasspathEntry[] newEntries = new IClasspathEntry[n-1];
+
+ // copy the entries before index
+ System.arraycopy(entries, 0, newEntries, 0, index);
+
+ // copy the entries after index
+ System.arraycopy(entries, index + 1, newEntries, index,
+ entries.length - index - 1);
+
+ return newEntries;
+ }
+
+ /**
+ * Converts a OS specific path into a path valid for the java doc location
+ * attributes of a project.
+ * @param javaDocOSLocation The OS specific path.
+ * @return a valid path for the java doc location.
+ */
+ public static String getJavaDocPath(String javaDocOSLocation) {
+ // first thing we do is convert the \ into /
+ String javaDoc = javaDocOSLocation.replaceAll("\\\\", //$NON-NLS-1$
+ AndroidConstants.WS_SEP);
+
+ // then we add file: at the beginning for unix path, and file:/ for non
+ // unix path
+ if (javaDoc.startsWith(AndroidConstants.WS_SEP)) {
+ return "file:" + javaDoc; //$NON-NLS-1$
+ }
+
+ return "file:/" + javaDoc; //$NON-NLS-1$
+ }
+
+ /**
+ * Look for a specific classpath entry by full path and return its index.
+ * @param entries The entry array to search in.
+ * @param entryPath The OS specific path of the entry.
+ * @param entryKind The kind of the entry. Accepted values are 0
+ * (no filter), IClasspathEntry.CPE_LIBRARY, IClasspathEntry.CPE_PROJECT,
+ * IClasspathEntry.CPE_SOURCE, IClasspathEntry.CPE_VARIABLE,
+ * and IClasspathEntry.CPE_CONTAINER
+ * @return the index of the found classpath entry or -1.
+ */
+ public static int findClasspathEntryByPath(IClasspathEntry[] entries,
+ String entryPath, int entryKind) {
+ for (int i = 0 ; i < entries.length ; i++) {
+ IClasspathEntry entry = entries[i];
+
+ int kind = entry.getEntryKind();
+
+ if (kind == entryKind || entryKind == 0) {
+ // get the path
+ IPath path = entry.getPath();
+
+ String osPathString = path.toOSString();
+ if (osPathString.equals(entryPath)) {
+ return i;
+ }
+ }
+ }
+
+ // not found, return bad index.
+ return -1;
+ }
+
+ /**
+ * Look for a specific classpath entry for file name only and return its
+ * index.
+ * @param entries The entry array to search in.
+ * @param entryName The filename of the entry.
+ * @param entryKind The kind of the entry. Accepted values are 0
+ * (no filter), IClasspathEntry.CPE_LIBRARY, IClasspathEntry.CPE_PROJECT,
+ * IClasspathEntry.CPE_SOURCE, IClasspathEntry.CPE_VARIABLE,
+ * and IClasspathEntry.CPE_CONTAINER
+ * @param startIndex Index where to start the search
+ * @return the index of the found classpath entry or -1.
+ */
+ public static int findClasspathEntryByName(IClasspathEntry[] entries,
+ String entryName, int entryKind, int startIndex) {
+ if (startIndex < 0) {
+ startIndex = 0;
+ }
+ for (int i = startIndex ; i < entries.length ; i++) {
+ IClasspathEntry entry = entries[i];
+
+ int kind = entry.getEntryKind();
+
+ if (kind == entryKind || entryKind == 0) {
+ // get the path
+ IPath path = entry.getPath();
+ String name = path.segment(path.segmentCount()-1);
+
+ if (name.equals(entryName)) {
+ return i;
+ }
+ }
+ }
+
+ // not found, return bad index.
+ return -1;
+ }
+
+ /**
+ * Fix the project. This checks the SDK location.
+ * @param project The project to fix.
+ * @throws JavaModelException
+ */
+ public static void fixProject(IProject project) throws JavaModelException {
+ if (AdtPlugin.getOsSdkFolder().length() == 0) {
+ AdtPlugin.printToConsole(project, "Unknown SDK Location, project not fixed.");
+ return;
+ }
+
+ // get a java project
+ IJavaProject javaProject = JavaCore.create(project);
+ fixProjectClasspathEntries(javaProject);
+ }
+
+ /**
+ * Fix the project classpath entries. The method ensures that:
+ * <ul>
+ * <li>The project does not reference any old android.zip/android.jar archive.</li>
+ * <li>The project does not use its output folder as a sourc folder.</li>
+ * <li>The project does not reference a desktop JRE</li>
+ * <li>The project references the AndroidClasspathContainer.
+ * </ul>
+ * @param javaProject The project to fix.
+ * @throws JavaModelException
+ */
+ public static void fixProjectClasspathEntries(IJavaProject javaProject)
+ throws JavaModelException {
+
+ // get the project classpath
+ IClasspathEntry[] entries = javaProject.getRawClasspath();
+ IClasspathEntry[] oldEntries = entries;
+
+ // check if the JRE is set as library
+ int jreIndex = ProjectHelper.findClasspathEntryByPath(entries, JavaRuntime.JRE_CONTAINER,
+ IClasspathEntry.CPE_CONTAINER);
+ if (jreIndex != -1) {
+ // the project has a JRE included, we remove it
+ entries = ProjectHelper.removeEntryFromClasspath(entries, jreIndex);
+ }
+
+ // get the output folder
+ IPath outputFolder = javaProject.getOutputLocation();
+
+ boolean foundContainer = false;
+
+ for (int i = 0 ; i < entries.length ;) {
+ // get the entry and kind
+ IClasspathEntry entry = entries[i];
+ int kind = entry.getEntryKind();
+
+ if (kind == IClasspathEntry.CPE_SOURCE) {
+ IPath path = entry.getPath();
+
+ if (path.equals(outputFolder)) {
+ entries = ProjectHelper.removeEntryFromClasspath(entries, i);
+
+ // continue, to skip the i++;
+ continue;
+ }
+ } else if (kind == IClasspathEntry.CPE_CONTAINER) {
+ if (AndroidClasspathContainerInitializer.checkPath(entry.getPath())) {
+ foundContainer = true;
+ }
+ }
+
+ i++;
+ }
+
+ // if the framework container is not there, we add it
+ if (foundContainer == false) {
+ // add the android container to the array
+ entries = ProjectHelper.addEntryToClasspath(entries,
+ AndroidClasspathContainerInitializer.getContainerEntry());
+ }
+
+ // set the new list of entries to the project
+ if (entries != oldEntries) {
+ javaProject.setRawClasspath(entries, new NullProgressMonitor());
+ }
+
+ // If needed, check and fix compiler compliance and source compatibility
+ ProjectHelper.checkAndFixCompilerCompliance(javaProject);
+ }
+ /**
+ * Checks the project compiler compliance level is supported.
+ * @param javaProject The project to check
+ * @return <ul>
+ * <li><code>COMPILER_COMPLIANCE_OK</code> if the project is properly configured</li>
+ * <li><code>COMPILER_COMPLIANCE_LEVEL</code> for unsupported compiler level</li>
+ * <li><code>COMPILER_COMPLIANCE_SOURCE</code> for unsupported source compatibility</li>
+ * <li><code>COMPILER_COMPLIANCE_CODEGEN_TARGET</code> for unsupported .class format</li>
+ * </ul>
+ */
+ public static final int checkCompilerCompliance(IJavaProject javaProject) {
+ // get the project compliance level option
+ String compliance = javaProject.getOption(JavaCore.COMPILER_COMPLIANCE, true);
+
+ // check it against a list of valid compliance level strings.
+ if (checkCompliance(compliance) == false) {
+ // if we didn't find the proper compliance level, we return an error
+ return COMPILER_COMPLIANCE_LEVEL;
+ }
+
+ // otherwise we check source compatibility
+ String source = javaProject.getOption(JavaCore.COMPILER_SOURCE, true);
+
+ // check it against a list of valid compliance level strings.
+ if (checkCompliance(source) == false) {
+ // if we didn't find the proper compliance level, we return an error
+ return COMPILER_COMPLIANCE_SOURCE;
+ }
+
+ // otherwise check codegen level
+ String codeGen = javaProject.getOption(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM, true);
+
+ // check it against a list of valid compliance level strings.
+ if (checkCompliance(codeGen) == false) {
+ // if we didn't find the proper compliance level, we return an error
+ return COMPILER_COMPLIANCE_CODEGEN_TARGET;
+ }
+
+ return COMPILER_COMPLIANCE_OK;
+ }
+
+ /**
+ * Checks the project compiler compliance level is supported.
+ * @param project The project to check
+ * @return <ul>
+ * <li><code>COMPILER_COMPLIANCE_OK</code> if the project is properly configured</li>
+ * <li><code>COMPILER_COMPLIANCE_LEVEL</code> for unsupported compiler level</li>
+ * <li><code>COMPILER_COMPLIANCE_SOURCE</code> for unsupported source compatibility</li>
+ * <li><code>COMPILER_COMPLIANCE_CODEGEN_TARGET</code> for unsupported .class format</li>
+ * </ul>
+ */
+ public static final int checkCompilerCompliance(IProject project) {
+ // get the java project from the IProject resource object
+ IJavaProject javaProject = JavaCore.create(project);
+
+ // check and return the result.
+ return checkCompilerCompliance(javaProject);
+ }
+
+
+ /**
+ * Checks, and fixes if needed, the compiler compliance level, and the source compatibility
+ * level
+ * @param project The project to check and fix.
+ */
+ public static final void checkAndFixCompilerCompliance(IProject project) {
+ // get the java project from the IProject resource object
+ IJavaProject javaProject = JavaCore.create(project);
+
+ // Now we check the compiler compliance level and make sure it is valid
+ checkAndFixCompilerCompliance(javaProject);
+ }
+
+ /**
+ * Checks, and fixes if needed, the compiler compliance level, and the source compatibility
+ * level
+ * @param javaProject The Java project to check and fix.
+ */
+ public static final void checkAndFixCompilerCompliance(IJavaProject javaProject) {
+ if (checkCompilerCompliance(javaProject) != COMPILER_COMPLIANCE_OK) {
+ // setup the preferred compiler compliance level.
+ javaProject.setOption(JavaCore.COMPILER_COMPLIANCE,
+ AndroidConstants.COMPILER_COMPLIANCE_PREFERRED);
+ javaProject.setOption(JavaCore.COMPILER_SOURCE,
+ AndroidConstants.COMPILER_COMPLIANCE_PREFERRED);
+ javaProject.setOption(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM,
+ AndroidConstants.COMPILER_COMPLIANCE_PREFERRED);
+
+ // clean the project to make sure we recompile
+ try {
+ javaProject.getProject().build(IncrementalProjectBuilder.CLEAN_BUILD,
+ new NullProgressMonitor());
+ } catch (CoreException e) {
+ AdtPlugin.printErrorToConsole(javaProject.getProject(),
+ "Project compiler settings changed. Clean your project.");
+ }
+ }
+ }
+
+ /**
+ * Returns a {@link IProject} by its running application name, as it returned by the AVD.
+ * <p/>
+ * <var>applicationName</var> will in most case be the package declared in the manifest, but
+ * can, in some cases, be a custom process name declared in the manifest, in the
+ * <code>application</code>, <code>activity</code>, <code>receiver</code>, or
+ * <code>service</code> nodes.
+ * @param applicationName The application name.
+ * @return a project or <code>null</code> if no matching project were found.
+ */
+ public static IProject findAndroidProjectByAppName(String applicationName) {
+ // Get the list of project for the current workspace
+ IWorkspace workspace = ResourcesPlugin.getWorkspace();
+ IProject[] projects = workspace.getRoot().getProjects();
+
+ // look for a project that matches the packageName of the app
+ // we're trying to debug
+ for (IProject p : projects) {
+ if (p.isOpen()) {
+ try {
+ if (p.hasNature(AndroidConstants.NATURE) == false) {
+ // ignore non android projects
+ continue;
+ }
+ } catch (CoreException e) {
+ // failed to get the nature? skip project.
+ continue;
+ }
+
+ AndroidManifestHelper androidManifest = new AndroidManifestHelper(p);
+
+ // check that there is indeed a manifest file.
+ if (androidManifest.getManifestIFile() == null) {
+ // no file? skip this project.
+ continue;
+ }
+
+ AndroidManifestParser parser = null;
+ try {
+ parser = AndroidManifestParser.parseForData(
+ androidManifest.getManifestIFile());
+ } catch (CoreException e) {
+ // skip this project.
+ continue;
+ }
+
+ String manifestPackage = parser.getPackage();
+
+ if (manifestPackage != null && manifestPackage.equals(applicationName)) {
+ // this is the project we were looking for!
+ return p;
+ } else {
+ // if the package and application name don't match,
+ // we look for other possible process names declared in the manifest.
+ String[] processes = parser.getProcesses();
+ for (String process : processes) {
+ if (process.equals(applicationName)) {
+ return p;
+ }
+ }
+ }
+ }
+ }
+
+ return null;
+
+ }
+
+ public static void fixProjectNatureOrder(IProject project) throws CoreException {
+ IProjectDescription description = project.getDescription();
+ String[] natures = description.getNatureIds();
+
+ // if the android nature is not the first one, we reorder them
+ if (AndroidConstants.NATURE.equals(natures[0]) == false) {
+ // look for the index
+ for (int i = 0 ; i < natures.length ; i++) {
+ if (AndroidConstants.NATURE.equals(natures[i])) {
+ // if we try to just reorder the array in one pass, this doesn't do
+ // anything. I guess JDT check that we are actually adding/removing nature.
+ // So, first we'll remove the android nature, and then add it back.
+
+ // remove the android nature
+ removeNature(project, AndroidConstants.NATURE);
+
+ // now add it back at the first index.
+ description = project.getDescription();
+ natures = description.getNatureIds();
+
+ String[] newNatures = new String[natures.length + 1];
+
+ // first one is android
+ newNatures[0] = AndroidConstants.NATURE;
+
+ // next the rest that was before the android nature
+ System.arraycopy(natures, 0, newNatures, 1, natures.length);
+
+ // set the new natures
+ description.setNatureIds(newNatures);
+ project.setDescription(description, null);
+
+ // and stop
+ break;
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Removes a specific nature from a project.
+ * @param project The project to remove the nature from.
+ * @param nature The nature id to remove.
+ * @throws CoreException
+ */
+ public static void removeNature(IProject project, String nature) throws CoreException {
+ IProjectDescription description = project.getDescription();
+ String[] natures = description.getNatureIds();
+
+ // check if the project already has the android nature.
+ for (int i = 0; i < natures.length; ++i) {
+ if (nature.equals(natures[i])) {
+ String[] newNatures = new String[natures.length - 1];
+ if (i > 0) {
+ System.arraycopy(natures, 0, newNatures, 0, i);
+ }
+ System.arraycopy(natures, i + 1, newNatures, i, natures.length - i - 1);
+ description.setNatureIds(newNatures);
+ project.setDescription(description, null);
+
+ return;
+ }
+ }
+
+ }
+
+ /**
+ * Returns if the project has error level markers.
+ * @param includeReferencedProjects flag to also test the referenced projects.
+ * @throws CoreException
+ */
+ public static boolean hasError(IProject project, boolean includeReferencedProjects)
+ throws CoreException {
+ IMarker[] markers = project.findMarkers(IMarker.PROBLEM, true, IResource.DEPTH_INFINITE);
+ if (markers != null && markers.length > 0) {
+ // the project has marker(s). even though they are "problem" we
+ // don't know their severity. so we loop on them and figure if they
+ // are warnings or errors
+ for (IMarker m : markers) {
+ int s = m.getAttribute(IMarker.SEVERITY, -1);
+ if (s == IMarker.SEVERITY_ERROR) {
+ return true;
+ }
+ }
+ }
+
+ // test the referenced projects if needed.
+ if (includeReferencedProjects) {
+ IProject[] projects = getReferencedProjects(project);
+
+ for (IProject p : projects) {
+ if (hasError(p, false)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Saves a String property into the persistent storage of a resource.
+ * @param resource The resource into which the string value is saved.
+ * @param propertyName the name of the property. The id of the plugin is added to this string.
+ * @param value the value to save
+ * @return true if the save succeeded.
+ */
+ public static boolean saveStringProperty(IResource resource, String propertyName,
+ String value) {
+ QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID, propertyName);
+
+ try {
+ resource.setPersistentProperty(qname, value);
+ } catch (CoreException e) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Loads a String property from the persistent storage of a resource.
+ * @param resource The resource from which the string value is loaded.
+ * @param propertyName the name of the property. The id of the plugin is added to this string.
+ * @return the property value or null if it was not found.
+ */
+ public static String loadStringProperty(IResource resource, String propertyName) {
+ QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID, propertyName);
+
+ try {
+ String value = resource.getPersistentProperty(qname);
+ return value;
+ } catch (CoreException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Saves a property into the persistent storage of a resource.
+ * @param resource The resource into which the boolean value is saved.
+ * @param propertyName the name of the property. The id of the plugin is added to this string.
+ * @param value the value to save
+ * @return true if the save succeeded.
+ */
+ public static boolean saveBooleanProperty(IResource resource, String propertyName,
+ boolean value) {
+ return saveStringProperty(resource, propertyName, Boolean.toString(value));
+ }
+
+ /**
+ * Loads a boolean property from the persistent storage of the project.
+ * @param resource The resource from which the boolean value is loaded.
+ * @param propertyName the name of the property. The id of the plugin is added to this string.
+ * @param defaultValue The default value to return if the property was not found.
+ * @return the property value or the default value if the property was not found.
+ */
+ public static boolean loadBooleanProperty(IResource resource, String propertyName,
+ boolean defaultValue) {
+ String value = loadStringProperty(resource, propertyName);
+ if (value != null) {
+ return Boolean.parseBoolean(value);
+ }
+
+ return defaultValue;
+ }
+
+ /**
+ * Saves the path of a resource into the persistent storate of the project.
+ * @param resource The resource into which the resource path is saved.
+ * @param propertyName the name of the property. The id of the plugin is added to this string.
+ * @param value The resource to save. It's its path that is actually stored. If null, an
+ * empty string is stored.
+ * @return true if the save succeeded
+ */
+ public static boolean saveResourceProperty(IResource resource, String propertyName,
+ IResource value) {
+ if (value != null) {
+ IPath iPath = value.getProjectRelativePath();
+ return saveStringProperty(resource, propertyName, iPath.toString());
+ }
+
+ return saveStringProperty(resource, propertyName, ""); //$NON-NLS-1$
+ }
+
+ /**
+ * Loads the path of a resource from the persistent storage of the project, and returns the
+ * corresponding IResource object, if it exists in the same project as <code>resource</code>.
+ * @param resource The resource from which the resource path is loaded.
+ * @param propertyName the name of the property. The id of the plugin is added to this string.
+ * @return The corresponding IResource object (or children interface) or null
+ */
+ public static IResource loadResourceProperty(IResource resource, String propertyName) {
+ String value = loadStringProperty(resource, propertyName);
+
+ if (value != null && value.length() > 0) {
+ return resource.getProject().findMember(value);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the list of referenced project that are opened and Java projects.
+ * @param project
+ * @return list of opened referenced java project.
+ * @throws CoreException
+ */
+ public static IProject[] getReferencedProjects(IProject project) throws CoreException {
+ IProject[] projects = project.getReferencedProjects();
+
+ ArrayList<IProject> list = new ArrayList<IProject>();
+
+ for (IProject p : projects) {
+ if (p.isOpen() && p.hasNature(JavaCore.NATURE_ID)) {
+ list.add(p);
+ }
+ }
+
+ return list.toArray(new IProject[list.size()]);
+ }
+
+
+ /**
+ * Checks a Java project compiler level option against a list of supported versions.
+ * @param optionValue the Compiler level option.
+ * @return true if the option value is supproted.
+ */
+ private static boolean checkCompliance(String optionValue) {
+ for (String s : AndroidConstants.COMPILER_COMPLIANCE) {
+ if (s != null && s.equals(optionValue)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/ExportWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/ExportWizard.java
new file mode 100644
index 0000000..399eac9
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/ExportWizard.java
@@ -0,0 +1,501 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.project.export;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.project.ProjectHelper;
+import com.android.jarutils.KeystoreHelper;
+import com.android.jarutils.SignedJarBuilder;
+import com.android.jarutils.DebugKeyProvider.IKeyGenOutput;
+import com.android.jarutils.DebugKeyProvider.KeytoolException;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.IAdaptable;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.wizard.Wizard;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.swt.events.VerifyEvent;
+import org.eclipse.swt.events.VerifyListener;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.IExportWizard;
+import org.eclipse.ui.IWorkbench;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.KeyStore.PrivateKeyEntry;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Export wizard to export an apk signed with a release key/certificate.
+ */
+public final class ExportWizard extends Wizard implements IExportWizard {
+
+ private static final String PROJECT_LOGO_LARGE = "icons/android_large.png"; //$NON-NLS-1$
+
+ private static final String PAGE_PROJECT_CHECK = "Page_ProjectCheck"; //$NON-NLS-1$
+ private static final String PAGE_KEYSTORE_SELECTION = "Page_KeystoreSelection"; //$NON-NLS-1$
+ private static final String PAGE_KEY_CREATION = "Page_KeyCreation"; //$NON-NLS-1$
+ private static final String PAGE_KEY_SELECTION = "Page_KeySelection"; //$NON-NLS-1$
+ private static final String PAGE_KEY_CHECK = "Page_KeyCheck"; //$NON-NLS-1$
+
+ static final String PROPERTY_KEYSTORE = "keystore"; //$NON-NLS-1$
+ static final String PROPERTY_ALIAS = "alias"; //$NON-NLS-1$
+ static final String PROPERTY_DESTINATION = "destination"; //$NON-NLS-1$
+
+ /**
+ * Base page class for the ExportWizard page. This class add the {@link #onShow()} callback.
+ */
+ static abstract class ExportWizardPage extends WizardPage {
+
+ /** bit mask constant for project data change event */
+ protected static final int DATA_PROJECT = 0x001;
+ /** bit mask constant for keystore data change event */
+ protected static final int DATA_KEYSTORE = 0x002;
+ /** bit mask constant for key data change event */
+ protected static final int DATA_KEY = 0x004;
+
+ protected static final VerifyListener sPasswordVerifier = new VerifyListener() {
+ public void verifyText(VerifyEvent e) {
+ // verify the characters are valid for password.
+ int len = e.text.length();
+
+ // first limit to 127 characters max
+ if (len + ((Text)e.getSource()).getText().length() > 127) {
+ e.doit = false;
+ return;
+ }
+
+ // now only take non control characters
+ for (int i = 0 ; i < len ; i++) {
+ if (e.text.charAt(i) < 32) {
+ e.doit = false;
+ return;
+ }
+ }
+ }
+ };
+
+ /**
+ * Bit mask indicating what changed while the page was hidden.
+ * @see #DATA_PROJECT
+ * @see #DATA_KEYSTORE
+ * @see #DATA_KEY
+ */
+ protected int mProjectDataChanged = 0;
+
+ ExportWizardPage(String name) {
+ super(name);
+ }
+
+ abstract void onShow();
+
+ @Override
+ public void setVisible(boolean visible) {
+ super.setVisible(visible);
+ if (visible) {
+ onShow();
+ mProjectDataChanged = 0;
+ }
+ }
+
+ final void projectDataChanged(int changeMask) {
+ mProjectDataChanged |= changeMask;
+ }
+
+ /**
+ * Calls {@link #setErrorMessage(String)} and {@link #setPageComplete(boolean)} based on a
+ * {@link Throwable} object.
+ */
+ protected final void onException(Throwable t) {
+ String message = getExceptionMessage(t);
+
+ setErrorMessage(message);
+ setPageComplete(false);
+ }
+ }
+
+ private ExportWizardPage mPages[] = new ExportWizardPage[5];
+
+ private IProject mProject;
+
+ private String mKeystore;
+ private String mKeystorePassword;
+ private boolean mKeystoreCreationMode;
+
+ private String mKeyAlias;
+ private String mKeyPassword;
+ private int mValidity;
+ private String mDName;
+
+ private PrivateKey mPrivateKey;
+ private X509Certificate mCertificate;
+
+ private String mDestinationPath;
+ private String mApkFilePath;
+ private String mApkFileName;
+
+ private ExportWizardPage mKeystoreSelectionPage;
+ private ExportWizardPage mKeyCreationPage;
+ private ExportWizardPage mKeySelectionPage;
+ private ExportWizardPage mKeyCheckPage;
+
+ private boolean mKeyCreationMode;
+
+ private List<String> mExistingAliases;
+
+ public ExportWizard() {
+ setHelpAvailable(false); // TODO have help
+ setWindowTitle("Export Android Application");
+ setImageDescriptor();
+ }
+
+ @Override
+ public void addPages() {
+ addPage(mPages[0] = new ProjectCheckPage(this, PAGE_PROJECT_CHECK));
+ addPage(mKeystoreSelectionPage = mPages[1] = new KeystoreSelectionPage(this,
+ PAGE_KEYSTORE_SELECTION));
+ addPage(mKeyCreationPage = mPages[2] = new KeyCreationPage(this, PAGE_KEY_CREATION));
+ addPage(mKeySelectionPage = mPages[3] = new KeySelectionPage(this, PAGE_KEY_SELECTION));
+ addPage(mKeyCheckPage = mPages[4] = new KeyCheckPage(this, PAGE_KEY_CHECK));
+ }
+
+ @Override
+ public boolean performFinish() {
+ // first we make sure export is fine if the destination file already exists
+ File f = new File(mDestinationPath);
+ if (f.isFile()) {
+ if (AdtPlugin.displayPrompt("Export Wizard",
+ "File already exists. Do you want to overwrite it?") == false) {
+ return false;
+ }
+ }
+
+ // save the properties
+ ProjectHelper.saveStringProperty(mProject, PROPERTY_KEYSTORE, mKeystore);
+ ProjectHelper.saveStringProperty(mProject, PROPERTY_ALIAS, mKeyAlias);
+ ProjectHelper.saveStringProperty(mProject, PROPERTY_DESTINATION, mDestinationPath);
+
+ try {
+ if (mKeystoreCreationMode || mKeyCreationMode) {
+ final ArrayList<String> output = new ArrayList<String>();
+ if (KeystoreHelper.createNewStore(
+ mKeystore,
+ null /*storeType*/,
+ mKeystorePassword,
+ mKeyAlias,
+ mKeyPassword,
+ mDName,
+ mValidity,
+ new IKeyGenOutput() {
+ public void err(String message) {
+ output.add(message);
+ }
+ public void out(String message) {
+ output.add(message);
+ }
+ }) == false) {
+ // keystore creation error!
+ displayError(output.toArray(new String[output.size()]));
+ return false;
+ }
+
+ // keystore is created, now load the private key and certificate.
+ KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+ FileInputStream fis = new FileInputStream(mKeystore);
+ keyStore.load(fis, mKeystorePassword.toCharArray());
+ fis.close();
+ PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(
+ mKeyAlias, new KeyStore.PasswordProtection(mKeyPassword.toCharArray()));
+
+ if (entry != null) {
+ mPrivateKey = entry.getPrivateKey();
+ mCertificate = (X509Certificate)entry.getCertificate();
+ } else {
+ // this really shouldn't happen since we now let the user choose the key
+ // from a list read from the store.
+ displayError("Could not find key");
+ return false;
+ }
+ }
+
+ // check the private key/certificate again since it may have been created just above.
+ if (mPrivateKey != null && mCertificate != null) {
+ FileOutputStream fos = new FileOutputStream(mDestinationPath);
+ SignedJarBuilder builder = new SignedJarBuilder(fos, mPrivateKey, mCertificate);
+
+ // get the input file.
+ FileInputStream fis = new FileInputStream(mApkFilePath);
+ try {
+ builder.writeZip(fis, null /* filter */);
+ } finally {
+ fis.close();
+ }
+
+ builder.close();
+ fos.close();
+
+ return true;
+ }
+ } catch (FileNotFoundException e) {
+ displayError(e);
+ } catch (NoSuchAlgorithmException e) {
+ displayError(e);
+ } catch (IOException e) {
+ displayError(e);
+ } catch (GeneralSecurityException e) {
+ displayError(e);
+ } catch (KeytoolException e) {
+ displayError(e);
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean canFinish() {
+ // check if we have the apk to resign, the destination location, and either
+ // a private key/certificate or the creation mode. In creation mode, unless
+ // all the key/keystore info is valid, the user cannot reach the last page, so there's
+ // no need to check them again here.
+ return mApkFilePath != null &&
+ ((mPrivateKey != null && mCertificate != null)
+ || mKeystoreCreationMode || mKeyCreationMode) &&
+ mDestinationPath != null;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.eclipse.ui.IWorkbenchWizard#init(org.eclipse.ui.IWorkbench, org.eclipse.jface.viewers.IStructuredSelection)
+ */
+ public void init(IWorkbench workbench, IStructuredSelection selection) {
+ // get the project from the selection
+ Object selected = selection.getFirstElement();
+
+ if (selected instanceof IProject) {
+ mProject = (IProject)selected;
+ } else if (selected instanceof IAdaptable) {
+ IResource r = (IResource)((IAdaptable)selected).getAdapter(IResource.class);
+ if (r != null) {
+ mProject = r.getProject();
+ }
+ }
+ }
+
+ ExportWizardPage getKeystoreSelectionPage() {
+ return mKeystoreSelectionPage;
+ }
+
+ ExportWizardPage getKeyCreationPage() {
+ return mKeyCreationPage;
+ }
+
+ ExportWizardPage getKeySelectionPage() {
+ return mKeySelectionPage;
+ }
+
+ ExportWizardPage getKeyCheckPage() {
+ return mKeyCheckPage;
+ }
+
+ /**
+ * Returns an image descriptor for the wizard logo.
+ */
+ private void setImageDescriptor() {
+ ImageDescriptor desc = AdtPlugin.getImageDescriptor(PROJECT_LOGO_LARGE);
+ setDefaultPageImageDescriptor(desc);
+ }
+
+ IProject getProject() {
+ return mProject;
+ }
+
+ void setProject(IProject project, String apkFilePath, String filename) {
+ mProject = project;
+ mApkFilePath = apkFilePath;
+ mApkFileName = filename;
+
+ updatePageOnChange(ExportWizardPage.DATA_PROJECT);
+ }
+
+ String getApkFilename() {
+ return mApkFileName;
+ }
+
+ void setKeystore(String path) {
+ mKeystore = path;
+ mPrivateKey = null;
+ mCertificate = null;
+
+ updatePageOnChange(ExportWizardPage.DATA_KEYSTORE);
+ }
+
+ String getKeystore() {
+ return mKeystore;
+ }
+
+ void setKeystoreCreationMode(boolean createStore) {
+ mKeystoreCreationMode = createStore;
+ updatePageOnChange(ExportWizardPage.DATA_KEYSTORE);
+ }
+
+ boolean getKeystoreCreationMode() {
+ return mKeystoreCreationMode;
+ }
+
+
+ void setKeystorePassword(String password) {
+ mKeystorePassword = password;
+ mPrivateKey = null;
+ mCertificate = null;
+
+ updatePageOnChange(ExportWizardPage.DATA_KEYSTORE);
+ }
+
+ String getKeystorePassword() {
+ return mKeystorePassword;
+ }
+
+ void setKeyCreationMode(boolean createKey) {
+ mKeyCreationMode = createKey;
+ updatePageOnChange(ExportWizardPage.DATA_KEY);
+ }
+
+ boolean getKeyCreationMode() {
+ return mKeyCreationMode;
+ }
+
+ void setExistingAliases(List<String> aliases) {
+ mExistingAliases = aliases;
+ }
+
+ List<String> getExistingAliases() {
+ return mExistingAliases;
+ }
+
+ void setKeyAlias(String name) {
+ mKeyAlias = name;
+ mPrivateKey = null;
+ mCertificate = null;
+
+ updatePageOnChange(ExportWizardPage.DATA_KEY);
+ }
+
+ String getKeyAlias() {
+ return mKeyAlias;
+ }
+
+ void setKeyPassword(String password) {
+ mKeyPassword = password;
+ mPrivateKey = null;
+ mCertificate = null;
+
+ updatePageOnChange(ExportWizardPage.DATA_KEY);
+ }
+
+ String getKeyPassword() {
+ return mKeyPassword;
+ }
+
+ void setValidity(int validity) {
+ mValidity = validity;
+ updatePageOnChange(ExportWizardPage.DATA_KEY);
+ }
+
+ int getValidity() {
+ return mValidity;
+ }
+
+ void setDName(String dName) {
+ mDName = dName;
+ updatePageOnChange(ExportWizardPage.DATA_KEY);
+ }
+
+ String getDName() {
+ return mDName;
+ }
+
+ void setSigningInfo(PrivateKey privateKey, X509Certificate certificate) {
+ mPrivateKey = privateKey;
+ mCertificate = certificate;
+ }
+
+ void setDestination(String path) {
+ mDestinationPath = path;
+ }
+
+ void updatePageOnChange(int changeMask) {
+ for (ExportWizardPage page : mPages) {
+ page.projectDataChanged(changeMask);
+ }
+ }
+
+ private void displayError(String... messages) {
+ String message = null;
+ if (messages.length == 1) {
+ message = messages[0];
+ } else {
+ StringBuilder sb = new StringBuilder(messages[0]);
+ for (int i = 1; i < messages.length; i++) {
+ sb.append('\n');
+ sb.append(messages[i]);
+ }
+
+ message = sb.toString();
+ }
+
+ AdtPlugin.displayError("Export Wizard", message);
+ }
+
+ private void displayError(Exception e) {
+ String message = getExceptionMessage(e);
+ displayError(message);
+
+ AdtPlugin.log(e, "Export Wizard Error");
+ }
+
+ /**
+ * Returns the {@link Throwable#getMessage()}. If the {@link Throwable#getMessage()} returns
+ * <code>null</code>, the method is called again on the cause of the Throwable object.
+ * <p/>If no Throwable in the chain has a valid message, the canonical name of the first
+ * exception is returned.
+ */
+ private static String getExceptionMessage(Throwable t) {
+ String message = t.getMessage();
+ if (message == null) {
+ Throwable cause = t.getCause();
+ if (cause != null) {
+ return getExceptionMessage(cause);
+ }
+
+ // no more cause and still no message. display the first exception.
+ return t.getClass().getCanonicalName();
+ }
+
+ return message;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeyCheckPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeyCheckPage.java
new file mode 100644
index 0000000..c64bf10
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeyCheckPage.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.project.export;
+
+import com.android.ide.eclipse.adt.project.ProjectHelper;
+import com.android.ide.eclipse.adt.project.export.ExportWizard.ExportWizardPage;
+
+import org.eclipse.core.resources.IProject;
+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.FileDialog;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.forms.widgets.FormText;
+
+import java.io.File;
+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.KeyStore.PrivateKeyEntry;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.Calendar;
+
+/**
+ * Final page of the wizard that checks the key and ask for the ouput location.
+ */
+final class KeyCheckPage extends ExportWizardPage {
+
+ private final ExportWizard mWizard;
+ private PrivateKey mPrivateKey;
+ private X509Certificate mCertificate;
+ private Text mDestination;
+ private boolean mFatalSigningError;
+ private FormText mDetailText;
+
+ protected KeyCheckPage(ExportWizard wizard, String pageName) {
+ super(pageName);
+ mWizard = wizard;
+
+ setTitle("Destination and key/certificate checks");
+ setDescription(""); // TODO
+ }
+
+ public void createControl(Composite parent) {
+ setErrorMessage(null);
+ setMessage(null);
+
+ // build the ui.
+ Composite composite = new Composite(parent, SWT.NULL);
+ composite.setLayoutData(new GridData(GridData.FILL_BOTH));
+ GridLayout gl = new GridLayout(3, false);
+ gl.verticalSpacing *= 3;
+ composite.setLayout(gl);
+
+ GridData gd;
+
+ new Label(composite, SWT.NONE).setText("Destination APK file:");
+ mDestination = new Text(composite, SWT.BORDER);
+ mDestination.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ mDestination.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ onDestinationChange();
+ }
+ });
+ final Button browseButton = new Button(composite, SWT.PUSH);
+ browseButton.setText("Browse...");
+ browseButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ FileDialog fileDialog = new FileDialog(browseButton.getShell(), SWT.SAVE);
+
+ fileDialog.setText("Destination file name");
+ fileDialog.setFileName(mWizard.getApkFilename());
+
+ String saveLocation = fileDialog.open();
+ if (saveLocation != null) {
+ mDestination.setText(saveLocation);
+ }
+ }
+ });
+
+ mDetailText = new FormText(composite, SWT.NONE);
+ mDetailText.setLayoutData(gd = new GridData(GridData.FILL_BOTH));
+ gd.horizontalSpan = 3;
+
+ setControl(composite);
+ }
+
+ @Override
+ void onShow() {
+ // fill the texts with information loaded from the project.
+ if ((mProjectDataChanged & DATA_PROJECT) != 0) {
+ // reset the destination from the content of the project
+ IProject project = mWizard.getProject();
+
+ String destination = ProjectHelper.loadStringProperty(project,
+ ExportWizard.PROPERTY_DESTINATION);
+ if (destination != null) {
+ mDestination.setText(destination);
+ }
+ }
+
+ // if anything change we basically reload the data.
+ if (mProjectDataChanged != 0) {
+ mFatalSigningError = false;
+
+ // reset the wizard with no key/cert to make it not finishable, unless a valid
+ // key/cert is found.
+ mWizard.setSigningInfo(null, null);
+
+ if (mWizard.getKeystoreCreationMode() || mWizard.getKeyCreationMode()) {
+ int validity = mWizard.getValidity();
+ StringBuilder sb = new StringBuilder(
+ String.format("<form><p>Certificate expires in %d years.</p>",
+ validity));
+
+ if (validity < 25) {
+ sb.append("<p>Make sure the certificate is valid for the planned lifetime of the product.</p>");
+ sb.append("<p>If the certificate expires, you will be forced to sign your application with a different one.</p>");
+ sb.append("<p>Applications cannot be upgraded if their certificate changes from one version to another, ");
+ sb.append("forcing a full uninstall/install, which will make the user lose his/her data.</p>");
+ sb.append("<p>Android Market currently requires certificates to be valid until 2033.</p>");
+ }
+
+ sb.append("</form>");
+ mDetailText.setText(sb.toString(), true /* parseTags */, true /* expandURLs */);
+ } else {
+ try {
+ KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+ FileInputStream fis = new FileInputStream(mWizard.getKeystore());
+ keyStore.load(fis, mWizard.getKeystorePassword().toCharArray());
+ fis.close();
+ PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(
+ mWizard.getKeyAlias(),
+ new KeyStore.PasswordProtection(
+ mWizard.getKeyPassword().toCharArray()));
+
+ if (entry != null) {
+ mPrivateKey = entry.getPrivateKey();
+ mCertificate = (X509Certificate)entry.getCertificate();
+ } else {
+ setErrorMessage("Unable to find key.");
+
+ setPageComplete(false);
+ }
+ } catch (FileNotFoundException e) {
+ // this was checked at the first previous step and will not happen here, unless
+ // the file was removed during the export wizard execution.
+ onException(e);
+ } catch (KeyStoreException e) {
+ onException(e);
+ } catch (NoSuchAlgorithmException e) {
+ onException(e);
+ } catch (UnrecoverableEntryException e) {
+ onException(e);
+ } catch (CertificateException e) {
+ onException(e);
+ } catch (IOException e) {
+ onException(e);
+ }
+
+ if (mPrivateKey != null && mCertificate != null) {
+ Calendar expirationCalendar = Calendar.getInstance();
+ expirationCalendar.setTime(mCertificate.getNotAfter());
+ Calendar today = Calendar.getInstance();
+
+ if (expirationCalendar.before(today)) {
+ mDetailText.setText(String.format(
+ "<form><p>Certificate expired on %s</p></form>",
+ mCertificate.getNotAfter().toString()),
+ true /* parseTags */, true /* expandURLs */);
+
+ // fatal error = nothing can make the page complete.
+ mFatalSigningError = true;
+
+ setErrorMessage("Certificate is expired.");
+ setPageComplete(false);
+ } else {
+ // valid, key/cert: put it in the wizard so that it can be finished
+ mWizard.setSigningInfo(mPrivateKey, mCertificate);
+
+ StringBuilder sb = new StringBuilder(String.format(
+ "<form><p>Certificate expires on %s.</p>",
+ mCertificate.getNotAfter().toString()));
+
+ int expirationYear = expirationCalendar.get(Calendar.YEAR);
+ int thisYear = today.get(Calendar.YEAR);
+
+ if (thisYear + 25 < expirationYear) {
+ // do nothing
+ } else {
+ if (expirationYear == thisYear) {
+ sb.append("<p>The certificate expires this year.</p>");
+ } else {
+ int count = expirationYear-thisYear;
+ sb.append(String.format(
+ "<p>The Certificate expires in %1$s %2$s.</p>",
+ count, count == 1 ? "year" : "years"));
+ }
+
+ sb.append("<p>Make sure the certificate is valid for the planned lifetime of the product.</p>");
+ sb.append("<p>If the certificate expires, you will be forced to sign your application with a different one.</p>");
+ sb.append("<p>Applications cannot be upgraded if their certificate changes from one version to another, ");
+ sb.append("forcing a full uninstall/install, which will make the user lose his/her data.</p>");
+ sb.append("<p>Android Market currently requires certificates to be valid until 2033.</p>");
+ }
+
+ sb.append("</form>");
+
+ mDetailText.setText(sb.toString(), true /* parseTags */, true /* expandURLs */);
+ }
+ mDetailText.getParent().layout();
+ } else {
+ // fatal error = nothing can make the page complete.
+ mFatalSigningError = true;
+ }
+ }
+ }
+
+ onDestinationChange();
+ }
+
+ private void onDestinationChange() {
+ if (mFatalSigningError == false) {
+ // reset messages for now.
+ setErrorMessage(null);
+ setMessage(null);
+
+ String path = mDestination.getText().trim();
+
+ if (path.length() == 0) {
+ setErrorMessage("Enter destination for the APK file.");
+ mWizard.setDestination(null); // this is to reset canFinish in the wizard
+ setPageComplete(false);
+ return;
+ }
+
+ File file = new File(path);
+ if (file.isDirectory()) {
+ setErrorMessage("Destination is a directory.");
+ mWizard.setDestination(null); // this is to reset canFinish in the wizard
+ setPageComplete(false);
+ return;
+ }
+
+ File parentFile = file.getParentFile();
+ if (parentFile == null || parentFile.isDirectory() == false) {
+ setErrorMessage("Not a valid directory.");
+ mWizard.setDestination(null); // this is to reset canFinish in the wizard
+ setPageComplete(false);
+ return;
+ }
+
+ // no error, set the destination in the wizard.
+ mWizard.setDestination(path);
+ setPageComplete(true);
+
+ // However, we should also test if the file already exists.
+ if (file.isFile()) {
+ setMessage("Destination file already exists.", WARNING);
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeyCreationPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeyCreationPage.java
new file mode 100644
index 0000000..d7365f7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeyCreationPage.java
@@ -0,0 +1,332 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.project.export;
+
+import com.android.ide.eclipse.adt.project.ProjectHelper;
+import com.android.ide.eclipse.adt.project.export.ExportWizard.ExportWizardPage;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.jface.wizard.IWizardPage;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+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.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+import java.util.List;
+
+/**
+ * Key creation page.
+ */
+final class KeyCreationPage extends ExportWizardPage {
+
+ private final ExportWizard mWizard;
+ private Text mAlias;
+ private Text mKeyPassword;
+ private Text mKeyPassword2;
+ private Text mCnField;
+ private boolean mDisableOnChange = false;
+ private Text mOuField;
+ private Text mOField;
+ private Text mLField;
+ private Text mStField;
+ private Text mCField;
+ private String mDName;
+ private int mValidity = 0;
+ private List<String> mExistingAliases;
+
+
+ protected KeyCreationPage(ExportWizard wizard, String pageName) {
+ super(pageName);
+ mWizard = wizard;
+
+ setTitle("Key Creation");
+ setDescription(""); // TODO?
+ }
+
+ public void createControl(Composite parent) {
+ Composite composite = new Composite(parent, SWT.NULL);
+ composite.setLayoutData(new GridData(GridData.FILL_BOTH));
+ GridLayout gl = new GridLayout(2, false);
+ composite.setLayout(gl);
+
+ GridData gd;
+
+ new Label(composite, SWT.NONE).setText("Alias:");
+ mAlias = new Text(composite, SWT.BORDER);
+ mAlias.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+
+ new Label(composite, SWT.NONE).setText("Password:");
+ mKeyPassword = new Text(composite, SWT.BORDER | SWT.PASSWORD);
+ mKeyPassword.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ mKeyPassword.addVerifyListener(sPasswordVerifier);
+
+ new Label(composite, SWT.NONE).setText("Confirm:");
+ mKeyPassword2 = new Text(composite, SWT.BORDER | SWT.PASSWORD);
+ mKeyPassword2.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ mKeyPassword2.addVerifyListener(sPasswordVerifier);
+
+ new Label(composite, SWT.NONE).setText("Validity (years):");
+ final Text validityText = new Text(composite, SWT.BORDER);
+ validityText.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ validityText.addVerifyListener(new VerifyListener() {
+ public void verifyText(VerifyEvent e) {
+ // check for digit only.
+ for (int i = 0 ; i < e.text.length(); i++) {
+ char letter = e.text.charAt(i);
+ if (letter < '0' || letter > '9') {
+ e.doit = false;
+ return;
+ }
+ }
+ }
+ });
+
+ new Label(composite, SWT.SEPARATOR | SWT.HORIZONTAL).setLayoutData(
+ gd = new GridData(GridData.FILL_HORIZONTAL));
+ gd.horizontalSpan = 2;
+
+ new Label(composite, SWT.NONE).setText("First and Last Name:");
+ mCnField = new Text(composite, SWT.BORDER);
+ mCnField.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+
+ new Label(composite, SWT.NONE).setText("Organizational Unit:");
+ mOuField = new Text(composite, SWT.BORDER);
+ mOuField.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+
+ new Label(composite, SWT.NONE).setText("Organization:");
+ mOField = new Text(composite, SWT.BORDER);
+ mOField.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+
+ new Label(composite, SWT.NONE).setText("City or Locality:");
+ mLField = new Text(composite, SWT.BORDER);
+ mLField.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+
+ new Label(composite, SWT.NONE).setText("State or Province:");
+ mStField = new Text(composite, SWT.BORDER);
+ mStField.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+
+ new Label(composite, SWT.NONE).setText("Country Code (XX):");
+ mCField = new Text(composite, SWT.BORDER);
+ mCField.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+
+ // Show description the first time
+ setErrorMessage(null);
+ setMessage(null);
+ setControl(composite);
+
+ mAlias.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ mWizard.setKeyAlias(mAlias.getText().trim());
+ onChange();
+ }
+ });
+ mKeyPassword.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ mWizard.setKeyPassword(mKeyPassword.getText());
+ onChange();
+ }
+ });
+ mKeyPassword2.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ onChange();
+ }
+ });
+
+ validityText.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ try {
+ mValidity = Integer.parseInt(validityText.getText());
+ } catch (NumberFormatException e2) {
+ // this should only happen if the text field is empty due to the verifyListener.
+ mValidity = 0;
+ }
+ mWizard.setValidity(mValidity);
+ onChange();
+ }
+ });
+
+ ModifyListener dNameListener = new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ onDNameChange();
+ }
+ };
+
+ mCnField.addModifyListener(dNameListener);
+ mOuField.addModifyListener(dNameListener);
+ mOField.addModifyListener(dNameListener);
+ mLField.addModifyListener(dNameListener);
+ mStField.addModifyListener(dNameListener);
+ mCField.addModifyListener(dNameListener);
+ }
+
+ @Override
+ void onShow() {
+ // fill the texts with information loaded from the project.
+ if ((mProjectDataChanged & (DATA_PROJECT | DATA_KEYSTORE)) != 0) {
+ // reset the keystore/alias from the content of the project
+ IProject project = mWizard.getProject();
+
+ // disable onChange for now. we'll call it once at the end.
+ mDisableOnChange = true;
+
+ String alias = ProjectHelper.loadStringProperty(project, ExportWizard.PROPERTY_ALIAS);
+ if (alias != null) {
+ mAlias.setText(alias);
+ }
+
+ // get the existing list of keys if applicable
+ if (mWizard.getKeyCreationMode()) {
+ mExistingAliases = mWizard.getExistingAliases();
+ } else {
+ mExistingAliases = null;
+ }
+
+ // reset the passwords
+ mKeyPassword.setText(""); //$NON-NLS-1$
+ mKeyPassword2.setText(""); //$NON-NLS-1$
+
+ // enable onChange, and call it to display errors and enable/disable pageCompleted.
+ mDisableOnChange = false;
+ onChange();
+ }
+ }
+
+ @Override
+ public IWizardPage getPreviousPage() {
+ if (mWizard.getKeyCreationMode()) { // this means we create a key from an existing store
+ return mWizard.getKeySelectionPage();
+ }
+
+ return mWizard.getKeystoreSelectionPage();
+ }
+
+ @Override
+ public IWizardPage getNextPage() {
+ return mWizard.getKeyCheckPage();
+ }
+
+ /**
+ * Handles changes and update the error message and calls {@link #setPageComplete(boolean)}.
+ */
+ private void onChange() {
+ if (mDisableOnChange) {
+ return;
+ }
+
+ setErrorMessage(null);
+ setMessage(null);
+
+ if (mAlias.getText().trim().length() == 0) {
+ setErrorMessage("Enter key alias.");
+ setPageComplete(false);
+ return;
+ } else if (mExistingAliases != null) {
+ // we cannot use indexOf, because we need to do a case-insensitive check
+ String keyAlias = mAlias.getText().trim();
+ for (String alias : mExistingAliases) {
+ if (alias.equalsIgnoreCase(keyAlias)) {
+ setErrorMessage("Key alias already exists in keystore.");
+ setPageComplete(false);
+ return;
+ }
+ }
+ }
+
+ String value = mKeyPassword.getText();
+ if (value.length() == 0) {
+ setErrorMessage("Enter key password.");
+ setPageComplete(false);
+ return;
+ } else if (value.length() < 6) {
+ setErrorMessage("Key password is too short - must be at least 6 characters.");
+ setPageComplete(false);
+ return;
+ }
+
+ if (value.equals(mKeyPassword2.getText()) == false) {
+ setErrorMessage("Key passwords don't match.");
+ setPageComplete(false);
+ return;
+ }
+
+ if (mValidity == 0) {
+ setErrorMessage("Key certificate validity is required.");
+ setPageComplete(false);
+ return;
+ } else if (mValidity < 25) {
+ setMessage("A 25 year certificate validity is recommended.", WARNING);
+ } else if (mValidity > 1000) {
+ setErrorMessage("Key certificate validity must be between 1 and 1000 years.");
+ setPageComplete(false);
+ return;
+ }
+
+ if (mDName == null || mDName.length() == 0) {
+ setErrorMessage("At least one Certificate issuer field is required to be non-empty.");
+ setPageComplete(false);
+ return;
+ }
+
+ setPageComplete(true);
+ }
+
+ /**
+ * Handles changes in the DName fields.
+ */
+ private void onDNameChange() {
+ StringBuilder sb = new StringBuilder();
+
+ buildDName("CN", mCnField, sb);
+ buildDName("OU", mOuField, sb);
+ buildDName("O", mOField, sb);
+ buildDName("L", mLField, sb);
+ buildDName("ST", mStField, sb);
+ buildDName("C", mCField, sb);
+
+ mDName = sb.toString();
+ mWizard.setDName(mDName);
+
+ onChange();
+ }
+
+ /**
+ * Builds the distinguished name string with the provided {@link StringBuilder}.
+ * @param prefix the prefix of the entry.
+ * @param textField The {@link Text} field containing the entry value.
+ * @param sb the string builder containing the dname.
+ */
+ private void buildDName(String prefix, Text textField, StringBuilder sb) {
+ if (textField != null) {
+ String value = textField.getText().trim();
+ if (value.length() > 0) {
+ if (sb.length() > 0) {
+ sb.append(",");
+ }
+
+ sb.append(prefix);
+ sb.append('=');
+ sb.append(value);
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeySelectionPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeySelectionPage.java
new file mode 100644
index 0000000..2fcd757
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeySelectionPage.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.project.export;
+
+import com.android.ide.eclipse.adt.project.ProjectHelper;
+import com.android.ide.eclipse.adt.project.export.ExportWizard.ExportWizardPage;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.jface.wizard.IWizardPage;
+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.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+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.cert.CertificateException;
+import java.util.ArrayList;
+import java.util.Enumeration;
+
+/**
+ * Key Selection Page. This is used when an existing keystore is used.
+ */
+final class KeySelectionPage extends ExportWizardPage {
+
+ private final ExportWizard mWizard;
+ private Label mKeyAliasesLabel;
+ private Combo mKeyAliases;
+ private Label mKeyPasswordLabel;
+ private Text mKeyPassword;
+ private boolean mDisableOnChange = false;
+ private Button mUseExistingKey;
+ private Button mCreateKey;
+
+ protected KeySelectionPage(ExportWizard wizard, String pageName) {
+ super(pageName);
+ mWizard = wizard;
+
+ setTitle("Key alias selection");
+ setDescription(""); // TODO
+ }
+
+ public void createControl(Composite parent) {
+ Composite composite = new Composite(parent, SWT.NULL);
+ composite.setLayoutData(new GridData(GridData.FILL_BOTH));
+ GridLayout gl = new GridLayout(3, false);
+ composite.setLayout(gl);
+
+ GridData gd;
+
+ mUseExistingKey = new Button(composite, SWT.RADIO);
+ mUseExistingKey.setText("Use existing key");
+ mUseExistingKey.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ gd.horizontalSpan = 3;
+ mUseExistingKey.setSelection(true);
+
+ new Composite(composite, SWT.NONE).setLayoutData(gd = new GridData());
+ gd.heightHint = 0;
+ gd.widthHint = 50;
+ mKeyAliasesLabel = new Label(composite, SWT.NONE);
+ mKeyAliasesLabel.setText("Alias:");
+ mKeyAliases = new Combo(composite, SWT.READ_ONLY);
+ mKeyAliases.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ new Composite(composite, SWT.NONE).setLayoutData(gd = new GridData());
+ gd.heightHint = 0;
+ gd.widthHint = 50;
+ mKeyPasswordLabel = new Label(composite, SWT.NONE);
+ mKeyPasswordLabel.setText("Password:");
+ mKeyPassword = new Text(composite, SWT.BORDER | SWT.PASSWORD);
+ mKeyPassword.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ mCreateKey = new Button(composite, SWT.RADIO);
+ mCreateKey.setText("Create new key");
+ mCreateKey.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ gd.horizontalSpan = 3;
+
+ // Show description the first time
+ setErrorMessage(null);
+ setMessage(null);
+ setControl(composite);
+
+ mUseExistingKey.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mWizard.setKeyCreationMode(!mUseExistingKey.getSelection());
+ enableWidgets();
+ onChange();
+ }
+ });
+
+ mKeyAliases.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mWizard.setKeyAlias(mKeyAliases.getItem(mKeyAliases.getSelectionIndex()));
+ onChange();
+ }
+ });
+
+ mKeyPassword.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ mWizard.setKeyPassword(mKeyPassword.getText());
+ onChange();
+ }
+ });
+ }
+
+ @Override
+ void onShow() {
+ // fill the texts with information loaded from the project.
+ if ((mProjectDataChanged & (DATA_PROJECT | DATA_KEYSTORE)) != 0) {
+ // disable onChange for now. we'll call it once at the end.
+ mDisableOnChange = true;
+
+ // reset the alias from the content of the project
+ try {
+ // reset to using a key
+ mWizard.setKeyCreationMode(false);
+ mUseExistingKey.setSelection(true);
+ mCreateKey.setSelection(false);
+ enableWidgets();
+
+ // remove the content of the alias combo always and first, in case the
+ // keystore password is wrong
+ mKeyAliases.removeAll();
+
+ // get the alias list (also used as a keystore password test)
+ KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+ FileInputStream fis = new FileInputStream(mWizard.getKeystore());
+ keyStore.load(fis, mWizard.getKeystorePassword().toCharArray());
+ fis.close();
+
+ Enumeration<String> aliases = keyStore.aliases();
+
+ // get the alias from the project previous export, and look for a match as
+ // we add the aliases to the combo.
+ IProject project = mWizard.getProject();
+
+ String keyAlias = ProjectHelper.loadStringProperty(project,
+ ExportWizard.PROPERTY_ALIAS);
+
+ ArrayList<String> aliasList = new ArrayList<String>();
+
+ int selection = -1;
+ int count = 0;
+ while (aliases.hasMoreElements()) {
+ String alias = aliases.nextElement();
+ mKeyAliases.add(alias);
+ aliasList.add(alias);
+ if (selection == -1 && alias.equalsIgnoreCase(keyAlias)) {
+ selection = count;
+ }
+ count++;
+ }
+
+ mWizard.setExistingAliases(aliasList);
+
+ if (selection != -1) {
+ mKeyAliases.select(selection);
+
+ // since a match was found and is selected, we need to give it to
+ // the wizard as well
+ mWizard.setKeyAlias(keyAlias);
+ } else {
+ mKeyAliases.clearSelection();
+ }
+
+ // reset the password
+ mKeyPassword.setText(""); //$NON-NLS-1$
+
+ // enable onChange, and call it to display errors and enable/disable pageCompleted.
+ mDisableOnChange = false;
+ onChange();
+ } catch (KeyStoreException e) {
+ onException(e);
+ } catch (FileNotFoundException e) {
+ onException(e);
+ } catch (NoSuchAlgorithmException e) {
+ onException(e);
+ } catch (CertificateException e) {
+ onException(e);
+ } catch (IOException e) {
+ onException(e);
+ } finally {
+ // in case we exit with an exception, we need to reset this
+ mDisableOnChange = false;
+ }
+ }
+ }
+
+ @Override
+ public IWizardPage getPreviousPage() {
+ return mWizard.getKeystoreSelectionPage();
+ }
+
+ @Override
+ public IWizardPage getNextPage() {
+ if (mWizard.getKeyCreationMode()) {
+ return mWizard.getKeyCreationPage();
+ }
+
+ return mWizard.getKeyCheckPage();
+ }
+
+ /**
+ * Handles changes and update the error message and calls {@link #setPageComplete(boolean)}.
+ */
+ private void onChange() {
+ if (mDisableOnChange) {
+ return;
+ }
+
+ setErrorMessage(null);
+ setMessage(null);
+
+ if (mWizard.getKeyCreationMode() == false) {
+ if (mKeyAliases.getSelectionIndex() == -1) {
+ setErrorMessage("Select a key alias.");
+ setPageComplete(false);
+ return;
+ }
+
+ if (mKeyPassword.getText().trim().length() == 0) {
+ setErrorMessage("Enter key password.");
+ setPageComplete(false);
+ return;
+ }
+ }
+
+ setPageComplete(true);
+ }
+
+ private void enableWidgets() {
+ boolean useKey = !mWizard.getKeyCreationMode();
+ mKeyAliasesLabel.setEnabled(useKey);
+ mKeyAliases.setEnabled(useKey);
+ mKeyPassword.setEnabled(useKey);
+ mKeyPasswordLabel.setEnabled(useKey);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeystoreSelectionPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeystoreSelectionPage.java
new file mode 100644
index 0000000..c5a4d47
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/KeystoreSelectionPage.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.project.export;
+
+import com.android.ide.eclipse.adt.project.ProjectHelper;
+import com.android.ide.eclipse.adt.project.export.ExportWizard.ExportWizardPage;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.jface.wizard.IWizardPage;
+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.FileDialog;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+import java.io.File;
+
+/**
+ * Keystore selection page. This page allows to choose to create a new keystore or use an
+ * existing one.
+ */
+final class KeystoreSelectionPage extends ExportWizardPage {
+
+ private final ExportWizard mWizard;
+ private Button mUseExistingKeystore;
+ private Button mCreateKeystore;
+ private Text mKeystore;
+ private Text mKeystorePassword;
+ private Label mConfirmLabel;
+ private Text mKeystorePassword2;
+ private boolean mDisableOnChange = false;
+
+ protected KeystoreSelectionPage(ExportWizard wizard, String pageName) {
+ super(pageName);
+ mWizard = wizard;
+
+ setTitle("Keystore selection");
+ setDescription(""); //TODO
+ }
+
+ public void createControl(Composite parent) {
+ Composite composite = new Composite(parent, SWT.NULL);
+ composite.setLayoutData(new GridData(GridData.FILL_BOTH));
+ GridLayout gl = new GridLayout(3, false);
+ composite.setLayout(gl);
+
+ GridData gd;
+
+ mUseExistingKeystore = new Button(composite, SWT.RADIO);
+ mUseExistingKeystore.setText("Use existing keystore");
+ mUseExistingKeystore.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ gd.horizontalSpan = 3;
+ mUseExistingKeystore.setSelection(true);
+
+ mCreateKeystore = new Button(composite, SWT.RADIO);
+ mCreateKeystore.setText("Create new keystore");
+ mCreateKeystore.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ gd.horizontalSpan = 3;
+
+ new Label(composite, SWT.NONE).setText("Location:");
+ mKeystore = new Text(composite, SWT.BORDER);
+ mKeystore.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ final Button browseButton = new Button(composite, SWT.PUSH);
+ browseButton.setText("Browse...");
+ browseButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ FileDialog fileDialog;
+ if (mUseExistingKeystore.getSelection()) {
+ fileDialog = new FileDialog(browseButton.getShell(),SWT.OPEN);
+ fileDialog.setText("Load Keystore");
+ } else {
+ fileDialog = new FileDialog(browseButton.getShell(),SWT.SAVE);
+ fileDialog.setText("Select Keystore Name");
+ }
+
+ String fileName = fileDialog.open();
+ if (fileName != null) {
+ mKeystore.setText(fileName);
+ }
+ }
+ });
+
+ new Label(composite, SWT.NONE).setText("Password:");
+ mKeystorePassword = new Text(composite, SWT.BORDER | SWT.PASSWORD);
+ mKeystorePassword.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ mKeystorePassword.addVerifyListener(sPasswordVerifier);
+ new Composite(composite, SWT.NONE).setLayoutData(gd = new GridData());
+ gd.heightHint = gd.widthHint = 0;
+
+ mConfirmLabel = new Label(composite, SWT.NONE);
+ mConfirmLabel.setText("Confirm:");
+ mKeystorePassword2 = new Text(composite, SWT.BORDER | SWT.PASSWORD);
+ mKeystorePassword2.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ mKeystorePassword2.addVerifyListener(sPasswordVerifier);
+ new Composite(composite, SWT.NONE).setLayoutData(gd = new GridData());
+ gd.heightHint = gd.widthHint = 0;
+ mKeystorePassword2.setEnabled(false);
+
+ // Show description the first time
+ setErrorMessage(null);
+ setMessage(null);
+ setControl(composite);
+
+ mUseExistingKeystore.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ boolean createStore = !mUseExistingKeystore.getSelection();
+ mKeystorePassword2.setEnabled(createStore);
+ mConfirmLabel.setEnabled(createStore);
+ mWizard.setKeystoreCreationMode(createStore);
+ onChange();
+ }
+ });
+
+ mKeystore.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ mWizard.setKeystore(mKeystore.getText().trim());
+ onChange();
+ }
+ });
+
+ mKeystorePassword.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ mWizard.setKeystorePassword(mKeystorePassword.getText());
+ onChange();
+ }
+ });
+
+ mKeystorePassword2.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ onChange();
+ }
+ });
+ }
+
+ @Override
+ public IWizardPage getNextPage() {
+ if (mUseExistingKeystore.getSelection()) {
+ return mWizard.getKeySelectionPage();
+ }
+
+ return mWizard.getKeyCreationPage();
+ }
+
+ @Override
+ void onShow() {
+ // fill the texts with information loaded from the project.
+ if ((mProjectDataChanged & DATA_PROJECT) != 0) {
+ // reset the keystore/alias from the content of the project
+ IProject project = mWizard.getProject();
+
+ // disable onChange for now. we'll call it once at the end.
+ mDisableOnChange = true;
+
+ String keystore = ProjectHelper.loadStringProperty(project,
+ ExportWizard.PROPERTY_KEYSTORE);
+ if (keystore != null) {
+ mKeystore.setText(keystore);
+ }
+
+ // reset the passwords
+ mKeystorePassword.setText(""); //$NON-NLS-1$
+ mKeystorePassword2.setText(""); //$NON-NLS-1$
+
+ // enable onChange, and call it to display errors and enable/disable pageCompleted.
+ mDisableOnChange = false;
+ onChange();
+ }
+ }
+
+ /**
+ * Handles changes and update the error message and calls {@link #setPageComplete(boolean)}.
+ */
+ private void onChange() {
+ if (mDisableOnChange) {
+ return;
+ }
+
+ setErrorMessage(null);
+ setMessage(null);
+
+ boolean createStore = !mUseExistingKeystore.getSelection();
+
+ // checks the keystore path is non null.
+ String keystore = mKeystore.getText().trim();
+ if (keystore.length() == 0) {
+ setErrorMessage("Enter path to keystore.");
+ setPageComplete(false);
+ return;
+ } else {
+ File f = new File(keystore);
+ if (f.exists() == false) {
+ if (createStore == false) {
+ setErrorMessage("Keystore does not exist.");
+ setPageComplete(false);
+ return;
+ }
+ } else if (f.isDirectory()) {
+ setErrorMessage("Keystore path is a directory.");
+ setPageComplete(false);
+ return;
+ } else if (f.isFile()) {
+ if (createStore) {
+ setErrorMessage("File already exists.");
+ setPageComplete(false);
+ return;
+ }
+ }
+ }
+
+ String value = mKeystorePassword.getText();
+ if (value.length() == 0) {
+ setErrorMessage("Enter keystore password.");
+ setPageComplete(false);
+ return;
+ } else if (createStore && value.length() < 6) {
+ setErrorMessage("Keystore password is too short - must be at least 6 characters.");
+ setPageComplete(false);
+ return;
+ }
+
+ if (createStore) {
+ if (mKeystorePassword2.getText().length() == 0) {
+ setErrorMessage("Confirm keystore password.");
+ setPageComplete(false);
+ return;
+ }
+
+ if (mKeystorePassword.getText().equals(mKeystorePassword2.getText()) == false) {
+ setErrorMessage("Keystore passwords do not match.");
+ setPageComplete(false);
+ return;
+ }
+ }
+
+ setPageComplete(true);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/ProjectCheckPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/ProjectCheckPage.java
new file mode 100644
index 0000000..e161e18
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/export/ProjectCheckPage.java
@@ -0,0 +1,322 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.project.export;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.project.ProjectHelper;
+import com.android.ide.eclipse.adt.project.export.ExportWizard.ExportWizardPage;
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.common.project.AndroidManifestParser;
+import com.android.ide.eclipse.common.project.BaseProjectHelper;
+import com.android.ide.eclipse.common.project.ProjectChooserHelper;
+
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.jdt.core.IJavaProject;
+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.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.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+import java.io.File;
+
+/**
+ * First Export Wizard Page. Display warning/errors.
+ */
+final class ProjectCheckPage extends ExportWizardPage {
+ private final static String IMG_ERROR = "error.png"; //$NON-NLS-1$
+ private final static String IMG_WARNING = "warning.png"; //$NON-NLS-1$
+
+ private final ExportWizard mWizard;
+ private Display mDisplay;
+ private Image mError;
+ private Image mWarning;
+ private boolean mHasMessage = false;
+ private Composite mTopComposite;
+ private Composite mErrorComposite;
+ private Text mProjectText;
+ private ProjectChooserHelper mProjectChooserHelper;
+ private boolean mFirstOnShow = true;
+
+ protected ProjectCheckPage(ExportWizard wizard, String pageName) {
+ super(pageName);
+ mWizard = wizard;
+
+ setTitle("Project Checks");
+ setDescription("Performs a set of checks to make sure the application can be exported.");
+ }
+
+ public void createControl(Composite parent) {
+ mProjectChooserHelper = new ProjectChooserHelper(parent.getShell());
+ mDisplay = parent.getDisplay();
+
+ GridLayout gl = null;
+ GridData gd = null;
+
+ mTopComposite = new Composite(parent, SWT.NONE);
+ mTopComposite.setLayoutData(new GridData(GridData.FILL_BOTH));
+ mTopComposite.setLayout(new GridLayout(1, false));
+
+ // composite for the project selection.
+ Composite projectComposite = new Composite(mTopComposite, SWT.NONE);
+ projectComposite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ projectComposite.setLayout(gl = new GridLayout(3, false));
+ gl.marginHeight = gl.marginWidth = 0;
+
+ Label label = new Label(projectComposite, SWT.NONE);
+ label.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ gd.horizontalSpan = 3;
+ label.setText("Select the project to export:");
+
+ new Label(projectComposite, SWT.NONE).setText("Project:");
+ mProjectText = new Text(projectComposite, SWT.BORDER);
+ mProjectText.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ mProjectText.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ handleProjectNameChange();
+ }
+ });
+
+ Button browseButton = new Button(projectComposite, SWT.PUSH);
+ browseButton.setText("Browse...");
+ browseButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ IJavaProject javaProject = mProjectChooserHelper.chooseJavaProject(
+ mProjectText.getText().trim());
+
+ if (javaProject != null) {
+ IProject project = javaProject.getProject();
+
+ // set the new name in the text field. The modify listener will take
+ // care of updating the status and the ExportWizard object.
+ mProjectText.setText(project.getName());
+ }
+ }
+ });
+
+ setControl(mTopComposite);
+ }
+
+ @Override
+ void onShow() {
+ if (mFirstOnShow) {
+ // get the project and init the ui
+ IProject project = mWizard.getProject();
+ if (project != null) {
+ mProjectText.setText(project.getName());
+ }
+
+ mFirstOnShow = false;
+ }
+ }
+
+ private void buildErrorUi(IProject project) {
+ // Show description the first time
+ setErrorMessage(null);
+ setMessage(null);
+ setPageComplete(true);
+ mHasMessage = false;
+
+ // composite parent for the warning/error
+ GridLayout gl = null;
+ mErrorComposite = new Composite(mTopComposite, SWT.NONE);
+ mErrorComposite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ gl = new GridLayout(2, false);
+ gl.marginHeight = gl.marginWidth = 0;
+ gl.verticalSpacing *= 3; // more spacing than normal.
+ mErrorComposite.setLayout(gl);
+
+ if (project == null) {
+ setErrorMessage("Select project to export.");
+ mHasMessage = true;
+ } else {
+ try {
+ if (project.hasNature(AndroidConstants.NATURE) == false) {
+ addError(mErrorComposite, "Project is not an Android project.");
+ } else {
+ // check for errors
+ if (ProjectHelper.hasError(project, true)) {
+ addError(mErrorComposite, "Project has compilation error(s)");
+ }
+
+ // check the project output
+ IFolder outputIFolder = BaseProjectHelper.getOutputFolder(project);
+ if (outputIFolder != null) {
+ String outputOsPath = outputIFolder.getLocation().toOSString();
+ String apkFilePath = outputOsPath + File.separator + project.getName() +
+ AndroidConstants.DOT_ANDROID_PACKAGE;
+
+ File f = new File(apkFilePath);
+ if (f.isFile() == false) {
+ addError(mErrorComposite,
+ String.format("%1$s/%2$s/%1$s%3$s does not exists!",
+ project.getName(),
+ outputIFolder.getName(),
+ AndroidConstants.DOT_ANDROID_PACKAGE));
+ }
+ } else {
+ addError(mErrorComposite,
+ "Unable to get the output folder of the project!");
+ }
+
+
+ // project is an android project, we check the debuggable attribute.
+ AndroidManifestParser manifestParser = AndroidManifestParser.parse(
+ BaseProjectHelper.getJavaProject(project), null /* errorListener */,
+ true /* gatherData */, false /* markErrors */);
+
+ Boolean debuggable = manifestParser.getDebuggable();
+
+ if (debuggable != null && debuggable == Boolean.TRUE) {
+ addWarning(mErrorComposite,
+ "The manifest 'debuggable' attribute is set to true.\nYou should set it to false for applications that you release to the public.");
+ }
+
+ // check for mapview stuff
+ }
+ } catch (CoreException e) {
+ // unable to access nature
+ addError(mErrorComposite, "Unable to get project nature");
+ }
+ }
+
+ if (mHasMessage == false) {
+ Label label = new Label(mErrorComposite, SWT.NONE);
+ GridData gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalSpan = 2;
+ label.setLayoutData(gd);
+ label.setText("No errors found. Click Next.");
+ }
+
+ mTopComposite.layout();
+ }
+
+ /**
+ * Adds an error label to a {@link Composite} object.
+ * @param parent the Composite parent.
+ * @param message the error message.
+ */
+ private void addError(Composite parent, String message) {
+ if (mError == null) {
+ mError = AdtPlugin.getImageLoader().loadImage(IMG_ERROR, mDisplay);
+ }
+
+ new Label(parent, SWT.NONE).setImage(mError);
+ Label label = new Label(parent, SWT.NONE);
+ label.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ label.setText(message);
+
+ setErrorMessage("Application cannot be exported due to the error(s) below.");
+ setPageComplete(false);
+ mHasMessage = true;
+ }
+
+ /**
+ * Adds a warning label to a {@link Composite} object.
+ * @param parent the Composite parent.
+ * @param message the warning message.
+ */
+ private void addWarning(Composite parent, String message) {
+ if (mWarning == null) {
+ mWarning = AdtPlugin.getImageLoader().loadImage(IMG_WARNING, mDisplay);
+ }
+
+ new Label(parent, SWT.NONE).setImage(mWarning);
+ Label label = new Label(parent, SWT.NONE);
+ label.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ label.setText(message);
+
+ mHasMessage = true;
+ }
+
+ /**
+ * Checks the parameters for correctness, and update the error message and buttons.
+ */
+ private void handleProjectNameChange() {
+ setPageComplete(false);
+
+ if (mErrorComposite != null) {
+ mErrorComposite.dispose();
+ mErrorComposite = null;
+ }
+
+ // update the wizard with the new project
+ mWizard.setProject(null, null, null);
+
+ //test the project name first!
+ String text = mProjectText.getText().trim();
+ if (text.length() == 0) {
+ setErrorMessage("Select project to export.");
+ } else if (text.matches("[a-zA-Z0-9_ \\.-]+") == false) {
+ setErrorMessage("Project name contains unsupported characters!");
+ } else {
+ IJavaProject[] projects = mProjectChooserHelper.getAndroidProjects(null);
+ IProject found = null;
+ for (IJavaProject javaProject : projects) {
+ if (javaProject.getProject().getName().equals(text)) {
+ found = javaProject.getProject();
+ break;
+ }
+
+ }
+
+ if (found != null) {
+ setErrorMessage(null);
+
+ // update the wizard with the new project
+ setApkFilePathInWizard(found);
+
+ // now rebuild the error ui.
+ buildErrorUi(found);
+ } else {
+ setErrorMessage(String.format("There is no android project named '%1$s'",
+ text));
+ }
+ }
+ }
+
+ private void setApkFilePathInWizard(IProject project) {
+ if (project != null) {
+ IFolder outputIFolder = BaseProjectHelper.getOutputFolder(project);
+ if (outputIFolder != null) {
+ String outputOsPath = outputIFolder.getLocation().toOSString();
+ String apkFilePath = outputOsPath + File.separator + project.getName() +
+ AndroidConstants.DOT_ANDROID_PACKAGE;
+
+ File f = new File(apkFilePath);
+ if (f.isFile()) {
+ mWizard.setProject(project, apkFilePath, f.getName());
+ return;
+ }
+ }
+ }
+
+ mWizard.setProject(null, null, null);
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/internal/AndroidClasspathContainer.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/internal/AndroidClasspathContainer.java
new file mode 100644
index 0000000..c7cb427
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/internal/AndroidClasspathContainer.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.project.internal;
+
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.jdt.core.IClasspathContainer;
+import org.eclipse.jdt.core.IClasspathEntry;
+
+/**
+ * Classpath container for the Android projects.
+ */
+class AndroidClasspathContainer implements IClasspathContainer {
+
+ private IClasspathEntry[] mClasspathEntry;
+ private IPath mContainerPath;
+ private String mName;
+
+ /**
+ * Constructs the container with the {@link IClasspathEntry} representing the android
+ * framework jar file and the container id
+ * @param entries the entries representing the android framework and optional libraries.
+ * @param path the path containing the classpath container id.
+ * @param name the name of the container to display.
+ */
+ AndroidClasspathContainer(IClasspathEntry[] entries, IPath path, String name) {
+ mClasspathEntry = entries;
+ mContainerPath = path;
+ mName = name;
+ }
+
+ public IClasspathEntry[] getClasspathEntries() {
+ return mClasspathEntry;
+ }
+
+ public String getDescription() {
+ return mName;
+ }
+
+ public int getKind() {
+ return IClasspathContainer.K_DEFAULT_SYSTEM;
+ }
+
+ public IPath getPath() {
+ return mContainerPath;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/internal/AndroidClasspathContainerInitializer.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/internal/AndroidClasspathContainerInitializer.java
new file mode 100644
index 0000000..d686830
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/internal/AndroidClasspathContainerInitializer.java
@@ -0,0 +1,627 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.project.internal;
+
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.project.ProjectHelper;
+import com.android.ide.eclipse.adt.sdk.LoadStatus;
+import com.android.ide.eclipse.adt.sdk.Sdk;
+import com.android.ide.eclipse.common.project.BaseProjectHelper;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.IAndroidTarget.IOptionalLibrary;
+
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.jdt.core.ClasspathContainerInitializer;
+import org.eclipse.jdt.core.IAccessRule;
+import org.eclipse.jdt.core.IClasspathAttribute;
+import org.eclipse.jdt.core.IClasspathContainer;
+import org.eclipse.jdt.core.IClasspathEntry;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+
+import java.io.File;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.regex.Pattern;
+
+/**
+ * Classpath container initializer responsible for binding {@link AndroidClasspathContainer} to
+ * {@link IProject}s. This removes the hard-coded path to the android.jar.
+ */
+public class AndroidClasspathContainerInitializer extends ClasspathContainerInitializer {
+ /** The container id for the android framework jar file */
+ private final static String CONTAINER_ID =
+ "com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"; //$NON-NLS-1$
+
+ /** path separator to store multiple paths in a single property. This is guaranteed to not
+ * be in a path.
+ */
+ private final static String PATH_SEPARATOR = "\u001C"; //$NON-NLS-1$
+
+ private final static String PROPERTY_CONTAINER_CACHE = "androidContainerCache"; //$NON-NLS-1$
+ private final static String PROPERTY_TARGET_NAME = "androidTargetCache"; //$NON-NLS-1$
+ private final static String CACHE_VERSION = "01"; //$NON-NLS-1$
+ private final static String CACHE_VERSION_SEP = CACHE_VERSION + PATH_SEPARATOR;
+
+ private final static int PATH_ANDROID_JAR = 0;
+ private final static int PATH_ANDROID_SRC = 1;
+ private final static int PATH_ANDROID_DOCS = 2;
+ private final static int PATH_ANDROID_OPT_DOCS = 3;
+
+ public AndroidClasspathContainerInitializer() {
+ // pass
+ }
+
+ /**
+ * Binds a classpath container to a {@link IClasspathContainer} for a given project,
+ * or silently fails if unable to do so.
+ * @param containerPath the container path that is the container id.
+ * @param project the project to bind
+ */
+ @Override
+ public void initialize(IPath containerPath, IJavaProject project) throws CoreException {
+ if (CONTAINER_ID.equals(containerPath.toString())) {
+ JavaCore.setClasspathContainer(new Path(CONTAINER_ID),
+ new IJavaProject[] { project },
+ new IClasspathContainer[] { allocateAndroidContainer(project) },
+ new NullProgressMonitor());
+ }
+ }
+
+ /**
+ * Creates a new {@link IClasspathEntry} of type {@link IClasspathEntry#CPE_CONTAINER}
+ * linking to the Android Framework.
+ */
+ public static IClasspathEntry getContainerEntry() {
+ return JavaCore.newContainerEntry(new Path(CONTAINER_ID));
+ }
+
+ /**
+ * Checks the {@link IPath} objects against the android framework container id and
+ * returns <code>true</code> if they are identical.
+ * @param path the <code>IPath</code> to check.
+ */
+ public static boolean checkPath(IPath path) {
+ return CONTAINER_ID.equals(path.toString());
+ }
+
+ /**
+ * Updates the {@link IJavaProject} objects with new android framework container. This forces
+ * JDT to recompile them.
+ * @param androidProjects the projects to update.
+ * @return <code>true</code> if success, <code>false</code> otherwise.
+ */
+ public static boolean updateProjects(IJavaProject[] androidProjects) {
+ try {
+ // Allocate a new AndroidClasspathContainer, and associate it to the android framework
+ // container id for each projects.
+ // By providing a new association between a container id and a IClasspathContainer,
+ // this forces the JDT to query the IClasspathContainer for new IClasspathEntry (with
+ // IClasspathContainer#getClasspathEntries()), and therefore force recompilation of
+ // the projects.
+ int projectCount = androidProjects.length;
+
+ IClasspathContainer[] containers = new IClasspathContainer[projectCount];
+ for (int i = 0 ; i < projectCount; i++) {
+ containers[i] = allocateAndroidContainer(androidProjects[i]);
+ }
+
+ // give each project their new container in one call.
+ JavaCore.setClasspathContainer(
+ new Path(CONTAINER_ID),
+ androidProjects, containers, new NullProgressMonitor());
+
+ return true;
+ } catch (JavaModelException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Allocates and returns an {@link AndroidClasspathContainer} object with the proper
+ * path to the framework jar file.
+ * @param javaProject The java project that will receive the container.
+ */
+ private static IClasspathContainer allocateAndroidContainer(IJavaProject javaProject) {
+ final IProject iProject = javaProject.getProject();
+
+ String markerMessage = null;
+ boolean outputToConsole = true;
+
+ try {
+ AdtPlugin plugin = AdtPlugin.getDefault();
+
+ // get the lock object for project manipulation during SDK load.
+ Object lock = plugin.getSdkLockObject();
+ synchronized (lock) {
+ boolean sdkIsLoaded = plugin.getSdkLoadStatus() == LoadStatus.LOADED;
+
+ // check if the project has a valid target.
+ IAndroidTarget target = null;
+ if (sdkIsLoaded) {
+ target = Sdk.getCurrent().getTarget(iProject);
+ }
+
+ // if we are loaded and the target is non null, we create a valid ClassPathContainer
+ if (sdkIsLoaded && target != null) {
+ String targetName = target.getFullName();
+
+ return new AndroidClasspathContainer(
+ createClasspathEntries(iProject, target, targetName),
+ new Path(CONTAINER_ID), targetName);
+ }
+
+ // In case of error, we'll try different thing to provide the best error message
+ // possible.
+ // Get the project's target's hash string (if it exists)
+ String hashString = Sdk.getProjectTargetHashString(iProject);
+
+ if (hashString == null || hashString.length() == 0) {
+ // if there is no hash string we only show this if the SDK is loaded.
+ // For a project opened at start-up with no target, this would be displayed
+ // twice, once when the project is opened, and once after the SDK has
+ // finished loading.
+ // By testing the sdk is loaded, we only show this once in the console.
+ if (sdkIsLoaded) {
+ markerMessage = String.format(
+ "Project has no target set. Edit the project properties to set one.");
+ }
+ } else if (sdkIsLoaded) {
+ markerMessage = String.format(
+ "Unable to resolve target '%s'", hashString);
+ } else {
+ // this is the case where there is a hashString but the SDK is not yet
+ // loaded and therefore we can't get the target yet.
+ // We check if there is a cache of the needed information.
+ AndroidClasspathContainer container = getContainerFromCache(iProject);
+
+ if (container == null) {
+ // either the cache was wrong (ie folder does not exists anymore), or
+ // there was no cache. In this case we need to make sure the project
+ // is resolved again after the SDK is loaded.
+ plugin.setProjectToResolve(javaProject);
+
+ markerMessage = String.format(
+ "Unable to resolve target '%s' until the SDK is loaded.",
+ hashString);
+
+ // let's not log this one to the console as it will happen at every boot,
+ // and it's expected. (we do keep the error marker though).
+ outputToConsole = false;
+
+ } else {
+ // we created a container from the cache, so we register the project
+ // to be checked for cache validity once the SDK is loaded
+ plugin.setProjectToCheck(javaProject);
+
+ // and return the container
+ return container;
+ }
+
+ }
+
+ // return a dummy container to replace the one we may have had before.
+ // It'll be replaced by the real when if/when the target is resolved if/when the
+ // SDK finishes loading.
+ return new IClasspathContainer() {
+ public IClasspathEntry[] getClasspathEntries() {
+ return new IClasspathEntry[0];
+ }
+
+ public String getDescription() {
+ return "Unable to get system library for the project";
+ }
+
+ public int getKind() {
+ return IClasspathContainer.K_DEFAULT_SYSTEM;
+ }
+
+ public IPath getPath() {
+ return null;
+ }
+ };
+ }
+ } finally {
+ if (markerMessage != null) {
+ // log the error and put the marker on the project if we can.
+ if (outputToConsole) {
+ AdtPlugin.printBuildToConsole(AdtConstants.BUILD_ALWAYS, iProject,
+ markerMessage);
+ }
+
+ try {
+ BaseProjectHelper.addMarker(iProject, AdtConstants.MARKER_TARGET, markerMessage,
+ -1, IMarker.SEVERITY_ERROR, IMarker.PRIORITY_HIGH);
+ } catch (CoreException e) {
+ // In some cases, the workspace may be locked for modification when we
+ // pass here.
+ // We schedule a new job to put the marker after.
+ final String fmessage = markerMessage;
+ Job markerJob = new Job("Android SDK: Resolving error markers") {
+ @Override
+ protected IStatus run(IProgressMonitor monitor) {
+ try {
+ BaseProjectHelper.addMarker(iProject, AdtConstants.MARKER_TARGET,
+ fmessage, -1, IMarker.SEVERITY_ERROR,
+ IMarker.PRIORITY_HIGH);
+ } catch (CoreException e2) {
+ return e2.getStatus();
+ }
+
+ return Status.OK_STATUS;
+ }
+ };
+
+ // build jobs are run after other interactive jobs
+ markerJob.setPriority(Job.BUILD);
+ markerJob.schedule();
+ }
+ } else {
+ // no error, remove potential MARKER_TARGETs.
+ try {
+ if (iProject.exists()) {
+ iProject.deleteMarkers(AdtConstants.MARKER_TARGET, true,
+ IResource.DEPTH_INFINITE);
+ }
+ } catch (CoreException ce) {
+ // In some cases, the workspace may be locked for modification when we pass
+ // here, so we schedule a new job to put the marker after.
+ Job markerJob = new Job("Android SDK: Resolving error markers") {
+ @Override
+ protected IStatus run(IProgressMonitor monitor) {
+ try {
+ iProject.deleteMarkers(AdtConstants.MARKER_TARGET, true,
+ IResource.DEPTH_INFINITE);
+ } catch (CoreException e2) {
+ return e2.getStatus();
+ }
+
+ return Status.OK_STATUS;
+ }
+ };
+
+ // build jobs are run after other interactive jobs
+ markerJob.setPriority(Job.BUILD);
+ markerJob.schedule();
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates and returns an array of {@link IClasspathEntry} objects for the android
+ * framework and optional libraries.
+ * <p/>This references the OS path to the android.jar and the
+ * java doc directory. This is dynamically created when a project is opened,
+ * and never saved in the project itself, so there's no risk of storing an
+ * obsolete path.
+ * The method also stores the paths used to create the entries in the project persistent
+ * properties. A new {@link AndroidClasspathContainer} can be created from the stored path
+ * using the {@link #getContainerFromCache(IProject)} method.
+ * @param project
+ * @param target The target that contains the libraries.
+ * @param targetName
+ */
+ private static IClasspathEntry[] createClasspathEntries(IProject project,
+ IAndroidTarget target, String targetName) {
+
+ // get the path from the target
+ String[] paths = getTargetPaths(target);
+
+ // create the classpath entry from the paths
+ IClasspathEntry[] entries = createClasspathEntriesFromPaths(paths);
+
+ // paths now contains all the path required to recreate the IClasspathEntry with no
+ // target info. We encode them in a single string, with each path separated by
+ // OS path separator.
+ StringBuilder sb = new StringBuilder(CACHE_VERSION);
+ for (String p : paths) {
+ sb.append(PATH_SEPARATOR);
+ sb.append(p);
+ }
+
+ // store this in a project persistent property
+ ProjectHelper.saveStringProperty(project, PROPERTY_CONTAINER_CACHE, sb.toString());
+ ProjectHelper.saveStringProperty(project, PROPERTY_TARGET_NAME, targetName);
+
+ return entries;
+ }
+
+ /**
+ * Generates an {@link AndroidClasspathContainer} from the project cache, if possible.
+ */
+ private static AndroidClasspathContainer getContainerFromCache(IProject project) {
+ // get the cached info from the project persistent properties.
+ String cache = ProjectHelper.loadStringProperty(project, PROPERTY_CONTAINER_CACHE);
+ String targetNameCache = ProjectHelper.loadStringProperty(project, PROPERTY_TARGET_NAME);
+ if (cache == null || targetNameCache == null) {
+ return null;
+ }
+
+ // the first 2 chars must match CACHE_VERSION. The 3rd char is the normal separator.
+ if (cache.startsWith(CACHE_VERSION_SEP) == false) {
+ return null;
+ }
+
+ cache = cache.substring(CACHE_VERSION_SEP.length());
+
+ // the cache contains multiple paths, separated by a character guaranteed to not be in
+ // the path (\u001C).
+ // The first 3 are for android.jar (jar, source, doc), the rest are for the optional
+ // libraries and should contain at least one doc and a jar (if there are any libraries).
+ // Therefore, the path count should be 3 or 5+
+ String[] paths = cache.split(Pattern.quote(PATH_SEPARATOR));
+ if (paths.length < 3 || paths.length == 4) {
+ return null;
+ }
+
+ // now we check the paths actually exist.
+ // There's an exception: If the source folder for android.jar does not exist, this is
+ // not a problem, so we skip it.
+ // Also paths[PATH_ANDROID_DOCS] is a URI to the javadoc, so we test it a bit differently.
+ try {
+ if (new File(paths[PATH_ANDROID_JAR]).exists() == false ||
+ new File(new URI(paths[PATH_ANDROID_DOCS])).exists() == false) {
+ return null;
+ }
+ } catch (URISyntaxException e) {
+ return null;
+ } finally {
+
+ }
+
+ for (int i = 3 ; i < paths.length; i++) {
+ String path = paths[i];
+ if (path.length() > 0) {
+ File f = new File(path);
+ if (f.exists() == false) {
+ return null;
+ }
+ }
+ }
+
+ IClasspathEntry[] entries = createClasspathEntriesFromPaths(paths);
+
+ return new AndroidClasspathContainer(entries,
+ new Path(CONTAINER_ID), targetNameCache);
+ }
+
+ /**
+ * Generates an array of {@link IClasspathEntry} from a set of paths.
+ * @see #getTargetPaths(IAndroidTarget)
+ */
+ private static IClasspathEntry[] createClasspathEntriesFromPaths(String[] paths) {
+ ArrayList<IClasspathEntry> list = new ArrayList<IClasspathEntry>();
+
+ // First, we create the IClasspathEntry for the framework.
+ // now add the android framework to the class path.
+ // create the path object.
+ IPath android_lib = new Path(paths[PATH_ANDROID_JAR]);
+ IPath android_src = new Path(paths[PATH_ANDROID_SRC]);
+
+ // create the java doc link.
+ IClasspathAttribute cpAttribute = JavaCore.newClasspathAttribute(
+ IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME,
+ paths[PATH_ANDROID_DOCS]);
+
+ // create the access rule to restrict access to classes in com.android.internal
+ IAccessRule accessRule = JavaCore.newAccessRule(
+ new Path("com/android/internal/**"), //$NON-NLS-1$
+ IAccessRule.K_NON_ACCESSIBLE);
+
+ IClasspathEntry frameworkClasspathEntry = JavaCore.newLibraryEntry(android_lib,
+ android_src, // source attachment path
+ null, // default source attachment root path.
+ new IAccessRule[] { accessRule },
+ new IClasspathAttribute[] { cpAttribute },
+ false // not exported.
+ );
+
+ list.add(frameworkClasspathEntry);
+
+ // now deal with optional libraries
+ if (paths.length >= 5) {
+ String docPath = paths[PATH_ANDROID_OPT_DOCS];
+ int i = 4;
+ while (i < paths.length) {
+ Path jarPath = new Path(paths[i++]);
+
+ IClasspathAttribute[] attributes = null;
+ if (docPath.length() > 0) {
+ attributes = new IClasspathAttribute[] {
+ JavaCore.newClasspathAttribute(
+ IClasspathAttribute.JAVADOC_LOCATION_ATTRIBUTE_NAME,
+ docPath)
+ };
+ }
+
+ IClasspathEntry entry = JavaCore.newLibraryEntry(
+ jarPath,
+ null, // source attachment path
+ null, // default source attachment root path.
+ null,
+ attributes,
+ false // not exported.
+ );
+ list.add(entry);
+ }
+ }
+
+ return list.toArray(new IClasspathEntry[list.size()]);
+ }
+
+ /**
+ * Checks the projects' caches. If the cache was valid, the project is removed from the list.
+ * @param projects the list of projects to check.
+ */
+ public static void checkProjectsCache(ArrayList<IJavaProject> projects) {
+ int i = 0;
+ projectLoop: while (i < projects.size()) {
+ IJavaProject javaProject = projects.get(i);
+ IProject iProject = javaProject.getProject();
+
+ // get the target from the project and its paths
+ IAndroidTarget target = Sdk.getCurrent().getTarget(javaProject.getProject());
+ if (target == null) {
+ // this is really not supposed to happen. This would mean there are cached paths,
+ // but default.properties was deleted. Keep the project in the list to force
+ // a resolve which will display the error.
+ i++;
+ continue;
+ }
+
+ String[] targetPaths = getTargetPaths(target);
+
+ // now get the cached paths
+ String cache = ProjectHelper.loadStringProperty(iProject, PROPERTY_CONTAINER_CACHE);
+ if (cache == null) {
+ // this should not happen. We'll force resolve again anyway.
+ i++;
+ continue;
+ }
+
+ String[] cachedPaths = cache.split(Pattern.quote(PATH_SEPARATOR));
+ if (cachedPaths.length < 3 || cachedPaths.length == 4) {
+ // paths length is wrong. simply resolve the project again
+ i++;
+ continue;
+ }
+
+ // Now we compare the paths. The first 4 can be compared directly.
+ // because of case sensitiveness we need to use File objects
+
+ if (targetPaths.length != cachedPaths.length) {
+ // different paths, force resolve again.
+ i++;
+ continue;
+ }
+
+ // compare the main paths (android.jar, main sources, main javadoc)
+ if (new File(targetPaths[PATH_ANDROID_JAR]).equals(
+ new File(cachedPaths[PATH_ANDROID_JAR])) == false ||
+ new File(targetPaths[PATH_ANDROID_SRC]).equals(
+ new File(cachedPaths[PATH_ANDROID_SRC])) == false ||
+ new File(targetPaths[PATH_ANDROID_DOCS]).equals(
+ new File(cachedPaths[PATH_ANDROID_DOCS])) == false) {
+ // different paths, force resolve again.
+ i++;
+ continue;
+ }
+
+ if (cachedPaths.length > PATH_ANDROID_OPT_DOCS) {
+ // compare optional libraries javadoc
+ if (new File(targetPaths[PATH_ANDROID_OPT_DOCS]).equals(
+ new File(cachedPaths[PATH_ANDROID_OPT_DOCS])) == false) {
+ // different paths, force resolve again.
+ i++;
+ continue;
+ }
+
+ // testing the optional jar files is a little bit trickier.
+ // The order is not guaranteed to be identical.
+ // From a previous test, we do know however that there is the same number.
+ // The number of libraries should be low enough that we can simply go through the
+ // lists manually.
+ targetLoop: for (int tpi = 4 ; tpi < targetPaths.length; tpi++) {
+ String targetPath = targetPaths[tpi];
+
+ // look for a match in the other array
+ for (int cpi = 4 ; cpi < cachedPaths.length; cpi++) {
+ if (new File(targetPath).equals(new File(cachedPaths[cpi]))) {
+ // found a match. Try the next targetPath
+ continue targetLoop;
+ }
+ }
+
+ // if we stop here, we haven't found a match, which means there's a
+ // discrepancy in the libraries. We force a resolve.
+ i++;
+ continue projectLoop;
+ }
+ }
+
+ // at the point the check passes, and we can remove the project from the list.
+ // we do not increment i in this case.
+ projects.remove(i);
+ }
+ }
+
+ /**
+ * Returns the paths necessary to create the {@link IClasspathEntry} for this targets.
+ * <p/>The paths are always in the same order.
+ * <ul>
+ * <li>Path to android.jar</li>
+ * <li>Path to the source code for android.jar</li>
+ * <li>Path to the javadoc for the android platform</li>
+ * </ul>
+ * Additionally, if there are optional libraries, the array will contain:
+ * <ul>
+ * <li>Path to the librairies javadoc</li>
+ * <li>Path to the first .jar file</li>
+ * <li>(more .jar as needed)</li>
+ * </ul>
+ */
+ private static String[] getTargetPaths(IAndroidTarget target) {
+ ArrayList<String> paths = new ArrayList<String>();
+
+ // first, we get the path for android.jar
+ // The order is: android.jar, source folder, docs folder
+ paths.add(target.getPath(IAndroidTarget.ANDROID_JAR));
+ paths.add(target.getPath(IAndroidTarget.SOURCES));
+ paths.add(AdtPlugin.getUrlDoc());
+
+ // now deal with optional libraries.
+ IOptionalLibrary[] libraries = target.getOptionalLibraries();
+ if (libraries != null) {
+ // all the optional libraries use the same javadoc, so we start with this
+ String targetDocPath = target.getPath(IAndroidTarget.DOCS);
+ if (targetDocPath != null) {
+ paths.add(targetDocPath);
+ } else {
+ // we add an empty string, to always have the same count.
+ paths.add("");
+ }
+
+ // because different libraries could use the same jar file, we make sure we add
+ // each jar file only once.
+ HashSet<String> visitedJars = new HashSet<String>();
+ for (IOptionalLibrary library : libraries) {
+ String jarPath = library.getJarPath();
+ if (visitedJars.contains(jarPath) == false) {
+ visitedJars.add(jarPath);
+ paths.add(jarPath);
+ }
+ }
+ }
+
+ return paths.toArray(new String[paths.size()]);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/properties/AndroidPropertyPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/properties/AndroidPropertyPage.java
new file mode 100644
index 0000000..a4c019f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/project/properties/AndroidPropertyPage.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.project.properties;
+
+import com.android.ide.eclipse.adt.sdk.Sdk;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdkuilib.ApkConfigWidget;
+import com.android.sdkuilib.SdkTargetSelector;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.ui.IWorkbenchPropertyPage;
+import org.eclipse.ui.dialogs.PropertyPage;
+
+import java.util.Map;
+
+/**
+ * Property page for "Android" project.
+ * This is accessible from the Package Explorer when right clicking a project and choosing
+ * "Properties".
+ *
+ */
+public class AndroidPropertyPage extends PropertyPage implements IWorkbenchPropertyPage {
+
+ private IProject mProject;
+ private SdkTargetSelector mSelector;
+ private ApkConfigWidget mApkConfigWidget;
+
+ public AndroidPropertyPage() {
+ // pass
+ }
+
+ @Override
+ protected Control createContents(Composite parent) {
+ // get the element (this is not yet valid in the constructor).
+ mProject = (IProject)getElement();
+
+ // get the targets from the sdk
+ IAndroidTarget[] targets = null;
+ if (Sdk.getCurrent() != null) {
+ targets = Sdk.getCurrent().getTargets();
+ }
+
+ // build the UI.
+ Composite top = new Composite(parent, SWT.NONE);
+ top.setLayoutData(new GridData(GridData.FILL_BOTH));
+ top.setLayout(new GridLayout(1, false));
+
+ Label l = new Label(top, SWT.NONE);
+ l.setText("Project Target");
+
+ mSelector = new SdkTargetSelector(top, targets, false /*allowMultipleSelection*/);
+
+ l = new Label(top, SWT.SEPARATOR | SWT.HORIZONTAL);
+ l.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ l = new Label(top, SWT.NONE);
+ l.setText("Project APK Configurations");
+
+ mApkConfigWidget = new ApkConfigWidget(top);
+
+ // fill the ui
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null && mProject.isOpen()) {
+ // get the target
+ IAndroidTarget target = currentSdk.getTarget(mProject);
+ if (target != null) {
+ mSelector.setSelection(target);
+ }
+
+ // get the apk configurations
+ Map<String, String> configs = currentSdk.getProjectApkConfigs(mProject);
+ mApkConfigWidget.fillTable(configs);
+ }
+
+ mSelector.setSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // look for the selection and validate the page if there is a selection
+ IAndroidTarget target = mSelector.getFirstSelected();
+ setValid(target != null);
+ }
+ });
+
+ if (mProject.isOpen() == false) {
+ // disable the ui.
+ }
+
+ return top;
+ }
+
+ @Override
+ public boolean performOk() {
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ currentSdk.setProject(mProject, mSelector.getFirstSelected(),
+ mApkConfigWidget.getApkConfigs());
+ }
+
+ return true;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/AndroidJarLoader.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/AndroidJarLoader.java
new file mode 100644
index 0000000..1f6ebf1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/AndroidJarLoader.java
@@ -0,0 +1,431 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.sdk;
+
+import com.android.ide.eclipse.common.AndroidConstants;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.SubMonitor;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import javax.management.InvalidAttributeValueException;
+
+/**
+ * Custom class loader able to load a class from the SDK jar file.
+ */
+public class AndroidJarLoader extends ClassLoader implements IAndroidClassLoader {
+
+ /**
+ * Wrapper around a {@link Class} to provide the methods of
+ * {@link IAndroidClassLoader.IClassDescriptor}.
+ */
+ public final static class ClassWrapper implements IClassDescriptor {
+ private Class<?> mClass;
+
+ public ClassWrapper(Class<?> clazz) {
+ mClass = clazz;
+ }
+
+ public String getCanonicalName() {
+ return mClass.getCanonicalName();
+ }
+
+ public IClassDescriptor[] getDeclaredClasses() {
+ Class<?>[] classes = mClass.getDeclaredClasses();
+ IClassDescriptor[] iclasses = new IClassDescriptor[classes.length];
+ for (int i = 0 ; i < classes.length ; i++) {
+ iclasses[i] = new ClassWrapper(classes[i]);
+ }
+
+ return iclasses;
+ }
+
+ public IClassDescriptor getEnclosingClass() {
+ return new ClassWrapper(mClass.getEnclosingClass());
+ }
+
+ public String getSimpleName() {
+ return mClass.getSimpleName();
+ }
+
+ public IClassDescriptor getSuperclass() {
+ return new ClassWrapper(mClass.getSuperclass());
+ }
+
+ @Override
+ public boolean equals(Object clazz) {
+ if (clazz instanceof ClassWrapper) {
+ return mClass.equals(((ClassWrapper)clazz).mClass);
+ }
+ return super.equals(clazz);
+ }
+
+ @Override
+ public int hashCode() {
+ return mClass.hashCode();
+ }
+
+
+ public boolean isInstantiable() {
+ int modifiers = mClass.getModifiers();
+ return Modifier.isAbstract(modifiers) == false && Modifier.isPublic(modifiers) == true;
+ }
+
+ public Class<?> wrappedClass() {
+ return mClass;
+ }
+
+ }
+
+ private String mOsFrameworkLocation;
+
+ /** A cache for binary data extracted from the zip */
+ private final HashMap<String, byte[]> mEntryCache = new HashMap<String, byte[]>();
+ /** A cache for already defined Classes */
+ private final HashMap<String, Class<?> > mClassCache = new HashMap<String, Class<?> >();
+
+ /**
+ * Creates the class loader by providing the os path to the framework jar archive
+ *
+ * @param osFrameworkLocation OS Path of the framework JAR file
+ */
+ public AndroidJarLoader(String osFrameworkLocation) {
+ super();
+ mOsFrameworkLocation = osFrameworkLocation;
+ }
+
+ public String getSource() {
+ return mOsFrameworkLocation;
+ }
+
+ /**
+ * Pre-loads all class binary data that belong to the given package by reading the archive
+ * once and caching them internally.
+ * <p/>
+ * This does not actually preload "classes", it just reads the unzipped bytes for a given
+ * class. To obtain a class, one must call {@link #findClass(String)} later.
+ * <p/>
+ * All classes which package name starts with "packageFilter" will be included and can be
+ * found later.
+ * <p/>
+ * May throw some exceptions if the framework JAR cannot be read.
+ *
+ * @param packageFilter The package that contains all the class data to preload, using a fully
+ * qualified binary name (.e.g "com.my.package."). The matching algorithm
+ * is simple "startsWith". Use an empty string to include everything.
+ * @param taskLabel An optional task name for the sub monitor. Can be null.
+ * @param monitor A progress monitor. Can be null. Caller is responsible for calling done.
+ * @throws IOException
+ * @throws InvalidAttributeValueException
+ * @throws ClassFormatError
+ */
+ public void preLoadClasses(String packageFilter, String taskLabel, IProgressMonitor monitor)
+ throws IOException, InvalidAttributeValueException, ClassFormatError {
+ // Transform the package name into a zip entry path
+ String pathFilter = packageFilter.replaceAll("\\.", "/"); //$NON-NLS-1$ //$NON-NLS-2$
+
+ SubMonitor progress = SubMonitor.convert(monitor, taskLabel == null ? "" : taskLabel, 100);
+
+ // create streams to read the intermediary archive
+ FileInputStream fis = new FileInputStream(mOsFrameworkLocation);
+ ZipInputStream zis = new ZipInputStream(fis);
+ ZipEntry entry;
+ while ((entry = zis.getNextEntry()) != null) {
+ // get the name of the entry.
+ String entryPath = entry.getName();
+
+ if (!entryPath.endsWith(AndroidConstants.DOT_CLASS)) {
+ // only accept class files
+ continue;
+ }
+
+ // check if it is part of the package to preload
+ if (pathFilter.length() > 0 && !entryPath.startsWith(pathFilter)) {
+ continue;
+ }
+ String className = entryPathToClassName(entryPath);
+
+ if (!mEntryCache.containsKey(className)) {
+ long entrySize = entry.getSize();
+ if (entrySize > Integer.MAX_VALUE) {
+ throw new InvalidAttributeValueException();
+ }
+ byte[] data = readZipData(zis, (int)entrySize);
+ mEntryCache.put(className, data);
+ }
+
+ // advance 5% of whatever is allocated on the progress bar
+ progress.setWorkRemaining(100);
+ progress.worked(5);
+ progress.subTask(String.format("Preload %1$s", className));
+ }
+ }
+
+ /**
+ * Finds and loads all classes that derive from a given set of super classes.
+ * <p/>
+ * As a side-effect this will load and cache most, if not all, classes in the input JAR file.
+ *
+ * @param packageFilter Base name of package of classes to find.
+ * Use an empty string to find everyting.
+ * @param superClasses The super classes of all the classes to find.
+ * @return An hash map which keys are the super classes looked for and which values are
+ * ArrayList of the classes found. The array lists are always created for all the
+ * valid keys, they are simply empty if no deriving class is found for a given
+ * super class.
+ * @throws IOException
+ * @throws InvalidAttributeValueException
+ * @throws ClassFormatError
+ */
+ public HashMap<String, ArrayList<IClassDescriptor>> findClassesDerivingFrom(
+ String packageFilter,
+ String[] superClasses)
+ throws IOException, InvalidAttributeValueException, ClassFormatError {
+
+ packageFilter = packageFilter.replaceAll("\\.", "/"); //$NON-NLS-1$ //$NON-NLS-2$
+
+ HashMap<String, ArrayList<IClassDescriptor>> mClassesFound =
+ new HashMap<String, ArrayList<IClassDescriptor>>();
+
+ for (String className : superClasses) {
+ mClassesFound.put(className, new ArrayList<IClassDescriptor>());
+ }
+
+ // create streams to read the intermediary archive
+ FileInputStream fis = new FileInputStream(mOsFrameworkLocation);
+ ZipInputStream zis = new ZipInputStream(fis);
+ ZipEntry entry;
+ while ((entry = zis.getNextEntry()) != null) {
+ // get the name of the entry and convert to a class binary name
+ String entryPath = entry.getName();
+ if (!entryPath.endsWith(AndroidConstants.DOT_CLASS)) {
+ // only accept class files
+ continue;
+ }
+ if (packageFilter.length() > 0 && !entryPath.startsWith(packageFilter)) {
+ // only accept stuff from the requested root package.
+ continue;
+ }
+ String className = entryPathToClassName(entryPath);
+
+ Class<?> loaded_class = mClassCache.get(className);
+ if (loaded_class == null) {
+ byte[] data = mEntryCache.get(className);
+ if (data == null) {
+ // Get the class and cache it
+ long entrySize = entry.getSize();
+ if (entrySize > Integer.MAX_VALUE) {
+ throw new InvalidAttributeValueException();
+ }
+ data = readZipData(zis, (int)entrySize);
+ }
+ loaded_class = defineAndCacheClass(className, data);
+ }
+
+ for (Class<?> superClass = loaded_class.getSuperclass();
+ superClass != null;
+ superClass = superClass.getSuperclass()) {
+ String superName = superClass.getCanonicalName();
+ if (mClassesFound.containsKey(superName)) {
+ mClassesFound.get(superName).add(new ClassWrapper(loaded_class));
+ break;
+ }
+ }
+ }
+
+ return mClassesFound;
+ }
+
+ /** Helper method that converts a Zip entry path into a corresponding
+ * Java full qualified binary class name.
+ * <p/>
+ * F.ex, this converts "com/my/package/Foo.class" into "com.my.package.Foo".
+ */
+ private String entryPathToClassName(String entryPath) {
+ return entryPath.replaceFirst("\\.class$", "").replaceAll("[/\\\\]", "."); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+ }
+
+ /**
+ * Finds the class with the specified binary name.
+ *
+ * {@inheritDoc}
+ */
+ @Override
+ protected Class<?> findClass(String name) throws ClassNotFoundException {
+ try {
+ // try to find the class in the cache
+ Class<?> cached_class = mClassCache.get(name);
+ if (cached_class == ClassNotFoundException.class) {
+ // we already know we can't find this class, don't try again
+ throw new ClassNotFoundException(name);
+ } else if (cached_class != null) {
+ return cached_class;
+ }
+
+ // if not found, look it up and cache it
+ byte[] data = loadClassData(name);
+ if (data != null) {
+ return defineAndCacheClass(name, data);
+ } else {
+ // if the class can't be found, record a CNFE class in the map so
+ // that we don't try to reload it next time
+ mClassCache.put(name, ClassNotFoundException.class);
+ throw new ClassNotFoundException(name);
+ }
+ } catch (ClassNotFoundException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new ClassNotFoundException(e.getMessage());
+ }
+ }
+
+ /**
+ * Defines a class based on its binary data and caches the resulting class object.
+ *
+ * @param name The binary name of the class (i.e. package.class1$class2)
+ * @param data The binary data from the loader.
+ * @return The class defined
+ * @throws ClassFormatError if defineClass failed.
+ */
+ private Class<?> defineAndCacheClass(String name, byte[] data) throws ClassFormatError {
+ Class<?> cached_class;
+ cached_class = defineClass(null, data, 0, data.length);
+
+ if (cached_class != null) {
+ // Add new class to the cache class and remove it from the zip entry data cache
+ mClassCache.put(name, cached_class);
+ mEntryCache.remove(name);
+ }
+ return cached_class;
+ }
+
+ /**
+ * Loads a class data from its binary name.
+ * <p/>
+ * This uses the class binary data that has been preloaded earlier by the preLoadClasses()
+ * method if possible.
+ *
+ * @param className the binary name
+ * @return an array of bytes representing the class data or null if not found
+ * @throws InvalidAttributeValueException
+ * @throws IOException
+ */
+ private synchronized byte[] loadClassData(String className)
+ throws InvalidAttributeValueException, IOException {
+
+ byte[] data = mEntryCache.get(className);
+ if (data != null) {
+ return data;
+ }
+
+ // The name is a binary name. Something like "android.R", or "android.R$id".
+ // Make a path out of it.
+ String entryName = className.replaceAll("\\.", "/") + AndroidConstants.DOT_CLASS; //$NON-NLS-1$ //$NON-NLS-2$
+
+ // create streams to read the intermediary archive
+ FileInputStream fis = new FileInputStream(mOsFrameworkLocation);
+ ZipInputStream zis = new ZipInputStream(fis);
+
+ // loop on the entries of the intermediary package and put them in the final package.
+ ZipEntry entry;
+
+ while ((entry = zis.getNextEntry()) != null) {
+ // get the name of the entry.
+ String currEntryName = entry.getName();
+
+ if (currEntryName.equals(entryName)) {
+ long entrySize = entry.getSize();
+ if (entrySize > Integer.MAX_VALUE) {
+ throw new InvalidAttributeValueException();
+ }
+
+ data = readZipData(zis, (int)entrySize);
+ return data;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Reads data for the <em>current</em> entry from the zip input stream.
+ *
+ * @param zis The Zip input stream
+ * @param entrySize The entry size. -1 if unknown.
+ * @return The new data for the <em>current</em> entry.
+ * @throws IOException If ZipInputStream.read() fails.
+ */
+ private byte[] readZipData(ZipInputStream zis, int entrySize) throws IOException {
+ int block_size = 1024;
+ int data_size = entrySize < 1 ? block_size : entrySize;
+ int offset = 0;
+ byte[] data = new byte[data_size];
+
+ while(zis.available() != 0) {
+ int count = zis.read(data, offset, data_size - offset);
+ if (count < 0) { // read data is done
+ break;
+ }
+ offset += count;
+
+ if (entrySize >= 1 && offset >= entrySize) { // we know the size and we're done
+ break;
+ }
+
+ // if we don't know the entry size and we're not done reading,
+ // expand the data buffer some more.
+ if (offset >= data_size) {
+ byte[] temp = new byte[data_size + block_size];
+ System.arraycopy(data, 0, temp, 0, data_size);
+ data_size += block_size;
+ data = temp;
+ block_size *= 2;
+ }
+ }
+
+ if (offset < data_size) {
+ // buffer was allocated too large, trim it
+ byte[] temp = new byte[offset];
+ if (offset > 0) {
+ System.arraycopy(data, 0, temp, 0, offset);
+ }
+ data = temp;
+ }
+
+ return data;
+ }
+
+ /**
+ * Returns a {@link IAndroidClassLoader.IClassDescriptor} by its fully-qualified name.
+ * @param className the fully-qualified name of the class to return.
+ * @throws ClassNotFoundException
+ */
+ public IClassDescriptor getClass(String className) throws ClassNotFoundException {
+ try {
+ return new ClassWrapper(loadClass(className));
+ } catch (ClassNotFoundException e) {
+ throw e; // useful for debugging
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/AndroidTargetData.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/AndroidTargetData.java
new file mode 100644
index 0000000..a8852e7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/AndroidTargetData.java
@@ -0,0 +1,321 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.sdk;
+
+import com.android.ide.eclipse.adt.build.DexWrapper;
+import com.android.ide.eclipse.common.resources.IResourceRepository;
+import com.android.ide.eclipse.editors.descriptors.IDescriptorProvider;
+import com.android.ide.eclipse.editors.layout.descriptors.LayoutDescriptors;
+import com.android.ide.eclipse.editors.manifest.descriptors.AndroidManifestDescriptors;
+import com.android.ide.eclipse.editors.menu.descriptors.MenuDescriptors;
+import com.android.ide.eclipse.editors.resources.descriptors.ResourcesDescriptors;
+import com.android.ide.eclipse.editors.resources.manager.ProjectResources;
+import com.android.ide.eclipse.editors.xml.descriptors.XmlDescriptors;
+import com.android.layoutlib.api.ILayoutBridge;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.IAndroidTarget.IOptionalLibrary;
+
+import java.util.Hashtable;
+import java.util.Map;
+
+/**
+ * This class contains the data of an Android Target as loaded from the SDK.
+ */
+public class AndroidTargetData {
+
+ public final static int DESCRIPTOR_MANIFEST = 1;
+ public final static int DESCRIPTOR_LAYOUT = 2;
+ public final static int DESCRIPTOR_MENU = 3;
+ public final static int DESCRIPTOR_XML = 4;
+ public final static int DESCRIPTOR_RESOURCES = 5;
+ public final static int DESCRIPTOR_SEARCHABLE = 6;
+ public final static int DESCRIPTOR_PREFERENCES = 7;
+ public final static int DESCRIPTOR_GADGET_PROVIDER = 8;
+
+ public final static class LayoutBridge {
+ /** Link to the layout bridge */
+ public ILayoutBridge bridge;
+
+ public LoadStatus status = LoadStatus.LOADING;
+
+ public ClassLoader classLoader;
+
+ public int apiLevel;
+ }
+
+ private final IAndroidTarget mTarget;
+
+ private DexWrapper mDexWrapper;
+
+ /**
+ * mAttributeValues is a map { key => list [ values ] }.
+ * The key for the map is "(element-xml-name,attribute-namespace:attribute-xml-local-name)".
+ * The attribute namespace prefix must be:
+ * - "android" for AndroidConstants.NS_RESOURCES
+ * - "xmlns" for the XMLNS URI.
+ *
+ * This is used for attributes that do not have a unique name, but still need to be populated
+ * with values in the UI. Uniquely named attributes have their values in {@link #mEnumValueMap}.
+ */
+ private Hashtable<String, String[]> mAttributeValues = new Hashtable<String, String[]>();
+
+ private IResourceRepository mSystemResourceRepository;
+
+ private AndroidManifestDescriptors mManifestDescriptors;
+ private LayoutDescriptors mLayoutDescriptors;
+ private MenuDescriptors mMenuDescriptors;
+ private XmlDescriptors mXmlDescriptors;
+
+ private Map<String, Map<String, Integer>> mEnumValueMap;
+
+ private ProjectResources mFrameworkResources;
+ private LayoutBridge mLayoutBridge;
+
+ private boolean mLayoutBridgeInit = false;
+
+ AndroidTargetData(IAndroidTarget androidTarget) {
+ mTarget = androidTarget;
+ }
+
+ void setDexWrapper(DexWrapper wrapper) {
+ mDexWrapper = wrapper;
+ }
+
+ /**
+ * Creates an AndroidTargetData object.
+ * @param optionalLibraries
+ */
+ void setExtraData(IResourceRepository systemResourceRepository,
+ AndroidManifestDescriptors manifestDescriptors,
+ LayoutDescriptors layoutDescriptors,
+ MenuDescriptors menuDescriptors,
+ XmlDescriptors xmlDescriptors,
+ Map<String, Map<String, Integer>> enumValueMap,
+ String[] permissionValues,
+ String[] activityIntentActionValues,
+ String[] broadcastIntentActionValues,
+ String[] serviceIntentActionValues,
+ String[] intentCategoryValues,
+ IOptionalLibrary[] optionalLibraries,
+ ProjectResources resources,
+ LayoutBridge layoutBridge) {
+
+ mSystemResourceRepository = systemResourceRepository;
+ mManifestDescriptors = manifestDescriptors;
+ mLayoutDescriptors = layoutDescriptors;
+ mMenuDescriptors = menuDescriptors;
+ mXmlDescriptors = xmlDescriptors;
+ mEnumValueMap = enumValueMap;
+ mFrameworkResources = resources;
+ mLayoutBridge = layoutBridge;
+
+ setPermissions(permissionValues);
+ setIntentFilterActionsAndCategories(activityIntentActionValues, broadcastIntentActionValues,
+ serviceIntentActionValues, intentCategoryValues);
+ setOptionalLibraries(optionalLibraries);
+ }
+
+ public DexWrapper getDexWrapper() {
+ return mDexWrapper;
+ }
+
+ public IResourceRepository getSystemResources() {
+ return mSystemResourceRepository;
+ }
+
+ /**
+ * Returns an {@link IDescriptorProvider} from a given Id.
+ * The Id can be one of {@link #DESCRIPTOR_MANIFEST}, {@link #DESCRIPTOR_LAYOUT},
+ * {@link #DESCRIPTOR_MENU}, or {@link #DESCRIPTOR_XML}.
+ * All other values will throw an {@link IllegalArgumentException}.
+ */
+ public IDescriptorProvider getDescriptorProvider(int descriptorId) {
+ switch (descriptorId) {
+ case DESCRIPTOR_MANIFEST:
+ return mManifestDescriptors;
+ case DESCRIPTOR_LAYOUT:
+ return mLayoutDescriptors;
+ case DESCRIPTOR_MENU:
+ return mMenuDescriptors;
+ case DESCRIPTOR_XML:
+ return mXmlDescriptors;
+ case DESCRIPTOR_RESOURCES:
+ // FIXME: since it's hard-coded the Resources Descriptors are not platform dependent.
+ return ResourcesDescriptors.getInstance();
+ case DESCRIPTOR_PREFERENCES:
+ return mXmlDescriptors.getPreferencesProvider();
+ case DESCRIPTOR_GADGET_PROVIDER:
+ return mXmlDescriptors.getGadgetProvider();
+ case DESCRIPTOR_SEARCHABLE:
+ return mXmlDescriptors.getSearchableProvider();
+ default :
+ throw new IllegalArgumentException();
+ }
+ }
+
+ /**
+ * Returns the manifest descriptors.
+ */
+ public AndroidManifestDescriptors getManifestDescriptors() {
+ return mManifestDescriptors;
+ }
+
+ /**
+ * Returns the layout Descriptors.
+ */
+ public LayoutDescriptors getLayoutDescriptors() {
+ return mLayoutDescriptors;
+ }
+
+ /**
+ * Returns the menu descriptors.
+ */
+ public MenuDescriptors getMenuDescriptors() {
+ return mMenuDescriptors;
+ }
+
+ /**
+ * Returns the XML descriptors
+ */
+ public XmlDescriptors getXmlDescriptors() {
+ return mXmlDescriptors;
+ }
+
+ /**
+ * Returns this list of possible values for an XML attribute.
+ * <p/>This should only be called for attributes for which possible values depend on the
+ * parent element node.
+ * <p/>For attributes that have the same values no matter the parent node, use
+ * {@link #getEnumValueMap()}.
+ * @param elementName the name of the element containing the attribute.
+ * @param attributeName the name of the attribute
+ * @return an array of String with the possible values, or <code>null</code> if no values were
+ * found.
+ */
+ public String[] getAttributeValues(String elementName, String attributeName) {
+ String key = String.format("(%1$s,%2$s)", elementName, attributeName); //$NON-NLS-1$
+ return mAttributeValues.get(key);
+ }
+
+ /**
+ * Returns this list of possible values for an XML attribute.
+ * <p/>This should only be called for attributes for which possible values depend on the
+ * parent and great-grand-parent element node.
+ * <p/>The typical example of this is for the 'name' attribute under
+ * activity/intent-filter/action
+ * <p/>For attributes that have the same values no matter the parent node, use
+ * {@link #getEnumValueMap()}.
+ * @param elementName the name of the element containing the attribute.
+ * @param attributeName the name of the attribute
+ * @param greatGrandParentElementName the great-grand-parent node.
+ * @return an array of String with the possible values, or <code>null</code> if no values were
+ * found.
+ */
+ public String[] getAttributeValues(String elementName, String attributeName,
+ String greatGrandParentElementName) {
+ if (greatGrandParentElementName != null) {
+ String key = String.format("(%1$s,%2$s,%3$s)", //$NON-NLS-1$
+ greatGrandParentElementName, elementName, attributeName);
+ String[] values = mAttributeValues.get(key);
+ if (values != null) {
+ return values;
+ }
+ }
+
+ return getAttributeValues(elementName, attributeName);
+ }
+
+ /**
+ * Returns the enum values map.
+ * <p/>The map defines the possible values for XML attributes. The key is the attribute name
+ * and the value is a map of (string, integer) in which the key (string) is the name of
+ * the value, and the Integer is the numerical value in the compiled binary XML files.
+ */
+ public Map<String, Map<String, Integer>> getEnumValueMap() {
+ return mEnumValueMap;
+ }
+
+ /**
+ * Returns the {@link ProjectResources} containing the Framework Resources.
+ */
+ public ProjectResources getFrameworkResources() {
+ return mFrameworkResources;
+ }
+
+ /**
+ * Returns a {@link LayoutBridge} object possibly containing a {@link ILayoutBridge} object.
+ * <p/>If {@link LayoutBridge#bridge} is <code>null</code>, {@link LayoutBridge#status} will
+ * contain the reason (either {@link LoadStatus#LOADING} or {@link LoadStatus#FAILED}).
+ * <p/>Valid {@link ILayoutBridge} objects are always initialized before being returned.
+ */
+ public synchronized LayoutBridge getLayoutBridge() {
+ if (mLayoutBridgeInit == false && mLayoutBridge.bridge != null) {
+ mLayoutBridge.bridge.init(mTarget.getPath(IAndroidTarget.FONTS),
+ getEnumValueMap());
+ mLayoutBridgeInit = true;
+ }
+ return mLayoutBridge;
+ }
+
+ /**
+ * Sets the permission values
+ * @param permissionValues the list of permissions
+ */
+ private void setPermissions(String[] permissionValues) {
+ setValues("(uses-permission,android:name)", permissionValues); //$NON-NLS-1$
+ setValues("(application,android:permission)", permissionValues); //$NON-NLS-1$
+ setValues("(activity,android:permission)", permissionValues); //$NON-NLS-1$
+ setValues("(receiver,android:permission)", permissionValues); //$NON-NLS-1$
+ setValues("(service,android:permission)", permissionValues); //$NON-NLS-1$
+ setValues("(provider,android:permission)", permissionValues); //$NON-NLS-1$
+ }
+
+ private void setIntentFilterActionsAndCategories(String[] activityIntentActions,
+ String[] broadcastIntentActions, String[] serviceIntentActions,
+ String[] intentCategoryValues) {
+ setValues("(activity,action,android:name)", activityIntentActions); //$NON-NLS-1$
+ setValues("(receiver,action,android:name)", broadcastIntentActions); //$NON-NLS-1$
+ setValues("(service,action,android:name)", serviceIntentActions); //$NON-NLS-1$
+ setValues("(category,android:name)", intentCategoryValues); //$NON-NLS-1$
+ }
+
+ private void setOptionalLibraries(IOptionalLibrary[] optionalLibraries) {
+ String[] values;
+
+ if (optionalLibraries == null) {
+ values = new String[0];
+ } else {
+ values = new String[optionalLibraries.length];
+ for (int i = 0; i < optionalLibraries.length; i++) {
+ values[i] = optionalLibraries[i].getName();
+ }
+ }
+ setValues("(uses-library,android:name)", values);
+ }
+
+ /**
+ * Sets a (name, values) pair in the hash map.
+ * <p/>
+ * If the name is already present in the map, it is first removed.
+ * @param name the name associated with the values.
+ * @param values The values to add.
+ */
+ private void setValues(String name, String[] values) {
+ mAttributeValues.remove(name);
+ mAttributeValues.put(name, values);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/AndroidTargetParser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/AndroidTargetParser.java
new file mode 100644
index 0000000..04baeba
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/AndroidTargetParser.java
@@ -0,0 +1,704 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.sdk;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.build.DexWrapper;
+import com.android.ide.eclipse.adt.sdk.AndroidTargetData.LayoutBridge;
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.common.resources.AttrsXmlParser;
+import com.android.ide.eclipse.common.resources.DeclareStyleableInfo;
+import com.android.ide.eclipse.common.resources.IResourceRepository;
+import com.android.ide.eclipse.common.resources.ResourceItem;
+import com.android.ide.eclipse.common.resources.ResourceType;
+import com.android.ide.eclipse.common.resources.ViewClassInfo;
+import com.android.ide.eclipse.editors.layout.descriptors.LayoutDescriptors;
+import com.android.ide.eclipse.editors.manifest.descriptors.AndroidManifestDescriptors;
+import com.android.ide.eclipse.editors.menu.descriptors.MenuDescriptors;
+import com.android.ide.eclipse.editors.resources.manager.ProjectResources;
+import com.android.ide.eclipse.editors.resources.manager.ResourceManager;
+import com.android.ide.eclipse.editors.xml.descriptors.XmlDescriptors;
+import com.android.layoutlib.api.ILayoutBridge;
+import com.android.sdklib.IAndroidTarget;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.SubMonitor;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.net.URL;
+import java.net.URLClassLoader;
+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 javax.management.InvalidAttributeValueException;
+
+/**
+ * Parser for the platform data in an SDK.
+ * <p/>
+ * This gather the following information:
+ * <ul>
+ * <li>Resource ID from <code>android.R</code></li>
+ * <li>The list of permissions values from <code>android.Manifest$permission</code></li>
+ * <li></li>
+ * </ul>
+ */
+public final class AndroidTargetParser {
+
+ private static final String TAG = "Framework Resource Parser";
+ private final IAndroidTarget mAndroidTarget;
+
+ /**
+ * Creates a platform data parser.
+ */
+ public AndroidTargetParser(IAndroidTarget platformTarget) {
+ mAndroidTarget = platformTarget;
+ }
+
+ /**
+ * Parses the framework, collects all interesting information and stores them in the
+ * {@link IAndroidTarget} given to the constructor.
+ *
+ * @param monitor A progress monitor. Can be null. Caller is responsible for calling done.
+ * @return True if the SDK path was valid and parsing has been attempted.
+ */
+ public IStatus run(IProgressMonitor monitor) {
+ try {
+ SubMonitor progress = SubMonitor.convert(monitor,
+ String.format("Parsing SDK %1$s", mAndroidTarget.getName()),
+ 14);
+
+ AndroidTargetData targetData = new AndroidTargetData(mAndroidTarget);
+
+ // load DX.
+ DexWrapper dexWrapper = new DexWrapper();
+ IStatus res = dexWrapper.loadDex(mAndroidTarget.getPath(IAndroidTarget.DX_JAR));
+ if (res != Status.OK_STATUS) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format("dx.jar loading failed for target '%1$s'",
+ mAndroidTarget.getFullName()));
+ }
+
+ // we have loaded dx.
+ targetData.setDexWrapper(dexWrapper);
+ progress.worked(1);
+
+ // parse the rest of the data.
+
+ AndroidJarLoader classLoader =
+ new AndroidJarLoader(mAndroidTarget.getPath(IAndroidTarget.ANDROID_JAR));
+
+ preload(classLoader, progress.newChild(40, SubMonitor.SUPPRESS_NONE));
+
+ if (progress.isCanceled()) {
+ return Status.CANCEL_STATUS;
+ }
+
+ // get the resource Ids.
+ progress.subTask("Resource IDs");
+ IResourceRepository frameworkRepository = collectResourceIds(classLoader);
+ progress.worked(1);
+
+ if (progress.isCanceled()) {
+ return Status.CANCEL_STATUS;
+ }
+
+ // get the permissions
+ progress.subTask("Permissions");
+ String[] permissionValues = collectPermissions(classLoader);
+ progress.worked(1);
+
+ if (progress.isCanceled()) {
+ return Status.CANCEL_STATUS;
+ }
+
+ // get the action and category values for the Intents.
+ progress.subTask("Intents");
+ ArrayList<String> activity_actions = new ArrayList<String>();
+ ArrayList<String> broadcast_actions = new ArrayList<String>();
+ ArrayList<String> service_actions = new ArrayList<String>();
+ ArrayList<String> categories = new ArrayList<String>();
+ collectIntentFilterActionsAndCategories(activity_actions, broadcast_actions,
+ service_actions, categories);
+ progress.worked(1);
+
+ if (progress.isCanceled()) {
+ return Status.CANCEL_STATUS;
+ }
+
+ // gather the attribute definition
+ progress.subTask("Attributes definitions");
+ AttrsXmlParser attrsXmlParser = new AttrsXmlParser(
+ mAndroidTarget.getPath(IAndroidTarget.ATTRIBUTES));
+ attrsXmlParser.preload();
+ progress.worked(1);
+
+ progress.subTask("Manifest definitions");
+ AttrsXmlParser attrsManifestXmlParser = new AttrsXmlParser(
+ mAndroidTarget.getPath(IAndroidTarget.MANIFEST_ATTRIBUTES),
+ attrsXmlParser);
+ attrsManifestXmlParser.preload();
+ progress.worked(1);
+
+ Collection<ViewClassInfo> mainList = new ArrayList<ViewClassInfo>();
+ Collection<ViewClassInfo> groupList = new ArrayList<ViewClassInfo>();
+
+ // collect the layout/widgets classes
+ progress.subTask("Widgets and layouts");
+ collectLayoutClasses(classLoader, attrsXmlParser, mainList, groupList,
+ progress.newChild(1));
+
+ if (progress.isCanceled()) {
+ return Status.CANCEL_STATUS;
+ }
+
+ ViewClassInfo[] layoutViewsInfo = mainList.toArray(new ViewClassInfo[mainList.size()]);
+ ViewClassInfo[] layoutGroupsInfo = groupList.toArray(
+ new ViewClassInfo[groupList.size()]);
+
+ // collect the preferences classes.
+ mainList.clear();
+ groupList.clear();
+ collectPreferenceClasses(classLoader, attrsXmlParser, mainList, groupList,
+ progress.newChild(1));
+
+ if (progress.isCanceled()) {
+ return Status.CANCEL_STATUS;
+ }
+
+ ViewClassInfo[] preferencesInfo = mainList.toArray(new ViewClassInfo[mainList.size()]);
+ ViewClassInfo[] preferenceGroupsInfo = groupList.toArray(
+ new ViewClassInfo[groupList.size()]);
+
+ Map<String, DeclareStyleableInfo> xmlMenuMap = collectMenuDefinitions(attrsXmlParser);
+ Map<String, DeclareStyleableInfo> xmlSearchableMap = collectSearchableDefinitions(
+ attrsXmlParser);
+ Map<String, DeclareStyleableInfo> manifestMap = collectManifestDefinitions(
+ attrsManifestXmlParser);
+ Map<String, Map<String, Integer>> enumValueMap = attrsXmlParser.getEnumFlagValues();
+
+ Map<String, DeclareStyleableInfo> xmlGadgetMap = null;
+ if (mAndroidTarget.getApiVersionNumber() >= 3) {
+ xmlGadgetMap = collectGadgetDefinitions(attrsXmlParser);
+ }
+
+ if (progress.isCanceled()) {
+ return Status.CANCEL_STATUS;
+ }
+
+ // From the information that was collected, create the pieces that will be put in
+ // the PlatformData object.
+ AndroidManifestDescriptors manifestDescriptors = new AndroidManifestDescriptors();
+ manifestDescriptors.updateDescriptors(manifestMap);
+ progress.worked(1);
+
+ if (progress.isCanceled()) {
+ return Status.CANCEL_STATUS;
+ }
+
+ LayoutDescriptors layoutDescriptors = new LayoutDescriptors();
+ layoutDescriptors.updateDescriptors(layoutViewsInfo, layoutGroupsInfo);
+ progress.worked(1);
+
+ if (progress.isCanceled()) {
+ return Status.CANCEL_STATUS;
+ }
+
+ MenuDescriptors menuDescriptors = new MenuDescriptors();
+ menuDescriptors.updateDescriptors(xmlMenuMap);
+ progress.worked(1);
+
+ if (progress.isCanceled()) {
+ return Status.CANCEL_STATUS;
+ }
+
+ XmlDescriptors xmlDescriptors = new XmlDescriptors();
+ xmlDescriptors.updateDescriptors(
+ xmlSearchableMap,
+ xmlGadgetMap,
+ preferencesInfo,
+ preferenceGroupsInfo);
+ progress.worked(1);
+
+ // load the framework resources.
+ ProjectResources resources = ResourceManager.getInstance().loadFrameworkResources(
+ mAndroidTarget);
+ progress.worked(1);
+
+ // now load the layout lib bridge
+ LayoutBridge layoutBridge = loadLayoutBridge();
+ progress.worked(1);
+
+ // and finally create the PlatformData with all that we loaded.
+ targetData.setExtraData(frameworkRepository,
+ manifestDescriptors,
+ layoutDescriptors,
+ menuDescriptors,
+ xmlDescriptors,
+ enumValueMap,
+ permissionValues,
+ activity_actions.toArray(new String[activity_actions.size()]),
+ broadcast_actions.toArray(new String[broadcast_actions.size()]),
+ service_actions.toArray(new String[service_actions.size()]),
+ categories.toArray(new String[categories.size()]),
+ mAndroidTarget.getOptionalLibraries(),
+ resources,
+ layoutBridge);
+
+ Sdk.getCurrent().setTargetData(mAndroidTarget, targetData);
+
+ return Status.OK_STATUS;
+ } catch (Exception e) {
+ AdtPlugin.logAndPrintError(e, TAG, "SDK parser failed"); //$NON-NLS-1$
+ AdtPlugin.printToConsole("SDK parser failed", e.getMessage());
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, "SDK parser failed", e);
+ }
+ }
+
+ /**
+ * Preloads all "interesting" classes from the framework SDK jar.
+ * <p/>
+ * Currently this preloads all classes from the framework jar
+ *
+ * @param classLoader The framework SDK jar classloader
+ * @param monitor A progress monitor. Can be null. Caller is responsible for calling done.
+ */
+ private void preload(AndroidJarLoader classLoader, IProgressMonitor monitor) {
+ try {
+ classLoader.preLoadClasses("" /* all classes */, //$NON-NLS-1$
+ mAndroidTarget.getName(), // monitor task label
+ monitor);
+ } catch (InvalidAttributeValueException e) {
+ AdtPlugin.log(e, "Problem preloading classes"); //$NON-NLS-1$
+ } catch (IOException e) {
+ AdtPlugin.log(e, "Problem preloading classes"); //$NON-NLS-1$
+ }
+ }
+
+ /**
+ * Creates an IResourceRepository for the framework resources.
+ *
+ * @param classLoader The framework SDK jar classloader
+ * @return a map of the resources, or null if it failed.
+ */
+ private IResourceRepository collectResourceIds(
+ AndroidJarLoader classLoader) {
+ try {
+ Class<?> r = classLoader.loadClass(AndroidConstants.CLASS_R);
+
+ if (r != null) {
+ Map<ResourceType, List<ResourceItem>> map = parseRClass(r);
+ if (map != null) {
+ return new FrameworkResourceRepository(map);
+ }
+ }
+ } catch (ClassNotFoundException e) {
+ AdtPlugin.logAndPrintError(e, TAG,
+ "Collect resource IDs failed, class %1$s not found in %2$s", //$NON-NLS-1$
+ AndroidConstants.CLASS_R,
+ mAndroidTarget.getPath(IAndroidTarget.ANDROID_JAR));
+ }
+
+ return null;
+ }
+
+ /**
+ * Parse the R class and build the resource map.
+ *
+ * @param rClass the Class object representing the Resources.
+ * @return a map of the resource or null
+ */
+ private Map<ResourceType, List<ResourceItem>> parseRClass(Class<?> rClass) {
+ // get the sub classes.
+ Class<?>[] classes = rClass.getClasses();
+
+ if (classes.length > 0) {
+ HashMap<ResourceType, List<ResourceItem>> map =
+ new HashMap<ResourceType, List<ResourceItem>>();
+
+ // get the fields of each class.
+ for (int c = 0 ; c < classes.length ; c++) {
+ Class<?> subClass = classes[c];
+ String name = subClass.getSimpleName();
+
+ // get the matching ResourceType
+ ResourceType type = ResourceType.getEnum(name);
+ if (type != null) {
+ List<ResourceItem> list = new ArrayList<ResourceItem>();
+ map.put(type, list);
+
+ Field[] fields = subClass.getFields();
+
+ for (Field f : fields) {
+ list.add(new ResourceItem(f.getName()));
+ }
+ }
+ }
+
+ return map;
+ }
+
+ return null;
+ }
+
+ /**
+ * Loads, collects and returns the list of default permissions from the framework.
+ *
+ * @param classLoader The framework SDK jar classloader
+ * @return a non null (but possibly empty) array containing the permission values.
+ */
+ private String[] collectPermissions(AndroidJarLoader classLoader) {
+ try {
+ Class<?> permissionClass =
+ classLoader.loadClass(AndroidConstants.CLASS_MANIFEST_PERMISSION);
+
+ if (permissionClass != null) {
+ ArrayList<String> list = new ArrayList<String>();
+
+ Field[] fields = permissionClass.getFields();
+
+ for (Field f : fields) {
+ int modifiers = f.getModifiers();
+ if (Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers) &&
+ Modifier.isPublic(modifiers)) {
+ try {
+ Object value = f.get(null);
+ if (value instanceof String) {
+ list.add((String)value);
+ }
+ } catch (IllegalArgumentException e) {
+ // since we provide null this should not happen
+ } catch (IllegalAccessException e) {
+ // if the field is inaccessible we ignore it.
+ } catch (NullPointerException npe) {
+ // looks like this is not a static field. we can ignore.
+ } catch (ExceptionInInitializerError eiie) {
+ // lets just ignore the field again
+ }
+ }
+ }
+
+ return list.toArray(new String[list.size()]);
+ }
+ } catch (ClassNotFoundException e) {
+ AdtPlugin.logAndPrintError(e, TAG,
+ "Collect permissions failed, class %1$s not found in %2$s", //$NON-NLS-1$
+ AndroidConstants.CLASS_MANIFEST_PERMISSION,
+ mAndroidTarget.getPath(IAndroidTarget.ANDROID_JAR));
+ }
+
+ return new String[0];
+ }
+
+ /**
+ * Loads and collects the action and category default values from the framework.
+ * The values are added to the <code>actions</code> and <code>categories</code> lists.
+ *
+ * @param activityActions the list which will receive the activity action values.
+ * @param broadcastActions the list which will receive the broadcast action values.
+ * @param serviceActions the list which will receive the service action values.
+ * @param categories the list which will receive the category values.
+ */
+ private void collectIntentFilterActionsAndCategories(ArrayList<String> activityActions,
+ ArrayList<String> broadcastActions,
+ ArrayList<String> serviceActions, ArrayList<String> categories) {
+ collectValues(mAndroidTarget.getPath(IAndroidTarget.ACTIONS_ACTIVITY),
+ activityActions);
+ collectValues(mAndroidTarget.getPath(IAndroidTarget.ACTIONS_BROADCAST),
+ broadcastActions);
+ collectValues(mAndroidTarget.getPath(IAndroidTarget.ACTIONS_SERVICE),
+ serviceActions);
+ collectValues(mAndroidTarget.getPath(IAndroidTarget.CATEGORIES),
+ categories);
+ }
+
+ /**
+ * Collects values from a text file located in the SDK
+ * @param osFilePath The path to the text file.
+ * @param values the {@link ArrayList} to fill with the values.
+ */
+ private void collectValues(String osFilePath, ArrayList<String> values) {
+ FileReader fr = null;
+ BufferedReader reader = null;
+ try {
+ fr = new FileReader(osFilePath);
+ reader = new BufferedReader(fr);
+
+ String line;
+ while ((line = reader.readLine()) != null) {
+ line = line.trim();
+ if (line.length() > 0 && line.startsWith("#") == false) { //$NON-NLS-1$
+ values.add(line);
+ }
+ }
+ } catch (IOException e) {
+ AdtPlugin.log(e, "Failed to read SDK values"); //$NON-NLS-1$
+ } finally {
+ try {
+ if (reader != null) {
+ reader.close();
+ }
+ } catch (IOException e) {
+ AdtPlugin.log(e, "Failed to read SDK values"); //$NON-NLS-1$
+ }
+
+ try {
+ if (fr != null) {
+ fr.close();
+ }
+ } catch (IOException e) {
+ AdtPlugin.log(e, "Failed to read SDK values"); //$NON-NLS-1$
+ }
+ }
+ }
+
+ /**
+ * Collects all layout classes information from the class loader and the
+ * attrs.xml and sets the corresponding structures in the resource manager.
+ *
+ * @param classLoader The framework SDK jar classloader in case we cannot get the widget from
+ * the platform directly
+ * @param attrsXmlParser The parser of the attrs.xml file
+ * @param mainList the Collection to receive the main list of {@link ViewClassInfo}.
+ * @param groupList the Collection to receive the group list of {@link ViewClassInfo}.
+ * @param monitor A progress monitor. Can be null. Caller is responsible for calling done.
+ */
+ private void collectLayoutClasses(AndroidJarLoader classLoader,
+ AttrsXmlParser attrsXmlParser,
+ Collection<ViewClassInfo> mainList, Collection<ViewClassInfo> groupList,
+ IProgressMonitor monitor) {
+ LayoutParamsParser ldp = null;
+ try {
+ WidgetClassLoader loader = new WidgetClassLoader(
+ mAndroidTarget.getPath(IAndroidTarget.WIDGETS));
+ if (loader.parseWidgetList(monitor)) {
+ ldp = new LayoutParamsParser(loader, attrsXmlParser);
+ }
+ // if the parsing failed, we'll use the old loader below.
+ } catch (FileNotFoundException e) {
+ AdtPlugin.log(e, "Android Framework Parser"); //$NON-NLS-1$
+ // the file does not exist, we'll use the old loader below.
+ }
+
+ if (ldp == null) {
+ ldp = new LayoutParamsParser(classLoader, attrsXmlParser);
+ }
+ ldp.parseLayoutClasses(monitor);
+
+ List<ViewClassInfo> views = ldp.getViews();
+ List<ViewClassInfo> groups = ldp.getGroups();
+
+ if (views != null && groups != null) {
+ mainList.addAll(views);
+ groupList.addAll(groups);
+ }
+ }
+
+ /**
+ * Collects all preferences definition information from the attrs.xml and
+ * sets the corresponding structures in the resource manager.
+ *
+ * @param classLoader The framework SDK jar classloader
+ * @param attrsXmlParser The parser of the attrs.xml file
+ * @param mainList the Collection to receive the main list of {@link ViewClassInfo}.
+ * @param groupList the Collection to receive the group list of {@link ViewClassInfo}.
+ * @param monitor A progress monitor. Can be null. Caller is responsible for calling done.
+ */
+ private void collectPreferenceClasses(AndroidJarLoader classLoader,
+ AttrsXmlParser attrsXmlParser, Collection<ViewClassInfo> mainList,
+ Collection<ViewClassInfo> groupList, IProgressMonitor monitor) {
+ LayoutParamsParser ldp = new LayoutParamsParser(classLoader, attrsXmlParser);
+
+ try {
+ ldp.parsePreferencesClasses(monitor);
+
+ List<ViewClassInfo> prefs = ldp.getViews();
+ List<ViewClassInfo> groups = ldp.getGroups();
+
+ if (prefs != null && groups != null) {
+ mainList.addAll(prefs);
+ groupList.addAll(groups);
+ }
+ } catch (NoClassDefFoundError e) {
+ AdtPlugin.logAndPrintError(e, TAG,
+ "Collect preferences failed, class %1$s not found in %2$s",
+ e.getMessage(),
+ classLoader.getSource());
+ } catch (Throwable e) {
+ AdtPlugin.log(e, "Android Framework Parser: failed to collect preference classes"); //$NON-NLS-1$
+ AdtPlugin.printErrorToConsole("Android Framework Parser",
+ "failed to collect preference classes");
+ }
+ }
+
+ /**
+ * Collects all menu definition information from the attrs.xml and returns it.
+ *
+ * @param attrsXmlParser The parser of the attrs.xml file
+ */
+ private Map<String, DeclareStyleableInfo> collectMenuDefinitions(
+ AttrsXmlParser attrsXmlParser) {
+ Map<String, DeclareStyleableInfo> map = attrsXmlParser.getDeclareStyleableList();
+ Map<String, DeclareStyleableInfo> map2 = new HashMap<String, DeclareStyleableInfo>();
+ for (String key : new String[] { "Menu", //$NON-NLS-1$
+ "MenuItem", //$NON-NLS-1$
+ "MenuGroup" }) { //$NON-NLS-1$
+ if (map.containsKey(key)) {
+ map2.put(key, map.get(key));
+ } else {
+ AdtPlugin.log(IStatus.WARNING,
+ "Menu declare-styleable %1$s not found in file %2$s", //$NON-NLS-1$
+ key, attrsXmlParser.getOsAttrsXmlPath());
+ AdtPlugin.printErrorToConsole("Android Framework Parser",
+ String.format("Menu declare-styleable %1$s not found in file %2$s", //$NON-NLS-1$
+ key, attrsXmlParser.getOsAttrsXmlPath()));
+ }
+ }
+
+ return Collections.unmodifiableMap(map2);
+ }
+
+ /**
+ * Collects all searchable definition information from the attrs.xml and returns it.
+ *
+ * @param attrsXmlParser The parser of the attrs.xml file
+ */
+ private Map<String, DeclareStyleableInfo> collectSearchableDefinitions(
+ AttrsXmlParser attrsXmlParser) {
+ Map<String, DeclareStyleableInfo> map = attrsXmlParser.getDeclareStyleableList();
+ Map<String, DeclareStyleableInfo> map2 = new HashMap<String, DeclareStyleableInfo>();
+ for (String key : new String[] { "Searchable", //$NON-NLS-1$
+ "SearchableActionKey" }) { //$NON-NLS-1$
+ if (map.containsKey(key)) {
+ map2.put(key, map.get(key));
+ } else {
+ AdtPlugin.log(IStatus.WARNING,
+ "Searchable declare-styleable %1$s not found in file %2$s", //$NON-NLS-1$
+ key, attrsXmlParser.getOsAttrsXmlPath());
+ AdtPlugin.printErrorToConsole("Android Framework Parser",
+ String.format("Searchable declare-styleable %1$s not found in file %2$s", //$NON-NLS-1$
+ key, attrsXmlParser.getOsAttrsXmlPath()));
+ }
+ }
+
+ return Collections.unmodifiableMap(map2);
+ }
+
+ /**
+ * Collects all gadgetProviderInfo definition information from the attrs.xml and returns it.
+ *
+ * @param attrsXmlParser The parser of the attrs.xml file
+ */
+ private Map<String, DeclareStyleableInfo> collectGadgetDefinitions(
+ AttrsXmlParser attrsXmlParser) {
+ Map<String, DeclareStyleableInfo> map = attrsXmlParser.getDeclareStyleableList();
+ Map<String, DeclareStyleableInfo> map2 = new HashMap<String, DeclareStyleableInfo>();
+ for (String key : new String[] { "GadgetProviderInfo" }) { //$NON-NLS-1$
+ if (map.containsKey(key)) {
+ map2.put(key, map.get(key));
+ } else {
+ AdtPlugin.log(IStatus.WARNING,
+ "Gadget declare-styleable %1$s not found in file %2$s", //$NON-NLS-1$
+ key, attrsXmlParser.getOsAttrsXmlPath());
+ AdtPlugin.printErrorToConsole("Android Framework Parser",
+ String.format("Gadget declare-styleable %1$s not found in file %2$s", //$NON-NLS-1$
+ key, attrsXmlParser.getOsAttrsXmlPath()));
+ }
+ }
+
+ return Collections.unmodifiableMap(map2);
+ }
+
+ /**
+ * Collects all manifest definition information from the attrs_manifest.xml and returns it.
+ */
+ private Map<String, DeclareStyleableInfo> collectManifestDefinitions(
+ AttrsXmlParser attrsXmlParser) {
+
+ return attrsXmlParser.getDeclareStyleableList();
+ }
+
+ /**
+ * Loads the layout bridge from the dynamically loaded layoutlib.jar
+ */
+ private LayoutBridge loadLayoutBridge() {
+ LayoutBridge layoutBridge = new LayoutBridge();
+
+ try {
+ // get the URL for the file.
+ File f = new File(mAndroidTarget.getPath(IAndroidTarget.LAYOUT_LIB));
+ if (f.isFile() == false) {
+ AdtPlugin.log(IStatus.ERROR, "layoutlib.jar is missing!"); //$NON-NLS-1$
+ } else {
+ URL url = f.toURL();
+
+ // create a class loader. Because this jar reference interfaces
+ // that are in the editors plugin, it's important to provide
+ // a parent class loader.
+ layoutBridge.classLoader = new URLClassLoader(new URL[] { url },
+ this.getClass().getClassLoader());
+
+ // load the class
+ Class<?> clazz = layoutBridge.classLoader.loadClass(AndroidConstants.CLASS_BRIDGE);
+ if (clazz != null) {
+ // instantiate an object of the class.
+ Constructor<?> constructor = clazz.getConstructor();
+ if (constructor != null) {
+ Object bridge = constructor.newInstance();
+ if (bridge instanceof ILayoutBridge) {
+ layoutBridge.bridge = (ILayoutBridge)bridge;
+ }
+ }
+ }
+
+ if (layoutBridge.bridge == null) {
+ layoutBridge.status = LoadStatus.FAILED;
+ AdtPlugin.log(IStatus.ERROR, "Failed to load " + AndroidConstants.CLASS_BRIDGE); //$NON-NLS-1$
+ } else {
+ // get the api level
+ try {
+ layoutBridge.apiLevel = layoutBridge.bridge.getApiLevel();
+ } catch (AbstractMethodError e) {
+ // the first version of the api did not have this method
+ layoutBridge.apiLevel = 1;
+ }
+
+ // and mark the lib as loaded.
+ layoutBridge.status = LoadStatus.LOADED;
+ }
+ }
+ } catch (Throwable t) {
+ layoutBridge.status = LoadStatus.FAILED;
+ // log the error.
+ AdtPlugin.log(t, "Failed to load the LayoutLib");
+ }
+
+ return layoutBridge;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/FrameworkResourceRepository.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/FrameworkResourceRepository.java
new file mode 100644
index 0000000..f4b10df
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/FrameworkResourceRepository.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.sdk;
+
+import com.android.ide.eclipse.common.resources.IResourceRepository;
+import com.android.ide.eclipse.common.resources.ResourceItem;
+import com.android.ide.eclipse.common.resources.ResourceType;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Implementation of the {@link IResourceRepository} interface to hold the system resource Ids
+ * parsed by {@link AndroidTargetParser}.
+ */
+final class FrameworkResourceRepository implements IResourceRepository {
+
+ private Map<ResourceType, List<ResourceItem>> mResourcesMap;
+
+ public FrameworkResourceRepository(Map<ResourceType, List<ResourceItem>> systemResourcesMap) {
+ mResourcesMap = systemResourcesMap;
+ }
+
+ public ResourceType[] getAvailableResourceTypes() {
+ if (mResourcesMap != null) {
+ Set<ResourceType> types = mResourcesMap.keySet();
+
+ if (types != null) {
+ return types.toArray(new ResourceType[types.size()]);
+ }
+ }
+
+ return null;
+ }
+
+ public ResourceItem[] getResources(ResourceType type) {
+ if (mResourcesMap != null) {
+ List<ResourceItem> items = mResourcesMap.get(type);
+
+ if (items != null) {
+ return items.toArray(new ResourceItem[items.size()]);
+ }
+ }
+
+ return null;
+ }
+
+ public boolean hasResources(ResourceType type) {
+ if (mResourcesMap != null) {
+ List<ResourceItem> items = mResourcesMap.get(type);
+
+ return (items != null && items.size() > 0);
+ }
+
+ return false;
+ }
+
+ public boolean isSystemRepository() {
+ return true;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/IAndroidClassLoader.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/IAndroidClassLoader.java
new file mode 100644
index 0000000..35057d1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/IAndroidClassLoader.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.sdk;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import javax.management.InvalidAttributeValueException;
+
+/**
+ * Classes which implements this interface provide methods to access framework resource
+ * data loaded from the SDK.
+ */
+public interface IAndroidClassLoader {
+
+ /**
+ * Classes which implement this interface provide methods to describe a class.
+ */
+ public interface IClassDescriptor {
+
+ String getCanonicalName();
+
+ IClassDescriptor getSuperclass();
+
+ String getSimpleName();
+
+ IClassDescriptor getEnclosingClass();
+
+ IClassDescriptor[] getDeclaredClasses();
+
+ boolean isInstantiable();
+ }
+
+ /**
+ * Finds and loads all classes that derive from a given set of super classes.
+ *
+ * @param rootPackage Root package of classes to find. Use an empty string to find everyting.
+ * @param superClasses The super classes of all the classes to find.
+ * @return An hash map which keys are the super classes looked for and which values are
+ * ArrayList of the classes found. The array lists are always created for all the
+ * valid keys, they are simply empty if no deriving class is found for a given
+ * super class.
+ * @throws IOException
+ * @throws InvalidAttributeValueException
+ * @throws ClassFormatError
+ */
+ public HashMap<String, ArrayList<IClassDescriptor>> findClassesDerivingFrom(
+ String rootPackage, String[] superClasses)
+ throws IOException, InvalidAttributeValueException, ClassFormatError;
+
+ /**
+ * Returns a {@link IClassDescriptor} by its fully-qualified name.
+ * @param className the fully-qualified name of the class to return.
+ * @throws ClassNotFoundException
+ */
+ public IClassDescriptor getClass(String className) throws ClassNotFoundException;
+
+ /**
+ * Returns a string indicating the source of the classes, typically for debugging
+ * or in error messages. This would typically be a JAR file name or some kind of
+ * identifier that would mean something to the user when looking at error messages.
+ *
+ * @return An informal string representing the source of the classes.
+ */
+ public String getSource();
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/LayoutParamsParser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/LayoutParamsParser.java
new file mode 100644
index 0000000..dc600d7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/LayoutParamsParser.java
@@ -0,0 +1,372 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.sdk;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.sdk.IAndroidClassLoader.IClassDescriptor;
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.common.resources.AttrsXmlParser;
+import com.android.ide.eclipse.common.resources.ViewClassInfo;
+import com.android.ide.eclipse.common.resources.ViewClassInfo.LayoutParamsInfo;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.SubMonitor;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import javax.management.InvalidAttributeValueException;
+
+/*
+ * TODO: refactor this. Could use some cleanup.
+ */
+
+/**
+ * Parser for the framework library.
+ * <p/>
+ * This gather the following information:
+ * <ul>
+ * <li>Resource ID from <code>android.R</code></li>
+ * <li>The list of permissions values from <code>android.Manifest$permission</code></li>
+ * <li></li>
+ * </ul>
+ */
+public class LayoutParamsParser {
+
+ /**
+ * Class extending {@link ViewClassInfo} by adding the notion of instantiability.
+ * {@link LayoutParamsParser#getViews()} and {@link LayoutParamsParser#getGroups()} should
+ * only return classes that can be instantiated.
+ */
+ final static class ExtViewClassInfo extends ViewClassInfo {
+
+ private boolean mIsInstantiable;
+
+ ExtViewClassInfo(boolean instantiable, boolean isLayout, String canonicalClassName,
+ String shortClassName) {
+ super(isLayout, canonicalClassName, shortClassName);
+ mIsInstantiable = instantiable;
+ }
+
+ boolean isInstantiable() {
+ return mIsInstantiable;
+ }
+ }
+
+ /* Note: protected members/methods are overridden in unit tests */
+
+ /** Reference to android.view.View */
+ protected IClassDescriptor mTopViewClass;
+ /** Reference to android.view.ViewGroup */
+ protected IClassDescriptor mTopGroupClass;
+ /** Reference to android.view.ViewGroup$LayoutParams */
+ protected IClassDescriptor mTopLayoutParamsClass;
+
+ /** Input list of all classes deriving from android.view.View */
+ protected ArrayList<IClassDescriptor> mViewList;
+ /** Input list of all classes deriving from android.view.ViewGroup */
+ protected ArrayList<IClassDescriptor> mGroupList;
+
+ /** Output map of FQCN => info on View classes */
+ protected TreeMap<String, ExtViewClassInfo> mViewMap;
+ /** Output map of FQCN => info on ViewGroup classes */
+ protected TreeMap<String, ExtViewClassInfo> mGroupMap;
+ /** Output map of FQCN => info on LayoutParams classes */
+ protected HashMap<String, LayoutParamsInfo> mLayoutParamsMap;
+
+ /** The attrs.xml parser */
+ protected AttrsXmlParser mAttrsXmlParser;
+
+ /** The android.jar class loader */
+ protected IAndroidClassLoader mClassLoader;
+
+ /**
+ * Instantiate a new LayoutParamsParser.
+ * @param classLoader The android.jar class loader
+ * @param attrsXmlParser The parser of the attrs.xml file
+ */
+ public LayoutParamsParser(IAndroidClassLoader classLoader,
+ AttrsXmlParser attrsXmlParser) {
+ mClassLoader = classLoader;
+ mAttrsXmlParser = attrsXmlParser;
+ }
+
+ /** Returns the map of FQCN => info on View classes */
+ public List<ViewClassInfo> getViews() {
+ return getInstantiables(mViewMap);
+ }
+
+ /** Returns the map of FQCN => info on ViewGroup classes */
+ public List<ViewClassInfo> getGroups() {
+ return getInstantiables(mGroupMap);
+ }
+
+ /**
+ * TODO: doc here.
+ * <p/>
+ * Note: on output we should have NO dependency on {@link IClassDescriptor},
+ * otherwise we wouldn't be able to unload the class loader later.
+ * <p/>
+ * Note on Vocabulary: FQCN=Fully Qualified Class Name (e.g. "my.package.class$innerClass")
+ * @param monitor A progress monitor. Can be null. Caller is responsible for calling done.
+ */
+ public void parseLayoutClasses(IProgressMonitor monitor) {
+ parseClasses(monitor,
+ AndroidConstants.CLASS_VIEW,
+ AndroidConstants.CLASS_VIEWGROUP,
+ AndroidConstants.CLASS_VIEWGROUP_LAYOUTPARAMS);
+ }
+
+ public void parsePreferencesClasses(IProgressMonitor monitor) {
+ parseClasses(monitor,
+ AndroidConstants.CLASS_PREFERENCE,
+ AndroidConstants.CLASS_PREFERENCEGROUP,
+ null /* paramsClassName */ );
+ }
+
+ private void parseClasses(IProgressMonitor monitor,
+ String rootClassName,
+ String groupClassName,
+ String paramsClassName) {
+ try {
+ SubMonitor progress = SubMonitor.convert(monitor, 100);
+
+ String[] superClasses = new String[2 + (paramsClassName == null ? 0 : 1)];
+ superClasses[0] = groupClassName;
+ superClasses[1] = rootClassName;
+ if (paramsClassName != null) {
+ superClasses[2] = paramsClassName;
+ }
+ HashMap<String, ArrayList<IClassDescriptor>> found =
+ mClassLoader.findClassesDerivingFrom("android.", superClasses);
+ mTopViewClass = mClassLoader.getClass(rootClassName);
+ mTopGroupClass = mClassLoader.getClass(groupClassName);
+ if (paramsClassName != null) {
+ mTopLayoutParamsClass = mClassLoader.getClass(paramsClassName);
+ }
+
+ mViewList = found.get(rootClassName);
+ mGroupList = found.get(groupClassName);
+
+ mViewMap = new TreeMap<String, ExtViewClassInfo>();
+ mGroupMap = new TreeMap<String, ExtViewClassInfo>();
+ if (mTopLayoutParamsClass != null) {
+ mLayoutParamsMap = new HashMap<String, LayoutParamsInfo>();
+ }
+
+ // Add top classes to the maps since by design they are not listed in classes deriving
+ // from themselves.
+ addGroup(mTopGroupClass);
+ addView(mTopViewClass);
+
+ // ViewGroup derives from View
+ mGroupMap.get(groupClassName).setSuperClass(
+ mViewMap.get(rootClassName));
+
+ progress.setWorkRemaining(mGroupList.size() + mViewList.size());
+
+ for (IClassDescriptor groupChild : mGroupList) {
+ addGroup(groupChild);
+ progress.worked(1);
+ }
+
+ for (IClassDescriptor viewChild : mViewList) {
+ if (viewChild != mTopGroupClass) {
+ addView(viewChild);
+ }
+ progress.worked(1);
+ }
+ } catch (ClassNotFoundException e) {
+ AdtPlugin.log(e, "Problem loading class %1$s or %2$s", //$NON-NLS-1$
+ rootClassName, groupClassName);
+ } catch (InvalidAttributeValueException e) {
+ AdtPlugin.log(e, "Problem loading classes"); //$NON-NLS-1$
+ } catch (ClassFormatError e) {
+ AdtPlugin.log(e, "Problem loading classes"); //$NON-NLS-1$
+ } catch (IOException e) {
+ AdtPlugin.log(e, "Problem loading classes"); //$NON-NLS-1$
+ }
+ }
+
+ /**
+ * Parses a View class and adds a ExtViewClassInfo for it in mViewMap.
+ * It calls itself recursively to handle super classes which are also Views.
+ */
+ private ExtViewClassInfo addView(IClassDescriptor viewClass) {
+ String fqcn = viewClass.getCanonicalName();
+ if (mViewMap.containsKey(fqcn)) {
+ return mViewMap.get(fqcn);
+ } else if (mGroupMap.containsKey(fqcn)) {
+ return mGroupMap.get(fqcn);
+ }
+
+ ExtViewClassInfo info = new ExtViewClassInfo(viewClass.isInstantiable(),
+ false /* layout */, fqcn, viewClass.getSimpleName());
+ mViewMap.put(fqcn, info);
+
+ // All view classes derive from mTopViewClass by design.
+ // Do not lookup the super class for mTopViewClass itself.
+ if (viewClass.equals(mTopViewClass) == false) {
+ IClassDescriptor superClass = viewClass.getSuperclass();
+ ExtViewClassInfo superClassInfo = addView(superClass);
+ info.setSuperClass(superClassInfo);
+ }
+
+ mAttrsXmlParser.loadViewAttributes(info);
+ return info;
+ }
+
+ /**
+ * Parses a ViewGroup class and adds a ExtViewClassInfo for it in mGroupMap.
+ * It calls itself recursively to handle super classes which are also ViewGroups.
+ */
+ private ExtViewClassInfo addGroup(IClassDescriptor groupClass) {
+ String fqcn = groupClass.getCanonicalName();
+ if (mGroupMap.containsKey(fqcn)) {
+ return mGroupMap.get(fqcn);
+ }
+
+ ExtViewClassInfo info = new ExtViewClassInfo(groupClass.isInstantiable(),
+ true /* layout */, fqcn, groupClass.getSimpleName());
+ mGroupMap.put(fqcn, info);
+
+ // All groups derive from android.view.ViewGroup, which in turns derives from
+ // android.view.View (i.e. mTopViewClass here). So the only group that can have View as
+ // its super class is the ViewGroup base class and we don't try to resolve it since groups
+ // are loaded before views.
+ IClassDescriptor superClass = groupClass.getSuperclass();
+
+ // Assertion: at this point, we should have
+ // superClass != mTopViewClass || fqcn.equals(AndroidConstants.CLASS_VIEWGROUP);
+
+ if (superClass != null && superClass.equals(mTopViewClass) == false) {
+ ExtViewClassInfo superClassInfo = addGroup(superClass);
+
+ // Assertion: we should have superClassInfo != null && superClassInfo != info;
+ if (superClassInfo != null && superClassInfo != info) {
+ info.setSuperClass(superClassInfo);
+ }
+ }
+
+ mAttrsXmlParser.loadViewAttributes(info);
+ if (mTopLayoutParamsClass != null) {
+ info.setLayoutParams(addLayoutParams(groupClass));
+ }
+ return info;
+ }
+
+ /**
+ * Parses a ViewGroup class and returns an info object on its inner LayoutParams.
+ *
+ * @return The {@link LayoutParamsInfo} for the ViewGroup class or null.
+ */
+ private LayoutParamsInfo addLayoutParams(IClassDescriptor groupClass) {
+
+ // Is there a LayoutParams in this group class?
+ IClassDescriptor layoutParamsClass = findLayoutParams(groupClass);
+
+ // if there's no layout data in the group class, link to the one from the
+ // super class.
+ if (layoutParamsClass == null) {
+ for (IClassDescriptor superClass = groupClass.getSuperclass();
+ layoutParamsClass == null &&
+ superClass != null &&
+ superClass.equals(mTopViewClass) == false;
+ superClass = superClass.getSuperclass()) {
+ layoutParamsClass = findLayoutParams(superClass);
+ }
+ }
+
+ if (layoutParamsClass != null) {
+ return getLayoutParamsInfo(layoutParamsClass);
+ }
+
+ return null;
+ }
+
+ /**
+ * Parses a LayoutParams class and returns a LayoutParamsInfo object for it.
+ * It calls itself recursively to handle the super class of the LayoutParams.
+ */
+ private LayoutParamsInfo getLayoutParamsInfo(IClassDescriptor layoutParamsClass) {
+ String fqcn = layoutParamsClass.getCanonicalName();
+ LayoutParamsInfo layoutParamsInfo = mLayoutParamsMap.get(fqcn);
+
+ if (layoutParamsInfo != null) {
+ return layoutParamsInfo;
+ }
+
+ // Find the link on the LayoutParams super class
+ LayoutParamsInfo superClassInfo = null;
+ if (layoutParamsClass.equals(mTopLayoutParamsClass) == false) {
+ IClassDescriptor superClass = layoutParamsClass.getSuperclass();
+ superClassInfo = getLayoutParamsInfo(superClass);
+ }
+
+ // Find the link on the enclosing ViewGroup
+ ExtViewClassInfo enclosingGroupInfo = addGroup(layoutParamsClass.getEnclosingClass());
+
+ layoutParamsInfo = new ExtViewClassInfo.LayoutParamsInfo(
+ enclosingGroupInfo, layoutParamsClass.getSimpleName(), superClassInfo);
+ mLayoutParamsMap.put(fqcn, layoutParamsInfo);
+
+ mAttrsXmlParser.loadLayoutParamsAttributes(layoutParamsInfo);
+
+ return layoutParamsInfo;
+ }
+
+ /**
+ * Given a ViewGroup-derived class, looks for an inner class named LayoutParams
+ * and if found returns its class definition.
+ * <p/>
+ * This uses the actual defined inner classes and does not look at inherited classes.
+ *
+ * @param groupClass The ViewGroup derived class
+ * @return The Class of the inner LayoutParams or null if none is declared.
+ */
+ private IClassDescriptor findLayoutParams(IClassDescriptor groupClass) {
+ IClassDescriptor[] innerClasses = groupClass.getDeclaredClasses();
+ for (IClassDescriptor innerClass : innerClasses) {
+ if (innerClass.getSimpleName().equals(AndroidConstants.CLASS_LAYOUTPARAMS)) {
+ return innerClass;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Computes and return a list of ViewClassInfo from a map by filtering out the class that
+ * cannot be instantiated.
+ */
+ private List<ViewClassInfo> getInstantiables(SortedMap<String, ExtViewClassInfo> map) {
+ Collection<ExtViewClassInfo> values = map.values();
+ ArrayList<ViewClassInfo> list = new ArrayList<ViewClassInfo>();
+
+ for (ExtViewClassInfo info : values) {
+ if (info.isInstantiable()) {
+ list.add(info);
+ }
+ }
+
+ return list;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/LoadStatus.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/LoadStatus.java
new file mode 100644
index 0000000..6bf0272
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/LoadStatus.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.sdk;
+
+/**
+ * Enum for loading status of various SDK parts.
+ */
+public enum LoadStatus {
+ LOADING, LOADED, FAILED;
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/Sdk.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/Sdk.java
new file mode 100644
index 0000000..ba0b568
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/Sdk.java
@@ -0,0 +1,492 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.sdk;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.project.internal.AndroidClasspathContainerInitializer;
+import com.android.ide.eclipse.adt.sdk.AndroidTargetData.LayoutBridge;
+import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor;
+import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IProjectListener;
+import com.android.prefs.AndroidLocation.AndroidLocationException;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.ISdkLog;
+import com.android.sdklib.SdkConstants;
+import com.android.sdklib.SdkManager;
+import com.android.sdklib.avd.AvdManager;
+import com.android.sdklib.project.ApkConfigurationHelper;
+import com.android.sdklib.project.ProjectProperties;
+import com.android.sdklib.project.ProjectProperties.PropertyType;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IncrementalProjectBuilder;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Central point to load, manipulate and deal with the Android SDK. Only one SDK can be used
+ * at the same time.
+ *
+ * To start using an SDK, call {@link #loadSdk(String)} which returns the instance of
+ * the Sdk object.
+ *
+ * To get the list of platforms or add-ons present in the SDK, call {@link #getTargets()}.
+ */
+public class Sdk implements IProjectListener {
+ private static Sdk sCurrentSdk = null;
+
+ private final SdkManager mManager;
+ private final AvdManager mAvdManager;
+
+ private final HashMap<IProject, IAndroidTarget> mProjectTargetMap =
+ new HashMap<IProject, IAndroidTarget>();
+ private final HashMap<IAndroidTarget, AndroidTargetData> mTargetDataMap =
+ new HashMap<IAndroidTarget, AndroidTargetData>();
+ private final HashMap<IProject, Map<String, String>> mProjectApkConfigMap =
+ new HashMap<IProject, Map<String, String>>();
+ private final String mDocBaseUrl;
+
+ /**
+ * Classes implementing this interface will receive notification when targets are changed.
+ */
+ public interface ITargetChangeListener {
+ /**
+ * Sent when project has its target changed.
+ */
+ void onProjectTargetChange(IProject changedProject);
+
+ /**
+ * Called when the targets are loaded (either the SDK finished loading when Eclipse starts,
+ * or the SDK is changed).
+ */
+ void onTargetsLoaded();
+ }
+
+ /**
+ * Loads an SDK and returns an {@link Sdk} object if success.
+ * @param sdkLocation the OS path to the SDK.
+ */
+ public static Sdk loadSdk(String sdkLocation) {
+ if (sCurrentSdk != null) {
+ sCurrentSdk.dispose();
+ sCurrentSdk = null;
+ }
+
+ final ArrayList<String> logMessages = new ArrayList<String>();
+ ISdkLog log = new ISdkLog() {
+ public void error(Throwable throwable, String errorFormat, Object... arg) {
+ if (errorFormat != null) {
+ logMessages.add(String.format(errorFormat, arg));
+ }
+
+ if (throwable != null) {
+ logMessages.add(throwable.getMessage());
+ }
+ }
+
+ public void warning(String warningFormat, Object... arg) {
+ logMessages.add(String.format(warningFormat, arg));
+ }
+
+ public void printf(String msgFormat, Object... arg) {
+ logMessages.add(String.format(msgFormat, arg));
+ }
+ };
+
+ // get an SdkManager object for the location
+ SdkManager manager = SdkManager.createManager(sdkLocation, log);
+ if (manager != null) {
+ AvdManager avdManager = null;
+ try {
+ avdManager = new AvdManager(manager, log);
+ } catch (AndroidLocationException e) {
+ log.error(e, "Error parsing the AVDs");
+ }
+ sCurrentSdk = new Sdk(manager, avdManager);
+ return sCurrentSdk;
+ } else {
+ StringBuilder sb = new StringBuilder("Error Loading the SDK:\n");
+ for (String msg : logMessages) {
+ sb.append('\n');
+ sb.append(msg);
+ }
+ AdtPlugin.displayError("Android SDK", sb.toString());
+ }
+ return null;
+ }
+
+ /**
+ * Returns the current {@link Sdk} object.
+ */
+ public static Sdk getCurrent() {
+ return sCurrentSdk;
+ }
+
+ /**
+ * Returns the location (OS path) of the current SDK.
+ */
+ public String getSdkLocation() {
+ return mManager.getLocation();
+ }
+
+ /**
+ * Returns the URL to the local documentation.
+ * Can return null if no documentation is found in the current SDK.
+ *
+ * @return A file:// URL on the local documentation folder if it exists or null.
+ */
+ public String getDocumentationBaseUrl() {
+ return mDocBaseUrl;
+ }
+
+ /**
+ * Returns the list of targets that are available in the SDK.
+ */
+ public IAndroidTarget[] getTargets() {
+ return mManager.getTargets();
+ }
+
+ /**
+ * 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.
+ */
+ public IAndroidTarget getTargetFromHashString(String hash) {
+ return mManager.getTargetFromHashString(hash);
+ }
+
+ /**
+ * Sets a new target and a new list of Apk configuration for a given project.
+ *
+ * @param project the project to receive the new apk configurations
+ * @param target The new target to set, or <code>null</code> to not change the current target.
+ * @param apkConfigMap a map of apk configurations. The map contains (name, filter) where name
+ * is the name of the configuration (a-zA-Z0-9 only), and filter is the comma separated list of
+ * resource configuration to include in the apk (see aapt -c). Can be <code>null</code> if the
+ * apk configurations should not be updated.
+ */
+ public void setProject(IProject project, IAndroidTarget target,
+ Map<String, String> apkConfigMap) {
+ synchronized (mProjectTargetMap) {
+ boolean resolveProject = false;
+ boolean compileProject = false;
+ boolean cleanProject = false;
+
+ ProjectProperties properties = ProjectProperties.load(
+ project.getLocation().toOSString(), PropertyType.DEFAULT);
+ if (properties == null) {
+ // doesn't exist yet? we create it.
+ properties = ProjectProperties.create(project.getLocation().toOSString(),
+ PropertyType.DEFAULT);
+ }
+
+ if (target != null) {
+ // look for the current target of the project
+ IAndroidTarget previousTarget = mProjectTargetMap.get(project);
+
+ if (target != previousTarget) {
+ // save the target hash string in the project persistent property
+ properties.setAndroidTarget(target);
+
+ // put it in a local map for easy access.
+ mProjectTargetMap.put(project, target);
+
+ resolveProject = true;
+ }
+ }
+
+ if (apkConfigMap != null) {
+ // save the apk configs in the project persistent property
+ cleanProject = ApkConfigurationHelper.setConfigs(properties, apkConfigMap);
+
+ // put it in a local map for easy access.
+ mProjectApkConfigMap.put(project, apkConfigMap);
+
+ compileProject = true;
+ }
+
+ // we are done with the modification. Save the property file.
+ try {
+ properties.save();
+ } catch (IOException e) {
+ AdtPlugin.log(e, "Failed to save default.properties for project '%s'",
+ project.getName());
+ }
+
+ if (resolveProject) {
+ // force a resolve of the project by updating the classpath container.
+ IJavaProject javaProject = JavaCore.create(project);
+ AndroidClasspathContainerInitializer.updateProjects(
+ new IJavaProject[] { javaProject });
+ } else if (compileProject) {
+ // If there was removed configs, we clean instead of build
+ // (to remove the obsolete ap_ and apk file from removed configs).
+ try {
+ project.build(cleanProject ?
+ IncrementalProjectBuilder.CLEAN_BUILD :
+ IncrementalProjectBuilder.FULL_BUILD,
+ null);
+ } catch (CoreException e) {
+ // failed to build? force resolve instead.
+ IJavaProject javaProject = JavaCore.create(project);
+ AndroidClasspathContainerInitializer.updateProjects(
+ new IJavaProject[] { javaProject });
+ }
+ }
+
+ // finally, update the opened editors.
+ if (resolveProject) {
+ AdtPlugin.getDefault().updateTargetListener(project);
+ }
+ }
+ }
+
+ /**
+ * Returns the {@link IAndroidTarget} object associated with the given {@link IProject}.
+ */
+ public IAndroidTarget getTarget(IProject project) {
+ synchronized (mProjectTargetMap) {
+ IAndroidTarget target = mProjectTargetMap.get(project);
+ if (target == null) {
+ // get the value from the project persistent property.
+ String targetHashString = loadProjectProperties(project, this);
+
+ if (targetHashString != null) {
+ target = mManager.getTargetFromHashString(targetHashString);
+ }
+ }
+
+ return target;
+ }
+ }
+
+
+ /**
+ * Parses the project properties and returns the hash string uniquely identifying the
+ * target of the given project.
+ * <p/>
+ * This methods reads the content of the <code>default.properties</code> file present in
+ * the root folder of the project.
+ * <p/>The returned string is equivalent to the return of {@link IAndroidTarget#hashString()}.
+ * @param project The project for which to return the target hash string.
+ * @param sdkStorage The sdk in which to store the Apk Configs. Can be null.
+ * @return the hash string or null if the project does not have a target set.
+ */
+ private static String loadProjectProperties(IProject project, Sdk sdkStorage) {
+ // load the default.properties from the project folder.
+ IPath location = project.getLocation();
+ if (location == null) { // can return null when the project is being deleted.
+ // do nothing and return null;
+ return null;
+ }
+ ProjectProperties properties = ProjectProperties.load(location.toOSString(),
+ PropertyType.DEFAULT);
+ if (properties == null) {
+ AdtPlugin.log(IStatus.ERROR, "Failed to load properties file for project '%s'",
+ project.getName());
+ return null;
+ }
+
+ if (sdkStorage != null) {
+ Map<String, String> configMap = ApkConfigurationHelper.getConfigs(properties);
+
+ if (configMap != null) {
+ sdkStorage.mProjectApkConfigMap.put(project, configMap);
+ }
+ }
+
+ return properties.getProperty(ProjectProperties.PROPERTY_TARGET);
+ }
+
+ /**
+ * Returns the hash string uniquely identifying the target of a project.
+ * <p/>
+ * This methods reads the content of the <code>default.properties</code> file present in
+ * the root folder of the project.
+ * <p/>The string is equivalent to the return of {@link IAndroidTarget#hashString()}.
+ * @param project The project for which to return the target hash string.
+ * @return the hash string or null if the project does not have a target set.
+ */
+ public static String getProjectTargetHashString(IProject project) {
+ return loadProjectProperties(project, null /*storeConfigs*/);
+ }
+
+ /**
+ * Sets a target hash string in given project's <code>default.properties</code> file.
+ * @param project The project in which to save the hash string.
+ * @param targetHashString The target hash string to save. This must be the result from
+ * {@link IAndroidTarget#hashString()}.
+ */
+ public static void setProjectTargetHashString(IProject project, String targetHashString) {
+ // because we don't want to erase other properties from default.properties, we first load
+ // them
+ ProjectProperties properties = ProjectProperties.load(project.getLocation().toOSString(),
+ PropertyType.DEFAULT);
+ if (properties == null) {
+ // doesn't exist yet? we create it.
+ properties = ProjectProperties.create(project.getLocation().toOSString(),
+ PropertyType.DEFAULT);
+ }
+
+ // add/change the target hash string.
+ properties.setProperty(ProjectProperties.PROPERTY_TARGET, targetHashString);
+
+ // and rewrite the file.
+ try {
+ properties.save();
+ } catch (IOException e) {
+ AdtPlugin.log(e, "Failed to save default.properties for project '%s'",
+ project.getName());
+ }
+ }
+ /**
+ * Return the {@link AndroidTargetData} for a given {@link IAndroidTarget}.
+ */
+ public AndroidTargetData getTargetData(IAndroidTarget target) {
+ synchronized (mTargetDataMap) {
+ return mTargetDataMap.get(target);
+ }
+ }
+
+ /**
+ * Returns the configuration map for a given project.
+ * <p/>The Map key are name to be used in the apk filename, while the values are comma separated
+ * config values. The config value can be passed directly to aapt through the -c option.
+ */
+ public Map<String, String> getProjectApkConfigs(IProject project) {
+ return mProjectApkConfigMap.get(project);
+ }
+
+ /**
+ * Returns the {@link AvdManager}. If the AvdManager failed to parse the AVD folder, this could
+ * be <code>null</code>.
+ */
+ public AvdManager getAvdManager() {
+ return mAvdManager;
+ }
+
+ private Sdk(SdkManager manager, AvdManager avdManager) {
+ mManager = manager;
+ mAvdManager = avdManager;
+
+ // listen to projects closing
+ ResourceMonitor monitor = ResourceMonitor.getMonitor();
+ monitor.addProjectListener(this);
+
+ // pre-compute some paths
+ mDocBaseUrl = getDocumentationBaseUrl(mManager.getLocation() +
+ SdkConstants.OS_SDK_DOCS_FOLDER);
+ }
+
+ /**
+ * Cleans and unloads the SDK.
+ */
+ private void dispose() {
+ ResourceMonitor.getMonitor().removeProjectListener(this);
+ }
+
+ void setTargetData(IAndroidTarget target, AndroidTargetData data) {
+ synchronized (mTargetDataMap) {
+ mTargetDataMap.put(target, data);
+ }
+ }
+
+ /**
+ * Returns the URL to the local documentation.
+ * Can return null if no documentation is found in the current SDK.
+ *
+ * @param osDocsPath Path to the documentation folder in the current SDK.
+ * The folder may not actually exist.
+ * @return A file:// URL on the local documentation folder if it exists or null.
+ */
+ private String getDocumentationBaseUrl(String osDocsPath) {
+ File f = new File(osDocsPath);
+
+ if (f.isDirectory()) {
+ try {
+ // Note: to create a file:// URL, one would typically use something like
+ // f.toURI().toURL().toString(). However this generates a broken path on
+ // Windows, namely "C:\\foo" is converted to "file:/C:/foo" instead of
+ // "file:///C:/foo" (i.e. there should be 3 / after "file:"). So we'll
+ // do the correct thing manually.
+
+ String path = f.getAbsolutePath();
+ if (File.separatorChar != '/') {
+ path = path.replace(File.separatorChar, '/');
+ }
+
+ // For some reason the URL class doesn't add the mandatory "//" after
+ // the "file:" protocol name, so it has to be hacked into the path.
+ URL url = new URL("file", null, "//" + path); //$NON-NLS-1$ //$NON-NLS-2$
+ String result = url.toString();
+ return result;
+ } catch (MalformedURLException e) {
+ // ignore malformed URLs
+ }
+ }
+
+ return null;
+ }
+
+ public void projectClosed(IProject project) {
+ // get the target project
+ synchronized (mProjectTargetMap) {
+ IAndroidTarget target = mProjectTargetMap.get(project);
+ if (target != null) {
+ // get the bridge for the target, and clear the cache for this project.
+ AndroidTargetData data = mTargetDataMap.get(target);
+ if (data != null) {
+ LayoutBridge bridge = data.getLayoutBridge();
+ if (bridge != null && bridge.status == LoadStatus.LOADED) {
+ bridge.bridge.clearCaches(project);
+ }
+ }
+ }
+
+ // now remove the project for the maps.
+ mProjectTargetMap.remove(project);
+ mProjectApkConfigMap.remove(project);
+ }
+ }
+
+ public void projectDeleted(IProject project) {
+ projectClosed(project);
+ }
+
+ public void projectOpened(IProject project) {
+ // ignore this. The project will be added to the map the first time the target needs
+ // to be resolved.
+ }
+
+ public void projectOpenedWithWorkspace(IProject project) {
+ // ignore this. The project will be added to the map the first time the target needs
+ // to be resolved.
+ }
+}
+
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/WidgetClassLoader.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/WidgetClassLoader.java
new file mode 100644
index 0000000..0e60f8a
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/sdk/WidgetClassLoader.java
@@ -0,0 +1,334 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.sdk;
+
+import com.android.ide.eclipse.common.AndroidConstants;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+
+import java.io.BufferedReader;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TreeMap;
+
+import javax.management.InvalidAttributeValueException;
+
+/**
+ * Parser for the text file containing the list of widgets, layouts and layout params.
+ * <p/>
+ * The file is a straight text file containing one class per line.<br>
+ * Each line is in the following format<br>
+ * <code>[code][class name] [super class name] [super class name]...</code>
+ * where code is a single letter (W for widget, L for layout, P for layout params), and class names
+ * are the fully qualified name of the classes.
+ */
+public final class WidgetClassLoader implements IAndroidClassLoader {
+
+ /**
+ * Basic class containing the class descriptions found in the text file.
+ */
+ private final static class ClassDescriptor implements IClassDescriptor {
+
+ private String mName;
+ private String mSimpleName;
+ private ClassDescriptor mSuperClass;
+ private ClassDescriptor mEnclosingClass;
+ private final ArrayList<IClassDescriptor> mDeclaredClasses =
+ new ArrayList<IClassDescriptor>();
+ private boolean mIsInstantiable = false;
+
+ ClassDescriptor(String fqcn) {
+ mName = fqcn;
+ mSimpleName = getSimpleName(fqcn);
+ }
+
+ public String getCanonicalName() {
+ return mName;
+ }
+
+ public String getSimpleName() {
+ return mSimpleName;
+ }
+
+ public IClassDescriptor[] getDeclaredClasses() {
+ return mDeclaredClasses.toArray(new IClassDescriptor[mDeclaredClasses.size()]);
+ }
+
+ private void addDeclaredClass(ClassDescriptor declaredClass) {
+ mDeclaredClasses.add(declaredClass);
+ }
+
+ public IClassDescriptor getEnclosingClass() {
+ return mEnclosingClass;
+ }
+
+ void setEnclosingClass(ClassDescriptor enclosingClass) {
+ // set the enclosing class.
+ mEnclosingClass = enclosingClass;
+
+ // add this to the list of declared class in the enclosing class.
+ mEnclosingClass.addDeclaredClass(this);
+
+ // finally change the name of declared class to make sure it uses the
+ // convention: package.enclosing$declared instead of package.enclosing.declared
+ mName = enclosingClass.mName + "$" + mName.substring(enclosingClass.mName.length() + 1);
+ }
+
+ public IClassDescriptor getSuperclass() {
+ return mSuperClass;
+ }
+
+ void setSuperClass(ClassDescriptor superClass) {
+ mSuperClass = superClass;
+ }
+
+ @Override
+ public boolean equals(Object clazz) {
+ if (clazz instanceof ClassDescriptor) {
+ return mName.equals(((ClassDescriptor)clazz).mName);
+ }
+ return super.equals(clazz);
+ }
+
+ @Override
+ public int hashCode() {
+ return mName.hashCode();
+ }
+
+ public boolean isInstantiable() {
+ return mIsInstantiable;
+ }
+
+ void setInstantiable(boolean state) {
+ mIsInstantiable = state;
+ }
+
+ private String getSimpleName(String fqcn) {
+ String[] segments = fqcn.split("\\.");
+ return segments[segments.length-1];
+ }
+ }
+
+ private BufferedReader mReader;
+
+ /** Output map of FQCN => descriptor on all classes */
+ private final Map<String, ClassDescriptor> mMap = new TreeMap<String, ClassDescriptor>();
+ /** Output map of FQCN => descriptor on View classes */
+ private final Map<String, ClassDescriptor> mWidgetMap = new TreeMap<String, ClassDescriptor>();
+ /** Output map of FQCN => descriptor on ViewGroup classes */
+ private final Map<String, ClassDescriptor> mLayoutMap = new TreeMap<String, ClassDescriptor>();
+ /** Output map of FQCN => descriptor on LayoutParams classes */
+ private final Map<String, ClassDescriptor> mLayoutParamsMap =
+ new HashMap<String, ClassDescriptor>();
+ /** File path of the source text file */
+ private String mOsFilePath;
+
+ /**
+ * Creates a loader with a given file path.
+ * @param osFilePath the OS path of the file to load.
+ * @throws FileNotFoundException if the file is not found.
+ */
+ WidgetClassLoader(String osFilePath) throws FileNotFoundException {
+ mOsFilePath = osFilePath;
+ mReader = new BufferedReader(new FileReader(osFilePath));
+ }
+
+ public String getSource() {
+ return mOsFilePath;
+ }
+
+ /**
+ * Parses the text file and return true if the file was successfully parsed.
+ * @param monitor
+ */
+ boolean parseWidgetList(IProgressMonitor monitor) {
+ try {
+ String line;
+ while ((line = mReader.readLine()) != null) {
+ if (line.length() > 0) {
+ char prefix = line.charAt(0);
+ String[] classes = null;
+ ClassDescriptor clazz = null;
+ switch (prefix) {
+ case 'W':
+ classes = line.substring(1).split(" ");
+ clazz = processClass(classes, 0, null /* map */);
+ if (clazz != null) {
+ clazz.setInstantiable(true);
+ mWidgetMap.put(classes[0], clazz);
+ }
+ break;
+ case 'L':
+ classes = line.substring(1).split(" ");
+ clazz = processClass(classes, 0, null /* map */);
+ if (clazz != null) {
+ clazz.setInstantiable(true);
+ mLayoutMap.put(classes[0], clazz);
+ }
+ break;
+ case 'P':
+ classes = line.substring(1).split(" ");
+ clazz = processClass(classes, 0, mLayoutParamsMap);
+ if (clazz != null) {
+ clazz.setInstantiable(true);
+ }
+ break;
+ case '#':
+ // comment, do nothing
+ break;
+ default:
+ throw new IllegalArgumentException();
+ }
+ }
+ }
+
+ // reconciliate the layout and their layout params
+ postProcess();
+
+ return true;
+ } catch (IOException e) {
+ } finally {
+ try {
+ mReader.close();
+ } catch (IOException e) {
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Parses a View class and adds a ViewClassInfo for it in mWidgetMap.
+ * It calls itself recursively to handle super classes which are also Views.
+ * @param classes the inheritance list of the class to process.
+ * @param index the index of the class to process in the <code>classes</code> array.
+ * @param map an optional map in which to put every {@link ClassDescriptor} created.
+ */
+ private ClassDescriptor processClass(String[] classes, int index,
+ Map<String, ClassDescriptor> map) {
+ if (index >= classes.length) {
+ return null;
+ }
+
+ String fqcn = classes[index];
+
+ if ("java.lang.Object".equals(fqcn)) { //$NON-NLS-1$
+ return null;
+ }
+
+ // check if the ViewInfoClass has not yet been created.
+ if (mMap.containsKey(fqcn)) {
+ return mMap.get(fqcn);
+ }
+
+ // create the custom class.
+ ClassDescriptor clazz = new ClassDescriptor(fqcn);
+ mMap.put(fqcn, clazz);
+ if (map != null) {
+ map.put(fqcn, clazz);
+ }
+
+ // get the super class
+ ClassDescriptor superClass = processClass(classes, index+1, map);
+ if (superClass != null) {
+ clazz.setSuperClass(superClass);
+ }
+
+ return clazz;
+ }
+
+ /**
+ * Goes through the layout params and look for the enclosed class. If the layout params
+ * has no known enclosed type it is dropped.
+ */
+ private void postProcess() {
+ Collection<ClassDescriptor> params = mLayoutParamsMap.values();
+
+ for (ClassDescriptor param : params) {
+ String fqcn = param.getCanonicalName();
+
+ // get the enclosed name.
+ String enclosed = getEnclosedName(fqcn);
+
+ // look for a match in the layouts. We don't use the layout map as it only contains the
+ // end classes, but in this case we also need to process the layout params for the base
+ // layout classes.
+ ClassDescriptor enclosingType = mMap.get(enclosed);
+ if (enclosingType != null) {
+ param.setEnclosingClass(enclosingType);
+
+ // remove the class from the map, and put it back with the fixed name
+ mMap.remove(fqcn);
+ mMap.put(param.getCanonicalName(), param);
+ }
+ }
+ }
+
+ private String getEnclosedName(String fqcn) {
+ int index = fqcn.lastIndexOf('.');
+ return fqcn.substring(0, index);
+ }
+
+ /**
+ * Finds and loads all classes that derive from a given set of super classes.
+ *
+ * @param rootPackage Root package of classes to find. Use an empty string to find everyting.
+ * @param superClasses The super classes of all the classes to find.
+ * @return An hash map which keys are the super classes looked for and which values are
+ * ArrayList of the classes found. The array lists are always created for all the
+ * valid keys, they are simply empty if no deriving class is found for a given
+ * super class.
+ * @throws IOException
+ * @throws InvalidAttributeValueException
+ * @throws ClassFormatError
+ */
+ public HashMap<String, ArrayList<IClassDescriptor>> findClassesDerivingFrom(String rootPackage,
+ String[] superClasses) throws IOException, InvalidAttributeValueException,
+ ClassFormatError {
+ HashMap<String, ArrayList<IClassDescriptor>> map =
+ new HashMap<String, ArrayList<IClassDescriptor>>();
+
+ ArrayList<IClassDescriptor> list = new ArrayList<IClassDescriptor>();
+ list.addAll(mWidgetMap.values());
+ map.put(AndroidConstants.CLASS_VIEW, list);
+
+ list = new ArrayList<IClassDescriptor>();
+ list.addAll(mLayoutMap.values());
+ map.put(AndroidConstants.CLASS_VIEWGROUP, list);
+
+ list = new ArrayList<IClassDescriptor>();
+ list.addAll(mLayoutParamsMap.values());
+ map.put(AndroidConstants.CLASS_VIEWGROUP_LAYOUTPARAMS, list);
+
+ return map;
+ }
+
+ /**
+ * Returns a {@link IAndroidClassLoader.IClassDescriptor} by its fully-qualified name.
+ * @param className the fully-qualified name of the class to return.
+ * @throws ClassNotFoundException
+ */
+ public IClassDescriptor getClass(String className) throws ClassNotFoundException {
+ return mMap.get(className);
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/NewProjectAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/NewProjectAction.java
new file mode 100644
index 0000000..e0d0d5e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/NewProjectAction.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.wizards.actions;
+
+import com.android.ide.eclipse.adt.wizards.newproject.NewProjectWizard;
+
+import org.eclipse.jface.action.IAction;
+import org.eclipse.ui.IWorkbenchWizard;
+
+/**
+ * Delegate for the toolbar action "Android Project".
+ * It displays the Android New Project wizard.
+ */
+public class NewProjectAction extends OpenWizardAction {
+
+ @Override
+ protected IWorkbenchWizard instanciateWizard(IAction action) {
+ return new NewProjectWizard();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/NewXmlFileAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/NewXmlFileAction.java
new file mode 100644
index 0000000..8c4a115
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/NewXmlFileAction.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.wizards.actions;
+
+import com.android.ide.eclipse.editors.wizards.NewXmlFileWizard;
+
+import org.eclipse.jface.action.IAction;
+import org.eclipse.ui.IWorkbenchWizard;
+
+/**
+ * Delegate for the toolbar action "Android Project".
+ * It displays the Android New XML file wizard.
+ */
+public class NewXmlFileAction extends OpenWizardAction {
+
+ @Override
+ protected IWorkbenchWizard instanciateWizard(IAction action) {
+ return new NewXmlFileWizard();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/OpenWizardAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/OpenWizardAction.java
new file mode 100644
index 0000000..4fc9dee
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/actions/OpenWizardAction.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.wizards.actions;
+
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.jface.wizard.WizardDialog;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.IWorkbenchPart;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.IWorkbenchWindowActionDelegate;
+import org.eclipse.ui.IWorkbenchWizard;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.internal.IWorkbenchHelpContextIds;
+import org.eclipse.ui.internal.LegacyResourceSupport;
+import org.eclipse.ui.internal.actions.NewWizardShortcutAction;
+import org.eclipse.ui.internal.util.Util;
+
+/**
+ * An abstract action that displays one of our wizards.
+ * Derived classes must provide the actual wizard to display.
+ */
+/*package*/ abstract class OpenWizardAction implements IWorkbenchWindowActionDelegate {
+
+ /**
+ * The wizard dialog width, extracted from {@link NewWizardShortcutAction}
+ */
+ private static final int SIZING_WIZARD_WIDTH = 500;
+
+ /**
+ * The wizard dialog height, extracted from {@link NewWizardShortcutAction}
+ */
+ private static final int SIZING_WIZARD_HEIGHT = 500;
+
+
+ /* (non-Javadoc)
+ * @see org.eclipse.ui.IWorkbenchWindowActionDelegate#dispose()
+ */
+ public void dispose() {
+ // pass
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.ui.IWorkbenchWindowActionDelegate#init(org.eclipse.ui.IWorkbenchWindow)
+ */
+ public void init(IWorkbenchWindow window) {
+ // pass
+ }
+
+ /**
+ * Opens and display the Android New Project Wizard.
+ * <p/>
+ * Most of this implementation is extracted from {@link NewWizardShortcutAction#run()}.
+ *
+ * @see org.eclipse.ui.IActionDelegate#run(org.eclipse.jface.action.IAction)
+ */
+ public void run(IAction action) {
+
+ // get the workbench and the current window
+ IWorkbench workbench = PlatformUI.getWorkbench();
+ IWorkbenchWindow window = workbench.getActiveWorkbenchWindow();
+
+ // This code from NewWizardShortcutAction#run() gets the current window selection
+ // and converts it to a workbench structured selection for the wizard, if possible.
+ ISelection selection = window.getSelectionService().getSelection();
+ IStructuredSelection selectionToPass = StructuredSelection.EMPTY;
+ if (selection instanceof IStructuredSelection) {
+ selectionToPass = (IStructuredSelection) selection;
+ } else {
+ // Build the selection from the IFile of the editor
+ IWorkbenchPart part = window.getPartService().getActivePart();
+ if (part instanceof IEditorPart) {
+ IEditorInput input = ((IEditorPart) part).getEditorInput();
+ Class<?> fileClass = LegacyResourceSupport.getFileClass();
+ if (input != null && fileClass != null) {
+ Object file = Util.getAdapter(input, fileClass);
+ if (file != null) {
+ selectionToPass = new StructuredSelection(file);
+ }
+ }
+ }
+ }
+
+ // Create the wizard and initialize it with the selection
+ IWorkbenchWizard wizard = instanciateWizard(action);
+ wizard.init(workbench, selectionToPass);
+
+ // It's not visible yet until a dialog is created and opened
+ Shell parent = window.getShell();
+ WizardDialog dialog = new WizardDialog(parent, wizard);
+ dialog.create();
+
+ // This code comes straight from NewWizardShortcutAction#run()
+ Point defaultSize = dialog.getShell().getSize();
+ dialog.getShell().setSize(
+ Math.max(SIZING_WIZARD_WIDTH, defaultSize.x),
+ Math.max(SIZING_WIZARD_HEIGHT, defaultSize.y));
+ window.getWorkbench().getHelpSystem().setHelp(dialog.getShell(),
+ IWorkbenchHelpContextIds.NEW_WIZARD_SHORTCUT);
+
+ dialog.open();
+ }
+
+ /**
+ * Called by {@link #run(IAction)} to instantiate the actual wizard.
+ *
+ * @param action The action parameter from {@link #run(IAction)}.
+ * @return A new wizard instance. Must not be null.
+ */
+ protected abstract IWorkbenchWizard instanciateWizard(IAction action);
+
+ /* (non-Javadoc)
+ * @see org.eclipse.ui.IActionDelegate#selectionChanged(org.eclipse.jface.action.IAction, org.eclipse.jface.viewers.ISelection)
+ */
+ public void selectionChanged(IAction action, ISelection selection) {
+ // pass
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newproject/NewProjectCreationPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newproject/NewProjectCreationPage.java
new file mode 100644
index 0000000..33ec2bc
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newproject/NewProjectCreationPage.java
@@ -0,0 +1,1319 @@
+/*
+ * 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.
+ */
+
+/*
+ * References:
+ * org.eclipse.jdt.internal.ui.wizards.JavaProjectWizard
+ * org.eclipse.jdt.internal.ui.wizards.JavaProjectWizardFirstPage
+ */
+
+package com.android.ide.eclipse.adt.wizards.newproject;
+
+import com.android.ide.eclipse.adt.sdk.Sdk;
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.common.project.AndroidManifestHelper;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.SdkConstants;
+import com.android.sdklib.project.ProjectProperties;
+import com.android.sdklib.project.ProjectProperties.PropertyType;
+import com.android.sdkuilib.SdkTargetSelector;
+
+import org.eclipse.core.filesystem.URIUtil;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IWorkspace;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.core.runtime.Platform;
+import org.eclipse.jdt.core.JavaConventions;
+import org.eclipse.jface.wizard.WizardPage;
+import org.eclipse.osgi.util.TextProcessor;
+import org.eclipse.swt.SWT;
+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.Composite;
+import org.eclipse.swt.widgets.DirectoryDialog;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Text;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.net.URI;
+import java.util.regex.Pattern;
+
+/**
+ * NewAndroidProjectCreationPage is a project creation page that provides the
+ * following fields:
+ * <ul>
+ * <li> Project name
+ * <li> SDK Target
+ * <li> Application name
+ * <li> Package name
+ * <li> Activity name
+ * </ul>
+ * Note: this class is public so that it can be accessed from unit tests.
+ * It is however an internal class. Its API may change without notice.
+ * It should semantically be considered as a private final class.
+ * Do not derive from this class.
+ */
+public class NewProjectCreationPage extends WizardPage {
+
+ // constants
+ /** Initial value for all name fields (project, activity, application, package). Used
+ * whenever a value is requested before controls are created. */
+ private static final String INITIAL_NAME = ""; //$NON-NLS-1$
+ /** Initial value for the Create New Project radio; False means Create From Existing would be
+ * the default.*/
+ private static final boolean INITIAL_CREATE_NEW_PROJECT = true;
+ /** Initial value for the Use Default Location check box. */
+ private static final boolean INITIAL_USE_DEFAULT_LOCATION = true;
+ /** Initial value for the Create Activity check box. */
+ private static final boolean INITIAL_CREATE_ACTIVITY = true;
+
+
+ /** 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. It cannot start with a space. */
+ private static final Pattern sProjectNamePattern = Pattern.compile("^[\\w][\\w. -]*$"); //$NON-NLS-1$
+ /** Last user-browsed location, static so that it be remembered for the whole session */
+ private static String sCustomLocationOsPath = ""; //$NON-NLS-1$
+ private static boolean sAutoComputeCustomLocation = true;
+
+ private final int MSG_NONE = 0;
+ private final int MSG_WARNING = 1;
+ private final int MSG_ERROR = 2;
+
+ private String mUserPackageName = ""; //$NON-NLS-1$
+ private String mUserActivityName = ""; //$NON-NLS-1$
+ private boolean mUserCreateActivityCheck = INITIAL_CREATE_ACTIVITY;
+ private String mSourceFolder = ""; //$NON-NLS-1$
+
+ // widgets
+ private Text mProjectNameField;
+ private Text mPackageNameField;
+ private Text mActivityNameField;
+ private Text mApplicationNameField;
+ private Button mCreateNewProjectRadio;
+ private Button mUseDefaultLocation;
+ private Label mLocationLabel;
+ private Text mLocationPathField;
+ private Button mBrowseButton;
+ private Button mCreateActivityCheck;
+ private Text mMinSdkVersionField;
+ private SdkTargetSelector mSdkTargetSelector;
+
+ private boolean mInternalLocationPathUpdate;
+ protected boolean mInternalProjectNameUpdate;
+ protected boolean mInternalApplicationNameUpdate;
+ private boolean mInternalCreateActivityUpdate;
+ private boolean mInternalActivityNameUpdate;
+ protected boolean mProjectNameModifiedByUser;
+ protected boolean mApplicationNameModifiedByUser;
+ private boolean mInternalMinSdkVersionUpdate;
+ private boolean mMinSdkVersionModifiedByUser;
+
+
+ /**
+ * Creates a new project creation wizard page.
+ *
+ * @param pageName the name of this page
+ */
+ public NewProjectCreationPage(String pageName) {
+ super(pageName);
+ setPageComplete(false);
+ }
+
+ // --- Getters used by NewProjectWizard ---
+
+ /**
+ * Returns the current project location path as entered by the user, or its
+ * anticipated initial value. Note that if the default has been returned the
+ * path in a project description used to create a project should not be set.
+ *
+ * @return the project location path or its anticipated initial value.
+ */
+ public IPath getLocationPath() {
+ return new Path(getProjectLocation());
+ }
+
+ /** Returns the value of the project name field with leading and trailing spaces removed. */
+ public String getProjectName() {
+ return mProjectNameField == null ? INITIAL_NAME : mProjectNameField.getText().trim();
+ }
+
+ /** Returns the value of the package name field with spaces trimmed. */
+ public String getPackageName() {
+ return mPackageNameField == null ? INITIAL_NAME : mPackageNameField.getText().trim();
+ }
+
+ /** Returns the value of the activity name field with spaces trimmed. */
+ public String getActivityName() {
+ return mActivityNameField == null ? INITIAL_NAME : mActivityNameField.getText().trim();
+ }
+
+ /** Returns the value of the min sdk version field with spaces trimmed. */
+ public String getMinSdkVersion() {
+ return mMinSdkVersionField == null ? "" : mMinSdkVersionField.getText().trim();
+ }
+
+ /** Returns the value of the application name field with spaces trimmed. */
+ public String getApplicationName() {
+ // Return the name of the activity as default application name.
+ return mApplicationNameField == null ? getActivityName()
+ : mApplicationNameField.getText().trim();
+
+ }
+
+ /** Returns the value of the "Create New Project" radio. */
+ public boolean isNewProject() {
+ return mCreateNewProjectRadio == null ? INITIAL_CREATE_NEW_PROJECT
+ : mCreateNewProjectRadio.getSelection();
+ }
+
+ /** Returns the value of the "Create Activity" checkbox. */
+ public boolean isCreateActivity() {
+ return mCreateActivityCheck == null ? INITIAL_CREATE_ACTIVITY
+ : mCreateActivityCheck.getSelection();
+ }
+
+ /** Returns the value of the Use Default Location field. */
+ public boolean useDefaultLocation() {
+ return mUseDefaultLocation == null ? INITIAL_USE_DEFAULT_LOCATION
+ : mUseDefaultLocation.getSelection();
+ }
+
+ /** Returns the internal source folder (for the "existing project" mode) or the default
+ * "src" constant. */
+ public String getSourceFolder() {
+ if (isNewProject() || mSourceFolder == null || mSourceFolder.length() == 0) {
+ return SdkConstants.FD_SOURCES;
+ } else {
+ return mSourceFolder;
+ }
+ }
+
+ /** Returns the current sdk target or null if none has been selected yet. */
+ public IAndroidTarget getSdkTarget() {
+ return mSdkTargetSelector == null ? null : mSdkTargetSelector.getFirstSelected();
+ }
+
+ /**
+ * Overrides @DialogPage.setVisible(boolean) to put the focus in the project name when
+ * the dialog is made visible.
+ */
+ @Override
+ public void setVisible(boolean visible) {
+ super.setVisible(visible);
+ if (visible) {
+ mProjectNameField.setFocus();
+ }
+ }
+
+ // --- UI creation ---
+
+ /**
+ * Creates the top level control for this dialog page under the given parent
+ * composite.
+ *
+ * @see org.eclipse.jface.dialogs.IDialogPage#createControl(org.eclipse.swt.widgets.Composite)
+ */
+ public void createControl(Composite parent) {
+ Composite composite = new Composite(parent, SWT.NULL);
+ composite.setFont(parent.getFont());
+
+ initializeDialogUnits(parent);
+
+ composite.setLayout(new GridLayout());
+ composite.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ createProjectNameGroup(composite);
+ createLocationGroup(composite);
+ createTargetGroup(composite);
+ createPropertiesGroup(composite);
+
+ // Update state the first time
+ enableLocationWidgets();
+
+ // Show description the first time
+ setErrorMessage(null);
+ setMessage(null);
+ setControl(composite);
+
+ // Validate. This will complain about the first empty field.
+ setPageComplete(validatePage());
+ }
+
+ /**
+ * Creates the group for the project name:
+ * [label: "Project Name"] [text field]
+ *
+ * @param parent the parent composite
+ */
+ private final void createProjectNameGroup(Composite parent) {
+ Composite group = new Composite(parent, SWT.NONE);
+ GridLayout layout = new GridLayout();
+ layout.numColumns = 2;
+ group.setLayout(layout);
+ group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ // new project label
+ Label label = new Label(group, SWT.NONE);
+ label.setText("Project name:");
+ label.setFont(parent.getFont());
+ label.setToolTipText("Name of the Eclipse project to create. It cannot be empty.");
+
+ // new project name entry field
+ mProjectNameField = new Text(group, SWT.BORDER);
+ GridData data = new GridData(GridData.FILL_HORIZONTAL);
+ mProjectNameField.setToolTipText("Name of the Eclipse project to create. It cannot be empty.");
+ mProjectNameField.setLayoutData(data);
+ mProjectNameField.setFont(parent.getFont());
+ mProjectNameField.addListener(SWT.Modify, new Listener() {
+ public void handleEvent(Event event) {
+ if (!mInternalProjectNameUpdate) {
+ mProjectNameModifiedByUser = true;
+ }
+ updateLocationPathField(null);
+ }
+ });
+ }
+
+
+ /**
+ * Creates the group for the Project options:
+ * [radio] Create new project
+ * [radio] Create project from existing sources
+ * [check] Use default location
+ * Location [text field] [browse button]
+ *
+ * @param parent the parent composite
+ */
+ private final void createLocationGroup(Composite parent) {
+ Group group = new Group(parent, SWT.SHADOW_ETCHED_IN);
+ // Layout has 4 columns of non-equal size
+ group.setLayout(new GridLayout());
+ group.setLayoutData(new GridData(GridData.FILL_BOTH));
+ group.setFont(parent.getFont());
+ group.setText("Contents");
+
+ mCreateNewProjectRadio = new Button(group, SWT.RADIO);
+ mCreateNewProjectRadio.setText("Create new project in workspace");
+ mCreateNewProjectRadio.setSelection(INITIAL_CREATE_NEW_PROJECT);
+ Button existing_project_radio = new Button(group, SWT.RADIO);
+ existing_project_radio.setText("Create project from existing source");
+ existing_project_radio.setSelection(!INITIAL_CREATE_NEW_PROJECT);
+
+ mUseDefaultLocation = new Button(group, SWT.CHECK);
+ mUseDefaultLocation.setText("Use default location");
+ mUseDefaultLocation.setSelection(INITIAL_USE_DEFAULT_LOCATION);
+
+ SelectionListener location_listener = new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ super.widgetSelected(e);
+ enableLocationWidgets();
+ extractNamesFromAndroidManifest();
+ setPageComplete(validatePage());
+ }
+ };
+
+ mCreateNewProjectRadio.addSelectionListener(location_listener);
+ existing_project_radio.addSelectionListener(location_listener);
+ mUseDefaultLocation.addSelectionListener(location_listener);
+
+ Composite location_group = new Composite(group, SWT.NONE);
+ location_group.setLayout(new GridLayout(4, /* num columns */
+ false /* columns of not equal size */));
+ location_group.setLayoutData(new GridData(GridData.FILL_BOTH));
+ location_group.setFont(parent.getFont());
+
+ mLocationLabel = new Label(location_group, SWT.NONE);
+ mLocationLabel.setText("Location:");
+
+ mLocationPathField = new Text(location_group, SWT.BORDER);
+ GridData data = new GridData(GridData.FILL, /* horizontal alignment */
+ GridData.BEGINNING, /* vertical alignment */
+ true, /* grabExcessHorizontalSpace */
+ false, /* grabExcessVerticalSpace */
+ 2, /* horizontalSpan */
+ 1); /* verticalSpan */
+ mLocationPathField.setLayoutData(data);
+ mLocationPathField.setFont(parent.getFont());
+ mLocationPathField.addListener(SWT.Modify, new Listener() {
+ public void handleEvent(Event event) {
+ onLocationPathFieldModified();
+ }
+ });
+
+ mBrowseButton = new Button(location_group, SWT.PUSH);
+ mBrowseButton.setText("Browse...");
+ setButtonLayoutData(mBrowseButton);
+ mBrowseButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ openDirectoryBrowser();
+ }
+ });
+ }
+
+ /**
+ * Creates the target group.
+ * It only contains an SdkTargetSelector.
+ */
+ private void createTargetGroup(Composite parent) {
+ Group group = new Group(parent, SWT.SHADOW_ETCHED_IN);
+ // Layout has 1 column
+ group.setLayout(new GridLayout());
+ group.setLayoutData(new GridData(GridData.FILL_BOTH));
+ group.setFont(parent.getFont());
+ group.setText("Target");
+
+ // get the targets from the sdk
+ IAndroidTarget[] targets = null;
+ if (Sdk.getCurrent() != null) {
+ targets = Sdk.getCurrent().getTargets();
+ }
+
+ mSdkTargetSelector = new SdkTargetSelector(group, targets, false /*multi-selection*/);
+
+ // If there's only one target, select it
+ if (targets != null && targets.length == 1) {
+ mSdkTargetSelector.setSelection(targets[0]);
+ }
+
+ mSdkTargetSelector.setSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ onSdkTargetModified();
+ updateLocationPathField(null);
+ setPageComplete(validatePage());
+ }
+ });
+ }
+
+ /**
+ * Display a directory browser and update the location path field with the selected path
+ */
+ private void openDirectoryBrowser() {
+
+ String existing_dir = getLocationPathFieldValue();
+
+ // Disable the path if it doesn't exist
+ if (existing_dir.length() == 0) {
+ existing_dir = null;
+ } else {
+ File f = new File(existing_dir);
+ if (!f.exists()) {
+ existing_dir = null;
+ }
+ }
+
+ DirectoryDialog dd = new DirectoryDialog(mLocationPathField.getShell());
+ dd.setMessage("Browse for folder");
+ dd.setFilterPath(existing_dir);
+ String abs_dir = dd.open();
+
+ if (abs_dir != null) {
+ updateLocationPathField(abs_dir);
+ extractNamesFromAndroidManifest();
+ setPageComplete(validatePage());
+ }
+ }
+
+ /**
+ * Creates the group for the project properties:
+ * - Package name [text field]
+ * - Activity name [text field]
+ * - Application name [text field]
+ *
+ * @param parent the parent composite
+ */
+ private final void createPropertiesGroup(Composite parent) {
+ // package specification group
+ Group group = new Group(parent, SWT.SHADOW_ETCHED_IN);
+ GridLayout layout = new GridLayout();
+ layout.numColumns = 2;
+ group.setLayout(layout);
+ group.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ group.setFont(parent.getFont());
+ group.setText("Properties");
+
+ // new application label
+ Label label = new Label(group, SWT.NONE);
+ label.setText("Application name:");
+ label.setFont(parent.getFont());
+ label.setToolTipText("Name of the Application. This is a free string. It can be empty.");
+
+ // new application name entry field
+ mApplicationNameField = new Text(group, SWT.BORDER);
+ GridData data = new GridData(GridData.FILL_HORIZONTAL);
+ mApplicationNameField.setToolTipText("Name of the Application. This is a free string. It can be empty.");
+ mApplicationNameField.setLayoutData(data);
+ mApplicationNameField.setFont(parent.getFont());
+ mApplicationNameField.addListener(SWT.Modify, new Listener() {
+ public void handleEvent(Event event) {
+ if (!mInternalApplicationNameUpdate) {
+ mApplicationNameModifiedByUser = true;
+ }
+ }
+ });
+
+ // new package label
+ label = new Label(group, SWT.NONE);
+ label.setText("Package name:");
+ label.setFont(parent.getFont());
+ label.setToolTipText("Namespace of the Package to create. This must be a Java namespace with at least two components.");
+
+ // new package name entry field
+ mPackageNameField = new Text(group, SWT.BORDER);
+ data = new GridData(GridData.FILL_HORIZONTAL);
+ mPackageNameField.setToolTipText("Namespace of the Package to create. This must be a Java namespace with at least two components.");
+ mPackageNameField.setLayoutData(data);
+ mPackageNameField.setFont(parent.getFont());
+ mPackageNameField.addListener(SWT.Modify, new Listener() {
+ public void handleEvent(Event event) {
+ onPackageNameFieldModified();
+ }
+ });
+
+ // new activity label
+ mCreateActivityCheck = new Button(group, SWT.CHECK);
+ mCreateActivityCheck.setText("Create Activity:");
+ mCreateActivityCheck.setToolTipText("Specifies if you want to create a default Activity.");
+ mCreateActivityCheck.setFont(parent.getFont());
+ mCreateActivityCheck.setSelection(INITIAL_CREATE_ACTIVITY);
+ mCreateActivityCheck.addListener(SWT.Selection, new Listener() {
+ public void handleEvent(Event event) {
+ onCreateActivityCheckModified();
+ enableLocationWidgets();
+ }
+ });
+
+ // new activity name entry field
+ mActivityNameField = new Text(group, SWT.BORDER);
+ data = new GridData(GridData.FILL_HORIZONTAL);
+ mActivityNameField.setToolTipText("Name of the Activity class to create. Must be a valid Java identifier.");
+ mActivityNameField.setLayoutData(data);
+ mActivityNameField.setFont(parent.getFont());
+ mActivityNameField.addListener(SWT.Modify, new Listener() {
+ public void handleEvent(Event event) {
+ onActivityNameFieldModified();
+ }
+ });
+
+ // min sdk version label
+ label = new Label(group, SWT.NONE);
+ label.setText("Min SDK Version:");
+ label.setFont(parent.getFont());
+ label.setToolTipText("The minimum SDK version number that the application requires. Must be an integer > 0. It can be empty.");
+
+ // min sdk version entry field
+ mMinSdkVersionField = new Text(group, SWT.BORDER);
+ data = new GridData(GridData.FILL_HORIZONTAL);
+ label.setToolTipText("The minimum SDK version number that the application requires. Must be an integer > 0. It can be empty.");
+ mMinSdkVersionField.setLayoutData(data);
+ mMinSdkVersionField.setFont(parent.getFont());
+ mMinSdkVersionField.addListener(SWT.Modify, new Listener() {
+ public void handleEvent(Event event) {
+ onMinSdkVersionFieldModified();
+ setPageComplete(validatePage());
+ }
+ });
+ }
+
+
+ //--- Internal getters & setters ------------------
+
+ /** Returns the location path field value with spaces trimmed. */
+ private String getLocationPathFieldValue() {
+ return mLocationPathField == null ? "" : mLocationPathField.getText().trim();
+ }
+
+ /** Returns the current project location, depending on the Use Default Location check box. */
+ public String getProjectLocation() {
+ if (isNewProject() && useDefaultLocation()) {
+ return Platform.getLocation().toString();
+ } else {
+ return getLocationPathFieldValue();
+ }
+ }
+
+ /**
+ * Creates a project resource handle for the current project name field
+ * value.
+ * <p>
+ * This method does not create the project resource; this is the
+ * responsibility of <code>IProject::create</code> invoked by the new
+ * project resource wizard.
+ * </p>
+ *
+ * @return the new project resource handle
+ */
+ private IProject getProjectHandle() {
+ return ResourcesPlugin.getWorkspace().getRoot().getProject(getProjectName());
+ }
+
+ // --- UI Callbacks ----
+
+ /**
+ * Enables or disable the location widgets depending on the user selection:
+ * the location path is enabled when using the "existing source" mode (i.e. not new project)
+ * or in new project mode with the "use default location" turned off.
+ */
+ private void enableLocationWidgets() {
+ boolean is_new_project = isNewProject();
+ boolean use_default = useDefaultLocation();
+ boolean location_enabled = !is_new_project || !use_default;
+ boolean create_activity = isCreateActivity();
+
+ mUseDefaultLocation.setEnabled(is_new_project);
+
+ mLocationLabel.setEnabled(location_enabled);
+ mLocationPathField.setEnabled(location_enabled);
+ mBrowseButton.setEnabled(location_enabled);
+
+ mPackageNameField.setEnabled(is_new_project);
+ mCreateActivityCheck.setEnabled(is_new_project);
+ mActivityNameField.setEnabled(is_new_project & create_activity);
+
+ updateLocationPathField(null);
+ updatePackageAndActivityFields();
+ }
+
+ /**
+ * Updates the location directory path field.
+ * <br/>
+ * When custom user selection is enabled, use the abs_dir argument if not null and also
+ * save it internally. If abs_dir is null, restore the last saved abs_dir. This allows the
+ * user selection to be remembered when the user switches from default to custom.
+ * <br/>
+ * When custom user selection is disabled, use the workspace default location with the
+ * current project name. This does not change the internally cached abs_dir.
+ *
+ * @param abs_dir A new absolute directory path or null to use the default.
+ */
+ private void updateLocationPathField(String abs_dir) {
+ boolean is_new_project = isNewProject();
+ boolean use_default = useDefaultLocation();
+ boolean custom_location = !is_new_project || !use_default;
+
+ if (!mInternalLocationPathUpdate) {
+ mInternalLocationPathUpdate = true;
+ if (custom_location) {
+ if (abs_dir != null) {
+ // We get here if the user selected a directory with the "Browse" button.
+ // Disable auto-compute of the custom location unless the user selected
+ // the exact same path.
+ sAutoComputeCustomLocation = sAutoComputeCustomLocation &&
+ abs_dir.equals(sCustomLocationOsPath);
+ sCustomLocationOsPath = TextProcessor.process(abs_dir);
+ } else if (sAutoComputeCustomLocation ||
+ !new File(sCustomLocationOsPath).isDirectory()) {
+ // By default select the samples directory of the current target
+ IAndroidTarget target = getSdkTarget();
+ if (target != null) {
+ sCustomLocationOsPath = target.getPath(IAndroidTarget.SAMPLES);
+ }
+
+ // If we don't have a target, select the base directory of the
+ // "universal sdk". If we don't even have that, use a root drive.
+ if (sCustomLocationOsPath == null || sCustomLocationOsPath.length() == 0) {
+ if (Sdk.getCurrent() != null) {
+ sCustomLocationOsPath = Sdk.getCurrent().getSdkLocation();
+ } else {
+ sCustomLocationOsPath = File.listRoots()[0].getAbsolutePath();
+ }
+ }
+ }
+ if (!mLocationPathField.getText().equals(sCustomLocationOsPath)) {
+ mLocationPathField.setText(sCustomLocationOsPath);
+ }
+ } else {
+ String value = Platform.getLocation().append(getProjectName()).toString();
+ value = TextProcessor.process(value);
+ if (!mLocationPathField.getText().equals(value)) {
+ mLocationPathField.setText(value);
+ }
+ }
+ setPageComplete(validatePage());
+ mInternalLocationPathUpdate = false;
+ }
+ }
+
+ /**
+ * The location path field is either modified internally (from updateLocationPathField)
+ * or manually by the user when the custom_location mode is not set.
+ *
+ * Ignore the internal modification. When modified by the user, memorize the choice and
+ * validate the page.
+ */
+ private void onLocationPathFieldModified() {
+ if (!mInternalLocationPathUpdate) {
+ // When the updates doesn't come from updateLocationPathField, it must be the user
+ // editing the field manually, in which case we want to save the value internally
+ // and we disable auto-compute of the custom location (to avoid overriding the user
+ // value)
+ String newPath = getLocationPathFieldValue();
+ sAutoComputeCustomLocation = sAutoComputeCustomLocation &&
+ newPath.equals(sCustomLocationOsPath);
+ sCustomLocationOsPath = newPath;
+ extractNamesFromAndroidManifest();
+ setPageComplete(validatePage());
+ }
+ }
+
+ /**
+ * The package name field is either modified internally (from extractNamesFromAndroidManifest)
+ * or manually by the user when the custom_location mode is not set.
+ *
+ * Ignore the internal modification. When modified by the user, memorize the choice and
+ * validate the page.
+ */
+ private void onPackageNameFieldModified() {
+ if (isNewProject()) {
+ mUserPackageName = getPackageName();
+ setPageComplete(validatePage());
+ }
+ }
+
+ /**
+ * The create activity checkbox is either modified internally (from
+ * extractNamesFromAndroidManifest) or manually by the user.
+ *
+ * Ignore the internal modification. When modified by the user, memorize the choice and
+ * validate the page.
+ */
+ private void onCreateActivityCheckModified() {
+ if (isNewProject() && !mInternalCreateActivityUpdate) {
+ mUserCreateActivityCheck = isCreateActivity();
+ }
+ setPageComplete(validatePage());
+ }
+
+ /**
+ * The activity name field is either modified internally (from extractNamesFromAndroidManifest)
+ * or manually by the user when the custom_location mode is not set.
+ *
+ * Ignore the internal modification. When modified by the user, memorize the choice and
+ * validate the page.
+ */
+ private void onActivityNameFieldModified() {
+ if (isNewProject() && !mInternalActivityNameUpdate) {
+ mUserActivityName = getActivityName();
+ setPageComplete(validatePage());
+ }
+ }
+
+ /**
+ * Called when the min sdk version field has been modified.
+ *
+ * Ignore the internal modifications. When modified by the user, try to match
+ * a target with the same API level.
+ */
+ private void onMinSdkVersionFieldModified() {
+ if (mInternalMinSdkVersionUpdate) {
+ return;
+ }
+
+ try {
+ int version = Integer.parseInt(getMinSdkVersion());
+
+ // Before changing, compare with the currently selected one, if any.
+ // There can be multiple targets with the same sdk api version, so don't change
+ // it if it's already at the right version.
+ IAndroidTarget curr_target = getSdkTarget();
+ if (curr_target != null && curr_target.getApiVersionNumber() == version) {
+ return;
+ }
+
+ for (IAndroidTarget target : mSdkTargetSelector.getTargets()) {
+ if (target.getApiVersionNumber() == version) {
+ mSdkTargetSelector.setSelection(target);
+ break;
+ }
+ }
+ } catch (NumberFormatException e) {
+ // ignore
+ }
+
+ mMinSdkVersionModifiedByUser = true;
+ }
+
+ /**
+ * Called when an SDK target is modified.
+ *
+ * If the minSdkVersion field hasn't been modified by the user yet, we change it
+ * to reflect the sdk api level that has just been selected.
+ */
+ private void onSdkTargetModified() {
+ IAndroidTarget target = getSdkTarget();
+
+ if (target != null && !mMinSdkVersionModifiedByUser) {
+ mInternalMinSdkVersionUpdate = true;
+ mMinSdkVersionField.setText(Integer.toString(target.getApiVersionNumber()));
+ mInternalMinSdkVersionUpdate = false;
+ }
+ }
+
+ /**
+ * Called when the radio buttons are changed between the "create new project" and the
+ * "use existing source" mode. This reverts the fields to whatever the user manually
+ * entered before.
+ */
+ private void updatePackageAndActivityFields() {
+ if (isNewProject()) {
+ if (mUserPackageName.length() > 0 &&
+ !mPackageNameField.getText().equals(mUserPackageName)) {
+ mPackageNameField.setText(mUserPackageName);
+ }
+
+ if (mUserActivityName.length() > 0 &&
+ !mActivityNameField.getText().equals(mUserActivityName)) {
+ mInternalActivityNameUpdate = true;
+ mActivityNameField.setText(mUserActivityName);
+ mInternalActivityNameUpdate = false;
+ }
+
+ if (mUserCreateActivityCheck != mCreateActivityCheck.getSelection()) {
+ mInternalCreateActivityUpdate = true;
+ mCreateActivityCheck.setSelection(mUserCreateActivityCheck);
+ mInternalCreateActivityUpdate = false;
+ }
+ }
+ }
+
+ /**
+ * Extract names from an android manifest.
+ * This is done only if the user selected the "use existing source" and a manifest xml file
+ * can actually be found in the custom user directory.
+ */
+ private void extractNamesFromAndroidManifest() {
+ if (isNewProject()) {
+ return;
+ }
+
+ String projectLocation = getProjectLocation();
+ File f = new File(projectLocation);
+ if (!f.isDirectory()) {
+ return;
+ }
+
+ Path path = new Path(f.getPath());
+ String osPath = path.append(AndroidConstants.FN_ANDROID_MANIFEST).toOSString();
+ AndroidManifestHelper manifest = new AndroidManifestHelper(osPath);
+ if (!manifest.exists()) {
+ return;
+ }
+
+ String packageName = null;
+ String activityName = null;
+ String minSdkVersion = null;
+ try {
+ packageName = manifest.getPackageName();
+ activityName = manifest.getActivityName(1);
+ minSdkVersion = manifest.getMinSdkVersion();
+ } catch (Exception e) {
+ // ignore exceptions
+ }
+
+
+ if (packageName != null && packageName.length() > 0) {
+ mPackageNameField.setText(packageName);
+ }
+
+ if (activityName != null && activityName.length() > 0) {
+ mInternalActivityNameUpdate = true;
+ mInternalCreateActivityUpdate = true;
+ mActivityNameField.setText(activityName);
+ mCreateActivityCheck.setSelection(true);
+ mInternalCreateActivityUpdate = false;
+ mInternalActivityNameUpdate = false;
+
+ // If project name and application names are empty, use the activity
+ // name as a default. If the activity name has dots, it's a part of a
+ // package specification and only the last identifier must be used.
+ if (activityName.indexOf('.') != -1) {
+ String[] ids = activityName.split(AndroidConstants.RE_DOT);
+ activityName = ids[ids.length - 1];
+ }
+ if (mProjectNameField.getText().length() == 0 ||
+ !mProjectNameModifiedByUser) {
+ mInternalProjectNameUpdate = true;
+ mProjectNameField.setText(activityName);
+ mInternalProjectNameUpdate = false;
+ }
+ if (mApplicationNameField.getText().length() == 0 ||
+ !mApplicationNameModifiedByUser) {
+ mInternalApplicationNameUpdate = true;
+ mApplicationNameField.setText(activityName);
+ mInternalApplicationNameUpdate = false;
+ }
+ } else {
+ mInternalActivityNameUpdate = true;
+ mInternalCreateActivityUpdate = true;
+ mActivityNameField.setText(""); //$NON-NLS-1$
+ mCreateActivityCheck.setSelection(false);
+ mInternalCreateActivityUpdate = false;
+ mInternalActivityNameUpdate = false;
+
+ // There is no activity name to use to fill in the project and application
+ // name. However if there's a package name, we can use this as a base.
+ if (packageName != null && packageName.length() > 0) {
+ // Package name is a java identifier, so it's most suitable for
+ // an application name.
+
+ if (mApplicationNameField.getText().length() == 0 ||
+ !mApplicationNameModifiedByUser) {
+ mInternalApplicationNameUpdate = true;
+ mApplicationNameField.setText(packageName);
+ mInternalApplicationNameUpdate = false;
+ }
+
+ // For the project name, remove any dots
+ packageName = packageName.replace('.', '_');
+ if (mProjectNameField.getText().length() == 0 ||
+ !mProjectNameModifiedByUser) {
+ mInternalProjectNameUpdate = true;
+ mProjectNameField.setText(packageName);
+ mInternalProjectNameUpdate = false;
+ }
+
+ }
+ }
+
+ // Select the target matching the manifest's sdk or build properties, if any
+ boolean foundTarget = false;
+
+ ProjectProperties p = ProjectProperties.create(projectLocation, null);
+ if (p != null) {
+ // Check the {build|default}.properties files if present
+ p.merge(PropertyType.BUILD).merge(PropertyType.DEFAULT);
+ String v = p.getProperty(ProjectProperties.PROPERTY_TARGET);
+ IAndroidTarget target = Sdk.getCurrent().getTargetFromHashString(v);
+ if (target != null) {
+ mSdkTargetSelector.setSelection(target);
+ foundTarget = true;
+ }
+ }
+
+ if (!foundTarget && minSdkVersion != null) {
+ try {
+ int sdkVersion = Integer.parseInt(minSdkVersion);
+
+ for (IAndroidTarget target : mSdkTargetSelector.getTargets()) {
+ if (target.getApiVersionNumber() == sdkVersion) {
+ mSdkTargetSelector.setSelection(target);
+ foundTarget = true;
+ break;
+ }
+ }
+ } catch(NumberFormatException e) {
+ // ignore
+ }
+ }
+
+ if (!foundTarget) {
+ for (IAndroidTarget target : mSdkTargetSelector.getTargets()) {
+ if (projectLocation.startsWith(target.getLocation())) {
+ mSdkTargetSelector.setSelection(target);
+ foundTarget = true;
+ break;
+ }
+ }
+ }
+
+ if (!foundTarget) {
+ mInternalMinSdkVersionUpdate = true;
+ mMinSdkVersionField.setText(minSdkVersion == null ? "" : minSdkVersion); //$NON-NLS-1$
+ mInternalMinSdkVersionUpdate = false;
+ }
+ }
+
+ /**
+ * Returns whether this page's controls currently all contain valid values.
+ *
+ * @return <code>true</code> if all controls are valid, and
+ * <code>false</code> if at least one is invalid
+ */
+ protected boolean validatePage() {
+ IWorkspace workspace = ResourcesPlugin.getWorkspace();
+
+ int status = validateProjectField(workspace);
+ if ((status & MSG_ERROR) == 0) {
+ status |= validateLocationPath(workspace);
+ }
+ if ((status & MSG_ERROR) == 0) {
+ status |= validateSdkTarget();
+ }
+ if ((status & MSG_ERROR) == 0) {
+ status |= validatePackageField();
+ }
+ if ((status & MSG_ERROR) == 0) {
+ status |= validateActivityField();
+ }
+ if ((status & MSG_ERROR) == 0) {
+ status |= validateMinSdkVersionField();
+ }
+ if ((status & MSG_ERROR) == 0) {
+ status |= validateSourceFolder();
+ }
+ if (status == MSG_NONE) {
+ setStatus(null, MSG_NONE);
+ }
+
+ // Return false if there's an error so that the finish button be disabled.
+ return (status & MSG_ERROR) == 0;
+ }
+
+ /**
+ * Validates the project name field.
+ *
+ * @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE.
+ */
+ private int validateProjectField(IWorkspace workspace) {
+ // Validate project field
+ String projectFieldContents = getProjectName();
+ if (projectFieldContents.length() == 0) {
+ return setStatus("Project name must be specified", MSG_ERROR);
+ }
+
+ // Limit the project name to shell-agnostic characters since it will be used to
+ // generate the final package
+ if (!sProjectNamePattern.matcher(projectFieldContents).matches()) {
+ return setStatus("The project name must start with an alphanumeric characters, followed by one or more alphanumerics, digits, dots, dashes, underscores or spaces.",
+ MSG_ERROR);
+ }
+
+ IStatus nameStatus = workspace.validateName(projectFieldContents, IResource.PROJECT);
+ if (!nameStatus.isOK()) {
+ return setStatus(nameStatus.getMessage(), MSG_ERROR);
+ }
+
+ if (getProjectHandle().exists()) {
+ return setStatus("A project with that name already exists in the workspace",
+ MSG_ERROR);
+ }
+
+ return MSG_NONE;
+ }
+
+ /**
+ * Validates the location path field.
+ *
+ * @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE.
+ */
+ private int validateLocationPath(IWorkspace workspace) {
+ Path path = new Path(getProjectLocation());
+ if (isNewProject()) {
+ if (!useDefaultLocation()) {
+ // If not using the default value validate the location.
+ URI uri = URIUtil.toURI(path.toOSString());
+ IStatus locationStatus = workspace.validateProjectLocationURI(getProjectHandle(),
+ uri);
+ if (!locationStatus.isOK()) {
+ return setStatus(locationStatus.getMessage(), MSG_ERROR);
+ } else {
+ // The location is valid as far as Eclipse is concerned (i.e. mostly not
+ // an existing workspace project.) Check it either doesn't exist or is
+ // a directory that is empty.
+ File f = path.toFile();
+ if (f.exists() && !f.isDirectory()) {
+ return setStatus("A directory name must be specified.", MSG_ERROR);
+ } else if (f.isDirectory()) {
+ // However if the directory exists, we should put a warning if it is not
+ // empty. We don't put an error (we'll ask the user again for confirmation
+ // before using the directory.)
+ String[] l = f.list();
+ if (l.length != 0) {
+ return setStatus("The selected output directory is not empty.",
+ MSG_WARNING);
+ }
+ }
+ }
+ } else {
+ // Otherwise validate the path string is not empty
+ if (getProjectLocation().length() == 0) {
+ return setStatus("A directory name must be specified.", MSG_ERROR);
+ }
+
+ File dest = path.append(getProjectName()).toFile();
+ if (dest.exists()) {
+ return setStatus(String.format("There is already a file or directory named \"%1$s\" in the selected location.",
+ getProjectName()), MSG_ERROR);
+ }
+ }
+ } else {
+ // Must be an existing directory
+ File f = path.toFile();
+ if (!f.isDirectory()) {
+ return setStatus("An existing directory name must be specified.", MSG_ERROR);
+ }
+
+ // Check there's an android manifest in the directory
+ String osPath = path.append(AndroidConstants.FN_ANDROID_MANIFEST).toOSString();
+ AndroidManifestHelper manifest = new AndroidManifestHelper(osPath);
+ if (!manifest.exists()) {
+ return setStatus(
+ String.format("File %1$s not found in %2$s.",
+ AndroidConstants.FN_ANDROID_MANIFEST, f.getName()),
+ MSG_ERROR);
+ }
+
+ // Parse it and check the important fields.
+ String packageName = manifest.getPackageName();
+ if (packageName == null || packageName.length() == 0) {
+ return setStatus(
+ String.format("No package name defined in %1$s.", osPath),
+ MSG_ERROR);
+ }
+
+ String activityName = manifest.getActivityName(1);
+ if (activityName == null || activityName.length() == 0) {
+ // This is acceptable now as long as no activity needs to be created
+ if (isCreateActivity()) {
+ return setStatus(
+ String.format("No activity name defined in %1$s.", osPath),
+ MSG_ERROR);
+ }
+ }
+
+ // If there's already a .project, tell the user to use import instead.
+ if (path.append(".project").toFile().exists()) { //$NON-NLS-1$
+ return setStatus("An Eclipse project already exists in this directory. Consider using File > Import > Existing Project instead.",
+ MSG_WARNING);
+ }
+ }
+
+ return MSG_NONE;
+ }
+
+ /**
+ * Validates the sdk target choice.
+ *
+ * @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE.
+ */
+ private int validateSdkTarget() {
+ if (getSdkTarget() == null) {
+ return setStatus("An SDK Target must be specified.", MSG_ERROR);
+ }
+ return MSG_NONE;
+ }
+
+ /**
+ * Validates the sdk target choice.
+ *
+ * @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE.
+ */
+ private int validateMinSdkVersionField() {
+
+ // If the min sdk version is empty, it is always accepted.
+ if (getMinSdkVersion().length() == 0) {
+ return MSG_NONE;
+ }
+
+ int version = -1;
+ try {
+ // If not empty, it must be a valid integer > 0
+ version = Integer.parseInt(getMinSdkVersion());
+ } catch (NumberFormatException e) {
+ // ignore
+ }
+
+ if (version < 1) {
+ return setStatus("Min SDK Version must be an integer > 0.", MSG_ERROR);
+ }
+
+ if (getSdkTarget() != null && getSdkTarget().getApiVersionNumber() != version) {
+ return setStatus("The API level for the selected SDK target does not match the Min SDK version.",
+ MSG_WARNING);
+ }
+
+ return MSG_NONE;
+ }
+
+ /**
+ * Validates the activity name field.
+ *
+ * @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE.
+ */
+ private int validateActivityField() {
+ // Disregard if not creating an activity
+ if (!isCreateActivity()) {
+ return MSG_NONE;
+ }
+
+ // Validate activity field
+ String activityFieldContents = getActivityName();
+ if (activityFieldContents.length() == 0) {
+ return setStatus("Activity name must be specified.", MSG_ERROR);
+ }
+
+ // The activity field can actually contain part of a sub-package name
+ // or it can start with a dot "." to indicates it comes from the parent package name.
+ String packageName = "";
+ int pos = activityFieldContents.lastIndexOf('.');
+ if (pos >= 0) {
+ packageName = activityFieldContents.substring(0, pos);
+ if (packageName.startsWith(".")) { //$NON-NLS-1$
+ packageName = packageName.substring(1);
+ }
+
+ activityFieldContents = activityFieldContents.substring(pos + 1);
+ }
+
+ // the activity field can contain a simple java identifier, or a
+ // package name or one that starts with a dot. So if it starts with a dot,
+ // ignore this dot -- the rest must look like a package name.
+ if (activityFieldContents.charAt(0) == '.') {
+ activityFieldContents = activityFieldContents.substring(1);
+ }
+
+ // Check it's a valid activity string
+ int result = MSG_NONE;
+ IStatus status = JavaConventions.validateTypeVariableName(activityFieldContents,
+ "1.5", "1.5"); //$NON-NLS-1$ $NON-NLS-2$
+ if (!status.isOK()) {
+ result = setStatus(status.getMessage(),
+ status.getSeverity() == IStatus.ERROR ? MSG_ERROR : MSG_WARNING);
+ }
+
+ // Check it's a valid package string
+ if (result != MSG_ERROR && packageName.length() > 0) {
+ status = JavaConventions.validatePackageName(packageName,
+ "1.5", "1.5"); //$NON-NLS-1$ $NON-NLS-2$
+ if (!status.isOK()) {
+ result = setStatus(status.getMessage() + " (in the activity name)",
+ status.getSeverity() == IStatus.ERROR ? MSG_ERROR : MSG_WARNING);
+ }
+ }
+
+
+ return result;
+ }
+
+ /**
+ * Validates the package name field.
+ *
+ * @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE.
+ */
+ private int validatePackageField() {
+ // Validate package field
+ String packageFieldContents = getPackageName();
+ if (packageFieldContents.length() == 0) {
+ return setStatus("Package name must be specified.", MSG_ERROR);
+ }
+
+ // Check it's a valid package string
+ int result = MSG_NONE;
+ IStatus status = JavaConventions.validatePackageName(packageFieldContents, "1.5", "1.5"); //$NON-NLS-1$ $NON-NLS-2$
+ if (!status.isOK()) {
+ result = setStatus(status.getMessage(),
+ status.getSeverity() == IStatus.ERROR ? MSG_ERROR : MSG_WARNING);
+ }
+
+ // The Android Activity Manager does not accept packages names with only one
+ // identifier. Check the package name has at least one dot in them (the previous rule
+ // validated that if such a dot exist, it's not the first nor last characters of the
+ // string.)
+ if (result != MSG_ERROR && packageFieldContents.indexOf('.') == -1) {
+ return setStatus("Package name must have at least two identifiers.", MSG_ERROR);
+ }
+
+ return result;
+ }
+
+ /**
+ * Validates that an existing project actually has a source folder.
+ *
+ * For project in "use existing source" mode, this tries to find the source folder.
+ * A source folder should be just under the project directory and it should have all
+ * the directories composing the package+activity name.
+ *
+ * As a side effect, it memorizes the source folder in mSourceFolder.
+ *
+ * TODO: support multiple source folders for multiple activities.
+ *
+ * @return The wizard message type, one of MSG_ERROR, MSG_WARNING or MSG_NONE.
+ */
+ private int validateSourceFolder() {
+ // This check does nothing when creating a new project.
+ // This check is also useless when no activity is present or created.
+ if (isNewProject() || !isCreateActivity()) {
+ return MSG_NONE;
+ }
+
+ String osTarget = getActivityName();
+
+ if (osTarget.indexOf('.') == -1) {
+ osTarget = getPackageName() + File.separator + osTarget;
+ } else if (osTarget.indexOf('.') == 0) {
+ osTarget = getPackageName() + osTarget;
+ }
+ osTarget = osTarget.replace('.', File.separatorChar) + AndroidConstants.DOT_JAVA;
+
+ String projectPath = getProjectLocation();
+ File projectDir = new File(projectPath);
+ File[] all_dirs = projectDir.listFiles(new FileFilter() {
+ public boolean accept(File pathname) {
+ return pathname.isDirectory();
+ }
+ });
+ for (File f : all_dirs) {
+ Path path = new Path(f.getAbsolutePath());
+ File java_activity = path.append(osTarget).toFile();
+ if (java_activity.isFile()) {
+ mSourceFolder = f.getName();
+ return MSG_NONE;
+ }
+ }
+
+ if (all_dirs.length > 0) {
+ return setStatus(
+ String.format("%1$s can not be found under %2$s.", osTarget, projectPath),
+ MSG_ERROR);
+ } else {
+ return setStatus(
+ String.format("No source folders can be found in %1$s.", projectPath),
+ MSG_ERROR);
+ }
+ }
+
+ /**
+ * Sets the error message for the wizard with the given message icon.
+ *
+ * @param message The wizard message type, one of MSG_ERROR or MSG_WARNING.
+ * @return As a convenience, always returns messageType so that the caller can return
+ * immediately.
+ */
+ private int setStatus(String message, int messageType) {
+ if (message == null) {
+ setErrorMessage(null);
+ setMessage(null);
+ } else if (!message.equals(getMessage())) {
+ setMessage(message, messageType == MSG_WARNING ? WizardPage.WARNING : WizardPage.ERROR);
+ }
+ return messageType;
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newproject/NewProjectWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newproject/NewProjectWizard.java
new file mode 100644
index 0000000..cb79796
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/wizards/newproject/NewProjectWizard.java
@@ -0,0 +1,737 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.wizards.newproject;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.project.AndroidNature;
+import com.android.ide.eclipse.adt.project.ProjectHelper;
+import com.android.ide.eclipse.adt.sdk.Sdk;
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.SdkConstants;
+
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IProjectDescription;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IResourceStatus;
+import org.eclipse.core.resources.IWorkspace;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.OperationCanceledException;
+import org.eclipse.core.runtime.Platform;
+import org.eclipse.core.runtime.SubProgressMonitor;
+import org.eclipse.jdt.core.IClasspathEntry;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.ui.actions.OpenJavaPerspectiveAction;
+import org.eclipse.jface.dialogs.ErrorDialog;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.wizard.Wizard;
+import org.eclipse.ui.INewWizard;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.actions.WorkspaceModifyOperation;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.net.MalformedURLException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.Map.Entry;
+
+/**
+ * A "New Android Project" Wizard.
+ * <p/>
+ * Note: this class is public so that it can be accessed from unit tests.
+ * It is however an internal class. Its API may change without notice.
+ * It should semantically be considered as a private final class.
+ * Do not derive from this class.
+
+ */
+public class NewProjectWizard extends Wizard implements INewWizard {
+
+ private static final String PARAM_SDK_TOOLS_DIR = "ANDROID_SDK_TOOLS"; //$NON-NLS-1$
+ private static final String PARAM_ACTIVITY = "ACTIVITY_NAME"; //$NON-NLS-1$
+ private static final String PARAM_APPLICATION = "APPLICATION_NAME"; //$NON-NLS-1$
+ private static final String PARAM_PACKAGE = "PACKAGE"; //$NON-NLS-1$
+ private static final String PARAM_PROJECT = "PROJECT_NAME"; //$NON-NLS-1$
+ private static final String PARAM_STRING_NAME = "STRING_NAME"; //$NON-NLS-1$
+ private static final String PARAM_STRING_CONTENT = "STRING_CONTENT"; //$NON-NLS-1$
+ private static final String PARAM_IS_NEW_PROJECT = "IS_NEW_PROJECT"; //$NON-NLS-1$
+ private static final String PARAM_SRC_FOLDER = "SRC_FOLDER"; //$NON-NLS-1$
+ private static final String PARAM_SDK_TARGET = "SDK_TARGET"; //$NON-NLS-1$
+ private static final String PARAM_MIN_SDK_VERSION = "MIN_SDK_VERSION"; //$NON-NLS-1$
+
+ private static final String PH_ACTIVITIES = "ACTIVITIES"; //$NON-NLS-1$
+ private static final String PH_USES_SDK = "USES-SDK"; //$NON-NLS-1$
+ private static final String PH_INTENT_FILTERS = "INTENT_FILTERS"; //$NON-NLS-1$
+ private static final String PH_STRINGS = "STRINGS"; //$NON-NLS-1$
+
+ private static final String BIN_DIRECTORY =
+ SdkConstants.FD_OUTPUT + AndroidConstants.WS_SEP;
+ private static final String RES_DIRECTORY =
+ SdkConstants.FD_RESOURCES + AndroidConstants.WS_SEP;
+ private static final String ASSETS_DIRECTORY =
+ SdkConstants.FD_ASSETS + AndroidConstants.WS_SEP;
+ private static final String DRAWABLE_DIRECTORY =
+ SdkConstants.FD_DRAWABLE + AndroidConstants.WS_SEP;
+ private static final String LAYOUT_DIRECTORY =
+ SdkConstants.FD_LAYOUT + AndroidConstants.WS_SEP;
+ private static final String VALUES_DIRECTORY =
+ SdkConstants.FD_VALUES + AndroidConstants.WS_SEP;
+ private static final String GEN_SRC_DIRECTORY =
+ SdkConstants.FD_GEN_SOURCES + AndroidConstants.WS_SEP;
+
+ private static final String TEMPLATES_DIRECTORY = "templates/"; //$NON-NLS-1$
+ private static final String TEMPLATE_MANIFEST = TEMPLATES_DIRECTORY
+ + "AndroidManifest.template"; //$NON-NLS-1$
+ private static final String TEMPLATE_ACTIVITIES = TEMPLATES_DIRECTORY
+ + "activity.template"; //$NON-NLS-1$
+ private static final String TEMPLATE_USES_SDK = TEMPLATES_DIRECTORY
+ + "uses-sdk.template"; //$NON-NLS-1$
+ private static final String TEMPLATE_INTENT_LAUNCHER = TEMPLATES_DIRECTORY
+ + "launcher_intent_filter.template"; //$NON-NLS-1$
+
+ private static final String TEMPLATE_STRINGS = TEMPLATES_DIRECTORY
+ + "strings.template"; //$NON-NLS-1$
+ private static final String TEMPLATE_STRING = TEMPLATES_DIRECTORY
+ + "string.template"; //$NON-NLS-1$
+ private static final String ICON = "icon.png"; //$NON-NLS-1$
+
+ private static final String STRINGS_FILE = "strings.xml"; //$NON-NLS-1$
+
+ private static final String STRING_RSRC_PREFIX = "@string/"; //$NON-NLS-1$
+ private static final String STRING_APP_NAME = "app_name"; //$NON-NLS-1$
+ private static final String STRING_HELLO_WORLD = "hello"; //$NON-NLS-1$
+
+ private static final String[] DEFAULT_DIRECTORIES = new String[] {
+ BIN_DIRECTORY, RES_DIRECTORY, ASSETS_DIRECTORY };
+ private static final String[] RES_DIRECTORIES = new String[] {
+ DRAWABLE_DIRECTORY, LAYOUT_DIRECTORY, VALUES_DIRECTORY};
+
+ private static final String PROJECT_LOGO_LARGE = "icons/android_large.png"; //$NON-NLS-1$
+ private static final String JAVA_ACTIVITY_TEMPLATE = "java_file.template"; //$NON-NLS-1$
+ private static final String LAYOUT_TEMPLATE = "layout.template"; //$NON-NLS-1$
+ private static final String MAIN_LAYOUT_XML = "main.xml"; //$NON-NLS-1$
+
+ protected static final String MAIN_PAGE_NAME = "newAndroidProjectPage"; //$NON-NLS-1$
+
+ private NewProjectCreationPage mMainPage;
+
+ /**
+ * Initializes this creation wizard using the passed workbench and object
+ * selection. Inherited from org.eclipse.ui.IWorkbenchWizard
+ */
+ public void init(IWorkbench workbench, IStructuredSelection selection) {
+ setHelpAvailable(false); // TODO have help
+ setWindowTitle("New Android Project");
+ setImageDescriptor();
+
+ mMainPage = createMainPage();
+ mMainPage.setTitle("New Android Project");
+ mMainPage.setDescription("Creates a new Android Project resource.");
+ }
+
+ /**
+ * Creates the wizard page.
+ * <p/>
+ * Please do NOT override this method.
+ * <p/>
+ * This is protected so that it can be overridden by unit tests.
+ * However the contract of this class is private and NO ATTEMPT will be made
+ * to maintain compatibility between different versions of the plugin.
+ */
+ protected NewProjectCreationPage createMainPage() {
+ return new NewProjectCreationPage(MAIN_PAGE_NAME);
+ }
+
+ // -- Methods inherited from org.eclipse.jface.wizard.Wizard --
+ // The Wizard class implements most defaults and boilerplate code needed by
+ // IWizard
+
+ /**
+ * Adds pages to this wizard.
+ */
+ @Override
+ public void addPages() {
+ addPage(mMainPage);
+ }
+
+ /**
+ * Performs any actions appropriate in response to the user having pressed
+ * the Finish button, or refuse if finishing now is not permitted: here, it
+ * actually creates the workspace project and then switch to the Java
+ * perspective.
+ *
+ * @return True
+ */
+ @Override
+ public boolean performFinish() {
+ if (!createAndroidProject()) {
+ return false;
+ }
+
+ // Open the default Java Perspective
+ OpenJavaPerspectiveAction action = new OpenJavaPerspectiveAction();
+ action.run();
+ return true;
+ }
+
+ // -- Custom Methods --
+
+ /**
+ * Before actually creating the project for a new project (as opposed to using an
+ * existing project), we check if the target location is a directory that either does
+ * not exist or is empty.
+ *
+ * If it's not empty, ask the user for confirmation.
+ *
+ * @param destination The destination folder where the new project is to be created.
+ * @return True if the destination doesn't exist yet or is an empty directory or is
+ * accepted by the user.
+ */
+ private boolean validateNewProjectLocationIsEmpty(IPath destination) {
+ File f = new File(destination.toOSString());
+ if (f.isDirectory() && f.list().length > 0) {
+ return AdtPlugin.displayPrompt("New Android Project",
+ "You are going to create a new Android Project in an existing, non-empty, directory. Are you sure you want to proceed?");
+ }
+ return true;
+ }
+
+ /**
+ * Creates the android project.
+ * @return True if the project could be created.
+ */
+ private boolean createAndroidProject() {
+ IWorkspace workspace = ResourcesPlugin.getWorkspace();
+ final IProject project = workspace.getRoot().getProject(mMainPage.getProjectName());
+ final IProjectDescription description = workspace.newProjectDescription(project.getName());
+
+ final Map<String, Object> parameters = new HashMap<String, Object>();
+ parameters.put(PARAM_PROJECT, mMainPage.getProjectName());
+ parameters.put(PARAM_PACKAGE, mMainPage.getPackageName());
+ parameters.put(PARAM_APPLICATION, STRING_RSRC_PREFIX + STRING_APP_NAME);
+ parameters.put(PARAM_SDK_TOOLS_DIR, AdtPlugin.getOsSdkToolsFolder());
+ parameters.put(PARAM_IS_NEW_PROJECT, mMainPage.isNewProject());
+ parameters.put(PARAM_SRC_FOLDER, mMainPage.getSourceFolder());
+ parameters.put(PARAM_SDK_TARGET, mMainPage.getSdkTarget());
+ parameters.put(PARAM_MIN_SDK_VERSION, mMainPage.getMinSdkVersion());
+
+ if (mMainPage.isCreateActivity()) {
+ // An activity name can be of the form ".package.Class" or ".Class".
+ // The initial dot is ignored, as it is always added later in the templates.
+ String activityName = mMainPage.getActivityName();
+ if (activityName.startsWith(".")) { //$NON-NLS-1$
+ activityName = activityName.substring(1);
+ }
+ parameters.put(PARAM_ACTIVITY, activityName);
+ }
+
+ // create a dictionary of string that will contain name+content.
+ // we'll put all the strings into values/strings.xml
+ final HashMap<String, String> stringDictionary = new HashMap<String, String>();
+ stringDictionary.put(STRING_APP_NAME, mMainPage.getApplicationName());
+
+ IPath path = mMainPage.getLocationPath();
+ IPath defaultLocation = Platform.getLocation();
+ if (!path.equals(defaultLocation)) {
+ description.setLocation(path);
+ }
+
+ if (mMainPage.isNewProject() && !mMainPage.useDefaultLocation() &&
+ !validateNewProjectLocationIsEmpty(path)) {
+ return false;
+ }
+
+ // Create a monitored operation to create the actual project
+ WorkspaceModifyOperation op = new WorkspaceModifyOperation() {
+ @Override
+ protected void execute(IProgressMonitor monitor) throws InvocationTargetException {
+ createProjectAsync(project, description, monitor, parameters, stringDictionary);
+ }
+ };
+
+ // Run the operation in a different thread
+ runAsyncOperation(op);
+ return true;
+ }
+
+ /**
+ * Runs the operation in a different thread and display generated
+ * exceptions.
+ *
+ * @param op The asynchronous operation to run.
+ */
+ private void runAsyncOperation(WorkspaceModifyOperation op) {
+ try {
+ getContainer().run(true /* fork */, true /* cancelable */, op);
+ } catch (InvocationTargetException e) {
+ // The runnable threw an exception
+ Throwable t = e.getTargetException();
+ if (t instanceof CoreException) {
+ CoreException core = (CoreException) t;
+ if (core.getStatus().getCode() == IResourceStatus.CASE_VARIANT_EXISTS) {
+ // The error indicates the file system is not case sensitive
+ // and there's a resource with a similar name.
+ MessageDialog.openError(getShell(), "Error", "Error: Case Variant Exists");
+ } else {
+ ErrorDialog.openError(getShell(), "Error", null, core.getStatus());
+ }
+ } else {
+ // Some other kind of exception
+ MessageDialog.openError(getShell(), "Error", t.getMessage());
+ }
+ e.printStackTrace();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Creates the actual project, sets its nature and adds the required folders
+ * and files to it. This is run asynchronously in a different thread.
+ *
+ * @param project The project to create.
+ * @param description A description of the project.
+ * @param monitor An existing monitor.
+ * @param parameters Template parameters.
+ * @param stringDictionary String definition.
+ * @throws InvocationTargetException to wrap any unmanaged exception and
+ * return it to the calling thread. The method can fail if it fails
+ * to create or modify the project or if it is canceled by the user.
+ */
+ private void createProjectAsync(IProject project, IProjectDescription description,
+ IProgressMonitor monitor, Map<String, Object> parameters,
+ Map<String, String> stringDictionary)
+ throws InvocationTargetException {
+ monitor.beginTask("Create Android Project", 100);
+ try {
+ // Create project and open it
+ project.create(description, new SubProgressMonitor(monitor, 10));
+ if (monitor.isCanceled()) throw new OperationCanceledException();
+ project.open(IResource.BACKGROUND_REFRESH, new SubProgressMonitor(monitor, 10));
+
+ // Add the Java and android nature to the project
+ AndroidNature.setupProjectNatures(project, monitor);
+
+ // Create folders in the project if they don't already exist
+ addDefaultDirectories(project, AndroidConstants.WS_ROOT, DEFAULT_DIRECTORIES, monitor);
+ String[] sourceFolders = new String[] {
+ (String) parameters.get(PARAM_SRC_FOLDER),
+ GEN_SRC_DIRECTORY
+ };
+ addDefaultDirectories(project, AndroidConstants.WS_ROOT, sourceFolders, monitor);
+
+ // Create the resource folders in the project if they don't already exist.
+ addDefaultDirectories(project, RES_DIRECTORY, RES_DIRECTORIES, monitor);
+
+ // Setup class path
+ IJavaProject javaProject = JavaCore.create(project);
+ for (String sourceFolder : sourceFolders) {
+ setupSourceFolder(javaProject, sourceFolder, monitor);
+ }
+
+ if (((Boolean) parameters.get(PARAM_IS_NEW_PROJECT)).booleanValue()) {
+ // Create files in the project if they don't already exist
+ addManifest(project, parameters, stringDictionary, monitor);
+
+ // add the default app icon
+ addIcon(project, monitor);
+
+ // Create the default package components
+ addSampleCode(project, sourceFolders[0], parameters, stringDictionary, monitor);
+
+ // add the string definition file if needed
+ if (stringDictionary.size() > 0) {
+ addStringDictionaryFile(project, stringDictionary, monitor);
+ }
+
+ // Set output location
+ javaProject.setOutputLocation(project.getFolder(BIN_DIRECTORY).getFullPath(),
+ monitor);
+ }
+
+ Sdk.getCurrent().setProject(project, (IAndroidTarget) parameters.get(PARAM_SDK_TARGET),
+ null /* apkConfigMap*/);
+
+ // Fix the project to make sure all properties are as expected.
+ // Necessary for existing projects and good for new ones to.
+ ProjectHelper.fixProject(project);
+
+ } catch (CoreException e) {
+ throw new InvocationTargetException(e);
+ } catch (IOException e) {
+ throw new InvocationTargetException(e);
+ } finally {
+ monitor.done();
+ }
+ }
+
+ /**
+ * Adds default directories to the project.
+ *
+ * @param project The Java Project to update.
+ * @param parentFolder The path of the parent folder. Must end with a
+ * separator.
+ * @param folders Folders to be added.
+ * @param monitor An existing monitor.
+ * @throws CoreException if the method fails to create the directories in
+ * the project.
+ */
+ private void addDefaultDirectories(IProject project, String parentFolder,
+ String[] folders, IProgressMonitor monitor) throws CoreException {
+ for (String name : folders) {
+ if (name.length() > 0) {
+ IFolder folder = project.getFolder(parentFolder + name);
+ if (!folder.exists()) {
+ folder.create(true /* force */, true /* local */,
+ new SubProgressMonitor(monitor, 10));
+ }
+ }
+ }
+ }
+
+ /**
+ * Adds the manifest to the project.
+ *
+ * @param project The Java Project to update.
+ * @param parameters Template Parameters.
+ * @param stringDictionary String List to be added to a string definition
+ * file. This map will be filled by this method.
+ * @param monitor An existing monitor.
+ * @throws CoreException if the method fails to update the project.
+ * @throws IOException if the method fails to create the files in the
+ * project.
+ */
+ private void addManifest(IProject project, Map<String, Object> parameters,
+ Map<String, String> stringDictionary, IProgressMonitor monitor)
+ throws CoreException, IOException {
+
+ // get IFile to the manifest and check if it's not already there.
+ IFile file = project.getFile(AndroidConstants.FN_ANDROID_MANIFEST);
+ if (!file.exists()) {
+
+ // Read manifest template
+ String manifestTemplate = AdtPlugin.readEmbeddedTextFile(TEMPLATE_MANIFEST);
+
+ // Replace all keyword parameters
+ manifestTemplate = replaceParameters(manifestTemplate, parameters);
+
+ if (parameters.containsKey(PARAM_ACTIVITY)) {
+ // now get the activity template
+ String activityTemplate = AdtPlugin.readEmbeddedTextFile(TEMPLATE_ACTIVITIES);
+
+ // Replace all keyword parameters to make main activity.
+ String activities = replaceParameters(activityTemplate, parameters);
+
+ // set the intent.
+ String intent = AdtPlugin.readEmbeddedTextFile(TEMPLATE_INTENT_LAUNCHER);
+
+ // set the intent to the main activity
+ activities = activities.replaceAll(PH_INTENT_FILTERS, intent);
+
+ // set the activity(ies) in the manifest
+ manifestTemplate = manifestTemplate.replaceAll(PH_ACTIVITIES, activities);
+ } else {
+ // remove the activity(ies) from the manifest
+ manifestTemplate = manifestTemplate.replaceAll(PH_ACTIVITIES, "");
+ }
+
+ String minSdkVersion = (String) parameters.get(PARAM_MIN_SDK_VERSION);
+ if (minSdkVersion != null && minSdkVersion.length() > 0) {
+ String usesSdkTemplate = AdtPlugin.readEmbeddedTextFile(TEMPLATE_USES_SDK);
+ String usesSdk = replaceParameters(usesSdkTemplate, parameters);
+ manifestTemplate = manifestTemplate.replaceAll(PH_USES_SDK, usesSdk);
+ } else {
+ manifestTemplate = manifestTemplate.replaceAll(PH_USES_SDK, "");
+ }
+
+ // Save in the project as UTF-8
+ InputStream stream = new ByteArrayInputStream(
+ manifestTemplate.getBytes("UTF-8")); //$NON-NLS-1$
+ file.create(stream, false /* force */, new SubProgressMonitor(monitor, 10));
+ }
+ }
+
+ /**
+ * Adds the string resource file.
+ *
+ * @param project The Java Project to update.
+ * @param strings The list of strings to be added to the string file.
+ * @param monitor An existing monitor.
+ * @throws CoreException if the method fails to update the project.
+ * @throws IOException if the method fails to create the files in the
+ * project.
+ */
+ private void addStringDictionaryFile(IProject project,
+ Map<String, String> strings, IProgressMonitor monitor)
+ throws CoreException, IOException {
+
+ // create the IFile object and check if the file doesn't already exist.
+ IFile file = project.getFile(RES_DIRECTORY + AndroidConstants.WS_SEP
+ + VALUES_DIRECTORY + AndroidConstants.WS_SEP + STRINGS_FILE);
+ if (!file.exists()) {
+ // get the Strings.xml template
+ String stringDefinitionTemplate = AdtPlugin.readEmbeddedTextFile(TEMPLATE_STRINGS);
+
+ // get the template for one string
+ String stringTemplate = AdtPlugin.readEmbeddedTextFile(TEMPLATE_STRING);
+
+ // get all the string names
+ Set<String> stringNames = strings.keySet();
+
+ // loop on it and create the string definitions
+ StringBuilder stringNodes = new StringBuilder();
+ for (String key : stringNames) {
+ // get the value from the key
+ String value = strings.get(key);
+
+ // place them in the template
+ String stringDef = stringTemplate.replace(PARAM_STRING_NAME, key);
+ stringDef = stringDef.replace(PARAM_STRING_CONTENT, value);
+
+ // append to the other string
+ if (stringNodes.length() > 0) {
+ stringNodes.append("\n");
+ }
+ stringNodes.append(stringDef);
+ }
+
+ // put the string nodes in the Strings.xml template
+ stringDefinitionTemplate = stringDefinitionTemplate.replace(PH_STRINGS,
+ stringNodes.toString());
+
+ // write the file as UTF-8
+ InputStream stream = new ByteArrayInputStream(
+ stringDefinitionTemplate.getBytes("UTF-8")); //$NON-NLS-1$
+ file.create(stream, false /* force */, new SubProgressMonitor(monitor, 10));
+ }
+ }
+
+
+ /**
+ * Adds default application icon to the project.
+ *
+ * @param project The Java Project to update.
+ * @param monitor An existing monitor.
+ * @throws CoreException if the method fails to update the project.
+ */
+ private void addIcon(IProject project, IProgressMonitor monitor)
+ throws CoreException {
+ IFile file = project.getFile(RES_DIRECTORY + AndroidConstants.WS_SEP
+ + DRAWABLE_DIRECTORY + AndroidConstants.WS_SEP + ICON);
+ if (!file.exists()) {
+ // read the content from the template
+ byte[] buffer = AdtPlugin.readEmbeddedFile(TEMPLATES_DIRECTORY + ICON);
+
+ // if valid
+ if (buffer != null) {
+ // Save in the project
+ InputStream stream = new ByteArrayInputStream(buffer);
+ file.create(stream, false /* force */, new SubProgressMonitor(monitor, 10));
+ }
+ }
+ }
+
+ /**
+ * Creates the package folder and copies the sample code in the project.
+ *
+ * @param project The Java Project to update.
+ * @param parameters Template Parameters.
+ * @param stringDictionary String List to be added to a string definition
+ * file. This map will be filled by this method.
+ * @param monitor An existing monitor.
+ * @throws CoreException if the method fails to update the project.
+ * @throws IOException if the method fails to create the files in the
+ * project.
+ */
+ private void addSampleCode(IProject project, String sourceFolder,
+ Map<String, Object> parameters, Map<String, String> stringDictionary,
+ IProgressMonitor monitor) throws CoreException, IOException {
+ // create the java package directories.
+ IFolder pkgFolder = project.getFolder(sourceFolder);
+ String packageName = (String) parameters.get(PARAM_PACKAGE);
+
+ // The PARAM_ACTIVITY key will be absent if no activity should be created,
+ // in which case activityName will be null.
+ String activityName = (String) parameters.get(PARAM_ACTIVITY);
+ Map<String, Object> java_activity_parameters = parameters;
+ if (activityName != null) {
+ if (activityName.indexOf('.') >= 0) {
+ // There are package names in the activity name. Transform packageName to add
+ // those sub packages and remove them from activityName.
+ packageName += "." + activityName; //$NON-NLS-1$
+ int pos = packageName.lastIndexOf('.');
+ activityName = packageName.substring(pos + 1);
+ packageName = packageName.substring(0, pos);
+
+ // Also update the values used in the JAVA_FILE_TEMPLATE below
+ // (but not the ones from the manifest so don't change the caller's dictionary)
+ java_activity_parameters = new HashMap<String, Object>(parameters);
+ java_activity_parameters.put(PARAM_PACKAGE, packageName);
+ java_activity_parameters.put(PARAM_ACTIVITY, activityName);
+ }
+ }
+
+ String[] components = packageName.split(AndroidConstants.RE_DOT);
+ for (String component : components) {
+ pkgFolder = pkgFolder.getFolder(component);
+ if (!pkgFolder.exists()) {
+ pkgFolder.create(true /* force */, true /* local */,
+ new SubProgressMonitor(monitor, 10));
+ }
+ }
+
+ if (activityName != null) {
+ // create the main activity Java file
+ String activityJava = activityName + AndroidConstants.DOT_JAVA;
+ IFile file = pkgFolder.getFile(activityJava);
+ if (!file.exists()) {
+ copyFile(JAVA_ACTIVITY_TEMPLATE, file, java_activity_parameters, monitor);
+ }
+ }
+
+ // create the layout file
+ IFolder layoutfolder = project.getFolder(RES_DIRECTORY).getFolder(LAYOUT_DIRECTORY);
+ IFile file = layoutfolder.getFile(MAIN_LAYOUT_XML);
+ if (!file.exists()) {
+ copyFile(LAYOUT_TEMPLATE, file, parameters, monitor);
+ if (activityName != null) {
+ stringDictionary.put(STRING_HELLO_WORLD, "Hello World, " + activityName + "!");
+ } else {
+ stringDictionary.put(STRING_HELLO_WORLD, "Hello World!");
+ }
+ }
+ }
+
+ /**
+ * Adds the given folder to the project's class path.
+ *
+ * @param javaProject The Java Project to update.
+ * @param sourceFolder Template Parameters.
+ * @param monitor An existing monitor.
+ * @throws JavaModelException if the classpath could not be set.
+ */
+ private void setupSourceFolder(IJavaProject javaProject, String sourceFolder,
+ IProgressMonitor monitor) throws JavaModelException {
+ IProject project = javaProject.getProject();
+
+ // Add "src" to class path
+ IFolder srcFolder = project.getFolder(sourceFolder);
+
+ IClasspathEntry[] entries = javaProject.getRawClasspath();
+ entries = removeSourceClasspath(entries, srcFolder);
+ entries = removeSourceClasspath(entries, srcFolder.getParent());
+
+ entries = ProjectHelper.addEntryToClasspath(entries,
+ JavaCore.newSourceEntry(srcFolder.getFullPath()));
+
+ javaProject.setRawClasspath(entries, new SubProgressMonitor(monitor, 10));
+ }
+
+
+ /**
+ * Removes the corresponding source folder from the class path entries if
+ * found.
+ *
+ * @param entries The class path entries to read. A copy will be returned.
+ * @param folder The parent source folder to remove.
+ * @return A new class path entries array.
+ */
+ private IClasspathEntry[] removeSourceClasspath(IClasspathEntry[] entries, IContainer folder) {
+ if (folder == null) {
+ return entries;
+ }
+ IClasspathEntry source = JavaCore.newSourceEntry(folder.getFullPath());
+ int n = entries.length;
+ for (int i = n - 1; i >= 0; i--) {
+ if (entries[i].equals(source)) {
+ IClasspathEntry[] newEntries = new IClasspathEntry[n - 1];
+ if (i > 0) System.arraycopy(entries, 0, newEntries, 0, i);
+ if (i < n - 1) System.arraycopy(entries, i + 1, newEntries, i, n - i - 1);
+ n--;
+ entries = newEntries;
+ }
+ }
+ return entries;
+ }
+
+
+ /**
+ * Copies the given file from our resource folder to the new project.
+ * Expects the file to the US-ASCII or UTF-8 encoded.
+ *
+ * @throws CoreException from IFile if failing to create the new file.
+ * @throws MalformedURLException from URL if failing to interpret the URL.
+ * @throws FileNotFoundException from RandomAccessFile.
+ * @throws IOException from RandomAccessFile.length() if can't determine the
+ * length.
+ */
+ private void copyFile(String resourceFilename, IFile destFile,
+ Map<String, Object> parameters, IProgressMonitor monitor)
+ throws CoreException, IOException {
+
+ // Read existing file.
+ String template = AdtPlugin.readEmbeddedTextFile(
+ TEMPLATES_DIRECTORY + resourceFilename);
+
+ // Replace all keyword parameters
+ template = replaceParameters(template, parameters);
+
+ // Save in the project as UTF-8
+ InputStream stream = new ByteArrayInputStream(template.getBytes("UTF-8")); //$NON-NLS-1$
+ destFile.create(stream, false /* force */, new SubProgressMonitor(monitor, 10));
+ }
+
+ /**
+ * Returns an image descriptor for the wizard logo.
+ */
+ private void setImageDescriptor() {
+ ImageDescriptor desc = AdtPlugin.getImageDescriptor(PROJECT_LOGO_LARGE);
+ setDefaultPageImageDescriptor(desc);
+ }
+
+ /**
+ * Replaces placeholders found in a string with values.
+ *
+ * @param str the string to search for placeholders.
+ * @param parameters a map of <placeholder, Value> to search for in the string
+ * @return A new String object with the placeholder replaced by the values.
+ */
+ private String replaceParameters(String str, Map<String, Object> parameters) {
+ for (Entry<String, Object> entry : parameters.entrySet()) {
+ if (entry.getValue() instanceof String) {
+ str = str.replaceAll(entry.getKey(), (String) entry.getValue());
+ }
+ }
+
+ return str;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/AndroidConstants.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/AndroidConstants.java
new file mode 100644
index 0000000..e201132
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/AndroidConstants.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.common;
+
+import com.android.sdklib.SdkConstants;
+
+import java.io.File;
+import java.util.regex.Pattern;
+
+/**
+ * Constant definition class.<br>
+ * <br>
+ * Most constants have a prefix defining the content.
+ * <ul>
+ * <li><code>WS_</code> Workspace path constant. Those are absolute paths,
+ * from the project root.</li>
+ * <li><code>OS_</code> OS path constant. These paths are different depending on the platform.</li>
+ * <li><code>FN_</code> File name constant.</li>
+ * <li><code>FD_</code> Folder name constant.</li>
+ * <li><code>MARKER_</code> Resource Marker Ids constant.</li>
+ * <li><code>EXT_</code> File extension constant. This does NOT include a dot.</li>
+ * <li><code>DOT_</code> File extension constant. This start with a dot.</li>
+ * <li><code>RE_</code> Regexp constant.</li>
+ * <li><code>NS_</code> Namespace constant.</li>
+ * <li><code>CLASS_</code> Fully qualified class name.</li>
+ * </ul>
+ *
+ */
+public class AndroidConstants {
+ /**
+ * The old Editors Plugin ID. It is still used in some places for compatibility.
+ * Please do not use for new features.
+ */
+ public static final String EDITORS_NAMESPACE = "com.android.ide.eclipse.editors"; // $NON-NLS-1$
+
+ /** Nature of android projects */
+ public final static String NATURE = "com.android.ide.eclipse.adt.AndroidNature"; //$NON-NLS-1$
+
+ /** Separator for workspace path, i.e. "/". */
+ public final static String WS_SEP = "/"; //$NON-NLS-1$
+ /** Separator character for workspace path, i.e. '/'. */
+ public final static char WS_SEP_CHAR = '/';
+
+ /** Extension of the Application package Files, i.e. "apk". */
+ public final static String EXT_ANDROID_PACKAGE = "apk"; //$NON-NLS-1$
+ /** Extension of java files, i.e. "java" */
+ public final static String EXT_JAVA = "java"; //$NON-NLS-1$
+ /** Extension of compiled java files, i.e. "class" */
+ public final static String EXT_CLASS = "class"; //$NON-NLS-1$
+ /** Extension of xml files, i.e. "xml" */
+ public final static String EXT_XML = "xml"; //$NON-NLS-1$
+ /** Extension of jar files, i.e. "jar" */
+ public final static String EXT_JAR = "jar"; //$NON-NLS-1$
+ /** Extension of aidl files, i.e. "aidl" */
+ public final static String EXT_AIDL = "aidl"; //$NON-NLS-1$
+ /** Extension of native libraries, i.e. "so" */
+ public final static String EXT_NATIVE_LIB = "so"; //$NON-NLS-1$
+
+ private final static String DOT = "."; //$NON-NLS-1$
+
+ /** Dot-Extension of the Application package Files, i.e. ".apk". */
+ public final static String DOT_ANDROID_PACKAGE = DOT + EXT_ANDROID_PACKAGE;
+ /** Dot-Extension of java files, i.e. ".java" */
+ public final static String DOT_JAVA = DOT + EXT_JAVA;
+ /** Dot-Extension of compiled java files, i.e. ".class" */
+ public final static String DOT_CLASS = DOT + EXT_CLASS;
+ /** Dot-Extension of xml files, i.e. ".xml" */
+ public final static String DOT_XML = DOT + EXT_XML;
+ /** Dot-Extension of jar files, i.e. ".jar" */
+ public final static String DOT_JAR = DOT + EXT_JAR;
+ /** Dot-Extension of aidl files, i.e. ".aidl" */
+ public final static String DOT_AIDL = DOT + EXT_AIDL;
+
+ /** Name of the manifest file, i.e. "AndroidManifest.xml". */
+ public static final String FN_ANDROID_MANIFEST = "AndroidManifest.xml"; //$NON-NLS-1$
+ public static final String FN_PROJECT_AIDL = "project.aidl"; //$NON-NLS-1$
+
+ /** Name of the android sources directory */
+ public static final String FD_ANDROID_SOURCES = "sources"; //$NON-NLS-1$
+
+ /** Resource java class filename, i.e. "R.java" */
+ public final static String FN_RESOURCE_CLASS = "R.java"; //$NON-NLS-1$
+ /** Resource class file filename, i.e. "R.class" */
+ public final static String FN_COMPILED_RESOURCE_CLASS = "R.class"; //$NON-NLS-1$
+ /** Manifest java class filename, i.e. "Manifest.java" */
+ public final static String FN_MANIFEST_CLASS = "Manifest.java"; //$NON-NLS-1$
+ /** Dex conversion output filname, i.e. "classes.dex" */
+ public final static String FN_CLASSES_DEX = "classes.dex"; //$NON-NLS-1$
+ /** Temporary packaged resources file name, i.e. "resources.ap_" */
+ public final static String FN_RESOURCES_AP_ = "resources.ap_"; //$NON-NLS-1$
+ /** Temporary packaged resources file name for a specific set of configuration */
+ public final static String FN_RESOURCES_S_AP_ = "resources-%s.ap_"; //$NON-NLS-1$
+ public final static Pattern PATTERN_RESOURCES_S_AP_ =
+ Pattern.compile("resources-.*\\.ap_", Pattern.CASE_INSENSITIVE);
+
+ public final static String FN_ADB =
+ (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) ?
+ "adb.exe" : "adb"; //$NON-NLS-1$ //$NON-NLS-2$
+
+ public final static String FN_EMULATOR =
+ (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) ?
+ "emulator.exe" : "emulator"; //$NON-NLS-1$ //$NON-NLS-2$
+
+ public final static String FN_TRACEVIEW =
+ (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) ?
+ "traceview.exe" : "traceview"; //$NON-NLS-1$ //$NON-NLS-2$
+
+ /** Absolute path of the workspace root, i.e. "/" */
+ public final static String WS_ROOT = WS_SEP;
+
+ /** Absolute path of the resource folder, eg "/res".<br> This is a workspace path. */
+ public final static String WS_RESOURCES = WS_SEP + SdkConstants.FD_RESOURCES;
+
+ /** Absolute path of the resource folder, eg "/assets".<br> This is a workspace path. */
+ public final static String WS_ASSETS = WS_SEP + SdkConstants.FD_ASSETS;
+
+ /** Leaf of the javaDoc folder. Does not start with a separator. */
+ public final static String WS_JAVADOC_FOLDER_LEAF = SdkConstants.FD_DOCS + "/reference"; //$NON-NLS-1$
+
+ /** Path of the samples directory relative to the sdk folder.
+ * This is an OS path, ending with a separator.
+ * FIXME: remove once the NPW is fixed. */
+ public final static String OS_SDK_SAMPLES_FOLDER = SdkConstants.FD_SAMPLES + File.separator;
+
+ public final static String RE_DOT = "\\."; //$NON-NLS-1$
+ /** Regexp for java extension, i.e. "\.java$" */
+ public final static String RE_JAVA_EXT = "\\.java$"; //$NON-NLS-1$
+ /** Regexp for aidl extension, i.e. "\.aidl$" */
+ public final static String RE_AIDL_EXT = "\\.aidl$"; //$NON-NLS-1$
+
+ /** Namespace pattern for the custom resource XML, i.e. "http://schemas.android.com/apk/res/%s" */
+ public final static String NS_CUSTOM_RESOURCES = "http://schemas.android.com/apk/res/%1$s"; //$NON-NLS-1$
+
+ /** The old common plug-in ID. Please do not use for new features. */
+ public static final String COMMON_PLUGIN_ID = "com.android.ide.eclipse.common"; //$NON-NLS-1$
+
+ /** aapt marker error when running the compile command */
+ public final static String MARKER_AAPT_COMPILE = COMMON_PLUGIN_ID + ".aaptProblem"; //$NON-NLS-1$
+
+ /** aapt marker error when running the package command */
+ public final static String MARKER_AAPT_PACKAGE = COMMON_PLUGIN_ID + ".aapt2Problem"; //$NON-NLS-1$
+
+ /** XML marker error. */
+ public final static String MARKER_XML = COMMON_PLUGIN_ID + ".xmlProblem"; //$NON-NLS-1$
+
+ /** aidl marker error. */
+ public final static String MARKER_AIDL = COMMON_PLUGIN_ID + ".aidlProblem"; //$NON-NLS-1$
+
+ /** android marker error */
+ public final static String MARKER_ANDROID = COMMON_PLUGIN_ID + ".androidProblem"; //$NON-NLS-1$
+
+ /** Name for the "type" marker attribute */
+ public final static String MARKER_ATTR_TYPE = "android.type"; //$NON-NLS-1$
+ /** Name for the "class" marker attribute */
+ public final static String MARKER_ATTR_CLASS = "android.class"; //$NON-NLS-1$
+ /** activity value for marker attribute "type" */
+ public final static String MARKER_ATTR_TYPE_ACTIVITY = "activity"; //$NON-NLS-1$
+ /** service value for marker attribute "type" */
+ public final static String MARKER_ATTR_TYPE_SERVICE = "service"; //$NON-NLS-1$
+ /** receiver value for marker attribute "type" */
+ public final static String MARKER_ATTR_TYPE_RECEIVER = "receiver"; //$NON-NLS-1$
+ /** provider value for marker attribute "type" */
+ public final static String MARKER_ATTR_TYPE_PROVIDER = "provider"; //$NON-NLS-1$
+
+ public final static String CLASS_ACTIVITY = "android.app.Activity"; //$NON-NLS-1$
+ public final static String CLASS_SERVICE = "android.app.Service"; //$NON-NLS-1$
+ public final static String CLASS_BROADCASTRECEIVER = "android.content.BroadcastReceiver"; //$NON-NLS-1$
+ public final static String CLASS_CONTENTPROVIDER = "android.content.ContentProvider"; //$NON-NLS-1$
+ public final static String CLASS_INSTRUMENTATION = "android.app.Instrumentation"; //$NON-NLS-1$
+ public final static String CLASS_BUNDLE = "android.os.Bundle"; //$NON-NLS-1$
+ public final static String CLASS_R = "android.R"; //$NON-NLS-1$
+ public final static String CLASS_MANIFEST_PERMISSION = "android.Manifest$permission"; //$NON-NLS-1$
+ public final static String CLASS_INTENT = "android.content.Intent"; //$NON-NLS-1$
+ public final static String CLASS_CONTEXT = "android.content.Context"; //$NON-NLS-1$
+ public final static String CLASS_VIEW = "android.view.View"; //$NON-NLS-1$
+ public final static String CLASS_VIEWGROUP = "android.view.ViewGroup"; //$NON-NLS-1$
+ public final static String CLASS_LAYOUTPARAMS = "LayoutParams"; //$NON-NLS-1$
+ public final static String CLASS_VIEWGROUP_LAYOUTPARAMS =
+ CLASS_VIEWGROUP + "$" + CLASS_LAYOUTPARAMS; //$NON-NLS-1$
+ public final static String CLASS_FRAMELAYOUT = "FrameLayout"; //$NON-NLS-1$
+ public final static String CLASS_PREFERENCE = "android.preference.Preference"; //$NON-NLS-1$
+ public final static String CLASS_PREFERENCE_SCREEN = "PreferenceScreen"; //$NON-NLS-1$
+ public final static String CLASS_PREFERENCES =
+ "android.preference." + CLASS_PREFERENCE_SCREEN; //$NON-NLS-1$
+ public final static String CLASS_PREFERENCEGROUP = "android.preference.PreferenceGroup"; //$NON-NLS-1$
+ public final static String CLASS_PARCELABLE = "android.os.Parcelable"; //$NON-NLS-1$
+
+ public final static String CLASS_BRIDGE = "com.android.layoutlib.bridge.Bridge"; //$NON-NLS-1$
+
+ /**
+ * Prefered compiler level, i.e. "1.5".
+ */
+ public final static String COMPILER_COMPLIANCE_PREFERRED = "1.5"; //$NON-NLS-1$
+ /**
+ * List of valid compiler level, i.e. "1.5" and "1.6"
+ */
+ public final static String[] COMPILER_COMPLIANCE = {
+ "1.5", //$NON-NLS-1$
+ "1.6", //$NON-NLS-1$
+ };
+
+ /** The base URL where to find the Android class & manifest documentation */
+ public static final String CODESITE_BASE_URL = "http://code.google.com/android"; //$NON-NLS-1$
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/EclipseUiHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/EclipseUiHelper.java
new file mode 100644
index 0000000..6dc8562
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/EclipseUiHelper.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.common;
+
+import org.eclipse.ui.IViewPart;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.PlatformUI;
+
+/**
+ * Helpers for Eclipse UI related stuff.
+ */
+public final class EclipseUiHelper {
+
+ /** View Id for the default Eclipse Content Outline view. */
+ public static final String CONTENT_OUTLINE_VIEW_ID = "org.eclipse.ui.views.ContentOutline";
+ /** View Id for the default Eclipse Property Sheet view. */
+ public static final String PROPERTY_SHEET_VIEW_ID = "org.eclipse.ui.views.PropertySheet";
+
+ /** This class never gets instantiated. */
+ private EclipseUiHelper() {
+ }
+
+ /**
+ * Shows the corresponding view.
+ * <p/>
+ * Silently fails in case of error.
+ *
+ * @param viewId One of {@link #CONTENT_OUTLINE_VIEW_ID}, {@link #PROPERTY_SHEET_VIEW_ID}.
+ * @param activate True to force activate (i.e. takes focus), false to just make visible (i.e.
+ * does not steal focus.)
+ */
+ public static void showView(String viewId, boolean activate) {
+ IWorkbenchWindow win = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
+ if (win != null) {
+ IWorkbenchPage page = win.getActivePage();
+ if (page != null) {
+ try {
+ IViewPart part = page.showView(viewId,
+ null /* secondaryId */,
+ activate ? IWorkbenchPage.VIEW_ACTIVATE : IWorkbenchPage.VIEW_VISIBLE);
+ } catch (PartInitException e) {
+ // ignore
+ }
+ }
+ }
+
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/Messages.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/Messages.java
new file mode 100644
index 0000000..3f1bde4
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/Messages.java
@@ -0,0 +1,21 @@
+
+package com.android.ide.eclipse.common;
+
+import org.eclipse.osgi.util.NLS;
+
+public class Messages extends NLS {
+ private static final String BUNDLE_NAME = "com.android.ide.eclipse.common.messages"; //$NON-NLS-1$
+
+ public static String Console_Data_Project_Tag;
+
+ public static String Console_Date_Tag;
+
+
+ static {
+ // initialize resource bundle
+ NLS.initializeMessages(BUNDLE_NAME, Messages.class);
+ }
+
+ private Messages() {
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/SdkStatsHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/SdkStatsHelper.java
new file mode 100644
index 0000000..345c663
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/SdkStatsHelper.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.common;
+
+import com.android.sdkstats.SdkStatsService;
+
+import org.osgi.framework.Version;
+
+/**
+ * Helper class to access the ping usage stat server.
+ */
+public class SdkStatsHelper {
+
+ /**
+ * Pings the usage start server.
+ * @param pluginName the name of the plugin to appear in the stats
+ * @param pluginVersion the {@link Version} of the plugin.
+ */
+ public static void pingUsageServer(String pluginName, Version pluginVersion) {
+ String versionString = String.format("%1$d.%2$d.%3$d", pluginVersion.getMajor(),
+ pluginVersion.getMinor(), pluginVersion.getMicro());
+
+ SdkStatsService.ping(pluginName, versionString);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/StreamHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/StreamHelper.java
new file mode 100644
index 0000000..6ccf4f2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/StreamHelper.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.common;
+
+import org.eclipse.ui.console.MessageConsoleStream;
+
+import java.util.Calendar;
+
+/**
+ * Stream helper class.
+ */
+public class StreamHelper {
+
+ /**
+ * Prints messages, associated with a project to the specified stream
+ * @param stream The stream to write to
+ * @param tag The tag associated to the message. Can be null
+ * @param objects The objects to print through their toString() method (or directly for
+ * {@link String} objects.
+ */
+ public static synchronized void printToStream(MessageConsoleStream stream, String tag,
+ Object... objects) {
+ String dateTag = getMessageTag(tag);
+
+ for (Object obj : objects) {
+ stream.print(dateTag);
+ if (obj instanceof String) {
+ stream.println((String)obj);
+ } else {
+ stream.println(obj.toString());
+ }
+ }
+ }
+
+ /**
+ * 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
+ */
+ public static String getMessageTag(String tag) {
+ Calendar c = Calendar.getInstance();
+
+ if (tag == null) {
+ return String.format(Messages.Console_Date_Tag, c);
+ }
+
+ return String.format(Messages.Console_Data_Project_Tag, c, tag);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/messages.properties b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/messages.properties
new file mode 100644
index 0000000..dba6edc
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/messages.properties
@@ -0,0 +1,2 @@
+Console_Date_Tag=[%1$tF %1$tT]
+Console_Data_Project_Tag=[%1$tF %1$tT - %2$s]
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/preferences/UsagePreferencePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/preferences/UsagePreferencePage.java
new file mode 100644
index 0000000..58c2f40
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/preferences/UsagePreferencePage.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.common.preferences;
+
+import com.android.sdkstats.SdkStatsService;
+
+import org.eclipse.jface.preference.BooleanFieldEditor;
+import org.eclipse.jface.preference.PreferencePage;
+import org.eclipse.jface.preference.PreferenceStore;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Link;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.IWorkbenchPreferencePage;
+
+import java.io.IOException;
+
+public class UsagePreferencePage extends PreferencePage implements IWorkbenchPreferencePage {
+
+ private BooleanFieldEditor mOptInCheckBox;
+
+ public UsagePreferencePage() {
+ }
+
+ public void init(IWorkbench workbench) {
+ // pass
+ }
+
+ @Override
+ protected Control createContents(Composite parent) {
+ Composite top = new Composite(parent, SWT.NONE);
+ top.setLayout(new GridLayout(1, false));
+ top.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ Link text = new Link(top, SWT.WRAP);
+ GridData gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.widthHint = 200;
+ text.setLayoutData(gd);
+ text.setText(SdkStatsService.BODY_TEXT);
+
+ text.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent event) {
+ SdkStatsService.openUrl(event.text);
+ }
+ });
+
+ mOptInCheckBox = new BooleanFieldEditor(SdkStatsService.PING_OPT_IN,
+ SdkStatsService.CHECKBOX_TEXT, top);
+ mOptInCheckBox.setPage(this);
+ mOptInCheckBox.setPreferenceStore(SdkStatsService.getPreferenceStore());
+ mOptInCheckBox.load();
+
+ return top;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jface.preference.PreferencePage#performCancel()
+ */
+ @Override
+ public boolean performCancel() {
+ mOptInCheckBox.load();
+ return super.performCancel();
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jface.preference.PreferencePage#performDefaults()
+ */
+ @Override
+ protected void performDefaults() {
+ mOptInCheckBox.loadDefault();
+ super.performDefaults();
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jface.preference.PreferencePage#performOk()
+ */
+ @Override
+ public boolean performOk() {
+ save();
+ return super.performOk();
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jface.preference.PreferencePage#performApply()
+ */
+ @Override
+ protected void performApply() {
+ save();
+ super.performApply();
+ }
+
+ private void save() {
+ try {
+ PreferenceStore store = SdkStatsService.getPreferenceStore();
+ if (store != null) {
+ store.setValue(SdkStatsService.PING_OPT_IN, mOptInCheckBox.getBooleanValue());
+ store.save();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidManifestHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidManifestHelper.java
new file mode 100644
index 0000000..cd238d2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidManifestHelper.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.common.project;
+
+import com.android.ide.eclipse.common.AndroidConstants;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.xml.sax.InputSource;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathExpressionException;
+
+/**
+ * Utility class that manages the AndroidManifest.xml file.
+ * <p/>
+ * All the get method work by XPath. Repeated calls to those may warrant using
+ * {@link AndroidManifestParser} instead.
+ */
+public class AndroidManifestHelper {
+ private IFile mManifestIFile;
+ private File mManifestFile;
+ private XPath mXPath;
+
+ /**
+ * Creates an AndroidManifest based on an existing Eclipse {@link IProject} object.
+ * </p>
+ * Use {@link #exists()} to check if the manifest file really exists in the project.
+ *
+ * @param project The project to search for the manifest.
+ */
+ public AndroidManifestHelper(IProject project) {
+ mXPath = AndroidXPathFactory.newXPath();
+ mManifestIFile = getManifest(project);
+ }
+
+ /**
+ * Creates an AndroidManifest based on a file path.
+ * <p/>
+ * Use {@link #exists()} to check if the manifest file really exists.
+ *
+ * @param osManifestFilePath the os path to the AndroidManifest.xml file.
+ */
+ public AndroidManifestHelper(String osManifestFilePath) {
+ mXPath = AndroidXPathFactory.newXPath();
+ mManifestFile = new File(osManifestFilePath);
+ }
+
+
+ /**
+ * Returns the underlying {@link IFile} for the android manifest XML file, if found in the
+ * given Eclipse project.
+ *
+ * Always return null if the constructor that takes an {@link IProject} was NOT called.
+ *
+ * @return The IFile for the androidManifest.xml or null if no such file could be found.
+ */
+ public IFile getManifestIFile() {
+ return mManifestIFile;
+ }
+
+ /**
+ * Returns the underlying {@link File} for the android manifest XML file.
+ */
+ public File getManifestFile() {
+ if (mManifestIFile != null) {
+ return mManifestIFile.getLocation().toFile();
+ }
+
+ return mManifestFile;
+ }
+
+ /**
+ * Returns the package name defined in the manifest file.
+ *
+ * @return A String object with the package or null if any error happened.
+ */
+ public String getPackageName() {
+ try {
+ return mXPath.evaluate("/manifest/@package", getSource()); //$NON-NLS-1$
+ } catch (XPathExpressionException e1) {
+ // If the XPath failed to evaluate, we'll return null.
+ } catch (Exception e) {
+ // if this happens this is due to the resource being out of sync.
+ // so we must refresh it and do it again
+
+ // for any other kind of exception we must return null as well;
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the minSdkVersion defined in the manifest file.
+ *
+ * @return A String object with the package or null if any error happened.
+ */
+ public String getMinSdkVersion() {
+ try {
+ return mXPath.evaluate("/manifest/uses-sdk/@" //$NON-NLS-1$
+ + AndroidXPathFactory.DEFAULT_NS_PREFIX
+ + ":minSdkVersion", getSource()); //$NON-NLS-1$
+ } catch (XPathExpressionException e1) {
+ // If the XPath failed to evaluate, we'll return null.
+ } catch (Exception e) {
+ // if this happens this is due to the resource being out of sync.
+ // so we must refresh it and do it again
+
+ // for any other kind of exception we must return null as well;
+ }
+
+ return null;
+ }
+ /**
+ * Returns the i-th activity defined in the manifest file.
+ *
+ * @param index The 1-based index of the activity to return.
+ * @return A String object with the activity or null if any error happened.
+ */
+ public String getActivityName(int index) {
+ try {
+ return mXPath.evaluate("/manifest/application/activity[" //$NON-NLS-1$
+ + index
+ + "]/@" //$NON-NLS-1$
+ + AndroidXPathFactory.DEFAULT_NS_PREFIX +":name", //$NON-NLS-1$
+ getSource());
+ } catch (XPathExpressionException e1) {
+ // If the XPath failed to evaluate, we'll return null.
+ } catch (Exception e) {
+ // if this happens this is due to the resource being out of sync.
+ // so we must refresh it and do it again
+
+ // for any other kind of exception we must return null as well;
+ }
+ return null;
+ }
+
+ /**
+ * Returns an IFile object representing the manifest for the specified
+ * project.
+ *
+ * @param project The project containing the manifest file.
+ * @return An IFile object pointing to the manifest or null if the manifest
+ * is missing.
+ */
+ public static IFile getManifest(IProject project) {
+ IResource r = project.findMember(AndroidConstants.WS_SEP
+ + AndroidConstants.FN_ANDROID_MANIFEST);
+
+ if (r == null || r.exists() == false || (r instanceof IFile) == false) {
+ return null;
+ }
+ return (IFile) r;
+ }
+
+ /**
+ * Combines a java package, with a class value from the manifest to make a fully qualified
+ * class name
+ * @param javaPackage the java package from the manifest.
+ * @param className the class name from the manifest.
+ * @return the fully qualified class name.
+ */
+ public static String combinePackageAndClassName(String javaPackage, String className) {
+ if (className == null || className.length() == 0) {
+ return javaPackage;
+ }
+ if (javaPackage == null || javaPackage.length() == 0) {
+ return className;
+ }
+
+ // the class name can be a subpackage (starts with a '.'
+ // char), a simple class name (no dot), or a full java package
+ boolean startWithDot = (className.charAt(0) == '.');
+ boolean hasDot = (className.indexOf('.') != -1);
+ if (startWithDot || hasDot == false) {
+
+ // add the concatenation of the package and class name
+ if (startWithDot) {
+ return javaPackage + className;
+ } else {
+ return javaPackage + '.' + className;
+ }
+ } else {
+ // just add the class as it should be a fully qualified java name.
+ return className;
+ }
+ }
+
+
+
+ /**
+ * Returns true either if an androidManifest.xml file was found in the project
+ * or if the given file path exists.
+ */
+ public boolean exists() {
+ if (mManifestIFile != null) {
+ return mManifestIFile.exists();
+ } else if (mManifestFile != null) {
+ return mManifestFile.exists();
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns an InputSource for XPath.
+ *
+ * @throws FileNotFoundException if file does not exist.
+ * @throws CoreException if the {@link IFile} does not exist.
+ */
+ private InputSource getSource() throws FileNotFoundException, CoreException {
+ if (mManifestIFile != null) {
+ return new InputSource(mManifestIFile.getContents());
+ } else if (mManifestFile != null) {
+ return new InputSource(new FileReader(mManifestFile));
+ }
+
+ return null;
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidManifestParser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidManifestParser.java
new file mode 100644
index 0000000..850c59d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidManifestParser.java
@@ -0,0 +1,661 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.common.project;
+
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.common.project.XmlErrorHandler.XmlErrorListener;
+import com.android.sdklib.SdkConstants;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.jdt.core.IJavaProject;
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.Locator;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Set;
+import java.util.TreeSet;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+public class AndroidManifestParser {
+
+ private final static String ATTRIBUTE_PACKAGE = "package"; //$NON-NLS-1$
+ private final static String ATTRIBUTE_NAME = "name"; //$NON-NLS-1$
+ private final static String ATTRIBUTE_PROCESS = "process"; //$NON-NLS-$
+ private final static String ATTRIBUTE_DEBUGGABLE = "debuggable"; //$NON-NLS-$
+ private final static String ATTRIBUTE_MIN_SDK_VERSION = "minSdkVersion"; //$NON-NLS-$
+ private final static String NODE_MANIFEST = "manifest"; //$NON-NLS-1$
+ private final static String NODE_APPLICATION = "application"; //$NON-NLS-1$
+ private final static String NODE_ACTIVITY = "activity"; //$NON-NLS-1$
+ private final static String NODE_SERVICE = "service"; //$NON-NLS-1$
+ private final static String NODE_RECEIVER = "receiver"; //$NON-NLS-1$
+ private final static String NODE_PROVIDER = "provider"; //$NON-NLS-1$
+ private final static String NODE_INTENT = "intent-filter"; //$NON-NLS-1$
+ private final static String NODE_ACTION = "action"; //$NON-NLS-1$
+ private final static String NODE_CATEGORY = "category"; //$NON-NLS-1$
+ private final static String NODE_USES_SDK = "uses-sdk"; //$NON-NLS-1$
+
+ private final static int LEVEL_MANIFEST = 0;
+ private final static int LEVEL_APPLICATION = 1;
+ private final static int LEVEL_ACTIVITY = 2;
+ private final static int LEVEL_INTENT_FILTER = 3;
+ private final static int LEVEL_CATEGORY = 4;
+
+ private final static String ACTION_MAIN = "android.intent.action.MAIN"; //$NON-NLS-1$
+ private final static String CATEGORY_LAUNCHER = "android.intent.category.LAUNCHER"; //$NON-NLS-1$
+
+ private static class ManifestHandler extends XmlErrorHandler {
+
+ //--- data read from the parsing
+
+ /** Application package */
+ private String mPackage;
+ /** List of all activities */
+ private final ArrayList<String> mActivities = new ArrayList<String>();
+ /** Launcher activity */
+ private String mLauncherActivity = null;
+ /** list of process names declared by the manifest */
+ private Set<String> mProcesses = null;
+ /** debuggable attribute value. If null, the attribute is not present. */
+ private Boolean mDebuggable = null;
+ /** API level requirement. if 0 the attribute was not present. */
+ private int mApiLevelRequirement = 0;
+
+ //--- temporary data/flags used during parsing
+ private IJavaProject mJavaProject;
+ private boolean mGatherData = false;
+ private boolean mMarkErrors = false;
+ private int mCurrentLevel = 0;
+ private int mValidLevel = 0;
+ private boolean mFoundMainAction = false;
+ private boolean mFoundLauncherCategory = false;
+ private String mCurrentActivity = null;
+ private Locator mLocator;
+
+ /**
+ *
+ * @param manifestFile
+ * @param errorListener
+ * @param gatherData
+ * @param javaProject
+ * @param markErrors
+ */
+ ManifestHandler(IFile manifestFile, XmlErrorListener errorListener,
+ boolean gatherData, IJavaProject javaProject, boolean markErrors) {
+ super(manifestFile, errorListener);
+ mGatherData = gatherData;
+ mJavaProject = javaProject;
+ mMarkErrors = markErrors;
+ }
+
+ /**
+ * Returns the package defined in the manifest, if found.
+ * @return The package name or null if not found.
+ */
+ String getPackage() {
+ return mPackage;
+ }
+
+ /**
+ * Returns the list of activities found in the manifest.
+ * @return An array of fully qualified class names, or empty if no activity were found.
+ */
+ String[] getActivities() {
+ return mActivities.toArray(new String[mActivities.size()]);
+ }
+
+ /**
+ * Returns the name of one activity found in the manifest, that is configured to show
+ * up in the HOME screen.
+ * @return the fully qualified name of a HOME activity or null if none were found.
+ */
+ String getLauncherActivity() {
+ return mLauncherActivity;
+ }
+
+ /**
+ * Returns the list of process names declared by the manifest.
+ */
+ String[] getProcesses() {
+ if (mProcesses != null) {
+ return mProcesses.toArray(new String[mProcesses.size()]);
+ }
+
+ return new String[0];
+ }
+
+ /**
+ * Returns the <code>debuggable</code> attribute value or null if it is not set.
+ */
+ Boolean getDebuggable() {
+ return mDebuggable;
+ }
+
+ /**
+ * Returns the <code>minSdkVersion</code> attribute, or 0 if it's not set.
+ */
+ int getApiLevelRequirement() {
+ return mApiLevelRequirement;
+ }
+
+ /* (non-Javadoc)
+ * @see org.xml.sax.helpers.DefaultHandler#setDocumentLocator(org.xml.sax.Locator)
+ */
+ @Override
+ public void setDocumentLocator(Locator locator) {
+ mLocator = locator;
+ super.setDocumentLocator(locator);
+ }
+
+ /* (non-Javadoc)
+ * @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, java.lang.String,
+ * java.lang.String, org.xml.sax.Attributes)
+ */
+ @Override
+ public void startElement(String uri, String localName, String name, Attributes attributes)
+ throws SAXException {
+ try {
+ if (mGatherData == false) {
+ return;
+ }
+
+ // if we're at a valid level
+ if (mValidLevel == mCurrentLevel) {
+ String value;
+ switch (mValidLevel) {
+ case LEVEL_MANIFEST:
+ if (NODE_MANIFEST.equals(localName)) {
+ // lets get the package name.
+ mPackage = getAttributeValue(attributes, ATTRIBUTE_PACKAGE,
+ false /* hasNamespace */);
+ mValidLevel++;
+ }
+ break;
+ case LEVEL_APPLICATION:
+ if (NODE_APPLICATION.equals(localName)) {
+ value = getAttributeValue(attributes, ATTRIBUTE_PROCESS,
+ true /* hasNamespace */);
+ if (value != null) {
+ addProcessName(value);
+ }
+
+ value = getAttributeValue(attributes, ATTRIBUTE_DEBUGGABLE,
+ true /* hasNamespace*/);
+ if (value != null) {
+ mDebuggable = Boolean.parseBoolean(value);
+ }
+
+ mValidLevel++;
+ } else if (NODE_USES_SDK.equals(localName)) {
+ value = getAttributeValue(attributes, ATTRIBUTE_MIN_SDK_VERSION,
+ true /* hasNamespace */);
+
+ try {
+ mApiLevelRequirement = Integer.parseInt(value);
+ } catch (NumberFormatException e) {
+ handleError(e, -1 /* lineNumber */);
+ }
+ }
+ break;
+ case LEVEL_ACTIVITY:
+ if (NODE_ACTIVITY.equals(localName)) {
+ processActivityNode(attributes);
+ mValidLevel++;
+ } else if (NODE_SERVICE.equals(localName)) {
+ processNode(attributes, AndroidConstants.CLASS_SERVICE);
+ mValidLevel++;
+ } else if (NODE_RECEIVER.equals(localName)) {
+ processNode(attributes, AndroidConstants.CLASS_BROADCASTRECEIVER);
+ mValidLevel++;
+ } else if (NODE_PROVIDER.equals(localName)) {
+ processNode(attributes, AndroidConstants.CLASS_CONTENTPROVIDER);
+ mValidLevel++;
+ }
+ break;
+ case LEVEL_INTENT_FILTER:
+ // only process this level if we are in an activity
+ if (mCurrentActivity != null && NODE_INTENT.equals(localName)) {
+ // if we're at the intent level, lets reset some flag to
+ // be used when parsing the children
+ mFoundMainAction = false;
+ mFoundLauncherCategory = false;
+ mValidLevel++;
+ }
+ break;
+ case LEVEL_CATEGORY:
+ if (mCurrentActivity != null && mLauncherActivity == null) {
+ if (NODE_ACTION.equals(localName)) {
+ // get the name attribute
+ if (ACTION_MAIN.equals(
+ getAttributeValue(attributes, ATTRIBUTE_NAME,
+ true /* hasNamespace */))) {
+ mFoundMainAction = true;
+ }
+ } else if (NODE_CATEGORY.equals(localName)) {
+ if (CATEGORY_LAUNCHER.equals(
+ getAttributeValue(attributes, ATTRIBUTE_NAME,
+ true /* hasNamespace */))) {
+ mFoundLauncherCategory = true;
+ }
+ }
+
+ // no need to increase mValidLevel as we don't process anything
+ // below this level.
+ }
+ break;
+ }
+ }
+
+ mCurrentLevel++;
+ } finally {
+ super.startElement(uri, localName, name, attributes);
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String, java.lang.String,
+ * java.lang.String)
+ */
+ @Override
+ public void endElement(String uri, String localName, String name) throws SAXException {
+ try {
+ if (mGatherData == false) {
+ return;
+ }
+
+ // decrement the levels.
+ if (mValidLevel == mCurrentLevel) {
+ mValidLevel--;
+ }
+ mCurrentLevel--;
+
+ // if we're at a valid level
+ // process the end of the element
+ if (mValidLevel == mCurrentLevel) {
+ switch (mValidLevel) {
+ case LEVEL_ACTIVITY:
+ mCurrentActivity = null;
+ break;
+ case LEVEL_INTENT_FILTER:
+ // if we found both a main action and a launcher category, this is our
+ // launcher activity!
+ if (mCurrentActivity != null &&
+ mFoundMainAction && mFoundLauncherCategory) {
+ mLauncherActivity = mCurrentActivity;
+ }
+ break;
+ default:
+ break;
+ }
+
+ }
+ } finally {
+ super.endElement(uri, localName, name);
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see org.xml.sax.helpers.DefaultHandler#error(org.xml.sax.SAXParseException)
+ */
+ @Override
+ public void error(SAXParseException e) {
+ if (mMarkErrors) {
+ handleError(e, e.getLineNumber());
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see org.xml.sax.helpers.DefaultHandler#fatalError(org.xml.sax.SAXParseException)
+ */
+ @Override
+ public void fatalError(SAXParseException e) {
+ if (mMarkErrors) {
+ handleError(e, e.getLineNumber());
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see org.xml.sax.helpers.DefaultHandler#warning(org.xml.sax.SAXParseException)
+ */
+ @Override
+ public void warning(SAXParseException e) throws SAXException {
+ if (mMarkErrors) {
+ super.warning(e);
+ }
+ }
+
+ /**
+ * Processes the activity node.
+ * @param attributes the attributes for the activity node.
+ */
+ private void processActivityNode(Attributes attributes) {
+ // lets get the activity name, and add it to the list
+ String activityName = getAttributeValue(attributes, ATTRIBUTE_NAME,
+ true /* hasNamespace */);
+ if (activityName != null) {
+ mCurrentActivity = AndroidManifestHelper.combinePackageAndClassName(mPackage,
+ activityName);
+ mActivities.add(mCurrentActivity);
+
+ if (mMarkErrors) {
+ checkClass(mCurrentActivity, AndroidConstants.CLASS_ACTIVITY,
+ true /* testVisibility */);
+ }
+ } else {
+ // no activity found! Aapt will output an error,
+ // so we don't have to do anything
+ mCurrentActivity = activityName;
+ }
+
+ String processName = getAttributeValue(attributes, ATTRIBUTE_PROCESS,
+ true /* hasNamespace */);
+ if (processName != null) {
+ addProcessName(processName);
+ }
+ }
+
+ /**
+ * Processes the service/receiver/provider nodes.
+ * @param attributes the attributes for the activity node.
+ * @param superClassName the fully qualified name of the super class that this
+ * node is representing
+ */
+ private void processNode(Attributes attributes, String superClassName) {
+ // lets get the class name, and check it if required.
+ String serviceName = getAttributeValue(attributes, ATTRIBUTE_NAME,
+ true /* hasNamespace */);
+ if (serviceName != null) {
+ serviceName = AndroidManifestHelper.combinePackageAndClassName(mPackage,
+ serviceName);
+
+ if (mMarkErrors) {
+ checkClass(serviceName, superClassName, false /* testVisibility */);
+ }
+ }
+
+ String processName = getAttributeValue(attributes, ATTRIBUTE_PROCESS,
+ true /* hasNamespace */);
+ if (processName != null) {
+ addProcessName(processName);
+ }
+ }
+
+ /**
+ * Checks that a class is valid and can be used in the Android Manifest.
+ * <p/>
+ * Errors are put as {@link IMarker} on the manifest file.
+ * @param className the fully qualified name of the class to test.
+ * @param superClassName the fully qualified name of the class it is supposed to extend.
+ * @param testVisibility if <code>true</code>, the method will check the visibility of
+ * the class or of its constructors.
+ */
+ private void checkClass(String className, String superClassName, boolean testVisibility) {
+ // we need to check the validity of the activity.
+ String result = BaseProjectHelper.testClassForManifest(mJavaProject,
+ className, superClassName, testVisibility);
+ if (result != BaseProjectHelper.TEST_CLASS_OK) {
+ // get the line number
+ int line = mLocator.getLineNumber();
+
+ // mark the file
+ IMarker marker = BaseProjectHelper.addMarker(getFile(),
+ AndroidConstants.MARKER_ANDROID,
+ result, line, IMarker.SEVERITY_ERROR);
+
+ // add custom attributes to be used by the manifest editor.
+ if (marker != null) {
+ try {
+ marker.setAttribute(AndroidConstants.MARKER_ATTR_TYPE,
+ AndroidConstants.MARKER_ATTR_TYPE_ACTIVITY);
+ marker.setAttribute(AndroidConstants.MARKER_ATTR_CLASS, className);
+ } catch (CoreException e) {
+ }
+ }
+ }
+
+ }
+
+ /**
+ * Searches through the attributes list for a particular one and returns its value.
+ * @param attributes the attribute list to search through
+ * @param attributeName the name of the attribute to look for.
+ * @param hasNamespace Indicates whether the attribute has an android namespace.
+ * @return a String with the value or null if the attribute was not found.
+ * @see SdkConstants#NS_RESOURCES
+ */
+ private String getAttributeValue(Attributes attributes, String attributeName,
+ boolean hasNamespace) {
+ int count = attributes.getLength();
+ for (int i = 0 ; i < count ; i++) {
+ if (attributeName.equals(attributes.getLocalName(i)) &&
+ ((hasNamespace &&
+ SdkConstants.NS_RESOURCES.equals(attributes.getURI(i))) ||
+ (hasNamespace == false && attributes.getURI(i).length() == 0))) {
+ return attributes.getValue(i);
+ }
+ }
+
+ return null;
+ }
+
+ private void addProcessName(String processName) {
+ if (mProcesses == null) {
+ mProcesses = new TreeSet<String>();
+ }
+
+ mProcesses.add(processName);
+ }
+ }
+
+ private static SAXParserFactory sParserFactory;
+
+ private final String mJavaPackage;
+ private final String[] mActivities;
+ private final String mLauncherActivity;
+ private final String[] mProcesses;
+ private final Boolean mDebuggable;
+ private final int mApiLevelRequirement;
+
+ static {
+ sParserFactory = SAXParserFactory.newInstance();
+ sParserFactory.setNamespaceAware(true);
+ }
+
+ /**
+ * Parses the Android Manifest, and returns an object containing
+ * the result of the parsing.
+ * @param javaProject The java project.
+ * @param manifestFile the {@link IFile} representing the manifest file.
+ * @param errorListener
+ * @param gatherData indicates whether the parsing will extract data from the manifest.
+ * @param markErrors indicates whether the error found during parsing should put a
+ * marker on the file. For class validation errors to put a marker, <code>gatherData</code>
+ * must be set to <code>true</code>
+ * @return an {@link AndroidManifestParser} or null if the parsing failed.
+ * @throws CoreException
+ */
+ public static AndroidManifestParser parse(IJavaProject javaProject, IFile manifestFile,
+ XmlErrorListener errorListener, boolean gatherData, boolean markErrors)
+ throws CoreException {
+ try {
+ SAXParser parser = sParserFactory.newSAXParser();
+
+ ManifestHandler manifestHandler = new ManifestHandler(manifestFile,
+ errorListener, gatherData, javaProject, markErrors);
+
+ parser.parse(new InputSource(manifestFile.getContents()), manifestHandler);
+
+ // get the result from the handler
+
+ return new AndroidManifestParser(manifestHandler.getPackage(),
+ manifestHandler.getActivities(), manifestHandler.getLauncherActivity(),
+ manifestHandler.getProcesses(), manifestHandler.getDebuggable(),
+ manifestHandler.getApiLevelRequirement());
+ } catch (ParserConfigurationException e) {
+ } catch (SAXException e) {
+ } catch (IOException e) {
+ } finally {
+ }
+
+ return null;
+ }
+
+ /**
+ * Parses the Android Manifest for the specified project, and returns an object containing
+ * the result of the parsing.
+ * @param javaProject The java project. Required if <var>markErrors</var> is <code>true</code>
+ * @param errorListener the {@link XmlErrorListener} object being notified of the presence
+ * of errors. Optional.
+ * @param gatherData indicates whether the parsing will extract data from the manifest.
+ * @param markErrors indicates whether the error found during parsing should put a
+ * marker on the file. For class validation errors to put a marker, <code>gatherData</code>
+ * must be set to <code>true</code>
+ * @return an {@link AndroidManifestParser} or null if the parsing failed.
+ * @throws CoreException
+ */
+ public static AndroidManifestParser parse(IJavaProject javaProject,
+ XmlErrorListener errorListener, boolean gatherData, boolean markErrors)
+ throws CoreException {
+ try {
+ SAXParser parser = sParserFactory.newSAXParser();
+
+ IFile manifestFile = AndroidManifestHelper.getManifest(javaProject.getProject());
+ if (manifestFile != null) {
+ ManifestHandler manifestHandler = new ManifestHandler(manifestFile,
+ errorListener, gatherData, javaProject, markErrors);
+
+ parser.parse(new InputSource(manifestFile.getContents()), manifestHandler);
+
+ // get the result from the handler
+ return new AndroidManifestParser(manifestHandler.getPackage(),
+ manifestHandler.getActivities(), manifestHandler.getLauncherActivity(),
+ manifestHandler.getProcesses(), manifestHandler.getDebuggable(),
+ manifestHandler.getApiLevelRequirement());
+ }
+ } catch (ParserConfigurationException e) {
+ } catch (SAXException e) {
+ } catch (IOException e) {
+ } finally {
+ }
+
+ return null;
+ }
+
+ /**
+ * Parses the manifest file, collects data, and checks for errors.
+ * @param javaProject The java project. Required.
+ * @param manifestFile The manifest file to parse.
+ * @param errorListener the {@link XmlErrorListener} object being notified of the presence
+ * of errors. Optional.
+ * @return an {@link AndroidManifestParser} or null if the parsing failed.
+ * @throws CoreException
+ */
+ public static AndroidManifestParser parseForError(IJavaProject javaProject, IFile manifestFile,
+ XmlErrorListener errorListener) throws CoreException {
+ return parse(javaProject, manifestFile, errorListener, true, true);
+ }
+
+ /**
+ * Parses the manifest file, and collects data.
+ * @param manifestFile The manifest file to parse.
+ * @return an {@link AndroidManifestParser} or null if the parsing failed.
+ * @throws CoreException
+ */
+ public static AndroidManifestParser parseForData(IFile manifestFile) throws CoreException {
+ return parse(null /* javaProject */, manifestFile, null /* errorListener */,
+ true /* gatherData */, false /* markErrors */);
+ }
+
+ /**
+ * Returns the package defined in the manifest, if found.
+ * @return The package name or null if not found.
+ */
+ public String getPackage() {
+ return mJavaPackage;
+ }
+
+ /**
+ * Returns the list of activities found in the manifest.
+ * @return An array of fully qualified class names, or empty if no activity were found.
+ */
+ public String[] getActivities() {
+ return mActivities;
+ }
+
+ /**
+ * Returns the name of one activity found in the manifest, that is configured to show
+ * up in the HOME screen.
+ * @return the fully qualified name of a HOME activity or null if none were found.
+ */
+ public String getLauncherActivity() {
+ return mLauncherActivity;
+ }
+
+ /**
+ * Returns the list of process names declared by the manifest.
+ */
+ public String[] getProcesses() {
+ return mProcesses;
+ }
+
+ /**
+ * Returns the debuggable attribute value or <code>null</code> if it is not set.
+ */
+ public Boolean getDebuggable() {
+ return mDebuggable;
+ }
+
+ /**
+ * Returns the <code>minSdkVersion</code> attribute, or 0 if it's not set.
+ */
+ public int getApiLevelRequirement() {
+ return mApiLevelRequirement;
+ }
+
+
+ /**
+ * Private constructor to enforce using
+ * {@link #parse(IJavaProject, XmlErrorListener, boolean, boolean)},
+ * {@link #parse(IJavaProject, IFile, XmlErrorListener, boolean, boolean)},
+ * or {@link #parseForError(IJavaProject, IFile, XmlErrorListener)} to get an
+ * {@link AndroidManifestParser} object.
+ * @param javaPackage the package parsed from the manifest.
+ * @param activities the list of activities parsed from the manifest.
+ * @param launcherActivity the launcher activity parser from the manifest.
+ * @param processes the list of custom processes declared in the manifest.
+ * @param debuggable the debuggable attribute, or null if not set.
+ * @param apiLevelRequirement the minSdkVersion attribute value or 0 if not set.
+ */
+ private AndroidManifestParser(String javaPackage, String[] activities,
+ String launcherActivity, String[] processes, Boolean debuggable,
+ int apiLevelRequirement) {
+ mJavaPackage = javaPackage;
+ mActivities = activities;
+ mLauncherActivity = launcherActivity;
+ mProcesses = processes;
+ mDebuggable = debuggable;
+ mApiLevelRequirement = apiLevelRequirement;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidXPathFactory.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidXPathFactory.java
new file mode 100644
index 0000000..0f1e255
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/AndroidXPathFactory.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.common.project;
+
+import com.android.sdklib.SdkConstants;
+
+import java.util.Iterator;
+
+import javax.xml.XMLConstants;
+import javax.xml.namespace.NamespaceContext;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathFactory;
+
+/**
+ * XPath factory with automatic support for the android namespace.
+ */
+public class AndroidXPathFactory {
+ public final static String DEFAULT_NS_PREFIX = "android"; //$NON-NLS-1$
+
+ private final static XPathFactory sFactory = XPathFactory.newInstance();
+
+ /** Namespace context for Android resource XML files. */
+ private static class AndroidNamespaceContext implements NamespaceContext {
+ private String mAndroidPrefix;
+
+ /**
+ * Construct the context with the prefix associated with the android namespace.
+ * @param androidPrefix the Prefix
+ */
+ public AndroidNamespaceContext(String androidPrefix) {
+ mAndroidPrefix = androidPrefix;
+ }
+
+ public String getNamespaceURI(String prefix) {
+ if (prefix != null) {
+ if (prefix.equals(mAndroidPrefix)) {
+ return SdkConstants.NS_RESOURCES;
+ }
+ }
+
+ return XMLConstants.NULL_NS_URI;
+ }
+
+ public String getPrefix(String namespaceURI) {
+ // This isn't necessary for our use.
+ assert false;
+ return null;
+ }
+
+ public Iterator<?> getPrefixes(String namespaceURI) {
+ // This isn't necessary for our use.
+ assert false;
+ return null;
+ }
+ }
+
+ /**
+ * Creates a new XPath object, specifying which prefix in the query is used for the
+ * android namespace.
+ * @param androidPrefix The namespace prefix.
+ */
+ public static XPath newXPath(String androidPrefix) {
+ XPath xpath = sFactory.newXPath();
+ xpath.setNamespaceContext(new AndroidNamespaceContext(androidPrefix));
+ return xpath;
+ }
+
+ /**
+ * Creates a new XPath object using the default prefix for the android namespace.
+ * @see #DEFAULT_NS_PREFIX
+ */
+ public static XPath newXPath() {
+ return newXPath(DEFAULT_NS_PREFIX);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/BaseProjectHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/BaseProjectHelper.java
new file mode 100644
index 0000000..bd8b444
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/BaseProjectHelper.java
@@ -0,0 +1,452 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.common.project;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.common.project.XmlErrorHandler.XmlErrorListener;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.jdt.core.Flags;
+import org.eclipse.jdt.core.IClasspathEntry;
+import org.eclipse.jdt.core.IJavaModel;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IMethod;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.ITypeHierarchy;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.ui.JavaUI;
+import org.eclipse.jdt.ui.actions.OpenJavaPerspectiveAction;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IRegion;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+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.texteditor.IDocumentProvider;
+import org.eclipse.ui.texteditor.ITextEditor;
+
+import java.util.ArrayList;
+
+/**
+ * Utility methods to manipulate projects.
+ */
+public final class BaseProjectHelper {
+
+ public static final String TEST_CLASS_OK = null;
+
+ /**
+ * returns a list of source classpath for a specified project
+ * @param javaProject
+ * @return a list of path relative to the workspace root.
+ */
+ public static ArrayList<IPath> getSourceClasspaths(IJavaProject javaProject) {
+ ArrayList<IPath> sourceList = new ArrayList<IPath>();
+ IClasspathEntry[] classpaths = javaProject.readRawClasspath();
+ if (classpaths != null) {
+ for (IClasspathEntry e : classpaths) {
+ if (e.getEntryKind() == IClasspathEntry.CPE_SOURCE) {
+ sourceList.add(e.getPath());
+ }
+ }
+ }
+ return sourceList;
+ }
+
+ /**
+ * Adds a marker to a file on a specific line. This methods catches thrown
+ * {@link CoreException}, and returns null instead.
+ * @param file the file to be marked
+ * @param markerId The id of the marker to add.
+ * @param message the message associated with the mark
+ * @param lineNumber the line number where to put the mark. If line is < 1, it puts the marker
+ * on line 1.
+ * @param severity the severity of the marker.
+ * @return the IMarker that was added or null if it failed to add one.
+ */
+ public final static IMarker addMarker(IResource file, String markerId,
+ String message, int lineNumber, int severity) {
+ try {
+ IMarker marker = file.createMarker(markerId);
+ marker.setAttribute(IMarker.MESSAGE, message);
+ marker.setAttribute(IMarker.SEVERITY, severity);
+ if (lineNumber < 1) {
+ lineNumber = 1;
+ }
+ marker.setAttribute(IMarker.LINE_NUMBER, lineNumber);
+
+ // on Windows, when adding a marker to a project, it takes a refresh for the marker
+ // to show. In order to fix this we're forcing a refresh of elements receiving
+ // markers (and only the element, not its children), to force the marker display.
+ file.refreshLocal(IResource.DEPTH_ZERO, new NullProgressMonitor());
+
+ return marker;
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "Failed to add marker '%1$s' to '%2$s'", //$NON-NLS-1$
+ markerId, file.getFullPath());
+ }
+
+ return null;
+ }
+
+ /**
+ * Adds a marker to a resource. This methods catches thrown {@link CoreException},
+ * and returns null instead.
+ * @param resource the file to be marked
+ * @param markerId The id of the marker to add.
+ * @param message the message associated with the mark
+ * @param severity the severity of the marker.
+ * @return the IMarker that was added or null if it failed to add one.
+ */
+ public final static IMarker addMarker(IResource resource, String markerId,
+ String message, int severity) {
+ try {
+ IMarker marker = resource.createMarker(markerId);
+ marker.setAttribute(IMarker.MESSAGE, message);
+ marker.setAttribute(IMarker.SEVERITY, severity);
+
+ // on Windows, when adding a marker to a project, it takes a refresh for the marker
+ // to show. In order to fix this we're forcing a refresh of elements receiving
+ // markers (and only the element, not its children), to force the marker display.
+ resource.refreshLocal(IResource.DEPTH_ZERO, new NullProgressMonitor());
+
+ return marker;
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "Failed to add marker '%1$s' to '%2$s'", //$NON-NLS-1$
+ markerId, resource.getFullPath());
+ }
+
+ return null;
+ }
+
+ /**
+ * Adds a marker to a resource. This method does not catch {@link CoreException} and instead
+ * throw them.
+ * @param resource the file to be marked
+ * @param markerId The id of the marker to add.
+ * @param message the message associated with the mark
+ * @param lineNumber the line number where to put the mark if != -1.
+ * @param severity the severity of the marker.
+ * @param priority the priority of the marker
+ * @return the IMarker that was added.
+ * @throws CoreException
+ */
+ public final static IMarker addMarker(IResource resource, String markerId,
+ String message, int lineNumber, int severity, int priority) throws CoreException {
+ IMarker marker = resource.createMarker(markerId);
+ marker.setAttribute(IMarker.MESSAGE, message);
+ marker.setAttribute(IMarker.SEVERITY, severity);
+ if (lineNumber != -1) {
+ marker.setAttribute(IMarker.LINE_NUMBER, lineNumber);
+ }
+ marker.setAttribute(IMarker.PRIORITY, priority);
+
+ // on Windows, when adding a marker to a project, it takes a refresh for the marker
+ // to show. In order to fix this we're forcing a refresh of elements receiving
+ // markers (and only the element, not its children), to force the marker display.
+ resource.refreshLocal(IResource.DEPTH_ZERO, new NullProgressMonitor());
+
+ return marker;
+ }
+
+ /**
+ * Tests that a class name is valid for usage in the manifest.
+ * <p/>
+ * This tests the class existence, that it can be instantiated (ie it must not be abstract,
+ * nor non static if enclosed), and that it extends the proper super class (not necessarily
+ * directly)
+ * @param javaProject the {@link IJavaProject} containing the class.
+ * @param className the fully qualified name of the class to test.
+ * @param superClassName the fully qualified name of the expected super class.
+ * @param testVisibility if <code>true</code>, the method will check the visibility of the class
+ * or of its constructors.
+ * @return {@link #TEST_CLASS_OK} or an error message.
+ */
+ public final static String testClassForManifest(IJavaProject javaProject, String className,
+ String superClassName, boolean testVisibility) {
+ try {
+ // replace $ by .
+ String javaClassName = className.replaceAll("\\$", "\\."); //$NON-NLS-1$ //$NON-NLS-2$
+
+ // look for the IType object for this class
+ IType type = javaProject.findType(javaClassName);
+ if (type != null && type.exists()) {
+ // test that the class is not abstract
+ int flags = type.getFlags();
+ if (Flags.isAbstract(flags)) {
+ return String.format("%1$s is abstract", className);
+ }
+
+ // test whether the class is public or not.
+ if (testVisibility && Flags.isPublic(flags) == false) {
+ // if its not public, it may have a public default constructor,
+ // which would then be fine.
+ IMethod basicConstructor = type.getMethod(type.getElementName(), new String[0]);
+ if (basicConstructor != null && basicConstructor.exists()) {
+ int constructFlags = basicConstructor.getFlags();
+ if (Flags.isPublic(constructFlags) == false) {
+ return String.format(
+ "%1$s or its default constructor must be public for the system to be able to instantiate it",
+ className);
+ }
+ } else {
+ return String.format(
+ "%1$s must be public, or the system will not be able to instantiate it.",
+ className);
+ }
+ }
+
+ // If it's enclosed, test that it's static. If its declaring class is enclosed
+ // as well, test that it is also static, and public.
+ IType declaringType = type;
+ do {
+ IType tmpType = declaringType.getDeclaringType();
+ if (tmpType != null) {
+ if (tmpType.exists()) {
+ flags = declaringType.getFlags();
+ if (Flags.isStatic(flags) == false) {
+ return String.format("%1$s is enclosed, but not static",
+ declaringType.getFullyQualifiedName());
+ }
+
+ flags = tmpType.getFlags();
+ if (testVisibility && Flags.isPublic(flags) == false) {
+ return String.format("%1$s is not public",
+ tmpType.getFullyQualifiedName());
+ }
+ } else {
+ // if it doesn't exist, we need to exit so we may as well mark it null.
+ tmpType = null;
+ }
+ }
+ declaringType = tmpType;
+ } while (declaringType != null);
+
+ // test the class inherit from the specified super class.
+ // get the type hierarchy
+ ITypeHierarchy hierarchy = type.newSupertypeHierarchy(new NullProgressMonitor());
+
+ // if the super class is not the reference class, it may inherit from
+ // it so we get its supertype. At some point it will be null and we
+ // will stop
+ IType superType = type;
+ boolean foundProperSuperClass = false;
+ while ((superType = hierarchy.getSuperclass(superType)) != null &&
+ superType.exists()) {
+ if (superClassName.equals(superType.getFullyQualifiedName())) {
+ foundProperSuperClass = true;
+ }
+ }
+
+ // didn't find the proper superclass? return false.
+ if (foundProperSuperClass == false) {
+ return String.format("%1$s does not extend %2$s", className, superClassName);
+ }
+
+ return TEST_CLASS_OK;
+ } else {
+ return String.format("Class %1$s does not exist", className);
+ }
+ } catch (JavaModelException e) {
+ return String.format("%1$s: %2$s", className, e.getMessage());
+ }
+ }
+
+ /**
+ * Parses the manifest file for errors.
+ * <p/>
+ * This starts by removing the current XML marker, and then parses the xml for errors, both
+ * of XML type and of Android type (checking validity of class files).
+ * @param manifestFile
+ * @param errorListener
+ * @throws CoreException
+ */
+ public static AndroidManifestParser parseManifestForError(IFile manifestFile,
+ XmlErrorListener errorListener) throws CoreException {
+ // remove previous markers
+ if (manifestFile.exists()) {
+ manifestFile.deleteMarkers(AndroidConstants.MARKER_XML, true, IResource.DEPTH_ZERO);
+ manifestFile.deleteMarkers(AndroidConstants.MARKER_ANDROID, true, IResource.DEPTH_ZERO);
+ }
+
+ // and parse
+ return AndroidManifestParser.parseForError(
+ BaseProjectHelper.getJavaProject(manifestFile.getProject()),
+ manifestFile, errorListener);
+ }
+
+ /**
+ * Returns the {@link IJavaProject} for a {@link IProject} object.
+ * <p/>
+ * This checks if the project has the Java Nature first.
+ * @param project
+ * @return the IJavaProject or null if the project couldn't be created or if the project
+ * does not have the Java Nature.
+ * @throws CoreException
+ */
+ public static IJavaProject getJavaProject(IProject project) throws CoreException {
+ if (project != null && project.hasNature(JavaCore.NATURE_ID)) {
+ return JavaCore.create(project);
+ }
+ return null;
+ }
+
+ /**
+ * Reveals a specific line in the source file defining a specified class,
+ * for a specific project.
+ * @param project
+ * @param className
+ * @param line
+ */
+ public static void revealSource(IProject project, String className, int line) {
+ // in case the type is enclosed, we need to replace the $ with .
+ className = className.replaceAll("\\$", "\\."); //$NON-NLS-1$ //$NON-NLS2$
+
+ // get the java project
+ IJavaProject javaProject = JavaCore.create(project);
+
+ try {
+ // look for the IType matching the class name.
+ IType result = javaProject.findType(className);
+ if (result != null && result.exists()) {
+ // before we show the type 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.isEditorAreaVisible() == false) {
+ // no editor area? we open the java perspective.
+ new OpenJavaPerspectiveAction().run();
+ }
+
+ IEditorPart editor = JavaUI.openInEditor(result);
+ if (editor instanceof ITextEditor) {
+ // get the text editor that was just opened.
+ ITextEditor textEditor = (ITextEditor)editor;
+
+ IEditorInput input = textEditor.getEditorInput();
+
+ // get the location of the line to show.
+ IDocumentProvider documentProvider = textEditor.getDocumentProvider();
+ IDocument document = documentProvider.getDocument(input);
+ IRegion lineInfo = document.getLineInformation(line - 1);
+
+ // select and reveal the line.
+ textEditor.selectAndReveal(lineInfo.getOffset(), lineInfo.getLength());
+ }
+ }
+ } catch (JavaModelException e) {
+ } catch (PartInitException e) {
+ } catch (BadLocationException e) {
+ }
+ }
+
+ /**
+ * Returns the list of android-flagged projects. This list contains projects that are opened
+ * in the workspace and that are flagged as android project (through the android nature)
+ * @return an array of IJavaProject, which can be empty if no projects match.
+ */
+ public static IJavaProject[] getAndroidProjects() {
+ IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot();
+ IJavaModel javaModel = JavaCore.create(workspaceRoot);
+
+ return getAndroidProjects(javaModel);
+ }
+
+ /**
+ * Returns the list of android-flagged projects for the specified java Model.
+ * This list contains projects that are opened in the workspace and that are flagged as android
+ * project (through the android nature)
+ * @param javaModel the Java Model object corresponding for the current workspace root.
+ * @return an array of IJavaProject, which can be empty if no projects match.
+ */
+ public static IJavaProject[] getAndroidProjects(IJavaModel javaModel) {
+ // get the java projects
+ IJavaProject[] javaProjectList = null;
+ try {
+ javaProjectList = javaModel.getJavaProjects();
+ }
+ catch (JavaModelException jme) {
+ return new IJavaProject[0];
+ }
+
+ // temp list to build the android project array
+ ArrayList<IJavaProject> androidProjectList = new ArrayList<IJavaProject>();
+
+ // loop through the projects and add the android flagged projects to the temp list.
+ for (IJavaProject javaProject : javaProjectList) {
+ // get the workspace project object
+ IProject project = javaProject.getProject();
+
+ // check if it's an android project based on its nature
+ try {
+ if (project.hasNature(AndroidConstants.NATURE)) {
+ androidProjectList.add(javaProject);
+ }
+ } catch (CoreException e) {
+ // this exception, thrown by IProject.hasNature(), means the project either doesn't
+ // exist or isn't opened. So, in any case we just skip it (the exception will
+ // bypass the ArrayList.add()
+ }
+ }
+
+ // return the android projects list.
+ return androidProjectList.toArray(new IJavaProject[androidProjectList.size()]);
+ }
+
+ /**
+ * Returns the {@link IFolder} representing the output for the project.
+ * <p>
+ * The project must be a java project and be opened, or the method will return null.
+ * @param project the {@link IProject}
+ * @return an IFolder item or null.
+ */
+ public final static IFolder getOutputFolder(IProject project) {
+ try {
+ if (project.isOpen() && project.hasNature(JavaCore.NATURE_ID)) {
+ // get a java project from the normal project object
+ IJavaProject javaProject = JavaCore.create(project);
+
+ IPath path = javaProject.getOutputLocation();
+ IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot();
+ IResource outputResource = wsRoot.findMember(path);
+ if (outputResource != null && outputResource.getType() == IResource.FOLDER) {
+ return (IFolder)outputResource;
+ }
+ }
+ } catch (JavaModelException e) {
+ // Let's do nothing and return null
+ } catch (CoreException e) {
+ // Let's do nothing and return null
+ }
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/ExportHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/ExportHelper.java
new file mode 100644
index 0000000..4b169a1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/ExportHelper.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.common.project;
+
+import com.android.ide.eclipse.common.AndroidConstants;
+
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.IPath;
+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.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.jar.JarEntry;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * Export helper for project.
+ */
+public final class ExportHelper {
+
+ private static IExportCallback sCallback;
+
+ public interface IExportCallback {
+ void startExportWizard(IProject project);
+ }
+
+ public static void setCallback(IExportCallback callback) {
+ sCallback = callback;
+ }
+
+ public static void startExportWizard(IProject project) {
+ if (sCallback != null) {
+ sCallback.startExportWizard(project);
+ }
+ }
+
+ /**
+ * Exports an <b>unsigned</b> version of the application created by the given project.
+ * @param project the project to export
+ */
+ public static void exportProject(IProject project) {
+ Shell shell = Display.getCurrent().getActiveShell();
+
+ // get the java project to get the output directory
+ IFolder outputFolder = BaseProjectHelper.getOutputFolder(project);
+ if (outputFolder != null) {
+ IPath binLocation = outputFolder.getLocation();
+
+ // make the full path to the package
+ String fileName = project.getName() + AndroidConstants.DOT_ANDROID_PACKAGE;
+
+ File file = new File(binLocation.toOSString() + File.separator + fileName);
+
+ if (file.exists() == false || file.isFile() == false) {
+ MessageDialog.openInformation(Display.getCurrent().getActiveShell(),
+ "Android IDE Plug-in",
+ String.format("Failed to export %1$s: %2$s doesn't exist!",
+ project.getName(), file.getPath()));
+ return;
+ }
+
+ // ok now pop up the file save window
+ FileDialog fileDialog = new FileDialog(shell, SWT.SAVE);
+
+ fileDialog.setText("Export Project");
+ fileDialog.setFileName(fileName);
+
+ String saveLocation = fileDialog.open();
+ if (saveLocation != null) {
+ // get the stream from the original file
+
+ ZipInputStream zis = null;
+ ZipOutputStream zos = null;
+ FileInputStream input = null;
+ FileOutputStream output = null;
+
+ try {
+ input = new FileInputStream(file);
+ zis = new ZipInputStream(input);
+
+ // get an output stream into the new file
+ File saveFile = new File(saveLocation);
+ output = new FileOutputStream(saveFile);
+ zos = new ZipOutputStream(output);
+ } catch (FileNotFoundException e) {
+ // only the input/output stream are throwing this exception.
+ // so we only have to close zis if output is the one that threw.
+ if (zis != null) {
+ try {
+ zis.close();
+ } catch (IOException e1) {
+ // pass
+ }
+ }
+
+ MessageDialog.openInformation(shell, "Android IDE Plug-in",
+ String.format("Failed to export %1$s: %2$s doesn't exist!",
+ project.getName(), file.getPath()));
+ return;
+ }
+
+ try {
+ ZipEntry entry;
+
+ byte[] buffer = new byte[4096];
+
+ while ((entry = zis.getNextEntry()) != null) {
+ String name = entry.getName();
+
+ // do not take directories or anything inside the META-INF folder since
+ // we want to strip the signature.
+ if (entry.isDirectory() || name.startsWith("META-INF/")) { //$NON-NL1$
+ continue;
+ }
+
+ ZipEntry 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);
+ }
+
+ // add the entry to the jar archive
+ zos.putNextEntry(newEntry);
+
+ // read the content of the entry from the input stream, and write it into the archive.
+ int count;
+ while ((count = zis.read(buffer)) != -1) {
+ zos.write(buffer, 0, count);
+ }
+
+ // close the entry for this file
+ zos.closeEntry();
+ zis.closeEntry();
+
+ }
+
+ } catch (IOException e) {
+ MessageDialog.openInformation(shell, "Android IDE Plug-in",
+ String.format("Failed to export %1$s: %2$s",
+ project.getName(), e.getMessage()));
+ } finally {
+ try {
+ zos.close();
+ } catch (IOException e) {
+ // pass
+ }
+ try {
+ zis.close();
+ } catch (IOException e) {
+ // pass
+ }
+ }
+ }
+ } else {
+ MessageDialog.openInformation(shell, "Android IDE Plug-in",
+ String.format("Failed to export %1$s: Could not get project output location",
+ project.getName()));
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/ProjectChooserHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/ProjectChooserHelper.java
new file mode 100644
index 0000000..0c43499
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/ProjectChooserHelper.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.common.project;
+
+import com.android.ide.eclipse.common.project.BaseProjectHelper;
+
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.jdt.core.IJavaModel;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.ui.JavaElementLabelProvider;
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.dialogs.ElementListSelectionDialog;
+
+/**
+ * Helper class to deal with displaying a project choosing dialog that lists only the
+ * projects with the Android nature.
+ */
+public class ProjectChooserHelper {
+
+ private final Shell mParentShell;
+
+ /**
+ * List of current android projects. Since the dialog is modal, we'll just get
+ * the list once on-demand.
+ */
+ private IJavaProject[] mAndroidProjects;
+
+ public ProjectChooserHelper(Shell parentShell) {
+ mParentShell = parentShell;
+ }
+ /**
+ * Displays a project chooser dialog which lists all available projects with the Android nature.
+ * <p/>
+ * The list of project is built from Android flagged projects currently opened in the workspace.
+ *
+ * @param projectName If non null and not empty, represents the name of an Android project
+ * that will be selected by default.
+ * @return the project chosen by the user in the dialog, or null if the dialog was canceled.
+ */
+ public IJavaProject chooseJavaProject(String projectName) {
+ ILabelProvider labelProvider = new JavaElementLabelProvider(
+ JavaElementLabelProvider.SHOW_DEFAULT);
+ ElementListSelectionDialog dialog = new ElementListSelectionDialog(
+ mParentShell, labelProvider);
+ dialog.setTitle("Project Selection");
+ dialog.setMessage("Select a project to constrain your search.");
+
+ IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot();
+ IJavaModel javaModel = JavaCore.create(workspaceRoot);
+
+ // set the elements in the dialog. These are opened android projects.
+ dialog.setElements(getAndroidProjects(javaModel));
+
+ // look for the project matching the given project name
+ IJavaProject javaProject = null;
+ if (projectName != null && projectName.length() > 0) {
+ javaProject = javaModel.getJavaProject(projectName);
+ }
+
+ // if we found it, we set the initial selection in the dialog to this one.
+ if (javaProject != null) {
+ dialog.setInitialSelections(new Object[] { javaProject });
+ }
+
+ // open the dialog and return the object selected if OK was clicked, or null otherwise
+ if (dialog.open() == Window.OK) {
+ return (IJavaProject)dialog.getFirstResult();
+ }
+ return null;
+ }
+
+ /**
+ * Returns the list of Android projects.
+ * <p/>
+ * Because this list can be time consuming, this class caches the list of project.
+ * It is recommended to call this method instead of
+ * {@link BaseProjectHelper#getAndroidProjects()}.
+ *
+ * @param javaModel the java model. Can be null.
+ */
+ public IJavaProject[] getAndroidProjects(IJavaModel javaModel) {
+ if (mAndroidProjects == null) {
+ if (javaModel == null) {
+ mAndroidProjects = BaseProjectHelper.getAndroidProjects();
+ } else {
+ mAndroidProjects = BaseProjectHelper.getAndroidProjects(javaModel);
+ }
+ }
+
+ return mAndroidProjects;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/XmlErrorHandler.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/XmlErrorHandler.java
new file mode 100644
index 0000000..fda55c4
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/project/XmlErrorHandler.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.common.project;
+
+import com.android.ide.eclipse.common.AndroidConstants;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IMarker;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+import org.xml.sax.helpers.DefaultHandler;
+
+/**
+ * XML error handler used by the parser to report errors/warnings.
+ */
+public class XmlErrorHandler extends DefaultHandler {
+
+ /** file being parsed */
+ private IFile mFile;
+
+ /** link to the delta visitor, to set the xml error flag */
+ private XmlErrorListener mErrorListener;
+
+ /**
+ * Classes which implement this interface provide a method that deals
+ * with XML errors.
+ */
+ public interface XmlErrorListener {
+ /**
+ * Sent when an XML error is detected.
+ */
+ public void errorFound();
+ }
+
+ public static class BasicXmlErrorListener implements XmlErrorListener {
+ public boolean mHasXmlError = false;
+
+ public void errorFound() {
+ mHasXmlError = true;
+ }
+ }
+
+ public XmlErrorHandler(IFile file, XmlErrorListener errorListener) {
+ mFile = file;
+ mErrorListener = errorListener;
+ }
+
+ /**
+ * Xml Error call back
+ * @param exception the parsing exception
+ * @throws SAXException
+ */
+ @Override
+ public void error(SAXParseException exception) throws SAXException {
+ handleError(exception, exception.getLineNumber());
+ }
+
+ /**
+ * Xml Fatal Error call back
+ * @param exception the parsing exception
+ * @throws SAXException
+ */
+ @Override
+ public void fatalError(SAXParseException exception) throws SAXException {
+ handleError(exception, exception.getLineNumber());
+ }
+
+ /**
+ * Xml Warning call back
+ * @param exception the parsing exception
+ * @throws SAXException
+ */
+ @Override
+ public void warning(SAXParseException exception) throws SAXException {
+ BaseProjectHelper.addMarker(mFile, AndroidConstants.MARKER_XML, exception.getMessage(),
+ exception.getLineNumber(), IMarker.SEVERITY_WARNING);
+ }
+
+ protected final IFile getFile() {
+ return mFile;
+ }
+
+ /**
+ * Handles a parsing error and an optional line number.
+ * @param exception
+ * @param lineNumber
+ */
+ protected void handleError(Exception exception, int lineNumber) {
+ if (mErrorListener != null) {
+ mErrorListener.errorFound();
+ }
+
+ if (lineNumber != -1) {
+ BaseProjectHelper.addMarker(mFile, AndroidConstants.MARKER_XML, exception.getMessage(),
+ lineNumber, IMarker.SEVERITY_ERROR);
+ } else {
+ BaseProjectHelper.addMarker(mFile, AndroidConstants.MARKER_XML, exception.getMessage(),
+ IMarker.SEVERITY_ERROR);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/AttrsXmlParser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/AttrsXmlParser.java
new file mode 100644
index 0000000..3875e81
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/AttrsXmlParser.java
@@ -0,0 +1,505 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.common.resources;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.common.resources.DeclareStyleableInfo.AttributeInfo;
+import com.android.ide.eclipse.common.resources.DeclareStyleableInfo.AttributeInfo.Format;
+import com.android.ide.eclipse.common.resources.ViewClassInfo.LayoutParamsInfo;
+import com.android.ide.eclipse.editors.descriptors.DescriptorsUtils;
+
+import org.eclipse.core.runtime.IStatus;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.xml.sax.SAXException;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.TreeSet;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+/**
+ * Parser for attributes description files.
+ */
+public final class AttrsXmlParser {
+
+ private Document mDocument;
+ private String mOsAttrsXmlPath;
+ // all attributes that have the same name are supposed to have the same
+ // parameters so we'll keep a cache of them to avoid processing them twice.
+ private HashMap<String, AttributeInfo> mAttributeMap;
+
+ /** Map of all attribute names for a given element */
+ private HashMap<String, DeclareStyleableInfo> mStyleMap;
+
+ /** Map of all (constant, value) pairs for attributes of format enum or flag.
+ * E.g. for attribute name=gravity, this tells us there's an enum/flag called "center"
+ * with value 0x11.
+ */
+ private Map<String, Map<String, Integer>> mEnumFlagValues;
+
+
+ /**
+ * Creates a new {@link AttrsXmlParser}, set to load things from the given
+ * XML file. Nothing has been parsed yet. Callers should call {@link #preload()}
+ * next.
+ */
+ public AttrsXmlParser(String osAttrsXmlPath) {
+ this(osAttrsXmlPath, null /* inheritableAttributes */);
+ }
+
+ /**
+ * Creates a new {@link AttrsXmlParser} set to load things from the given
+ * XML file. If inheritableAttributes is non-null, it must point to a preloaded
+ * {@link AttrsXmlParser} which attributes will be used for this one. Since
+ * already defined attributes are not modifiable, they are thus "inherited".
+ */
+ public AttrsXmlParser(String osAttrsXmlPath, AttrsXmlParser inheritableAttributes) {
+ mOsAttrsXmlPath = osAttrsXmlPath;
+
+ // styles are not inheritable.
+ mStyleMap = new HashMap<String, DeclareStyleableInfo>();
+
+ if (inheritableAttributes == null) {
+ mAttributeMap = new HashMap<String, AttributeInfo>();
+ mEnumFlagValues = new HashMap<String, Map<String,Integer>>();
+ } else {
+ mAttributeMap = new HashMap<String, AttributeInfo>(inheritableAttributes.mAttributeMap);
+ mEnumFlagValues = new HashMap<String, Map<String,Integer>>(
+ inheritableAttributes.mEnumFlagValues);
+ }
+ }
+
+ /**
+ * @return The OS path of the attrs.xml file parsed
+ */
+ public String getOsAttrsXmlPath() {
+ return mOsAttrsXmlPath;
+ }
+
+ /**
+ * Preloads the document, parsing all attributes and declared styles.
+ *
+ * @return Self, for command chaining.
+ */
+ public AttrsXmlParser preload() {
+ Document doc = getDocument();
+
+ if (doc == null) {
+ AdtPlugin.log(IStatus.WARNING, "Failed to find %1$s", //$NON-NLS-1$
+ mOsAttrsXmlPath);
+ return this;
+ }
+
+ Node res = doc.getFirstChild();
+ while (res != null &&
+ res.getNodeType() != Node.ELEMENT_NODE &&
+ !res.getNodeName().equals("resources")) { //$NON-NLS-1$
+ res = res.getNextSibling();
+ }
+
+ if (res == null) {
+ AdtPlugin.log(IStatus.WARNING, "Failed to find a <resources> node in %1$s", //$NON-NLS-1$
+ mOsAttrsXmlPath);
+ return this;
+ }
+
+ parseResources(res);
+ return this;
+ }
+
+ /**
+ * Loads all attributes & javadoc for the view class info based on the class name.
+ */
+ public void loadViewAttributes(ViewClassInfo info) {
+ if (getDocument() != null) {
+ String xmlName = info.getShortClassName();
+ DeclareStyleableInfo style = mStyleMap.get(xmlName);
+ if (style != null) {
+ info.setAttributes(style.getAttributes());
+ info.setJavaDoc(style.getJavaDoc());
+ }
+ }
+ }
+
+ /**
+ * Loads all attributes for the layout data info based on the class name.
+ */
+ public void loadLayoutParamsAttributes(LayoutParamsInfo info) {
+ if (getDocument() != null) {
+ // Transforms "LinearLayout" and "LayoutParams" into "LinearLayout_Layout".
+ String xmlName = String.format("%1$s_%2$s", //$NON-NLS-1$
+ info.getViewLayoutClass().getShortClassName(),
+ info.getShortClassName());
+ xmlName = xmlName.replaceFirst("Params$", ""); //$NON-NLS-1$ //$NON-NLS-2$
+
+ DeclareStyleableInfo style = mStyleMap.get(xmlName);
+ if (style != null) {
+ info.setAttributes(style.getAttributes());
+ }
+ }
+ }
+
+ /**
+ * Returns a list of all decleare-styleable found in the xml file.
+ */
+ public Map<String, DeclareStyleableInfo> getDeclareStyleableList() {
+ return Collections.unmodifiableMap(mStyleMap);
+ }
+
+ /**
+ * Returns a map of all enum and flag constants sorted by parent attribute name.
+ * The map is attribute_name => (constant_name => integer_value).
+ */
+ public Map<String, Map<String, Integer>> getEnumFlagValues() {
+ return mEnumFlagValues;
+ }
+
+ //-------------------------
+
+ /**
+ * Creates an XML document from the attrs.xml OS path.
+ * May return null if the file doesn't exist or cannot be parsed.
+ */
+ private Document getDocument() {
+ if (mDocument == null) {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setIgnoringComments(false);
+ try {
+ DocumentBuilder builder = factory.newDocumentBuilder();
+ mDocument = builder.parse(new File(mOsAttrsXmlPath));
+ } catch (ParserConfigurationException e) {
+ AdtPlugin.log(e, "Failed to create XML document builder for %1$s", //$NON-NLS-1$
+ mOsAttrsXmlPath);
+ } catch (SAXException e) {
+ AdtPlugin.log(e, "Failed to parse XML document %1$s", //$NON-NLS-1$
+ mOsAttrsXmlPath);
+ } catch (IOException e) {
+ AdtPlugin.log(e, "Failed to read XML document %1$s", //$NON-NLS-1$
+ mOsAttrsXmlPath);
+ }
+ }
+ return mDocument;
+ }
+
+ /**
+ * Finds all the <declare-styleable> and <attr> nodes in the top <resources> node.
+ */
+ private void parseResources(Node res) {
+ Node lastComment = null;
+ for (Node node = res.getFirstChild(); node != null; node = node.getNextSibling()) {
+ switch (node.getNodeType()) {
+ case Node.COMMENT_NODE:
+ lastComment = node;
+ break;
+ case Node.ELEMENT_NODE:
+ if (node.getNodeName().equals("declare-styleable")) { //$NON-NLS-1$
+ Node nameNode = node.getAttributes().getNamedItem("name"); //$NON-NLS-1$
+ if (nameNode != null) {
+ String name = nameNode.getNodeValue();
+
+ Node parentNode = node.getAttributes().getNamedItem("parent"); //$NON-NLS-1$
+ String parents = parentNode == null ? null : parentNode.getNodeValue();
+
+ if (name != null && !mStyleMap.containsKey(name)) {
+ DeclareStyleableInfo style = parseDeclaredStyleable(name, node);
+ if (parents != null) {
+ style.setParents(parents.split("[ ,|]")); //$NON-NLS-1$
+ }
+ mStyleMap.put(name, style);
+ if (lastComment != null) {
+ style.setJavaDoc(parseJavadoc(lastComment.getNodeValue()));
+ }
+ }
+ }
+ } else if (node.getNodeName().equals("attr")) { //$NON-NLS-1$
+ parseAttr(node, lastComment);
+ }
+ lastComment = null;
+ break;
+ }
+ }
+ }
+
+ /**
+ * Parses an <attr> node and convert it into an {@link AttributeInfo} if it is valid.
+ */
+ private AttributeInfo parseAttr(Node attrNode, Node lastComment) {
+ AttributeInfo info = null;
+ Node nameNode = attrNode.getAttributes().getNamedItem("name"); //$NON-NLS-1$
+ if (nameNode != null) {
+ String name = nameNode.getNodeValue();
+ if (name != null) {
+ info = mAttributeMap.get(name);
+ // If the attribute is unknown yet, parse it.
+ // If the attribute is know but its format is unknown, parse it too.
+ if (info == null || info.getFormats().length == 0) {
+ info = parseAttributeTypes(attrNode, name);
+ if (info != null) {
+ mAttributeMap.put(name, info);
+ }
+ } else if (lastComment != null) {
+ info = new AttributeInfo(info);
+ }
+ if (info != null) {
+ if (lastComment != null) {
+ info.setJavaDoc(parseJavadoc(lastComment.getNodeValue()));
+ info.setDeprecatedDoc(parseDeprecatedDoc(lastComment.getNodeValue()));
+ }
+ }
+ }
+ }
+ return info;
+ }
+
+ /**
+ * Finds all the attributes for a particular style node,
+ * e.g. a declare-styleable of name "TextView" or "LinearLayout_Layout".
+ *
+ * @param styleName The name of the declare-styleable node
+ * @param declareStyleableNode The declare-styleable node itself
+ */
+ private DeclareStyleableInfo parseDeclaredStyleable(String styleName,
+ Node declareStyleableNode) {
+ ArrayList<AttributeInfo> attrs = new ArrayList<AttributeInfo>();
+ Node lastComment = null;
+ for (Node node = declareStyleableNode.getFirstChild();
+ node != null;
+ node = node.getNextSibling()) {
+
+ switch (node.getNodeType()) {
+ case Node.COMMENT_NODE:
+ lastComment = node;
+ break;
+ case Node.ELEMENT_NODE:
+ if (node.getNodeName().equals("attr")) { //$NON-NLS-1$
+ AttributeInfo info = parseAttr(node, lastComment);
+ if (info != null) {
+ attrs.add(info);
+ }
+ }
+ lastComment = null;
+ break;
+ }
+
+ }
+
+ return new DeclareStyleableInfo(styleName, attrs.toArray(new AttributeInfo[attrs.size()]));
+ }
+
+ /**
+ * Returns the {@link AttributeInfo} for a specific <attr> XML node.
+ * This gets the javadoc, the type, the name and the enum/flag values if any.
+ * <p/>
+ * The XML node is expected to have the following attributes:
+ * <ul>
+ * <li>"name", which is mandatory. The node is skipped if this is missing.</li>
+ * <li>"format".</li>
+ * </ul>
+ * The format may be one type or two types (e.g. "reference|color").
+ * An extra format can be implied: "enum" or "flag" are not specified in the "format" attribute,
+ * they are implicitely stated by the presence of sub-nodes <enum> or <flag>.
+ * <p/>
+ * By design, <attr> nodes of the same name MUST have the same type.
+ * Attribute nodes are thus cached by name and reused as much as possible.
+ * When reusing a node, it is duplicated and its javadoc reassigned.
+ */
+ private AttributeInfo parseAttributeTypes(Node attrNode, String name) {
+ TreeSet<AttributeInfo.Format> formats = new TreeSet<AttributeInfo.Format>();
+ String[] enumValues = null;
+ String[] flagValues = null;
+
+ Node attrFormat = attrNode.getAttributes().getNamedItem("format"); //$NON-NLS-1$
+ if (attrFormat != null) {
+ for (String f : attrFormat.getNodeValue().split("\\|")) { //$NON-NLS-1$
+ try {
+ Format format = AttributeInfo.Format.valueOf(f.toUpperCase());
+ // enum and flags are handled differently right below
+ if (format != null &&
+ format != AttributeInfo.Format.ENUM &&
+ format != AttributeInfo.Format.FLAG) {
+ formats.add(format);
+ }
+ } catch (IllegalArgumentException e) {
+ AdtPlugin.log(e, "Unknown format name '%s' in <attr name=\"%s\">, file '%s'.", //$NON-NLS-1$
+ f, name, getOsAttrsXmlPath());
+ }
+ }
+ }
+
+ // does this <attr> have <enum> children?
+ enumValues = parseEnumFlagValues(attrNode, "enum", name); //$NON-NLS-1$
+ if (enumValues != null) {
+ formats.add(AttributeInfo.Format.ENUM);
+ }
+
+ // does this <attr> have <flag> children?
+ flagValues = parseEnumFlagValues(attrNode, "flag", name); //$NON-NLS-1$
+ if (flagValues != null) {
+ formats.add(AttributeInfo.Format.FLAG);
+ }
+
+ AttributeInfo info = new AttributeInfo(name,
+ formats.toArray(new AttributeInfo.Format[formats.size()]));
+ info.setEnumValues(enumValues);
+ info.setFlagValues(flagValues);
+ return info;
+ }
+
+ /**
+ * Given an XML node that represents an <attr> node, this method searches
+ * if the node has any children nodes named "target" (e.g. "enum" or "flag").
+ * Such nodes must have a "name" attribute.
+ * <p/>
+ * If "attrNode" is null, look for any <attr> that has the given attrNode
+ * and the requested children nodes.
+ * <p/>
+ * This method collects all the possible names of these children nodes and
+ * return them.
+ *
+ * @param attrNode The <attr> XML node
+ * @param filter The child node to look for, either "enum" or "flag".
+ * @param attrName The value of the name attribute of <attr>
+ *
+ * @return Null if there are no such children nodes, otherwise an array of length >= 1
+ * of all the names of these children nodes.
+ */
+ private String[] parseEnumFlagValues(Node attrNode, String filter, String attrName) {
+ ArrayList<String> names = null;
+ for (Node child = attrNode.getFirstChild(); child != null; child = child.getNextSibling()) {
+ if (child.getNodeType() == Node.ELEMENT_NODE && child.getNodeName().equals(filter)) {
+ Node nameNode = child.getAttributes().getNamedItem("name"); //$NON-NLS-1$
+ if (nameNode == null) {
+ AdtPlugin.log(IStatus.WARNING,
+ "Missing name attribute in <attr name=\"%s\"><%s></attr>", //$NON-NLS-1$
+ attrName, filter);
+ } else {
+ if (names == null) {
+ names = new ArrayList<String>();
+ }
+ String name = nameNode.getNodeValue();
+ names.add(name);
+
+ Node valueNode = child.getAttributes().getNamedItem("value"); //$NON-NLS-1$
+ if (valueNode == null) {
+ AdtPlugin.log(IStatus.WARNING,
+ "Missing value attribute in <attr name=\"%s\"><%s name=\"%s\"></attr>", //$NON-NLS-1$
+ attrName, filter, name);
+ } else {
+ String value = valueNode.getNodeValue();
+ try {
+ int i = value.startsWith("0x") ?
+ Integer.parseInt(value.substring(2), 16 /* radix */) :
+ Integer.parseInt(value);
+
+ Map<String, Integer> map = mEnumFlagValues.get(attrName);
+ if (map == null) {
+ map = new HashMap<String, Integer>();
+ mEnumFlagValues.put(attrName, map);
+ }
+ map.put(name, Integer.valueOf(i));
+
+ } catch(NumberFormatException e) {
+ AdtPlugin.log(e,
+ "Value in <attr name=\"%s\"><%s name=\"%s\" value=\"%s\"></attr> is not a valid decimal or hexadecimal", //$NON-NLS-1$
+ attrName, filter, name, value);
+ }
+ }
+ }
+ }
+ }
+ return names == null ? null : names.toArray(new String[names.size()]);
+ }
+
+ /**
+ * Parses the javadoc comment.
+ * Only keeps the first sentence.
+ * <p/>
+ * This does not remove nor simplify links and references. Such a transformation
+ * is done later at "display" time in {@link DescriptorsUtils#formatTooltip(String)} and co.
+ */
+ private String parseJavadoc(String comment) {
+ if (comment == null) {
+ return null;
+ }
+
+ // sanitize & collapse whitespace
+ comment = comment.replaceAll("\\s+", " "); //$NON-NLS-1$ //$NON-NLS-2$
+
+ // Explicitly remove any @deprecated tags since they are handled separately.
+ comment = comment.replaceAll("(?:\\{@deprecated[^}]*\\}|@deprecated[^@}]*)", "");
+
+ // take everything up to the first dot that is followed by a space or the end of the line.
+ // I love regexps :-). For the curious, the regexp is:
+ // - start of line
+ // - ignore whitespace
+ // - group:
+ // - everything, not greedy
+ // - non-capturing group (?: )
+ // - end of string
+ // or
+ // - not preceded by a letter, a dot and another letter (for "i.e" and "e.g" )
+ // (<! non-capturing zero-width negative look-behind)
+ // - a dot
+ // - followed by a space (?= non-capturing zero-width positive look-ahead)
+ // - anything else is ignored
+ comment = comment.replaceFirst("^\\s*(.*?(?:$|(?<![a-zA-Z]\\.[a-zA-Z])\\.(?=\\s))).*", "$1"); //$NON-NLS-1$ //$NON-NLS-2$
+
+ return comment;
+ }
+
+
+ /**
+ * Parses the javadoc and extract the first @deprecated tag, if any.
+ * Returns null if there's no @deprecated tag.
+ * The deprecated tag can be of two forms:
+ * - {+@deprecated ...text till the next bracket }
+ * Note: there should be no space or + between { and @. I need one in this comment otherwise
+ * this method will be tagged as deprecated ;-)
+ * - @deprecated ...text till the next @tag or end of the comment.
+ * In both cases the comment can be multi-line.
+ */
+ private String parseDeprecatedDoc(String comment) {
+ // Skip if we can't even find the tag in the comment.
+ if (comment == null) {
+ return null;
+ }
+
+ // sanitize & collapse whitespace
+ comment = comment.replaceAll("\\s+", " "); //$NON-NLS-1$ //$NON-NLS-2$
+
+ int pos = comment.indexOf("{@deprecated");
+ if (pos >= 0) {
+ comment = comment.substring(pos + 12 /* len of {@deprecated */);
+ comment = comment.replaceFirst("^([^}]*).*", "$1");
+ } else if ((pos = comment.indexOf("@deprecated")) >= 0) {
+ comment = comment.substring(pos + 11 /* len of @deprecated */);
+ comment = comment.replaceFirst("^(.*?)(?:@.*|$)", "$1");
+ } else {
+ return null;
+ }
+
+ return comment.trim();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/DeclareStyleableInfo.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/DeclareStyleableInfo.java
new file mode 100644
index 0000000..efa5981
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/DeclareStyleableInfo.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.common.resources;
+
+
+/**
+ * Information needed to represent a View or ViewGroup (aka Layout) item
+ * in the layout hierarchy, as extracted from the main android.jar and the
+ * associated attrs.xml.
+ */
+public class DeclareStyleableInfo {
+ /** The style name, never null. */
+ private String mStyleName;
+ /** Attributes for this view or view group. Can be empty but never null. */
+ private AttributeInfo[] mAttributes;
+ /** Short javadoc. Can be null. */
+ private String mJavaDoc;
+ /** Optional name of the parents stylable. Can be null. */
+ private String[] mParents;
+
+ public static class AttributeInfo {
+ /** XML Name of the attribute */
+ private String mName;
+
+ public enum Format {
+ STRING,
+ BOOLEAN,
+ INTEGER,
+ FLOAT,
+ REFERENCE,
+ COLOR,
+ DIMENSION,
+ FRACTION,
+ ENUM,
+ FLAG,
+ }
+
+ /** Formats of the attribute. Cannot be null. Should have at least one format. */
+ private Format[] mFormats;
+ /** Values for enum. null for other types. */
+ private String[] mEnumValues;
+ /** Values for flag. null for other types. */
+ private String[] mFlagValues;
+ /** Short javadoc (i.e. the first sentence). */
+ private String mJavaDoc;
+ /** Documentation for deprecated attributes. Null if not deprecated. */
+ private String mDeprecatedDoc;
+
+ /**
+ * @param name The XML Name of the attribute
+ * @param formats The formats of the attribute. Cannot be null.
+ * Should have at least one format.
+ */
+ public AttributeInfo(String name, Format[] formats) {
+ mName = name;
+ mFormats = formats;
+ }
+
+ public AttributeInfo(AttributeInfo info) {
+ mName = info.mName;
+ mFormats = info.mFormats;
+ mEnumValues = info.mEnumValues;
+ mFlagValues = info.mFlagValues;
+ mJavaDoc = info.mJavaDoc;
+ mDeprecatedDoc = info.mDeprecatedDoc;
+ }
+
+ /** Returns the XML Name of the attribute */
+ public String getName() {
+ return mName;
+ }
+ /** Returns the formats of the attribute. Cannot be null.
+ * Should have at least one format. */
+ public Format[] getFormats() {
+ return mFormats;
+ }
+ /** Returns the values for enums. null for other types. */
+ public String[] getEnumValues() {
+ return mEnumValues;
+ }
+ /** Returns the values for flags. null for other types. */
+ public String[] getFlagValues() {
+ return mFlagValues;
+ }
+ /** Returns a short javadoc, .i.e. the first sentence. */
+ public String getJavaDoc() {
+ return mJavaDoc;
+ }
+ /** Returns the documentation for deprecated attributes. Null if not deprecated. */
+ public String getDeprecatedDoc() {
+ return mDeprecatedDoc;
+ }
+
+ /** Sets the values for enums. null for other types. */
+ public void setEnumValues(String[] values) {
+ mEnumValues = values;
+ }
+ /** Sets the values for flags. null for other types. */
+ public void setFlagValues(String[] values) {
+ mFlagValues = values;
+ }
+ /** Sets a short javadoc, .i.e. the first sentence. */
+ public void setJavaDoc(String javaDoc) {
+ mJavaDoc = javaDoc;
+ }
+ /** Sets the documentation for deprecated attributes. Null if not deprecated. */
+ public void setDeprecatedDoc(String deprecatedDoc) {
+ mDeprecatedDoc = deprecatedDoc;
+ }
+
+ }
+
+ // --------
+
+ /**
+ * Creates a new {@link DeclareStyleableInfo}.
+ *
+ * @param styleName The name of the style. Should not be empty nor null.
+ * @param attributes The initial list of attributes. Can be null.
+ */
+ public DeclareStyleableInfo(String styleName, AttributeInfo[] attributes) {
+ mStyleName = styleName;
+ mAttributes = attributes == null ? new AttributeInfo[0] : attributes;
+ }
+
+ /** Returns style name */
+ public String getStyleName() {
+ return mStyleName;
+ }
+
+ /** Returns the attributes for this view or view group. Maybe empty but not null. */
+ public AttributeInfo[] getAttributes() {
+ return mAttributes;
+ }
+
+ /** Sets the list of attributes for this View or ViewGroup. */
+ public void setAttributes(AttributeInfo[] attributes) {
+ mAttributes = attributes;
+ }
+
+ /** Returns a short javadoc */
+ public String getJavaDoc() {
+ return mJavaDoc;
+ }
+
+ /** Sets the javadoc. */
+ public void setJavaDoc(String javaDoc) {
+ mJavaDoc = javaDoc;
+ }
+
+ /** Sets the name of the parents styleable. Can be null. */
+ public void setParents(String[] parents) {
+ mParents = parents;
+ }
+
+ /** Returns the name of the parents styleable. Can be null. */
+ public String[] getParents() {
+ return mParents;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/IIdResourceItem.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/IIdResourceItem.java
new file mode 100644
index 0000000..38b7e03
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/IIdResourceItem.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.common.resources;
+
+/**
+ * Classes which implements this interface provides a method indicating the state of a resource of
+ * type {@link ResourceType#ID}.
+ */
+public interface IIdResourceItem {
+ /**
+ * Returns whether the ID resource has been declared inline inside another resource XML file.
+ */
+ public boolean isDeclaredInline();
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/IPathChangedListener.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/IPathChangedListener.java
new file mode 100644
index 0000000..53d9077
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/IPathChangedListener.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.common.resources;
+
+/**
+ * Classes which implement this interface provide a method that deals with
+ * a path change.
+ */
+public interface IPathChangedListener {
+ /**
+ * Sent when the location of the android sdk directory changed.
+ * @param osPath The new android sdk directory location.
+ */
+ public void pathChanged(String osPath);
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/IResourceRepository.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/IResourceRepository.java
new file mode 100644
index 0000000..3819997
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/IResourceRepository.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.common.resources;
+
+/**
+ * A repository of resources. This allows access to the resource by {@link ResourceType}.
+ */
+public interface IResourceRepository {
+
+ /**
+ * Returns the present {@link ResourceType}s in the project.
+ * @return an array containing all the type of resources existing in the project.
+ */
+ public abstract ResourceType[] getAvailableResourceTypes();
+
+ /**
+ * Returns an array of the existing resource for the specified type.
+ * @param type the type of the resources to return
+ */
+ public abstract ResourceItem[] getResources(ResourceType type);
+
+ /**
+ * Returns whether resources of the specified type are present.
+ * @param type the type of the resources to check.
+ */
+ public abstract boolean hasResources(ResourceType type);
+
+ /**
+ * Returns whether the repository is a system repository.
+ */
+ public abstract boolean isSystemRepository();
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/ResourceItem.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/ResourceItem.java
new file mode 100644
index 0000000..83527f3
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/ResourceItem.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.common.resources;
+
+/**
+ * Base class representing a Resource Item, as returned by a {@link IResourceRepository}.
+ */
+public class ResourceItem implements Comparable<ResourceItem> {
+
+ private final String mName;
+
+ /**
+ * Constructs a new ResourceItem
+ * @param name the name of the resource as it appears in the XML and R.java files.
+ */
+ public ResourceItem(String name) {
+ mName = name;
+ }
+
+ /**
+ * Returns the name of the resource item.
+ */
+ public final String getName() {
+ return mName;
+ }
+
+ /**
+ * Compares the {@link ResourceItem} to another.
+ * @param other the ResourceItem to be compared to.
+ */
+ public int compareTo(ResourceItem other) {
+ return mName.compareTo(other.mName);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/ResourceType.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/ResourceType.java
new file mode 100644
index 0000000..60c471e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/ResourceType.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.common.resources;
+
+/**
+ * Enum representing a type of compiled resource.
+ */
+public enum ResourceType {
+ ANIM("anim", "Animation"), //$NON-NLS-1$
+ ARRAY("array", "Array", "string-array", "integer-array"), //$NON-NLS-1$ //$NON-NLS-3$ //$NON-NLS-4$
+ ATTR("attr", "Attr"), //$NON-NLS-1$
+ COLOR("color", "Color"), //$NON-NLS-1$
+ DIMEN("dimen", "Dimension"), //$NON-NLS-1$
+ DRAWABLE("drawable", "Drawable"), //$NON-NLS-1$
+ ID("id", "ID"), //$NON-NLS-1$
+ LAYOUT("layout", "Layout"), //$NON-NLS-1$
+ MENU("menu", "Menu"), //$NON-NLS-1$
+ RAW("raw", "Raw"), //$NON-NLS-1$
+ STRING("string", "String"), //$NON-NLS-1$
+ STYLE("style", "Style"), //$NON-NLS-1$
+ STYLEABLE("styleable", "Styleable"), //$NON-NLS-1$
+ XML("xml", "XML"); //$NON-NLS-1$
+
+ private final String mName;
+ private final String mDisplayName;
+ private final String[] mAlternateXmlNames;
+
+ ResourceType(String name, String displayName, String... alternateXmlNames) {
+ mName = name;
+ mDisplayName = displayName;
+ mAlternateXmlNames = alternateXmlNames;
+ }
+
+ /**
+ * Returns the resource type name, as used by XML files.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Returns a translated display name for the resource type.
+ */
+ public String getDisplayName() {
+ return mDisplayName;
+ }
+
+ /**
+ * Returns the enum by its name as it appears in the XML or the R class.
+ * @param name name of the resource
+ * @return the matching {@link ResourceType} or <code>null</code> if no match was found.
+ */
+ public static ResourceType getEnum(String name) {
+ for (ResourceType rType : values()) {
+ if (rType.mName.equals(name)) {
+ return rType;
+ } else if (rType.mAlternateXmlNames != null) {
+ // if there are alternate Xml Names, we test those too
+ for (String alternate : rType.mAlternateXmlNames) {
+ if (alternate.equals(name)) {
+ return rType;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns a formatted string usable in an XML to use the specified {@link ResourceItem}.
+ * @param resourceItem The resource item.
+ * @param system Whether this is a system resource or a project resource.
+ * @return a string in the format @[type]/[name]
+ */
+ public String getXmlString(ResourceItem resourceItem, boolean system) {
+ if (this == ID && resourceItem instanceof IIdResourceItem) {
+ IIdResourceItem idResource = (IIdResourceItem)resourceItem;
+ if (idResource.isDeclaredInline()) {
+ return (system?"@android:":"@+") + mName + "/" + resourceItem.getName(); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+ }
+ }
+
+ return (system?"@android:":"@") + mName + "/" + resourceItem.getName(); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+ }
+
+ /**
+ * Returns an array with all the names defined by this enum.
+ */
+ public static String[] getNames() {
+ ResourceType[] values = values();
+ String[] names = new String[values.length];
+ for (int i = values.length - 1; i >= 0; --i) {
+ names[i] = values[i].getName();
+ }
+ return names;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/ViewClassInfo.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/ViewClassInfo.java
new file mode 100644
index 0000000..619e3cc
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/common/resources/ViewClassInfo.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.common.resources;
+
+import com.android.ide.eclipse.common.resources.DeclareStyleableInfo.AttributeInfo;
+
+/**
+ * Information needed to represent a View or ViewGroup (aka Layout) item
+ * in the layout hierarchy, as extracted from the main android.jar and the
+ * associated attrs.xml.
+ */
+public class ViewClassInfo {
+ /** Is this a layout class (i.e. ViewGroup) or just a view? */
+ private boolean mIsLayout;
+ /** FQCN e.g. android.view.View, never null. */
+ private String mCanonicalClassName;
+ /** Short class name, e.g. View, never null. */
+ private String mShortClassName;
+ /** Super class. Can be null. */
+ private ViewClassInfo mSuperClass;
+ /** Short javadoc. Can be null. */
+ private String mJavaDoc;
+ /** Attributes for this view or view group. Can be empty but never null. */
+ private AttributeInfo[] mAttributes;
+
+ public static class LayoutParamsInfo {
+ /** Short class name, e.g. LayoutData, never null. */
+ private String mShortClassName;
+ /** ViewLayout class info owning this layout data */
+ private ViewClassInfo mViewLayoutClass;
+ /** Super class. Can be null. */
+ private LayoutParamsInfo mSuperClass;
+ /** Layout Data Attributes for layout classes. Can be empty but not null. */
+ private AttributeInfo[] mAttributes;
+
+ public LayoutParamsInfo(ViewClassInfo enclosingViewClassInfo,
+ String shortClassName, LayoutParamsInfo superClassInfo) {
+ mShortClassName = shortClassName;
+ mViewLayoutClass = enclosingViewClassInfo;
+ mSuperClass = superClassInfo;
+ mAttributes = new AttributeInfo[0];
+ }
+
+ /** Returns short class name, e.g. "LayoutData" */
+ public String getShortClassName() {
+ return mShortClassName;
+ }
+ /** Returns the ViewLayout class info enclosing this layout data. Cannot null. */
+ public ViewClassInfo getViewLayoutClass() {
+ return mViewLayoutClass;
+ }
+ /** Returns the super class info. Can be null. */
+ public LayoutParamsInfo getSuperClass() {
+ return mSuperClass;
+ }
+ /** Returns the LayoutData attributes. Can be empty but not null. */
+ public AttributeInfo[] getAttributes() {
+ return mAttributes;
+ }
+ /** Sets the LayoutData attributes. Can be empty but not null. */
+ public void setAttributes(AttributeInfo[] attributes) {
+ mAttributes = attributes;
+ }
+ }
+
+ /** Layout data info for a layout class. Null for all non-layout classes and always
+ * non-null for a layout class. */
+ public LayoutParamsInfo mLayoutData;
+
+ // --------
+
+ public ViewClassInfo(boolean isLayout, String canonicalClassName, String shortClassName) {
+ mIsLayout = isLayout;
+ mCanonicalClassName = canonicalClassName;
+ mShortClassName = shortClassName;
+ mAttributes = new AttributeInfo[0];
+ }
+
+ /** Returns whether this is a layout class (i.e. ViewGroup) or just a View */
+ public boolean isLayout() {
+ return mIsLayout;
+ }
+
+ /** Returns FQCN e.g. "android.view.View" */
+ public String getCanonicalClassName() {
+ return mCanonicalClassName;
+ }
+
+ /** Returns short class name, e.g. "View" */
+ public String getShortClassName() {
+ return mShortClassName;
+ }
+
+ /** Returns the super class. Can be null. */
+ public ViewClassInfo getSuperClass() {
+ return mSuperClass;
+ }
+
+ /** Returns a short javadoc */
+ public String getJavaDoc() {
+ return mJavaDoc;
+ }
+
+ /** Returns the attributes for this view or view group. Maybe empty but not null. */
+ public AttributeInfo[] getAttributes() {
+ return mAttributes;
+ }
+
+ /** Returns the LayoutData info for layout classes. Null for non-layout view classes. */
+ public LayoutParamsInfo getLayoutData() {
+ return mLayoutData;
+ }
+
+ /**
+ * Sets a link on the info of the super class of this View or ViewGroup.
+ * <p/>
+ * The super class info must be of the same kind (i.e. group to group or view to view)
+ * except for the top ViewGroup which links to the View info.
+ * <p/>
+ * The super class cannot be null except for the top View info.
+ */
+ public void setSuperClass(ViewClassInfo superClass) {
+ mSuperClass = superClass;
+ }
+
+ /** Sets the javadoc for this View or ViewGroup. */
+ public void setJavaDoc(String javaDoc) {
+ mJavaDoc = javaDoc;
+ }
+
+ /** Sets the list of attributes for this View or ViewGroup. */
+ public void setAttributes(AttributeInfo[] attributes) {
+ mAttributes = attributes;
+ }
+
+ /**
+ * Sets the {@link LayoutParamsInfo} for layout classes.
+ * Does nothing for non-layout view classes.
+ */
+ public void setLayoutParams(LayoutParamsInfo layoutData) {
+ if (mIsLayout) {
+ mLayoutData = layoutData;
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/AndroidContentAssist.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/AndroidContentAssist.java
new file mode 100644
index 0000000..a6db786
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/AndroidContentAssist.java
@@ -0,0 +1,791 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors;
+
+import com.android.ide.eclipse.adt.sdk.AndroidTargetData;
+import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.editors.descriptors.IDescriptorProvider;
+import com.android.ide.eclipse.editors.descriptors.SeparatorAttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.TextValueDescriptor;
+import com.android.ide.eclipse.editors.descriptors.XmlnsAttributeDescriptor;
+import com.android.ide.eclipse.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.editors.uimodel.UiFlagAttributeNode;
+import com.android.sdklib.SdkConstants;
+
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.ITextViewer;
+import org.eclipse.jface.text.TextSelection;
+import org.eclipse.jface.text.contentassist.CompletionProposal;
+import org.eclipse.jface.text.contentassist.ICompletionProposal;
+import org.eclipse.jface.text.contentassist.IContentAssistProcessor;
+import org.eclipse.jface.text.contentassist.IContextInformation;
+import org.eclipse.jface.text.contentassist.IContextInformationValidator;
+import org.eclipse.jface.text.source.ISourceViewer;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.wst.sse.core.StructuredModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.regex.Pattern;
+
+/**
+ * Content Assist Processor for Android XML files
+ */
+public abstract class AndroidContentAssist implements IContentAssistProcessor {
+
+ /** Regexp to detect a full attribute after an element tag.
+ * <pre>Syntax:
+ * name = "..." quoted string with all but < and "
+ * or:
+ * name = '...' quoted string with all but < and '
+ * </pre>
+ */
+ private static Pattern sFirstAttribute = Pattern.compile(
+ "^ *[a-zA-Z_:]+ *= *(?:\"[^<\"]*\"|'[^<']*')"); //$NON-NLS-1$
+
+ /** Regexp to detect an element tag name */
+ private static Pattern sFirstElementWord = Pattern.compile("^[a-zA-Z0-9_:]+"); //$NON-NLS-1$
+
+ /** Regexp to detect whitespace */
+ private static Pattern sWhitespace = Pattern.compile("\\s+"); //$NON-NLS-1$
+
+ protected final static String ROOT_ELEMENT = "";
+
+ /** Descriptor of the root of the XML hierarchy. This a "fake" ElementDescriptor which
+ * is used to list all the possible roots given by actual implementations.
+ * DO NOT USE DIRECTLY. Call {@link #getRootDescriptor()} instead. */
+ private ElementDescriptor mRootDescriptor;
+
+ private final int mDescriptorId;
+
+ private AndroidEditor mEditor;
+
+ /**
+ * Constructor for AndroidContentAssist
+ * @param descriptorId An id for {@link AndroidTargetData#getDescriptorProvider(int)}.
+ * The Id can be one of {@link AndroidTargetData#DESCRIPTOR_MANIFEST},
+ * {@link AndroidTargetData#DESCRIPTOR_LAYOUT},
+ * {@link AndroidTargetData#DESCRIPTOR_MENU},
+ * or {@link AndroidTargetData#DESCRIPTOR_XML}.
+ * All other values will throw an {@link IllegalArgumentException} later at runtime.
+ */
+ public AndroidContentAssist(int descriptorId) {
+ mDescriptorId = descriptorId;
+ }
+
+ /**
+ * Returns a list of completion proposals based on the
+ * specified location within the document that corresponds
+ * to the current cursor position within the text viewer.
+ *
+ * @param viewer the viewer whose document is used to compute the proposals
+ * @param offset an offset within the document for which completions should be computed
+ * @return an array of completion proposals or <code>null</code> if no proposals are possible
+ *
+ * @see org.eclipse.jface.text.contentassist.IContentAssistProcessor#computeCompletionProposals(org.eclipse.jface.text.ITextViewer, int)
+ */
+ public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset) {
+
+ if (mEditor == null) {
+ mEditor = getAndroidEditor(viewer);
+ }
+
+ UiElementNode rootUiNode = mEditor.getUiRootNode();
+
+ Object[] choices = null; /* An array of ElementDescriptor, or AttributeDescriptor
+ or String or null */
+ String parent = ""; //$NON-NLS-1$
+ String wordPrefix = extractElementPrefix(viewer, offset);
+ char needTag = 0;
+ boolean isElement = false;
+ boolean isAttribute = false;
+
+ Node currentNode = getNode(viewer, offset);
+ if (currentNode == null)
+ return null;
+
+ // check to see if we can find a UiElementNode matching this XML node
+ UiElementNode currentUiNode =
+ rootUiNode == null ? null : rootUiNode.findXmlNode(currentNode);
+
+ if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
+ parent = currentNode.getNodeName();
+
+ if (wordPrefix.equals(parent)) {
+ // We are still editing the element's tag name, not the attributes
+ // (the element's tag name may not even be complete)
+ isElement = true;
+ choices = getChoicesForElement(parent, currentNode);
+ } else {
+ // We're not editing the current node name, so we might be editing its
+ // attributes instead...
+ isAttribute = true;
+ AttribInfo info = parseAttributeInfo(viewer, offset);
+ if (info != null) {
+ // We're editing attributes in an element node (either the attributes' names
+ // or their values).
+ choices = getChoicesForAttribute(parent, currentNode, currentUiNode, info);
+
+ if (info.correctedPrefix != null) {
+ wordPrefix = info.correctedPrefix;
+ }
+ needTag = info.needTag;
+ }
+ }
+ } else if (currentNode.getNodeType() == Node.TEXT_NODE) {
+ isElement = true;
+ // Examine the parent of the text node.
+ choices = getChoicesForTextNode(currentNode);
+ }
+
+ // Abort if we can't recognize the context or there are no completion choices
+ if (choices == null || choices.length == 0) return null;
+
+ if (isElement) {
+ // If we found some suggestions, do we need to add an opening "<" bracket
+ // for the element? We don't if the cursor is right after "<" or "</".
+ // Per XML Spec, there's no whitespace between "<" or "</" and the tag name.
+ int offset2 = offset - wordPrefix.length() - 1;
+ int c1 = extractChar(viewer, offset2);
+ if (!((c1 == '<') || (c1 == '/' && extractChar(viewer, offset2 - 1) == '<'))) {
+ needTag = '<';
+ }
+ }
+
+ // get the selection length
+ int selectionLength = 0;
+ ISelection selection = viewer.getSelectionProvider().getSelection();
+ if (selection instanceof TextSelection) {
+ TextSelection textSelection = (TextSelection)selection;
+ selectionLength = textSelection.getLength();
+ }
+
+ return computeProposals(offset, currentNode, choices, wordPrefix, needTag,
+ isAttribute, selectionLength);
+ }
+
+ /**
+ * Returns the namespace prefix matching the Android Resource URI.
+ * If no such declaration is found, returns the default "android" prefix.
+ *
+ * @param node The current node. Must not be null.
+ * @param nsUri The namespace URI of which the prefix is to be found,
+ * e.g. {@link SdkConstants#NS_RESOURCES}
+ * @return The first prefix declared or the default "android" prefix.
+ */
+ private String lookupNamespacePrefix(Node node, String nsUri) {
+ // Note: Node.lookupPrefix is not implemented in wst/xml/core NodeImpl.java
+ // The following emulates this:
+ // String prefix = node.lookupPrefix(SdkConstants.NS_RESOURCES);
+
+ if (XmlnsAttributeDescriptor.XMLNS_URI.equals(nsUri)) {
+ return "xmlns"; //$NON-NLS-1$
+ }
+
+ HashSet<String> visited = new HashSet<String>();
+
+ String prefix = null;
+ for (; prefix == null &&
+ node != null &&
+ node.getNodeType() == Node.ELEMENT_NODE;
+ node = node.getParentNode()) {
+ NamedNodeMap attrs = node.getAttributes();
+ for (int n = attrs.getLength() - 1; n >= 0; --n) {
+ Node attr = attrs.item(n);
+ if ("xmlns".equals(attr.getPrefix())) { //$NON-NLS-1$
+ String uri = attr.getNodeValue();
+ if (SdkConstants.NS_RESOURCES.equals(uri)) {
+ return attr.getLocalName();
+ }
+ visited.add(uri);
+ }
+ }
+ }
+
+ // Use a sensible default prefix if we can't find one.
+ // We need to make sure the prefix is not one that was declared in the scope
+ // visited above.
+ prefix = SdkConstants.NS_RESOURCES.equals(nsUri) ? "android" : "ns"; //$NON-NLS-1$ //$NON-NLS-2$
+ String base = prefix;
+ for (int i = 1; visited.contains(prefix); i++) {
+ prefix = base + Integer.toString(i);
+ }
+ return prefix;
+ }
+
+ /**
+ * Gets the choices when the user is editing the name of an XML element.
+ * <p/>
+ * The user is editing the name of an element (the "parent").
+ * Find the grand-parent and if one is found, return its children element list.
+ * The name which is being edited should be one of those.
+ * <p/>
+ * Example: <manifest><applic*cursor* => returns the list of all elements that
+ * can be found under <manifest>, of which <application> is one of the choices.
+ *
+ * @return an ElementDescriptor[] or null if no valid element was found.
+ */
+ private Object[] getChoicesForElement(String parent, Node current_node) {
+ ElementDescriptor grandparent = null;
+ if (current_node.getParentNode().getNodeType() == Node.ELEMENT_NODE) {
+ grandparent = getDescriptor(current_node.getParentNode().getNodeName());
+ } else if (current_node.getParentNode().getNodeType() == Node.DOCUMENT_NODE) {
+ grandparent = getRootDescriptor();
+ }
+ if (grandparent != null) {
+ for (ElementDescriptor e : grandparent.getChildren()) {
+ if (e.getXmlName().startsWith(parent)) {
+ return grandparent.getChildren();
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Gets the choices when the user is editing an XML attribute.
+ * <p/>
+ * In input, attrInfo contains details on the analyzed context, namely whether the
+ * user is editing an attribute value (isInValue) or an attribute name.
+ * <p/>
+ * In output, attrInfo also contains two possible new values (this is a hack to circumvent
+ * the lack of out-parameters in Java):
+ * - AttribInfo.correctedPrefix if the user has been editing an attribute value and it has
+ * been detected that what the user typed is different from what extractElementPrefix()
+ * predicted. This happens because extractElementPrefix() stops when a character that
+ * cannot be an element name appears whereas parseAttributeInfo() uses a grammar more
+ * lenient as suitable for attribute values.
+ * - AttribInfo.needTag will be non-zero if we find that the attribute completion proposal
+ * must be double-quoted.
+ * @param currentUiNode
+ *
+ * @return an AttributeDescriptor[] if the user is editing an attribute name.
+ * a String[] if the user is editing an attribute value with some known values,
+ * or null if nothing is known about the context.
+ */
+ private Object[] getChoicesForAttribute(String parent,
+ Node currentNode, UiElementNode currentUiNode, AttribInfo attrInfo) {
+ Object[] choices = null;
+ if (attrInfo.isInValue) {
+ // Editing an attribute's value... Get the attribute name and then the
+ // possible choice for the tuple(parent,attribute)
+ String value = attrInfo.value;
+ if (value.startsWith("'") || value.startsWith("\"")) { //$NON-NLS-1$ //$NON-NLS-2$
+ value = value.substring(1);
+ // The prefix that was found at the beginning only scan for characters
+ // valid of tag name. We now know the real prefix for this attribute's
+ // value, which is needed to generate the completion choices below.
+ attrInfo.correctedPrefix = value;
+ } else {
+ attrInfo.needTag = '"';
+ }
+
+ if (currentUiNode != null) {
+ // look for an UI attribute matching the current attribute name
+ String attrName = attrInfo.name;
+ // remove any namespace prefix from the attribute name
+ int pos = attrName.indexOf(':');
+ if (pos >= 0) {
+ attrName = attrName.substring(pos + 1);
+ }
+
+ UiAttributeNode currAttrNode = null;
+ for (UiAttributeNode attrNode : currentUiNode.getUiAttributes()) {
+ if (attrNode.getDescriptor().getXmlLocalName().equals(attrName)) {
+ currAttrNode = attrNode;
+ break;
+ }
+ }
+
+ if (currAttrNode != null) {
+ choices = currAttrNode.getPossibleValues();
+
+ if (currAttrNode instanceof UiFlagAttributeNode) {
+ // A "flag" can consist of several values separated by "or" (|).
+ // If the correct prefix contains such a pipe character, we change
+ // it so that only the currently edited value is completed.
+ pos = value.indexOf('|');
+ if (pos >= 0) {
+ attrInfo.correctedPrefix = value = value.substring(pos + 1);
+ attrInfo.needTag = 0;
+ }
+ }
+ }
+ }
+
+ if (choices == null) {
+ // fallback on the older descriptor-only based lookup.
+
+ // in order to properly handle the special case of the name attribute in
+ // the action tag, we need the grandparent of the action node, to know
+ // what type of actions we need.
+ // e.g. activity -> intent-filter -> action[@name]
+ String greatGrandParentName = null;
+ Node grandParent = currentNode.getParentNode();
+ if (grandParent != null) {
+ Node greatGrandParent = grandParent.getParentNode();
+ if (greatGrandParent != null) {
+ greatGrandParentName = greatGrandParent.getLocalName();
+ }
+ }
+
+ AndroidTargetData data = mEditor.getTargetData();
+ if (data != null) {
+ choices = data.getAttributeValues(parent, attrInfo.name, greatGrandParentName);
+ }
+ }
+ } else {
+ // Editing an attribute's name... Get attributes valid for the parent node.
+ if (currentUiNode != null) {
+ choices = currentUiNode.getAttributeDescriptors();
+ } else {
+ ElementDescriptor parent_desc = getDescriptor(parent);
+ choices = parent_desc.getAttributes();
+ }
+ }
+ return choices;
+ }
+
+ /**
+ * Gets the choices when the user is editing an XML text node.
+ * <p/>
+ * This means the user is editing outside of any XML element or attribute.
+ * Simply return the list of XML elements that can be present there, based on the
+ * parent of the current node.
+ *
+ * @return An ElementDescriptor[] or null.
+ */
+ private Object[] getChoicesForTextNode(Node currentNode) {
+ Object[] choices = null;
+ String parent;
+ Node parent_node = currentNode.getParentNode();
+ if (parent_node.getNodeType() == Node.ELEMENT_NODE) {
+ // We're editing a text node which parent is an element node. Limit
+ // content assist to elements valid for the parent.
+ parent = parent_node.getNodeName();
+ ElementDescriptor desc = getDescriptor(parent);
+ if (desc != null) {
+ choices = desc.getChildren();
+ }
+ } else if (parent_node.getNodeType() == Node.DOCUMENT_NODE) {
+ // We're editing a text node at the first level (i.e. root node).
+ // Limit content assist to the only valid root elements.
+ choices = getRootDescriptor().getChildren();
+ }
+ return choices;
+ }
+
+ /**
+ * Given a list of choices found, generates the proposals to be displayed to the user.
+ * <p/>
+ * Choices is an object array. Items of the array can be:
+ * - ElementDescriptor: a possible element descriptor which XML name should be completed.
+ * - AttributeDescriptor: a possible attribute descriptor which XML name should be completed.
+ * - String: string values to display as-is to the user. Typically those are possible
+ * values for a given attribute.
+ *
+ * @return The ICompletionProposal[] to display to the user.
+ */
+ private ICompletionProposal[] computeProposals(int offset, Node currentNode,
+ Object[] choices, String wordPrefix, char need_tag,
+ boolean is_attribute, int selectionLength) {
+ ArrayList<CompletionProposal> proposals = new ArrayList<CompletionProposal>();
+ HashMap<String, String> nsUriMap = new HashMap<String, String>();
+
+ for (Object choice : choices) {
+ String keyword = null;
+ String nsPrefix = null;
+ Image icon = null;
+ String tooltip = null;
+ if (choice instanceof ElementDescriptor) {
+ keyword = ((ElementDescriptor)choice).getXmlName();
+ icon = ((ElementDescriptor)choice).getIcon();
+ tooltip = DescriptorsUtils.formatTooltip(((ElementDescriptor)choice).getTooltip());
+ } else if (choice instanceof TextValueDescriptor) {
+ continue; // Value nodes are not part of the completion choices
+ } else if (choice instanceof SeparatorAttributeDescriptor) {
+ continue; // not real attribute descriptors
+ } else if (choice instanceof AttributeDescriptor) {
+ keyword = ((AttributeDescriptor)choice).getXmlLocalName();
+ icon = ((AttributeDescriptor)choice).getIcon();
+ if (choice instanceof TextAttributeDescriptor) {
+ tooltip = ((TextAttributeDescriptor) choice).getTooltip();
+ }
+
+ String nsUri = ((AttributeDescriptor)choice).getNamespaceUri();
+ nsPrefix = nsUriMap.get(nsUri);
+ if (nsPrefix == null) {
+ nsPrefix = lookupNamespacePrefix(currentNode, nsUri);
+ nsUriMap.put(nsUri, nsPrefix);
+ }
+ if (nsPrefix != null) {
+ nsPrefix += ":"; //$NON-NLS-1$
+ }
+
+ } else if (choice instanceof String) {
+ keyword = (String) choice;
+ } else {
+ continue; // discard unknown choice
+ }
+
+ String nsKeyword = nsPrefix == null ? keyword : (nsPrefix + keyword);
+
+ if (keyword.startsWith(wordPrefix) ||
+ (nsPrefix != null && keyword.startsWith(nsPrefix)) ||
+ (nsPrefix != null && nsKeyword.startsWith(wordPrefix))) {
+ if (nsPrefix != null) {
+ keyword = nsPrefix + keyword;
+ }
+ String end_tag = ""; //$NON-NLS-1$
+ if (need_tag != 0) {
+ if (need_tag == '"') {
+ keyword = need_tag + keyword;
+ end_tag = String.valueOf(need_tag);
+ } else if (need_tag == '<') {
+ if (elementCanHaveChildren(choice)) {
+ end_tag = String.format("></%1$s>", keyword); //$NON-NLS-1$
+ keyword = need_tag + keyword;
+ } else {
+ keyword = need_tag + keyword;
+ end_tag = "/>"; //$NON-NLS-1$
+ }
+ }
+ }
+ CompletionProposal proposal = new CompletionProposal(
+ keyword + end_tag, // String replacementString
+ offset - wordPrefix.length(), // int replacementOffset
+ wordPrefix.length() + selectionLength, // int replacementLength
+ keyword.length(), // int cursorPosition (rel. to rplcmntOffset)
+ icon, // Image image
+ null, // String displayString
+ null, // IContextInformation contextInformation
+ tooltip // String additionalProposalInfo
+ );
+
+ proposals.add(proposal);
+ }
+ }
+
+ return proposals.toArray(new ICompletionProposal[proposals.size()]);
+ }
+
+ /**
+ * Indicates whether this descriptor describes an element that can potentially
+ * have children (either sub-elements or text value). If an element can have children,
+ * we want to explicitly write an opening and a separate closing tag.
+ * <p/>
+ * Elements can have children if the descriptor has children element descriptors
+ * or if one of the attributes is a TextValueDescriptor.
+ *
+ * @param descriptor An ElementDescriptor or an AttributeDescriptor
+ * @return True if the descriptor is an ElementDescriptor that can have children or a text value
+ */
+ private boolean elementCanHaveChildren(Object descriptor) {
+ if (descriptor instanceof ElementDescriptor) {
+ ElementDescriptor desc = (ElementDescriptor) descriptor;
+ if (desc.hasChildren()) {
+ return true;
+ }
+ for (AttributeDescriptor attr_desc : desc.getAttributes()) {
+ if (attr_desc instanceof TextValueDescriptor) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the element descriptor matching a given XML node name or null if it can't be
+ * found.
+ * <p/>
+ * This is simplistic; ideally we should consider the parent's chain to make sure we
+ * can differentiate between different hierarchy trees. Right now the first match found
+ * is returned.
+ */
+ private ElementDescriptor getDescriptor(String nodeName) {
+ return getRootDescriptor().findChildrenDescriptor(nodeName, true /* recursive */);
+ }
+
+ public IContextInformation[] computeContextInformation(ITextViewer viewer, int offset) {
+ return null;
+ }
+
+ /**
+ * Returns the characters which when entered by the user should
+ * automatically trigger the presentation of possible completions.
+ *
+ * In our case, we auto-activate on opening tags and attributes namespace.
+ *
+ * @return the auto activation characters for completion proposal or <code>null</code>
+ * if no auto activation is desired
+ */
+ public char[] getCompletionProposalAutoActivationCharacters() {
+ return new char[]{ '<', ':', '=' };
+ }
+
+ public char[] getContextInformationAutoActivationCharacters() {
+ return null;
+ }
+
+ public IContextInformationValidator getContextInformationValidator() {
+ return null;
+ }
+
+ public String getErrorMessage() {
+ return null;
+ }
+
+ /**
+ * Heuristically extracts the prefix used for determining template relevance
+ * from the viewer's document. The default implementation returns the String from
+ * offset backwards that forms a potential XML element name, attribute name or
+ * attribute value.
+ *
+ * The part were we access the docment was extracted from
+ * org.eclipse.jface.text.templatesTemplateCompletionProcessor and adapted to our needs.
+ *
+ * @param viewer the viewer
+ * @param offset offset into document
+ * @return the prefix to consider
+ */
+ protected String extractElementPrefix(ITextViewer viewer, int offset) {
+ int i = offset;
+ IDocument document = viewer.getDocument();
+ if (i > document.getLength()) return ""; //$NON-NLS-1$
+
+ try {
+ for (; i > 0; --i) {
+ char ch = document.getChar(i - 1);
+
+ // We want all characters that can form a valid:
+ // - element name, e.g. anything that is a valid Java class/variable literal.
+ // - attribute name, including : for the namespace
+ // - attribute value.
+ // Before we were inclusive and that made the code fragile. So now we're
+ // going to be exclusive: take everything till we get one of:
+ // - any form of whitespace
+ // - any xml separator, e.g. < > ' " and =
+ if (Character.isWhitespace(ch) ||
+ ch == '<' || ch == '>' || ch == '\'' || ch == '"' || ch == '=') {
+ break;
+ }
+ }
+
+ return document.get(i, offset - i);
+ } catch (BadLocationException e) {
+ return ""; //$NON-NLS-1$
+ }
+ }
+
+ /**
+ * Extracts the character at the given offset.
+ * Returns 0 if the offset is invalid.
+ */
+ protected char extractChar(ITextViewer viewer, int offset) {
+ IDocument document = viewer.getDocument();
+ if (offset > document.getLength()) return 0;
+
+ try {
+ return document.getChar(offset);
+ } catch (BadLocationException e) {
+ return 0;
+ }
+ }
+
+ /**
+ * Information about the current edit of an attribute as reported by parseAttributeInfo.
+ */
+ private class AttribInfo {
+ /** True if the cursor is located in an attribute's value, false if in an attribute name */
+ public boolean isInValue = false;
+ /** The attribute name. Null when not set. */
+ public String name = null;
+ /** The attribute value. Null when not set. The value *may* start with a quote
+ * (' or "), in which case we know we don't need to quote the string for the user */
+ public String value = null;
+ /** String typed by the user so far (i.e. right before requesting code completion),
+ * which will be corrected if we find a possible completion for an attribute value.
+ * See the long comment in getChoicesForAttribute(). */
+ public String correctedPrefix = null;
+ /** Non-zero if an attribute value need a start/end tag (i.e. quotes or brackets) */
+ public char needTag = 0;
+ }
+
+
+ /**
+ * Try to guess if the cursor is editing an element's name or an attribute following an
+ * element. If it's an attribute, try to find if an attribute name is being defined or
+ * its value.
+ * <br/>
+ * This is currently *only* called when we know the cursor is after a complete element
+ * tag name, so it should never return null.
+ * <br/>
+ * Reference for XML syntax: http://www.w3.org/TR/2006/REC-xml-20060816/#sec-starttags
+ * <br/>
+ * @return An AttribInfo describing which attribute is being edited or null if the cursor is
+ * not editing an attribute (in which case it must be an element's name).
+ */
+ private AttribInfo parseAttributeInfo(ITextViewer viewer, int offset) {
+ AttribInfo info = new AttribInfo();
+
+ IDocument document = viewer.getDocument();
+ int n = document.getLength();
+ if (offset <= n) {
+ try {
+ n = offset;
+ for (;offset > 0; --offset) {
+ char ch = document.getChar(offset - 1);
+ if (ch == '<') break;
+ }
+
+ // text will contain the full string of the current element,
+ // i.e. whatever is after the "<" to the current cursor
+ String text = document.get(offset, n - offset);
+
+ // Normalize whitespace to single spaces
+ text = sWhitespace.matcher(text).replaceAll(" "); //$NON-NLS-1$
+
+ // Remove the leading element name. By spec, it must be after the < without
+ // any whitespace. If there's nothing left, no attribute has been defined yet.
+ // Be sure to keep any whitespace after the initial word if any, as it matters.
+ text = sFirstElementWord.matcher(text).replaceFirst(""); //$NON-NLS-1$
+
+ // There MUST be space after the element name. If not, the cursor is still
+ // defining the element name.
+ if (!text.startsWith(" ")) { //$NON-NLS-1$
+ return null;
+ }
+
+ // Remove full attributes:
+ // Syntax:
+ // name = "..." quoted string with all but < and "
+ // or:
+ // name = '...' quoted string with all but < and '
+ String temp;
+ do {
+ temp = text;
+ text = sFirstAttribute.matcher(temp).replaceFirst(""); //$NON-NLS-1$
+ } while(!temp.equals(text));
+
+ // Now we're left with 3 cases:
+ // - nothing: either there is no attribute definition or the cursor located after
+ // a completed attribute definition.
+ // - a string with no =: the user is writing an attribute name. This case can be
+ // merged with the previous one.
+ // - string with an = sign, optionally followed by a quote (' or "): the user is
+ // writing the value of the attribute.
+ int pos_equal = text.indexOf('=');
+ if (pos_equal == -1) {
+ info.isInValue = false;
+ info.name = text.trim();
+ } else {
+ info.isInValue = true;
+ info.name = text.substring(0, pos_equal).trim();
+ info.value = text.substring(pos_equal + 1).trim();
+ }
+ return info;
+ } catch (BadLocationException e) {
+ // pass
+ }
+ }
+
+ return null;
+ }
+
+
+ /**
+ * Returns the XML DOM node corresponding to the given offset of the given document.
+ */
+ protected Node getNode(ITextViewer viewer, int offset) {
+ Node node = null;
+ try {
+ IModelManager mm = StructuredModelManager.getModelManager();
+ if (mm != null) {
+ IStructuredModel model = mm.getExistingModelForRead(viewer.getDocument());
+ if (model != null) {
+ for(; offset >= 0 && node == null; --offset) {
+ node = (Node) model.getIndexedRegion(offset);
+ }
+ }
+ }
+ } catch (Exception e) {
+ // Ignore exceptions.
+ }
+
+ return node;
+ }
+
+ /**
+ * Computes (if needed) and returns the root descriptor.
+ */
+ private ElementDescriptor getRootDescriptor() {
+ if (mRootDescriptor == null) {
+ AndroidTargetData data = mEditor.getTargetData();
+ if (data != null) {
+ IDescriptorProvider descriptorProvider = data.getDescriptorProvider(mDescriptorId);
+
+ if (descriptorProvider != null) {
+ mRootDescriptor = new ElementDescriptor("",
+ descriptorProvider.getRootElementDescriptors());
+ }
+ }
+ }
+
+ return mRootDescriptor;
+ }
+
+ /**
+ * Returns the active {@link AndroidEditor} matching this source viewer.
+ */
+ private AndroidEditor getAndroidEditor(ITextViewer viewer) {
+ IWorkbenchWindow wwin = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
+ if (wwin != null) {
+ IWorkbenchPage page = wwin.getActivePage();
+ if (page != null) {
+ IEditorPart editor = page.getActiveEditor();
+ if (editor instanceof AndroidEditor) {
+ ISourceViewer ssviewer = ((AndroidEditor) editor).getStructuredSourceViewer();
+ if (ssviewer == viewer) {
+ return (AndroidEditor) editor;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/AndroidEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/AndroidEditor.java
new file mode 100644
index 0000000..c7541e9
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/AndroidEditor.java
@@ -0,0 +1,828 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.sdk.Sdk;
+import com.android.ide.eclipse.adt.sdk.Sdk.ITargetChangeListener;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+import com.android.sdklib.IAndroidTarget;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IResourceChangeEvent;
+import org.eclipse.core.resources.IResourceChangeListener;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.QualifiedName;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.dialogs.ErrorDialog;
+import org.eclipse.jface.text.source.ISourceViewer;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.ui.IActionBars;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IEditorSite;
+import org.eclipse.ui.IFileEditorInput;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.actions.ActionFactory;
+import org.eclipse.ui.browser.IWorkbenchBrowserSupport;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.editor.FormEditor;
+import org.eclipse.ui.forms.editor.IFormPage;
+import org.eclipse.ui.forms.events.HyperlinkAdapter;
+import org.eclipse.ui.forms.events.HyperlinkEvent;
+import org.eclipse.ui.forms.events.IHyperlinkListener;
+import org.eclipse.ui.forms.widgets.FormText;
+import org.eclipse.ui.internal.browser.WorkbenchBrowserSupport;
+import org.eclipse.ui.part.FileEditorInput;
+import org.eclipse.ui.part.MultiPageEditorPart;
+import org.eclipse.ui.part.WorkbenchPart;
+import org.eclipse.wst.sse.core.StructuredModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IModelStateListener;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+import org.eclipse.wst.sse.ui.StructuredTextEditor;
+import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
+import org.w3c.dom.Document;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+/**
+ * Multi-page form editor for Android XML files.
+ * <p/>
+ * It is designed to work with a {@link StructuredTextEditor} that will display an XML file.
+ * <br/>
+ * Derived classes must implement createFormPages to create the forms before the
+ * source editor. This can be a no-op if desired.
+ */
+public abstract class AndroidEditor extends FormEditor implements IResourceChangeListener {
+
+ /** Preference name for the current page of this file */
+ private static final String PREF_CURRENT_PAGE = "_current_page";
+
+ /** Id string used to create the Android SDK browser */
+ private static String BROWSER_ID = "android"; // $NON-NLS-1$
+
+ /** Page id of the XML source editor, used for switching tabs programmatically */
+ public final static String TEXT_EDITOR_ID = "editor_part"; //$NON-NLS-1$
+
+ /** Width hint for text fields. Helps the grid layout resize properly on smaller screens */
+ public static final int TEXT_WIDTH_HINT = 50;
+
+ /** Page index of the text editor (always the last page) */
+ private int mTextPageIndex;
+ /** The text editor */
+ private StructuredTextEditor mTextEditor;
+ /** Listener for the XML model from the StructuredEditor */
+ private XmlModelStateListener mXmlModelStateListener;
+ /** Listener to update the root node if the target of the file is changed because of a
+ * SDK location change or a project target change */
+ private ITargetChangeListener mTargetListener;
+
+ /**
+ * Creates a form editor.
+ */
+ public AndroidEditor() {
+ super();
+ ResourcesPlugin.getWorkspace().addResourceChangeListener(this);
+
+ mTargetListener = new ITargetChangeListener() {
+ public void onProjectTargetChange(IProject changedProject) {
+ if (changedProject == getProject()) {
+ onTargetsLoaded();
+ }
+ }
+
+ public void onTargetsLoaded() {
+ commitPages(false /* onSave */);
+
+ // recreate the ui root node always
+ initUiRootNode(true /*force*/);
+ }
+ };
+ AdtPlugin.getDefault().addTargetListener(mTargetListener);
+ }
+
+ // ---- Abstract Methods ----
+
+ /**
+ * Returns the root node of the UI element hierarchy manipulated by the current
+ * UI node editor.
+ */
+ abstract public UiElementNode getUiRootNode();
+
+ /**
+ * Creates the various form pages.
+ * <p/>
+ * Derived classes must implement this to add their own specific tabs.
+ */
+ abstract protected void createFormPages();
+
+ /**
+ * Creates the initial UI Root Node, including the known mandatory elements.
+ * @param force if true, a new UiManifestNode is recreated even if it already exists.
+ */
+ abstract protected void initUiRootNode(boolean force);
+
+ /**
+ * Subclasses should override this method to process the new XML Model, which XML
+ * root node is given.
+ *
+ * The base implementation is empty.
+ *
+ * @param xml_doc The XML document, if available, or null if none exists.
+ */
+ protected void xmlModelChanged(Document xml_doc) {
+ // pass
+ }
+
+ // ---- Base Class Overrides, Interfaces Implemented ----
+
+ /**
+ * Creates the pages of the multi-page editor.
+ */
+ @Override
+ protected void addPages() {
+ createAndroidPages();
+ selectDefaultPage(null /* defaultPageId */);
+ }
+
+ /**
+ * Creates the page for the Android Editors
+ */
+ protected void createAndroidPages() {
+ createFormPages();
+ createTextEditor();
+
+ createUndoRedoActions();
+ }
+
+ /**
+ * Creates undo redo actions for the editor site (so that it works for any page of this
+ * multi-page editor) by re-using the actions defined by the {@link StructuredTextEditor}
+ * (aka the XML text editor.)
+ */
+ private void createUndoRedoActions() {
+ IActionBars bars = getEditorSite().getActionBars();
+ if (bars != null) {
+ IAction action = mTextEditor.getAction(ActionFactory.UNDO.getId());
+ bars.setGlobalActionHandler(ActionFactory.UNDO.getId(), action);
+
+ action = mTextEditor.getAction(ActionFactory.REDO.getId());
+ bars.setGlobalActionHandler(ActionFactory.REDO.getId(), action);
+
+ bars.updateActionBars();
+ }
+ }
+
+ /**
+ * Selects the default active page.
+ * @param defaultPageId the id of the page to show. If <code>null</code> the editor attempts to
+ * find the default page in the properties of the {@link IResource} object being edited.
+ */
+ protected void selectDefaultPage(String defaultPageId) {
+ if (defaultPageId == null) {
+ if (getEditorInput() instanceof IFileEditorInput) {
+ IFile file = ((IFileEditorInput) getEditorInput()).getFile();
+
+ QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID,
+ getClass().getSimpleName() + PREF_CURRENT_PAGE);
+ String pageId;
+ try {
+ pageId = file.getPersistentProperty(qname);
+ if (pageId != null) {
+ defaultPageId = pageId;
+ }
+ } catch (CoreException e) {
+ // ignored
+ }
+ }
+ }
+
+ if (defaultPageId != null) {
+ try {
+ setActivePage(Integer.parseInt(defaultPageId));
+ } catch (Exception e) {
+ // We can get NumberFormatException from parseInt but also
+ // AssertionError from setActivePage when the index is out of bounds.
+ // Generally speaking we just want to ignore any exception and fall back on the
+ // first page rather than crash the editor load. Logging the error is enough.
+ AdtPlugin.log(e, "Selecting page '%s' in AndroidEditor failed", defaultPageId);
+ }
+ }
+ }
+
+ /**
+ * Removes all the pages from the editor.
+ */
+ protected void removePages() {
+ int count = getPageCount();
+ for (int i = count - 1 ; i >= 0 ; i--) {
+ removePage(i);
+ }
+ }
+
+ /**
+ * Overrides the parent's setActivePage to be able to switch to the xml editor.
+ *
+ * If the special pageId TEXT_EDITOR_ID is given, switches to the mTextPageIndex page.
+ * This is needed because the editor doesn't actually derive from IFormPage and thus
+ * doesn't have the get-by-page-id method. In this case, the method returns null since
+ * IEditorPart does not implement IFormPage.
+ */
+ @Override
+ public IFormPage setActivePage(String pageId) {
+ if (pageId.equals(TEXT_EDITOR_ID)) {
+ super.setActivePage(mTextPageIndex);
+ return null;
+ } else {
+ return super.setActivePage(pageId);
+ }
+ }
+
+
+ /**
+ * Notifies this multi-page editor that the page with the given id has been
+ * activated. This method is called when the user selects a different tab.
+ *
+ * @see MultiPageEditorPart#pageChange(int)
+ */
+ @Override
+ protected void pageChange(int newPageIndex) {
+ super.pageChange(newPageIndex);
+
+ if (getEditorInput() instanceof IFileEditorInput) {
+ IFile file = ((IFileEditorInput) getEditorInput()).getFile();
+
+ QualifiedName qname = new QualifiedName(AdtPlugin.PLUGIN_ID,
+ getClass().getSimpleName() + PREF_CURRENT_PAGE);
+ try {
+ file.setPersistentProperty(qname, Integer.toString(newPageIndex));
+ } catch (CoreException e) {
+ // ignore
+ }
+ }
+ }
+
+ /**
+ * Notifies this listener that some resource changes
+ * are happening, or have already happened.
+ *
+ * Closes all project files on project close.
+ * @see IResourceChangeListener
+ */
+ public void resourceChanged(final IResourceChangeEvent event) {
+ if (event.getType() == IResourceChangeEvent.PRE_CLOSE) {
+ Display.getDefault().asyncExec(new Runnable() {
+ public void run() {
+ IWorkbenchPage[] pages = getSite().getWorkbenchWindow()
+ .getPages();
+ for (int i = 0; i < pages.length; i++) {
+ if (((FileEditorInput)mTextEditor.getEditorInput())
+ .getFile().getProject().equals(
+ event.getResource())) {
+ IEditorPart editorPart = pages[i].findEditor(mTextEditor
+ .getEditorInput());
+ pages[i].closeEditor(editorPart, true);
+ }
+ }
+ }
+ });
+ }
+ }
+
+ /**
+ * Initializes the editor part with a site and input.
+ * <p/>
+ * Checks that the input is an instance of {@link IFileEditorInput}.
+ *
+ * @see FormEditor
+ */
+ @Override
+ public void init(IEditorSite site, IEditorInput editorInput) throws PartInitException {
+ if (!(editorInput instanceof IFileEditorInput))
+ throw new PartInitException("Invalid Input: Must be IFileEditorInput");
+ super.init(site, editorInput);
+ }
+
+ /**
+ * Removes attached listeners.
+ *
+ * @see WorkbenchPart
+ */
+ @Override
+ public void dispose() {
+ IStructuredModel xml_model = getModelForRead();
+ if (xml_model != null) {
+ try {
+ if (mXmlModelStateListener != null) {
+ xml_model.removeModelStateListener(mXmlModelStateListener);
+ }
+
+ } finally {
+ xml_model.releaseFromRead();
+ }
+ }
+ ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
+
+ if (mTargetListener != null) {
+ AdtPlugin.getDefault().removeTargetListener(mTargetListener);
+ mTargetListener = null;
+ }
+
+ super.dispose();
+ }
+
+ /**
+ * Commit all dirty pages then saves the contents of the text editor.
+ * <p/>
+ * This works by committing all data to the XML model and then
+ * asking the Structured XML Editor to save the XML.
+ *
+ * @see IEditorPart
+ */
+ @Override
+ public void doSave(IProgressMonitor monitor) {
+ commitPages(true /* onSave */);
+
+ // The actual "save" operation is done by the Structured XML Editor
+ getEditor(mTextPageIndex).doSave(monitor);
+ }
+
+ /* (non-Javadoc)
+ * Saves the contents of this editor to another object.
+ * <p>
+ * Subclasses must override this method to implement the open-save-close lifecycle
+ * for an editor. For greater details, see <code>IEditorPart</code>
+ * </p>
+ *
+ * @see IEditorPart
+ */
+ @Override
+ public void doSaveAs() {
+ commitPages(true /* onSave */);
+
+ IEditorPart editor = getEditor(mTextPageIndex);
+ editor.doSaveAs();
+ setPageText(mTextPageIndex, editor.getTitle());
+ setInput(editor.getEditorInput());
+ }
+
+ /**
+ * Commits all dirty pages in the editor. This method should
+ * be called as a first step of a 'save' operation.
+ * <p/>
+ * This is the same implementation as in {@link FormEditor}
+ * except it fixes two bugs: a cast to IFormPage is done
+ * from page.get(i) <em>before</em> being tested with instanceof.
+ * Another bug is that the last page might be a null pointer.
+ * <p/>
+ * The incorrect casting makes the original implementation crash due
+ * to our {@link StructuredTextEditor} not being an {@link IFormPage}
+ * so we have to override and duplicate to fix it.
+ *
+ * @param onSave <code>true</code> if commit is performed as part
+ * of the 'save' operation, <code>false</code> otherwise.
+ * @since 3.3
+ */
+ @Override
+ public void commitPages(boolean onSave) {
+ if (pages != null) {
+ for (int i = 0; i < pages.size(); i++) {
+ Object page = pages.get(i);
+ if (page != null && page instanceof IFormPage) {
+ IFormPage form_page = (IFormPage) page;
+ IManagedForm managed_form = form_page.getManagedForm();
+ if (managed_form != null && managed_form.isDirty()) {
+ managed_form.commit(onSave);
+ }
+ }
+ }
+ }
+ }
+
+ /* (non-Javadoc)
+ * Returns whether the "save as" operation is supported by this editor.
+ * <p>
+ * Subclasses must override this method to implement the open-save-close lifecycle
+ * for an editor. For greater details, see <code>IEditorPart</code>
+ * </p>
+ *
+ * @see IEditorPart
+ */
+ @Override
+ public boolean isSaveAsAllowed() {
+ return false;
+ }
+
+ // ---- Local methods ----
+
+
+ /**
+ * Helper method that creates a new hyper-link Listener.
+ * Used by derived classes which need active links in {@link FormText}.
+ * <p/>
+ * This link listener handles two kinds of URLs:
+ * <ul>
+ * <li> Links starting with "http" are simply sent to a local browser.
+ * <li> Links starting with "file:/" are simply sent to a local browser.
+ * <li> Links starting with "page:" are expected to be an editor page id to switch to.
+ * <li> Other links are ignored.
+ * </ul>
+ *
+ * @return A new hyper-link listener for FormText to use.
+ */
+ public final IHyperlinkListener createHyperlinkListener() {
+ return new HyperlinkAdapter() {
+ /**
+ * Switch to the page corresponding to the link that has just been clicked.
+ * For this purpose, the HREF of the &lt;a&gt; tags above is the page ID to switch to.
+ */
+ @Override
+ public void linkActivated(HyperlinkEvent e) {
+ super.linkActivated(e);
+ String link = e.data.toString();
+ if (link.startsWith("http") || //$NON-NLS-1$
+ link.startsWith("file:/")) { //$NON-NLS-1$
+ openLinkInBrowser(link);
+ } else if (link.startsWith("page:")) { //$NON-NLS-1$
+ // Switch to an internal page
+ setActivePage(link.substring(5 /* strlen("page:") */));
+ }
+ }
+ };
+ }
+
+ /**
+ * Open the http link into a browser
+ *
+ * @param link The URL to open in a browser
+ */
+ private void openLinkInBrowser(String link) {
+ try {
+ IWorkbenchBrowserSupport wbs = WorkbenchBrowserSupport.getInstance();
+ wbs.createBrowser(BROWSER_ID).openURL(new URL(link));
+ } catch (PartInitException e1) {
+ // pass
+ } catch (MalformedURLException e1) {
+ // pass
+ }
+ }
+
+ /**
+ * Creates the XML source editor.
+ * <p/>
+ * Memorizes the index page of the source editor (it's always the last page, but the number
+ * of pages before can change.)
+ * <br/>
+ * Retrieves the underlying XML model from the StructuredEditor and attaches a listener to it.
+ * Finally triggers modelChanged() on the model listener -- derived classes can use this
+ * to initialize the model the first time.
+ * <p/>
+ * Called only once <em>after</em> createFormPages.
+ */
+ private void createTextEditor() {
+ try {
+ mTextEditor = new StructuredTextEditor();
+ int index = addPage(mTextEditor, getEditorInput());
+ mTextPageIndex = index;
+ setPageText(index, mTextEditor.getTitle());
+
+ if (!(mTextEditor.getTextViewer().getDocument() instanceof IStructuredDocument)) {
+ Status status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "Error opening the Android XML editor. Is the document an XML file?");
+ throw new RuntimeException("Android XML Editor Error", new CoreException(status));
+ }
+
+ IStructuredModel xml_model = getModelForRead();
+ if (xml_model != null) {
+ try {
+ mXmlModelStateListener = new XmlModelStateListener();
+ xml_model.addModelStateListener(mXmlModelStateListener);
+ mXmlModelStateListener.modelChanged(xml_model);
+ } catch (Exception e) {
+ AdtPlugin.log(e, "Error while loading editor"); //$NON-NLS-1$
+ } finally {
+ xml_model.releaseFromRead();
+ }
+ }
+ } catch (PartInitException e) {
+ ErrorDialog.openError(getSite().getShell(),
+ "Android XML Editor Error", null, e.getStatus());
+ }
+ }
+
+ /**
+ * Returns the ISourceViewer associated with the Structured Text editor.
+ */
+ public final ISourceViewer getStructuredSourceViewer() {
+ if (mTextEditor != null) {
+ // We can't access mEditor.getSourceViewer() because it is protected,
+ // however getTextViewer simply returns the SourceViewer casted, so we
+ // can use it instead.
+ return mTextEditor.getTextViewer();
+ }
+ return null;
+ }
+
+ /**
+ * Returns the {@link IStructuredDocument} used by the StructuredTextEditor (aka Source
+ * Editor) or null if not available.
+ */
+ public final IStructuredDocument getStructuredDocument() {
+ if (mTextEditor != null && mTextEditor.getTextViewer() != null) {
+ return (IStructuredDocument) mTextEditor.getTextViewer().getDocument();
+ }
+ return null;
+ }
+
+ /**
+ * Returns a version of the model that has been shared for read.
+ * <p/>
+ * Callers <em>must</em> call model.releaseFromRead() when done, typically
+ * in a try..finally clause.
+ *
+ * @return The model for the XML document or null if cannot be obtained from the editor
+ */
+ public final IStructuredModel getModelForRead() {
+ IStructuredDocument document = getStructuredDocument();
+ if (document != null) {
+ IModelManager mm = StructuredModelManager.getModelManager();
+ if (mm != null) {
+ return mm.getModelForRead(document);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns a version of the model that has been shared for edit.
+ * <p/>
+ * Callers <em>must</em> call model.releaseFromEdit() when done, typically
+ * in a try..finally clause.
+ *
+ * @return The model for the XML document or null if cannot be obtained from the editor
+ */
+ public final IStructuredModel getModelForEdit() {
+ IStructuredDocument document = getStructuredDocument();
+ if (document != null) {
+ IModelManager mm = StructuredModelManager.getModelManager();
+ if (mm != null) {
+ return mm.getModelForEdit(document);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Helper class to perform edits on the XML model whilst making sure the
+ * model has been prepared to be changed.
+ * <p/>
+ * It first gets a model for edition using {@link #getModelForEdit()},
+ * then calls {@link IStructuredModel#aboutToChangeModel()},
+ * then performs the requested action
+ * and finally calls {@link IStructuredModel#changedModel()}
+ * and {@link IStructuredModel#releaseFromEdit()}.
+ * <p/>
+ * The method is synchronous. As soon as the {@link IStructuredModel#changedModel()} method
+ * is called, XML model listeners will be triggered.
+ *
+ * @param edit_action Something that will change the XML.
+ */
+ public final void editXmlModel(Runnable edit_action) {
+ IStructuredModel model = getModelForEdit();
+ try {
+ model.aboutToChangeModel();
+ edit_action.run();
+ } finally {
+ // Notify the model we're done modifying it. This must *always* be executed.
+ model.changedModel();
+ model.releaseFromEdit();
+ }
+ }
+
+ /**
+ * Starts an "undo recording" session. This is managed by the underlying undo manager
+ * associated to the structured XML model.
+ * <p/>
+ * There <em>must</em> be a corresponding call to {@link #endUndoRecording()}.
+ * <p/>
+ * beginUndoRecording/endUndoRecording calls can be nested (inner calls are ignored, only one
+ * undo operation is recorded.)
+ *
+ * @param label The label for the undo operation. Can be null but we should really try to put
+ * something meaningful if possible.
+ * @return True if the undo recording actually started, false if any kind of error occured.
+ * {@link #endUndoRecording()} should only be called if True is returned.
+ */
+ private final boolean beginUndoRecording(String label) {
+ IStructuredDocument document = getStructuredDocument();
+ if (document != null) {
+ IModelManager mm = StructuredModelManager.getModelManager();
+ if (mm != null) {
+ IStructuredModel model = mm.getModelForEdit(document);
+ if (model != null) {
+ model.beginRecording(this, label);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Ends an "undo recording" session.
+ * <p/>
+ * This is the counterpart call to {@link #beginUndoRecording(String)} and should only be
+ * used if the initial call returned true.
+ */
+ private final void endUndoRecording() {
+ IStructuredDocument document = getStructuredDocument();
+ if (document != null) {
+ IModelManager mm = StructuredModelManager.getModelManager();
+ if (mm != null) {
+ IStructuredModel model = mm.getModelForEdit(document);
+ if (model != null) {
+ model.endRecording(this);
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates an "undo recording" session by calling the undoableAction runnable
+ * using {@link #beginUndoRecording(String)} and {@link #endUndoRecording()}.
+ * <p>
+ * You can nest several calls to {@link #wrapUndoRecording(String, Runnable)}, only one
+ * recording session will be created.
+ *
+ * @param label The label for the undo operation. Can be null. Ideally we should really try
+ * to put something meaningful if possible.
+ */
+ public void wrapUndoRecording(String label, Runnable undoableAction) {
+ boolean recording = false;
+ try {
+ recording = beginUndoRecording(label);
+ undoableAction.run();
+ } finally {
+ if (recording) {
+ endUndoRecording();
+ }
+ }
+ }
+
+ /**
+ * Returns the XML {@link Document} or null if we can't get it
+ */
+ protected final Document getXmlDocument(IStructuredModel model) {
+ if (model == null) {
+ AdtPlugin.log(IStatus.WARNING, "Android Editor: No XML model for root node."); //$NON-NLS-1$
+ return null;
+ }
+
+ if (model instanceof IDOMModel) {
+ IDOMModel dom_model = (IDOMModel) model;
+ return dom_model.getDocument();
+ }
+ return null;
+ }
+
+ /**
+ * Returns the {@link IProject} for the edited file.
+ */
+ public IProject getProject() {
+ if (mTextEditor != null) {
+ IEditorInput input = mTextEditor.getEditorInput();
+ if (input instanceof FileEditorInput) {
+ FileEditorInput fileInput = (FileEditorInput)input;
+ IFile inputFile = fileInput.getFile();
+
+ if (inputFile != null) {
+ return inputFile.getProject();
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the {@link AndroidTargetData} for the edited file.
+ */
+ public AndroidTargetData getTargetData() {
+ IProject project = getProject();
+ if (project != null) {
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ IAndroidTarget target = currentSdk.getTarget(project);
+
+ if (target != null) {
+ return currentSdk.getTargetData(target);
+ }
+ }
+ }
+
+ return null;
+ }
+
+
+ /**
+ * Listen to changes in the underlying XML model in the structured editor.
+ */
+ private class XmlModelStateListener implements IModelStateListener {
+
+ /**
+ * A model is about to be changed. This typically is initiated by one
+ * client of the model, to signal a large change and/or a change to the
+ * model's ID or base Location. A typical use might be if a client might
+ * want to suspend processing until all changes have been made.
+ * <p/>
+ * This AndroidEditor implementation of IModelChangedListener is empty.
+ */
+ public void modelAboutToBeChanged(IStructuredModel model) {
+ // pass
+ }
+
+ /**
+ * Signals that the changes foretold by modelAboutToBeChanged have been
+ * made. A typical use might be to refresh, or to resume processing that
+ * was suspended as a result of modelAboutToBeChanged.
+ * <p/>
+ * This AndroidEditor implementation calls the xmlModelChanged callback.
+ */
+ public void modelChanged(IStructuredModel model) {
+ xmlModelChanged(getXmlDocument(model));
+ }
+
+ /**
+ * Notifies that a model's dirty state has changed, and passes that state
+ * in isDirty. A model becomes dirty when any change is made, and becomes
+ * not-dirty when the model is saved.
+ * <p/>
+ * This AndroidEditor implementation of IModelChangedListener is empty.
+ */
+ public void modelDirtyStateChanged(IStructuredModel model, boolean isDirty) {
+ // pass
+ }
+
+ /**
+ * A modelDeleted means the underlying resource has been deleted. The
+ * model itself is not removed from model management until all have
+ * released it. Note: baseLocation is not (necessarily) changed in this
+ * event, but may not be accurate.
+ * <p/>
+ * This AndroidEditor implementation of IModelChangedListener is empty.
+ */
+ public void modelResourceDeleted(IStructuredModel model) {
+ // pass
+ }
+
+ /**
+ * A model has been renamed or copied (as in saveAs..). In the renamed
+ * case, the two paramenters are the same instance, and only contain the
+ * new info for id and base location.
+ * <p/>
+ * This AndroidEditor implementation of IModelChangedListener is empty.
+ */
+ public void modelResourceMoved(IStructuredModel oldModel, IStructuredModel newModel) {
+ // pass
+ }
+
+ /**
+ * This AndroidEditor implementation of IModelChangedListener is empty.
+ */
+ public void modelAboutToBeReinitialized(IStructuredModel structuredModel) {
+ // pass
+ }
+
+ /**
+ * This AndroidEditor implementation of IModelChangedListener is empty.
+ */
+ public void modelReinitialized(IStructuredModel structuredModel) {
+ // pass
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/AndroidSourceViewerConfig.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/AndroidSourceViewerConfig.java
new file mode 100644
index 0000000..ab17bef
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/AndroidSourceViewerConfig.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors;
+
+
+import org.eclipse.jface.text.IAutoEditStrategy;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.ITextHover;
+import org.eclipse.jface.text.contentassist.IContentAssistProcessor;
+import org.eclipse.jface.text.contentassist.IContentAssistant;
+import org.eclipse.jface.text.formatter.IContentFormatter;
+import org.eclipse.jface.text.source.ISourceViewer;
+import org.eclipse.jface.viewers.IInputProvider;
+import org.eclipse.wst.sse.core.text.IStructuredPartitions;
+import org.eclipse.wst.xml.core.text.IXMLPartitions;
+import org.eclipse.wst.xml.ui.StructuredTextViewerConfigurationXML;
+
+import java.util.ArrayList;
+
+/**
+ * Base Source Viewer Configuration for Android resources.
+ */
+public class AndroidSourceViewerConfig extends StructuredTextViewerConfigurationXML {
+
+ /** Content Assist Processor to use for all handled partitions. */
+ private IContentAssistProcessor mProcessor;
+
+ public AndroidSourceViewerConfig(IContentAssistProcessor processor) {
+ super();
+ mProcessor = processor;
+ }
+
+ @Override
+ public IContentAssistant getContentAssistant(ISourceViewer sourceViewer) {
+ return super.getContentAssistant(sourceViewer);
+ }
+
+ /**
+ * Returns the content assist processors that will be used for content
+ * assist in the given source viewer and for the given partition type.
+ *
+ * @param sourceViewer the source viewer to be configured by this
+ * configuration
+ * @param partitionType the partition type for which the content assist
+ * processors are applicable
+ * @return IContentAssistProcessors or null if should not be supported
+ */
+ @Override
+ protected IContentAssistProcessor[] getContentAssistProcessors(
+ ISourceViewer sourceViewer, String partitionType) {
+ ArrayList<IContentAssistProcessor> processors = new ArrayList<IContentAssistProcessor>();
+ if (partitionType == IStructuredPartitions.UNKNOWN_PARTITION ||
+ partitionType == IStructuredPartitions.DEFAULT_PARTITION ||
+ partitionType == IXMLPartitions.XML_DEFAULT) {
+ if (sourceViewer instanceof IInputProvider) {
+ IInputProvider input = (IInputProvider) sourceViewer;
+ Object a = input.getInput();
+ if (a != null)
+ a.toString();
+ }
+
+ IDocument doc = sourceViewer.getDocument();
+ if (doc != null)
+ doc.toString();
+
+ processors.add(mProcessor);
+ }
+
+ IContentAssistProcessor[] others = super.getContentAssistProcessors(sourceViewer,
+ partitionType);
+ if (others != null && others.length > 0) {
+ for (IContentAssistProcessor p : others) {
+ processors.add(p);
+ }
+ }
+
+ if (processors.size() > 0) {
+ return processors.toArray(new IContentAssistProcessor[processors.size()]);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public ITextHover getTextHover(ISourceViewer sourceViewer, String contentType) {
+ // TODO text hover for android xml
+ return super.getTextHover(sourceViewer, contentType);
+ }
+
+ @Override
+ public IAutoEditStrategy[] getAutoEditStrategies(
+ ISourceViewer sourceViewer, String contentType) {
+ // TODO auto edit strategies for android xml
+ return super.getAutoEditStrategies(sourceViewer, contentType);
+ }
+
+ @Override
+ public IContentFormatter getContentFormatter(ISourceViewer sourceViewer) {
+ // TODO content formatter for android xml
+ return super.getContentFormatter(sourceViewer);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/FirstElementParser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/FirstElementParser.java
new file mode 100644
index 0000000..bb0996b
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/FirstElementParser.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+/**
+ * Quickly parses a (potential) XML file to extract its first element (i.e. the root element)
+ * and namespace, if any.
+ * <p/>
+ * This is used to determine if a file is an XML document that the XmlEditor can process.
+ * <p/>
+ * TODO use this to remove the hardcoded "android" namespace prefix limitation.
+ */
+public final class FirstElementParser {
+
+ private static SAXParserFactory sSaxfactory;
+
+ /**
+ * Result from the XML parsing. <br/>
+ * Contains the name of the root XML element. <br/>
+ * If an XMLNS URI was specified and found, the XMLNS prefix is recorded. Otherwise it is null.
+ */
+ public static final class Result {
+ private String mElement;
+ private String mXmlnsPrefix;
+ private String mXmlnsUri;
+
+ public String getElement() {
+ return mElement;
+ }
+
+ public String getXmlnsPrefix() {
+ return mXmlnsPrefix;
+ }
+
+ public String getXmlnsUri() {
+ return mXmlnsUri;
+ }
+
+ void setElement(String element) {
+ mElement = element;
+ }
+
+ void setXmlnsPrefix(String xmlnsPrefix) {
+ mXmlnsPrefix = xmlnsPrefix;
+ }
+
+ void setXmlnsUri(String xmlnsUri) {
+ mXmlnsUri = xmlnsUri;
+ }
+ }
+
+ private static class ResultFoundException extends SAXException { }
+
+ /**
+ * Parses the given filename.
+ *
+ * @param osFilename The file to parse.
+ * @param xmlnsUri An optional URL of which we want to know the prefix.
+ * @return The element details found or null if not found.
+ */
+ public static Result parse(String osFilename, String xmlnsUri) {
+ if (sSaxfactory == null) {
+ // TODO just create a single factory in CommonPlugin and reuse it
+ sSaxfactory = SAXParserFactory.newInstance();
+ sSaxfactory.setNamespaceAware(true);
+ }
+
+ Result result = new Result();
+ if (xmlnsUri != null && xmlnsUri.length() > 0) {
+ result.setXmlnsUri(xmlnsUri);
+ }
+
+ try {
+ SAXParser parser = sSaxfactory.newSAXParser();
+ XmlHandler handler = new XmlHandler(result);
+ parser.parse(new InputSource(new FileReader(osFilename)), handler);
+
+ } catch(ResultFoundException e) {
+ // XML handling was aborted because the required element was found.
+ // Simply return the result.
+ return result;
+ } catch (ParserConfigurationException e) {
+ } catch (SAXException e) {
+ } catch (FileNotFoundException e) {
+ } catch (IOException e) {
+ }
+
+ return null;
+ }
+
+ /**
+ * Private constructor. Use the static parse() method instead.
+ */
+ private FirstElementParser() {
+ // pass
+ }
+
+ /**
+ * A specialized SAX handler that captures the arguments of the very first element
+ * (i.e. the root element)
+ */
+ private static class XmlHandler extends DefaultHandler {
+ private final Result mResult;
+
+ public XmlHandler(Result result) {
+ mResult = result;
+ }
+
+ /**
+ * Processes a namespace prefix mapping.
+ * I.e. for xmlns:android="some-uri", this received prefix="android" and uri="some-uri".
+ * <p/>
+ * The prefix is recorded in the result structure if the URI is the one searched for.
+ * <p/>
+ * This event happens <em>before</em> the corresponding startElement event.
+ */
+ @Override
+ public void startPrefixMapping(String prefix, String uri) {
+ if (uri.equals(mResult.getXmlnsUri())) {
+ mResult.setXmlnsPrefix(prefix);
+ }
+ }
+
+ /**
+ * Processes a new element start.
+ * <p/>
+ * This simply records the element name and abort processing by throwing an exception.
+ */
+ @Override
+ public void startElement(String uri, String localName, String name, Attributes attributes)
+ throws SAXException {
+ mResult.setElement(localName);
+ throw new ResultFoundException();
+ }
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/IconFactory.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/IconFactory.java
new file mode 100644
index 0000000..2c24772
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/IconFactory.java
@@ -0,0 +1,255 @@
+/*
+ * 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.ide.eclipse.editors;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.sdklib.SdkConstants;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.SWT;
+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.Point;
+import org.eclipse.swt.widgets.Display;
+
+import java.util.HashMap;
+
+/**
+ * Factory to generate icons for Android Editors.
+ * <p/>
+ * Icons are kept here and reused.
+ */
+public class IconFactory {
+
+ public static final int COLOR_RED = SWT.COLOR_DARK_RED;
+ public static final int COLOR_GREEN = SWT.COLOR_DARK_GREEN;
+ public static final int COLOR_BLUE = SWT.COLOR_DARK_BLUE;
+ public static final int COLOR_DEFAULT = SWT.COLOR_BLACK;
+
+ public static final int SHAPE_CIRCLE = 'C';
+ public static final int SHAPE_RECT = 'R';
+ public static final int SHAPE_DEFAULT = SHAPE_CIRCLE;
+
+ private static IconFactory sInstance;
+
+ private HashMap<String, Image> mIconMap = new HashMap<String, Image>();
+ private HashMap<String, ImageDescriptor> mImageDescMap = new HashMap<String, ImageDescriptor>();
+
+ private IconFactory() {
+ }
+
+ public static synchronized IconFactory getInstance() {
+ if (sInstance == null) {
+ sInstance = new IconFactory();
+ }
+ return sInstance;
+ }
+
+ public void Dispose() {
+ // Dispose icons
+ for (Image icon : mIconMap.values()) {
+ // The map can contain null values
+ if (icon != null) {
+ icon.dispose();
+ }
+ }
+ mIconMap.clear();
+ }
+
+ /**
+ * Returns an Image for a given icon name.
+ * <p/>
+ * 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 exists, a default icon will be
+ * generated automatically based on the name.
+ */
+ public Image getIcon(String osName) {
+ return getIcon(osName, COLOR_DEFAULT, SHAPE_DEFAULT);
+ }
+
+ /**
+ * Returns an Image for a given icon name.
+ * <p/>
+ * 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 exists, a default icon will be
+ * generated automatically based on the name.
+ * @param color The color of the text in the automatically generated icons,
+ * one of COLOR_DEFAULT, COLOR_RED, COLOR_BLUE or COLOR_RED.
+ * @param shape The shape of the icon in the automatically generated icons,
+ * one of SHAPE_DEFAULT, SHAPE_CIRCLE or SHAPE_RECT.
+ */
+ public Image getIcon(String osName, int color, int shape) {
+ String key = Character.toString((char) shape) + Integer.toString(color) + osName;
+ Image icon = mIconMap.get(key);
+ if (icon == null && !mIconMap.containsKey(key)) {
+ ImageDescriptor id = getImageDescriptor(osName, color, shape);
+ if (id != null) {
+ icon = id.createImage();
+ }
+ // Note that we store null references in the icon map, to avoid looking them
+ // up every time. If it didn't exist once, it will not exist later.
+ mIconMap.put(key, icon);
+ }
+ return icon;
+ }
+
+ /**
+ * Returns an ImageDescriptor for a given icon name.
+ * <p/>
+ * 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 exists, a default icon will be
+ * generated automatically based on the name.
+ */
+ public ImageDescriptor getImageDescriptor(String osName) {
+ return getImageDescriptor(osName, COLOR_DEFAULT, SHAPE_DEFAULT);
+ }
+
+ /**
+ * Returns an ImageDescriptor for a given icon name.
+ * <p/>
+ * 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 exists, a default icon will be
+ * generated automatically based on the name.
+ * @param color The color of the text in the automatically generated icons.
+ * one of COLOR_DEFAULT, COLOR_RED, COLOR_BLUE or COLOR_RED.
+ * @param shape The shape of the icon in the automatically generated icons,
+ * one of SHAPE_DEFAULT, SHAPE_CIRCLE or SHAPE_RECT.
+ */
+ public ImageDescriptor getImageDescriptor(String osName, int color, int shape) {
+ String key = Character.toString((char) shape) + Integer.toString(color) + osName;
+ ImageDescriptor id = mImageDescMap.get(key);
+ if (id == null && !mImageDescMap.containsKey(key)) {
+ id = AdtPlugin.imageDescriptorFromPlugin(
+ AdtPlugin.PLUGIN_ID,
+ String.format("/icons/%1$s.png", osName)); //$NON-NLS-1$
+
+ if (id == null) {
+ id = new LetterImageDescriptor(osName.charAt(0), color, shape);
+ }
+
+ // Note that we store null references in the icon map, to avoid looking them
+ // up every time. If it didn't exist once, it will not exist later.
+ mImageDescMap.put(key, id);
+ }
+ return id;
+ }
+
+ /**
+ * A simple image description that generates a 16x16 image which consists
+ * of a colored letter inside a black & white circle.
+ */
+ private static class LetterImageDescriptor extends ImageDescriptor {
+
+ private final char mLetter;
+ private final int mColor;
+ private final int mShape;
+
+ public LetterImageDescriptor(char letter, int color, int shape) {
+ mLetter = letter;
+ mColor = color;
+ mShape = shape;
+ }
+
+ @Override
+ public ImageData getImageData() {
+
+ final int SX = 15;
+ final int SY = 15;
+ final int RX = 4;
+ final int RY = 4;
+
+ Display display = Display.getCurrent();
+ if (display == null) {
+ return null;
+ }
+
+ Image image = new Image(display, SX, SY);
+
+ image.setBackground(display.getSystemColor(SWT.COLOR_WHITE));
+
+ GC gc = new GC(image);
+ gc.setAdvanced(true);
+ gc.setAntialias(SWT.ON);
+ gc.setTextAntialias(SWT.ON);
+
+ gc.setBackground(display.getSystemColor(SWT.COLOR_WHITE));
+ if (mShape == SHAPE_CIRCLE) {
+ gc.fillOval(0, 0, SX - 1, SY - 1);
+ } else if (mShape == SHAPE_RECT) {
+ gc.fillRoundRectangle(0, 0, SX - 1, SY - 1, RX, RY);
+ }
+
+ gc.setForeground(display.getSystemColor(SWT.COLOR_BLACK));
+ gc.setLineWidth(1);
+ if (mShape == SHAPE_CIRCLE) {
+ gc.drawOval(0, 0, SX - 1, SY - 1);
+ } else if (mShape == SHAPE_RECT) {
+ gc.drawRoundRectangle(0, 0, SX - 1, SY - 1, RX, RY);
+ }
+
+ // Get a bold version of the default system font, if possible.
+ Font font = display.getSystemFont();
+ FontData[] fds = font.getFontData();
+ fds[0].setStyle(SWT.BOLD);
+ // use 3/4th of the circle diameter for the font size (in pixels)
+ // and convert it to "font points" (font points in SWT are hardcoded in an
+ // arbitrary 72 dpi and then converted in real pixels using whatever is
+ // indicated by getDPI -- at least that's how it works under Win32).
+ fds[0].setHeight((int) ((SY + 1) * 3./4. * 72./display.getDPI().y));
+ // Note: win32 implementation always uses fds[0] so we change just that one.
+ // getFontData indicates that the array of fd is really an unusual thing for X11.
+ font = new Font(display, fds);
+ gc.setFont(font);
+ gc.setForeground(display.getSystemColor(mColor));
+
+ // Text measurement varies so slightly depending on the platform
+ int ofx = 0;
+ int ofy = 0;
+ if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) {
+ ofx = +1;
+ ofy = -1;
+ }
+
+ String s = Character.toString(mLetter).toUpperCase();
+ Point p = gc.textExtent(s);
+ int tx = (SX + ofx - p.x) / 2;
+ int ty = (SY + ofy - p.y) / 2;
+ gc.drawText(s, tx, ty, true /* isTransparent */);
+
+ font.dispose();
+ gc.dispose();
+
+ ImageData data = image.getImageData();
+ image.dispose();
+ return data;
+ }
+
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/AttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/AttributeDescriptor.java
new file mode 100644
index 0000000..e0ec86b
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/AttributeDescriptor.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.descriptors;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.editors.IconFactory;
+import com.android.ide.eclipse.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+import com.android.sdklib.SdkConstants;
+
+import org.eclipse.swt.graphics.Image;
+
+/**
+ * {@link AttributeDescriptor} describes an XML attribute with its XML attribute name.
+ * <p/>
+ * An attribute descriptor also knows which UI node should be instantiated to represent
+ * this particular attribute (e.g. text field, icon chooser, class selector, etc.)
+ * Some attributes may be hidden and have no user interface at all.
+ * <p/>
+ * This is an abstract class. Derived classes must implement data description and return
+ * the correct UiAttributeNode-derived class.
+ */
+public abstract class AttributeDescriptor {
+ private String mXmlLocalName;
+ private ElementDescriptor mParent;
+ private final String mNsUri;
+ private boolean mDeprecated;
+
+ /**
+ * Creates a new {@link AttributeDescriptor}
+ *
+ * @param xmlLocalName The XML name of the attribute (case sensitive)
+ * @param nsUri The URI of the attribute. Can be null if attribute has no namespace.
+ * See {@link SdkConstants#NS_RESOURCES} for a common value.
+ */
+ public AttributeDescriptor(String xmlLocalName, String nsUri) {
+ mXmlLocalName = xmlLocalName;
+ mNsUri = nsUri;
+ }
+
+ /**
+ * Returns the XML local name of the attribute (case sensitive)
+ */
+ public final String getXmlLocalName() {
+ return mXmlLocalName;
+ }
+
+ public final String getNamespaceUri() {
+ return mNsUri;
+ }
+
+ final void setParent(ElementDescriptor parent) {
+ mParent = parent;
+ }
+
+ public final ElementDescriptor getParent() {
+ return mParent;
+ }
+
+ public void setDeprecated(boolean isDeprecated) {
+ mDeprecated = isDeprecated;
+ }
+
+ public boolean isDeprecated() {
+ return mDeprecated;
+ }
+
+ /**
+ * Returns an optional icon for the attribute.
+ * <p/>
+ * By default this tries to return an icon based on the XML name of the attribute.
+ * If this fails, it tries to return the default Android logo as defined in the
+ * plugin. If all fails, it returns null.
+ *
+ * @return An icon for this element or null.
+ */
+ public Image getIcon() {
+ IconFactory factory = IconFactory.getInstance();
+ Image icon;
+ icon = factory.getIcon(getXmlLocalName(), IconFactory.COLOR_RED, IconFactory.SHAPE_CIRCLE);
+ return icon != null ? icon : AdtPlugin.getAndroidLogo();
+ }
+
+ /**
+ * @param uiParent The {@link UiElementNode} parent of this UI attribute.
+ * @return A new {@link UiAttributeNode} linked to this descriptor or null if this
+ * attribute has no user interface.
+ */
+ public abstract UiAttributeNode createUiNode(UiElementNode uiParent);
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/AttributeDescriptorLabelProvider.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/AttributeDescriptorLabelProvider.java
new file mode 100644
index 0000000..2729565
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/AttributeDescriptorLabelProvider.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.descriptors;
+
+import com.android.ide.eclipse.editors.IconFactory;
+import com.android.ide.eclipse.editors.uimodel.UiAbstractTextAttributeNode;
+
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.swt.graphics.Image;
+
+/**
+ * Label provider for {@link UiAbstractTextAttributeNode}.
+ */
+public class AttributeDescriptorLabelProvider implements ILabelProvider {
+
+ private final static AttributeDescriptorLabelProvider sThis =
+ new AttributeDescriptorLabelProvider();
+
+ public static ILabelProvider getProvider() {
+ return sThis;
+ }
+
+ public Image getImage(Object element) {
+ if (element instanceof UiAbstractTextAttributeNode) {
+ UiAbstractTextAttributeNode node = (UiAbstractTextAttributeNode) element;
+ if (node.getDescriptor().isDeprecated()) {
+ String v = node.getCurrentValue();
+ if (v != null && v.length() > 0) {
+ IconFactory factory = IconFactory.getInstance();
+ return factory.getIcon("warning"); //$NON-NLS-1$
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public String getText(Object element) {
+ if (element instanceof UiAbstractTextAttributeNode) {
+ return ((UiAbstractTextAttributeNode)element).getCurrentValue();
+ }
+
+ return null;
+ }
+
+ public void addListener(ILabelProviderListener listener) {
+ // TODO Auto-generated method stub
+
+ }
+
+ public void dispose() {
+ // TODO Auto-generated method stub
+
+ }
+
+ public boolean isLabelProperty(Object element, String property) {
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+ public void removeListener(ILabelProviderListener listener) {
+ // TODO Auto-generated method stub
+
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/BooleanAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/BooleanAttributeDescriptor.java
new file mode 100644
index 0000000..9ebf5f1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/BooleanAttributeDescriptor.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.descriptors;
+
+import com.android.ide.eclipse.editors.uimodel.UiListAttributeNode;
+
+/**
+ * Describes a text attribute that can only contain boolean values.
+ * It is displayed by a {@link UiListAttributeNode}.
+ */
+public class BooleanAttributeDescriptor extends ListAttributeDescriptor {
+
+ public BooleanAttributeDescriptor(String xmlLocalName, String uiName, String nsUri,
+ String tooltip) {
+ super(xmlLocalName, uiName, nsUri, tooltip,
+ new String[] { "true", "false" } );
+ }
+}
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/DescriptorsUtils.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/DescriptorsUtils.java
new file mode 100644
index 0000000..f1d62a1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/DescriptorsUtils.java
@@ -0,0 +1,845 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.descriptors;
+
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.common.resources.ResourceType;
+import com.android.ide.eclipse.common.resources.DeclareStyleableInfo.AttributeInfo;
+import com.android.ide.eclipse.common.resources.DeclareStyleableInfo.AttributeInfo.Format;
+import com.android.ide.eclipse.editors.layout.LayoutConstants;
+import com.android.ide.eclipse.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+import com.android.sdklib.SdkConstants;
+
+import org.eclipse.swt.graphics.Image;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.Map.Entry;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+/**
+ * Utility methods related to descriptors handling.
+ */
+public final class DescriptorsUtils {
+
+ private static final String DEFAULT_WIDGET_PREFIX = "widget";
+
+ private static final int JAVADOC_BREAK_LENGTH = 60;
+
+ /**
+ * The path in the online documentation for the manifest description.
+ * <p/>
+ * This is NOT a complete URL. To be used, it needs to be appended
+ * to {@link AndroidConstants#CODESITE_BASE_URL} or to the local SDK
+ * documentation.
+ */
+ public static final String MANIFEST_SDK_URL = "/reference/android/R.styleable.html#"; //$NON-NLS-1$
+
+ public static final String IMAGE_KEY = "image"; //$NON-NLS-1$
+
+ private static final String CODE = "$code"; //$NON-NLS-1$
+ private static final String LINK = "$link"; //$NON-NLS-1$
+ private static final String ELEM = "$elem"; //$NON-NLS-1$
+ private static final String BREAK = "$break"; //$NON-NLS-1$
+
+ /**
+ * The {@link ITextAttributeCreator} interface is used by the appendAttribute() method
+ * to provide a way for caller to override the kind of {@link TextAttributeDescriptor}
+ * created for a give XML attribute name.
+ */
+ public interface ITextAttributeCreator {
+ /**
+ * Creates a new {@link TextAttributeDescriptor} instance for the given XML name,
+ * UI name and tooltip.
+ *
+ * @param xmlName The XML attribute name.
+ * @param uiName The UI attribute name.
+ * @param nsUri The URI of the attribute. Can be null if attribute has no namespace.
+ * See {@link SdkConstants#NS_RESOURCES} for a common value.
+ * @param tooltip An optional tooltip.
+ * @return A new {@link TextAttributeDescriptor} (or derived) instance.
+ */
+ public TextAttributeDescriptor create(String xmlName, String uiName, String nsUri,
+ String tooltip);
+ }
+
+ /**
+ * Add all {@link AttributeInfo} to the the array of {@link AttributeDescriptor}.
+ *
+ * @param attributes The list of {@link AttributeDescriptor} to append to
+ * @param elementXmlName Optional XML local name of the element to which attributes are
+ * being added. When not null, this is used to filter overrides.
+ * @param nsUri The URI of the attribute. Can be null if attribute has no namespace.
+ * See {@link SdkConstants#NS_RESOURCES} for a common value.
+ * @param infos The array of {@link AttributeInfo} to read and append to attributes
+ * @param requiredAttributes An optional set of attributes to mark as "required" (i.e. append
+ * a "*" to their UI name as a hint for the user.) If not null, must contains
+ * entries in the form "elem-name/attr-name". Elem-name can be "*".
+ * @param overrides A map [attribute name => TextAttributeDescriptor creator]. A creator
+ * can either by a Class<? extends TextAttributeDescriptor> or an instance of
+ * {@link ITextAttributeCreator} that instantiates the right TextAttributeDescriptor.
+ */
+ public static void appendAttributes(ArrayList<AttributeDescriptor> attributes,
+ String elementXmlName,
+ String nsUri, AttributeInfo[] infos,
+ Set<String> requiredAttributes,
+ Map<String, Object> overrides) {
+ for (AttributeInfo info : infos) {
+ boolean required = false;
+ if (requiredAttributes != null) {
+ String attr_name = info.getName();
+ if (requiredAttributes.contains("*/" + attr_name) ||
+ requiredAttributes.contains(elementXmlName + "/" + attr_name)) {
+ required = true;
+ }
+ }
+ appendAttribute(attributes, elementXmlName, nsUri, info, required, overrides);
+ }
+ }
+
+ /**
+ * Add an {@link AttributeInfo} to the the array of {@link AttributeDescriptor}.
+ *
+ * @param attributes The list of {@link AttributeDescriptor} to append to
+ * @param elementXmlName Optional XML local name of the element to which attributes are
+ * being added. When not null, this is used to filter overrides.
+ * @param info The {@link AttributeInfo} to append to attributes
+ * @param nsUri The URI of the attribute. Can be null if attribute has no namespace.
+ * See {@link SdkConstants#NS_RESOURCES} for a common value.
+ * @param required True if the attribute is to be marked as "required" (i.e. append
+ * a "*" to its UI name as a hint for the user.)
+ * @param overrides A map [attribute name => TextAttributeDescriptor creator]. A creator
+ * can either by a Class<? extends TextAttributeDescriptor> or an instance of
+ * {@link ITextAttributeCreator} that instantiates the right TextAttributeDescriptor.
+ */
+ public static void appendAttribute(ArrayList<AttributeDescriptor> attributes,
+ String elementXmlName,
+ String nsUri,
+ AttributeInfo info, boolean required,
+ Map<String, Object> overrides) {
+ AttributeDescriptor attr = null;
+
+ String xmlLocalName = info.getName();
+ String uiName = prettyAttributeUiName(info.getName()); // ui_name
+ if (required) {
+ uiName += "*"; //$NON-NLS-1$
+ }
+
+ String tooltip = null;
+ String rawTooltip = info.getJavaDoc();
+ if (rawTooltip == null) {
+ rawTooltip = "";
+ }
+
+ String deprecated = info.getDeprecatedDoc();
+ if (deprecated != null) {
+ if (rawTooltip.length() > 0) {
+ rawTooltip += "@@"; //$NON-NLS-1$ insert a break
+ }
+ rawTooltip += "* Deprecated";
+ if (deprecated.length() != 0) {
+ rawTooltip += ": " + deprecated; //$NON-NLS-1$
+ }
+ if (deprecated.length() == 0 || !deprecated.endsWith(".")) { //$NON-NLS-1$
+ rawTooltip += "."; //$NON-NLS-1$
+ }
+ }
+
+ // Add the known types to the tooltip
+ Format[] formats_list = info.getFormats();
+ int flen = formats_list.length;
+ if (flen > 0) {
+ // Fill the formats in a set for faster access
+ HashSet<Format> formats_set = new HashSet<Format>();
+
+ StringBuilder sb = new StringBuilder();
+ if (rawTooltip != null && rawTooltip.length() > 0) {
+ sb.append(rawTooltip);
+ sb.append(" "); //$NON-NLS-1$
+ }
+ if (sb.length() > 0) {
+ sb.append("@@"); //$NON-NLS-1$ @@ inserts a break before the types
+ }
+ sb.append("["); //$NON-NLS-1$
+ for (int i = 0; i < flen; i++) {
+ Format f = formats_list[i];
+ formats_set.add(f);
+
+ sb.append(f.toString().toLowerCase());
+ if (i < flen - 1) {
+ sb.append(", "); //$NON-NLS-1$
+ }
+ }
+ // The extra space at the end makes the tooltip more readable on Windows.
+ sb.append("]"); //$NON-NLS-1$
+
+ if (required) {
+ sb.append(".@@* "); //$NON-NLS-1$ @@ inserts a break.
+ sb.append("Required.");
+ }
+
+ // The extra space at the end makes the tooltip more readable on Windows.
+ sb.append(" "); //$NON-NLS-1$
+
+ rawTooltip = sb.toString();
+ tooltip = formatTooltip(rawTooltip);
+
+ // Create a specialized attribute if we can
+ if (overrides != null) {
+ for (Entry<String, Object> entry: overrides.entrySet()) {
+ String key = entry.getKey();
+ String elements[] = key.split("/"); //$NON-NLS-1$
+ String overrideAttrLocalName = null;
+ if (elements.length < 1) {
+ continue;
+ } else if (elements.length == 1) {
+ overrideAttrLocalName = elements[0];
+ elements = null;
+ } else {
+ overrideAttrLocalName = elements[elements.length - 1];
+ elements = elements[0].split(","); //$NON-NLS-1$
+ }
+
+ if (overrideAttrLocalName == null ||
+ !overrideAttrLocalName.equals(xmlLocalName)) {
+ continue;
+ }
+
+ boolean ok_element = elements.length < 1;
+ if (!ok_element) {
+ for (String element : elements) {
+ if (element.equals("*") //$NON-NLS-1$
+ || element.equals(elementXmlName)) {
+ ok_element = true;
+ break;
+ }
+ }
+ }
+
+ if (!ok_element) {
+ continue;
+ }
+
+ Object override = entry.getValue();
+ if (override instanceof Class) {
+ try {
+ // The override is instance of the class to create, which must
+ // have a constructor compatible with TextAttributeDescriptor.
+ @SuppressWarnings("unchecked") //$NON-NLS-1$
+ Class<? extends TextAttributeDescriptor> clazz =
+ (Class<? extends TextAttributeDescriptor>) override;
+ Constructor<? extends TextAttributeDescriptor> cons;
+ cons = clazz.getConstructor(new Class<?>[] {
+ String.class, String.class, String.class, String.class } );
+ attr = cons.newInstance(
+ new Object[] { xmlLocalName, uiName, nsUri, tooltip });
+ } catch (SecurityException e) {
+ // ignore
+ } catch (NoSuchMethodException e) {
+ // ignore
+ } catch (IllegalArgumentException e) {
+ // ignore
+ } catch (InstantiationException e) {
+ // ignore
+ } catch (IllegalAccessException e) {
+ // ignore
+ } catch (InvocationTargetException e) {
+ // ignore
+ }
+ } else if (override instanceof ITextAttributeCreator) {
+ attr = ((ITextAttributeCreator) override).create(
+ xmlLocalName, uiName, nsUri, tooltip);
+ }
+ }
+ } // if overrides
+
+ // Create a specialized descriptor if we can, based on type
+ if (attr == null) {
+ if (formats_set.contains(Format.REFERENCE)) {
+ // This is either a multi-type reference or a generic reference.
+ attr = new ReferenceAttributeDescriptor(xmlLocalName, uiName, nsUri, tooltip);
+ } else if (formats_set.contains(Format.ENUM)) {
+ attr = new ListAttributeDescriptor(xmlLocalName, uiName, nsUri, tooltip,
+ info.getEnumValues());
+ } else if (formats_set.contains(Format.FLAG)) {
+ attr = new FlagAttributeDescriptor(xmlLocalName, uiName, nsUri, tooltip,
+ info.getFlagValues());
+ } else if (formats_set.contains(Format.BOOLEAN)) {
+ attr = new BooleanAttributeDescriptor(xmlLocalName, uiName, nsUri, tooltip);
+ } else if (formats_set.contains(Format.STRING)) {
+ attr = new ReferenceAttributeDescriptor(ResourceType.STRING,
+ xmlLocalName, uiName, nsUri,
+ tooltip);
+ }
+ }
+ }
+
+ // By default a simple text field is used
+ if (attr == null) {
+ if (tooltip == null) {
+ tooltip = formatTooltip(rawTooltip);
+ }
+ attr = new TextAttributeDescriptor(xmlLocalName, uiName, nsUri, tooltip);
+ }
+ attr.setDeprecated(info.getDeprecatedDoc() != null);
+ attributes.add(attr);
+ }
+
+ /**
+ * Indicates the the given {@link AttributeInfo} already exists in the ArrayList of
+ * {@link AttributeDescriptor}. This test for the presence of a descriptor with the same
+ * XML name.
+ *
+ * @param attributes The list of {@link AttributeDescriptor} to compare to.
+ * @param nsUri The URI of the attribute. Can be null if attribute has no namespace.
+ * See {@link SdkConstants#NS_RESOURCES} for a common value.
+ * @param info The {@link AttributeInfo} to know whether it is included in the above list.
+ * @return True if this {@link AttributeInfo} is already present in
+ * the {@link AttributeDescriptor} list.
+ */
+ public static boolean containsAttribute(ArrayList<AttributeDescriptor> attributes,
+ String nsUri,
+ AttributeInfo info) {
+ String xmlLocalName = info.getName();
+ for (AttributeDescriptor desc : attributes) {
+ if (desc.getXmlLocalName().equals(xmlLocalName)) {
+ if (nsUri == desc.getNamespaceUri() ||
+ (nsUri != null && nsUri.equals(desc.getNamespaceUri()))) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Create a pretty attribute UI name from an XML name.
+ * <p/>
+ * The original xml name starts with a lower case and is camel-case,
+ * e.g. "maxWidthForView". The pretty name starts with an upper case
+ * and has space separators, e.g. "Max width for view".
+ */
+ public static String prettyAttributeUiName(String name) {
+ if (name.length() < 1) {
+ return name;
+ }
+ StringBuffer buf = new StringBuffer();
+
+ char c = name.charAt(0);
+ // Use upper case initial letter
+ buf.append((char)(c >= 'a' && c <= 'z' ? c + 'A' - 'a' : c));
+ int len = name.length();
+ for (int i = 1; i < len; i++) {
+ c = name.charAt(i);
+ if (c >= 'A' && c <= 'Z') {
+ // Break camel case into separate words
+ buf.append(' ');
+ // Use a lower case initial letter for the next word, except if the
+ // word is solely X, Y or Z.
+ if (c >= 'X' && c <= 'Z' &&
+ (i == len-1 ||
+ (i < len-1 && name.charAt(i+1) >= 'A' && name.charAt(i+1) <= 'Z'))) {
+ buf.append(c);
+ } else {
+ buf.append((char)(c - 'A' + 'a'));
+ }
+ } else if (c == '_') {
+ buf.append(' ');
+ } else {
+ buf.append(c);
+ }
+ }
+
+ name = buf.toString();
+
+ // Replace these acronyms by upper-case versions
+ // - (?<=^| ) means "if preceded by a space or beginning of string"
+ // - (?=$| ) means "if followed by a space or end of string"
+ name = name.replaceAll("(?<=^| )sdk(?=$| )", "SDK");
+ name = name.replaceAll("(?<=^| )uri(?=$| )", "URI");
+
+ return name;
+ }
+
+ /**
+ * Capitalizes the string, i.e. transforms the initial [a-z] into [A-Z].
+ * Returns the string unmodified if the first character is not [a-z].
+ *
+ * @param str The string to capitalize.
+ * @return The capitalized string
+ */
+ public static String capitalize(String str) {
+ if (str == null || str.length() < 1 || str.charAt(0) < 'a' || str.charAt(0) > 'z') {
+ return str;
+ }
+
+ StringBuilder sb = new StringBuilder();
+ sb.append((char)(str.charAt(0) + 'A' - 'a'));
+ sb.append(str.substring(1));
+ return sb.toString();
+ }
+
+ /**
+ * Formats the javadoc tooltip to be usable in a tooltip.
+ */
+ public static String formatTooltip(String javadoc) {
+ ArrayList<String> spans = scanJavadoc(javadoc);
+
+ StringBuilder sb = new StringBuilder();
+ boolean needBreak = false;
+
+ for (int n = spans.size(), i = 0; i < n; ++i) {
+ String s = spans.get(i);
+ if (CODE.equals(s)) {
+ s = spans.get(++i);
+ if (s != null) {
+ sb.append('"').append(s).append('"');
+ }
+ } else if (LINK.equals(s)) {
+ String base = spans.get(++i);
+ String anchor = spans.get(++i);
+ String text = spans.get(++i);
+
+ if (base != null) {
+ base = base.trim();
+ }
+ if (anchor != null) {
+ anchor = anchor.trim();
+ }
+ if (text != null) {
+ text = text.trim();
+ }
+
+ // If there's no text, use the anchor if there's one
+ if (text == null || text.length() == 0) {
+ text = anchor;
+ }
+
+ if (base != null && base.length() > 0) {
+ if (text == null || text.length() == 0) {
+ // If we still have no text, use the base as text
+ text = base;
+ }
+ }
+
+ if (text != null) {
+ sb.append(text);
+ }
+
+ } else if (ELEM.equals(s)) {
+ s = spans.get(++i);
+ if (s != null) {
+ sb.append(s);
+ }
+ } else if (BREAK.equals(s)) {
+ needBreak = true;
+ } else if (s != null) {
+ if (needBreak && s.trim().length() > 0) {
+ sb.append('\r');
+ }
+ sb.append(s);
+ needBreak = false;
+ }
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Formats the javadoc tooltip to be usable in a FormText.
+ * <p/>
+ * If the descriptor can provide an icon, the caller should provide
+ * elementsDescriptor.getIcon() as "image" to FormText, e.g.:
+ * <code>formText.setImage(IMAGE_KEY, elementsDescriptor.getIcon());</code>
+ *
+ * @param javadoc The javadoc to format. Cannot be null.
+ * @param elementDescriptor The element descriptor parent of the javadoc. Cannot be null.
+ * @param androidDocBaseUrl The base URL for the documentation. Cannot be null. Should be
+ * <code>FrameworkResourceManager.getInstance().getDocumentationBaseUrl()</code>
+ */
+ public static String formatFormText(String javadoc,
+ ElementDescriptor elementDescriptor,
+ String androidDocBaseUrl) {
+ ArrayList<String> spans = scanJavadoc(javadoc);
+
+ String fullSdkUrl = androidDocBaseUrl + MANIFEST_SDK_URL;
+ String sdkUrl = elementDescriptor.getSdkUrl();
+ if (sdkUrl != null && sdkUrl.startsWith(MANIFEST_SDK_URL)) {
+ fullSdkUrl = androidDocBaseUrl + sdkUrl;
+ }
+
+ StringBuilder sb = new StringBuilder();
+
+ Image icon = elementDescriptor.getIcon();
+ if (icon != null) {
+ sb.append("<form><li style=\"image\" value=\"" + //$NON-NLS-1$
+ IMAGE_KEY + "\">"); //$NON-NLS-1$
+ } else {
+ sb.append("<form><p>"); //$NON-NLS-1$
+ }
+
+ for (int n = spans.size(), i = 0; i < n; ++i) {
+ String s = spans.get(i);
+ if (CODE.equals(s)) {
+ s = spans.get(++i);
+ if (elementDescriptor.getXmlName().equals(s) && fullSdkUrl != null) {
+ sb.append("<a href=\""); //$NON-NLS-1$
+ sb.append(fullSdkUrl);
+ sb.append("\">"); //$NON-NLS-1$
+ sb.append(s);
+ sb.append("</a>"); //$NON-NLS-1$
+ } else if (s != null) {
+ sb.append('"').append(s).append('"');
+ }
+ } else if (LINK.equals(s)) {
+ String base = spans.get(++i);
+ String anchor = spans.get(++i);
+ String text = spans.get(++i);
+
+ if (base != null) {
+ base = base.trim();
+ }
+ if (anchor != null) {
+ anchor = anchor.trim();
+ }
+ if (text != null) {
+ text = text.trim();
+ }
+
+ // If there's no text, use the anchor if there's one
+ if (text == null || text.length() == 0) {
+ text = anchor;
+ }
+
+ // TODO specialize with a base URL for views, menus & other resources
+ // Base is empty for a local page anchor, in which case we'll replace it
+ // by the element SDK URL if it exists.
+ if ((base == null || base.length() == 0) && fullSdkUrl != null) {
+ base = fullSdkUrl;
+ }
+
+ String url = null;
+ if (base != null && base.length() > 0) {
+ if (base.startsWith("http")) { //$NON-NLS-1$
+ // If base looks an URL, use it, with the optional anchor
+ url = base;
+ if (anchor != null && anchor.length() > 0) {
+ // If the base URL already has an anchor, it needs to be
+ // removed first. If there's no anchor, we need to add "#"
+ int pos = url.lastIndexOf('#');
+ if (pos < 0) {
+ url += "#"; //$NON-NLS-1$
+ } else if (pos < url.length() - 1) {
+ url = url.substring(0, pos + 1);
+ }
+
+ url += anchor;
+ }
+ } else if (text == null || text.length() == 0) {
+ // If we still have no text, use the base as text
+ text = base;
+ }
+ }
+
+ if (url != null && text != null) {
+ sb.append("<a href=\""); //$NON-NLS-1$
+ sb.append(url);
+ sb.append("\">"); //$NON-NLS-1$
+ sb.append(text);
+ sb.append("</a>"); //$NON-NLS-1$
+ } else if (text != null) {
+ sb.append("<b>").append(text).append("</b>"); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+
+ } else if (ELEM.equals(s)) {
+ s = spans.get(++i);
+ if (sdkUrl != null && s != null) {
+ sb.append("<a href=\""); //$NON-NLS-1$
+ sb.append(sdkUrl);
+ sb.append("\">"); //$NON-NLS-1$
+ sb.append(s);
+ sb.append("</a>"); //$NON-NLS-1$
+ } else if (s != null) {
+ sb.append("<b>").append(s).append("</b>"); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+ } else if (BREAK.equals(s)) {
+ // ignore line breaks in pseudo-HTML rendering
+ } else if (s != null) {
+ sb.append(s);
+ }
+ }
+
+ if (icon != null) {
+ sb.append("</li></form>"); //$NON-NLS-1$
+ } else {
+ sb.append("</p></form>"); //$NON-NLS-1$
+ }
+ return sb.toString();
+ }
+
+ private static ArrayList<String> scanJavadoc(String javadoc) {
+ ArrayList<String> spans = new ArrayList<String>();
+
+ // Standardize all whitespace in the javadoc to single spaces.
+ if (javadoc != null) {
+ javadoc = javadoc.replaceAll("[ \t\f\r\n]+", " "); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+
+ // Detects {@link <base>#<name> <text>} where all 3 are optional
+ Pattern p_link = Pattern.compile("\\{@link\\s+([^#\\}\\s]*)(?:#([^\\s\\}]*))?(?:\\s*([^\\}]*))?\\}(.*)"); //$NON-NLS-1$
+ // Detects <code>blah</code>
+ Pattern p_code = Pattern.compile("<code>(.+?)</code>(.*)"); //$NON-NLS-1$
+ // Detects @blah@, used in hard-coded tooltip descriptors
+ Pattern p_elem = Pattern.compile("@([\\w -]+)@(.*)"); //$NON-NLS-1$
+ // Detects a buffer that starts by @@ (request for a break)
+ Pattern p_break = Pattern.compile("@@(.*)"); //$NON-NLS-1$
+ // Detects a buffer that starts by @ < or { (one that was not matched above)
+ Pattern p_open = Pattern.compile("([@<\\{])(.*)"); //$NON-NLS-1$
+ // Detects everything till the next potential separator, i.e. @ < or {
+ Pattern p_text = Pattern.compile("([^@<\\{]+)(.*)"); //$NON-NLS-1$
+
+ int currentLength = 0;
+ String text = null;
+
+ while(javadoc != null && javadoc.length() > 0) {
+ Matcher m;
+ String s = null;
+ if ((m = p_code.matcher(javadoc)).matches()) {
+ spans.add(CODE);
+ spans.add(text = cleanupJavadocHtml(m.group(1))); // <code> text
+ javadoc = m.group(2);
+ if (text != null) {
+ currentLength += text.length();
+ }
+ } else if ((m = p_link.matcher(javadoc)).matches()) {
+ spans.add(LINK);
+ spans.add(m.group(1)); // @link base
+ spans.add(m.group(2)); // @link anchor
+ spans.add(text = cleanupJavadocHtml(m.group(3))); // @link text
+ javadoc = m.group(4);
+ if (text != null) {
+ currentLength += text.length();
+ }
+ } else if ((m = p_elem.matcher(javadoc)).matches()) {
+ spans.add(ELEM);
+ spans.add(text = cleanupJavadocHtml(m.group(1))); // @text@
+ javadoc = m.group(2);
+ if (text != null) {
+ currentLength += text.length() - 2;
+ }
+ } else if ((m = p_break.matcher(javadoc)).matches()) {
+ spans.add(BREAK);
+ currentLength = 0;
+ javadoc = m.group(1);
+ } else if ((m = p_open.matcher(javadoc)).matches()) {
+ s = m.group(1);
+ javadoc = m.group(2);
+ } else if ((m = p_text.matcher(javadoc)).matches()) {
+ s = m.group(1);
+ javadoc = m.group(2);
+ } else {
+ // This is not supposed to happen. In case of, just use everything.
+ s = javadoc;
+ javadoc = null;
+ }
+ if (s != null && s.length() > 0) {
+ s = cleanupJavadocHtml(s);
+
+ if (currentLength >= JAVADOC_BREAK_LENGTH) {
+ spans.add(BREAK);
+ currentLength = 0;
+ }
+ while (currentLength + s.length() > JAVADOC_BREAK_LENGTH) {
+ int pos = s.indexOf(' ', JAVADOC_BREAK_LENGTH - currentLength);
+ if (pos <= 0) {
+ break;
+ }
+ spans.add(s.substring(0, pos + 1));
+ spans.add(BREAK);
+ currentLength = 0;
+ s = s.substring(pos + 1);
+ }
+
+ spans.add(s);
+ currentLength += s.length();
+ }
+ }
+
+ return spans;
+ }
+
+ /**
+ * Remove anything that looks like HTML from a javadoc snippet, as it is supported
+ * neither by FormText nor a standard text tooltip.
+ */
+ private static String cleanupJavadocHtml(String s) {
+ if (s != null) {
+ s = s.replaceAll("&lt;", "\""); //$NON-NLS-1$ $NON-NLS-2$
+ s = s.replaceAll("&gt;", "\""); //$NON-NLS-1$ $NON-NLS-2$
+ s = s.replaceAll("<[^>]+>", ""); //$NON-NLS-1$ $NON-NLS-2$
+ }
+ return s;
+ }
+
+ /**
+ * Sets the default layout attributes for the a new UiElementNode.
+ * <p/>
+ * Note that ideally the node should already be part of a hierarchy so that its
+ * parent layout and previous sibling can be determined, if any.
+ * <p/>
+ * This does not override attributes which are not empty.
+ */
+ public static void setDefaultLayoutAttributes(UiElementNode ui_node, boolean updateLayout) {
+ // if this ui_node is a layout and we're adding it to a document, use fill_parent for
+ // both W/H. Otherwise default to wrap_layout.
+ boolean fill = ui_node.getDescriptor().hasChildren() &&
+ ui_node.getUiParent() instanceof UiDocumentNode;
+ ui_node.setAttributeValue(LayoutConstants.ATTR_LAYOUT_WIDTH,
+ fill ? LayoutConstants.VALUE_FILL_PARENT : LayoutConstants.VALUE_WRAP_CONTENT,
+ false /* override */);
+ ui_node.setAttributeValue(LayoutConstants.ATTR_LAYOUT_HEIGHT,
+ fill ? LayoutConstants.VALUE_FILL_PARENT : LayoutConstants.VALUE_WRAP_CONTENT,
+ false /* override */);
+
+ String widget_id = getFreeWidgetId(ui_node.getUiRoot(),
+ new Object[] { ui_node.getDescriptor().getXmlLocalName(), null, null, null });
+ if (widget_id != null) {
+ ui_node.setAttributeValue(LayoutConstants.ATTR_ID, "@+id/" + widget_id, //$NON-NLS-1$
+ false /* override */);
+ }
+
+ ui_node.setAttributeValue(LayoutConstants.ATTR_TEXT, widget_id, false /*override*/);
+
+ if (updateLayout) {
+ UiElementNode ui_parent = ui_node.getUiParent();
+ if (ui_parent != null &&
+ ui_parent.getDescriptor().getXmlLocalName().equals(
+ LayoutConstants.RELATIVE_LAYOUT)) {
+ UiElementNode ui_previous = ui_node.getUiPreviousSibling();
+ if (ui_previous != null) {
+ String id = ui_previous.getAttributeValue(LayoutConstants.ATTR_ID);
+ if (id != null && id.length() > 0) {
+ id = id.replace("@+", "@"); //$NON-NLS-1$ //$NON-NLS-2$
+ ui_node.setAttributeValue(LayoutConstants.ATTR_LAYOUT_BELOW, id,
+ false /* override */);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Given a UI root node, returns the first available id that matches the
+ * pattern "prefix%02d".
+ *
+ * @param uiNode The UI node that gives the prefix to match.
+ * @return A suitable generated id
+ */
+ public static String getFreeWidgetId(UiElementNode uiNode) {
+ return getFreeWidgetId(uiNode.getUiRoot(),
+ new Object[] { uiNode.getDescriptor().getXmlLocalName(), null, null, null });
+ }
+
+ /**
+ * Given a UI root node, returns the first available id that matches the
+ * pattern "prefix%02d".
+ *
+ * For recursion purposes, a "context" is given. Since Java doesn't have in-out parameters
+ * in methods and we're not going to do a dedicated type, we just use an object array which
+ * must contain one initial item and several are built on the fly just for internal storage:
+ * <ul>
+ * <li> prefix(String): The prefix of the generated id, i.e. "widget". Cannot be null.
+ * <li> index(Integer): The minimum index of the generated id. Must start with null.
+ * <li> generated(String): The generated widget currently being searched. Must start with null.
+ * <li> map(Set<String>): A set of the ids collected so far when walking through the widget
+ * hierarchy. Must start with null.
+ * </ul>
+ *
+ * @param uiRoot The Ui root node where to start searching recursively. For the initial call
+ * you want to pass the document root.
+ * @param params An in-out context of parameters used during recursion, as explained above.
+ * @return A suitable generated id
+ */
+ @SuppressWarnings("unchecked")
+ private static String getFreeWidgetId(UiElementNode uiRoot,
+ Object[] params) {
+
+ Set<String> map = (Set<String>)params[3];
+ if (map == null) {
+ params[3] = map = new HashSet<String>();
+ }
+
+ int num = params[1] == null ? 0 : ((Integer)params[1]).intValue();
+
+ String generated = (String) params[2];
+ String prefix = (String) params[0];
+ if (generated == null) {
+ int pos = prefix.indexOf('.');
+ if (pos >= 0) {
+ prefix = prefix.substring(pos + 1);
+ }
+ pos = prefix.indexOf('$');
+ if (pos >= 0) {
+ prefix = prefix.substring(pos + 1);
+ }
+ prefix = prefix.replaceAll("[^a-zA-Z]", ""); //$NON-NLS-1$ $NON-NLS-2$
+ if (prefix.length() == 0) {
+ prefix = DEFAULT_WIDGET_PREFIX;
+ }
+
+ do {
+ num++;
+ generated = String.format("%1$s%2$02d", prefix, num); //$NON-NLS-1$
+ } while (map.contains(generated));
+
+ params[0] = prefix;
+ params[1] = num;
+ params[2] = generated;
+ }
+
+ String id = uiRoot.getAttributeValue(LayoutConstants.ATTR_ID);
+ if (id != null) {
+ id = id.replace("@+id/", ""); //$NON-NLS-1$ $NON-NLS-2$
+ id = id.replace("@id/", ""); //$NON-NLS-1$ $NON-NLS-2$
+ if (map.add(id) && map.contains(generated)) {
+
+ do {
+ num++;
+ generated = String.format("%1$s%2$02d", prefix, num); //$NON-NLS-1$
+ } while (map.contains(generated));
+
+ params[1] = num;
+ params[2] = generated;
+ }
+ }
+
+ for (UiElementNode uiChild : uiRoot.getUiChildren()) {
+ getFreeWidgetId(uiChild, params);
+ }
+
+ // Note: return params[2] (not "generated") since it could have changed during recursion.
+ return (String) params[2];
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/DocumentDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/DocumentDescriptor.java
new file mode 100644
index 0000000..7d296f7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/DocumentDescriptor.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.descriptors;
+
+import com.android.ide.eclipse.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+/**
+ * {@link DocumentDescriptor} describes the properties expected for an XML document node.
+ *
+ * Compared to ElementDescriptor, {@link DocumentDescriptor} does not have XML name nor UI name,
+ * tooltip, SDK url and attributes list.
+ * <p/>
+ * It has a children list which represent all the possible roots of the document.
+ * <p/>
+ * The document nodes are "mandatory", meaning the UI node is never deleted and it may lack
+ * an actual XML node attached.
+ */
+public class DocumentDescriptor extends ElementDescriptor {
+
+ /**
+ * Constructs a new {@link DocumentDescriptor} based on its XML name and children list.
+ * The UI name is build by capitalizing the XML name.
+ * The UI nodes will be non-mandatory.
+ * <p/>
+ * The XML name is never shown in the UI directly. It is however used when an icon
+ * needs to be found for the node.
+ *
+ * @param xml_name The XML element node name. Case sensitive.
+ * @param children The list of allowed children. Can be null or empty.
+ */
+ public DocumentDescriptor(String xml_name, ElementDescriptor[] children) {
+ super(xml_name, children, true /* mandatory */);
+ }
+
+ /**
+ * @return A new {@link UiElementNode} linked to this descriptor.
+ */
+ @Override
+ public UiElementNode createUiNode() {
+ return new UiDocumentNode(this);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/ElementDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/ElementDescriptor.java
new file mode 100644
index 0000000..5550155
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/ElementDescriptor.java
@@ -0,0 +1,318 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.descriptors;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.editors.IconFactory;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+import com.android.sdklib.SdkConstants;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.graphics.Image;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * {@link ElementDescriptor} describes the properties expected for a given XML element node.
+ *
+ * {@link ElementDescriptor} have an XML name, UI name, a tooltip, an SDK url,
+ * an attributes list and a children list.
+ *
+ * An UI node can be "mandatory", meaning the UI node is never deleted and it may lack
+ * an actual XML node attached. A non-mandatory UI node MUST have an XML node attached
+ * and it will cease to exist when the XML node ceases to exist.
+ */
+public class ElementDescriptor {
+ /** The XML element node name. Case sensitive. */
+ private String mXmlName;
+ /** The XML element name for the user interface, typically capitalized. */
+ private String mUiName;
+ /** The list of allowed attributes. */
+ private AttributeDescriptor[] mAttributes;
+ /** The list of allowed children */
+ private ElementDescriptor[] mChildren;
+ /* An optional tooltip. Can be empty. */
+ private String mTooltip;
+ /** An optional SKD URL. Can be empty. */
+ private String mSdkUrl;
+ /** Whether this UI node must always exist (even for empty models). */
+ private boolean mMandatory;
+
+ /**
+ * Constructs a new {@link ElementDescriptor} based on its XML name, UI name,
+ * tooltip, SDK url, attributes list, children list and mandatory.
+ *
+ * @param xml_name The XML element node name. Case sensitive.
+ * @param ui_name The XML element name for the user interface, typically capitalized.
+ * @param tooltip An optional tooltip. Can be null or empty.
+ * @param sdk_url An optional SKD URL. Can be null or empty.
+ * @param attributes The list of allowed attributes. Can be null or empty.
+ * @param children The list of allowed children. Can be null or empty.
+ * @param mandatory Whether this node must always exist (even for empty models). A mandatory
+ * UI node is never deleted and it may lack an actual XML node attached. A non-mandatory
+ * UI node MUST have an XML node attached and it will cease to exist when the XML node
+ * ceases to exist.
+ */
+ public ElementDescriptor(String xml_name, String ui_name, String tooltip, String sdk_url,
+ AttributeDescriptor[] attributes,
+ ElementDescriptor[] children,
+ boolean mandatory) {
+ mMandatory = mandatory;
+ mXmlName = xml_name;
+ mUiName = ui_name;
+ mTooltip = (tooltip != null && tooltip.length() > 0) ? tooltip : null;
+ mSdkUrl = (sdk_url != null && sdk_url.length() > 0) ? sdk_url : null;
+ setAttributes(attributes != null ? attributes : new AttributeDescriptor[]{});
+ mChildren = children != null ? children : new ElementDescriptor[]{};
+ }
+
+ /**
+ * Constructs a new {@link ElementDescriptor} based on its XML name and children list.
+ * The UI name is build by capitalizing the XML name.
+ * The UI nodes will be non-mandatory.
+ *
+ * @param xml_name The XML element node name. Case sensitive.
+ * @param children The list of allowed children. Can be null or empty.
+ * @param mandatory Whether this node must always exist (even for empty models). A mandatory
+ * UI node is never deleted and it may lack an actual XML node attached. A non-mandatory
+ * UI node MUST have an XML node attached and it will cease to exist when the XML node
+ * ceases to exist.
+ */
+ public ElementDescriptor(String xml_name, ElementDescriptor[] children, boolean mandatory) {
+ this(xml_name, prettyName(xml_name), null, null, null, children, mandatory);
+ }
+
+ /**
+ * Constructs a new {@link ElementDescriptor} based on its XML name and children list.
+ * The UI name is build by capitalizing the XML name.
+ * The UI nodes will be non-mandatory.
+ *
+ * @param xml_name The XML element node name. Case sensitive.
+ * @param children The list of allowed children. Can be null or empty.
+ */
+ public ElementDescriptor(String xml_name, ElementDescriptor[] children) {
+ this(xml_name, prettyName(xml_name), null, null, null, children, false);
+ }
+
+ /**
+ * Constructs a new {@link ElementDescriptor} based on its XML name.
+ * The UI name is build by capitalizing the XML name.
+ * The UI nodes will be non-mandatory.
+ *
+ * @param xml_name The XML element node name. Case sensitive.
+ */
+ public ElementDescriptor(String xml_name) {
+ this(xml_name, prettyName(xml_name), null, null, null, null, false);
+ }
+
+ /** Returns whether this node must always exist (even for empty models) */
+ public boolean isMandatory() {
+ return mMandatory;
+ }
+
+ /**
+ * Returns the XML element node local name (case sensitive)
+ */
+ public final String getXmlLocalName() {
+ int pos = mXmlName.indexOf(':');
+ if (pos != -1) {
+ return mXmlName.substring(pos+1);
+ }
+ return mXmlName;
+ }
+
+ /** Returns the XML element node name. Case sensitive. */
+ public String getXmlName() {
+ return mXmlName;
+ }
+
+ /**
+ * Returns the namespace of the attribute.
+ */
+ public final String getNamespace() {
+ // For now we hard-code the prefix as being "android"
+ if (mXmlName.startsWith("android:")) { //$NON-NLs-1$
+ return SdkConstants.NS_RESOURCES;
+ }
+
+ return ""; //$NON-NLs-1$
+ }
+
+
+ /** Returns the XML element name for the user interface, typically capitalized. */
+ public String getUiName() {
+ return mUiName;
+ }
+
+ /**
+ * Returns an optional icon for the element.
+ * <p/>
+ * By default this tries to return an icon based on the XML name of the element.
+ * If this fails, it tries to return the default Android logo as defined in the
+ * plugin. If all fails, it returns null.
+ *
+ * @return An icon for this element or null.
+ */
+ public Image getIcon() {
+ IconFactory factory = IconFactory.getInstance();
+ int color = hasChildren() ? IconFactory.COLOR_BLUE : IconFactory.COLOR_GREEN;
+ int shape = hasChildren() ? IconFactory.SHAPE_RECT : IconFactory.SHAPE_CIRCLE;
+ Image icon = factory.getIcon(mXmlName, color, shape);
+ return icon != null ? icon : AdtPlugin.getAndroidLogo();
+ }
+
+ /**
+ * Returns an optional ImageDescriptor for the element.
+ * <p/>
+ * By default this tries to return an image based on the XML name of the element.
+ * If this fails, it tries to return the default Android logo as defined in the
+ * plugin. If all fails, it returns null.
+ *
+ * @return An ImageDescriptor for this element or null.
+ */
+ public ImageDescriptor getImageDescriptor() {
+ IconFactory factory = IconFactory.getInstance();
+ int color = hasChildren() ? IconFactory.COLOR_BLUE : IconFactory.COLOR_GREEN;
+ int shape = hasChildren() ? IconFactory.SHAPE_RECT : IconFactory.SHAPE_CIRCLE;
+ ImageDescriptor id = factory.getImageDescriptor(mXmlName, color, shape);
+ return id != null ? id : AdtPlugin.getAndroidLogoDesc();
+ }
+
+ /* Returns the list of allowed attributes. */
+ public AttributeDescriptor[] getAttributes() {
+ return mAttributes;
+ }
+
+ /* Sets the list of allowed attributes. */
+ public void setAttributes(AttributeDescriptor[] attributes) {
+ mAttributes = attributes;
+ for (AttributeDescriptor attribute : attributes) {
+ attribute.setParent(this);
+ }
+ }
+
+ /** Returns the list of allowed children */
+ public ElementDescriptor[] getChildren() {
+ return mChildren;
+ }
+
+ /** @return True if this descriptor has children available */
+ public boolean hasChildren() {
+ return mChildren.length > 0;
+ }
+
+ /** Sets the list of allowed children. */
+ public void setChildren(ElementDescriptor[] newChildren) {
+ mChildren = newChildren;
+ }
+
+ /**
+ * Returns an optional tooltip. Will be null if not present.
+ * <p/>
+ * The tooltip is based on the Javadoc of the element and already processed via
+ * {@link DescriptorsUtils#formatTooltip(String)} to be displayed right away as
+ * a UI tooltip.
+ */
+ public String getTooltip() {
+ return mTooltip;
+ }
+
+ /** Returns an optional SKD URL. Will be null if not present. */
+ public String getSdkUrl() {
+ return mSdkUrl;
+ }
+
+ /** Sets the optional tooltip. Can be null or empty. */
+ public void setTooltip(String tooltip) {
+ mTooltip = tooltip;
+ }
+
+ /** Sets the optional SDK URL. Can be null or empty. */
+ public void setSdkUrl(String sdkUrl) {
+ mSdkUrl = sdkUrl;
+ }
+
+ /**
+ * @return A new {@link UiElementNode} linked to this descriptor.
+ */
+ public UiElementNode createUiNode() {
+ return new UiElementNode(this);
+ }
+
+ /**
+ * Returns the first children of this descriptor that describes the given XML element name.
+ * <p/>
+ * In recursive mode, searches the direct children first before descending in the hierarchy.
+ *
+ * @return The ElementDescriptor matching the requested XML node element name or null.
+ */
+ public ElementDescriptor findChildrenDescriptor(String element_name, boolean recursive) {
+ return findChildrenDescriptorInternal(element_name, recursive, null);
+ }
+
+ private ElementDescriptor findChildrenDescriptorInternal(String element_name,
+ boolean recursive,
+ Set<ElementDescriptor> visited) {
+ if (recursive && visited == null) {
+ visited = new HashSet<ElementDescriptor>();
+ }
+
+ for (ElementDescriptor e : getChildren()) {
+ if (e.getXmlName().equals(element_name)) {
+ return e;
+ }
+ }
+
+ if (visited != null) {
+ visited.add(this);
+ }
+
+ if (recursive) {
+ for (ElementDescriptor e : getChildren()) {
+ if (visited != null) {
+ if (!visited.add(e)) { // Set.add() returns false if element is already present
+ continue;
+ }
+ }
+ ElementDescriptor f = e.findChildrenDescriptorInternal(element_name,
+ recursive, visited);
+ if (f != null) {
+ return f;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Utility helper than pretty-formats an XML Name for the UI.
+ * This is used by the simplified constructor that takes only an XML element name.
+ *
+ * @param xml_name The XML name to convert.
+ * @return The XML name with dashes replaced by spaces and capitalized.
+ */
+ private static String prettyName(String xml_name) {
+ char c[] = xml_name.toCharArray();
+ if (c.length > 0) {
+ c[0] = Character.toUpperCase(c[0]);
+ }
+ return new String(c).replace("-", " "); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/EnumAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/EnumAttributeDescriptor.java
new file mode 100644
index 0000000..cab9883
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/EnumAttributeDescriptor.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.descriptors;
+
+import com.android.ide.eclipse.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.editors.uimodel.UiListAttributeNode;
+
+/**
+ * Describes a text attribute that can only contains some predefined values.
+ * It is displayed by a {@link UiListAttributeNode}.
+ */
+public class EnumAttributeDescriptor extends ListAttributeDescriptor {
+
+ public EnumAttributeDescriptor(String xmlLocalName, String uiName, String nsUri,
+ String tooltip) {
+ super(xmlLocalName, uiName, nsUri, tooltip);
+ }
+
+ /**
+ * @return A new {@link UiListAttributeNode} linked to this descriptor.
+ */
+ @Override
+ public UiAttributeNode createUiNode(UiElementNode uiParent) {
+ return new UiListAttributeNode(this, uiParent);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/FlagAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/FlagAttributeDescriptor.java
new file mode 100644
index 0000000..903417b
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/FlagAttributeDescriptor.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.descriptors;
+
+import com.android.ide.eclipse.editors.ui.FlagValueCellEditor;
+import com.android.ide.eclipse.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.editors.uimodel.UiFlagAttributeNode;
+import com.android.ide.eclipse.editors.uimodel.UiListAttributeNode;
+
+import org.eclipse.jface.viewers.CellEditor;
+import org.eclipse.swt.widgets.Composite;
+
+/**
+ * Describes a text attribute that can only contains some predefined values.
+ * It is displayed by a {@link UiListAttributeNode}.
+ *
+ * Note: in Android resources, a "flag" is a list of fixed values where one or
+ * more values can be selected using an "or", e.g. "align='left|top'".
+ * By contrast, an "enum" is a list of fixed values of which only one can be
+ * selected at a given time, e.g. "gravity='right'".
+ * <p/>
+ * This class handles the "flag" case.
+ * The "enum" case is done using {@link ListAttributeDescriptor}.
+ */
+public class FlagAttributeDescriptor extends TextAttributeDescriptor {
+
+ private String[] mNames;
+
+ /**
+ * Creates a new {@link FlagAttributeDescriptor} which automatically gets its
+ * values from the FrameworkResourceManager.
+ */
+ public FlagAttributeDescriptor(String xmlLocalName, String uiName, String nsUri,
+ String tooltip) {
+ super(xmlLocalName, uiName, nsUri, tooltip);
+ }
+
+ /**
+ * Creates a new {@link FlagAttributeDescriptor} which uses the provided values.
+ */
+ public FlagAttributeDescriptor(String xmlLocalName, String uiName, String nsUri,
+ String tooltip, String[] names) {
+ super(xmlLocalName, uiName, nsUri, tooltip);
+ mNames = names;
+ }
+
+ /**
+ * @return The initial names of the flags. Can be null, in which case the Framework
+ * resource parser should be checked.
+ */
+ public String[] getNames() {
+ return mNames;
+ }
+
+ /**
+ * @return A new {@link UiListAttributeNode} linked to this descriptor.
+ */
+ @Override
+ public UiAttributeNode createUiNode(UiElementNode uiParent) {
+ return new UiFlagAttributeNode(this, uiParent);
+ }
+
+ // ------- IPropertyDescriptor Methods
+
+ @Override
+ public CellEditor createPropertyEditor(Composite parent) {
+ return new FlagValueCellEditor(parent);
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/IDescriptorProvider.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/IDescriptorProvider.java
new file mode 100644
index 0000000..4c115e9
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/IDescriptorProvider.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.descriptors;
+
+public interface IDescriptorProvider {
+
+ ElementDescriptor[] getRootElementDescriptors();
+
+ ElementDescriptor getDescriptor();
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/ListAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/ListAttributeDescriptor.java
new file mode 100644
index 0000000..93969e9
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/ListAttributeDescriptor.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.descriptors;
+
+import com.android.ide.eclipse.editors.ui.ListValueCellEditor;
+import com.android.ide.eclipse.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.editors.uimodel.UiListAttributeNode;
+
+import org.eclipse.jface.viewers.CellEditor;
+import org.eclipse.swt.widgets.Composite;
+
+/**
+ * Describes a text attribute that can contains some predefined values.
+ * It is displayed by a {@link UiListAttributeNode}.
+ */
+public class ListAttributeDescriptor extends TextAttributeDescriptor {
+
+ private String[] mValues = null;
+
+ /**
+ * Creates a new {@link ListAttributeDescriptor} which automatically gets its
+ * values from the FrameworkResourceManager.
+ */
+ public ListAttributeDescriptor(String xmlLocalName, String uiName, String nsUri,
+ String tooltip) {
+ super(xmlLocalName, uiName, nsUri, tooltip);
+ }
+
+ /**
+ * Creates a new {@link ListAttributeDescriptor} which uses the provided values.
+ */
+ public ListAttributeDescriptor(String xmlLocalName, String uiName, String nsUri,
+ String tooltip, String[] values) {
+ super(xmlLocalName, uiName, nsUri, tooltip);
+ mValues = values;
+ }
+
+ public String[] getValues() {
+ return mValues;
+ }
+
+ /**
+ * @return A new {@link UiListAttributeNode} linked to this descriptor.
+ */
+ @Override
+ public UiAttributeNode createUiNode(UiElementNode uiParent) {
+ return new UiListAttributeNode(this, uiParent);
+ }
+
+ // ------- IPropertyDescriptor Methods
+
+ @Override
+ public CellEditor createPropertyEditor(Composite parent) {
+ return new ListValueCellEditor(parent);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/ReferenceAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/ReferenceAttributeDescriptor.java
new file mode 100644
index 0000000..336dfe2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/ReferenceAttributeDescriptor.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.descriptors;
+
+import com.android.ide.eclipse.common.resources.ResourceType;
+import com.android.ide.eclipse.editors.ui.ResourceValueCellEditor;
+import com.android.ide.eclipse.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.editors.uimodel.UiResourceAttributeNode;
+import com.android.sdklib.SdkConstants;
+
+import org.eclipse.jface.viewers.CellEditor;
+import org.eclipse.swt.widgets.Composite;
+
+/**
+ * Describes an XML attribute displayed containing a value or a reference to a resource.
+ * It is displayed by a {@link UiResourceAttributeNode}.
+ */
+public final class ReferenceAttributeDescriptor extends TextAttributeDescriptor {
+
+ private ResourceType mResourceType;
+
+ /**
+ * Creates a reference attributes that can contain any type of resources.
+ * @param xmlLocalName The XML name of the attribute (case sensitive)
+ * @param uiName The UI name of the attribute. Cannot be an empty string and cannot be null.
+ * @param nsUri The URI of the attribute. Can be null if attribute has no namespace.
+ * See {@link SdkConstants#NS_RESOURCES} for a common value.
+ * @param tooltip A non-empty tooltip string or null
+ */
+ public ReferenceAttributeDescriptor(String xmlLocalName, String uiName, String nsUri,
+ String tooltip) {
+ super(xmlLocalName, uiName, nsUri, tooltip);
+ }
+
+ /**
+ * Creates a reference attributes that can contain a reference to a specific
+ * {@link ResourceType}.
+ * @param resourceType The specific {@link ResourceType} that this reference attribute supports.
+ * It can be <code>null</code>, in which case, all resource types are supported.
+ * @param xmlLocalName The XML name of the attribute (case sensitive)
+ * @param uiName The UI name of the attribute. Cannot be an empty string and cannot be null.
+ * @param nsUri The URI of the attribute. Can be null if attribute has no namespace.
+ * See {@link SdkConstants#NS_RESOURCES} for a common value.
+ * @param tooltip A non-empty tooltip string or null
+ */
+ public ReferenceAttributeDescriptor(ResourceType resourceType,
+ String xmlLocalName, String uiName, String nsUri,
+ String tooltip) {
+ super(xmlLocalName, uiName, nsUri, tooltip);
+ mResourceType = resourceType;
+ }
+
+
+ /**
+ * @return A new {@link UiResourceAttributeNode} linked to this reference descriptor.
+ */
+ @Override
+ public UiAttributeNode createUiNode(UiElementNode uiParent) {
+ return new UiResourceAttributeNode(mResourceType, this, uiParent);
+ }
+
+ // ------- IPropertyDescriptor Methods
+
+ @Override
+ public CellEditor createPropertyEditor(Composite parent) {
+ return new ResourceValueCellEditor(parent);
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/SeparatorAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/SeparatorAttributeDescriptor.java
new file mode 100644
index 0000000..8fb1c7c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/SeparatorAttributeDescriptor.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.descriptors;
+
+import com.android.ide.eclipse.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.editors.uimodel.UiSeparatorAttributeNode;
+
+/**
+ * {@link SeparatorAttributeDescriptor} does not represent any real attribute.
+ * <p/>
+ * It is used to separate groups of attributes visually.
+ */
+public class SeparatorAttributeDescriptor extends AttributeDescriptor {
+
+ /**
+ * Creates a new {@link SeparatorAttributeDescriptor}
+ */
+ public SeparatorAttributeDescriptor(String label) {
+ super(label /* xmlLocalName */, null /* nsUri */);
+ }
+
+ /**
+ * @return A new {@link UiAttributeNode} linked to this descriptor or null if this
+ * attribute has no user interface.
+ */
+ @Override
+ public UiAttributeNode createUiNode(UiElementNode uiParent) {
+ return new UiSeparatorAttributeNode(this, uiParent);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/TextAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/TextAttributeDescriptor.java
new file mode 100644
index 0000000..77fc067
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/TextAttributeDescriptor.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.descriptors;
+
+import com.android.ide.eclipse.editors.ui.TextValueCellEditor;
+import com.android.ide.eclipse.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.editors.uimodel.UiTextAttributeNode;
+import com.android.sdklib.SdkConstants;
+
+import org.eclipse.jface.viewers.CellEditor;
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.ui.views.properties.IPropertyDescriptor;
+
+
+/**
+ * Describes a textual XML attribute.
+ * <p/>
+ * Such an attribute has a tooltip and would typically be displayed by
+ * {@link UiTextAttributeNode} using a label widget and text field.
+ * <p/>
+ * This is the "default" kind of attribute. If in doubt, use this.
+ */
+public class TextAttributeDescriptor extends AttributeDescriptor implements IPropertyDescriptor {
+ private String mUiName;
+ private String mTooltip;
+
+ /**
+ * Creates a new {@link TextAttributeDescriptor}
+ *
+ * @param xmlLocalName The XML name of the attribute (case sensitive)
+ * @param uiName The UI name of the attribute. Cannot be an empty string and cannot be null.
+ * @param nsUri The URI of the attribute. Can be null if attribute has no namespace.
+ * See {@link SdkConstants#NS_RESOURCES} for a common value.
+ * @param tooltip A non-empty tooltip string or null
+ */
+ public TextAttributeDescriptor(String xmlLocalName, String uiName,
+ String nsUri, String tooltip) {
+ super(xmlLocalName, nsUri);
+ mUiName = uiName;
+ mTooltip = (tooltip != null && tooltip.length() > 0) ? tooltip : null;
+ }
+
+ /**
+ * @return The UI name of the attribute. Cannot be an empty string and cannot be null.
+ */
+ public final String getUiName() {
+ return mUiName;
+ }
+
+ /**
+ * The tooltip string is either null or a non-empty string.
+ * <p/>
+ * The tooltip is based on the Javadoc of the attribute and already processed via
+ * {@link DescriptorsUtils#formatTooltip(String)} to be displayed right away as
+ * a UI tooltip.
+ * <p/>
+ * An empty string is converted to null, to match the behavior of setToolTipText() in
+ * {@link Control}.
+ *
+ * @return A non-empty tooltip string or null
+ */
+ public final String getTooltip() {
+ return mTooltip;
+ }
+
+ /**
+ * @return A new {@link UiTextAttributeNode} linked to this descriptor.
+ */
+ @Override
+ public UiAttributeNode createUiNode(UiElementNode uiParent) {
+ return new UiTextAttributeNode(this, uiParent);
+ }
+
+ // ------- IPropertyDescriptor Methods
+
+ public CellEditor createPropertyEditor(Composite parent) {
+ return new TextValueCellEditor(parent);
+ }
+
+ public String getCategory() {
+ if (isDeprecated()) {
+ return "Deprecated";
+ }
+
+ ElementDescriptor parent = getParent();
+ if (parent != null) {
+ return parent.getUiName();
+ }
+
+ return null;
+ }
+
+ public String getDescription() {
+ return mTooltip;
+ }
+
+ public String getDisplayName() {
+ return mUiName;
+ }
+
+ public String[] getFilterFlags() {
+ return null;
+ }
+
+ public Object getHelpContextIds() {
+ return null;
+ }
+
+ public Object getId() {
+ return this;
+ }
+
+ public ILabelProvider getLabelProvider() {
+ return AttributeDescriptorLabelProvider.getProvider();
+ }
+
+ public boolean isCompatibleWith(IPropertyDescriptor anotherProperty) {
+ return anotherProperty == this;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/TextValueDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/TextValueDescriptor.java
new file mode 100644
index 0000000..2015d71
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/TextValueDescriptor.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.descriptors;
+
+import com.android.ide.eclipse.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.editors.uimodel.UiTextValueNode;
+
+
+/**
+ * Describes the value of an XML element.
+ * <p/>
+ * The value is a simple text string, displayed by an {@link UiTextValueNode}.
+ */
+public class TextValueDescriptor extends TextAttributeDescriptor {
+
+ /**
+ * Creates a new {@link TextValueDescriptor}
+ *
+ * @param uiName The UI name of the attribute. Cannot be an empty string and cannot be null.
+ * @param tooltip A non-empty tooltip string or null
+ */
+ public TextValueDescriptor(String uiName, String tooltip) {
+ super("#text" /* xmlLocalName */, uiName, null /* nsUri */, tooltip);
+ }
+
+ /**
+ * @return A new {@link UiTextValueNode} linked to this descriptor.
+ */
+ @Override
+ public UiAttributeNode createUiNode(UiElementNode uiParent) {
+ return new UiTextValueNode(this, uiParent);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/XmlnsAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/XmlnsAttributeDescriptor.java
new file mode 100644
index 0000000..ed9c897
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/descriptors/XmlnsAttributeDescriptor.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.descriptors;
+
+import com.android.ide.eclipse.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+
+/**
+ * Describes an XMLNS attribute that is hidden.
+ * <p/>
+ * Such an attribute has no user interface and no corresponding {@link UiAttributeNode}.
+ * It also has a single constant default value.
+ * <p/>
+ * When loading an XML, we'll ignore this attribute.
+ * However when writing a new XML, we should always write this attribute.
+ * <p/>
+ * Currently this is used for the xmlns:android attribute in the manifest element.
+ */
+public final class XmlnsAttributeDescriptor extends AttributeDescriptor {
+
+ /**
+ * URI of the reserved "xmlns" prefix, as described in
+ * http://www.w3.org/TR/2004/REC-DOM-Level-3-Core-20040407/namespaces-algorithms.html#normalizeDocumentAlgo
+ */
+ public final static String XMLNS_URI = "http://www.w3.org/2000/xmlns/"; //$NON-NLS-1$
+
+ private String mValue;
+
+
+ public XmlnsAttributeDescriptor(String defaultPrefix, String value) {
+ super(defaultPrefix, XMLNS_URI);
+ mValue = value;
+ }
+
+ /**
+ * Returns the value of this specialized attribute descriptor, which is the URI associated
+ * to the declared namespace prefix.
+ */
+ public String getValue() {
+ return mValue;
+ }
+
+ /**
+ * Returns the "xmlns" prefix that is always used by this node for its namespace URI.
+ * This is defined by the XML specification.
+ */
+ public String getXmlNsPrefix() {
+ return "xmlns"; //$NON-NLS-1$
+ }
+
+ /**
+ * Returns the fully-qualified attribute name, namely "xmlns:xxx" where xxx is
+ * the defaultPrefix passed in the constructor.
+ */
+ public String getXmlNsName() {
+ return getXmlNsPrefix() + ":" + getXmlLocalName(); //$NON-NLS-1$
+ }
+
+ /**
+ * @return Always returns null. {@link XmlnsAttributeDescriptor} has no user interface.
+ */
+ @Override
+ public UiAttributeNode createUiNode(UiElementNode uiParent) {
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/BasePullParser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/BasePullParser.java
new file mode 100644
index 0000000..381539b
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/BasePullParser.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.layout;
+
+import com.android.layoutlib.api.IXmlPullParser;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.InputStream;
+import java.io.Reader;
+
+/**
+ * Base implementation of an {@link IXmlPullParser} for cases where the parser is not sitting
+ * on top of an actual XML file.
+ * <p/>It's designed to work on layout files, and will most likely not work on other resource
+ * files.
+ */
+public abstract class BasePullParser implements IXmlPullParser {
+
+ protected int mParsingState = START_DOCUMENT;
+
+ public BasePullParser() {
+ }
+
+ // --- new methods to override ---
+
+ public abstract void onNextFromStartDocument();
+ public abstract void onNextFromStartTag();
+ public abstract void onNextFromEndTag();
+
+ // --- basic implementation of IXmlPullParser ---
+
+ public void setFeature(String name, boolean state) throws XmlPullParserException {
+ if (FEATURE_PROCESS_NAMESPACES.equals(name) && state) {
+ return;
+ }
+ if (FEATURE_REPORT_NAMESPACE_ATTRIBUTES.equals(name) && state) {
+ return;
+ }
+ throw new XmlPullParserException("Unsupported feature: " + name);
+ }
+
+ public boolean getFeature(String name) {
+ if (FEATURE_PROCESS_NAMESPACES.equals(name)) {
+ return true;
+ }
+ if (FEATURE_REPORT_NAMESPACE_ATTRIBUTES.equals(name)) {
+ return true;
+ }
+ return false;
+ }
+
+ public void setProperty(String name, Object value) throws XmlPullParserException {
+ throw new XmlPullParserException("setProperty() not supported");
+ }
+
+ public Object getProperty(String name) {
+ return null;
+ }
+
+ public void setInput(Reader in) throws XmlPullParserException {
+ throw new XmlPullParserException("setInput() not supported");
+ }
+
+ public void setInput(InputStream inputStream, String inputEncoding)
+ throws XmlPullParserException {
+ throw new XmlPullParserException("setInput() not supported");
+ }
+
+ public void defineEntityReplacementText(String entityName, String replacementText)
+ throws XmlPullParserException {
+ throw new XmlPullParserException("defineEntityReplacementText() not supported");
+ }
+
+ public String getNamespacePrefix(int pos) throws XmlPullParserException {
+ throw new XmlPullParserException("getNamespacePrefix() not supported");
+ }
+
+ public String getInputEncoding() {
+ return null;
+ }
+
+ public String getNamespace(String prefix) {
+ throw new RuntimeException("getNamespace() not supported");
+ }
+
+ public int getNamespaceCount(int depth) throws XmlPullParserException {
+ throw new XmlPullParserException("getNamespaceCount() not supported");
+ }
+
+ public String getNamespaceUri(int pos) throws XmlPullParserException {
+ throw new XmlPullParserException("getNamespaceUri() not supported");
+ }
+
+ public int getColumnNumber() {
+ return -1;
+ }
+
+ public int getLineNumber() {
+ return -1;
+ }
+
+ public String getAttributeType(int arg0) {
+ return "CDATA";
+ }
+
+ public int getEventType() {
+ return mParsingState;
+ }
+
+ public String getText() {
+ return null;
+ }
+
+ public char[] getTextCharacters(int[] arg0) {
+ return null;
+ }
+
+ public boolean isAttributeDefault(int arg0) {
+ return false;
+ }
+
+ public boolean isWhitespace() {
+ return false;
+ }
+
+ public int next() throws XmlPullParserException {
+ switch (mParsingState) {
+ case END_DOCUMENT:
+ throw new XmlPullParserException("Nothing after the end");
+ case START_DOCUMENT:
+ onNextFromStartDocument();
+ break;
+ case START_TAG:
+ onNextFromStartTag();
+ break;
+ case END_TAG:
+ onNextFromEndTag();
+ break;
+ case TEXT:
+ // not used
+ break;
+ case CDSECT:
+ // not used
+ break;
+ case ENTITY_REF:
+ // not used
+ break;
+ case IGNORABLE_WHITESPACE:
+ // not used
+ break;
+ case PROCESSING_INSTRUCTION:
+ // not used
+ break;
+ case COMMENT:
+ // not used
+ break;
+ case DOCDECL:
+ // not used
+ break;
+ }
+
+ return mParsingState;
+ }
+
+ public int nextTag() throws XmlPullParserException {
+ int eventType = next();
+ if (eventType != START_TAG && eventType != END_TAG) {
+ throw new XmlPullParserException("expected start or end tag", this, null);
+ }
+ return eventType;
+ }
+
+ public String nextText() throws XmlPullParserException {
+ if (getEventType() != START_TAG) {
+ throw new XmlPullParserException("parser must be on START_TAG to read next text", this,
+ null);
+ }
+ int eventType = next();
+ if (eventType == TEXT) {
+ String result = getText();
+ eventType = next();
+ if (eventType != END_TAG) {
+ throw new XmlPullParserException(
+ "event TEXT it must be immediately followed by END_TAG", this, null);
+ }
+ return result;
+ } else if (eventType == END_TAG) {
+ return "";
+ } else {
+ throw new XmlPullParserException("parser must be on START_TAG or TEXT to read text",
+ this, null);
+ }
+ }
+
+ public int nextToken() throws XmlPullParserException {
+ return next();
+ }
+
+ public void require(int type, String namespace, String name) throws XmlPullParserException {
+ if (type != getEventType() || (namespace != null && !namespace.equals(getNamespace()))
+ || (name != null && !name.equals(getName())))
+ throw new XmlPullParserException("expected " + TYPES[type] + getPositionDescription());
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/GraphicalLayoutEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/GraphicalLayoutEditor.java
new file mode 100644
index 0000000..eb7dee6
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/GraphicalLayoutEditor.java
@@ -0,0 +1,2402 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.layout;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.sdk.LoadStatus;
+import com.android.ide.eclipse.adt.sdk.Sdk;
+import com.android.ide.eclipse.adt.sdk.AndroidTargetData.LayoutBridge;
+import com.android.ide.eclipse.adt.sdk.Sdk.ITargetChangeListener;
+import com.android.ide.eclipse.common.resources.ResourceType;
+import com.android.ide.eclipse.editors.IconFactory;
+import com.android.ide.eclipse.editors.layout.LayoutEditor.UiEditorActions;
+import com.android.ide.eclipse.editors.layout.LayoutReloadMonitor.ILayoutReloadListener;
+import com.android.ide.eclipse.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.editors.layout.parts.ElementCreateCommand;
+import com.android.ide.eclipse.editors.layout.parts.UiElementEditPart;
+import com.android.ide.eclipse.editors.layout.parts.UiElementsEditPartFactory;
+import com.android.ide.eclipse.editors.resources.configurations.CountryCodeQualifier;
+import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration;
+import com.android.ide.eclipse.editors.resources.configurations.KeyboardStateQualifier;
+import com.android.ide.eclipse.editors.resources.configurations.LanguageQualifier;
+import com.android.ide.eclipse.editors.resources.configurations.NavigationMethodQualifier;
+import com.android.ide.eclipse.editors.resources.configurations.NetworkCodeQualifier;
+import com.android.ide.eclipse.editors.resources.configurations.PixelDensityQualifier;
+import com.android.ide.eclipse.editors.resources.configurations.RegionQualifier;
+import com.android.ide.eclipse.editors.resources.configurations.ScreenDimensionQualifier;
+import com.android.ide.eclipse.editors.resources.configurations.ScreenOrientationQualifier;
+import com.android.ide.eclipse.editors.resources.configurations.TextInputMethodQualifier;
+import com.android.ide.eclipse.editors.resources.configurations.TouchScreenQualifier;
+import com.android.ide.eclipse.editors.resources.configurations.KeyboardStateQualifier.KeyboardState;
+import com.android.ide.eclipse.editors.resources.configurations.NavigationMethodQualifier.NavigationMethod;
+import com.android.ide.eclipse.editors.resources.configurations.ScreenOrientationQualifier.ScreenOrientation;
+import com.android.ide.eclipse.editors.resources.configurations.TextInputMethodQualifier.TextInputMethod;
+import com.android.ide.eclipse.editors.resources.configurations.TouchScreenQualifier.TouchScreenType;
+import com.android.ide.eclipse.editors.resources.manager.ProjectResources;
+import com.android.ide.eclipse.editors.resources.manager.ResourceFile;
+import com.android.ide.eclipse.editors.resources.manager.ResourceFolderType;
+import com.android.ide.eclipse.editors.resources.manager.ResourceManager;
+import com.android.ide.eclipse.editors.ui.tree.CopyCutAction;
+import com.android.ide.eclipse.editors.ui.tree.PasteAction;
+import com.android.ide.eclipse.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.editors.wizards.ConfigurationSelector.DensityVerifier;
+import com.android.ide.eclipse.editors.wizards.ConfigurationSelector.DimensionVerifier;
+import com.android.ide.eclipse.editors.wizards.ConfigurationSelector.LanguageRegionVerifier;
+import com.android.ide.eclipse.editors.wizards.ConfigurationSelector.MobileCodeVerifier;
+import com.android.layoutlib.api.ILayoutLog;
+import com.android.layoutlib.api.ILayoutResult;
+import com.android.layoutlib.api.IResourceValue;
+import com.android.layoutlib.api.IStyleResourceValue;
+import com.android.layoutlib.api.ILayoutResult.ILayoutViewInfo;
+import com.android.sdklib.IAndroidTarget;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.draw2d.geometry.Rectangle;
+import org.eclipse.gef.DefaultEditDomain;
+import org.eclipse.gef.EditPart;
+import org.eclipse.gef.EditPartViewer;
+import org.eclipse.gef.GraphicalViewer;
+import org.eclipse.gef.SelectionManager;
+import org.eclipse.gef.dnd.TemplateTransferDragSourceListener;
+import org.eclipse.gef.dnd.TemplateTransferDropTargetListener;
+import org.eclipse.gef.editparts.ScalableFreeformRootEditPart;
+import org.eclipse.gef.palette.PaletteRoot;
+import org.eclipse.gef.requests.CreationFactory;
+import org.eclipse.gef.ui.parts.GraphicalEditorWithPalette;
+import org.eclipse.gef.ui.parts.SelectionSynchronizer;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.IMenuListener;
+import org.eclipse.jface.action.IMenuManager;
+import org.eclipse.jface.action.MenuManager;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.dnd.Clipboard;
+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.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.PaletteData;
+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.Label;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.ide.IDE;
+import org.eclipse.ui.part.FileEditorInput;
+
+import java.awt.image.BufferedImage;
+import java.awt.image.DataBufferInt;
+import java.awt.image.Raster;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Graphical layout editor, based on GEF.
+ * <p/>
+ * To understand GEF: http://www.ibm.com/developerworks/opensource/library/os-gef/
+ * <p/>
+ * To understand Drag'n'drop: http://www.eclipse.org/articles/Article-Workbench-DND/drag_drop.html
+ */
+public class GraphicalLayoutEditor extends GraphicalEditorWithPalette
+ implements ILayoutReloadListener {
+
+ private final static String THEME_SEPARATOR = "----------"; //$NON-NLS-1$
+
+ /** Reference to the layout editor */
+ private final LayoutEditor mLayoutEditor;
+
+ /** reference to the file being edited. */
+ private IFile mEditedFile;
+
+ private Clipboard mClipboard;
+ private Composite mParent;
+ private PaletteRoot mPaletteRoot;
+
+ private Text mCountry;
+ private Text mNetwork;
+ private Combo mLanguage;
+ private Combo mRegion;
+ private Combo mOrientation;
+ private Text mDensity;
+ private Combo mTouch;
+ private Combo mKeyboard;
+ private Combo mTextInput;
+ private Combo mNavigation;
+ private Text mSize1;
+ private Text mSize2;
+ private Combo mThemeCombo;
+ private Button mCreateButton;
+
+ private Label mCountryIcon;
+ private Label mNetworkIcon;
+ private Label mLanguageIcon;
+ private Label mRegionIcon;
+ private Label mOrientationIcon;
+ private Label mDensityIcon;
+ private Label mTouchIcon;
+ private Label mKeyboardIcon;
+ private Label mTextInputIcon;
+ private Label mNavigationIcon;
+ private Label mSizeIcon;
+
+ private Label mCurrentLayoutLabel;
+
+ private Image mWarningImage;
+ private Image mMatchImage;
+ private Image mErrorImage;
+
+ /** The {@link FolderConfiguration} representing the state of the UI controls */
+ private FolderConfiguration mCurrentConfig = new FolderConfiguration();
+ /** The {@link FolderConfiguration} being edited. */
+ private FolderConfiguration mEditedConfig;
+
+ private Map<String, Map<String, IResourceValue>> mConfiguredFrameworkRes;
+ private Map<String, Map<String, IResourceValue>> mConfiguredProjectRes;
+ private ProjectCallback mProjectCallback;
+ private ILayoutLog mLogger;
+
+ private boolean mNeedsXmlReload = false;
+ private boolean mNeedsRecompute = false;
+ private int mPlatformThemeCount = 0;
+ private boolean mDisableUpdates = false;
+
+ /** Listener to update the root node if the target of the file is changed because of a
+ * SDK location change or a project target change */
+ private ITargetChangeListener mTargetListener = new ITargetChangeListener() {
+ public void onProjectTargetChange(IProject changedProject) {
+ if (changedProject == getLayoutEditor().getProject()) {
+ onTargetsLoaded();
+ }
+ }
+
+ public void onTargetsLoaded() {
+ // because the SDK changed we must reset the configured framework resource.
+ mConfiguredFrameworkRes = null;
+
+ updateUIFromResources();
+
+ mThemeCombo.getParent().layout();
+
+ // updateUiFromFramework will reset language/region combo, so we must call
+ // setConfiguration after, or the settext on language/region will be lost.
+ if (mEditedConfig != null) {
+ setConfiguration(mEditedConfig);
+ }
+
+ // make sure we remove the custom view loader, since its parent class loader is the
+ // bridge class loader.
+ mProjectCallback = null;
+
+ recomputeLayout();
+ }
+ };
+
+ private final Runnable mConditionalRecomputeRunnable = new Runnable() {
+ public void run() {
+ if (mLayoutEditor.isGraphicalEditorActive()) {
+ recomputeLayout();
+ } else {
+ mNeedsRecompute = true;
+ }
+ }
+ };
+
+ private final Runnable mUiUpdateFromResourcesRunnable = new Runnable() {
+ public void run() {
+ updateUIFromResources();
+ mThemeCombo.getParent().layout();
+ }
+ };
+
+ public GraphicalLayoutEditor(LayoutEditor layoutEditor) {
+ mLayoutEditor = layoutEditor;
+ setEditDomain(new DefaultEditDomain(this));
+ setPartName("Layout");
+
+ IconFactory factory = IconFactory.getInstance();
+ mWarningImage = factory.getIcon("warning"); //$NON-NLS-1$
+ mMatchImage = factory.getIcon("match"); //$NON-NLS-1$
+ mErrorImage = factory.getIcon("error"); //$NON-NLS-1$
+
+ AdtPlugin.getDefault().addTargetListener(mTargetListener);
+ }
+
+ // ------------------------------------
+ // Methods overridden from base classes
+ //------------------------------------
+
+ @Override
+ public void createPartControl(Composite parent) {
+ mParent = parent;
+ GridLayout gl;
+ GridData gd;
+
+ mClipboard = new Clipboard(parent.getDisplay());
+
+ parent.setLayout(gl = new GridLayout(1, false));
+ gl.marginHeight = gl.marginWidth = 0;
+
+ // create the top part for the configuration control
+ int cols = 10;
+
+ Composite topParent = new Composite(parent, SWT.NONE);
+ topParent.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ topParent.setLayout(gl = new GridLayout(cols, false));
+
+ new Label(topParent, SWT.NONE).setText("MCC");
+ mCountryIcon = createControlComposite(topParent, true /* grab_horizontal */);
+ mCountry = new Text(mCountryIcon.getParent(), SWT.BORDER);
+ mCountry.setLayoutData(new GridData(
+ GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
+ mCountry.addVerifyListener(new MobileCodeVerifier());
+ mCountry.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetDefaultSelected(SelectionEvent e) {
+ onCountryCodeChange();
+ }
+ });
+ mCountry.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ onCountryCodeChange();
+ }
+ });
+
+ new Label(topParent, SWT.NONE).setText("MNC");
+ mNetworkIcon = createControlComposite(topParent, true /* grab_horizontal */);
+ mNetwork = new Text(mNetworkIcon.getParent(), SWT.BORDER);
+ mNetwork.setLayoutData(new GridData(
+ GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
+ mNetwork.addVerifyListener(new MobileCodeVerifier());
+ mNetwork.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetDefaultSelected(SelectionEvent e) {
+ onNetworkCodeChange();
+ }
+ });
+ mNetwork.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ onNetworkCodeChange();
+ }
+ });
+
+ new Label(topParent, SWT.NONE).setText("Lang");
+ mLanguageIcon = createControlComposite(topParent, true /* grab_horizontal */);
+ mLanguage = new Combo(mLanguageIcon.getParent(), SWT.DROP_DOWN);
+ mLanguage.setLayoutData(new GridData(
+ GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
+ mLanguage.addVerifyListener(new LanguageRegionVerifier());
+ mLanguage.addSelectionListener(new SelectionListener() {
+ public void widgetDefaultSelected(SelectionEvent e) {
+ onLanguageChange();
+ }
+ public void widgetSelected(SelectionEvent e) {
+ onLanguageChange();
+ }
+ });
+ mLanguage.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ onLanguageChange();
+ }
+ });
+
+ new Label(topParent, SWT.NONE).setText("Region");
+ mRegionIcon = createControlComposite(topParent, true /* grab_horizontal */);
+ mRegion = new Combo(mRegionIcon.getParent(), SWT.DROP_DOWN);
+ mRegion.setLayoutData(new GridData(
+ GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
+ mRegion.addVerifyListener(new LanguageRegionVerifier());
+ mRegion.addSelectionListener(new SelectionListener() {
+ public void widgetDefaultSelected(SelectionEvent e) {
+ onRegionChange();
+ }
+ public void widgetSelected(SelectionEvent e) {
+ onRegionChange();
+ }
+ });
+ mRegion.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ onRegionChange();
+ }
+ });
+
+ new Label(topParent, SWT.NONE).setText("Orient");
+ mOrientationIcon = createControlComposite(topParent, true /* grab_horizontal */);
+ mOrientation = new Combo(mOrientationIcon.getParent(), SWT.DROP_DOWN | SWT.READ_ONLY);
+ ScreenOrientation[] soValues = ScreenOrientation.values();
+ mOrientation.add("(Default)");
+ for (ScreenOrientation value : soValues) {
+ mOrientation.add(value.getDisplayValue());
+ }
+ mOrientation.select(0);
+ mOrientation.setLayoutData(new GridData(
+ GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
+ mOrientation.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ onOrientationChange();
+ }
+ });
+
+ new Label(topParent, SWT.NONE).setText("Density");
+ mDensityIcon = createControlComposite(topParent, true /* grab_horizontal */);
+ mDensity = new Text(mDensityIcon.getParent(), SWT.BORDER);
+ mDensity.setLayoutData(new GridData(
+ GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
+ mDensity.addVerifyListener(new DensityVerifier());
+ mDensity.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetDefaultSelected(SelectionEvent e) {
+ onDensityChange();
+ }
+ });
+ mDensity.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ onDensityChange();
+ }
+ });
+
+ new Label(topParent, SWT.NONE).setText("Touch");
+ mTouchIcon = createControlComposite(topParent, true /* grab_horizontal */);
+ mTouch = new Combo(mTouchIcon.getParent(), SWT.DROP_DOWN | SWT.READ_ONLY);
+ TouchScreenType[] tstValues = TouchScreenType.values();
+ mTouch.add("(Default)");
+ for (TouchScreenType value : tstValues) {
+ mTouch.add(value.getDisplayValue());
+ }
+ mTouch.select(0);
+ mTouch.setLayoutData(new GridData(
+ GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
+ mTouch.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ onTouchChange();
+ }
+ });
+
+ new Label(topParent, SWT.NONE).setText("Keybrd");
+ mKeyboardIcon = createControlComposite(topParent, true /* grab_horizontal */);
+ mKeyboard = new Combo(mKeyboardIcon.getParent(), SWT.DROP_DOWN | SWT.READ_ONLY);
+ KeyboardState[] ksValues = KeyboardState.values();
+ mKeyboard.add("(Default)");
+ for (KeyboardState value : ksValues) {
+ mKeyboard.add(value.getDisplayValue());
+ }
+ mKeyboard.select(0);
+ mKeyboard.setLayoutData(new GridData(
+ GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
+ mKeyboard.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ onKeyboardChange();
+ }
+ });
+
+ new Label(topParent, SWT.NONE).setText("Input");
+ mTextInputIcon = createControlComposite(topParent, true /* grab_horizontal */);
+ mTextInput = new Combo(mTextInputIcon.getParent(), SWT.DROP_DOWN | SWT.READ_ONLY);
+ TextInputMethod[] timValues = TextInputMethod.values();
+ mTextInput.add("(Default)");
+ for (TextInputMethod value : timValues) {
+ mTextInput.add(value.getDisplayValue());
+ }
+ mTextInput.select(0);
+ mTextInput.setLayoutData(new GridData(
+ GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
+ mTextInput.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ onTextInputChange();
+ }
+ });
+
+ new Label(topParent, SWT.NONE).setText("Nav");
+ mNavigationIcon = createControlComposite(topParent, true /* grab_horizontal */);
+ mNavigation = new Combo(mNavigationIcon.getParent(), SWT.DROP_DOWN | SWT.READ_ONLY);
+ NavigationMethod[] nValues = NavigationMethod.values();
+ mNavigation.add("(Default)");
+ for (NavigationMethod value : nValues) {
+ mNavigation.add(value.getDisplayValue());
+ }
+ mNavigation.select(0);
+ mNavigation.setLayoutData(new GridData(
+ GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
+ mNavigation.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ onNavigationChange();
+ }
+ });
+
+ Composite labelParent = new Composite(topParent, SWT.NONE);
+ labelParent.setLayout(gl = new GridLayout(8, false));
+ gl.marginWidth = gl.marginHeight = 0;
+ labelParent.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ gd.horizontalSpan = cols;
+
+ new Label(labelParent, SWT.NONE).setText("Editing config:");
+ mCurrentLayoutLabel = new Label(labelParent, SWT.NONE);
+ mCurrentLayoutLabel.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ gd.widthHint = 50;
+
+ new Label(labelParent, SWT.NONE).setText("Size");
+ mSizeIcon = createControlComposite(labelParent, false);
+ Composite sizeParent = new Composite(mSizeIcon.getParent(), SWT.NONE);
+ sizeParent.setLayout(gl = new GridLayout(3, false));
+ gl.marginWidth = gl.marginHeight = 0;
+ gl.horizontalSpacing = 0;
+
+ mSize1 = new Text(sizeParent, SWT.BORDER);
+ mSize1.setLayoutData(gd = new GridData());
+ gd.widthHint = 30;
+ new Label(sizeParent, SWT.NONE).setText("x");
+ mSize2 = new Text(sizeParent, SWT.BORDER);
+ mSize2.setLayoutData(gd = new GridData());
+ gd.widthHint = 30;
+
+ DimensionVerifier verifier = new DimensionVerifier();
+ mSize1.addVerifyListener(verifier);
+ mSize2.addVerifyListener(verifier);
+
+ SelectionListener sl = new SelectionListener() {
+ public void widgetDefaultSelected(SelectionEvent e) {
+ onSizeChange();
+ }
+ public void widgetSelected(SelectionEvent e) {
+ onSizeChange();
+ }
+ };
+
+ mSize1.addSelectionListener(sl);
+ mSize2.addSelectionListener(sl);
+
+ ModifyListener sizeModifyListener = new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ onSizeChange();
+ }
+ };
+
+ mSize1.addModifyListener(sizeModifyListener);
+ mSize2.addModifyListener(sizeModifyListener);
+
+ // first separator
+ Label separator = new Label(labelParent, SWT.SEPARATOR | SWT.VERTICAL);
+ separator.setLayoutData(gd = new GridData(
+ GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL));
+ gd.heightHint = 0;
+
+ mThemeCombo = new Combo(labelParent, SWT.READ_ONLY | SWT.DROP_DOWN);
+ mThemeCombo.setEnabled(false);
+ updateUIFromResources();
+ mThemeCombo.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ onThemeChange();
+ }
+ });
+
+ // second separator
+ separator = new Label(labelParent, SWT.SEPARATOR | SWT.VERTICAL);
+ separator.setLayoutData(gd = new GridData(
+ GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL));
+ gd.heightHint = 0;
+
+ mCreateButton = new Button(labelParent, SWT.PUSH | SWT.FLAT);
+ mCreateButton.setText("Create...");
+ mCreateButton.setEnabled(false);
+ mCreateButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ LayoutCreatorDialog dialog = new LayoutCreatorDialog(mCreateButton.getShell(),
+ mEditedFile.getName(), mCurrentConfig);
+ if (dialog.open() == Dialog.OK) {
+ final FolderConfiguration config = new FolderConfiguration();
+ dialog.getConfiguration(config);
+
+ createAlternateLayout(config);
+ }
+ }
+ });
+
+ // create a new composite that will contain the standard editor controls.
+ Composite editorParent = new Composite(parent, SWT.NONE);
+ editorParent.setLayoutData(new GridData(GridData.FILL_BOTH));
+ editorParent.setLayout(new FillLayout());
+ super.createPartControl(editorParent);
+ }
+
+ @Override
+ public void dispose() {
+ if (mTargetListener != null) {
+ AdtPlugin.getDefault().removeTargetListener(mTargetListener);
+ mTargetListener = null;
+ }
+
+ LayoutReloadMonitor.getMonitor().removeListener(mEditedFile.getProject(), this);
+
+ if (mClipboard != null) {
+ mClipboard.dispose();
+ mClipboard = null;
+ }
+
+ super.dispose();
+ }
+
+ /* (non-Javadoc)
+ * Creates the palette root.
+ */
+ @Override
+ protected PaletteRoot getPaletteRoot() {
+ mPaletteRoot = PaletteFactory.createPaletteRoot(mPaletteRoot,
+ mLayoutEditor.getTargetData());
+ return mPaletteRoot;
+ }
+
+ public Clipboard getClipboard() {
+ return mClipboard;
+ }
+
+ /**
+ * Save operation in the Graphical Layout Editor.
+ * <p/>
+ * In our workflow, the model is owned by the Structured XML Editor.
+ * The graphical layout editor just displays it -- thus we don't really
+ * save anything here.
+ * <p/>
+ * This must NOT call the parent editor part. At the contrary, the parent editor
+ * part will call this *after* having done the actual save operation.
+ * <p/>
+ * The only action this editor must do is mark the undo command stack as
+ * being no longer dirty.
+ */
+ @Override
+ public void doSave(IProgressMonitor monitor) {
+ getCommandStack().markSaveLocation();
+ firePropertyChange(PROP_DIRTY);
+ }
+
+ @Override
+ protected void configurePaletteViewer() {
+ super.configurePaletteViewer();
+
+ // Create a drag source listener on an edit part that is a viewer.
+ // What this does is use DND with a TemplateTransfer type which is actually
+ // the PaletteTemplateEntry held in the PaletteRoot.
+ TemplateTransferDragSourceListener dragSource =
+ new TemplateTransferDragSourceListener(getPaletteViewer());
+
+ // Create a drag source on the palette viewer.
+ // See the drag target associated with the GraphicalViewer in configureGraphicalViewer.
+ getPaletteViewer().addDragSourceListener(dragSource);
+ }
+
+ /* (non-javadoc)
+ * Configure the graphical viewer before it receives its contents.
+ */
+ @Override
+ protected void configureGraphicalViewer() {
+ super.configureGraphicalViewer();
+
+ GraphicalViewer viewer = getGraphicalViewer();
+ viewer.setEditPartFactory(new UiElementsEditPartFactory(mParent.getDisplay()));
+ viewer.setRootEditPart(new ScalableFreeformRootEditPart());
+
+ // Disable the following -- we don't drag *from* the GraphicalViewer yet:
+ // viewer.addDragSourceListener(new TemplateTransferDragSourceListener(viewer));
+
+ viewer.addDropTargetListener(new DropListener(viewer));
+ }
+
+ class DropListener extends TemplateTransferDropTargetListener {
+ public DropListener(EditPartViewer viewer) {
+ super(viewer);
+ }
+
+ // TODO explain
+ @Override
+ protected CreationFactory getFactory(final Object template) {
+ return new CreationFactory() {
+ public Object getNewObject() {
+ // We don't know the newly created EditPart since "creating" new
+ // elements is done by ElementCreateCommand.execute() directly by
+ // manipulating the XML elements..
+ return null;
+ }
+
+ public Object getObjectType() {
+ return template;
+ }
+
+ };
+ }
+ }
+
+ /* (non-javadoc)
+ * Set the contents of the GraphicalViewer after it has been created.
+ */
+ @Override
+ protected void initializeGraphicalViewer() {
+ GraphicalViewer viewer = getGraphicalViewer();
+ viewer.setContents(getModel());
+
+ IEditorInput input = getEditorInput();
+ if (input instanceof FileEditorInput) {
+ FileEditorInput fileInput = (FileEditorInput)input;
+ mEditedFile = fileInput.getFile();
+
+ updateUIFromResources();
+
+ LayoutReloadMonitor.getMonitor().addListener(mEditedFile.getProject(), this);
+ } else {
+ // really this shouldn't happen! Log it in case it happens
+ mEditedFile = null;
+ AdtPlugin.log(IStatus.ERROR, "Input is not of type FileEditorInput: %1$s",
+ input.toString());
+ }
+ }
+
+ /* (non-javadoc)
+ * Sets the graphicalViewer for this EditorPart.
+ * @param viewer the graphical viewer
+ */
+ @Override
+ protected void setGraphicalViewer(GraphicalViewer viewer) {
+ super.setGraphicalViewer(viewer);
+
+ // TODO: viewer.setKeyHandler()
+ viewer.setContextMenu(createContextMenu(viewer));
+ }
+
+ /**
+ * Used by LayoutEditor.UiEditorActions.selectUiNode to select a new UI Node
+ * created by {@link ElementCreateCommand#execute()}.
+ *
+ * @param uiNodeModel The {@link UiElementNode} to select.
+ */
+ public void selectModel(UiElementNode uiNodeModel) {
+ GraphicalViewer viewer = getGraphicalViewer();
+
+ // Give focus to the graphical viewer (in case the outline has it)
+ viewer.getControl().forceFocus();
+
+ Object editPart = viewer.getEditPartRegistry().get(uiNodeModel);
+
+ if (editPart instanceof EditPart) {
+ viewer.select((EditPart)editPart);
+ }
+ }
+
+
+ //--------------
+ // Local methods
+ //--------------
+
+ public LayoutEditor getLayoutEditor() {
+ return mLayoutEditor;
+ }
+
+ private MenuManager createContextMenu(GraphicalViewer viewer) {
+ MenuManager menuManager = new MenuManager();
+ menuManager.setRemoveAllWhenShown(true);
+ menuManager.addMenuListener(new ActionMenuListener(viewer));
+
+ return menuManager;
+ }
+
+ private class ActionMenuListener implements IMenuListener {
+ private final GraphicalViewer mViewer;
+
+ public ActionMenuListener(GraphicalViewer viewer) {
+ mViewer = viewer;
+ }
+
+ /**
+ * The menu is about to be shown. The menu manager has already been
+ * requested to remove any existing menu item. This method gets the
+ * tree selection and if it is of the appropriate type it re-creates
+ * the necessary actions.
+ */
+ public void menuAboutToShow(IMenuManager manager) {
+ ArrayList<UiElementNode> selected = new ArrayList<UiElementNode>();
+
+ // filter selected items and only keep those we can handle
+ for (Object obj : mViewer.getSelectedEditParts()) {
+ if (obj instanceof UiElementEditPart) {
+ UiElementEditPart part = (UiElementEditPart) obj;
+ UiElementNode uiNode = part.getUiNode();
+ if (uiNode != null) {
+ selected.add(uiNode);
+ }
+ }
+ }
+
+ if (selected.size() > 0) {
+ doCreateMenuAction(manager, mViewer, selected);
+ }
+ }
+ }
+
+ private void doCreateMenuAction(IMenuManager manager,
+ final GraphicalViewer viewer,
+ final ArrayList<UiElementNode> selected) {
+ if (selected != null) {
+ boolean hasXml = false;
+ for (UiElementNode uiNode : selected) {
+ if (uiNode.getXmlNode() != null) {
+ hasXml = true;
+ break;
+ }
+ }
+
+ if (hasXml) {
+ manager.add(new CopyCutAction(mLayoutEditor, getClipboard(),
+ null, selected, true /* cut */));
+ manager.add(new CopyCutAction(mLayoutEditor, getClipboard(),
+ null, selected, false /* cut */));
+
+ // Can't paste with more than one element selected (the selection is the target)
+ if (selected.size() <= 1) {
+ // Paste is not valid if it would add a second element on a terminal element
+ // which parent is a document -- an XML document can only have one child. This
+ // means paste is valid if the current UI node can have children or if the
+ // parent is not a document.
+ UiElementNode ui_root = selected.get(0).getUiRoot();
+ if (ui_root.getDescriptor().hasChildren() ||
+ !(ui_root.getUiParent() instanceof UiDocumentNode)) {
+ manager.add(new PasteAction(mLayoutEditor, getClipboard(),
+ selected.get(0)));
+ }
+ }
+ manager.add(new Separator());
+ }
+ }
+
+ // Append "add" and "remove" actions. They do the same thing as the add/remove
+ // buttons on the side.
+ IconFactory factory = IconFactory.getInstance();
+
+ final UiEditorActions uiActions = mLayoutEditor.getUiEditorActions();
+
+ // "Add" makes sense only if there's 0 or 1 item selected since the
+ // one selected item becomes the target.
+ if (selected == null || selected.size() <= 1) {
+ manager.add(new Action("Add...", factory.getImageDescriptor("add")) { //$NON-NLS-2$
+ @Override
+ public void run() {
+ UiElementNode node = selected != null && selected.size() > 0 ? selected.get(0)
+ : null;
+ uiActions.doAdd(node, viewer.getControl().getShell());
+ }
+ });
+ }
+
+ if (selected != null) {
+ manager.add(new Action("Remove", factory.getImageDescriptor("delete")) { //$NON-NLS-2$
+ @Override
+ public void run() {
+ uiActions.doRemove(selected, viewer.getControl().getShell());
+ }
+ });
+
+ manager.add(new Separator());
+
+ manager.add(new Action("Up", factory.getImageDescriptor("up")) { //$NON-NLS-2$
+ @Override
+ public void run() {
+ uiActions.doUp(selected);
+ }
+ });
+ manager.add(new Action("Down", factory.getImageDescriptor("down")) { //$NON-NLS-2$
+ @Override
+ public void run() {
+ uiActions.doDown(selected);
+ }
+ });
+ }
+
+ }
+
+ /**
+ * Sets the UI for the edition of a new file.
+ * @param configuration the configuration of the new file.
+ */
+ public void editNewFile(FolderConfiguration configuration) {
+ // update the configuration UI
+ setConfiguration(configuration);
+
+ // enable the create button if the current and edited config are not equals
+ mCreateButton.setEnabled(mEditedConfig.equals(mCurrentConfig) == false);
+ }
+
+ public Rectangle getBounds() {
+ ScreenOrientation orientation = null;
+ if (mOrientation.getSelectionIndex() == 0) {
+ orientation = ScreenOrientation.PORTRAIT;
+ } else {
+ orientation = ScreenOrientation.getByIndex(
+ mOrientation.getSelectionIndex() - 1);
+ }
+
+ int s1, s2;
+
+ // get the size from the UI controls. If it fails, revert to default values.
+ try {
+ s1 = Integer.parseInt(mSize1.getText().trim());
+ } catch (NumberFormatException e) {
+ s1 = 480;
+ }
+
+ try {
+ s2 = Integer.parseInt(mSize2.getText().trim());
+ } catch (NumberFormatException e) {
+ s2 = 320;
+ }
+
+ // make sure s1 is bigger than s2
+ if (s1 < s2) {
+ int tmp = s1;
+ s1 = s2;
+ s2 = tmp;
+ }
+
+ switch (orientation) {
+ default:
+ case PORTRAIT:
+ return new Rectangle(0, 0, s2, s1);
+ case LANDSCAPE:
+ return new Rectangle(0, 0, s1, s2);
+ case SQUARE:
+ return new Rectangle(0, 0, s1, s1);
+ }
+ }
+
+ /**
+ * Renders an Android View described by a {@link ViewElementDescriptor}.
+ * <p/>This uses the <code>wrap_content</code> mode for both <code>layout_width</code> and
+ * <code>layout_height</code>, and use the class name for the <code>text</code> attribute.
+ * @param descriptor the descriptor for the class to render.
+ * @return an ImageData containing the rendering or <code>null</code> if rendering failed.
+ */
+ public ImageData renderWidget(ViewElementDescriptor descriptor) {
+ if (mEditedFile == null) {
+ return null;
+ }
+
+ IAndroidTarget target = Sdk.getCurrent().getTarget(mEditedFile.getProject());
+ if (target == null) {
+ return null;
+ }
+
+ AndroidTargetData data = Sdk.getCurrent().getTargetData(target);
+ if (data == null) {
+ return null;
+ }
+
+ LayoutBridge bridge = data.getLayoutBridge();
+
+ if (bridge.bridge != null) { // bridge can never be null.
+ ResourceManager resManager = ResourceManager.getInstance();
+
+ ProjectCallback projectCallback = null;
+ Map<String, Map<String, IResourceValue>> configuredProjectResources = null;
+ if (mEditedFile != null) {
+ ProjectResources projectRes = resManager.getProjectResources(
+ mEditedFile.getProject());
+ projectCallback = new ProjectCallback(bridge.classLoader,
+ projectRes, mEditedFile.getProject());
+
+ // get the configured resources for the project
+ // get the resources of the file's project.
+ if (mConfiguredProjectRes == null && projectRes != null) {
+ // make sure they are loaded
+ projectRes.loadAll();
+
+ // get the project resource values based on the current config
+ mConfiguredProjectRes = projectRes.getConfiguredResources(mCurrentConfig);
+ }
+
+ configuredProjectResources = mConfiguredProjectRes;
+ } else {
+ // we absolutely need a Map of configured project resources.
+ configuredProjectResources = new HashMap<String, Map<String, IResourceValue>>();
+ }
+
+ // get the framework resources
+ Map<String, Map<String, IResourceValue>> frameworkResources =
+ getConfiguredFrameworkResources();
+
+ if (configuredProjectResources != null && frameworkResources != null) {
+ // get the selected theme
+ int themeIndex = mThemeCombo.getSelectionIndex();
+ if (themeIndex != -1) {
+ String theme = mThemeCombo.getItem(themeIndex);
+
+ // change the string if it's a custom theme to make sure we can
+ // differentiate them
+ if (themeIndex >= mPlatformThemeCount) {
+ theme = "*" + theme; //$NON-NLS-1$
+ }
+
+ // Render a single object as described by the ViewElementDescriptor.
+ WidgetPullParser parser = new WidgetPullParser(descriptor);
+ ILayoutResult result = bridge.bridge.computeLayout(parser,
+ null /* projectKey */,
+ 300 /* width */, 300 /* height */, theme,
+ configuredProjectResources, frameworkResources, projectCallback,
+ null /* logger */);
+
+ // update the UiElementNode with the layout info.
+ if (result.getSuccess() == ILayoutResult.SUCCESS) {
+ BufferedImage largeImage = result.getImage();
+
+ // we need to resize it to the actual widget size, and convert it into
+ // an SWT image object.
+ int width = result.getRootView().getRight();
+ int height = result.getRootView().getBottom();
+ Raster raster = largeImage.getData(new java.awt.Rectangle(width, height));
+ int[] imageDataBuffer = ((DataBufferInt)raster.getDataBuffer()).getData();
+
+ ImageData imageData = new ImageData(width, height, 32,
+ new PaletteData(0x00FF0000, 0x0000FF00, 0x000000FF));
+
+ imageData.setPixels(0, 0, imageDataBuffer.length, imageDataBuffer, 0);
+
+ return imageData;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Reloads this editor, by getting the new model from the {@link LayoutEditor}.
+ */
+ void reloadEditor() {
+ GraphicalViewer viewer = getGraphicalViewer();
+ viewer.setContents(getModel());
+
+ IEditorInput input = mLayoutEditor.getEditorInput();
+ setInput(input);
+
+ if (input instanceof FileEditorInput) {
+ FileEditorInput fileInput = (FileEditorInput)input;
+ mEditedFile = fileInput.getFile();
+ } else {
+ // really this shouldn't happen! Log it in case it happens
+ mEditedFile = null;
+ AdtPlugin.log(IStatus.ERROR, "Input is not of type FileEditorInput: %1$s",
+ input.toString());
+ }
+ }
+
+ /**
+ * Callback for XML model changed. Only update/recompute the layout if the editor is visible
+ */
+ void onXmlModelChanged() {
+ if (mLayoutEditor.isGraphicalEditorActive()) {
+ doXmlReload(true /* force */);
+ recomputeLayout();
+ } else {
+ mNeedsXmlReload = true;
+ }
+ }
+
+ /**
+ * Actually performs the XML reload
+ * @see #onXmlModelChanged()
+ */
+ private void doXmlReload(boolean force) {
+ if (force || mNeedsXmlReload) {
+ GraphicalViewer viewer = getGraphicalViewer();
+
+ // try to preserve the selection before changing the content
+ SelectionManager selMan = viewer.getSelectionManager();
+ ISelection selection = selMan.getSelection();
+
+ try {
+ viewer.setContents(getModel());
+ } finally {
+ selMan.setSelection(selection);
+ }
+
+ mNeedsXmlReload = false;
+ }
+ }
+
+ /**
+ * Update the UI controls state with a given {@link FolderConfiguration}.
+ * <p/>If a qualifier is not present in the {@link FolderConfiguration} object, the UI control
+ * is not modified. However if the value in the control is not the default value, a warning
+ * icon is showed.
+ */
+ void setConfiguration(FolderConfiguration config) {
+ mDisableUpdates = true; // we do not want to trigger onXXXChange when setting new values in the widgets.
+
+ mEditedConfig = config;
+ mConfiguredFrameworkRes = mConfiguredProjectRes = null;
+
+ mCountryIcon.setImage(mMatchImage);
+ CountryCodeQualifier countryQualifier = config.getCountryCodeQualifier();
+ if (countryQualifier != null) {
+ mCountry.setText(String.format("%1$d", countryQualifier.getCode()));
+ mCurrentConfig.setCountryCodeQualifier(countryQualifier);
+ } else if (mCountry.getText().length() > 0) {
+ mCountryIcon.setImage(mWarningImage);
+ }
+
+ mNetworkIcon.setImage(mMatchImage);
+ NetworkCodeQualifier networkQualifier = config.getNetworkCodeQualifier();
+ if (networkQualifier != null) {
+ mNetwork.setText(String.format("%1$d", networkQualifier.getCode()));
+ mCurrentConfig.setNetworkCodeQualifier(networkQualifier);
+ } else if (mNetwork.getText().length() > 0) {
+ mNetworkIcon.setImage(mWarningImage);
+ }
+
+ mLanguageIcon.setImage(mMatchImage);
+ LanguageQualifier languageQualifier = config.getLanguageQualifier();
+ if (languageQualifier != null) {
+ mLanguage.setText(languageQualifier.getValue());
+ mCurrentConfig.setLanguageQualifier(languageQualifier);
+ } else if (mLanguage.getText().length() > 0) {
+ mLanguageIcon.setImage(mWarningImage);
+ }
+
+ mRegionIcon.setImage(mMatchImage);
+ RegionQualifier regionQualifier = config.getRegionQualifier();
+ if (regionQualifier != null) {
+ mRegion.setText(regionQualifier.getValue());
+ mCurrentConfig.setRegionQualifier(regionQualifier);
+ } else if (mRegion.getText().length() > 0) {
+ mRegionIcon.setImage(mWarningImage);
+ }
+
+ mOrientationIcon.setImage(mMatchImage);
+ ScreenOrientationQualifier orientationQualifier = config.getScreenOrientationQualifier();
+ if (orientationQualifier != null) {
+ mOrientation.select(
+ ScreenOrientation.getIndex(orientationQualifier.getValue()) + 1);
+ mCurrentConfig.setScreenOrientationQualifier(orientationQualifier);
+ } else if (mOrientation.getSelectionIndex() != 0) {
+ mOrientationIcon.setImage(mWarningImage);
+ }
+
+ mDensityIcon.setImage(mMatchImage);
+ PixelDensityQualifier densityQualifier = config.getPixelDensityQualifier();
+ if (densityQualifier != null) {
+ mDensity.setText(String.format("%1$d", densityQualifier.getValue()));
+ mCurrentConfig.setPixelDensityQualifier(densityQualifier);
+ } else if (mDensity.getText().length() > 0) {
+ mDensityIcon.setImage(mWarningImage);
+ }
+
+ mTouchIcon.setImage(mMatchImage);
+ TouchScreenQualifier touchQualifier = config.getTouchTypeQualifier();
+ if (touchQualifier != null) {
+ mTouch.select(TouchScreenType.getIndex(touchQualifier.getValue()) + 1);
+ mCurrentConfig.setTouchTypeQualifier(touchQualifier);
+ } else if (mTouch.getSelectionIndex() != 0) {
+ mTouchIcon.setImage(mWarningImage);
+ }
+
+ mKeyboardIcon.setImage(mMatchImage);
+ KeyboardStateQualifier keyboardQualifier = config.getKeyboardStateQualifier();
+ if (keyboardQualifier != null) {
+ mKeyboard.select(KeyboardState.getIndex(keyboardQualifier.getValue()) + 1);
+ mCurrentConfig.setKeyboardStateQualifier(keyboardQualifier);
+ } else if (mKeyboard.getSelectionIndex() != 0) {
+ mKeyboardIcon.setImage(mWarningImage);
+ }
+
+ mTextInputIcon.setImage(mMatchImage);
+ TextInputMethodQualifier inputQualifier = config.getTextInputMethodQualifier();
+ if (inputQualifier != null) {
+ mTextInput.select(TextInputMethod.getIndex(inputQualifier.getValue()) + 1);
+ mCurrentConfig.setTextInputMethodQualifier(inputQualifier);
+ } else if (mTextInput.getSelectionIndex() != 0) {
+ mTextInputIcon.setImage(mWarningImage);
+ }
+
+ mNavigationIcon.setImage(mMatchImage);
+ NavigationMethodQualifier navigationQualifiter = config.getNavigationMethodQualifier();
+ if (navigationQualifiter != null) {
+ mNavigation.select(
+ NavigationMethod.getIndex(navigationQualifiter.getValue()) + 1);
+ mCurrentConfig.setNavigationMethodQualifier(navigationQualifiter);
+ } else if (mNavigation.getSelectionIndex() != 0) {
+ mNavigationIcon.setImage(mWarningImage);
+ }
+
+ mSizeIcon.setImage(mMatchImage);
+ ScreenDimensionQualifier sizeQualifier = config.getScreenDimensionQualifier();
+ if (sizeQualifier != null) {
+ mSize1.setText(String.format("%1$d", sizeQualifier.getValue1()));
+ mSize2.setText(String.format("%1$d", sizeQualifier.getValue2()));
+ mCurrentConfig.setScreenDimensionQualifier(sizeQualifier);
+ } else if (mSize1.getText().length() > 0 && mSize2.getText().length() > 0) {
+ mSizeIcon.setImage(mWarningImage);
+ }
+
+ // update the string showing the folder name
+ String current = config.toDisplayString();
+ mCurrentLayoutLabel.setText(current != null ? current : "(Default)");
+
+ mDisableUpdates = false;
+ }
+
+ /**
+ * Displays an error icon in front of all the non-null qualifiers.
+ */
+ void displayConfigError() {
+ mCountryIcon.setImage(mMatchImage);
+ CountryCodeQualifier countryQualifier = mCurrentConfig.getCountryCodeQualifier();
+ if (countryQualifier != null) {
+ mCountryIcon.setImage(mErrorImage);
+ }
+
+ mNetworkIcon.setImage(mMatchImage);
+ NetworkCodeQualifier networkQualifier = mCurrentConfig.getNetworkCodeQualifier();
+ if (networkQualifier != null) {
+ mNetworkIcon.setImage(mErrorImage);
+ }
+
+ mLanguageIcon.setImage(mMatchImage);
+ LanguageQualifier languageQualifier = mCurrentConfig.getLanguageQualifier();
+ if (languageQualifier != null) {
+ mLanguageIcon.setImage(mErrorImage);
+ }
+
+ mRegionIcon.setImage(mMatchImage);
+ RegionQualifier regionQualifier = mCurrentConfig.getRegionQualifier();
+ if (regionQualifier != null) {
+ mRegionIcon.setImage(mErrorImage);
+ }
+
+ mOrientationIcon.setImage(mMatchImage);
+ ScreenOrientationQualifier orientationQualifier =
+ mCurrentConfig.getScreenOrientationQualifier();
+ if (orientationQualifier != null) {
+ mOrientationIcon.setImage(mErrorImage);
+ }
+
+ mDensityIcon.setImage(mMatchImage);
+ PixelDensityQualifier densityQualifier = mCurrentConfig.getPixelDensityQualifier();
+ if (densityQualifier != null) {
+ mDensityIcon.setImage(mErrorImage);
+ }
+
+ mTouchIcon.setImage(mMatchImage);
+ TouchScreenQualifier touchQualifier = mCurrentConfig.getTouchTypeQualifier();
+ if (touchQualifier != null) {
+ mTouchIcon.setImage(mErrorImage);
+ }
+
+ mKeyboardIcon.setImage(mMatchImage);
+ KeyboardStateQualifier keyboardQualifier = mCurrentConfig.getKeyboardStateQualifier();
+ if (keyboardQualifier != null) {
+ mKeyboardIcon.setImage(mErrorImage);
+ }
+
+ mTextInputIcon.setImage(mMatchImage);
+ TextInputMethodQualifier inputQualifier = mCurrentConfig.getTextInputMethodQualifier();
+ if (inputQualifier != null) {
+ mTextInputIcon.setImage(mErrorImage);
+ }
+
+ mNavigationIcon.setImage(mMatchImage);
+ NavigationMethodQualifier navigationQualifiter =
+ mCurrentConfig.getNavigationMethodQualifier();
+ if (navigationQualifiter != null) {
+ mNavigationIcon.setImage(mErrorImage);
+ }
+
+ mSizeIcon.setImage(mMatchImage);
+ ScreenDimensionQualifier sizeQualifier = mCurrentConfig.getScreenDimensionQualifier();
+ if (sizeQualifier != null) {
+ mSizeIcon.setImage(mErrorImage);
+ }
+
+ // update the string showing the folder name
+ String current = mCurrentConfig.toDisplayString();
+ mCurrentLayoutLabel.setText(current != null ? current : "(Default)");
+ }
+
+ UiDocumentNode getModel() {
+ return mLayoutEditor.getUiRootNode();
+ }
+
+ void reloadPalette() {
+ PaletteFactory.createPaletteRoot(mPaletteRoot, mLayoutEditor.getTargetData());
+ }
+
+ private void onCountryCodeChange() {
+ // because mCountry triggers onCountryCodeChange at each modification, calling setText()
+ // will trigger notifications, and we don't want that.
+ if (mDisableUpdates == true) {
+ return;
+ }
+
+ // update the current config
+ String value = mCountry.getText();
+
+ // empty string, means no qualifier.
+ if (value.length() == 0) {
+ mCurrentConfig.setCountryCodeQualifier(null);
+ } else {
+ try {
+ CountryCodeQualifier qualifier = CountryCodeQualifier.getQualifier(
+ CountryCodeQualifier.getFolderSegment(Integer.parseInt(value)));
+ if (qualifier != null) {
+ mCurrentConfig.setCountryCodeQualifier(qualifier);
+ } else {
+ // Failure! Looks like the value is wrong (for instance a one letter string).
+ // We do nothing in this case.
+ mCountryIcon.setImage(mErrorImage);
+ return;
+ }
+ } catch (NumberFormatException e) {
+ // Looks like the code is not a number. This should not happen since the text
+ // field has a VerifyListener that prevents it.
+ mCurrentConfig.setCountryCodeQualifier(null);
+ mCountryIcon.setImage(mErrorImage);
+ }
+ }
+
+ // look for a file to open/create
+ onConfigurationChange();
+ }
+
+ private void onNetworkCodeChange() {
+ // because mNetwork triggers onNetworkCodeChange at each modification, calling setText()
+ // will trigger notifications, and we don't want that.
+ if (mDisableUpdates == true) {
+ return;
+ }
+
+ // update the current config
+ String value = mNetwork.getText();
+
+ // empty string, means no qualifier.
+ if (value.length() == 0) {
+ mCurrentConfig.setNetworkCodeQualifier(null);
+ } else {
+ try {
+ NetworkCodeQualifier qualifier = NetworkCodeQualifier.getQualifier(
+ NetworkCodeQualifier.getFolderSegment(Integer.parseInt(value)));
+ if (qualifier != null) {
+ mCurrentConfig.setNetworkCodeQualifier(qualifier);
+ } else {
+ // Failure! Looks like the value is wrong (for instance a one letter string).
+ // We do nothing in this case.
+ mNetworkIcon.setImage(mErrorImage);
+ return;
+ }
+ } catch (NumberFormatException e) {
+ // Looks like the code is not a number. This should not happen since the text
+ // field has a VerifyListener that prevents it.
+ mCurrentConfig.setNetworkCodeQualifier(null);
+ mNetworkIcon.setImage(mErrorImage);
+ }
+ }
+
+ // look for a file to open/create
+ onConfigurationChange();
+ }
+
+ /**
+ * Call back for language combo selection
+ */
+ private void onLanguageChange() {
+ // because mLanguage triggers onLanguageChange at each modification, the filling
+ // of the combo with data will trigger notifications, and we don't want that.
+ if (mDisableUpdates == true) {
+ return;
+ }
+
+ // update the current config
+ String value = mLanguage.getText();
+
+ updateRegionUi(null /* projectResources */, null /* frameworkResources */);
+
+ // empty string, means no qualifier.
+ if (value.length() == 0) {
+ mCurrentConfig.setLanguageQualifier(null);
+ } else {
+ LanguageQualifier qualifier = null;
+ String segment = LanguageQualifier.getFolderSegment(value);
+ if (segment != null) {
+ qualifier = LanguageQualifier.getQualifier(segment);
+ }
+
+ if (qualifier != null) {
+ mCurrentConfig.setLanguageQualifier(qualifier);
+ } else {
+ // Failure! Looks like the value is wrong (for instance a one letter string).
+ mCurrentConfig.setLanguageQualifier(null);
+ mLanguageIcon.setImage(mErrorImage);
+ }
+ }
+
+ // look for a file to open/create
+ onConfigurationChange();
+ }
+
+ private void onRegionChange() {
+ // because mRegion triggers onRegionChange at each modification, the filling
+ // of the combo with data will trigger notifications, and we don't want that.
+ if (mDisableUpdates == true) {
+ return;
+ }
+
+ // update the current config
+ String value = mRegion.getText();
+
+ // empty string, means no qualifier.
+ if (value.length() == 0) {
+ mCurrentConfig.setRegionQualifier(null);
+ } else {
+ RegionQualifier qualifier = null;
+ String segment = RegionQualifier.getFolderSegment(value);
+ if (segment != null) {
+ qualifier = RegionQualifier.getQualifier(segment);
+ }
+
+ if (qualifier != null) {
+ mCurrentConfig.setRegionQualifier(qualifier);
+ } else {
+ // Failure! Looks like the value is wrong (for instance a one letter string).
+ mCurrentConfig.setRegionQualifier(null);
+ mRegionIcon.setImage(mErrorImage);
+ }
+ }
+
+ // look for a file to open/create
+ onConfigurationChange();
+ }
+
+ private void onOrientationChange() {
+ // update the current config
+ int index = mOrientation.getSelectionIndex();
+ if (index != 0) {
+ mCurrentConfig.setScreenOrientationQualifier(new ScreenOrientationQualifier(
+ ScreenOrientation.getByIndex(index-1)));
+ } else {
+ mCurrentConfig.setScreenOrientationQualifier(null);
+ }
+
+ // look for a file to open/create
+ onConfigurationChange();
+ }
+
+ private void onDensityChange() {
+ // because mDensity triggers onDensityChange at each modification, calling setText()
+ // will trigger notifications, and we don't want that.
+ if (mDisableUpdates == true) {
+ return;
+ }
+
+ // update the current config
+ String value = mDensity.getText();
+
+ // empty string, means no qualifier.
+ if (value.length() == 0) {
+ mCurrentConfig.setPixelDensityQualifier(null);
+ } else {
+ try {
+ PixelDensityQualifier qualifier = PixelDensityQualifier.getQualifier(
+ PixelDensityQualifier.getFolderSegment(Integer.parseInt(value)));
+ if (qualifier != null) {
+ mCurrentConfig.setPixelDensityQualifier(qualifier);
+ } else {
+ // Failure! Looks like the value is wrong (for instance a one letter string).
+ // We do nothing in this case.
+ return;
+ }
+ } catch (NumberFormatException e) {
+ // Looks like the code is not a number. This should not happen since the text
+ // field has a VerifyListener that prevents it.
+ // We do nothing in this case.
+ mDensityIcon.setImage(mErrorImage);
+ return;
+ }
+ }
+
+ // look for a file to open/create
+ onConfigurationChange();
+ }
+
+ private void onTouchChange() {
+ // update the current config
+ int index = mTouch.getSelectionIndex();
+ if (index != 0) {
+ mCurrentConfig.setTouchTypeQualifier(new TouchScreenQualifier(
+ TouchScreenType.getByIndex(index-1)));
+ } else {
+ mCurrentConfig.setTouchTypeQualifier(null);
+ }
+
+ // look for a file to open/create
+ onConfigurationChange();
+ }
+
+ private void onKeyboardChange() {
+ // update the current config
+ int index = mKeyboard.getSelectionIndex();
+ if (index != 0) {
+ mCurrentConfig.setKeyboardStateQualifier(new KeyboardStateQualifier(
+ KeyboardState.getByIndex(index-1)));
+ } else {
+ mCurrentConfig.setKeyboardStateQualifier(null);
+ }
+
+ // look for a file to open/create
+ onConfigurationChange();
+ }
+
+ private void onTextInputChange() {
+ // update the current config
+ int index = mTextInput.getSelectionIndex();
+ if (index != 0) {
+ mCurrentConfig.setTextInputMethodQualifier(new TextInputMethodQualifier(
+ TextInputMethod.getByIndex(index-1)));
+ } else {
+ mCurrentConfig.setTextInputMethodQualifier(null);
+ }
+
+ // look for a file to open/create
+ onConfigurationChange();
+ }
+
+ private void onNavigationChange() {
+ // update the current config
+ int index = mNavigation.getSelectionIndex();
+ if (index != 0) {
+ mCurrentConfig.setNavigationMethodQualifier(new NavigationMethodQualifier(
+ NavigationMethod.getByIndex(index-1)));
+ } else {
+ mCurrentConfig.setNavigationMethodQualifier(null);
+ }
+
+ // look for a file to open/create
+ onConfigurationChange();
+ }
+
+ private void onSizeChange() {
+ // because mSize1 and mSize2 trigger onSizeChange at each modification, calling setText()
+ // will trigger notifications, and we don't want that.
+ if (mDisableUpdates == true) {
+ return;
+ }
+
+ // update the current config
+ String size1 = mSize1.getText();
+ String size2 = mSize2.getText();
+
+ // if only one of the strings is empty, do nothing
+ if ((size1.length() == 0) ^ (size2.length() == 0)) {
+ mSizeIcon.setImage(mErrorImage);
+ return;
+ } else if (size1.length() == 0 && size2.length() == 0) {
+ // both sizes are empty: remove the qualifier.
+ mCurrentConfig.setScreenDimensionQualifier(null);
+ } else {
+ ScreenDimensionQualifier qualifier = ScreenDimensionQualifier.getQualifier(size1,
+ size2);
+
+ if (qualifier != null) {
+ mCurrentConfig.setScreenDimensionQualifier(qualifier);
+ } else {
+ // Failure! Looks like the value is wrong.
+ // we do nothing in this case.
+ return;
+ }
+ }
+
+ // look for a file to open/create
+ onConfigurationChange();
+ }
+
+
+ /**
+ * Looks for a file matching the new {@link FolderConfiguration} and attempts to open it.
+ * <p/>If there is no match, notify the user.
+ */
+ private void onConfigurationChange() {
+ mConfiguredFrameworkRes = mConfiguredProjectRes = null;
+
+ if (mEditedFile == null || mEditedConfig == null) {
+ return;
+ }
+
+ // get the resources of the file's project.
+ ProjectResources resources = ResourceManager.getInstance().getProjectResources(
+ mEditedFile.getProject());
+
+ // from the resources, look for a matching file
+ ResourceFile match = null;
+ if (resources != null) {
+ match = resources.getMatchingFile(mEditedFile.getName(),
+ ResourceFolderType.LAYOUT,
+ mCurrentConfig);
+ }
+
+ if (match != null) {
+ if (match.getFile().equals(mEditedFile) == false) {
+ try {
+ IDE.openEditor(
+ getSite().getWorkbenchWindow().getActivePage(),
+ match.getFile().getIFile());
+
+ // we're done!
+ return;
+ } catch (PartInitException e) {
+ // FIXME: do something!
+ }
+ }
+
+ // at this point, we have not opened a new file.
+
+ // update the configuration icons with the new edited config.
+ setConfiguration(mEditedConfig);
+
+ // enable the create button if the current and edited config are not equals
+ mCreateButton.setEnabled(mEditedConfig.equals(mCurrentConfig) == false);
+
+ // Even though the layout doesn't change, the config changed, and referenced
+ // resources need to be updated.
+ recomputeLayout();
+ } else {
+ // update the configuration icons with the new edited config.
+ displayConfigError();
+
+ // enable the Create button
+ mCreateButton.setEnabled(true);
+
+ // display the error.
+ String message = String.format(
+ "No resources match the configuration\n \n\t%1$s\n \nChange the configuration or create:\n \n\tres/%2$s/%3$s\n \nYou can also click the 'Create' button above.",
+ mCurrentConfig.toDisplayString(),
+ mCurrentConfig.getFolderName(ResourceFolderType.LAYOUT),
+ mEditedFile.getName());
+ showErrorInEditor(message);
+ }
+ }
+
+ private void onThemeChange() {
+ int themeIndex = mThemeCombo.getSelectionIndex();
+ if (themeIndex != -1) {
+ String theme = mThemeCombo.getItem(themeIndex);
+
+ if (theme.equals(THEME_SEPARATOR)) {
+ mThemeCombo.select(0);
+ }
+
+ recomputeLayout();
+ }
+ }
+
+ /**
+ * Creates a composite with no margin/spacing, and puts a {@link Label} in it with the matching
+ * icon.
+ * @param parent the parent to receive the composite
+ * @return the created {@link Label} object.
+ */
+ private Label createControlComposite(Composite parent, boolean grab) {
+ GridLayout gl;
+
+ Composite composite = new Composite(parent, SWT.NONE);
+ composite.setLayout(gl = new GridLayout(2, false));
+ gl.marginHeight = gl.marginWidth = 0;
+ gl.horizontalSpacing = 0;
+ if (grab) {
+ composite.setLayoutData(
+ new GridData(GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
+ }
+
+ // create the label
+ Label icon = new Label(composite, SWT.NONE);
+ icon.setImage(mMatchImage);
+
+ return icon;
+ }
+
+ /**
+ * Recomputes the layout with the help of layoutlib.
+ */
+ @SuppressWarnings("deprecation")
+ void recomputeLayout() {
+ doXmlReload(false /* force */);
+ try {
+ // check that the resource exists. If the file is opened but the project is closed
+ // or deleted for some reason (changed from outside of eclipse), then this will
+ // return false;
+ if (mEditedFile.exists() == false) {
+ String message = String.format("Resource '%1$s' does not exist.",
+ mEditedFile.getFullPath().toString());
+
+ showErrorInEditor(message);
+
+ return;
+ }
+
+ IProject iProject = mEditedFile.getProject();
+
+ if (mEditedFile.isSynchronized(IResource.DEPTH_ZERO) == false) {
+ String message = String.format("%1$s is out of sync. Please refresh.",
+ mEditedFile.getName());
+
+ showErrorInEditor(message);
+
+ // also print it in the error console.
+ AdtPlugin.printErrorToConsole(iProject.getName(), message);
+ return;
+ }
+
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject());
+ if (target == null) {
+ showErrorInEditor("The project target is not set.");
+ return;
+ }
+
+ AndroidTargetData data = currentSdk.getTargetData(target);
+ if (data == null) {
+ // It can happen that the workspace refreshes while the SDK is loading its
+ // data, which could trigger a redraw of the opened layout if some resources
+ // changed while Eclipse is closed.
+ // In this case data could be null, but this is not an error.
+ // We can just silently return, as all the opened editors are automatically
+ // refreshed once the SDK finishes loading.
+ if (AdtPlugin.getDefault().getSdkLoadStatus() != LoadStatus.LOADING) {
+ showErrorInEditor(String.format(
+ "The project target (%s) was not properly loaded.",
+ target.getName()));
+ }
+ return;
+ }
+
+ // check there is actually a model (maybe the file is empty).
+ UiDocumentNode model = getModel();
+
+ if (model.getUiChildren().size() == 0) {
+ showErrorInEditor("No Xml content. Go to the Outline view and add nodes.");
+ return;
+ }
+
+ LayoutBridge bridge = data.getLayoutBridge();
+
+ if (bridge.bridge != null) { // bridge can never be null.
+ ResourceManager resManager = ResourceManager.getInstance();
+
+ ProjectResources projectRes = resManager.getProjectResources(iProject);
+ if (projectRes == null) {
+ return;
+ }
+
+ // get the resources of the file's project.
+ if (mConfiguredProjectRes == null) {
+ // make sure they are loaded
+ projectRes.loadAll();
+
+ // get the project resource values based on the current config
+ mConfiguredProjectRes = projectRes.getConfiguredResources(mCurrentConfig);
+ }
+
+ // get the framework resources
+ Map<String, Map<String, IResourceValue>> frameworkResources =
+ getConfiguredFrameworkResources();
+
+ if (mConfiguredProjectRes != null && frameworkResources != null) {
+ if (mProjectCallback == null) {
+ mProjectCallback = new ProjectCallback(
+ bridge.classLoader, projectRes, iProject);
+ }
+
+ if (mLogger == null) {
+ mLogger = new ILayoutLog() {
+ public void error(String message) {
+ AdtPlugin.printErrorToConsole(mEditedFile.getName(), message);
+ }
+
+ public void error(Throwable error) {
+ String message = error.getMessage();
+ if (message == null) {
+ message = error.getClass().getName();
+ }
+
+ PrintStream ps = new PrintStream(AdtPlugin.getErrorStream());
+ error.printStackTrace(ps);
+ }
+
+ public void warning(String message) {
+ AdtPlugin.printToConsole(mEditedFile.getName(), message);
+ }
+ };
+ }
+
+ // get the selected theme
+ int themeIndex = mThemeCombo.getSelectionIndex();
+ if (themeIndex != -1) {
+ String theme = mThemeCombo.getItem(themeIndex);
+
+ // Compute the layout
+ UiElementPullParser parser = new UiElementPullParser(getModel());
+ Rectangle rect = getBounds();
+ ILayoutResult result = null;
+ if (bridge.apiLevel >= 3) {
+ // call the new api with proper theme differentiator and
+ // density/dpi support.
+ boolean isProjectTheme = themeIndex >= mPlatformThemeCount;
+
+ // FIXME pass the density/dpi from somewhere (resource config or skin).
+ result = bridge.bridge.computeLayout(parser,
+ iProject /* projectKey */,
+ rect.width, rect.height, 160, 160.f, 160.f,
+ theme, isProjectTheme,
+ mConfiguredProjectRes, frameworkResources, mProjectCallback,
+ mLogger);
+ } else if (bridge.apiLevel == 2) {
+ // api with boolean for separation of project/framework theme
+ boolean isProjectTheme = themeIndex >= mPlatformThemeCount;
+
+ result = bridge.bridge.computeLayout(parser,
+ iProject /* projectKey */,
+ rect.width, rect.height, theme, isProjectTheme,
+ mConfiguredProjectRes, frameworkResources, mProjectCallback,
+ mLogger);
+ } else {
+ // oldest api with no density/dpi, and project theme boolean mixed
+ // into the theme name.
+
+ // change the string if it's a custom theme to make sure we can
+ // differentiate them
+ if (themeIndex >= mPlatformThemeCount) {
+ theme = "*" + theme; //$NON-NLS-1$
+ }
+
+ result = bridge.bridge.computeLayout(parser,
+ iProject /* projectKey */,
+ rect.width, rect.height, theme,
+ mConfiguredProjectRes, frameworkResources, mProjectCallback,
+ mLogger);
+ }
+
+ // update the UiElementNode with the layout info.
+ if (result.getSuccess() == ILayoutResult.SUCCESS) {
+ model.setEditData(result.getImage());
+
+ updateNodeWithBounds(result.getRootView());
+ } else {
+ String message = result.getErrorMessage();
+
+ // Reset the edit data for all the nodes.
+ resetNodeBounds(model);
+
+ if (message != null) {
+ // set the error in the top element.
+ model.setEditData(message);
+ }
+ }
+
+ model.refreshUi();
+ }
+ }
+ } else {
+ // SDK is loaded but not the layout library!
+ String message = null;
+ // check whether the bridge managed to load, or not
+ if (bridge.status == LoadStatus.LOADING) {
+ message = String.format(
+ "Eclipse is loading framework information and the Layout library from the SDK folder.\n%1$s will refresh automatically once the process is finished.",
+ mEditedFile.getName());
+ } else {
+ message = String.format("Eclipse failed to load the framework information and the Layout library!");
+ }
+ showErrorInEditor(message);
+ }
+ } else {
+ String message = String.format(
+ "Eclipse is loading the SDK.\n%1$s will refresh automatically once the process is finished.",
+ mEditedFile.getName());
+
+ showErrorInEditor(message);
+ }
+ } finally {
+ // no matter the result, we are done doing the recompute based on the latest
+ // resource/code change.
+ mNeedsRecompute = false;
+ }
+ }
+
+ private void showErrorInEditor(String message) {
+ // get the model to display the error directly in the editor
+ UiDocumentNode model = getModel();
+
+ // Reset the edit data for all the nodes.
+ resetNodeBounds(model);
+
+ if (message != null) {
+ // set the error in the top element.
+ model.setEditData(message);
+ }
+
+ model.refreshUi();
+ }
+
+ private void resetNodeBounds(UiElementNode node) {
+ node.setEditData(null);
+
+ List<UiElementNode> children = node.getUiChildren();
+ for (UiElementNode child : children) {
+ resetNodeBounds(child);
+ }
+ }
+
+ private void updateNodeWithBounds(ILayoutViewInfo r) {
+ if (r != null) {
+ // update the node itself, as the viewKey is the XML node in this implementation.
+ Object viewKey = r.getViewKey();
+ if (viewKey instanceof UiElementNode) {
+ Rectangle bounds = new Rectangle(r.getLeft(), r.getTop(),
+ r.getRight()-r.getLeft(), r.getBottom() - r.getTop());
+
+ ((UiElementNode)viewKey).setEditData(bounds);
+ }
+
+ // and then its children.
+ ILayoutViewInfo[] children = r.getChildren();
+ if (children != null) {
+ for (ILayoutViewInfo child : children) {
+ updateNodeWithBounds(child);
+ }
+ }
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ide.eclipse.editors.layout.LayoutReloadMonitor.ILayoutReloadListener#reloadLayout(boolean, boolean, boolean)
+ *
+ * Called when the file changes triggered a redraw of the layout
+ */
+ public void reloadLayout(boolean codeChange, boolean rChange, boolean resChange) {
+ boolean recompute = rChange;
+
+ if (resChange) {
+ recompute = true;
+
+ // TODO: differentiate between single and multi resource file changed, and whether the resource change affects the cache.
+
+ // force a reparse in case a value XML file changed.
+ mConfiguredProjectRes = null;
+
+ // clear the cache in the bridge in case a bitmap/9-patch changed.
+ IAndroidTarget target = Sdk.getCurrent().getTarget(mEditedFile.getProject());
+ if (target != null) {
+
+ AndroidTargetData data = Sdk.getCurrent().getTargetData(target);
+ if (data != null) {
+ LayoutBridge bridge = data.getLayoutBridge();
+
+ if (bridge.bridge != null) {
+ bridge.bridge.clearCaches(mEditedFile.getProject());
+ }
+ }
+ }
+
+ mParent.getDisplay().asyncExec(mUiUpdateFromResourcesRunnable);
+ }
+
+ if (codeChange) {
+ // only recompute if the custom view loader was used to load some code.
+ if (mProjectCallback != null && mProjectCallback.isUsed()) {
+ mProjectCallback = null;
+ recompute = true;
+ }
+ }
+
+ if (recompute) {
+ mParent.getDisplay().asyncExec(mConditionalRecomputeRunnable);
+ }
+ }
+
+ /**
+ * Responds to a page change that made the Graphical editor page the activated page.
+ */
+ void activated() {
+ if (mNeedsRecompute || mNeedsXmlReload) {
+ recomputeLayout();
+ }
+ }
+
+ /**
+ * Responds to a page change that made the Graphical editor page the deactivated page
+ */
+ void deactivated() {
+ // nothing to be done here for now.
+ }
+
+ /**
+ * Updates the UI from values in the resources, such as languages, regions, themes, etc...
+ * This must be called from the UI thread.
+ */
+ private void updateUIFromResources() {
+
+ ResourceManager manager = ResourceManager.getInstance();
+
+ ProjectResources frameworkProject = getFrameworkResources();
+
+ mDisableUpdates = true;
+
+ // Reset stuff
+ int selection = mThemeCombo.getSelectionIndex();
+ mThemeCombo.removeAll();
+ mPlatformThemeCount = 0;
+ mLanguage.removeAll();
+
+ Set<String> languages = new HashSet<String>();
+ ArrayList<String> themes = new ArrayList<String>();
+
+ // get the themes, and languages from the Framework.
+ if (frameworkProject != null) {
+ // get the configured resources for the framework
+ Map<String, Map<String, IResourceValue>> frameworResources =
+ getConfiguredFrameworkResources();
+
+ if (frameworResources != null) {
+ // get the styles.
+ Map<String, IResourceValue> styles = frameworResources.get(
+ ResourceType.STYLE.getName());
+
+
+ // collect the themes out of all the styles.
+ for (IResourceValue value : styles.values()) {
+ String name = value.getName();
+ if (name.startsWith("Theme.") || name.equals("Theme")) {
+ themes.add(value.getName());
+ mPlatformThemeCount++;
+ }
+ }
+
+ // sort them and add them to the combo
+ Collections.sort(themes);
+
+ for (String theme : themes) {
+ mThemeCombo.add(theme);
+ }
+
+ mPlatformThemeCount = themes.size();
+ themes.clear();
+ }
+ // now get the languages from the framework.
+ Set<String> frameworkLanguages = frameworkProject.getLanguages();
+ if (frameworkLanguages != null) {
+ languages.addAll(frameworkLanguages);
+ }
+ }
+
+ // now get the themes and languages from the project.
+ ProjectResources project = null;
+ if (mEditedFile != null) {
+ project = manager.getProjectResources(mEditedFile.getProject());
+
+ // in cases where the opened file is not linked to a project, this could be null.
+ if (project != null) {
+ // get the configured resources for the project
+ if (mConfiguredProjectRes == null) {
+ // make sure they are loaded
+ project.loadAll();
+
+ // get the project resource values based on the current config
+ mConfiguredProjectRes = project.getConfiguredResources(mCurrentConfig);
+ }
+
+ if (mConfiguredProjectRes != null) {
+ // get the styles.
+ Map<String, IResourceValue> styleMap = mConfiguredProjectRes.get(
+ ResourceType.STYLE.getName());
+
+ if (styleMap != null) {
+ // collect the themes out of all the styles, ie styles that extend,
+ // directly or indirectly a platform theme.
+ for (IResourceValue value : styleMap.values()) {
+ if (isTheme(value, styleMap)) {
+ themes.add(value.getName());
+ }
+ }
+
+ // sort them and add them the to the combo.
+ if (mPlatformThemeCount > 0 && themes.size() > 0) {
+ mThemeCombo.add(THEME_SEPARATOR);
+ }
+
+ Collections.sort(themes);
+
+ for (String theme : themes) {
+ mThemeCombo.add(theme);
+ }
+ }
+ }
+
+ // now get the languages from the project.
+ Set<String> projectLanguages = project.getLanguages();
+ if (projectLanguages != null) {
+ languages.addAll(projectLanguages);
+ }
+ }
+ }
+
+ // add the languages to the Combo
+ for (String language : languages) {
+ mLanguage.add(language);
+ }
+
+ mDisableUpdates = false;
+
+ // and update the Region UI based on the current language
+ updateRegionUi(project, frameworkProject);
+
+ // handle default selection of themes
+ if (mThemeCombo.getItemCount() > 0) {
+ mThemeCombo.setEnabled(true);
+ if (selection == -1) {
+ selection = 0;
+ }
+
+ if (mThemeCombo.getItemCount() <= selection) {
+ mThemeCombo.select(0);
+ } else {
+ mThemeCombo.select(selection);
+ }
+ } else {
+ mThemeCombo.setEnabled(false);
+ }
+ }
+
+ /**
+ * Returns whether the given <var>style</var> is a theme.
+ * This is done by making sure the parent is a theme.
+ * @param value the style to check
+ * @param styleMap the map of styles for the current project. Key is the style name.
+ * @return True if the given <var>style</var> is a theme.
+ */
+ private boolean isTheme(IResourceValue value, Map<String, IResourceValue> styleMap) {
+ if (value instanceof IStyleResourceValue) {
+ IStyleResourceValue style = (IStyleResourceValue)value;
+
+ boolean frameworkStyle = false;
+ String parentStyle = style.getParentStyle();
+ if (parentStyle == null) {
+ // if there is no specified parent style we look an implied one.
+ // For instance 'Theme.light' is implied child style of 'Theme',
+ // and 'Theme.light.fullscreen' is implied child style of 'Theme.light'
+ String name = style.getName();
+ int index = name.lastIndexOf('.');
+ if (index != -1) {
+ parentStyle = name.substring(0, index);
+ }
+ } else {
+ // remove the useless @ if it's there
+ if (parentStyle.startsWith("@")) {
+ parentStyle = parentStyle.substring(1);
+ }
+
+ // check for framework identifier.
+ if (parentStyle.startsWith("android:")) {
+ frameworkStyle = true;
+ parentStyle = parentStyle.substring("android:".length());
+ }
+
+ // at this point we could have the format style/<name>. we want only the name
+ if (parentStyle.startsWith("style/")) {
+ parentStyle = parentStyle.substring("style/".length());
+ }
+ }
+
+ if (frameworkStyle) {
+ // if the parent is a framework style, it has to be 'Theme' or 'Theme.*'
+ return parentStyle.equals("Theme") || parentStyle.startsWith("Theme.");
+ } else {
+ // if it's a project style, we check this is a theme.
+ value = styleMap.get(parentStyle);
+ if (value != null) {
+ return isTheme(value, styleMap);
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Update the Region UI widget based on the current language selection
+ * @param projectResources the project resources or {@code null}.
+ * @param frameworkResources the framework resource or {@code null}
+ */
+ private void updateRegionUi(ProjectResources projectResources,
+ ProjectResources frameworkResources) {
+ if (projectResources == null && mEditedFile != null) {
+ projectResources = ResourceManager.getInstance().getProjectResources(
+ mEditedFile.getProject());
+ }
+
+ if (frameworkResources == null) {
+ frameworkResources = getFrameworkResources();
+ }
+
+ String currentLanguage = mLanguage.getText();
+
+ Set<String> set = null;
+
+ if (projectResources != null) {
+ set = projectResources.getRegions(currentLanguage);
+ }
+
+ if (frameworkResources != null) {
+ if (set != null) {
+ Set<String> set2 = frameworkResources.getRegions(currentLanguage);
+ set.addAll(set2);
+ } else {
+ set = frameworkResources.getRegions(currentLanguage);
+ }
+ }
+
+ if (set != null) {
+ mDisableUpdates = true;
+
+ mRegion.removeAll();
+ for (String region : set) {
+ mRegion.add(region);
+ }
+
+ mDisableUpdates = false;
+ }
+ }
+
+ private Map<String, Map<String, IResourceValue>> getConfiguredFrameworkResources() {
+ if (mConfiguredFrameworkRes == null) {
+ ProjectResources frameworkRes = getFrameworkResources();
+
+ if (frameworkRes == null) {
+ AdtPlugin.log(IStatus.ERROR, "Failed to get ProjectResource for the framework");
+ }
+
+ // get the framework resource values based on the current config
+ mConfiguredFrameworkRes = frameworkRes.getConfiguredResources(mCurrentConfig);
+ }
+
+ return mConfiguredFrameworkRes;
+ }
+
+ /**
+ * Returns the selection synchronizer object.
+ * The synchronizer can be used to sync the selection of 2 or more EditPartViewers.
+ * <p/>
+ * This is changed from protected to public so that the outline can use it.
+ *
+ * @return the synchronizer
+ */
+ @Override
+ public SelectionSynchronizer getSelectionSynchronizer() {
+ return super.getSelectionSynchronizer();
+ }
+
+ /**
+ * Returns the edit domain.
+ * <p/>
+ * This is changed from protected to public so that the outline can use it.
+ *
+ * @return the edit domain
+ */
+ @Override
+ public DefaultEditDomain getEditDomain() {
+ return super.getEditDomain();
+ }
+
+ /**
+ * Creates a new layout file from the specificed {@link FolderConfiguration}.
+ */
+ private void createAlternateLayout(final FolderConfiguration config) {
+ new Job("Create Alternate Resource") {
+ @Override
+ protected IStatus run(IProgressMonitor monitor) {
+ // get the folder name
+ String folderName = config.getFolderName(ResourceFolderType.LAYOUT);
+ try {
+
+ // look to see if it exists.
+ // get the res folder
+ IFolder res = (IFolder)mEditedFile.getParent().getParent();
+ String path = res.getLocation().toOSString();
+
+ File newLayoutFolder = new File(path + File.separator + folderName);
+ if (newLayoutFolder.isFile()) {
+ // this should not happen since aapt would have complained
+ // before, but if one disable the automatic build, this could
+ // happen.
+ String message = String.format("File 'res/%1$s' is in the way!",
+ folderName);
+
+ AdtPlugin.displayError("Layout Creation", message);
+
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, message);
+ } else if (newLayoutFolder.exists() == false) {
+ // create it.
+ newLayoutFolder.mkdir();
+ }
+
+ // now create the file
+ File newLayoutFile = new File(newLayoutFolder.getAbsolutePath() +
+ File.separator + mEditedFile.getName());
+
+ newLayoutFile.createNewFile();
+
+ InputStream input = mEditedFile.getContents();
+
+ FileOutputStream fos = new FileOutputStream(newLayoutFile);
+
+ byte[] data = new byte[512];
+ int count;
+ while ((count = input.read(data)) != -1) {
+ fos.write(data, 0, count);
+ }
+
+ input.close();
+ fos.close();
+
+ // refreshes the res folder to show up the new
+ // layout folder (if needed) and the file.
+ // We use a progress monitor to catch the end of the refresh
+ // to trigger the edit of the new file.
+ res.refreshLocal(IResource.DEPTH_INFINITE, new IProgressMonitor() {
+ public void done() {
+ mCurrentConfig.set(config);
+ mParent.getDisplay().asyncExec(new Runnable() {
+ public void run() {
+ onConfigurationChange();
+ }
+ });
+ }
+
+ public void beginTask(String name, int totalWork) {
+ // pass
+ }
+
+ public void internalWorked(double work) {
+ // pass
+ }
+
+ public boolean isCanceled() {
+ // pass
+ return false;
+ }
+
+ public void setCanceled(boolean value) {
+ // pass
+ }
+
+ public void setTaskName(String name) {
+ // pass
+ }
+
+ public void subTask(String name) {
+ // pass
+ }
+
+ public void worked(int work) {
+ // pass
+ }
+ });
+ } catch (IOException e2) {
+ String message = String.format(
+ "Failed to create File 'res/%1$s/%2$s' : %3$s",
+ folderName, mEditedFile.getName(), e2.getMessage());
+
+ AdtPlugin.displayError("Layout Creation", message);
+
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ message, e2);
+ } catch (CoreException e2) {
+ String message = String.format(
+ "Failed to create File 'res/%1$s/%2$s' : %3$s",
+ folderName, mEditedFile.getName(), e2.getMessage());
+
+ AdtPlugin.displayError("Layout Creation", message);
+
+ return e2.getStatus();
+ }
+
+ return Status.OK_STATUS;
+
+ }
+ }.schedule();
+ }
+
+ /**
+ * Returns a {@link ProjectResources} for the framework resources.
+ * @return the framework resources or null if not found.
+ */
+ private ProjectResources getFrameworkResources() {
+ if (mEditedFile != null) {
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ IAndroidTarget target = currentSdk.getTarget(mEditedFile.getProject());
+
+ if (target != null) {
+ AndroidTargetData data = currentSdk.getTargetData(target);
+
+ if (data != null) {
+ return data.getFrameworkResources();
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutConstants.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutConstants.java
new file mode 100644
index 0000000..d4ec5e1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutConstants.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.layout;
+
+/**
+ * A bunch of constants that map to either:
+ * <ul>
+ * <li>Android Layouts XML element names (Linear, Relative, Absolute, etc.)
+ * <li>Attributes for layout XML elements.
+ * <li>Values for attributes.
+ * </ul>
+ */
+public class LayoutConstants {
+
+ public static final String RELATIVE_LAYOUT = "RelativeLayout"; //$NON-NLS-1$
+ public static final String LINEAR_LAYOUT = "LinearLayout"; //$NON-NLS-1$
+ public static final String ABSOLUTE_LAYOUT = "AbsoluteLayout"; //$NON-NLS-1$
+
+ public static final String ATTR_TEXT = "text"; //$NON-NLS-1$
+ public static final String ATTR_ID = "id"; //$NON-NLS-1$
+
+ public static final String ATTR_LAYOUT_HEIGHT = "layout_height"; //$NON-NLS-1$
+ public static final String ATTR_LAYOUT_WIDTH = "layout_width"; //$NON-NLS-1$
+
+ public static final String ATTR_LAYOUT_ALIGN_PARENT_TOP = "layout_alignParentTop"; //$NON-NLS-1$
+ public static final String ATTR_LAYOUT_ALIGN_PARENT_BOTTOM = "layout_alignParentBottom"; //$NON-NLS-1$
+ public static final String ATTR_LAYOUT_ALIGN_PARENT_LEFT = "layout_alignParentLeft";//$NON-NLS-1$
+ public static final String ATTR_LAYOUT_ALIGN_PARENT_RIGHT = "layout_alignParentRight"; //$NON-NLS-1$
+
+ public static final String ATTR_LAYOUT_ALIGN_BASELINE = "layout_alignBaseline"; //$NON-NLS-1$
+
+ public static final String ATTR_LAYOUT_CENTER_VERTICAL = "layout_centerVertical"; //$NON-NLS-1$
+ public static final String ATTR_LAYOUT_CENTER_HORIZONTAL = "layout_centerHorizontal"; //$NON-NLS-1$
+
+ public static final String ATTR_LAYOUT_TO_RIGHT_OF = "layout_toRightOf"; //$NON-NLS-1$
+ public static final String ATTR_LAYOUT_TO_LEFT_OF = "layout_toLeftOf"; //$NON-NLS-1$
+
+ public static final String ATTR_LAYOUT_BELOW = "layout_below"; //$NON-NLS-1$
+ public static final String ATTR_LAYOUT_ABOVE = "layout_above"; //$NON-NLS-1$
+
+ public static final String ATTR_LAYOUT_Y = "layout_y"; //$NON-NLS-1$
+ public static final String ATTR_LAYOUT_X = "layout_x"; //$NON-NLS-1$
+
+ public static final String VALUE_WRAP_CONTENT = "wrap_content"; //$NON-NLS-1$
+ public static final String VALUE_FILL_PARENT = "fill_parent"; //$NON-NLS-1$
+ public static final String VALUE_TRUE = "true"; //$NON-NLS-1$
+ public static final String VALUE_N_DIP = "%ddip"; //$NON-NLS-1$
+
+ private LayoutConstants() {
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutContentAssist.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutContentAssist.java
new file mode 100644
index 0000000..9f39495
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutContentAssist.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.layout;
+
+import com.android.ide.eclipse.adt.sdk.AndroidTargetData;
+import com.android.ide.eclipse.editors.AndroidContentAssist;
+
+/**
+ * Content Assist Processor for /res/layout XML files
+ */
+class LayoutContentAssist extends AndroidContentAssist {
+
+ /**
+ * Constructor for LayoutContentAssist
+ */
+ public LayoutContentAssist() {
+ super(AndroidTargetData.DESCRIPTOR_LAYOUT);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutCreatorDialog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutCreatorDialog.java
new file mode 100644
index 0000000..c4a8f5c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutCreatorDialog.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.layout;
+
+import com.android.ide.eclipse.editors.IconFactory;
+import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration;
+import com.android.ide.eclipse.editors.resources.configurations.ResourceQualifier;
+import com.android.ide.eclipse.editors.resources.manager.ResourceFolderType;
+import com.android.ide.eclipse.editors.wizards.ConfigurationSelector;
+import com.android.ide.eclipse.editors.wizards.ConfigurationSelector.ConfigurationState;
+
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.jface.dialogs.TrayDialog;
+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.Label;
+import org.eclipse.swt.widgets.Shell;
+
+/**
+ * Dialog to choose a non existing {@link FolderConfiguration}.
+ */
+class LayoutCreatorDialog extends TrayDialog {
+
+ private ConfigurationSelector mSelector;
+ private Composite mStatusComposite;
+ private Label mStatusLabel;
+ private Label mStatusImage;
+
+ private final FolderConfiguration mConfig = new FolderConfiguration();
+ private final String mFileName;
+
+ /**
+ * Creates a dialog, and init the UI from a {@link FolderConfiguration}.
+ * @param parentShell the parent {@link Shell}.
+ * @param config The starting configuration.
+ */
+ LayoutCreatorDialog(Shell parentShell, String fileName, FolderConfiguration config) {
+ super(parentShell);
+
+ mFileName = fileName;
+ // FIXME: add some data to know what configurations already exist.
+ mConfig.set(config);
+ }
+
+ @Override
+ protected Control createDialogArea(Composite parent) {
+ Composite top = new Composite(parent, SWT.NONE);
+ top.setLayoutData(new GridData());
+ top.setLayout(new GridLayout(1, false));
+
+ new Label(top, SWT.NONE).setText(
+ String.format("Configuration for the alternate version of %1$s", mFileName));
+
+ mSelector = new ConfigurationSelector(top);
+ mSelector.setConfiguration(mConfig);
+
+ // parent's layout is a GridLayout as specified in the javadoc.
+ GridData gd = new GridData();
+ gd.widthHint = ConfigurationSelector.WIDTH_HINT;
+ gd.heightHint = ConfigurationSelector.HEIGHT_HINT;
+ mSelector.setLayoutData(gd);
+
+ // add a listener to check on the validity of the FolderConfiguration as
+ // they are built.
+ mSelector.setOnChangeListener(new Runnable() {
+ public void run() {
+ ConfigurationState state = mSelector.getState();
+
+ switch (state) {
+ case OK:
+ mSelector.getConfiguration(mConfig);
+
+ resetStatus();
+ mStatusImage.setImage(null);
+ getButton(IDialogConstants.OK_ID).setEnabled(true);
+ break;
+ case INVALID_CONFIG:
+ ResourceQualifier invalidQualifier = mSelector.getInvalidQualifier();
+ mStatusLabel.setText(String.format(
+ "Invalid Configuration: %1$s has no filter set.",
+ invalidQualifier.getName()));
+ mStatusImage.setImage(IconFactory.getInstance().getIcon("warning")); //$NON-NLS-1$
+ getButton(IDialogConstants.OK_ID).setEnabled(false);
+ break;
+ case REGION_WITHOUT_LANGUAGE:
+ mStatusLabel.setText(
+ "The Region qualifier requires the Language qualifier.");
+ mStatusImage.setImage(IconFactory.getInstance().getIcon("warning")); //$NON-NLS-1$
+ getButton(IDialogConstants.OK_ID).setEnabled(false);
+ break;
+ }
+
+ // need to relayout, because of the change in size in mErrorImage.
+ mStatusComposite.layout();
+ }
+ });
+
+ mStatusComposite = new Composite(top, SWT.NONE);
+ mStatusComposite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ GridLayout gl = new GridLayout(2, false);
+ mStatusComposite.setLayout(gl);
+ gl.marginHeight = gl.marginWidth = 0;
+
+ mStatusImage = new Label(mStatusComposite, SWT.NONE);
+ mStatusLabel = new Label(mStatusComposite, SWT.NONE);
+ mStatusLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ resetStatus();
+
+ return top;
+ }
+
+ public void getConfiguration(FolderConfiguration config) {
+ config.set(mConfig);
+ }
+
+ /**
+ * resets the status label to show the file that will be created.
+ */
+ private void resetStatus() {
+ mStatusLabel.setText(String.format("New File: res/%1$s/%2$s",
+ mConfig.getFolderName(ResourceFolderType.LAYOUT), mFileName));
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutEditor.java
new file mode 100644
index 0000000..dabe797
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutEditor.java
@@ -0,0 +1,419 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.layout;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.sdk.AndroidTargetData;
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.common.EclipseUiHelper;
+import com.android.ide.eclipse.editors.AndroidEditor;
+import com.android.ide.eclipse.editors.descriptors.DocumentDescriptor;
+import com.android.ide.eclipse.editors.resources.manager.ResourceFolder;
+import com.android.ide.eclipse.editors.resources.manager.ResourceManager;
+import com.android.ide.eclipse.editors.ui.tree.UiActions;
+import com.android.ide.eclipse.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.gef.ui.parts.TreeViewer;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IPartListener;
+import org.eclipse.ui.IShowEditorInput;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchPart;
+import org.eclipse.ui.IWorkbenchPartSite;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.part.FileEditorInput;
+import org.eclipse.ui.views.contentoutline.IContentOutlinePage;
+import org.eclipse.ui.views.properties.IPropertySheetPage;
+import org.w3c.dom.Document;
+
+/**
+ * Multi-page form editor for /res/layout XML files.
+ */
+public class LayoutEditor extends AndroidEditor implements IShowEditorInput, IPartListener {
+
+ public static final String ID = AndroidConstants.EDITORS_NAMESPACE + ".layout.LayoutEditor"; //$NON-NLS-1$
+
+ /** Root node of the UI element hierarchy */
+ private UiDocumentNode mUiRootNode;
+
+ private GraphicalLayoutEditor mGraphicalEditor;
+ private int mGraphicalEditorIndex;
+ /** Implementation of the {@link IContentOutlinePage} for this editor */
+ private UiContentOutlinePage mOutline;
+ /** Custom implementation of {@link IPropertySheetPage} for this editor */
+ private UiPropertySheetPage mPropertyPage;
+
+ private UiEditorActions mUiEditorActions;
+
+ /**
+ * Creates the form editor for resources XML files.
+ */
+ public LayoutEditor() {
+ super();
+ }
+
+ /**
+ * @return The root node of the UI element hierarchy
+ */
+ @Override
+ public UiDocumentNode getUiRootNode() {
+ return mUiRootNode;
+ }
+
+ // ---- Base Class Overrides ----
+
+ @Override
+ public void dispose() {
+ getSite().getPage().removePartListener(this);
+
+ super.dispose();
+ }
+
+ /**
+ * Save the XML.
+ * <p/>
+ * The actual save operation is done in the super class by committing
+ * all data to the XML model and then having the Structured XML Editor
+ * save the XML.
+ * <p/>
+ * Here we just need to tell the graphical editor that the model has
+ * been saved.
+ */
+ @Override
+ public void doSave(IProgressMonitor monitor) {
+ super.doSave(monitor);
+ if (mGraphicalEditor != null) {
+ mGraphicalEditor.doSave(monitor);
+ }
+ }
+
+ /**
+ * Returns whether the "save as" operation is supported by this editor.
+ * <p/>
+ * Save-As is a valid operation for the ManifestEditor since it acts on a
+ * single source file.
+ *
+ * @see IEditorPart
+ */
+ @Override
+ public boolean isSaveAsAllowed() {
+ return true;
+ }
+
+ /**
+ * Create the various form pages.
+ */
+ @Override
+ protected void createFormPages() {
+ try {
+ // The graphical layout editor is now enabled by default.
+ // In case there's an issue we provide a way to disable it using an
+ // env variable.
+ if (System.getenv("ANDROID_DISABLE_LAYOUT") == null) {
+ if (mGraphicalEditor == null) {
+ mGraphicalEditor = new GraphicalLayoutEditor(this);
+ mGraphicalEditorIndex = addPage(mGraphicalEditor, getEditorInput());
+ setPageText(mGraphicalEditorIndex, mGraphicalEditor.getTitle());
+ } else {
+ mGraphicalEditor.reloadEditor();
+ }
+
+ // update the config based on the opened file.
+ IEditorInput input = getEditorInput();
+ if (input instanceof FileEditorInput) {
+ FileEditorInput fileInput = (FileEditorInput)input;
+ ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(
+ fileInput.getFile());
+ if (resFolder != null) {
+ mGraphicalEditor.editNewFile(resFolder.getConfiguration());
+ }
+ }
+
+ // put in place the listener to handle layout recompute only when needed.
+ getSite().getPage().addPartListener(this);
+ }
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, "Error creating nested page"); //$NON-NLS-1$
+ }
+ }
+
+ /* (non-java doc)
+ * Change the tab/title name to include the name of the layout.
+ */
+ @Override
+ protected void setInput(IEditorInput input) {
+ super.setInput(input);
+ handleNewInput(input);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.eclipse.ui.part.EditorPart#setInputWithNotify(org.eclipse.ui.IEditorInput)
+ */
+ @Override
+ protected void setInputWithNotify(IEditorInput input) {
+ super.setInputWithNotify(input);
+ handleNewInput(input);
+ }
+
+ /**
+ * Called to replace the current {@link IEditorInput} with another one.
+ * <p/>This is used when {@link MatchingStrategy} returned <code>true</code> which means we're
+ * opening a different configuration of the same layout.
+ */
+ public void showEditorInput(IEditorInput editorInput) {
+ // save the current editor input.
+ doSave(new NullProgressMonitor());
+
+ // get the current page
+ int currentPage = getActivePage();
+
+ // remove the pages, except for the graphical editor, which will be dynamically adapted
+ // to the new model.
+ // page after the graphical editor:
+ int count = getPageCount();
+ for (int i = count - 1 ; i > mGraphicalEditorIndex ; i--) {
+ removePage(i);
+ }
+ // pages before the graphical editor
+ for (int i = mGraphicalEditorIndex - 1 ; i >= 0 ; i--) {
+ removePage(i);
+ }
+
+ // set the current input.
+ setInputWithNotify(editorInput);
+
+ // re-create or reload the pages with the default page shown as the previous active page.
+ createAndroidPages();
+ selectDefaultPage(Integer.toString(currentPage));
+
+ // update the outline
+ if (mOutline != null && mGraphicalEditor != null) {
+ mOutline.reloadModel();
+ }
+ }
+
+ /**
+ * Processes the new XML Model, which XML root node is given.
+ *
+ * @param xml_doc The XML document, if available, or null if none exists.
+ */
+ @Override
+ protected void xmlModelChanged(Document xml_doc) {
+ // init the ui root on demand
+ initUiRootNode(false /*force*/);
+
+ mUiRootNode.loadFromXmlNode(xml_doc);
+
+ // update the model first, since it is used by the viewers.
+ super.xmlModelChanged(xml_doc);
+
+ if (mGraphicalEditor != null) {
+ mGraphicalEditor.onXmlModelChanged();
+ }
+
+ if (mOutline != null) {
+ mOutline.reloadModel();
+ }
+ }
+
+ /* (non-java doc)
+ * Returns the IContentOutlinePage when asked for it.
+ */
+ @SuppressWarnings("unchecked")
+ @Override
+ public Object getAdapter(Class adapter) {
+ // for the outline, force it to come from the Graphical Editor.
+ // This fixes the case where a layout file is opened in XML view first and the outline
+ // gets stuck in the XML outline.
+ if (IContentOutlinePage.class == adapter && mGraphicalEditor != null) {
+ if (mOutline == null) {
+ mOutline = new UiContentOutlinePage(mGraphicalEditor, new TreeViewer());
+ }
+
+ return mOutline;
+ }
+
+ if (IPropertySheetPage.class == adapter && mGraphicalEditor != null) {
+ if (mPropertyPage == null) {
+ mPropertyPage = new UiPropertySheetPage();
+ }
+
+ return mPropertyPage;
+ }
+
+ // return default
+ return super.getAdapter(adapter);
+ }
+
+ @Override
+ protected void pageChange(int newPageIndex) {
+ super.pageChange(newPageIndex);
+
+ if (mGraphicalEditor != null) {
+ if (newPageIndex == mGraphicalEditorIndex) {
+ mGraphicalEditor.activated();
+ } else {
+ mGraphicalEditor.deactivated();
+ }
+ }
+ }
+
+ // ----- IPartListener Methods ----
+
+ public void partActivated(IWorkbenchPart part) {
+ if (part == this) {
+ if (mGraphicalEditor != null) {
+ if (getActivePage() == mGraphicalEditorIndex) {
+ mGraphicalEditor.activated();
+ } else {
+ mGraphicalEditor.deactivated();
+ }
+ }
+ }
+ }
+
+ public void partBroughtToTop(IWorkbenchPart part) {
+ partActivated(part);
+ }
+
+ public void partClosed(IWorkbenchPart part) {
+ // pass
+ }
+
+ public void partDeactivated(IWorkbenchPart part) {
+ if (part == this) {
+ if (mGraphicalEditor != null && getActivePage() == mGraphicalEditorIndex) {
+ mGraphicalEditor.deactivated();
+ }
+ }
+ }
+
+ public void partOpened(IWorkbenchPart part) {
+ EclipseUiHelper.showView(EclipseUiHelper.CONTENT_OUTLINE_VIEW_ID, false /* activate */);
+ EclipseUiHelper.showView(EclipseUiHelper.PROPERTY_SHEET_VIEW_ID, false /* activate */);
+ }
+
+ public class UiEditorActions extends UiActions {
+
+ @Override
+ protected UiDocumentNode getRootNode() {
+ return mUiRootNode;
+ }
+
+ // Select the new item
+ @Override
+ protected void selectUiNode(UiElementNode uiNodeToSelect) {
+ mGraphicalEditor.selectModel(uiNodeToSelect);
+ }
+
+ @Override
+ public void commitPendingXmlChanges() {
+ // Pass. There is nothing to commit before the XML is changed here.
+ }
+ }
+
+ public UiEditorActions getUiEditorActions() {
+ if (mUiEditorActions == null) {
+ mUiEditorActions = new UiEditorActions();
+ }
+ return mUiEditorActions;
+ }
+
+ // ---- Local Methods ----
+
+ /**
+ * Returns true if the Graphics editor page is visible. This <b>must</b> be
+ * called from the UI thread.
+ */
+ boolean isGraphicalEditorActive() {
+ IWorkbenchPartSite workbenchSite = getSite();
+ IWorkbenchPage workbenchPage = workbenchSite.getPage();
+
+ // check if the editor is visible in the workbench page
+ if (workbenchPage.isPartVisible(this) && workbenchPage.getActiveEditor() == this) {
+ // and then if the page of the editor is visible (not to be confused with
+ // the workbench page)
+ return mGraphicalEditorIndex == getActivePage();
+ }
+
+ return false;
+ }
+
+ @Override
+ protected void initUiRootNode(boolean force) {
+ // The root UI node is always created, even if there's no corresponding XML node.
+ if (mUiRootNode == null || force) {
+ // get the target data from the opened file (and its project)
+ AndroidTargetData data = getTargetData();
+
+ Document doc = null;
+ if (mUiRootNode != null) {
+ doc = mUiRootNode.getXmlDocument();
+ }
+
+ DocumentDescriptor desc;
+ if (data == null) {
+ desc = new DocumentDescriptor("temp", null /*children*/);
+ } else {
+ desc = data.getLayoutDescriptors().getDescriptor();
+ }
+
+ // get the descriptors from the data.
+ mUiRootNode = (UiDocumentNode) desc.createUiNode();
+ mUiRootNode.setEditor(this);
+
+ onDescriptorsChanged(doc);
+ }
+ }
+
+ private void onDescriptorsChanged(Document document) {
+ if (document != null) {
+ mUiRootNode.loadFromXmlNode(document);
+ } else {
+ mUiRootNode.reloadFromXmlNode(mUiRootNode.getXmlDocument());
+ }
+
+ if (mOutline != null) {
+ mOutline.reloadModel();
+ }
+
+ if (mGraphicalEditor != null) {
+ mGraphicalEditor.reloadEditor();
+ mGraphicalEditor.reloadPalette();
+ mGraphicalEditor.recomputeLayout();
+ }
+ }
+
+ /**
+ * Handles a new input, and update the part name.
+ * @param input the new input.
+ */
+ private void handleNewInput(IEditorInput input) {
+ if (input instanceof FileEditorInput) {
+ FileEditorInput fileInput = (FileEditorInput) input;
+ IFile file = fileInput.getFile();
+ setPartName(String.format("%1$s",
+ file.getName()));
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutReloadMonitor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutReloadMonitor.java
new file mode 100644
index 0000000..cf20288
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutReloadMonitor.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.layout;
+
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.editors.resources.manager.ResourceFolder;
+import com.android.ide.eclipse.editors.resources.manager.ResourceFolderType;
+import com.android.ide.eclipse.editors.resources.manager.ResourceManager;
+import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor;
+import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IFileListener;
+import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IResourceEventListener;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IMarkerDelta;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResourceDelta;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * Monitor for file changes triggering a layout redraw.
+ */
+public final class LayoutReloadMonitor implements IFileListener, IResourceEventListener {
+
+ // singleton, enforced by private constructor.
+ private final static LayoutReloadMonitor sThis = new LayoutReloadMonitor();
+
+ /**
+ * Map of listeners by IProject.
+ */
+ private final Map<IProject, List<ILayoutReloadListener>> mListenerMap =
+ new HashMap<IProject, List<ILayoutReloadListener>>();
+
+ private final static int CHANGE_CODE = 0;
+ private final static int CHANGE_RESOURCES = 1;
+ private final static int CHANGE_R = 2;
+ private final static int CHANGE_COUNT = 3;
+ /**
+ * List of projects having received a file change. the boolean[] contains 3 values:
+ * <ul><li>CHANGE_CODE: code change flag.</li>
+ * <li>CHANGE_RESOURCES: resource change flag.</li>
+ * <li>CHANGE_R: R clas change flag</li></ul>
+ */
+ private final Map<IProject, boolean[]> mChangedProjects = new HashMap<IProject, boolean[]>();
+
+ /**
+ * Classes which implement this interface provide a method to respond to resource changes
+ * triggering a layout redraw
+ */
+ public interface ILayoutReloadListener {
+ /**
+ * Sent when the layout needs to be redrawn
+ * @param codeChange The trigger happened due to a code change.
+ * @param rChange The trigger happened due to a change in the R class.
+ * @param resChange The trigger happened due to a resource change.
+ */
+ void reloadLayout(boolean codeChange, boolean rChange, boolean resChange);
+ }
+
+ /**
+ * Returns the single instance of {@link LayoutReloadMonitor}.
+ */
+ public static LayoutReloadMonitor getMonitor() {
+ return sThis;
+ }
+
+ private LayoutReloadMonitor() {
+ ResourceMonitor monitor = ResourceMonitor.getMonitor();
+ monitor.addFileListener(this, IResourceDelta.ADDED | IResourceDelta.CHANGED);
+ monitor.addResourceEventListener(this);
+ }
+
+ /**
+ * Adds a listener for a given {@link IProject}.
+ * @param project
+ * @param listener
+ */
+ public void addListener(IProject project, ILayoutReloadListener listener) {
+ synchronized (mListenerMap) {
+ List<ILayoutReloadListener> list = mListenerMap.get(project);
+ if (list == null) {
+ list = new ArrayList<ILayoutReloadListener>();
+ mListenerMap.put(project, list);
+ }
+
+ list.add(listener);
+ }
+ }
+
+ /**
+ * Removes a listener for a given {@link IProject}.
+ * @param project
+ * @param listener
+ */
+ public void removeListener(IProject project, ILayoutReloadListener listener) {
+ synchronized (mListenerMap) {
+ List<ILayoutReloadListener> list = mListenerMap.get(project);
+ if (list != null) {
+ list.remove(listener);
+ }
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IFileListener#fileChanged(org.eclipse.core.resources.IFile, org.eclipse.core.resources.IMarkerDelta[], int)
+ *
+ * Callback for ResourceMonitor.IFileListener. Called when a file changed.
+ * This records the changes for each project, but does not notify listeners.
+ * @see #resourceChangeEventEnd
+ */
+ public void fileChanged(IFile file, IMarkerDelta[] markerDeltas, int kind) {
+ // get the file project
+ IProject project = file.getProject();
+
+ // if this project has already been marked as modified, we do nothing.
+ boolean[] changeFlags = mChangedProjects.get(project);
+ if (changeFlags != null && changeFlags[CHANGE_CODE] && changeFlags[CHANGE_RESOURCES] &&
+ changeFlags[CHANGE_R]) {
+ return;
+ }
+
+ // now check that the file is *NOT* a layout file (those automatically trigger a layout
+ // reload and we don't want to do it twice.
+ ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(file);
+ if (resFolder != null) {
+ if (resFolder.getType() != ResourceFolderType.LAYOUT) {
+ // this is a resource change!
+ if (changeFlags == null) {
+ changeFlags = new boolean[CHANGE_COUNT];
+ mChangedProjects.put(project, changeFlags);
+ }
+
+ changeFlags[CHANGE_RESOURCES] = true;
+ }
+ } else if (AndroidConstants.EXT_CLASS.equals(file.getFileExtension())) {
+ if (file.getName().matches("R[\\$\\.](.*)")) {
+ // this is a R change!
+ if (changeFlags == null) {
+ changeFlags = new boolean[CHANGE_COUNT];
+ mChangedProjects.put(project, changeFlags);
+ }
+
+ changeFlags[CHANGE_R] = true;
+ } else {
+ // this is a code change!
+ if (changeFlags == null) {
+ changeFlags = new boolean[CHANGE_COUNT];
+ mChangedProjects.put(project, changeFlags);
+ }
+
+ changeFlags[CHANGE_CODE] = true;
+ }
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IResourceEventListener#resourceChangeEventStart()
+ *
+ * Callback for ResourceMonitor.IResourceEventListener. Called at the beginning of a resource
+ * change event. This is called once, while fileChanged can be called several times.
+ *
+ */
+ public void resourceChangeEventStart() {
+ // nothing to be done here, it all happens in the resourceChangeEventEnd
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IResourceEventListener#resourceChangeEventEnd()
+ *
+ * Callback for ResourceMonitor.IResourceEventListener. Called at the end of a resource
+ * change event. This is where we notify the listeners.
+ */
+ public void resourceChangeEventEnd() {
+ // for each IProject that was changed, we notify all the listeners.
+ synchronized (mListenerMap) {
+ for (Entry<IProject, boolean[]> project : mChangedProjects.entrySet()) {
+ List<ILayoutReloadListener> listeners = mListenerMap.get(project.getKey());
+
+ boolean[] flags = project.getValue();
+
+ if (listeners != null) {
+ for (ILayoutReloadListener listener : listeners) {
+ listener.reloadLayout(flags[CHANGE_CODE], flags[CHANGE_R],
+ flags[CHANGE_RESOURCES]);
+ }
+ }
+ }
+ }
+
+ // empty the list.
+ mChangedProjects.clear();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutSourceViewerConfig.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutSourceViewerConfig.java
new file mode 100644
index 0000000..1aa1f4c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/LayoutSourceViewerConfig.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.layout;
+
+
+import com.android.ide.eclipse.editors.AndroidSourceViewerConfig;
+
+/**
+ * Source Viewer Configuration that calls in LayoutContentAssist.
+ */
+public class LayoutSourceViewerConfig extends AndroidSourceViewerConfig {
+
+ public LayoutSourceViewerConfig() {
+ super(new LayoutContentAssist());
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/MatchingStrategy.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/MatchingStrategy.java
new file mode 100644
index 0000000..bb075c2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/MatchingStrategy.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.layout;
+
+import com.android.ide.eclipse.editors.resources.manager.ResourceFolder;
+import com.android.ide.eclipse.editors.resources.manager.ResourceFolderType;
+import com.android.ide.eclipse.editors.resources.manager.ResourceManager;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorMatchingStrategy;
+import org.eclipse.ui.IEditorReference;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.part.FileEditorInput;
+
+/**
+ * Matching strategy for the Layout Editor. This is used to open all configurations of a layout
+ * in the same editor.
+ */
+public class MatchingStrategy implements IEditorMatchingStrategy {
+
+ public boolean matches(IEditorReference editorRef, IEditorInput input) {
+ // first check that the file being opened is a layout file.
+ if (input instanceof FileEditorInput) {
+ FileEditorInput fileInput = (FileEditorInput)input;
+
+ // get the IFile object and check it's in one of the layout folders.
+ IFile iFile = fileInput.getFile();
+ ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(iFile);
+
+ // if it's a layout, we know check the name of the fileInput against the name of the
+ // file being currently edited by the editor since those are independent of the config.
+ if (resFolder != null && resFolder.getType() == ResourceFolderType.LAYOUT) {
+ try {
+ IEditorInput editorInput = editorRef.getEditorInput();
+ if (editorInput instanceof FileEditorInput) {
+ FileEditorInput editorFileInput = (FileEditorInput)editorInput;
+ IFile editorIFile = editorFileInput.getFile();
+
+ return editorIFile.getProject().equals(iFile.getProject())
+ && editorIFile.getName().equals(iFile.getName());
+ }
+ } catch (PartInitException e) {
+ // we do nothing, we'll just return false.
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/PaletteFactory.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/PaletteFactory.java
new file mode 100644
index 0000000..94df28f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/PaletteFactory.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.layout;
+
+import com.android.ide.eclipse.adt.sdk.AndroidTargetData;
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+
+import org.eclipse.gef.palette.PaletteDrawer;
+import org.eclipse.gef.palette.PaletteGroup;
+import org.eclipse.gef.palette.PaletteRoot;
+import org.eclipse.gef.palette.PaletteTemplateEntry;
+
+import java.util.List;
+
+/**
+ * Factory that creates the palette for the {@link GraphicalLayoutEditor}.
+ */
+public class PaletteFactory {
+
+ /** Static factory, nothing to instantiate here. */
+ private PaletteFactory() {
+ }
+
+ public static PaletteRoot createPaletteRoot(PaletteRoot currentPalette,
+ AndroidTargetData targetData) {
+
+ if (currentPalette == null) {
+ currentPalette = new PaletteRoot();
+ }
+
+ for (int n = currentPalette.getChildren().size() - 1; n >= 0; n--) {
+ currentPalette.getChildren().remove(n);
+ }
+
+ if (targetData != null) {
+ addTools(currentPalette);
+ addViews(currentPalette, "Layouts",
+ targetData.getLayoutDescriptors().getLayoutDescriptors());
+ addViews(currentPalette, "Views",
+ targetData.getLayoutDescriptors().getViewDescriptors());
+ }
+
+ return currentPalette;
+ }
+
+ private static void addTools(PaletteRoot paletteRoot) {
+ PaletteGroup group = new PaletteGroup("Tools");
+
+ // Default tools: selection.
+ // Do not use the MarqueeToolEntry since we don't support multiple selection.
+ /* -- Do not put the selection tool. It's the unique tool so it looks useless.
+ Leave this piece of code here in case we want it back later.
+ PanningSelectionToolEntry entry = new PanningSelectionToolEntry();
+ group.add(entry);
+ paletteRoot.setDefaultEntry(entry);
+ */
+
+ paletteRoot.add(group);
+ }
+
+ private static void addViews(PaletteRoot paletteRoot, String groupName,
+ List<ElementDescriptor> descriptors) {
+ PaletteDrawer group = new PaletteDrawer(groupName);
+
+ for (ElementDescriptor desc : descriptors) {
+ PaletteTemplateEntry entry = new PaletteTemplateEntry(
+ desc.getUiName(), // label
+ desc.getTooltip(), // short description
+ desc, // template
+ desc.getImageDescriptor(), // small icon
+ desc.getImageDescriptor() // large icon
+ );
+
+ group.add(entry);
+ }
+
+ paletteRoot.add(group);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/ProjectCallback.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/ProjectCallback.java
new file mode 100644
index 0000000..81fd2ed
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/ProjectCallback.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.layout;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.common.project.AndroidManifestHelper;
+import com.android.ide.eclipse.editors.resources.manager.ProjectClassLoader;
+import com.android.ide.eclipse.editors.resources.manager.ProjectResources;
+import com.android.layoutlib.api.IProjectCallback;
+
+import org.eclipse.core.resources.IProject;
+
+import java.lang.reflect.Constructor;
+import java.util.HashMap;
+
+/**
+ * Loader for Android Project class in order to use them in the layout editor.
+ */
+public final class ProjectCallback implements IProjectCallback {
+
+ private final HashMap<String, Class<?>> mLoadedClasses = new HashMap<String, Class<?>>();
+ private final IProject mProject;
+ private final ClassLoader mParentClassLoader;
+ private final ProjectResources mProjectRes;
+ private boolean mUsed = false;
+ private String mNamespace;
+
+ ProjectCallback(ClassLoader classLoader, ProjectResources projectRes, IProject project) {
+ mParentClassLoader = classLoader;
+ mProjectRes = projectRes;
+ mProject = project;
+ }
+
+
+ /**
+ * {@inheritDoc}
+ *
+ * This implementation goes through the output directory of the Eclipse project and loads the
+ * <code>.class</code> file directly.
+ */
+ @SuppressWarnings("unchecked")
+ public Object loadView(String className, Class[] constructorSignature,
+ Object[] constructorParameters)
+ throws ClassNotFoundException, Exception {
+
+ // look for a cached version
+ Class<?> clazz = mLoadedClasses.get(className);
+ if (clazz != null) {
+ return instantiateClass(clazz, constructorSignature, constructorParameters);
+ }
+
+ // load the class.
+ ProjectClassLoader loader = new ProjectClassLoader(mParentClassLoader, mProject);
+ try {
+ clazz = loader.loadClass(className);
+
+ if (clazz != null) {
+ mUsed = true;
+ mLoadedClasses.put(className, clazz);
+ return instantiateClass(clazz, constructorSignature, constructorParameters);
+ }
+ } catch (Error e) {
+ // Log this error with the class name we're trying to load and abort.
+ AdtPlugin.log(e, "ProjectCallback.loadView failed to find class %1$s", className); //$NON-NLS-1$
+ }
+
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Returns the namespace for the project. The namespace contains a standard part + the
+ * application package.
+ */
+ public String getNamespace() {
+ if (mNamespace == null) {
+ AndroidManifestHelper manifest = new AndroidManifestHelper(mProject);
+ String javaPackage = manifest.getPackageName();
+
+ mNamespace = String.format(AndroidConstants.NS_CUSTOM_RESOURCES, javaPackage);
+ }
+
+ return mNamespace;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.layoutlib.api.IProjectCallback#resolveResourceValue(int)
+ */
+ public String[] resolveResourceValue(int id) {
+ if (mProjectRes != null) {
+ return mProjectRes.resolveResourceValue(id);
+ }
+
+ return null;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.layoutlib.api.IProjectCallback#resolveResourceValue(int[])
+ */
+ public String resolveResourceValue(int[] id) {
+ if (mProjectRes != null) {
+ return mProjectRes.resolveResourceValue(id);
+ }
+
+ return null;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.layoutlib.api.IProjectCallback#getResourceValue(java.lang.String, java.lang.String)
+ */
+ public Integer getResourceValue(String type, String name) {
+ if (mProjectRes != null) {
+ return mProjectRes.getResourceValue(type, name);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns whether the loader has received requests to load custom views.
+ * <p/>This allows to efficiently only recreate when needed upon code change in the project.
+ */
+ boolean isUsed() {
+ return mUsed;
+ }
+
+ /**
+ * Instantiate a class object, using a specific constructor and parameters.
+ * @param clazz the class to instantiate
+ * @param constructorSignature the signature of the constructor to use
+ * @param constructorParameters the parameters to use in the constructor.
+ * @return A new class object, created using a specific constructor and parameters.
+ * @throws Exception
+ */
+ @SuppressWarnings("unchecked")
+ private Object instantiateClass(Class<?> clazz, Class[] constructorSignature,
+ Object[] constructorParameters) throws Exception {
+ Constructor<?> constructor = clazz.getConstructor(constructorSignature);
+ constructor.setAccessible(true);
+ return constructor.newInstance(constructorParameters);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/UiContentOutlinePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/UiContentOutlinePage.java
new file mode 100644
index 0000000..3e0f5d8
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/UiContentOutlinePage.java
@@ -0,0 +1,615 @@
+/*
+ * 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.ide.eclipse.editors.layout;
+
+import com.android.ide.eclipse.common.EclipseUiHelper;
+import com.android.ide.eclipse.editors.IconFactory;
+import com.android.ide.eclipse.editors.layout.parts.UiDocumentTreeEditPart;
+import com.android.ide.eclipse.editors.layout.parts.UiElementTreeEditPart;
+import com.android.ide.eclipse.editors.layout.parts.UiElementTreeEditPartFactory;
+import com.android.ide.eclipse.editors.layout.parts.UiLayoutTreeEditPart;
+import com.android.ide.eclipse.editors.layout.parts.UiViewTreeEditPart;
+import com.android.ide.eclipse.editors.ui.tree.CopyCutAction;
+import com.android.ide.eclipse.editors.ui.tree.PasteAction;
+import com.android.ide.eclipse.editors.ui.tree.UiActions;
+import com.android.ide.eclipse.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+import org.eclipse.gef.EditPartViewer;
+import org.eclipse.gef.ui.parts.ContentOutlinePage;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.IMenuListener;
+import org.eclipse.jface.action.IMenuManager;
+import org.eclipse.jface.action.IToolBarManager;
+import org.eclipse.jface.action.MenuManager;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.jface.viewers.TreePath;
+import org.eclipse.jface.viewers.TreeSelection;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+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.Event;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeItem;
+import org.eclipse.ui.IActionBars;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Implementation of the {@link ContentOutlinePage} to display {@link UiElementNode}.
+ */
+class UiContentOutlinePage extends ContentOutlinePage {
+
+ private GraphicalLayoutEditor mEditor;
+
+ private Action mAddAction;
+ private Action mDeleteAction;
+ private Action mUpAction;
+ private Action mDownAction;
+
+ private UiOutlineActions mUiActions = new UiOutlineActions();
+
+ public UiContentOutlinePage(GraphicalLayoutEditor editor, final EditPartViewer viewer) {
+ super(viewer);
+ mEditor = editor;
+ IconFactory factory = IconFactory.getInstance();
+
+ mAddAction = new Action("Add...") {
+ @Override
+ public void run() {
+ List<UiElementNode> nodes = getModelSelections();
+ UiElementNode node = nodes != null && nodes.size() > 0 ? nodes.get(0) : null;
+
+ mUiActions.doAdd(node, viewer.getControl().getShell());
+ }
+ };
+ mAddAction.setToolTipText("Adds a new element.");
+ mAddAction.setImageDescriptor(factory.getImageDescriptor("add")); //$NON-NLS-1$
+
+ mDeleteAction = new Action("Remove...") {
+ @Override
+ public void run() {
+ List<UiElementNode> nodes = getModelSelections();
+
+ mUiActions.doRemove(nodes, viewer.getControl().getShell());
+ }
+ };
+ mDeleteAction.setToolTipText("Removes an existing selected element.");
+ mDeleteAction.setImageDescriptor(factory.getImageDescriptor("delete")); //$NON-NLS-1$
+
+ mUpAction = new Action("Up") {
+ @Override
+ public void run() {
+ List<UiElementNode> nodes = getModelSelections();
+
+ mUiActions.doUp(nodes);
+ }
+ };
+ mUpAction.setToolTipText("Moves the selected element up");
+ mUpAction.setImageDescriptor(factory.getImageDescriptor("up")); //$NON-NLS-1$
+
+ mDownAction = new Action("Down") {
+ @Override
+ public void run() {
+ List<UiElementNode> nodes = getModelSelections();
+
+ mUiActions.doDown(nodes);
+ }
+ };
+ mDownAction.setToolTipText("Moves the selected element down");
+ mDownAction.setImageDescriptor(factory.getImageDescriptor("down")); //$NON-NLS-1$
+
+ // all actions disabled by default.
+ mAddAction.setEnabled(false);
+ mDeleteAction.setEnabled(false);
+ mUpAction.setEnabled(false);
+ mDownAction.setEnabled(false);
+
+ addSelectionChangedListener(new ISelectionChangedListener() {
+ public void selectionChanged(SelectionChangedEvent event) {
+ ISelection selection = event.getSelection();
+
+ // the selection is never empty. The least it'll contain is the
+ // UiDocumentTreeEditPart object.
+ if (selection instanceof StructuredSelection) {
+ StructuredSelection structSel = (StructuredSelection)selection;
+
+ if (structSel.size() == 1 &&
+ structSel.getFirstElement() instanceof UiDocumentTreeEditPart) {
+ mDeleteAction.setEnabled(false);
+ mUpAction.setEnabled(false);
+ mDownAction.setEnabled(false);
+ } else {
+ mDeleteAction.setEnabled(true);
+ mUpAction.setEnabled(true);
+ mDownAction.setEnabled(true);
+ }
+
+ // the "add" button is always enabled, in order to be able to set the
+ // initial root node
+ mAddAction.setEnabled(true);
+ }
+ }
+ });
+ }
+
+
+ /* (non-Javadoc)
+ * @see org.eclipse.ui.part.IPage#createControl(org.eclipse.swt.widgets.Composite)
+ */
+ @Override
+ public void createControl(Composite parent) {
+ // create outline viewer page
+ getViewer().createControl(parent);
+
+ // configure outline viewer
+ getViewer().setEditPartFactory(new UiElementTreeEditPartFactory());
+
+ setupOutline();
+ setupContextMenu();
+ setupTooltip();
+ setupDoubleClick();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.eclipse.ui.part.Page#setActionBars(org.eclipse.ui.IActionBars)
+ *
+ * Called automatically after createControl
+ */
+ @Override
+ public void setActionBars(IActionBars actionBars) {
+ IToolBarManager toolBarManager = actionBars.getToolBarManager();
+ toolBarManager.add(mAddAction);
+ toolBarManager.add(mDeleteAction);
+ toolBarManager.add(new Separator());
+ toolBarManager.add(mUpAction);
+ toolBarManager.add(mDownAction);
+
+ IMenuManager menuManager = actionBars.getMenuManager();
+ menuManager.add(mAddAction);
+ menuManager.add(mDeleteAction);
+ menuManager.add(new Separator());
+ menuManager.add(mUpAction);
+ menuManager.add(mDownAction);
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.ui.part.IPage#dispose()
+ */
+ @Override
+ public void dispose() {
+ breakConnectionWithEditor();
+
+ // dispose
+ super.dispose();
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.ui.part.IPage#getControl()
+ */
+ @Override
+ public Control getControl() {
+ return getViewer().getControl();
+ }
+
+ void setNewEditor(GraphicalLayoutEditor editor) {
+ mEditor = editor;
+ setupOutline();
+ }
+
+ void breakConnectionWithEditor() {
+ // unhook outline viewer
+ mEditor.getSelectionSynchronizer().removeViewer(getViewer());
+ }
+
+ private void setupOutline() {
+ getViewer().setEditDomain(mEditor.getEditDomain());
+
+ // hook outline viewer
+ mEditor.getSelectionSynchronizer().addViewer(getViewer());
+
+ // initialize outline viewer with model
+ getViewer().setContents(mEditor.getModel());
+ }
+
+ private void setupContextMenu() {
+ MenuManager menuManager = new MenuManager();
+ menuManager.setRemoveAllWhenShown(true);
+ menuManager.addMenuListener(new IMenuListener() {
+ /**
+ * The menu is about to be shown. The menu manager has already been
+ * requested to remove any existing menu item. This method gets the
+ * tree selection and if it is of the appropriate type it re-creates
+ * the necessary actions.
+ */
+ public void menuAboutToShow(IMenuManager manager) {
+ List<UiElementNode> selected = getModelSelections();
+
+ if (selected != null) {
+ doCreateMenuAction(manager, selected);
+ return;
+ }
+ doCreateMenuAction(manager, null /* ui_node */);
+ }
+ });
+ Control control = getControl();
+ Menu contextMenu = menuManager.createContextMenu(control);
+ control.setMenu(contextMenu);
+ }
+
+ /**
+ * Adds the menu actions to the context menu when the given UI node is selected in
+ * the tree view.
+ *
+ * @param manager The context menu manager
+ * @param selected The UI node selected in the tree. Can be null, in which case the root
+ * is to be modified.
+ */
+ private void doCreateMenuAction(IMenuManager manager, List<UiElementNode> selected) {
+
+ if (selected != null) {
+ boolean hasXml = false;
+ for (UiElementNode uiNode : selected) {
+ if (uiNode.getXmlNode() != null) {
+ hasXml = true;
+ break;
+ }
+ }
+
+ if (hasXml) {
+ manager.add(new CopyCutAction(mEditor.getLayoutEditor(), mEditor.getClipboard(),
+ null, selected, true /* cut */));
+ manager.add(new CopyCutAction(mEditor.getLayoutEditor(), mEditor.getClipboard(),
+ null, selected, false /* cut */));
+
+ // Can't paste with more than one element selected (the selection is the target)
+ if (selected.size() <= 1) {
+ // Paste is not valid if it would add a second element on a terminal element
+ // which parent is a document -- an XML document can only have one child. This
+ // means paste is valid if the current UI node can have children or if the parent
+ // is not a document.
+ UiElementNode ui_root = selected.get(0).getUiRoot();
+ if (ui_root.getDescriptor().hasChildren() ||
+ !(ui_root.getUiParent() instanceof UiDocumentNode)) {
+ manager.add(new PasteAction(mEditor.getLayoutEditor(),
+ mEditor.getClipboard(),
+ selected.get(0)));
+ }
+ }
+ manager.add(new Separator());
+ }
+ }
+
+ // Append "add" and "remove" actions. They do the same thing as the add/remove
+ // buttons on the side.
+ //
+ // "Add" makes sense only if there's 0 or 1 item selected since the
+ // one selected item becomes the target.
+ if (selected == null || selected.size() <= 1) {
+ manager.add(mAddAction);
+ }
+
+ if (selected != null) {
+ manager.add(mDeleteAction);
+ manager.add(new Separator());
+
+ manager.add(mUpAction);
+ manager.add(mDownAction);
+ }
+
+ if (selected != null && selected.size() == 1) {
+ manager.add(new Separator());
+
+ Action propertiesAction = new Action("Properties") {
+ @Override
+ public void run() {
+ EclipseUiHelper.showView(EclipseUiHelper.PROPERTY_SHEET_VIEW_ID,
+ true /* activate */);
+ }
+ };
+ propertiesAction.setToolTipText("Displays properties of the selected element.");
+ manager.add(propertiesAction);
+ }
+ }
+
+ /**
+ * Updates the outline view with the model of the {@link GraphicalLayoutEditor}.
+ * <p/>
+ * This attemps to preserve the selection, if any.
+ */
+ public void reloadModel() {
+ // Attemps to preserve the UiNode selection, if any
+ List<UiElementNode> uiNodes = null;
+ try {
+ // get current selection using the model rather than the edit part as
+ // reloading the content may change the actual edit part.
+ uiNodes = getModelSelections();
+
+ // perform the update
+ getViewer().setContents(mEditor.getModel());
+
+ } finally {
+ // restore selection
+ if (uiNodes != null) {
+ setModelSelection(uiNodes.get(0));
+ }
+ }
+ }
+
+ /**
+ * Returns the currently selected element, if any, in the viewer.
+ * This returns the viewer's elements (i.e. an {@link UiElementTreeEditPart})
+ * and not the underlying model node.
+ * <p/>
+ * When there is no actual selection, this might still return the root node,
+ * which is of type {@link UiDocumentTreeEditPart}.
+ */
+ @SuppressWarnings("unchecked")
+ private List<UiElementTreeEditPart> getViewerSelections() {
+ ISelection selection = getSelection();
+ if (selection instanceof StructuredSelection) {
+ StructuredSelection structuredSelection = (StructuredSelection)selection;
+
+ if (structuredSelection.size() > 0) {
+ ArrayList<UiElementTreeEditPart> selected = new ArrayList<UiElementTreeEditPart>();
+
+ for (Iterator it = structuredSelection.iterator(); it.hasNext(); ) {
+ Object selectedObj = it.next();
+
+ if (selectedObj instanceof UiElementTreeEditPart) {
+ selected.add((UiElementTreeEditPart) selectedObj);
+ }
+ }
+
+ return selected.size() > 0 ? selected : null;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the currently selected model element, which is either an
+ * {@link UiViewTreeEditPart} or an {@link UiLayoutTreeEditPart}.
+ * <p/>
+ * Returns null if there is no selection or if the implicit root is "selected"
+ * (which actually represents the lack of a real element selection.)
+ */
+ private List<UiElementNode> getModelSelections() {
+
+ List<UiElementTreeEditPart> parts = getViewerSelections();
+
+ if (parts != null) {
+ ArrayList<UiElementNode> selected = new ArrayList<UiElementNode>();
+
+ for (UiElementTreeEditPart part : parts) {
+ if (part instanceof UiViewTreeEditPart || part instanceof UiLayoutTreeEditPart) {
+ selected.add((UiElementNode) part.getModel());
+ }
+ }
+
+ return selected.size() > 0 ? selected : null;
+ }
+
+ return null;
+ }
+
+ /**
+ * Selects the corresponding edit part in the tree viewer.
+ */
+ private void setViewerSelection(UiElementTreeEditPart selectedPart) {
+ if (selectedPart != null && !(selectedPart instanceof UiDocumentTreeEditPart)) {
+ LinkedList<UiElementTreeEditPart> segments = new LinkedList<UiElementTreeEditPart>();
+ for (UiElementTreeEditPart part = selectedPart;
+ !(part instanceof UiDocumentTreeEditPart);
+ part = (UiElementTreeEditPart) part.getParent()) {
+ segments.add(0, part);
+ }
+ setSelection(new TreeSelection(new TreePath(segments.toArray())));
+ }
+ }
+
+ /**
+ * Selects the corresponding model element in the tree viewer.
+ */
+ private void setModelSelection(UiElementNode uiNodeToSelect) {
+ if (uiNodeToSelect != null) {
+
+ // find an edit part that has the requested model element
+ UiElementTreeEditPart part = findPartForModel(
+ (UiElementTreeEditPart) getViewer().getContents(),
+ uiNodeToSelect);
+
+ // if we found a part, select it and reveal it
+ if (part != null) {
+ setViewerSelection(part);
+ getViewer().reveal(part);
+ }
+ }
+ }
+
+ /**
+ * Utility method that tries to find an edit part that matches a given model UI node.
+ *
+ * @param rootPart The root of the viewer edit parts
+ * @param uiNode The UI node model to find
+ * @return The part that matches the model or null if it's not in the sub tree.
+ */
+ private UiElementTreeEditPart findPartForModel(UiElementTreeEditPart rootPart,
+ UiElementNode uiNode) {
+ if (rootPart.getModel() == uiNode) {
+ return rootPart;
+ }
+
+ for (Object part : rootPart.getChildren()) {
+ if (part instanceof UiElementTreeEditPart) {
+ UiElementTreeEditPart found = findPartForModel(
+ (UiElementTreeEditPart) part, uiNode);
+ if (found != null) {
+ return found;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Sets up a custom tooltip when hovering over tree items.
+ * <p/>
+ * The tooltip will display the element's javadoc, if any, or the item's getText otherwise.
+ */
+ private void setupTooltip() {
+ final Tree tree = (Tree) getControl();
+
+ /*
+ * 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() {
+ Shell tip = null;
+ Label label = null;
+
+ public void handleEvent(Event event) {
+ switch(event.type) {
+ case SWT.Dispose:
+ case SWT.KeyDown:
+ case SWT.MouseExit:
+ case SWT.MouseDown:
+ case SWT.MouseMove:
+ if (tip != null) {
+ tip.dispose();
+ tip = null;
+ label = null;
+ }
+ break;
+ case SWT.MouseHover:
+ if (tip != null) {
+ tip.dispose();
+ tip = null;
+ label = null;
+ }
+
+ String tooltip = null;
+
+ TreeItem item = tree.getItem(new Point(event.x, event.y));
+ if (item != null) {
+ Object data = item.getData();
+ if (data instanceof UiElementTreeEditPart) {
+ Object model = ((UiElementTreeEditPart) data).getModel();
+ if (model instanceof UiElementNode) {
+ tooltip = ((UiElementNode) model).getDescriptor().getTooltip();
+ }
+ }
+
+ if (tooltip == null) {
+ tooltip = item.getText();
+ } else {
+ tooltip = item.getText() + ":\r" + tooltip;
+ }
+ }
+
+
+ if (tooltip != null) {
+ Shell shell = tree.getShell();
+ Display display = tree.getDisplay();
+
+ tip = new Shell(shell, SWT.ON_TOP | SWT.NO_FOCUS | SWT.TOOL);
+ tip.setBackground(display .getSystemColor(SWT.COLOR_INFO_BACKGROUND));
+ FillLayout layout = new FillLayout();
+ layout.marginWidth = 2;
+ tip.setLayout(layout);
+ label = new Label(tip, SWT.NONE);
+ label.setForeground(display.getSystemColor(SWT.COLOR_INFO_FOREGROUND));
+ label.setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND));
+ label.setData("_TABLEITEM", item);
+ label.setText(tooltip);
+ label.addListener(SWT.MouseExit, this);
+ label.addListener(SWT.MouseDown, this);
+ Point size = tip.computeSize(SWT.DEFAULT, SWT.DEFAULT);
+ Rectangle rect = item.getBounds(0);
+ Point pt = tree.toDisplay(rect.x, rect.y);
+ tip.setBounds(pt.x, pt.y, size.x, size.y);
+ tip.setVisible(true);
+ }
+ }
+ }
+ };
+
+ tree.addListener(SWT.Dispose, listener);
+ tree.addListener(SWT.KeyDown, listener);
+ tree.addListener(SWT.MouseMove, listener);
+ tree.addListener(SWT.MouseHover, listener);
+ }
+
+ /**
+ * Sets up double-click action on the tree.
+ * <p/>
+ * By default, double-click (a.k.a. "default selection") on a valid list item will
+ * show the property view.
+ */
+ private void setupDoubleClick() {
+ final Tree tree = (Tree) getControl();
+
+ tree.addListener(SWT.DefaultSelection, new Listener() {
+ public void handleEvent(Event event) {
+ EclipseUiHelper.showView(EclipseUiHelper.PROPERTY_SHEET_VIEW_ID,
+ true /* activate */);
+ }
+ });
+ }
+
+ // ---------------
+
+ private class UiOutlineActions extends UiActions {
+
+ @Override
+ protected UiDocumentNode getRootNode() {
+ return mEditor.getModel(); // this is LayoutEditor.getUiRootNode()
+ }
+
+ // Select the new item
+ @Override
+ protected void selectUiNode(UiElementNode uiNodeToSelect) {
+ setModelSelection(uiNodeToSelect);
+ }
+
+ @Override
+ public void commitPendingXmlChanges() {
+ // Pass. There is nothing to commit before the XML is changed here.
+ }
+
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/UiElementPullParser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/UiElementPullParser.java
new file mode 100644
index 0000000..b0e6fdb
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/UiElementPullParser.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.layout;
+
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+import com.android.layoutlib.api.IXmlPullParser;
+
+import org.w3c.dom.Node;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * {@link IXmlPullParser} implementation on top of {@link UiElementNode}.
+ * <p/>It's designed to work on layout files, and will most likely not work on other resource
+ * files.
+ */
+public final class UiElementPullParser extends BasePullParser {
+
+ private final ArrayList<UiElementNode> mNodeStack = new ArrayList<UiElementNode>();
+ private UiElementNode mRoot;
+
+ public UiElementPullParser(UiElementNode top) {
+ super();
+ mRoot = top;
+ push(mRoot);
+ }
+
+ private UiElementNode getCurrentNode() {
+ if (mNodeStack.size() > 0) {
+ return mNodeStack.get(mNodeStack.size()-1);
+ }
+
+ return null;
+ }
+
+ private Node getAttribute(int i) {
+ if (mParsingState != START_TAG) {
+ throw new IndexOutOfBoundsException();
+ }
+
+ // get the current uiNode
+ UiElementNode uiNode = getCurrentNode();
+
+ // get its xml node
+ Node xmlNode = uiNode.getXmlNode();
+
+ if (xmlNode != null) {
+ return xmlNode.getAttributes().item(i);
+ }
+
+ return null;
+ }
+
+ private void push(UiElementNode node) {
+ mNodeStack.add(node);
+ }
+
+ private UiElementNode pop() {
+ return mNodeStack.remove(mNodeStack.size()-1);
+ }
+
+ // ------------- IXmlPullParser --------
+
+ /**
+ * {@inheritDoc}
+ *
+ * This implementation returns the underlying DOM node.
+ */
+ public Object getViewKey() {
+ return getCurrentNode();
+ }
+
+ // ------------- XmlPullParser --------
+
+ public String getPositionDescription() {
+ return "XML DOM element depth:" + mNodeStack.size();
+ }
+
+ public int getAttributeCount() {
+ UiElementNode node = getCurrentNode();
+ if (node != null) {
+ return node.getUiAttributes().size();
+ }
+
+ return 0;
+ }
+
+ public String getAttributeName(int i) {
+ Node attribute = getAttribute(i);
+ if (attribute != null) {
+ return attribute.getLocalName();
+ }
+
+ return null;
+ }
+
+ public String getAttributeNamespace(int i) {
+ Node attribute = getAttribute(i);
+ if (attribute != null) {
+ return attribute.getNamespaceURI();
+ }
+ return ""; //$NON-NLS-1$
+ }
+
+ public String getAttributePrefix(int i) {
+ Node attribute = getAttribute(i);
+ if (attribute != null) {
+ return attribute.getPrefix();
+ }
+ return null;
+ }
+
+ public String getAttributeValue(int i) {
+ Node attribute = getAttribute(i);
+ if (attribute != null) {
+ return attribute.getNodeValue();
+ }
+
+ return null;
+ }
+
+ public String getAttributeValue(String namespace, String localName) {
+ // get the current uiNode
+ UiElementNode uiNode = getCurrentNode();
+
+ // get its xml node
+ Node xmlNode = uiNode.getXmlNode();
+
+ if (xmlNode != null) {
+ Node attribute = xmlNode.getAttributes().getNamedItemNS(namespace, localName);
+ if (attribute != null) {
+ return attribute.getNodeValue();
+ }
+ }
+
+ return null;
+ }
+
+ public int getDepth() {
+ return mNodeStack.size();
+ }
+
+ public String getName() {
+ if (mParsingState == START_TAG || mParsingState == END_TAG) {
+ return getCurrentNode().getDescriptor().getXmlLocalName();
+ }
+
+ return null;
+ }
+
+ public String getNamespace() {
+ if (mParsingState == START_TAG || mParsingState == END_TAG) {
+ return getCurrentNode().getDescriptor().getNamespace();
+ }
+
+ return null;
+ }
+
+ public String getPrefix() {
+ if (mParsingState == START_TAG || mParsingState == END_TAG) {
+ // FIXME will NEVER work
+ if (getCurrentNode().getDescriptor().getXmlLocalName().startsWith("android:")) { //$NON-NLS-1$
+ return "android"; //$NON-NLS-1$
+ }
+ }
+
+ return null;
+ }
+
+ public boolean isEmptyElementTag() throws XmlPullParserException {
+ if (mParsingState == START_TAG) {
+ return getCurrentNode().getUiChildren().size() == 0;
+ }
+
+ throw new XmlPullParserException("Call to isEmptyElementTag while not in START_TAG",
+ this, null);
+ }
+
+ @Override
+ public void onNextFromStartDocument() {
+ onNextFromStartTag();
+ }
+
+ @Override
+ public void onNextFromStartTag() {
+ // get the current node, and look for text or children (children first)
+ UiElementNode node = getCurrentNode();
+ List<UiElementNode> children = node.getUiChildren();
+ if (children.size() > 0) {
+ // move to the new child, and don't change the state.
+ push(children.get(0));
+
+ // in case the current state is CURRENT_DOC, we set the proper state.
+ mParsingState = START_TAG;
+ } else {
+ if (mParsingState == START_DOCUMENT) {
+ // this handles the case where there's no node.
+ mParsingState = END_DOCUMENT;
+ } else {
+ mParsingState = END_TAG;
+ }
+ }
+ }
+
+ @Override
+ public void onNextFromEndTag() {
+ // look for a sibling. if no sibling, go back to the parent
+ UiElementNode node = getCurrentNode();
+ node = node.getUiNextSibling();
+ if (node != null) {
+ // to go to the sibling, we need to remove the current node,
+ pop();
+ // and add its sibling.
+ push(node);
+ mParsingState = START_TAG;
+ } else {
+ // move back to the parent
+ pop();
+
+ // we have only one element left (mRoot), then we're done with the document.
+ if (mNodeStack.size() == 1) {
+ mParsingState = END_DOCUMENT;
+ } else {
+ mParsingState = END_TAG;
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/UiPropertySheetPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/UiPropertySheetPage.java
new file mode 100644
index 0000000..8093c90
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/UiPropertySheetPage.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.layout;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.FillLayout;
+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.Tree;
+import org.eclipse.swt.widgets.TreeItem;
+import org.eclipse.ui.views.properties.PropertySheetEntry;
+import org.eclipse.ui.views.properties.PropertySheetPage;
+
+/**
+ * A customized property sheet page for the graphical layout editor.
+ * <p/>
+ * Currently it just provides a custom tooltip to display attributes javadocs.
+ */
+public class UiPropertySheetPage extends PropertySheetPage {
+
+
+ public UiPropertySheetPage() {
+ super();
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ super.createControl(parent);
+
+ setupTooltip();
+ }
+
+ /**
+ * Sets up a custom tooltip when hovering over tree items.
+ * <p/>
+ * The tooltip will display the element's javadoc, if any, or the item's getText otherwise.
+ */
+ private void setupTooltip() {
+ final Tree tree = (Tree) getControl();
+
+ /*
+ * 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() {
+ Shell tip = null;
+ Label label = null;
+
+ public void handleEvent(Event event) {
+ switch(event.type) {
+ case SWT.Dispose:
+ case SWT.KeyDown:
+ case SWT.MouseExit:
+ case SWT.MouseDown:
+ case SWT.MouseMove:
+ if (tip != null) {
+ tip.dispose();
+ tip = null;
+ label = null;
+ }
+ break;
+ case SWT.MouseHover:
+ if (tip != null) {
+ tip.dispose();
+ tip = null;
+ label = null;
+ }
+
+ String tooltip = null;
+
+ TreeItem item = tree.getItem(new Point(event.x, event.y));
+ if (item != null) {
+ Object data = item.getData();
+ if (data instanceof PropertySheetEntry) {
+ tooltip = ((PropertySheetEntry) data).getDescription();
+ }
+
+ if (tooltip == null) {
+ tooltip = item.getText();
+ } else {
+ tooltip = item.getText() + ":\r" + tooltip;
+ }
+ }
+
+ if (tooltip != null) {
+ Shell shell = tree.getShell();
+ Display display = tree.getDisplay();
+
+ tip = new Shell(shell, SWT.ON_TOP | SWT.NO_FOCUS | SWT.TOOL);
+ tip.setBackground(display .getSystemColor(SWT.COLOR_INFO_BACKGROUND));
+ FillLayout layout = new FillLayout();
+ layout.marginWidth = 2;
+ tip.setLayout(layout);
+ label = new Label(tip, SWT.NONE);
+ label.setForeground(display.getSystemColor(SWT.COLOR_INFO_FOREGROUND));
+ label.setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND));
+ label.setData("_TABLEITEM", item);
+ label.setText(tooltip);
+ label.addListener(SWT.MouseExit, this);
+ label.addListener(SWT.MouseDown, this);
+ Point size = tip.computeSize(SWT.DEFAULT, SWT.DEFAULT);
+ Rectangle rect = item.getBounds(0);
+ Point pt = tree.toDisplay(rect.x, rect.y);
+ tip.setBounds(pt.x, pt.y, size.x, size.y);
+ tip.setVisible(true);
+ }
+ }
+ }
+ };
+
+ tree.addListener(SWT.Dispose, listener);
+ tree.addListener(SWT.KeyDown, listener);
+ tree.addListener(SWT.MouseMove, listener);
+ tree.addListener(SWT.MouseHover, listener);
+
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/WidgetPullParser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/WidgetPullParser.java
new file mode 100644
index 0000000..e62ab69
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/WidgetPullParser.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.layout;
+
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.layoutlib.api.IXmlPullParser;
+import com.android.sdklib.SdkConstants;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+/**
+ * {@link IXmlPullParser} implementation to render android widget bitmap.
+ * <p/>The parser emulates a layout that contains just one widget, described by the
+ * {@link ViewElementDescriptor} passed in the constructor.
+ */
+public class WidgetPullParser extends BasePullParser {
+
+ private final ViewElementDescriptor mDescriptor;
+ private String[][] mAttributes = new String[][] {
+ { "text", null },
+ { "layout_width", "wrap_content" },
+ { "layout_height", "wrap_content" },
+ };
+
+ public WidgetPullParser(ViewElementDescriptor descriptor) {
+ mDescriptor = descriptor;
+
+ String[] segments = mDescriptor.getCanonicalClassName().split(AndroidConstants.RE_DOT);
+ mAttributes[0][1] = segments[segments.length-1];
+ }
+
+ public Object getViewKey() {
+ // we need a viewKey or the ILayoutResult will not contain any ILayoutViewInfo
+ return mDescriptor;
+ }
+
+ public int getAttributeCount() {
+ return mAttributes.length; // text attribute
+ }
+
+ public String getAttributeName(int index) {
+ if (index < mAttributes.length) {
+ return mAttributes[index][0];
+ }
+
+ return null;
+ }
+
+ public String getAttributeNamespace(int index) {
+ return SdkConstants.NS_RESOURCES;
+ }
+
+ public String getAttributePrefix(int index) {
+ // pass
+ return null;
+ }
+
+ public String getAttributeValue(int index) {
+ if (index < mAttributes.length) {
+ return mAttributes[index][1];
+ }
+
+ return null;
+ }
+
+ public String getAttributeValue(String ns, String name) {
+ if (SdkConstants.NS_RESOURCES.equals(ns)) {
+ for (String[] attribute : mAttributes) {
+ if (name.equals(attribute[0])) {
+ return attribute[1];
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public int getDepth() {
+ // pass
+ return 0;
+ }
+
+ public String getName() {
+ return mDescriptor.getXmlLocalName();
+ }
+
+ public String getNamespace() {
+ // pass
+ return null;
+ }
+
+ public String getPositionDescription() {
+ // pass
+ return null;
+ }
+
+ public String getPrefix() {
+ // pass
+ return null;
+ }
+
+ public boolean isEmptyElementTag() throws XmlPullParserException {
+ if (mParsingState == START_TAG) {
+ return true;
+ }
+
+ throw new XmlPullParserException("Call to isEmptyElementTag while not in START_TAG",
+ this, null);
+ }
+
+ @Override
+ public void onNextFromStartDocument() {
+ // just go to start_tag
+ mParsingState = START_TAG;
+ }
+
+ @Override
+ public void onNextFromStartTag() {
+ // since we have no children, just go to end_tag
+ mParsingState = END_TAG;
+ }
+
+ @Override
+ public void onNextFromEndTag() {
+ // just one tag. we are done.
+ mParsingState = END_DOCUMENT;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/descriptors/CustomViewDescriptorService.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/descriptors/CustomViewDescriptorService.java
new file mode 100644
index 0000000..d5ee2ca
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/descriptors/CustomViewDescriptorService.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.layout.descriptors;
+
+import com.android.ide.eclipse.adt.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.sdk.Sdk;
+import com.android.ide.eclipse.common.resources.ViewClassInfo;
+import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+import com.android.sdklib.IAndroidTarget;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.ITypeHierarchy;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Service responsible for creating/managing {@link ElementDescriptor} objects for custom
+ * View classes per project.
+ * <p/>
+ * The service provides an on-demand monitoring of custom classes to check for changes. Monitoring
+ * starts once a request for an {@link ElementDescriptor} object has been done for a specific
+ * class.<br>
+ * The monitoring will notify a listen of any changes in the class triggering a change in its
+ * associated {@link ElementDescriptor} object.
+ * <p/>
+ * If the custom class does not exist, no monitoring is put in place to avoid having to listen
+ * to all class changes in the projects.
+ *
+ */
+public final class CustomViewDescriptorService {
+
+ private static CustomViewDescriptorService sThis = new CustomViewDescriptorService();
+
+ /**
+ * Map where keys are the project, and values are another map containing all the known
+ * custom View class for this project. The custom View class are stored in a map
+ * where the keys are the fully qualified class name, and the values are their associated
+ * {@link ElementDescriptor}.
+ */
+ private HashMap<IProject, HashMap<String, ElementDescriptor>> mCustomDescriptorMap =
+ new HashMap<IProject, HashMap<String, ElementDescriptor>>();
+
+ /**
+ * TODO will be used to update the ElementDescriptor of the custom view when it
+ * is modified (either the class itself or its attributes.xml)
+ */
+ @SuppressWarnings("unused")
+ private ICustomViewDescriptorListener mListener;
+
+ /**
+ * Classes which implements this interface provide a method that deal with modifications
+ * in custom View class triggering a change in its associated {@link ViewClassInfo} object.
+ */
+ public interface ICustomViewDescriptorListener {
+ /**
+ * Sent when a custom View class has changed and its {@link ElementDescriptor} was modified.
+ * @param project the project containing the class.
+ * @param className the fully qualified class name.
+ * @param descriptor the updated ElementDescriptor.
+ */
+ public void updatedClassInfo(IProject project, String className, ElementDescriptor descriptor);
+ }
+
+ /**
+ * Returns the singleton instance of {@link CustomViewDescriptorService}.
+ */
+ public static CustomViewDescriptorService getInstance() {
+ return sThis;
+ }
+
+ /**
+ * Sets the listener receiving custom View class modification notifications.
+ * @param listener the listener to receive the notifications.
+ *
+ * TODO will be used to update the ElementDescriptor of the custom view when it
+ * is modified (either the class itself or its attributes.xml)
+ */
+ public void setListener(ICustomViewDescriptorListener listener) {
+ mListener = listener;
+ }
+
+ /**
+ * Returns the {@link ElementDescriptor} for a particular project/class.
+ * <p/>
+ * If it is the first time the <code>ElementDescriptor</code> is requested, the method
+ * will check that the specified class is in fact a custom View class. Once this is
+ * established, a monitoring for that particular class is initiated. Any change will
+ * trigger a notification to the {@link ICustomViewDescriptorListener}.
+ * @param project the project containing the class.
+ * @param fqClassName the fully qualified name of the class.
+ * @return a <code>ElementDescriptor</code> or <code>null</code> if the class was not
+ * a custom View class.
+ */
+ public ElementDescriptor getDescriptor(IProject project, String fqClassName) {
+ // look in the map first
+ synchronized (mCustomDescriptorMap) {
+ HashMap<String, ElementDescriptor> map = mCustomDescriptorMap.get(project);
+
+ if (map != null) {
+ ElementDescriptor descriptor = map.get(fqClassName);
+ if (descriptor != null) {
+ return descriptor;
+ }
+ }
+
+ // if we step here, it looks like we haven't created it yet.
+ // First lets check this is in fact a valid type in the project
+
+ try {
+ // We expect the project to be both opened and of java type (since it's an android
+ // project), so we can create a IJavaProject object from our IProject.
+ IJavaProject javaProject = JavaCore.create(project);
+
+ // replace $ by . in the class name
+ String javaClassName = fqClassName.replaceAll("\\$", "\\."); //$NON-NLS-1$ //$NON-NLS-2$
+
+ // look for the IType object for this class
+ IType type = javaProject.findType(javaClassName);
+ if (type != null && type.exists()) {
+ // the type exists. Let's get the parent class and its ViewClassInfo.
+
+ // get the type hierarchy
+ ITypeHierarchy hierarchy = type.newSupertypeHierarchy(
+ new NullProgressMonitor());
+
+ ElementDescriptor parentDescriptor = getDescriptor(
+ hierarchy.getSuperclass(type), project, hierarchy);
+
+ if (parentDescriptor != null) {
+ // we have a valid parent, lets create a new ElementDescriptor.
+
+ ViewElementDescriptor descriptor = new ViewElementDescriptor(fqClassName,
+ fqClassName, // ui_name
+ fqClassName, // canonical class name
+ null, // tooltip
+ null, // sdk_url
+ getAttributeDescriptor(type, parentDescriptor),
+ null, // layout attributes
+ null, // children
+ false /* mandatory */);
+
+ synchronized (mCustomDescriptorMap) {
+ map = mCustomDescriptorMap.get(project);
+ if (map == null) {
+ map = new HashMap<String, ElementDescriptor>();
+ mCustomDescriptorMap.put(project, map);
+ }
+
+ map.put(fqClassName, descriptor);
+ }
+
+ //TODO setup listener on this resource change.
+
+ return descriptor;
+ }
+ }
+ } catch (JavaModelException e) {
+ // there was an error accessing any of the IType, we'll just return null;
+ }
+ }
+
+
+ return null;
+ }
+
+ /**
+ * Computes (if needed) and returns the {@link ElementDescriptor} for the specified type.
+ *
+ * @param type
+ * @param project
+ * @param typeHierarchy
+ * @return A ViewElementDescriptor or null if type or typeHierarchy is null.
+ */
+ private ViewElementDescriptor getDescriptor(IType type, IProject project,
+ ITypeHierarchy typeHierarchy) {
+ // check if the type is a built-in View class.
+ List<ElementDescriptor> builtInList = null;
+
+ Sdk currentSdk = Sdk.getCurrent();
+ IAndroidTarget target = currentSdk == null ? null : currentSdk.getTarget(project);
+ if (target != null) {
+ AndroidTargetData data = currentSdk.getTargetData(target);
+ builtInList = data.getLayoutDescriptors().getViewDescriptors();
+ }
+
+ // give up if there's no type
+ if (type == null) {
+ return null;
+ }
+
+ String canonicalName = type.getFullyQualifiedName();
+
+ if (builtInList != null) {
+ for (ElementDescriptor desc : builtInList) {
+ if (desc instanceof ViewElementDescriptor) {
+ ViewElementDescriptor viewDescriptor = (ViewElementDescriptor)desc;
+ if (canonicalName.equals(viewDescriptor.getCanonicalClassName())) {
+ return viewDescriptor;
+ }
+ }
+ }
+ }
+
+ // it's not a built-in class? Lets look if the superclass is built-in
+ // give up if there's no type
+ if (typeHierarchy == null) {
+ return null;
+ }
+
+ IType parentType = typeHierarchy.getSuperclass(type);
+ if (parentType != null) {
+ ViewElementDescriptor parentDescriptor = getDescriptor(parentType, project,
+ typeHierarchy);
+
+ if (parentDescriptor != null) {
+ // parent class is a valid View class with a descriptor, so we create one
+ // for this class.
+ ViewElementDescriptor descriptor = new ViewElementDescriptor(canonicalName,
+ canonicalName, // ui_name
+ canonicalName, // canonical name
+ null, // tooltip
+ null, // sdk_url
+ getAttributeDescriptor(type, parentDescriptor),
+ null, // layout attributes
+ null, // children
+ false /* mandatory */);
+
+ // add it to the map
+ synchronized (mCustomDescriptorMap) {
+ HashMap<String, ElementDescriptor> map = mCustomDescriptorMap.get(project);
+
+ if (map == null) {
+ map = new HashMap<String, ElementDescriptor>();
+ mCustomDescriptorMap.put(project, map);
+ }
+
+ map.put(canonicalName, descriptor);
+
+ }
+
+ //TODO setup listener on this resource change.
+
+ return descriptor;
+ }
+ }
+
+ // class is neither a built-in view class, nor extend one. return null.
+ return null;
+ }
+
+ /**
+ * Returns the array of {@link AttributeDescriptor} for the specified {@link IType}.
+ * <p/>
+ * The array should contain the descriptor for this type and all its supertypes.
+ * @param type the type for which the {@link AttributeDescriptor} are returned.
+ * @param parentDescriptor the {@link ElementDescriptor} of the direct superclass.
+ */
+ private AttributeDescriptor[] getAttributeDescriptor(IType type,
+ ElementDescriptor parentDescriptor) {
+ // TODO add the class attribute descriptors to the parent descriptors.
+ return parentDescriptor.getAttributes();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/descriptors/LayoutDescriptors.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/descriptors/LayoutDescriptors.java
new file mode 100644
index 0000000..7caa50f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/descriptors/LayoutDescriptors.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.layout.descriptors;
+
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.common.resources.ViewClassInfo;
+import com.android.ide.eclipse.common.resources.DeclareStyleableInfo.AttributeInfo;
+import com.android.ide.eclipse.common.resources.ViewClassInfo.LayoutParamsInfo;
+import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.editors.descriptors.DocumentDescriptor;
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.editors.descriptors.IDescriptorProvider;
+import com.android.ide.eclipse.editors.descriptors.SeparatorAttributeDescriptor;
+import com.android.sdklib.SdkConstants;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+
+/**
+ * Complete description of the layout structure.
+ */
+public final class LayoutDescriptors implements IDescriptorProvider {
+
+ // Public attributes names, attributes descriptors and elements descriptors
+ public static final String ID_ATTR = "id"; //$NON-NLS-1$
+
+ /** The document descriptor. Contains all layouts and views linked together. */
+ private DocumentDescriptor mDescriptor =
+ new DocumentDescriptor("layout_doc", null); //$NON-NLS-1$
+
+ /** The list of all known ViewLayout descriptors. */
+ private ArrayList<ElementDescriptor> mLayoutDescriptors = new ArrayList<ElementDescriptor>();
+
+ /** Read-Only list of View Descriptors. */
+ private List<ElementDescriptor> mROLayoutDescriptors;
+
+ /** The list of all known View (not ViewLayout) descriptors. */
+ private ArrayList<ElementDescriptor> mViewDescriptors = new ArrayList<ElementDescriptor>();
+
+ /** Read-Only list of View Descriptors. */
+ private List<ElementDescriptor> mROViewDescriptors;
+
+ /** @return the document descriptor. Contains all layouts and views linked together. */
+ public DocumentDescriptor getDescriptor() {
+ return mDescriptor;
+ }
+
+ /** @return The read-only list of all known ViewLayout descriptors. */
+ public List<ElementDescriptor> getLayoutDescriptors() {
+ return mROLayoutDescriptors;
+ }
+
+ /** @return The read-only list of all known View (not ViewLayout) descriptors. */
+ public List<ElementDescriptor> getViewDescriptors() {
+ return mROViewDescriptors;
+ }
+
+ public ElementDescriptor[] getRootElementDescriptors() {
+ return mDescriptor.getChildren();
+ }
+
+ /**
+ * Updates the document descriptor.
+ * <p/>
+ * It first computes the new children of the descriptor and then update them
+ * all at once.
+ * <p/>
+ * TODO: differentiate groups from views in the tree UI? => rely on icons
+ * <p/>
+ *
+ * @param views The list of views in the framework.
+ * @param layouts The list of layouts in the framework.
+ */
+ public synchronized void updateDescriptors(ViewClassInfo[] views, ViewClassInfo[] layouts) {
+ ArrayList<ElementDescriptor> newViews = new ArrayList<ElementDescriptor>();
+ if (views != null) {
+ for (ViewClassInfo info : views) {
+ ElementDescriptor desc = convertView(info);
+ newViews.add(desc);
+ }
+ }
+
+ ArrayList<ElementDescriptor> newLayouts = new ArrayList<ElementDescriptor>();
+ if (layouts != null) {
+ for (ViewClassInfo info : layouts) {
+ ElementDescriptor desc = convertView(info);
+ newLayouts.add(desc);
+ }
+ }
+
+ ArrayList<ElementDescriptor> newDescriptors = new ArrayList<ElementDescriptor>();
+ newDescriptors.addAll(newLayouts);
+ newDescriptors.addAll(newViews);
+ ElementDescriptor[] newArray = newDescriptors.toArray(
+ new ElementDescriptor[newDescriptors.size()]);
+
+ // Link all layouts to everything else here.. recursively
+ for (ElementDescriptor layoutDesc : newLayouts) {
+ layoutDesc.setChildren(newArray);
+ }
+
+ mViewDescriptors = newViews;
+ mLayoutDescriptors = newLayouts;
+ mDescriptor.setChildren(newArray);
+
+ mROLayoutDescriptors = Collections.unmodifiableList(mLayoutDescriptors);
+ mROViewDescriptors = Collections.unmodifiableList(mViewDescriptors);
+ }
+
+ /**
+ * Creates an element descriptor from a given {@link ViewClassInfo}.
+ */
+ private ElementDescriptor convertView(ViewClassInfo info) {
+ String xml_name = info.getShortClassName();
+ String tooltip = info.getJavaDoc();
+
+ // Process all View attributes
+ ArrayList<AttributeDescriptor> attributes = new ArrayList<AttributeDescriptor>();
+ DescriptorsUtils.appendAttributes(attributes,
+ null, // elementName
+ SdkConstants.NS_RESOURCES,
+ info.getAttributes(),
+ null, // requiredAttributes
+ null /* overrides */);
+
+ for (ViewClassInfo link = info.getSuperClass();
+ link != null;
+ link = link.getSuperClass()) {
+ AttributeInfo[] attrList = link.getAttributes();
+ if (attrList.length > 0) {
+ attributes.add(new SeparatorAttributeDescriptor(
+ String.format("Attributes from %1$s", link.getShortClassName())));
+ DescriptorsUtils.appendAttributes(attributes,
+ null, // elementName
+ SdkConstants.NS_RESOURCES,
+ attrList,
+ null, // requiredAttributes
+ null /* overrides */);
+ }
+ }
+
+ // Process all LayoutParams attributes
+ ArrayList<AttributeDescriptor> layoutAttributes = new ArrayList<AttributeDescriptor>();
+ LayoutParamsInfo layoutParams = info.getLayoutData();
+
+ for(; layoutParams != null; layoutParams = layoutParams.getSuperClass()) {
+ boolean need_separator = true;
+ for (AttributeInfo attr_info : layoutParams.getAttributes()) {
+ if (DescriptorsUtils.containsAttribute(layoutAttributes,
+ SdkConstants.NS_RESOURCES, attr_info)) {
+ continue;
+ }
+ if (need_separator) {
+ String title;
+ if (layoutParams.getShortClassName().equals(
+ AndroidConstants.CLASS_LAYOUTPARAMS)) {
+ title = String.format("Layout Attributes from %1$s",
+ layoutParams.getViewLayoutClass().getShortClassName());
+ } else {
+ title = String.format("Layout Attributes from %1$s (%2$s)",
+ layoutParams.getViewLayoutClass().getShortClassName(),
+ layoutParams.getShortClassName());
+ }
+ layoutAttributes.add(new SeparatorAttributeDescriptor(title));
+ need_separator = false;
+ }
+ DescriptorsUtils.appendAttribute(layoutAttributes,
+ null, // elementName
+ SdkConstants.NS_RESOURCES,
+ attr_info,
+ false, // required
+ null /* overrides */);
+ }
+ }
+
+ return new ViewElementDescriptor(xml_name,
+ xml_name, // ui_name
+ info.getCanonicalClassName(),
+ tooltip,
+ null, // sdk_url
+ attributes.toArray(new AttributeDescriptor[attributes.size()]),
+ layoutAttributes.toArray(new AttributeDescriptor[layoutAttributes.size()]),
+ null, // children
+ false /* mandatory */);
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/descriptors/ViewElementDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/descriptors/ViewElementDescriptor.java
new file mode 100644
index 0000000..d718ebd
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/descriptors/ViewElementDescriptor.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.layout.descriptors;
+
+import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.editors.layout.uimodel.UiViewElementNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+/**
+ * {@link ViewElementDescriptor} describes the properties expected for a given XML element node
+ * representing a class in an XML Layout file.
+ *
+ * @see ElementDescriptor
+ */
+public final class ViewElementDescriptor extends ElementDescriptor {
+
+ private String mCanonicalClassName;
+
+ /** The list of layout attributes. Can be empty but not null. */
+ private AttributeDescriptor[] mLayoutAttributes;
+
+
+ /**
+ * Constructs a new {@link ViewElementDescriptor} based on its XML name, UI name,
+ * the canonical name of the class it represents, its tooltip, its SDK url, its attributes list,
+ * its children list and its mandatory flag.
+ *
+ * @param xml_name The XML element node name. Case sensitive.
+ * @param ui_name The XML element name for the user interface, typically capitalized.
+ * @param canonicalClassName The canonical class name the {@link ViewElementDescriptor} is
+ * representing.
+ * @param tooltip An optional tooltip. Can be null or empty.
+ * @param sdk_url An optional SKD URL. Can be null or empty.
+ * @param attributes The list of allowed attributes. Can be null or empty.
+ * @param layoutAttributes The list of layout attributes. Can be null or empty.
+ * @param children The list of allowed children. Can be null or empty.
+ * @param mandatory Whether this node must always exist (even for empty models). A mandatory
+ * UI node is never deleted and it may lack an actual XML node attached. A non-mandatory
+ * UI node MUST have an XML node attached and it will cease to exist when the XML node
+ * ceases to exist.
+ */
+ public ViewElementDescriptor(String xml_name, String ui_name,
+ String canonicalClassName,
+ String tooltip, String sdk_url,
+ AttributeDescriptor[] attributes, AttributeDescriptor[] layoutAttributes,
+ ElementDescriptor[] children, boolean mandatory) {
+ super(xml_name, ui_name, tooltip, sdk_url, attributes, children, mandatory);
+ mCanonicalClassName = canonicalClassName;
+ mLayoutAttributes = layoutAttributes != null ? layoutAttributes : new AttributeDescriptor[0];
+ }
+
+ /**
+ * Constructs a new {@link ElementDescriptor} based on its XML name, the canonical
+ * name of the class it represents, and its children list.
+ * The UI name is build by capitalizing the XML name.
+ * The UI nodes will be non-mandatory.
+ *
+ * @param xml_name The XML element node name. Case sensitive.
+ * @param canonicalClassName The canonical class name the {@link ViewElementDescriptor} is
+ * representing.
+ * @param children The list of allowed children. Can be null or empty.
+ * @param mandatory Whether this node must always exist (even for empty models). A mandatory
+ * UI node is never deleted and it may lack an actual XML node attached. A non-mandatory
+ * UI node MUST have an XML node attached and it will cease to exist when the XML node
+ * ceases to exist.
+ */
+ public ViewElementDescriptor(String xml_name, String canonicalClassName,
+ ElementDescriptor[] children,
+ boolean mandatory) {
+ super(xml_name, children, mandatory);
+ mCanonicalClassName = canonicalClassName;
+ }
+
+ /**
+ * Constructs a new {@link ElementDescriptor} based on its XML name and children list.
+ * The UI name is build by capitalizing the XML name.
+ * The UI nodes will be non-mandatory.
+ *
+ * @param xml_name The XML element node name. Case sensitive.
+ * @param canonicalClassName The canonical class name the {@link ViewElementDescriptor} is
+ * representing.
+ * @param children The list of allowed children. Can be null or empty.
+ */
+ public ViewElementDescriptor(String xml_name, String canonicalClassName,
+ ElementDescriptor[] children) {
+ super(xml_name, children);
+ mCanonicalClassName = canonicalClassName;
+ }
+
+ /**
+ * Constructs a new {@link ElementDescriptor} based on its XML name and on the canonical
+ * name of the class it represents.
+ * The UI name is build by capitalizing the XML name.
+ * The UI nodes will be non-mandatory.
+ *
+ * @param xml_name The XML element node name. Case sensitive.
+ * @param canonicalClassName The canonical class name the {@link ViewElementDescriptor} is
+ * representing.
+ */
+ public ViewElementDescriptor(String xml_name, String canonicalClassName) {
+ super(xml_name);
+ mCanonicalClassName = canonicalClassName;
+ }
+
+ /**
+ * Returns the canonical name of the class represented by this element descriptor.
+ */
+ public String getCanonicalClassName() {
+ return mCanonicalClassName;
+ }
+
+ /** Returns the list of layout attributes. Can be empty but not null. */
+ public AttributeDescriptor[] getLayoutAttributes() {
+ return mLayoutAttributes;
+ }
+
+ /**
+ * @return A new {@link UiViewElementNode} linked to this descriptor.
+ */
+ @Override
+ public UiElementNode createUiNode() {
+ return new UiViewElementNode(this);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/DropFeedback.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/DropFeedback.java
new file mode 100644
index 0000000..6e79d64
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/DropFeedback.java
@@ -0,0 +1,761 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.layout.parts;
+
+import com.android.ide.eclipse.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.editors.layout.LayoutConstants;
+import com.android.ide.eclipse.editors.layout.LayoutEditor.UiEditorActions;
+import com.android.ide.eclipse.editors.layout.parts.UiLayoutEditPart.HighlightInfo;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+import org.eclipse.draw2d.geometry.Point;
+import org.eclipse.draw2d.geometry.Rectangle;
+
+import java.util.HashMap;
+import java.util.Map.Entry;
+
+/**
+ * Utility methods used when dealing with dropping EditPart on the GLE.
+ * <p/>
+ * This class uses some temporary static storage to avoid excessive allocations during
+ * drop operations. It is expected to only be invoked from the main UI thread with no
+ * concurrent access.
+ */
+class DropFeedback {
+
+ private static final int TOP = 0;
+ private static final int LEFT = 1;
+ private static final int BOTTOM = 2;
+ private static final int RIGHT = 3;
+ private static final int MAX_DIR = RIGHT;
+
+ private static final int sOppositeDirection[] = { BOTTOM, RIGHT, TOP, LEFT };
+
+ private static final UiElementEditPart sTempClosests[] = new UiElementEditPart[4];
+ private static final int sTempMinDists[] = new int[4];
+
+
+ /**
+ * Target information computed from a drop on a RelativeLayout.
+ * We need only one instance of this and it is sRelativeInfo.
+ */
+ private static class RelativeInfo {
+ /** The two target parts 0 and 1. They can be null, meaning a border is used.
+ * The direction from part 0 to 1 is always to-the-right or to-the-bottom. */
+ final UiElementEditPart targetParts[] = new UiElementEditPart[2];
+ /** Direction from the anchor part to the drop point. */
+ int direction;
+ /** The index of the "anchor" part, i.e. the closest one selected by the drop.
+ * This can be either 0 or 1. The corresponding part can be null. */
+ int anchorIndex;
+ }
+
+ /** The single RelativeInfo used to compute results from a drop on a RelativeLayout */
+ private static final RelativeInfo sRelativeInfo = new RelativeInfo();
+ /** A temporary array of 2 {@link UiElementEditPart} to avoid allocations. */
+ private static final UiElementEditPart sTempTwoParts[] = new UiElementEditPart[2];
+
+
+ private DropFeedback() {
+ }
+
+
+ //----- Package methods called by users of this helper class -----
+
+
+ /**
+ * This method is used by {@link ElementCreateCommand#execute()} when a new item
+ * needs to be "dropped" in the current XML document. It creates the new item using
+ * the given descriptor as a child of the given parent part.
+ *
+ * @param parentPart The parent part.
+ * @param descriptor The descriptor for the new XML element.
+ * @param where The drop location (in parent coordinates)
+ * @param actions The helper that actually modifies the XML model.
+ */
+ static void addElementToXml(UiElementEditPart parentPart,
+ ElementDescriptor descriptor, Point where,
+ UiEditorActions actions) {
+
+ String layoutXmlName = getXmlLocalName(parentPart);
+ RelativeInfo info = null;
+ UiElementEditPart sibling = null;
+
+ if (LayoutConstants.LINEAR_LAYOUT.equals(layoutXmlName)) {
+ sibling = findLinearTarget(parentPart, where)[1];
+
+ } else if (LayoutConstants.RELATIVE_LAYOUT.equals(layoutXmlName)) {
+ info = findRelativeTarget(parentPart, where, sRelativeInfo);
+ if (info != null) {
+ sibling = info.targetParts[info.anchorIndex];
+ sibling = getNextUiSibling(sibling);
+ }
+ }
+
+ if (actions != null) {
+ UiElementNode uiSibling = sibling != null ? sibling.getUiNode() : null;
+ UiElementNode uiParent = parentPart.getUiNode();
+ UiElementNode uiNode = actions.addElement(uiParent, uiSibling, descriptor,
+ false /*updateLayout*/);
+
+ if (LayoutConstants.ABSOLUTE_LAYOUT.equals(layoutXmlName)) {
+ adjustAbsoluteAttributes(uiNode, where);
+ } else if (LayoutConstants.RELATIVE_LAYOUT.equals(layoutXmlName)) {
+ adustRelativeAttributes(uiNode, info);
+ }
+ }
+ }
+
+ /**
+ * This method is used by {@link UiLayoutEditPart#showDropTarget(Point)} to compute
+ * highlight information when a drop target is moved over a valid drop area.
+ * <p/>
+ * Since there are no "out" parameters in Java, all the information is returned
+ * via the {@link HighlightInfo} structure passed as parameter.
+ *
+ * @param parentPart The parent part, always a layout.
+ * @param highlightInfo A structure where result is stored to perform highlight.
+ * @param where The target drop point, in parent's coordinates
+ * @return The {@link HighlightInfo} structured passed as a parameter, for convenience.
+ */
+ static HighlightInfo computeDropFeedback(UiLayoutEditPart parentPart,
+ HighlightInfo highlightInfo,
+ Point where) {
+ String layoutType = getXmlLocalName(parentPart);
+
+ if (LayoutConstants.ABSOLUTE_LAYOUT.equals(layoutType)) {
+ highlightInfo.anchorPoint = where;
+
+ } else if (LayoutConstants.LINEAR_LAYOUT.equals(layoutType)) {
+ boolean isVertical = isVertical(parentPart);
+
+ highlightInfo.childParts = findLinearTarget(parentPart, where);
+ computeLinearLine(parentPart, isVertical, highlightInfo);
+
+ } else if (LayoutConstants.RELATIVE_LAYOUT.equals(layoutType)) {
+
+ RelativeInfo info = findRelativeTarget(parentPart, where, sRelativeInfo);
+ if (info != null) {
+ highlightInfo.childParts = sRelativeInfo.targetParts;
+ computeRelativeLine(parentPart, info, highlightInfo);
+ }
+ }
+
+ return highlightInfo;
+ }
+
+
+ //----- Misc utilities -----
+
+ /**
+ * Returns the next UI sibling of this part, i.e. the element which is just after in
+ * the UI/XML order in the same parent. Returns null if there's no such part.
+ * <p/>
+ * Note: by "UI sibling" here we mean the sibling in the UiNode hierarchy. By design the
+ * UiNode model has the <em>exact</em> same order as the XML model. This has nothing to do
+ * with the "user interface" order that you see on the rendered Android layouts (e.g. for
+ * LinearLayout they are the same but for AbsoluteLayout or RelativeLayout the UI/XML model
+ * order can be vastly different from the user interface order.)
+ */
+ private static UiElementEditPart getNextUiSibling(UiElementEditPart part) {
+ if (part != null) {
+ UiElementNode uiNode = part.getUiNode();
+ if (uiNode != null) {
+ uiNode = uiNode.getUiNextSibling();
+ }
+ if (uiNode != null) {
+ for (Object childPart : part.getParent().getChildren()) {
+ if (childPart instanceof UiElementEditPart &&
+ ((UiElementEditPart) childPart).getUiNode() == uiNode) {
+ return (UiElementEditPart) childPart;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the XML local name of the ui node associated with this edit part or null.
+ */
+ private static String getXmlLocalName(UiElementEditPart editPart) {
+ UiElementNode uiNode = editPart.getUiNode();
+ if (uiNode != null) {
+ ElementDescriptor desc = uiNode.getDescriptor();
+ if (desc != null) {
+ return desc.getXmlLocalName();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Adjusts the attributes of a new node dropped in an AbsoluteLayout.
+ *
+ * @param uiNode The new node being dropped.
+ * @param where The drop location (in parent coordinates)
+ */
+ private static void adjustAbsoluteAttributes(final UiElementNode uiNode, final Point where) {
+ if (where == null) {
+ return;
+ }
+ uiNode.getEditor().editXmlModel(new Runnable() {
+ public void run() {
+ uiNode.setAttributeValue(LayoutConstants.ATTR_LAYOUT_X,
+ String.format(LayoutConstants.VALUE_N_DIP, where.x),
+ false /* override */);
+ uiNode.setAttributeValue(LayoutConstants.ATTR_LAYOUT_Y,
+ String.format(LayoutConstants.VALUE_N_DIP, where.y),
+ false /* override */);
+
+ uiNode.commitDirtyAttributesToXml();
+ }
+ });
+ }
+
+ /**
+ * Adjusts the attributes of a new node dropped in a RelativeLayout:
+ * <ul>
+ * <li> anchor part: the one the user selected (or the closest) and to which the new one
+ * will "attach". The anchor part can be null, either because the layout is currently
+ * empty or the user is attaching to an existing empty border.
+ * <li> direction: the direction from the anchor part to the drop point. That's also the
+ * direction from the anchor part to the new part.
+ * <li> the new node; it is created either after the anchor for right or top directions
+ * or before the anchor for left or bottom directions. This means the new part can
+ * reference the id of the anchor part.
+ * </ul>
+ *
+ * Several cases:
+ * <ul>
+ * <li> set: layout_above/below/toLeftOf/toRightOf to point to the anchor.
+ * <li> copy: layout_centerHorizontal for top/bottom directions
+ * <li> copy: layout_centerVertical for left/right directions.
+ * <li> copy: layout_above/below/toLeftOf/toRightOf for the orthogonal direction
+ * (i.e. top/bottom or left/right.)
+ * </ul>
+ *
+ * @param uiNode The new node being dropped.
+ * @param info The context computed by {@link #findRelativeTarget(UiElementEditPart, Point, RelativeInfo)}.
+ */
+ private static void adustRelativeAttributes(final UiElementNode uiNode, RelativeInfo info) {
+ if (uiNode == null || info == null) {
+ return;
+ }
+
+ final UiElementEditPart anchorPart = info.targetParts[info.anchorIndex]; // can be null
+ final int direction = info.direction;
+
+ uiNode.getEditor().editXmlModel(new Runnable() {
+ public void run() {
+ HashMap<String, String> map = new HashMap<String, String>();
+
+ UiElementNode anchorUiNode = anchorPart != null ? anchorPart.getUiNode() : null;
+ String anchorId = anchorUiNode != null
+ ? anchorUiNode.getAttributeValue("id") //$NON-NLS-1$
+ : null;
+
+ if (anchorId == null) {
+ anchorId = DescriptorsUtils.getFreeWidgetId(anchorUiNode);
+ anchorUiNode.setAttributeValue("id", anchorId, true /*override*/); //$NON-NLS-1$
+ }
+
+ if (anchorId != null) {
+ switch(direction) {
+ case TOP:
+ map.put(LayoutConstants.ATTR_LAYOUT_ABOVE, anchorId);
+ break;
+ case BOTTOM:
+ map.put(LayoutConstants.ATTR_LAYOUT_BELOW, anchorId);
+ break;
+ case LEFT:
+ map.put(LayoutConstants.ATTR_LAYOUT_TO_LEFT_OF, anchorId);
+ break;
+ case RIGHT:
+ map.put(LayoutConstants.ATTR_LAYOUT_TO_RIGHT_OF, anchorId);
+ break;
+ }
+
+ switch(direction) {
+ case TOP:
+ case BOTTOM:
+ map.put(LayoutConstants.ATTR_LAYOUT_CENTER_HORIZONTAL,
+ anchorUiNode.getAttributeValue(
+ LayoutConstants.ATTR_LAYOUT_CENTER_HORIZONTAL));
+
+ map.put(LayoutConstants.ATTR_LAYOUT_TO_LEFT_OF,
+ anchorUiNode.getAttributeValue(
+ LayoutConstants.ATTR_LAYOUT_TO_LEFT_OF));
+ map.put(LayoutConstants.ATTR_LAYOUT_TO_RIGHT_OF,
+ anchorUiNode.getAttributeValue(
+ LayoutConstants.ATTR_LAYOUT_TO_RIGHT_OF));
+ break;
+ case LEFT:
+ case RIGHT:
+ map.put(LayoutConstants.ATTR_LAYOUT_CENTER_VERTICAL,
+ anchorUiNode.getAttributeValue(
+ LayoutConstants.ATTR_LAYOUT_CENTER_VERTICAL));
+ map.put(LayoutConstants.ATTR_LAYOUT_ALIGN_BASELINE,
+ anchorUiNode.getAttributeValue(
+ LayoutConstants.ATTR_LAYOUT_ALIGN_BASELINE));
+
+ map.put(LayoutConstants.ATTR_LAYOUT_ABOVE,
+ anchorUiNode.getAttributeValue(LayoutConstants.ATTR_LAYOUT_ABOVE));
+ map.put(LayoutConstants.ATTR_LAYOUT_BELOW,
+ anchorUiNode.getAttributeValue(LayoutConstants.ATTR_LAYOUT_BELOW));
+ break;
+ }
+ } else {
+ // We don't have an anchor node. Assume we're targeting a border and align
+ // to the parent.
+ switch(direction) {
+ case TOP:
+ map.put(LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_TOP,
+ LayoutConstants.VALUE_TRUE);
+ break;
+ case BOTTOM:
+ map.put(LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_BOTTOM,
+ LayoutConstants.VALUE_TRUE);
+ break;
+ case LEFT:
+ map.put(LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT,
+ LayoutConstants.VALUE_TRUE);
+ break;
+ case RIGHT:
+ map.put(LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT,
+ LayoutConstants.VALUE_TRUE);
+ break;
+ }
+ }
+
+ for (Entry<String, String> entry : map.entrySet()) {
+ uiNode.setAttributeValue(entry.getKey(), entry.getValue(), true /* override */);
+ }
+ uiNode.commitDirtyAttributesToXml();
+ }
+ });
+ }
+
+
+ //----- LinearLayout --------
+
+ /**
+ * For a given parent edit part that MUST represent a LinearLayout, finds the
+ * element before which the location points.
+ * <p/>
+ * This computes the edit part that corresponds to what will be the "next sibling" of the new
+ * element.
+ * <p/>
+ * It returns null if it can't be determined, in which case the element will be added at the
+ * end of the parent child list.
+ *
+ * @return The edit parts that correspond to what will be the "prev" and "next sibling" of the
+ * new element. The previous sibling can be null if adding before the first element.
+ * The next sibling can be null if adding after the last element.
+ */
+ private static UiElementEditPart[] findLinearTarget(UiElementEditPart parent, Point point) {
+ // default orientation is horizontal
+ boolean isVertical = isVertical(parent);
+
+ int target = isVertical ? point.y : point.x;
+
+ UiElementEditPart prev = null;
+ UiElementEditPart next = null;
+
+ for (Object child : parent.getChildren()) {
+ if (child instanceof UiElementEditPart) {
+ UiElementEditPart childPart = (UiElementEditPart) child;
+ Point p = childPart.getBounds().getCenter();
+ int middle = isVertical ? p.y : p.x;
+ if (target < middle) {
+ next = childPart;
+ break;
+ }
+ prev = childPart;
+ }
+ }
+
+ sTempTwoParts[0] = prev;
+ sTempTwoParts[1] = next;
+ return sTempTwoParts;
+ }
+
+ /**
+ * Computes the highlight line between two parts.
+ * <p/>
+ * The two parts are listed in HighlightInfo.childParts[2]. Any of the parts
+ * can be null.
+ * The result is stored in HighlightInfo.
+ * <p/>
+ * Caller must clear the HighlightInfo as appropriate before this call.
+ *
+ * @param parentPart The parent part, always a layout.
+ * @param isVertical True for vertical parts, thus computing an horizontal line.
+ * @param highlightInfo The in-out highlight info.
+ */
+ private static void computeLinearLine(UiLayoutEditPart parentPart,
+ boolean isVertical, HighlightInfo highlightInfo) {
+ Rectangle r = parentPart.getBounds();
+
+ if (isVertical) {
+ Point p = null;
+ UiElementEditPart part = highlightInfo.childParts[0];
+ if (part != null) {
+ p = part.getBounds().getBottom();
+ } else {
+ part = highlightInfo.childParts[1];
+ if (part != null) {
+ p = part.getBounds().getTop();
+ }
+ }
+ if (p != null) {
+ // horizontal line with middle anchor point
+ highlightInfo.tempPoints[0].setLocation(0, p.y);
+ highlightInfo.tempPoints[1].setLocation(r.width, p.y);
+ highlightInfo.linePoints = highlightInfo.tempPoints;
+ highlightInfo.anchorPoint = p.setLocation(r.width / 2, p.y);
+ }
+ } else {
+ Point p = null;
+ UiElementEditPart part = highlightInfo.childParts[0];
+ if (part != null) {
+ p = part.getBounds().getRight();
+ } else {
+ part = highlightInfo.childParts[1];
+ if (part != null) {
+ p = part.getBounds().getLeft();
+ }
+ }
+ if (p != null) {
+ // vertical line with middle anchor point
+ highlightInfo.tempPoints[0].setLocation(p.x, 0);
+ highlightInfo.tempPoints[1].setLocation(p.x, r.height);
+ highlightInfo.linePoints = highlightInfo.tempPoints;
+ highlightInfo.anchorPoint = p.setLocation(p.x, r.height / 2);
+ }
+ }
+ }
+
+ /**
+ * Returns true if the linear layout is marked as vertical.
+ *
+ * @param parent The a layout part that must be a LinearLayout
+ * @return True if the linear layout has a vertical orientation attribute.
+ */
+ private static boolean isVertical(UiElementEditPart parent) {
+ String orientation = parent.getStringAttr("orientation"); //$NON-NLS-1$
+ boolean isVertical = "vertical".equals(orientation) || //$NON-NLS-1$
+ "1".equals(orientation); //$NON-NLS-1$
+ return isVertical;
+ }
+
+
+ //----- RelativeLayout --------
+
+ /**
+ * Finds the "target" relative layout item for the drop operation & feedback.
+ * <p/>
+ * If the drop point is exactly on a current item, simply returns the side the drop will occur
+ * compared to the center of that element. For the actual XML, we'll need to insert *after*
+ * that element to make sure that referenced are defined in the right order.
+ * In that case the result contains two elements, the second one always being on the right or
+ * bottom side of the first one. When insert in XML, we want to insert right before that
+ * second element or at the end of the child list if the second element is null.
+ * <p/>
+ * If the drop point is not exactly on a current element, find the closest in each
+ * direction and align with the two closest of these.
+ *
+ * @return null if we fail to find anything (such as there are currently no items to compare
+ * with); otherwise fills the {@link RelativeInfo} and return it.
+ */
+ private static RelativeInfo findRelativeTarget(UiElementEditPart parent,
+ Point point,
+ RelativeInfo outInfo) {
+
+ for (int i = 0; i < 4; i++) {
+ sTempMinDists[i] = Integer.MAX_VALUE;
+ sTempClosests[i] = null;
+ }
+
+
+ for (Object child : parent.getChildren()) {
+ if (child instanceof UiElementEditPart) {
+ UiElementEditPart childPart = (UiElementEditPart) child;
+ Rectangle r = childPart.getBounds();
+ if (r.contains(point)) {
+
+ float rx = ((float)(point.x - r.x) / (float)r.width ) - 0.5f;
+ float ry = ((float)(point.y - r.y) / (float)r.height) - 0.5f;
+
+ /* TOP
+ * \ /
+ * \ /
+ * L X R
+ * / \
+ * / \
+ * BOT
+ */
+
+ int index = 0;
+ if (Math.abs(rx) >= Math.abs(ry)) {
+ if (rx < 0) {
+ outInfo.direction = LEFT;
+ index = 1;
+ } else {
+ outInfo.direction = RIGHT;
+ }
+ } else {
+ if (ry < 0) {
+ outInfo.direction = TOP;
+ index = 1;
+ } else {
+ outInfo.direction = BOTTOM;
+ }
+ }
+
+ outInfo.anchorIndex = index;
+ outInfo.targetParts[index] = childPart;
+ outInfo.targetParts[1 - index] = findClosestPart(childPart,
+ outInfo.direction);
+
+ return outInfo;
+ }
+
+ computeClosest(point, childPart, sTempClosests, sTempMinDists, TOP);
+ computeClosest(point, childPart, sTempClosests, sTempMinDists, LEFT);
+ computeClosest(point, childPart, sTempClosests, sTempMinDists, BOTTOM);
+ computeClosest(point, childPart, sTempClosests, sTempMinDists, RIGHT);
+ }
+ }
+
+ UiElementEditPart closest = null;
+ int minDist = Integer.MAX_VALUE;
+ int minDir = -1;
+
+ for (int i = 0; i <= MAX_DIR; i++) {
+ if (sTempClosests[i] != null && sTempMinDists[i] < minDist) {
+ closest = sTempClosests[i];
+ minDist = sTempMinDists[i];
+ minDir = i;
+ }
+ }
+
+ if (closest != null) {
+ int index = 0;
+ switch(minDir) {
+ case TOP:
+ case LEFT:
+ index = 0;
+ break;
+ case BOTTOM:
+ case RIGHT:
+ index = 1;
+ break;
+ }
+ outInfo.anchorIndex = index;
+ outInfo.targetParts[index] = closest;
+ outInfo.targetParts[1 - index] = findClosestPart(closest, sOppositeDirection[minDir]);
+ outInfo.direction = sOppositeDirection[minDir];
+ return outInfo;
+ }
+
+ return null;
+ }
+
+ /**
+ * Computes the highlight line for a drop on a RelativeLayout.
+ * <p/>
+ * The line is always placed on the side of the anchor part indicated by the
+ * direction. The direction always point from the anchor part to the drop point.
+ * <p/>
+ * If there's no anchor part, use the other one with a reversed direction.
+ * <p/>
+ * On output, this updates the {@link HighlightInfo}.
+ */
+ private static void computeRelativeLine(UiLayoutEditPart parentPart,
+ RelativeInfo relInfo,
+ HighlightInfo highlightInfo) {
+
+ UiElementEditPart[] parts = relInfo.targetParts;
+ int dir = relInfo.direction;
+ int index = relInfo.anchorIndex;
+ UiElementEditPart part = parts[index];
+
+ if (part == null) {
+ dir = sOppositeDirection[dir];
+ part = parts[1 - index];
+ }
+ if (part == null) {
+ // give up if both parts are null
+ return;
+ }
+
+ Rectangle r = part.getBounds();
+ Point p = null;
+ switch(dir) {
+ case TOP:
+ p = r.getTop();
+ break;
+ case BOTTOM:
+ p = r.getBottom();
+ break;
+ case LEFT:
+ p = r.getLeft();
+ break;
+ case RIGHT:
+ p = r.getRight();
+ break;
+ }
+
+ highlightInfo.anchorPoint = p;
+
+ r = parentPart.getBounds();
+ switch(dir) {
+ case TOP:
+ case BOTTOM:
+ // horizontal line with middle anchor point
+ highlightInfo.tempPoints[0].setLocation(0, p.y);
+ highlightInfo.tempPoints[1].setLocation(r.width, p.y);
+ highlightInfo.linePoints = highlightInfo.tempPoints;
+ highlightInfo.anchorPoint = p;
+ break;
+ case LEFT:
+ case RIGHT:
+ // vertical line with middle anchor point
+ highlightInfo.tempPoints[0].setLocation(p.x, 0);
+ highlightInfo.tempPoints[1].setLocation(p.x, r.height);
+ highlightInfo.linePoints = highlightInfo.tempPoints;
+ highlightInfo.anchorPoint = p;
+ break;
+ }
+ }
+
+ /**
+ * Given a certain reference point (drop point), computes the distance to the given
+ * part in the given direction. For example if direction is top, only accepts parts which
+ * bottom is above the reference point, computes their distance and then updates the
+ * current minimal distances and current closest parts arrays accordingly.
+ */
+ private static void computeClosest(Point refPoint,
+ UiElementEditPart compareToPart,
+ UiElementEditPart[] currClosests,
+ int[] currMinDists,
+ int direction) {
+ Rectangle r = compareToPart.getBounds();
+
+ Point p = null;
+ boolean usable = false;
+
+ switch(direction) {
+ case TOP:
+ p = r.getBottom();
+ usable = p.y <= refPoint.y;
+ break;
+ case BOTTOM:
+ p = r.getTop();
+ usable = p.y >= refPoint.y;
+ break;
+ case LEFT:
+ p = r.getRight();
+ usable = p.x <= refPoint.x;
+ break;
+ case RIGHT:
+ p = r.getLeft();
+ usable = p.x >= refPoint.x;
+ break;
+ }
+
+ if (usable) {
+ int d = p.getDistance2(refPoint);
+ if (d < currMinDists[direction]) {
+ currMinDists[direction] = d;
+ currClosests[direction] = compareToPart;
+ }
+ }
+ }
+
+ /**
+ * Given a reference parts, finds the closest part in the parent in the given direction.
+ * For example if direction is top, finds the closest sibling part which is above the
+ * reference part and non-overlapping (they can touch.)
+ */
+ private static UiElementEditPart findClosestPart(UiElementEditPart referencePart,
+ int direction) {
+ if (referencePart == null || referencePart.getParent() == null) {
+ return null;
+ }
+
+ Rectangle r = referencePart.getBounds();
+ Point ref = null;
+ switch(direction) {
+ case TOP:
+ ref = r.getTop();
+ break;
+ case BOTTOM:
+ ref = r.getBottom();
+ break;
+ case LEFT:
+ ref = r.getLeft();
+ break;
+ case RIGHT:
+ ref = r.getRight();
+ break;
+ }
+
+ int minDist = Integer.MAX_VALUE;
+ UiElementEditPart closestPart = null;
+
+ for (Object childPart : referencePart.getParent().getChildren()) {
+ if (childPart != referencePart && childPart instanceof UiElementEditPart) {
+ r = ((UiElementEditPart) childPart).getBounds();
+ Point p = null;
+ boolean usable = false;
+
+ switch(direction) {
+ case TOP:
+ p = r.getBottom();
+ usable = p.y <= ref.y;
+ break;
+ case BOTTOM:
+ p = r.getTop();
+ usable = p.y >= ref.y;
+ break;
+ case LEFT:
+ p = r.getRight();
+ usable = p.x <= ref.x;
+ break;
+ case RIGHT:
+ p = r.getLeft();
+ usable = p.x >= ref.x;
+ break;
+ }
+
+ if (usable) {
+ int d = p.getDistance2(ref);
+ if (d < minDist) {
+ minDist = d;
+ closestPart = (UiElementEditPart) childPart;
+ }
+ }
+ }
+ }
+
+ return closestPart;
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/ElementCreateCommand.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/ElementCreateCommand.java
new file mode 100644
index 0000000..d36d9f7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/ElementCreateCommand.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.ide.eclipse.editors.layout.parts;
+
+import com.android.ide.eclipse.editors.AndroidEditor;
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.editors.layout.LayoutEditor;
+import com.android.ide.eclipse.editors.layout.LayoutEditor.UiEditorActions;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+import org.eclipse.draw2d.geometry.Point;
+import org.eclipse.gef.commands.Command;
+
+/**
+ * A command that knows how to instantiate a new element based on a given {@link ElementDescriptor},
+ * the parent {@link UiElementEditPart} and an optional target location.
+ */
+public class ElementCreateCommand extends Command {
+
+ /** Descriptor of the new element to create */
+ private final ElementDescriptor mDescriptor;
+ /** The edit part that hosts the new edit part */
+ private final UiElementEditPart mParentPart;
+ /** The drop location in parent coordinates */
+ private final Point mTargetPoint;
+
+ /**
+ * Creates a new {@link ElementCreateCommand}.
+ *
+ * @param descriptor Descriptor of the new element to create
+ * @param targetPart The edit part that hosts the new edit part
+ * @param targetPoint The drop location in parent coordinates
+ */
+ public ElementCreateCommand(ElementDescriptor descriptor,
+ UiElementEditPart targetPart, Point targetPoint) {
+ mDescriptor = descriptor;
+ mParentPart = targetPart;
+ mTargetPoint = targetPoint;
+ }
+
+ // --- Methods inherited from Command ---
+
+ @Override
+ public boolean canExecute() {
+ return mDescriptor != null &&
+ mParentPart != null &&
+ mParentPart.getUiNode() != null &&
+ mParentPart.getUiNode().getEditor() instanceof LayoutEditor;
+ }
+
+ @Override
+ public void execute() {
+ super.execute();
+ UiElementNode uiParent = mParentPart.getUiNode();
+ if (uiParent != null) {
+ final AndroidEditor editor = uiParent.getEditor();
+ if (editor instanceof LayoutEditor) {
+ ((LayoutEditor) editor).wrapUndoRecording(
+ String.format("Create %1$s", mDescriptor.getXmlLocalName()),
+ new Runnable() {
+ public void run() {
+ UiEditorActions actions = ((LayoutEditor) editor).getUiEditorActions();
+ if (actions != null) {
+ DropFeedback.addElementToXml(mParentPart, mDescriptor, mTargetPoint,
+ actions);
+ }
+ }
+ });
+ }
+ }
+ }
+
+ @Override
+ public void redo() {
+ throw new UnsupportedOperationException("redo not supported by this command"); //$NON-NLS-1$
+ }
+
+ @Override
+ public void undo() {
+ throw new UnsupportedOperationException("undo not supported by this command"); //$NON-NLS-1$
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/ElementFigure.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/ElementFigure.java
new file mode 100644
index 0000000..f863037
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/ElementFigure.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.layout.parts;
+
+import org.eclipse.draw2d.ColorConstants;
+import org.eclipse.draw2d.Figure;
+import org.eclipse.draw2d.Graphics;
+import org.eclipse.draw2d.geometry.Rectangle;
+import org.eclipse.swt.SWT;
+
+
+/**
+ * The figure used to draw basic elements.
+ * <p/>
+ * The figure is totally empty and transparent except for the selection border.
+ */
+class ElementFigure extends Figure {
+
+ private boolean mIsSelected;
+ private Rectangle mInnerBounds;
+
+ public ElementFigure() {
+ setOpaque(false);
+ }
+
+ public void setSelected(boolean isSelected) {
+ if (isSelected != mIsSelected) {
+ mIsSelected = isSelected;
+ repaint();
+ }
+ }
+
+ @Override
+ public void setBounds(Rectangle rect) {
+ super.setBounds(rect);
+
+ mInnerBounds = getBounds().getCopy();
+ if (mInnerBounds.width > 0) {
+ mInnerBounds.width--;
+ }
+ if (mInnerBounds.height > 0) {
+ mInnerBounds.height--;
+ }
+ }
+
+ public Rectangle getInnerBounds() {
+ return mInnerBounds;
+ }
+
+ @Override
+ protected void paintBorder(Graphics graphics) {
+ super.paintBorder(graphics);
+
+ if (mIsSelected) {
+ graphics.setLineWidth(1);
+ graphics.setLineStyle(SWT.LINE_SOLID);
+ graphics.setForegroundColor(ColorConstants.red);
+ graphics.drawRectangle(getInnerBounds());
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/LayoutFigure.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/LayoutFigure.java
new file mode 100644
index 0000000..55ed39b
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/LayoutFigure.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.layout.parts;
+
+import com.android.ide.eclipse.editors.layout.parts.UiLayoutEditPart.HighlightInfo;
+
+import org.eclipse.draw2d.ColorConstants;
+import org.eclipse.draw2d.Figure;
+import org.eclipse.draw2d.Graphics;
+import org.eclipse.draw2d.geometry.Rectangle;
+import org.eclipse.swt.SWT;
+
+/**
+ * The figure used to draw the feedback on a layout.
+ * <p/>
+ * By default the figure is transparent and empty.
+ * The base {@link ElementFigure} knows how to draw the selection border.
+ * This figure knows how to draw the drop feedback.
+ */
+class LayoutFigure extends ElementFigure {
+
+ private HighlightInfo mHighlightInfo;
+
+ public LayoutFigure() {
+ super();
+ }
+
+ public void setHighlighInfo(HighlightInfo highlightInfo) {
+ mHighlightInfo = highlightInfo;
+ repaint();
+ }
+
+ /**
+ * Paints the "border" for this figure.
+ * <p/>
+ * The parent {@link Figure#paint(Graphics)} calls {@link #paintFigure(Graphics)} then
+ * {@link #paintClientArea(Graphics)} then {@link #paintBorder(Graphics)}. Here we thus
+ * draw the actual highlight border but also the highlight anchor lines and points so that
+ * we can make sure they are all drawn on top of the border.
+ * <p/>
+ * Note: This method doesn't really need to restore its graphic state. The parent
+ * Figure will do it for us.
+ * <p/>
+ *
+ * @param graphics The Graphics object used for painting
+ */
+ @Override
+ protected void paintBorder(Graphics graphics) {
+ super.paintBorder(graphics);
+
+ if (mHighlightInfo == null) {
+ return;
+ }
+
+ // Draw the border. We want other highlighting to be drawn on top of the border.
+ if (mHighlightInfo.drawDropBorder) {
+ graphics.setLineWidth(3);
+ graphics.setLineStyle(SWT.LINE_SOLID);
+ graphics.setForegroundColor(ColorConstants.green);
+ graphics.drawRectangle(getInnerBounds().getCopy().shrink(1, 1));
+ }
+
+ Rectangle bounds = getBounds();
+ int bx = bounds.x;
+ int by = bounds.y;
+ int w = bounds.width;
+ int h = bounds.height;
+
+ // Draw frames of target child parts, if any
+ if (mHighlightInfo.childParts != null) {
+ graphics.setLineWidth(2);
+ graphics.setLineStyle(SWT.LINE_DOT);
+ graphics.setForegroundColor(ColorConstants.lightBlue);
+ for (UiElementEditPart part : mHighlightInfo.childParts) {
+ if (part != null) {
+ graphics.drawRectangle(part.getBounds().getCopy().translate(bx, by));
+ }
+ }
+ }
+
+ // Draw the target line, if any
+ if (mHighlightInfo.linePoints != null) {
+ int x1 = mHighlightInfo.linePoints[0].x;
+ int y1 = mHighlightInfo.linePoints[0].y;
+ int x2 = mHighlightInfo.linePoints[1].x;
+ int y2 = mHighlightInfo.linePoints[1].y;
+
+ // if the line is right to the edge, draw it one pixel more inside so that the
+ // full 2-pixel width be visible.
+ if (x1 <= 0) x1++;
+ if (x2 <= 0) x2++;
+ if (y1 <= 0) y1++;
+ if (y2 <= 0) y2++;
+
+ if (x1 >= w - 1) x1--;
+ if (x2 >= w - 1) x2--;
+ if (y1 >= h - 1) y1--;
+ if (y2 >= h - 1) y2--;
+
+ x1 += bx;
+ x2 += bx;
+ y1 += by;
+ y2 += by;
+
+ graphics.setLineWidth(2);
+ graphics.setLineStyle(SWT.LINE_DASH);
+ graphics.setLineCap(SWT.CAP_ROUND);
+ graphics.setForegroundColor(ColorConstants.orange);
+ graphics.drawLine(x1, y1, x2, y2);
+ }
+
+ // Draw the anchor point, if any
+ if (mHighlightInfo.anchorPoint != null) {
+ int x = mHighlightInfo.anchorPoint.x;
+ int y = mHighlightInfo.anchorPoint.y;
+
+ // If the point is right on the edge, draw it one pixel inside so that it
+ // matches the highlight line. It makes it slightly more visible that way.
+ if (x <= 0) x++;
+ if (y <= 0) y++;
+ if (x >= w - 1) x--;
+ if (y >= h - 1) y--;
+ x += bx;
+ y += by;
+
+ graphics.setLineWidth(2);
+ graphics.setLineStyle(SWT.LINE_SOLID);
+ graphics.setLineCap(SWT.CAP_ROUND);
+ graphics.setForegroundColor(ColorConstants.orange);
+ graphics.drawLine(x-5, y-5, x+5, y+5);
+ graphics.drawLine(x-5, y+5, x+5, y-5);
+ // 7 * cos(45) == 5 so we use 8 for the circle radius (it looks slightly better than 7)
+ graphics.setLineWidth(1);
+ graphics.drawOval(x-8, y-8, 16, 16);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiDocumentEditPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiDocumentEditPart.java
new file mode 100644
index 0000000..2f7636d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiDocumentEditPart.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.layout.parts;
+
+import com.android.ide.eclipse.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+import org.eclipse.draw2d.AbstractBackground;
+import org.eclipse.draw2d.ColorConstants;
+import org.eclipse.draw2d.FreeformLayer;
+import org.eclipse.draw2d.FreeformLayout;
+import org.eclipse.draw2d.Graphics;
+import org.eclipse.draw2d.IFigure;
+import org.eclipse.draw2d.Label;
+import org.eclipse.draw2d.geometry.Insets;
+import org.eclipse.draw2d.geometry.Point;
+import org.eclipse.draw2d.geometry.Rectangle;
+import org.eclipse.gef.EditPart;
+import org.eclipse.gef.EditPolicy;
+import org.eclipse.gef.Request;
+import org.eclipse.gef.RequestConstants;
+import org.eclipse.gef.editpolicies.RootComponentEditPolicy;
+import org.eclipse.gef.requests.DropRequest;
+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.awt.image.BufferedImage;
+import java.awt.image.DataBufferInt;
+
+/**
+ * Graphical edit part for the root document.
+ * <p/>
+ * It acts as a simple container.
+ */
+public class UiDocumentEditPart extends UiElementEditPart {
+
+ private Display mDisplay;
+ private FreeformLayer mLayer;
+ private ImageBackground mImage;
+ private Label mChild = null;
+
+ final static class ImageBackground extends AbstractBackground {
+
+ private BufferedImage mBufferedImage;
+ private Image mImage;
+
+ ImageBackground() {
+ }
+
+ ImageBackground(BufferedImage image, Display display) {
+ setImage(image, display);
+ }
+
+ @Override
+ public void paintBackground(IFigure figure, Graphics graphics, Insets insets) {
+ if (mImage != null) {
+ Rectangle rect = getPaintRectangle(figure, insets);
+ graphics.drawImage(mImage, rect.x, rect.y);
+ }
+ }
+
+ void setImage(BufferedImage image, Display display) {
+ if (image != null) {
+ int[] data = ((DataBufferInt)image.getData().getDataBuffer()).getData();
+
+ ImageData imageData = new ImageData(image.getWidth(), image.getHeight(), 32,
+ new PaletteData(0x00FF0000, 0x0000FF00, 0x000000FF));
+
+ imageData.setPixels(0, 0, data.length, data, 0);
+
+ mImage = new Image(display, imageData);
+ } else {
+ mImage = null;
+ }
+ }
+
+ BufferedImage getBufferedImage() {
+ return mBufferedImage;
+ }
+ }
+
+ public UiDocumentEditPart(UiDocumentNode uiDocumentNode, Display display) {
+ super(uiDocumentNode);
+ mDisplay = display;
+ }
+
+ @Override
+ protected IFigure createFigure() {
+ mLayer = new FreeformLayer();
+ mLayer.setLayoutManager(new FreeformLayout());
+
+ mLayer.setOpaque(true);
+ mLayer.setBackgroundColor(ColorConstants.lightGray);
+
+ return mLayer;
+ }
+
+ @Override
+ protected void refreshVisuals() {
+ UiElementNode model = (UiElementNode)getModel();
+
+ Object editData = model.getEditData();
+ if (editData instanceof BufferedImage) {
+ BufferedImage image = (BufferedImage)editData;
+
+ if (mImage == null || image != mImage.getBufferedImage()) {
+ mImage = new ImageBackground(image, mDisplay);
+ }
+
+ mLayer.setBorder(mImage);
+
+ if (mChild != null && mChild.getParent() == mLayer) {
+ mLayer.remove(mChild);
+ }
+ } else if (editData instanceof String) {
+ mLayer.setBorder(null);
+ if (mChild == null) {
+ mChild = new Label();
+ }
+ mChild.setText((String)editData);
+
+ if (mChild != null && mChild.getParent() != mLayer) {
+ mLayer.add(mChild);
+ }
+ Rectangle bounds = mChild.getTextBounds();
+ bounds.x = bounds.y = 0;
+ mLayer.setConstraint(mChild, bounds);
+ }
+
+ // refresh the children as well
+ refreshChildrenVisuals();
+ }
+
+ @Override
+ protected void hideSelection() {
+ // no selection at this level.
+ }
+
+ @Override
+ protected void showSelection() {
+ // no selection at this level.
+ }
+
+ @Override
+ protected void createEditPolicies() {
+ super.createEditPolicies();
+
+ // This policy indicates this a root component that cannot be removed
+ installEditPolicy(EditPolicy.COMPONENT_ROLE, new RootComponentEditPolicy());
+
+ installLayoutEditPolicy(this);
+ }
+
+ /**
+ * Returns the EditPart that should be used as the target for the specified Request.
+ * For instance this is called during drag'n'drop with a CreateRequest.
+ * <p/>
+ * For the root document, we want the first child edit part to the be the target
+ * since an XML document can have only one root element.
+ *
+ * {@inheritDoc}
+ */
+ @Override
+ public EditPart getTargetEditPart(Request request) {
+ if (request != null && request.getType() == RequestConstants.REQ_CREATE) {
+ // We refuse the drop if it's not in the bounds of the document.
+ if (request instanceof DropRequest) {
+ Point where = ((DropRequest) request).getLocation().getCopy();
+ UiElementNode uiNode = getUiNode();
+ if (uiNode instanceof UiDocumentNode) {
+ // Take the bounds of the background image as the valid drop zone
+ Object editData = uiNode.getEditData();
+ if (editData instanceof BufferedImage) {
+ BufferedImage image = (BufferedImage)editData;
+ int w = image.getWidth();
+ int h = image.getHeight();
+ if (where.x > w || where.y > h) {
+ return null;
+ }
+ }
+
+ }
+ }
+
+ // For the root document, we want the first child edit part to the be the target
+ // since an XML document can have only one root element.
+ if (getChildren().size() > 0) {
+ Object o = getChildren().get(0);
+ if (o instanceof EditPart) {
+ return ((EditPart) o).getTargetEditPart(request);
+ }
+ }
+ }
+ return super.getTargetEditPart(request);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiDocumentTreeEditPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiDocumentTreeEditPart.java
new file mode 100644
index 0000000..af22afb
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiDocumentTreeEditPart.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.layout.parts;
+
+import com.android.ide.eclipse.editors.uimodel.UiDocumentNode;
+
+import java.util.List;
+
+/**
+ * Implementation of {@link UiElementTreeEditPart} for the document root.
+ */
+public class UiDocumentTreeEditPart extends UiElementTreeEditPart {
+
+ public UiDocumentTreeEditPart(UiDocumentNode model) {
+ super(model);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ protected List getModelChildren() {
+ return getUiNode().getUiChildren();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiElementEditPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiElementEditPart.java
new file mode 100644
index 0000000..a2e05c7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiElementEditPart.java
@@ -0,0 +1,345 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.layout.parts;
+
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.editors.uimodel.IUiUpdateListener;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+import com.android.sdklib.SdkConstants;
+
+import org.eclipse.draw2d.IFigure;
+import org.eclipse.draw2d.geometry.Point;
+import org.eclipse.draw2d.geometry.Rectangle;
+import org.eclipse.gef.DragTracker;
+import org.eclipse.gef.EditPart;
+import org.eclipse.gef.EditPolicy;
+import org.eclipse.gef.GraphicalEditPart;
+import org.eclipse.gef.Request;
+import org.eclipse.gef.RequestConstants;
+import org.eclipse.gef.commands.Command;
+import org.eclipse.gef.editparts.AbstractGraphicalEditPart;
+import org.eclipse.gef.editpolicies.LayoutEditPolicy;
+import org.eclipse.gef.editpolicies.SelectionEditPolicy;
+import org.eclipse.gef.requests.CreateRequest;
+import org.eclipse.gef.requests.DropRequest;
+import org.eclipse.gef.tools.SelectEditPartTracker;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+
+import java.util.List;
+
+/**
+ * An {@link EditPart} for a {@link UiElementNode}.
+ */
+public abstract class UiElementEditPart extends AbstractGraphicalEditPart
+ implements IUiUpdateListener {
+
+ public UiElementEditPart(UiElementNode uiElementNode) {
+ setModel(uiElementNode);
+ }
+
+ //-------------------------
+ // Derived classes must define these
+
+ abstract protected void hideSelection();
+ abstract protected void showSelection();
+
+ //-------------------------
+ // Base class overrides
+
+ @Override
+ public DragTracker getDragTracker(Request request) {
+ return new SelectEditPartTracker(this);
+ }
+
+ @Override
+ protected void createEditPolicies() {
+ /*
+ * This is no longer needed, as a selection edit policy is set by the parent layout.
+ * Leave this code commented out right now, I'll want to play with this later.
+ *
+ installEditPolicy(EditPolicy.SELECTION_FEEDBACK_ROLE,
+ new NonResizableSelectionEditPolicy(this));
+ */
+ }
+
+ /* (non-javadoc)
+ * Returns a List containing the children model objects.
+ * Must not return null, instead use the super which returns an empty list.
+ */
+ @SuppressWarnings("unchecked")
+ @Override
+ protected List getModelChildren() {
+ return getUiNode().getUiChildren();
+ }
+
+ @Override
+ public void activate() {
+ super.activate();
+ getUiNode().addUpdateListener(this);
+ }
+
+ @Override
+ public void deactivate() {
+ super.deactivate();
+ getUiNode().removeUpdateListener(this);
+ }
+
+ @Override
+ protected void refreshVisuals() {
+ if (getFigure().getParent() != null) {
+ ((GraphicalEditPart) getParent()).setLayoutConstraint(this, getFigure(), getBounds());
+ }
+
+ // update the visuals of the children as well
+ refreshChildrenVisuals();
+ }
+
+ protected void refreshChildrenVisuals() {
+ if (children != null) {
+ for (Object child : children) {
+ if (child instanceof UiElementEditPart) {
+ UiElementEditPart childPart = (UiElementEditPart)child;
+ childPart.refreshVisuals();
+ }
+ }
+ }
+ }
+
+ //-------------------------
+ // IUiUpdateListener implementation
+
+ public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state) {
+ // TODO: optimize by refreshing only when needed
+ switch(state) {
+ case ATTR_UPDATED:
+ refreshVisuals();
+ break;
+ case CHILDREN_CHANGED:
+ refreshChildren();
+
+ // new children list, need to update the layout
+ refreshVisuals();
+ break;
+ case CREATED:
+ refreshVisuals();
+ break;
+ case DELETED:
+ // pass
+ break;
+ }
+ }
+
+ //-------------------------
+ // Local methods
+
+ /** @return The object model casted to an {@link UiElementNode} */
+ public final UiElementNode getUiNode() {
+ return (UiElementNode) getModel();
+ }
+
+ protected final ElementDescriptor getDescriptor() {
+ return getUiNode().getDescriptor();
+ }
+
+ protected final UiElementEditPart getEditPartParent() {
+ EditPart parent = getParent();
+ if (parent instanceof UiElementEditPart) {
+ return (UiElementEditPart)parent;
+ }
+ return null;
+ }
+
+ /**
+ * Returns a given XML attribute.
+ * @param attrName The local name of the attribute.
+ * @return the attribute as a {@link String}, if it exists, or <code>null</code>
+ */
+ protected final String getStringAttr(String attrName) {
+ UiElementNode uiNode = getUiNode();
+ if (uiNode.getXmlNode() != null) {
+ Node xmlNode = uiNode.getXmlNode();
+ if (xmlNode != null) {
+ NamedNodeMap nodeAttributes = xmlNode.getAttributes();
+ if (nodeAttributes != null) {
+ Node attr = nodeAttributes.getNamedItemNS(
+ SdkConstants.NS_RESOURCES, attrName);
+ if (attr != null) {
+ return attr.getNodeValue();
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ protected final Rectangle getBounds() {
+ UiElementNode model = (UiElementNode)getModel();
+
+ Object editData = model.getEditData();
+
+ if (editData != null) {
+ // assert with fully qualified class name to prevent import changes to another
+ // Rectangle class.
+ assert (editData instanceof org.eclipse.draw2d.geometry.Rectangle);
+
+ return (Rectangle)editData;
+ }
+
+ // return a dummy rect
+ return new Rectangle(0, 0, 0, 0);
+ }
+
+ /**
+ * Returns the EditPart that should be used as the target for the specified Request.
+ * <p/>
+ * For instance this is called during drag'n'drop with a CreateRequest.
+ * <p/>
+ * Reject being a target for elements which descriptor does not allow children.
+ *
+ * {@inheritDoc}
+ */
+ @Override
+ public EditPart getTargetEditPart(Request request) {
+ if (request != null && request.getType() == RequestConstants.REQ_CREATE) {
+ // Reject being a target for elements which descriptor does not allow children.
+ if (!getUiNode().getDescriptor().hasChildren()) {
+ return null;
+ }
+ }
+ return super.getTargetEditPart(request);
+ }
+
+ /**
+ * Used by derived classes {@link UiDocumentEditPart} and {@link UiLayoutEditPart}
+ * to accept drag'n'drop of new items from the palette.
+ *
+ * @param layoutEditPart The layout edit part where this policy is installed. It can
+ * be either a {@link UiDocumentEditPart} or a {@link UiLayoutEditPart}.
+ */
+ protected void installLayoutEditPolicy(final UiElementEditPart layoutEditPart) {
+ // This policy indicates how elements can be constrained by the layout.
+ // TODO Right now we use the XY layout policy since our constraints are
+ // handled by the android rendering engine rather than GEF. Tweak as
+ // appropriate.
+ installEditPolicy(EditPolicy.LAYOUT_ROLE, new LayoutEditPolicy() {
+
+ /**
+ * We don't allow layout children to be resized yet.
+ * <p/>
+ * Typical choices would be:
+ * <ul>
+ * <li> ResizableEditPolicy, to allow for selection, move and resize.
+ * <li> NonResizableEditPolicy, to allow for selection, move but not resize.
+ * <li> SelectionEditPolicy to allow for only selection.
+ * </ul>
+ * <p/>
+ * TODO: make this depend on the part layout. For an AbsoluteLayout we should
+ * probably use a NonResizableEditPolicy and SelectionEditPolicy for the rest.
+ * Whether to use ResizableEditPolicy or NonResizableEditPolicy should depend
+ * on the child in an AbsoluteLayout.
+ */
+ @Override
+ protected EditPolicy createChildEditPolicy(EditPart child) {
+ if (child instanceof UiElementEditPart) {
+ return new NonResizableSelectionEditPolicy((UiElementEditPart) child);
+ }
+ return null;
+ }
+
+ @Override
+ protected Command getCreateCommand(CreateRequest request) {
+ // We store the ElementDescriptor in the request.factory.type
+ Object newType = request.getNewObjectType();
+ if (newType instanceof ElementDescriptor) {
+ Point where = request.getLocation().getCopy();
+ Point origin = getLayoutContainer().getClientArea().getLocation();
+ where.translate(origin.getNegated());
+
+ // The host is the EditPart where this policy is installed,
+ // e.g. this UiElementEditPart.
+ EditPart host = getHost();
+ if (host instanceof UiElementEditPart) {
+
+ return new ElementCreateCommand((ElementDescriptor) newType,
+ (UiElementEditPart) host,
+ where);
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ protected Command getMoveChildrenCommand(Request request) {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public void showLayoutTargetFeedback(Request request) {
+ super.showLayoutTargetFeedback(request);
+
+ // for debugging
+ // System.out.println("target: " + request.toString() + " -- " + layoutEditPart.getUiNode().getBreadcrumbTrailDescription(false));
+
+ if (layoutEditPart instanceof UiLayoutEditPart &&
+ request instanceof DropRequest) {
+ Point where = ((DropRequest) request).getLocation().getCopy();
+ Point origin = getLayoutContainer().getClientArea().getLocation();
+ where.translate(origin.getNegated());
+
+ ((UiLayoutEditPart) layoutEditPart).showDropTarget(where);
+ }
+ }
+
+ @Override
+ protected void eraseLayoutTargetFeedback(Request request) {
+ super.eraseLayoutTargetFeedback(request);
+ if (layoutEditPart instanceof UiLayoutEditPart) {
+ ((UiLayoutEditPart) layoutEditPart).hideDropTarget();
+ }
+ }
+
+ @Override
+ protected IFigure createSizeOnDropFeedback(CreateRequest createRequest) {
+ // TODO understand if this is useful for us or remove
+ return super.createSizeOnDropFeedback(createRequest);
+ }
+
+ });
+ }
+
+ protected static class NonResizableSelectionEditPolicy extends SelectionEditPolicy {
+
+ private final UiElementEditPart mEditPart;
+
+ public NonResizableSelectionEditPolicy(UiElementEditPart editPart) {
+ mEditPart = editPart;
+ }
+
+ @Override
+ protected void hideSelection() {
+ mEditPart.hideSelection();
+ }
+
+ @Override
+ protected void showSelection() {
+ mEditPart.showSelection();
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiElementTreeEditPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiElementTreeEditPart.java
new file mode 100644
index 0000000..fd788dd
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiElementTreeEditPart.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.layout.parts;
+
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+import org.eclipse.gef.editparts.AbstractTreeEditPart;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.ui.views.contentoutline.IContentOutlinePage;
+
+/**
+ * Base {@link AbstractTreeEditPart} to represent {@link UiElementNode} objects in the
+ * {@link IContentOutlinePage} linked to the layout editor.
+ */
+public class UiElementTreeEditPart extends AbstractTreeEditPart {
+
+ public UiElementTreeEditPart(UiElementNode uiElementNode) {
+ setModel(uiElementNode);
+ }
+
+ @Override
+ protected void createEditPolicies() {
+ // TODO Auto-generated method stub
+ super.createEditPolicies();
+ }
+
+ @Override
+ protected Image getImage() {
+ return getUiNode().getDescriptor().getIcon();
+ }
+
+ @Override
+ protected String getText() {
+ return getUiNode().getShortDescription();
+ }
+
+ @Override
+ public void activate() {
+ if (!isActive()) {
+ super.activate();
+ // TODO
+ }
+ }
+
+ @Override
+ public void deactivate() {
+ if (isActive()) {
+ super.deactivate();
+ // TODO
+ }
+ }
+
+ /**
+ * Returns the casted model object represented by this {@link AbstractTreeEditPart}.
+ */
+ protected UiElementNode getUiNode() {
+ return (UiElementNode)getModel();
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiElementTreeEditPartFactory.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiElementTreeEditPartFactory.java
new file mode 100644
index 0000000..de6c404
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiElementTreeEditPartFactory.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.layout.parts;
+
+import com.android.ide.eclipse.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+import org.eclipse.gef.EditPart;
+import org.eclipse.gef.EditPartFactory;
+import org.eclipse.gef.editparts.AbstractTreeEditPart;
+import org.eclipse.ui.views.contentoutline.IContentOutlinePage;
+
+/**
+ * {@link EditPartFactory} to create {@link AbstractTreeEditPart} for {@link UiElementNode} objects.
+ * These objects are used in the {@link IContentOutlinePage} linked to the layout editor.
+ */
+public class UiElementTreeEditPartFactory implements EditPartFactory {
+
+ public EditPart createEditPart(EditPart context, Object model) {
+ if (model instanceof UiDocumentNode) {
+ return new UiDocumentTreeEditPart((UiDocumentNode) model);
+ } else if (model instanceof UiElementNode) {
+ UiElementNode node = (UiElementNode) model;
+ if (node.getDescriptor().hasChildren()) {
+ return new UiLayoutTreeEditPart(node);
+ } else {
+ return new UiViewTreeEditPart(node);
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiElementsEditPartFactory.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiElementsEditPartFactory.java
new file mode 100644
index 0000000..18dcd9c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiElementsEditPartFactory.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.layout.parts;
+
+import com.android.ide.eclipse.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+import org.eclipse.gef.EditPart;
+import org.eclipse.gef.EditPartFactory;
+import org.eclipse.swt.widgets.Display;
+
+/**
+ * A factory that returns the appropriate {@link EditPart} for a given model object.
+ * <p/>
+ * The only model objects we use are {@link UiElementNode} objects and they are
+ * edited using {@link UiElementEditPart}.
+ */
+public class UiElementsEditPartFactory implements EditPartFactory {
+
+ private Display mDisplay;
+
+ public UiElementsEditPartFactory(Display display) {
+ mDisplay = display;
+ }
+
+ public EditPart createEditPart(EditPart context, Object model) {
+ if (model instanceof UiDocumentNode) {
+ return new UiDocumentEditPart((UiDocumentNode) model, mDisplay);
+ } else if (model instanceof UiElementNode) {
+ UiElementNode node = (UiElementNode) model;
+
+ if (node.getDescriptor().hasChildren()) {
+ return new UiLayoutEditPart(node);
+ }
+
+ return new UiViewEditPart(node);
+ }
+ return null;
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiLayoutEditPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiLayoutEditPart.java
new file mode 100644
index 0000000..43a70a5
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiLayoutEditPart.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.layout.parts;
+
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+import org.eclipse.draw2d.IFigure;
+import org.eclipse.draw2d.XYLayout;
+import org.eclipse.draw2d.geometry.Point;
+import org.eclipse.gef.EditPolicy;
+import org.eclipse.gef.commands.Command;
+import org.eclipse.gef.editpolicies.ContainerEditPolicy;
+import org.eclipse.gef.requests.CreateRequest;
+
+/**
+ * Graphical edit part for an {@link UiElementNode} that represents a ViewLayout.
+ * <p/>
+ * It acts as a simple container.
+ */
+public final class UiLayoutEditPart extends UiElementEditPart {
+
+ static class HighlightInfo {
+ public boolean drawDropBorder;
+ public UiElementEditPart[] childParts;
+ public Point anchorPoint;
+ public Point linePoints[];
+
+ public final Point tempPoints[] = new Point[] { new Point(), new Point() };
+
+ public void clear() {
+ drawDropBorder = false;
+ childParts = null;
+ anchorPoint = null;
+ linePoints = null;
+ }
+ }
+
+ private final HighlightInfo mHighlightInfo = new HighlightInfo();
+
+ public UiLayoutEditPart(UiElementNode uiElementNode) {
+ super(uiElementNode);
+ }
+
+ @Override
+ protected void createEditPolicies() {
+ super.createEditPolicies();
+
+ installEditPolicy(EditPolicy.CONTAINER_ROLE, new ContainerEditPolicy() {
+ @Override
+ protected Command getCreateCommand(CreateRequest request) {
+ return null;
+ }
+ });
+
+ installLayoutEditPolicy(this);
+ }
+
+ @Override
+ protected IFigure createFigure() {
+ IFigure f = new LayoutFigure();
+ f.setLayoutManager(new XYLayout());
+ return f;
+ }
+
+ @Override
+ protected void showSelection() {
+ IFigure f = getFigure();
+ if (f instanceof ElementFigure) {
+ ((ElementFigure) f).setSelected(true);
+ }
+ }
+
+ @Override
+ protected void hideSelection() {
+ IFigure f = getFigure();
+ if (f instanceof ElementFigure) {
+ ((ElementFigure) f).setSelected(false);
+ }
+ }
+
+ public void showDropTarget(Point where) {
+ if (where != null) {
+ mHighlightInfo.clear();
+ mHighlightInfo.drawDropBorder = true;
+ DropFeedback.computeDropFeedback(this, mHighlightInfo, where);
+
+ IFigure f = getFigure();
+ if (f instanceof LayoutFigure) {
+ ((LayoutFigure) f).setHighlighInfo(mHighlightInfo);
+ }
+ }
+ }
+
+ public void hideDropTarget() {
+ mHighlightInfo.clear();
+ IFigure f = getFigure();
+ if (f instanceof LayoutFigure) {
+ ((LayoutFigure) f).setHighlighInfo(mHighlightInfo);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiLayoutTreeEditPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiLayoutTreeEditPart.java
new file mode 100644
index 0000000..4359e23
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiLayoutTreeEditPart.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.layout.parts;
+
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+import java.util.List;
+
+/**
+ * Implementation of {@link UiElementTreeEditPart} for layout objects.
+ */
+public class UiLayoutTreeEditPart extends UiElementTreeEditPart {
+
+ public UiLayoutTreeEditPart(UiElementNode node) {
+ super(node);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ protected List getModelChildren() {
+ return getUiNode().getUiChildren();
+ }
+
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiViewEditPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiViewEditPart.java
new file mode 100644
index 0000000..05329f3
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiViewEditPart.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.layout.parts;
+
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+import org.eclipse.draw2d.IFigure;
+import org.eclipse.draw2d.XYLayout;
+
+/**
+ * Graphical edit part for an {@link UiElementNode} that represents a View.
+ */
+public class UiViewEditPart extends UiElementEditPart {
+
+ public UiViewEditPart(UiElementNode uiElementNode) {
+ super(uiElementNode);
+ }
+
+ @Override
+ protected IFigure createFigure() {
+ IFigure f = new ElementFigure();
+ f.setLayoutManager(new XYLayout());
+ return f;
+ }
+
+ @Override
+ protected void showSelection() {
+ IFigure f = getFigure();
+ if (f instanceof ElementFigure) {
+ ((ElementFigure) f).setSelected(true);
+ }
+ }
+
+ @Override
+ protected void hideSelection() {
+ IFigure f = getFigure();
+ if (f instanceof ElementFigure) {
+ ((ElementFigure) f).setSelected(false);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiViewTreeEditPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiViewTreeEditPart.java
new file mode 100644
index 0000000..62b5e8a
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/parts/UiViewTreeEditPart.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.layout.parts;
+
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+/**
+ * Implementation of {@link UiElementTreeEditPart} for view objects.
+ */
+public class UiViewTreeEditPart extends UiElementTreeEditPart {
+
+ public UiViewTreeEditPart(UiElementNode node) {
+ super(node);
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/uimodel/UiViewElementNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/uimodel/UiViewElementNode.java
new file mode 100644
index 0000000..1bf5d5a
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/layout/uimodel/UiViewElementNode.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.layout.uimodel;
+
+import com.android.ide.eclipse.adt.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.sdk.Sdk;
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.editors.descriptors.XmlnsAttributeDescriptor;
+import com.android.ide.eclipse.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.SdkConstants;
+
+import org.eclipse.core.resources.IProject;
+
+import java.util.List;
+
+/**
+ * Specialized version of {@link UiElementNode} for the {@link ViewElementDescriptor}s.
+ */
+public class UiViewElementNode extends UiElementNode {
+
+ private AttributeDescriptor[] mCachedAttributeDescriptors;
+
+ public UiViewElementNode(ViewElementDescriptor elementDescriptor) {
+ super(elementDescriptor);
+ }
+
+ /**
+ * Returns an AttributeDescriptor array that depends on the current UiParent.
+ * <p/>
+ * The array merges both "direct" attributes with the descriptor layout attributes.
+ * The array instance is cached and cleared if the UiParent is changed.
+ */
+ @Override
+ public AttributeDescriptor[] getAttributeDescriptors() {
+ if (mCachedAttributeDescriptors != null) {
+ return mCachedAttributeDescriptors;
+ }
+
+ UiElementNode ui_parent = getUiParent();
+ AttributeDescriptor[] direct_attrs = super.getAttributeDescriptors();
+ mCachedAttributeDescriptors = direct_attrs;
+
+ AttributeDescriptor[] layout_attrs = null;
+ boolean need_xmlns = false;
+
+ if (ui_parent instanceof UiDocumentNode) {
+ // Limitation: right now the layout behaves as if everything was
+ // owned by a FrameLayout.
+ // TODO replace by something user-configurable.
+
+ List<ElementDescriptor> layoutDescriptors = null;
+ IProject project = getEditor().getProject();
+ if (project != null) {
+ Sdk currentSdk = Sdk.getCurrent();
+ IAndroidTarget target = currentSdk.getTarget(project);
+ if (target != null) {
+ AndroidTargetData data = currentSdk.getTargetData(target);
+ layoutDescriptors = data.getLayoutDescriptors().getLayoutDescriptors();
+ }
+ }
+
+ if (layoutDescriptors != null) {
+ for (ElementDescriptor desc : layoutDescriptors) {
+ if (desc instanceof ViewElementDescriptor &&
+ desc.getXmlName().equals(AndroidConstants.CLASS_FRAMELAYOUT)) {
+ layout_attrs = ((ViewElementDescriptor) desc).getLayoutAttributes();
+ need_xmlns = true;
+ break;
+ }
+ }
+ }
+ } else if (ui_parent instanceof UiViewElementNode){
+ layout_attrs =
+ ((ViewElementDescriptor) ui_parent.getDescriptor()).getLayoutAttributes();
+ }
+
+ if (layout_attrs == null || layout_attrs.length == 0) {
+ return mCachedAttributeDescriptors;
+ }
+
+ mCachedAttributeDescriptors =
+ new AttributeDescriptor[direct_attrs.length +
+ layout_attrs.length +
+ (need_xmlns ? 1 : 0)];
+ System.arraycopy(direct_attrs, 0,
+ mCachedAttributeDescriptors, 0,
+ direct_attrs.length);
+ System.arraycopy(layout_attrs, 0,
+ mCachedAttributeDescriptors, direct_attrs.length,
+ layout_attrs.length);
+ if (need_xmlns) {
+ AttributeDescriptor desc = new XmlnsAttributeDescriptor(
+ "android", //$NON-NLS-1$
+ SdkConstants.NS_RESOURCES);
+ mCachedAttributeDescriptors[direct_attrs.length + layout_attrs.length] = desc;
+ }
+
+ return mCachedAttributeDescriptors;
+ }
+
+ /**
+ * Sets the parent of this UI node.
+ * <p/>
+ * Also removes the cached AttributeDescriptor array that depends on the current UiParent.
+ */
+ @Override
+ protected void setUiParent(UiElementNode parent) {
+ super.setUiParent(parent);
+ mCachedAttributeDescriptors = null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/ManifestContentAssist.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/ManifestContentAssist.java
new file mode 100644
index 0000000..b40e458
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/ManifestContentAssist.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.manifest;
+
+import com.android.ide.eclipse.adt.sdk.AndroidTargetData;
+import com.android.ide.eclipse.editors.AndroidContentAssist;
+
+/**
+ * Content Assist Processor for AndroidManifest.xml
+ */
+final class ManifestContentAssist extends AndroidContentAssist {
+
+ /**
+ * Constructor for ManifestContentAssist
+ */
+ public ManifestContentAssist() {
+ super(AndroidTargetData.DESCRIPTOR_MANIFEST);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/ManifestEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/ManifestEditor.java
new file mode 100644
index 0000000..d0f8d7b
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/ManifestEditor.java
@@ -0,0 +1,387 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.manifest;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.sdk.AndroidTargetData;
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.common.project.AndroidXPathFactory;
+import com.android.ide.eclipse.editors.AndroidEditor;
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.editors.manifest.descriptors.AndroidManifestDescriptors;
+import com.android.ide.eclipse.editors.manifest.pages.ApplicationPage;
+import com.android.ide.eclipse.editors.manifest.pages.InstrumentationPage;
+import com.android.ide.eclipse.editors.manifest.pages.OverviewPage;
+import com.android.ide.eclipse.editors.manifest.pages.PermissionPage;
+import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor;
+import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IFileListener;
+import com.android.ide.eclipse.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IMarkerDelta;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IResourceDelta;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.part.FileEditorInput;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+
+import java.util.List;
+
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpressionException;
+
+/**
+ * Multi-page form editor for AndroidManifest.xml.
+ */
+public final class ManifestEditor extends AndroidEditor {
+ private final static String EMPTY = ""; //$NON-NLS-1$
+
+
+ /** Root node of the UI element hierarchy */
+ private UiElementNode mUiManifestNode;
+ /** The Application Page tab */
+ private ApplicationPage mAppPage;
+ /** The Overview Manifest Page tab */
+ private OverviewPage mOverviewPage;
+ /** The Permission Page tab */
+ private PermissionPage mPermissionPage;
+ /** The Instrumentation Page tab */
+ private InstrumentationPage mInstrumentationPage;
+
+ private IFileListener mMarkerMonitor;
+
+
+ /**
+ * Creates the form editor for AndroidManifest.xml.
+ */
+ public ManifestEditor() {
+ super();
+ }
+
+ @Override
+ public void dispose() {
+ super.dispose();
+
+ ResourceMonitor.getMonitor().removeFileListener(mMarkerMonitor);
+ }
+
+ /**
+ * Return the root node of the UI element hierarchy, which here
+ * is the "manifest" node.
+ */
+ @Override
+ public UiElementNode getUiRootNode() {
+ return mUiManifestNode;
+ }
+
+ /**
+ * Returns the Manifest descriptors for the file being edited.
+ */
+ public AndroidManifestDescriptors getManifestDescriptors() {
+ AndroidTargetData data = getTargetData();
+ if (data != null) {
+ return data.getManifestDescriptors();
+ }
+
+ return null;
+ }
+
+ // ---- Base Class Overrides ----
+
+ /**
+ * Returns whether the "save as" operation is supported by this editor.
+ * <p/>
+ * Save-As is a valid operation for the ManifestEditor since it acts on a
+ * single source file.
+ *
+ * @see IEditorPart
+ */
+ @Override
+ public boolean isSaveAsAllowed() {
+ return true;
+ }
+
+ /**
+ * Creates the various form pages.
+ */
+ @Override
+ protected void createFormPages() {
+ try {
+ addPage(mOverviewPage = new OverviewPage(this));
+ addPage(mAppPage = new ApplicationPage(this));
+ addPage(mPermissionPage = new PermissionPage(this));
+ addPage(mInstrumentationPage = new InstrumentationPage(this));
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, "Error creating nested page"); //$NON-NLS-1$
+ }
+ }
+
+ /* (non-java doc)
+ * Change the tab/title name to include the project name.
+ */
+ @Override
+ protected void setInput(IEditorInput input) {
+ super.setInput(input);
+ IFile inputFile = getInputFile();
+ if (inputFile != null) {
+ startMonitoringMarkers();
+ setPartName(String.format("%1$s Manifest", inputFile.getProject().getName()));
+ }
+ }
+
+ /**
+ * Processes the new XML Model, which XML root node is given.
+ *
+ * @param xml_doc The XML document, if available, or null if none exists.
+ */
+ @Override
+ protected void xmlModelChanged(Document xml_doc) {
+ // create the ui root node on demand.
+ initUiRootNode(false /*force*/);
+
+ loadFromXml(xml_doc);
+
+ super.xmlModelChanged(xml_doc);
+ }
+
+ private void loadFromXml(Document xmlDoc) {
+ mUiManifestNode.setXmlDocument(xmlDoc);
+ if (xmlDoc != null) {
+ ElementDescriptor manifest_desc = mUiManifestNode.getDescriptor();
+ try {
+ XPath xpath = AndroidXPathFactory.newXPath();
+ Node node = (Node) xpath.evaluate("/" + manifest_desc.getXmlName(), //$NON-NLS-1$
+ xmlDoc,
+ XPathConstants.NODE);
+ assert node != null && node.getNodeName().equals(manifest_desc.getXmlName());
+
+ // Refresh the manifest UI node and all its descendants
+ mUiManifestNode.loadFromXmlNode(node);
+ } catch (XPathExpressionException e) {
+ AdtPlugin.log(e, "XPath error when trying to find '%s' element in XML.", //$NON-NLS-1$
+ manifest_desc.getXmlName());
+ }
+ }
+ }
+
+ private void onDescriptorsChanged(UiElementNode oldManifestNode) {
+ mUiManifestNode.reloadFromXmlNode(oldManifestNode.getXmlNode());
+
+ if (mOverviewPage != null) {
+ mOverviewPage.refreshUiApplicationNode();
+ }
+
+ if (mAppPage != null) {
+ mAppPage.refreshUiApplicationNode();
+ }
+
+ if (mPermissionPage != null) {
+ mPermissionPage.refreshUiNode();
+ }
+
+ if (mInstrumentationPage != null) {
+ mInstrumentationPage.refreshUiNode();
+ }
+ }
+
+ /**
+ * Reads and processes the current markers and adds a listener for marker changes.
+ */
+ private void startMonitoringMarkers() {
+ final IFile inputFile = getInputFile();
+ if (inputFile != null) {
+ updateFromExistingMarkers(inputFile);
+
+ mMarkerMonitor = new IFileListener() {
+ public void fileChanged(IFile file, IMarkerDelta[] markerDeltas, int kind) {
+ if (file.equals(inputFile)) {
+ processMarkerChanges(markerDeltas);
+ }
+ }
+ };
+
+ ResourceMonitor.getMonitor().addFileListener(mMarkerMonitor, IResourceDelta.CHANGED);
+ }
+ }
+
+ /**
+ * Processes the markers of the specified {@link IFile} and updates the error status of
+ * {@link UiElementNode}s and {@link UiAttributeNode}s.
+ * @param inputFile the file being edited.
+ */
+ private void updateFromExistingMarkers(IFile inputFile) {
+ try {
+ // get the markers for the file
+ IMarker[] markers = inputFile.findMarkers(AndroidConstants.MARKER_ANDROID, true,
+ IResource.DEPTH_ZERO);
+
+ AndroidManifestDescriptors desc = getManifestDescriptors();
+ if (desc != null) {
+ ElementDescriptor appElement = desc.getApplicationElement();
+
+ if (appElement != null) {
+ UiElementNode app_ui_node = mUiManifestNode.findUiChildNode(
+ appElement.getXmlName());
+ List<UiElementNode> children = app_ui_node.getUiChildren();
+
+ for (IMarker marker : markers) {
+ processMarker(marker, children, IResourceDelta.ADDED);
+ }
+ }
+ }
+
+ } catch (CoreException e) {
+ // findMarkers can throw an exception, in which case, we'll do nothing.
+ }
+ }
+
+ /**
+ * Processes a {@link IMarker} change.
+ * @param markerDeltas the list of {@link IMarkerDelta}
+ */
+ private void processMarkerChanges(IMarkerDelta[] markerDeltas) {
+ AndroidManifestDescriptors descriptors = getManifestDescriptors();
+ if (descriptors != null && descriptors.getApplicationElement() != null) {
+ UiElementNode app_ui_node = mUiManifestNode.findUiChildNode(
+ descriptors.getApplicationElement().getXmlName());
+ List<UiElementNode> children = app_ui_node.getUiChildren();
+
+ for (IMarkerDelta markerDelta : markerDeltas) {
+ processMarker(markerDelta.getMarker(), children, markerDelta.getKind());
+ }
+ }
+ }
+
+ /**
+ * Processes a new/old/updated marker.
+ * @param marker The marker being added/removed/changed
+ * @param nodeList the list of activity/service/provider/receiver nodes.
+ * @param kind the change kind. Can be {@link IResourceDelta#ADDED},
+ * {@link IResourceDelta#REMOVED}, or {@link IResourceDelta#CHANGED}
+ */
+ private void processMarker(IMarker marker, List<UiElementNode> nodeList, int kind) {
+ // get the data from the marker
+ String nodeType = marker.getAttribute(AndroidConstants.MARKER_ATTR_TYPE, EMPTY);
+ if (nodeType == EMPTY) {
+ return;
+ }
+
+ String className = marker.getAttribute(AndroidConstants.MARKER_ATTR_CLASS, EMPTY);
+ if (className == EMPTY) {
+ return;
+ }
+
+ for (UiElementNode ui_node : nodeList) {
+ if (ui_node.getDescriptor().getXmlName().equals(nodeType)) {
+ for (UiAttributeNode attr : ui_node.getUiAttributes()) {
+ if (attr.getDescriptor().getXmlLocalName().equals(
+ AndroidManifestDescriptors.ANDROID_NAME_ATTR)) {
+ if (attr.getCurrentValue().equals(className)) {
+ if (kind == IResourceDelta.REMOVED) {
+ attr.setHasError(false);
+ } else {
+ attr.setHasError(true);
+ }
+ return;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates the initial UI Root Node, including the known mandatory elements.
+ * @param force if true, a new UiManifestNode is recreated even if it already exists.
+ */
+ @Override
+ protected void initUiRootNode(boolean force) {
+ // The manifest UI node is always created, even if there's no corresponding XML node.
+ if (mUiManifestNode != null && force == false) {
+ return;
+ }
+
+
+ AndroidManifestDescriptors manifestDescriptor = getManifestDescriptors();
+
+ if (manifestDescriptor != null) {
+ // save the old manifest node if it exists
+ UiElementNode oldManifestNode = mUiManifestNode;
+
+ ElementDescriptor manifestElement = manifestDescriptor.getManifestElement();
+ mUiManifestNode = manifestElement.createUiNode();
+ mUiManifestNode.setEditor(this);
+
+ // Similarly, always create the /manifest/application and /manifest/uses-sdk nodes
+ ElementDescriptor appElement = manifestDescriptor.getApplicationElement();
+ boolean present = false;
+ for (UiElementNode ui_node : mUiManifestNode.getUiChildren()) {
+ if (ui_node.getDescriptor() == appElement) {
+ present = true;
+ break;
+ }
+ }
+ if (!present) {
+ mUiManifestNode.appendNewUiChild(appElement);
+ }
+
+ appElement = manifestDescriptor.getUsesSdkElement();
+ present = false;
+ for (UiElementNode ui_node : mUiManifestNode.getUiChildren()) {
+ if (ui_node.getDescriptor() == appElement) {
+ present = true;
+ break;
+ }
+ }
+ if (!present) {
+ mUiManifestNode.appendNewUiChild(appElement);
+ }
+
+ if (oldManifestNode != null) {
+ onDescriptorsChanged(oldManifestNode);
+ }
+ } else {
+ // create a dummy descriptor/uinode until we have real descriptors
+ ElementDescriptor desc = new ElementDescriptor("manifest", //$NON-NLS-1$
+ "temporary descriptors due to missing decriptors", //$NON-NLS-1$
+ null /*tooltip*/, null /*sdk_url*/, null /*attributes*/,
+ null /*children*/, false /*mandatory*/);
+ mUiManifestNode = desc.createUiNode();
+ mUiManifestNode.setEditor(this);
+ }
+ }
+
+ /**
+ * Returns the {@link IFile} being edited, or <code>null</code> if it couldn't be computed.
+ */
+ private IFile getInputFile() {
+ IEditorInput input = getEditorInput();
+ if (input instanceof FileEditorInput) {
+ FileEditorInput fileInput = (FileEditorInput) input;
+ return fileInput.getFile();
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/ManifestEditorContributor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/ManifestEditorContributor.java
new file mode 100644
index 0000000..911faa1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/ManifestEditorContributor.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.manifest;
+
+import org.eclipse.jface.action.IAction;
+import org.eclipse.ui.IActionBars;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.actions.ActionFactory;
+import org.eclipse.ui.ide.IDEActionFactory;
+import org.eclipse.ui.part.MultiPageEditorActionBarContributor;
+import org.eclipse.ui.texteditor.ITextEditor;
+import org.eclipse.ui.texteditor.ITextEditorActionConstants;
+
+/**
+ * Manages the installation/deinstallation of global actions for multi-page
+ * editors. Responsible for the redirection of global actions to the active
+ * editor. Multi-page contributor replaces the contributors for the individual
+ * editors in the multi-page editor.
+ *
+ * TODO: Doesn't look like we need this. Remove it if not needed.
+ * @deprecated
+ */
+final class ManifestEditorContributor extends MultiPageEditorActionBarContributor {
+ private IEditorPart mActiveEditorPart;
+
+ /**
+ * Creates a multi-page contributor.
+ *
+ * Marked as Private so it can't be instanciated. This is a cheap way to make sure
+ * it's not being used. As noted in constructor, should be removed if not used.
+ * @deprecated
+ */
+ private ManifestEditorContributor() {
+ super();
+ }
+
+ /**
+ * Returns the action registed with the given text editor.
+ *
+ * @return IAction or null if editor is null.
+ */
+ protected IAction getAction(ITextEditor editor, String actionID) {
+ return (editor == null ? null : editor.getAction(actionID));
+ }
+
+ /*
+ * (non-JavaDoc) Method declared in
+ * AbstractMultiPageEditorActionBarContributor.
+ */
+
+ @Override
+ public void setActivePage(IEditorPart part) {
+ if (mActiveEditorPart == part)
+ return;
+
+ mActiveEditorPart = part;
+
+ IActionBars actionBars = getActionBars();
+ if (actionBars != null) {
+
+ ITextEditor editor =
+ (part instanceof ITextEditor) ? (ITextEditor)part : null;
+
+ actionBars.setGlobalActionHandler(ActionFactory.DELETE.getId(),
+ getAction(editor, ITextEditorActionConstants.DELETE));
+ actionBars.setGlobalActionHandler(ActionFactory.UNDO.getId(),
+ getAction(editor, ITextEditorActionConstants.UNDO));
+ actionBars.setGlobalActionHandler(ActionFactory.REDO.getId(),
+ getAction(editor, ITextEditorActionConstants.REDO));
+ actionBars.setGlobalActionHandler(ActionFactory.CUT.getId(),
+ getAction(editor, ITextEditorActionConstants.CUT));
+ actionBars.setGlobalActionHandler(ActionFactory.COPY.getId(),
+ getAction(editor, ITextEditorActionConstants.COPY));
+ actionBars.setGlobalActionHandler(ActionFactory.PASTE.getId(),
+ getAction(editor, ITextEditorActionConstants.PASTE));
+ actionBars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(),
+ getAction(editor, ITextEditorActionConstants.SELECT_ALL));
+ actionBars.setGlobalActionHandler(ActionFactory.FIND.getId(),
+ getAction(editor, ITextEditorActionConstants.FIND));
+ actionBars.setGlobalActionHandler(
+ IDEActionFactory.BOOKMARK.getId(), getAction(editor,
+ IDEActionFactory.BOOKMARK.getId()));
+ actionBars.updateActionBars();
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/ManifestSourceViewerConfig.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/ManifestSourceViewerConfig.java
new file mode 100644
index 0000000..e33e1ef
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/ManifestSourceViewerConfig.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.manifest;
+
+
+import com.android.ide.eclipse.editors.AndroidSourceViewerConfig;
+
+/**
+ * Source Viewer Configuration that calls in ManifestContentAssist.
+ */
+public final class ManifestSourceViewerConfig extends AndroidSourceViewerConfig {
+
+ public ManifestSourceViewerConfig() {
+ super(new ManifestContentAssist());
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/AndroidManifestDescriptors.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/AndroidManifestDescriptors.java
new file mode 100644
index 0000000..77c08b5
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/AndroidManifestDescriptors.java
@@ -0,0 +1,562 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.manifest.descriptors;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.common.resources.DeclareStyleableInfo;
+import com.android.ide.eclipse.common.resources.ResourceType;
+import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.editors.descriptors.IDescriptorProvider;
+import com.android.ide.eclipse.editors.descriptors.ListAttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.ReferenceAttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.XmlnsAttributeDescriptor;
+import com.android.sdklib.SdkConstants;
+
+import org.eclipse.core.runtime.IStatus;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.Map.Entry;
+
+
+/**
+ * Complete description of the AndroidManifest.xml structure.
+ * <p/>
+ * The root element are static instances which always exists.
+ * However their sub-elements and attributes are created only when the SDK changes or is
+ * loaded the first time.
+ */
+public final class AndroidManifestDescriptors implements IDescriptorProvider {
+
+ private static final String MANIFEST_NODE_NAME = "manifest"; //$NON-NLS-1$
+ private static final String ANDROID_MANIFEST_STYLEABLE = "AndroidManifest"; //$NON-NLS-1$
+
+ // Public attributes names, attributes descriptors and elements descriptors
+
+ public static final String ANDROID_LABEL_ATTR = "label"; //$NON-NLS-1$
+ public static final String ANDROID_NAME_ATTR = "name"; //$NON-NLS-1$
+ public static final String PACKAGE_ATTR = "package"; //$NON-NLS-1$
+
+ /** The {@link ElementDescriptor} for the root Manifest element. */
+ private final ElementDescriptor MANIFEST_ELEMENT;
+ /** The {@link ElementDescriptor} for the root Application element. */
+ private final ElementDescriptor APPLICATION_ELEMENT;
+
+ /** The {@link ElementDescriptor} for the root Instrumentation element. */
+ private final ElementDescriptor INTRUMENTATION_ELEMENT;
+ /** The {@link ElementDescriptor} for the root Permission element. */
+ private final ElementDescriptor PERMISSION_ELEMENT;
+ /** The {@link ElementDescriptor} for the root UsesPermission element. */
+ private final ElementDescriptor USES_PERMISSION_ELEMENT;
+ /** The {@link ElementDescriptor} for the root UsesSdk element. */
+ private final ElementDescriptor USES_SDK_ELEMENT;
+
+ /** The {@link ElementDescriptor} for the root PermissionGroup element. */
+ private final ElementDescriptor PERMISSION_GROUP_ELEMENT;
+ /** The {@link ElementDescriptor} for the root PermissionTree element. */
+ private final ElementDescriptor PERMISSION_TREE_ELEMENT;
+
+ /** Private package attribute for the manifest element. Needs to be handled manually. */
+ private final TextAttributeDescriptor PACKAGE_ATTR_DESC;
+
+ public AndroidManifestDescriptors() {
+ APPLICATION_ELEMENT = createElement("application", null, true); //$NON-NLS-1$ + no child & mandatory
+ INTRUMENTATION_ELEMENT = createElement("instrumentation"); //$NON-NLS-1$
+
+ PERMISSION_ELEMENT = createElement("permission"); //$NON-NLS-1$
+ USES_PERMISSION_ELEMENT = createElement("uses-permission"); //$NON-NLS-1$
+ USES_SDK_ELEMENT = createElement("uses-sdk", null, true); //$NON-NLS-1$ + no child & mandatory
+
+ PERMISSION_GROUP_ELEMENT = createElement("permission-group"); //$NON-NLS-1$
+ PERMISSION_TREE_ELEMENT = createElement("permission-tree"); //$NON-NLS-1$
+
+ MANIFEST_ELEMENT = createElement(
+ MANIFEST_NODE_NAME, // xml name
+ new ElementDescriptor[] {
+ APPLICATION_ELEMENT,
+ INTRUMENTATION_ELEMENT,
+ PERMISSION_ELEMENT,
+ USES_PERMISSION_ELEMENT,
+ PERMISSION_GROUP_ELEMENT,
+ PERMISSION_TREE_ELEMENT,
+ USES_SDK_ELEMENT,
+ },
+ true /* mandatory */);
+
+ // The "package" attribute is treated differently as it doesn't have the standard
+ // Android XML namespace.
+ PACKAGE_ATTR_DESC = new PackageAttributeDescriptor(PACKAGE_ATTR,
+ "Package",
+ null /* nsUri */,
+ "This attribute gives a unique name for the package, using a Java-style naming convention to avoid name collisions.\nFor example, applications published by Google could have names of the form com.google.app.appname");
+ }
+
+ public ElementDescriptor[] getRootElementDescriptors() {
+ return new ElementDescriptor[] { MANIFEST_ELEMENT };
+ }
+
+ public ElementDescriptor getDescriptor() {
+ return getManifestElement();
+ }
+
+ public ElementDescriptor getApplicationElement() {
+ return APPLICATION_ELEMENT;
+ }
+
+ public ElementDescriptor getManifestElement() {
+ return MANIFEST_ELEMENT;
+ }
+
+ public ElementDescriptor getUsesSdkElement() {
+ return USES_SDK_ELEMENT;
+ }
+
+ public ElementDescriptor getInstrumentationElement() {
+ return INTRUMENTATION_ELEMENT;
+ }
+
+ public ElementDescriptor getPermissionElement() {
+ return PERMISSION_ELEMENT;
+ }
+
+ public ElementDescriptor getUsesPermissionElement() {
+ return USES_PERMISSION_ELEMENT;
+ }
+
+ public ElementDescriptor getPermissionGroupElement() {
+ return PERMISSION_GROUP_ELEMENT;
+ }
+
+ public ElementDescriptor getPermissionTreeElement() {
+ return PERMISSION_TREE_ELEMENT;
+ }
+
+ /**
+ * Updates the document descriptor.
+ * <p/>
+ * It first computes the new children of the descriptor and then updates them
+ * all at once.
+ *
+ * @param manifestMap The map style => attributes from the attrs_manifest.xml file
+ */
+ public synchronized void updateDescriptors(
+ Map<String, DeclareStyleableInfo> manifestMap) {
+
+ XmlnsAttributeDescriptor xmlns = new XmlnsAttributeDescriptor(
+ "android", //$NON-NLS-1$
+ SdkConstants.NS_RESOURCES);
+
+ // -- setup the required attributes overrides --
+
+ Set<String> required = new HashSet<String>();
+ required.add("provider/authorities"); //$NON-NLS-1$
+
+ // -- setup the various attribute format overrides --
+
+ // The key for each override is "element1,element2,.../attr-xml-local-name" or
+ // "*/attr-xml-local-name" to match the attribute in any element.
+
+ Map<String, Object> overrides = new HashMap<String, Object>();
+
+ overrides.put("*/icon", new DescriptorsUtils.ITextAttributeCreator() { //$NON-NLS-1$
+ public TextAttributeDescriptor create(String xmlName, String uiName, String nsUri,
+ String tooltip) {
+ return new ReferenceAttributeDescriptor(
+ ResourceType.DRAWABLE,
+ xmlName, uiName, nsUri,
+ tooltip);
+ }
+ });
+
+ overrides.put("*/theme", ThemeAttributeDescriptor.class); //$NON-NLS-1$
+ overrides.put("*/permission", ListAttributeDescriptor.class); //$NON-NLS-1$
+ overrides.put("*/targetPackage", PackageAttributeDescriptor.class); //$NON-NLS-1$
+
+ overrides.put("uses-library/name", ListAttributeDescriptor.class); //$NON-NLS-1$
+
+ overrides.put("action,category,uses-permission/" + ANDROID_NAME_ATTR, //$NON-NLS-1$
+ ListAttributeDescriptor.class);
+ overrides.put("application/" + ANDROID_NAME_ATTR, ApplicationAttributeDescriptor.class); //$NON-NLS-1$
+
+ overrideClassName(overrides, "activity", AndroidConstants.CLASS_ACTIVITY); //$NON-NLS-1$
+ overrideClassName(overrides, "receiver", AndroidConstants.CLASS_BROADCASTRECEIVER); //$NON-NLS-1$
+ overrideClassName(overrides, "service", AndroidConstants.CLASS_SERVICE); //$NON-NLS-1$
+ overrideClassName(overrides, "provider", AndroidConstants.CLASS_CONTENTPROVIDER); //$NON-NLS-1$
+
+ // -- list element nodes already created --
+ // These elements are referenced by already opened editors, so we want to update them
+ // but not re-create them when reloading an SDK on the fly.
+
+ HashMap<String, ElementDescriptor> elementDescs =
+ new HashMap<String, ElementDescriptor>();
+ elementDescs.put(MANIFEST_ELEMENT.getXmlLocalName(), MANIFEST_ELEMENT);
+ elementDescs.put(APPLICATION_ELEMENT.getXmlLocalName(), APPLICATION_ELEMENT);
+ elementDescs.put(INTRUMENTATION_ELEMENT.getXmlLocalName(), INTRUMENTATION_ELEMENT);
+ elementDescs.put(PERMISSION_ELEMENT.getXmlLocalName(), PERMISSION_ELEMENT);
+ elementDescs.put(USES_PERMISSION_ELEMENT.getXmlLocalName(), USES_PERMISSION_ELEMENT);
+ elementDescs.put(USES_SDK_ELEMENT.getXmlLocalName(), USES_SDK_ELEMENT);
+ elementDescs.put(PERMISSION_GROUP_ELEMENT.getXmlLocalName(), PERMISSION_GROUP_ELEMENT);
+ elementDescs.put(PERMISSION_TREE_ELEMENT.getXmlLocalName(), PERMISSION_TREE_ELEMENT);
+
+ // --
+
+ inflateElement(manifestMap,
+ overrides,
+ required,
+ elementDescs,
+ MANIFEST_ELEMENT,
+ "AndroidManifest"); //$NON-NLS-1$
+ insertAttribute(MANIFEST_ELEMENT, PACKAGE_ATTR_DESC);
+
+ sanityCheck(manifestMap, MANIFEST_ELEMENT);
+ }
+
+ /**
+ * Sets up an attribute override for ANDROID_NAME_ATTR using a ClassAttributeDescriptor
+ * with the specified class name.
+ */
+ private static void overrideClassName(Map<String, Object> overrides,
+ String elementName, final String className) {
+ overrides.put(elementName + "/" + ANDROID_NAME_ATTR,
+ new DescriptorsUtils.ITextAttributeCreator() {
+ public TextAttributeDescriptor create(String xmlName, String uiName, String nsUri,
+ String tooltip) {
+ if (AndroidConstants.CLASS_ACTIVITY.equals(className)) {
+ return new ClassAttributeDescriptor(
+ className,
+ PostActivityCreationAction.getAction(),
+ xmlName, uiName + "*", //$NON-NLS-1$
+ nsUri,
+ tooltip,
+ true /*mandatory */);
+ } else if (AndroidConstants.CLASS_BROADCASTRECEIVER.equals(className)) {
+ return new ClassAttributeDescriptor(
+ className,
+ PostReceiverCreationAction.getAction(),
+ xmlName, uiName + "*", //$NON-NLS-1$
+ nsUri,
+ tooltip,
+ true /*mandatory */);
+
+ } else {
+ return new ClassAttributeDescriptor(
+ className,
+ xmlName, uiName + "*", //$NON-NLS-1$
+ nsUri,
+ tooltip,
+ true /*mandatory */);
+ }
+ }
+ });
+ }
+
+ /**
+ * Returns a new ElementDescriptor constructed from the information given here
+ * and the javadoc & attributes extracted from the style map if any.
+ * <p/>
+ * Creates an element with no attribute overrides.
+ */
+ private ElementDescriptor createElement(
+ String xmlName,
+ ElementDescriptor[] childrenElements,
+ boolean mandatory) {
+ // Creates an element with no attribute overrides.
+ String styleName = guessStyleName(xmlName);
+ String sdkUrl = DescriptorsUtils.MANIFEST_SDK_URL + styleName;
+ String uiName = getUiName(xmlName);
+
+ ElementDescriptor element = new ManifestElementDescriptor(xmlName, uiName, null, sdkUrl,
+ null, childrenElements, mandatory);
+
+ return element;
+ }
+
+ /**
+ * Returns a new ElementDescriptor constructed from its XML local name.
+ * <p/>
+ * This version creates an element not mandatory.
+ */
+ private ElementDescriptor createElement(String xmlName) {
+ // Creates an element with no child and not mandatory
+ return createElement(xmlName, null, false);
+ }
+
+ /**
+ * Inserts an attribute in this element attribute list if it is not present there yet
+ * (based on the attribute XML name.)
+ * The attribute is inserted at the beginning of the attribute list.
+ */
+ private void insertAttribute(ElementDescriptor element, AttributeDescriptor newAttr) {
+ AttributeDescriptor[] attributes = element.getAttributes();
+ for (AttributeDescriptor attr : attributes) {
+ if (attr.getXmlLocalName().equals(newAttr.getXmlLocalName())) {
+ return;
+ }
+ }
+
+ AttributeDescriptor[] newArray = new AttributeDescriptor[attributes.length + 1];
+ newArray[0] = newAttr;
+ System.arraycopy(attributes, 0, newArray, 1, attributes.length);
+ element.setAttributes(newArray);
+ }
+
+ /**
+ * "Inflates" the properties of an {@link ElementDescriptor} from the styleable declaration.
+ * <p/>
+ * This first creates all the attributes for the given ElementDescriptor.
+ * It then finds all children of the descriptor, inflate them recursively and set them
+ * as child to this ElementDescriptor.
+ *
+ * @param styleMap The input styleable map for manifest elements & attributes.
+ * @param overrides A list of attribute overrides (to customize the type of the attribute
+ * descriptors).
+ * @param requiredAttributes Set of attributes to be marked as required.
+ * @param existingElementDescs A map of already created element descriptors, keyed by
+ * XML local name. This is used to use the static elements created initially by this
+ * class, which are referenced directly by editors (so that reloading an SDK won't
+ * break these references).
+ * @param elemDesc The current {@link ElementDescriptor} to inflate.
+ * @param styleName The name of the {@link ElementDescriptor} to inflate. Its XML local name
+ * will be guessed automatically from the style name.
+ */
+ private void inflateElement(
+ Map<String, DeclareStyleableInfo> styleMap,
+ Map<String, Object> overrides,
+ Set<String> requiredAttributes,
+ HashMap<String, ElementDescriptor> existingElementDescs,
+ ElementDescriptor elemDesc,
+ String styleName) {
+ assert elemDesc != null;
+ assert styleName != null;
+
+ // define attributes
+ DeclareStyleableInfo style = styleMap != null ? styleMap.get(styleName) : null;
+ if (style != null) {
+ ArrayList<AttributeDescriptor> attrDescs = new ArrayList<AttributeDescriptor>();
+ DescriptorsUtils.appendAttributes(attrDescs,
+ elemDesc.getXmlLocalName(),
+ SdkConstants.NS_RESOURCES,
+ style.getAttributes(),
+ requiredAttributes,
+ overrides);
+ elemDesc.setTooltip(style.getJavaDoc());
+ elemDesc.setAttributes(attrDescs.toArray(new AttributeDescriptor[attrDescs.size()]));
+ }
+
+ // find all elements that have this one as parent
+ ArrayList<ElementDescriptor> children = new ArrayList<ElementDescriptor>();
+ for (Entry<String, DeclareStyleableInfo> entry : styleMap.entrySet()) {
+ DeclareStyleableInfo childStyle = entry.getValue();
+ boolean isParent = false;
+ String[] parents = childStyle.getParents();
+ if (parents != null) {
+ for (String parent: parents) {
+ if (styleName.equals(parent)) {
+ isParent = true;
+ break;
+ }
+ }
+ }
+ if (isParent) {
+ String childStyleName = entry.getKey();
+ String childXmlName = guessXmlName(childStyleName);
+
+ // create or re-use element
+ ElementDescriptor child = existingElementDescs.get(childXmlName);
+ if (child == null) {
+ child = createElement(childXmlName);
+ existingElementDescs.put(childXmlName, child);
+ }
+ children.add(child);
+
+ inflateElement(styleMap,
+ overrides,
+ requiredAttributes,
+ existingElementDescs,
+ child,
+ childStyleName);
+ }
+ }
+ elemDesc.setChildren(children.toArray(new ElementDescriptor[children.size()]));
+ }
+
+ /**
+ * Get an UI name from the element XML name.
+ * <p/>
+ * Capitalizes the first letter and replace non-alphabet by a space followed by a capital.
+ */
+ private String getUiName(String xmlName) {
+ StringBuilder sb = new StringBuilder();
+
+ boolean capitalize = true;
+ for (char c : xmlName.toCharArray()) {
+ if (capitalize && c >= 'a' && c <= 'z') {
+ sb.append((char)(c + 'A' - 'a'));
+ capitalize = false;
+ } else if ((c < 'A' || c > 'Z') && (c < 'a' || c > 'z')) {
+ sb.append(' ');
+ capitalize = true;
+ } else {
+ sb.append(c);
+ }
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Guesses the style name for a given XML element name.
+ * <p/>
+ * The rules are:
+ * - capitalize the first letter:
+ * - if there's a dash, skip it and capitalize the next one
+ * - prefix AndroidManifest
+ * The exception is "manifest" which just becomes AndroidManifest.
+ * <p/>
+ * Examples:
+ * - manifest => AndroidManifest
+ * - application => AndroidManifestApplication
+ * - uses-permission => AndroidManifestUsesPermission
+ */
+ private String guessStyleName(String xmlName) {
+ StringBuilder sb = new StringBuilder();
+
+ if (!xmlName.equals(MANIFEST_NODE_NAME)) {
+ boolean capitalize = true;
+ for (char c : xmlName.toCharArray()) {
+ if (capitalize && c >= 'a' && c <= 'z') {
+ sb.append((char)(c + 'A' - 'a'));
+ capitalize = false;
+ } else if ((c < 'A' || c > 'Z') && (c < 'a' || c > 'z')) {
+ // not a letter -- skip the character and capitalize the next one
+ capitalize = true;
+ } else {
+ sb.append(c);
+ }
+ }
+ }
+
+ sb.insert(0, ANDROID_MANIFEST_STYLEABLE);
+ return sb.toString();
+ }
+
+ /**
+ * This method performs a sanity check to make sure all the styles declared in the
+ * manifestMap are actually defined in the actual element descriptors and reachable from
+ * the manifestElement root node.
+ */
+ private void sanityCheck(Map<String, DeclareStyleableInfo> manifestMap,
+ ElementDescriptor manifestElement) {
+ TreeSet<String> elementsDeclared = new TreeSet<String>();
+ findAllElementNames(manifestElement, elementsDeclared);
+
+ TreeSet<String> stylesDeclared = new TreeSet<String>();
+ for (String styleName : manifestMap.keySet()) {
+ if (styleName.startsWith(ANDROID_MANIFEST_STYLEABLE)) {
+ stylesDeclared.add(styleName);
+ }
+ }
+
+ for (Iterator<String> it = elementsDeclared.iterator(); it.hasNext();) {
+ String xmlName = it.next();
+ String styleName = guessStyleName(xmlName);
+ if (stylesDeclared.remove(styleName)) {
+ it.remove();
+ }
+ }
+
+ StringBuilder sb = new StringBuilder();
+ if (!stylesDeclared.isEmpty()) {
+ sb.append("Warning, ADT/SDK Mismatch! The following elements are declared by the SDK but unknown to ADT: ");
+ for (String name : stylesDeclared) {
+ name = guessXmlName(name);
+
+ if (name != stylesDeclared.last()) {
+ sb.append(", "); //$NON-NLS-1$
+ }
+ }
+
+ AdtPlugin.log(IStatus.WARNING, "%s", sb.toString());
+ AdtPlugin.printToConsole((String)null, sb);
+ sb.setLength(0);
+ }
+
+ if (!elementsDeclared.isEmpty()) {
+ sb.append("Warning, ADT/SDK Mismatch! The following elements are declared by ADT but not by the SDK: ");
+ for (String name : elementsDeclared) {
+ sb.append(name);
+ if (name != elementsDeclared.last()) {
+ sb.append(", "); //$NON-NLS-1$
+ }
+ }
+
+ AdtPlugin.log(IStatus.WARNING, "%s", sb.toString());
+ AdtPlugin.printToConsole((String)null, sb);
+ }
+ }
+
+ /**
+ * Performs an approximate translation of the style name into a potential
+ * xml name. This is more or less the reverse from guessStyleName().
+ *
+ * @return The XML local name for a given style name.
+ */
+ private String guessXmlName(String name) {
+ StringBuilder sb = new StringBuilder();
+ if (ANDROID_MANIFEST_STYLEABLE.equals(name)) {
+ sb.append(MANIFEST_NODE_NAME);
+ } else {
+ name = name.replace(ANDROID_MANIFEST_STYLEABLE, ""); //$NON-NLS-1$
+ boolean first_char = true;
+ for (char c : name.toCharArray()) {
+ if (c >= 'A' && c <= 'Z') {
+ if (!first_char) {
+ sb.append('-');
+ }
+ c = (char) (c - 'A' + 'a');
+ }
+ sb.append(c);
+ first_char = false;
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Helper method used by {@link #sanityCheck(Map, ElementDescriptor)} to find all the
+ * {@link ElementDescriptor} names defined by the tree of descriptors.
+ * <p/>
+ * Note: this assumes no circular reference in the tree of {@link ElementDescriptor}s.
+ */
+ private void findAllElementNames(ElementDescriptor element, TreeSet<String> declared) {
+ declared.add(element.getXmlName());
+ for (ElementDescriptor desc : element.getChildren()) {
+ findAllElementNames(desc, declared);
+ }
+ }
+
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/ApplicationAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/ApplicationAttributeDescriptor.java
new file mode 100644
index 0000000..eab7f09
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/ApplicationAttributeDescriptor.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.manifest.descriptors;
+
+import com.android.ide.eclipse.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.editors.manifest.model.UiClassAttributeNode;
+import com.android.ide.eclipse.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+/**
+ * Describes an 'Application' class XML attribute. It is displayed by a
+ * {@link UiClassAttributeNode}, that restricts creation and selection to classes
+ * inheriting from android.app.Application.
+ */
+public class ApplicationAttributeDescriptor extends TextAttributeDescriptor {
+
+ public ApplicationAttributeDescriptor(String xmlLocalName, String uiName,
+ String nsUri, String tooltip) {
+ super(xmlLocalName, uiName, nsUri, tooltip);
+ }
+
+ /**
+ * @return A new {@link UiClassAttributeNode} linked to this descriptor.
+ */
+ @Override
+ public UiAttributeNode createUiNode(UiElementNode uiParent) {
+ return new UiClassAttributeNode("android.app.Application", //$NON-NLS-1$
+ null /* postCreationAction */, false /* mandatory */, this, uiParent);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/ClassAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/ClassAttributeDescriptor.java
new file mode 100644
index 0000000..629b37c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/ClassAttributeDescriptor.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.manifest.descriptors;
+
+import com.android.ide.eclipse.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.editors.manifest.model.UiClassAttributeNode;
+import com.android.ide.eclipse.editors.manifest.model.UiClassAttributeNode.IPostTypeCreationAction;
+import com.android.ide.eclipse.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+import com.android.sdklib.SdkConstants;
+
+/**
+ * Describes an XML attribute representing a class name.
+ * It is displayed by a {@link UiClassAttributeNode}.
+ */
+public class ClassAttributeDescriptor extends TextAttributeDescriptor {
+
+ /** Superclass of the class value. */
+ private String mSuperClassName;
+
+ private IPostTypeCreationAction mPostCreationAction;
+
+ /** indicates if the class parameter is mandatory */
+ boolean mMandatory;
+
+ /**
+ * Creates a new {@link ClassAttributeDescriptor}
+ * @param superClassName the fully qualified name of the superclass of the class represented
+ * by the attribute.
+ * @param xmlLocalName The XML name of the attribute (case sensitive, with android: prefix).
+ * @param uiName The UI name of the attribute. Cannot be an empty string and cannot be null.
+ * @param nsUri The URI of the attribute. Can be null if attribute has no namespace.
+ * See {@link SdkConstants#NS_RESOURCES} for a common value.
+ * @param tooltip A non-empty tooltip string or null.
+ * @param mandatory indicates if the class attribute is mandatory.
+ */
+ public ClassAttributeDescriptor(String superClassName,
+ String xmlLocalName, String uiName, String nsUri,
+ String tooltip, boolean mandatory) {
+ super(xmlLocalName, uiName, nsUri, tooltip);
+ mSuperClassName = superClassName;
+ }
+
+ /**
+ * Creates a new {@link ClassAttributeDescriptor}
+ * @param superClassName the fully qualified name of the superclass of the class represented
+ * by the attribute.
+ * @param postCreationAction the {@link IPostTypeCreationAction} to be executed on the
+ * newly created class.
+ * @param xmlLocalName The XML local name of the attribute (case sensitive).
+ * @param uiName The UI name of the attribute. Cannot be an empty string and cannot be null.
+ * @param nsUri The URI of the attribute. Can be null if attribute has no namespace.
+ * See {@link SdkConstants#NS_RESOURCES} for a common value.
+ * @param tooltip A non-empty tooltip string or null.
+ * @param mandatory indicates if the class attribute is mandatory.
+ */
+ public ClassAttributeDescriptor(String superClassName,
+ IPostTypeCreationAction postCreationAction,
+ String xmlLocalName, String uiName, String nsUri,
+ String tooltip, boolean mandatory) {
+ super(xmlLocalName, uiName, nsUri, tooltip);
+ mSuperClassName = superClassName;
+ mPostCreationAction = postCreationAction;
+ }
+
+ /**
+ * @return A new {@link UiClassAttributeNode} linked to this descriptor.
+ */
+ @Override
+ public UiAttributeNode createUiNode(UiElementNode uiParent) {
+ return new UiClassAttributeNode(mSuperClassName, mPostCreationAction,
+ mMandatory, this, uiParent);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/InstrumentationAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/InstrumentationAttributeDescriptor.java
new file mode 100644
index 0000000..6e589f7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/InstrumentationAttributeDescriptor.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.manifest.descriptors;
+
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.editors.manifest.model.UiClassAttributeNode;
+import com.android.ide.eclipse.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+/**
+ * Describes a 'Instrumentation' class XML attribute. It is displayed by a
+ * {@link UiClassAttributeNode}, that restricts creation and selection to classes inheriting from
+ * android.app.Instrumentation.
+ */
+public class InstrumentationAttributeDescriptor extends TextAttributeDescriptor {
+
+ public InstrumentationAttributeDescriptor(String xmlLocalName, String uiName, String nsUri,
+ String tooltip) {
+ super(xmlLocalName, uiName, nsUri, tooltip);
+ }
+
+ /**
+ * @return A new {@link UiClassAttributeNode} linked to this descriptor.
+ */
+ @Override
+ public UiAttributeNode createUiNode(UiElementNode uiParent) {
+ return new UiClassAttributeNode(AndroidConstants.CLASS_INSTRUMENTATION,
+ null /* postCreationAction */, true /* mandatory */, this, uiParent);
+ }
+}
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/ManifestElementDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/ManifestElementDescriptor.java
new file mode 100644
index 0000000..d89292b
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/ManifestElementDescriptor.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.manifest.descriptors;
+
+import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.editors.manifest.model.UiManifestElementNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+/**
+ * {@link ManifestElementDescriptor} describes an XML element node, with its
+ * element name, its possible attributes, its possible child elements but also
+ * its display name and tooltip.
+ *
+ * This {@link ElementDescriptor} is specialized to create {@link UiManifestElementNode} UI nodes.
+ */
+public class ManifestElementDescriptor extends ElementDescriptor {
+
+ /**
+ * Constructs a new {@link ManifestElementDescriptor}.
+ *
+ * @param xml_name The XML element node name. Case sensitive.
+ * @param ui_name The XML element name for the user interface, typically capitalized.
+ * @param tooltip An optional tooltip. Can be null or empty.
+ * @param sdk_url An optional SKD URL. Can be null or empty.
+ * @param attributes The list of allowed attributes. Can be null or empty.
+ * @param children The list of allowed children. Can be null or empty.
+ * @param mandatory Whether this node must always exist (even for empty models).
+ */
+ public ManifestElementDescriptor(String xml_name, String ui_name, String tooltip, String sdk_url,
+ AttributeDescriptor[] attributes,
+ ElementDescriptor[] children,
+ boolean mandatory) {
+ super(xml_name, ui_name, tooltip, sdk_url, attributes, children, mandatory);
+ }
+
+ /**
+ * Constructs a new {@link ManifestElementDescriptor}.
+ *
+ * @param xml_name The XML element node name. Case sensitive.
+ * @param ui_name The XML element name for the user interface, typically capitalized.
+ * @param tooltip An optional tooltip. Can be null or empty.
+ * @param sdk_url An optional SKD URL. Can be null or empty.
+ * @param attributes The list of allowed attributes. Can be null or empty.
+ * @param children The list of allowed children. Can be null or empty.
+ */
+ public ManifestElementDescriptor(String xml_name, String ui_name, String tooltip, String sdk_url,
+ AttributeDescriptor[] attributes,
+ ElementDescriptor[] children) {
+ super(xml_name, ui_name, tooltip, sdk_url, attributes, children, false);
+ }
+
+ /**
+ * This is a shortcut for
+ * ManifestElementDescriptor(xml_name, xml_name.capitalize(), null, null, null, children).
+ * This constructor is mostly used for unit tests.
+ *
+ * @param xml_name The XML element node name. Case sensitive.
+ */
+ public ManifestElementDescriptor(String xml_name, ElementDescriptor[] children) {
+ super(xml_name, children);
+ }
+
+ /**
+ * This is a shortcut for
+ * ManifestElementDescriptor(xml_name, xml_name.capitalize(), null, null, null, null).
+ * This constructor is mostly used for unit tests.
+ *
+ * @param xml_name The XML element node name. Case sensitive.
+ */
+ public ManifestElementDescriptor(String xml_name) {
+ super(xml_name, null);
+ }
+
+ /**
+ * @return A new {@link UiElementNode} linked to this descriptor.
+ */
+ @Override
+ public UiElementNode createUiNode() {
+ return new UiManifestElementNode(this);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/PackageAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/PackageAttributeDescriptor.java
new file mode 100644
index 0000000..34c5d0d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/PackageAttributeDescriptor.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.manifest.descriptors;
+
+import com.android.ide.eclipse.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.editors.manifest.model.UiPackageAttributeNode;
+import com.android.ide.eclipse.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+/**
+ * Describes a package XML attribute. It is displayed by a {@link UiPackageAttributeNode}.
+ */
+public class PackageAttributeDescriptor extends TextAttributeDescriptor {
+
+ public PackageAttributeDescriptor(String xmlLocalName, String uiName, String nsUri,
+ String tooltip) {
+ super(xmlLocalName, uiName, nsUri, tooltip);
+ }
+
+ /**
+ * @return A new {@link UiPackageAttributeNode} linked to this descriptor.
+ */
+ @Override
+ public UiAttributeNode createUiNode(UiElementNode uiParent) {
+ return new UiPackageAttributeNode(this, uiParent);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/PostActivityCreationAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/PostActivityCreationAction.java
new file mode 100644
index 0000000..3442c24
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/PostActivityCreationAction.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.manifest.descriptors;
+
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.editors.manifest.model.UiClassAttributeNode.IPostTypeCreationAction;
+
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.jdt.core.ICompilationUnit;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.JavaModelException;
+
+/**
+ * Action to be executed after an Activity class is created.
+ */
+class PostActivityCreationAction implements IPostTypeCreationAction {
+
+ private final static PostActivityCreationAction sAction = new PostActivityCreationAction();
+
+ private PostActivityCreationAction() {
+ // private constructor to enforce singleton.
+ }
+
+
+ /**
+ * Returns the action.
+ */
+ public static IPostTypeCreationAction getAction() {
+ return sAction;
+ }
+
+ /**
+ * Processes a newly created Activity.
+ *
+ */
+ public void processNewType(IType newType) {
+ try {
+ String methodContent =
+ " /** Called when the activity is first created. */\n" +
+ " @Override\n" +
+ " public void onCreate(Bundle savedInstanceState) {\n" +
+ " super.onCreate(savedInstanceState);\n" +
+ "\n" +
+ " // TODO Auto-generated method stub\n" +
+ " }";
+ newType.createMethod(methodContent, null /* sibling*/, false /* force */,
+ new NullProgressMonitor());
+
+ // we need to add the import for Bundle, so we need the compilation unit.
+ // Since the type could be enclosed in other types, we loop till we find it.
+ ICompilationUnit compilationUnit = null;
+ IJavaElement element = newType;
+ do {
+ IJavaElement parentElement = element.getParent();
+ if (parentElement != null) {
+ if (parentElement.getElementType() == IJavaElement.COMPILATION_UNIT) {
+ compilationUnit = (ICompilationUnit)parentElement;
+ }
+
+ element = parentElement;
+ } else {
+ break;
+ }
+ } while (compilationUnit == null);
+
+ if (compilationUnit != null) {
+ compilationUnit.createImport(AndroidConstants.CLASS_BUNDLE,
+ null /* sibling */, new NullProgressMonitor());
+ }
+ } catch (JavaModelException e) {
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/PostReceiverCreationAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/PostReceiverCreationAction.java
new file mode 100644
index 0000000..5a8137d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/PostReceiverCreationAction.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.manifest.descriptors;
+
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.editors.manifest.model.UiClassAttributeNode.IPostTypeCreationAction;
+
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.jdt.core.ICompilationUnit;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.JavaModelException;
+
+/**
+ * Action to be executed after an BroadcastReceiver class is created.
+ */
+class PostReceiverCreationAction implements IPostTypeCreationAction {
+
+ private final static PostReceiverCreationAction sAction = new PostReceiverCreationAction();
+
+ private PostReceiverCreationAction() {
+ // private constructor to enforce singleton.
+ }
+
+ /**
+ * Returns the action.
+ */
+ public static IPostTypeCreationAction getAction() {
+ return sAction;
+ }
+
+ /**
+ * Processes a newly created Activity.
+ *
+ */
+ public void processNewType(IType newType) {
+ try {
+ String methodContent =
+ " @Override\n" +
+ " public void onReceive(Context context, Intent intent) {\n" +
+ " // TODO Auto-generated method stub\n" +
+ " }";
+ newType.createMethod(methodContent, null /* sibling*/, false /* force */,
+ new NullProgressMonitor());
+
+ // we need to add the import for Bundle, so we need the compilation unit.
+ // Since the type could be enclosed in other types, we loop till we find it.
+ ICompilationUnit compilationUnit = null;
+ IJavaElement element = newType;
+ do {
+ IJavaElement parentElement = element.getParent();
+ if (parentElement != null) {
+ if (parentElement.getElementType() == IJavaElement.COMPILATION_UNIT) {
+ compilationUnit = (ICompilationUnit)parentElement;
+ }
+
+ element = parentElement;
+ } else {
+ break;
+ }
+ } while (compilationUnit == null);
+
+ if (compilationUnit != null) {
+ compilationUnit.createImport(AndroidConstants.CLASS_CONTEXT,
+ null /* sibling */, new NullProgressMonitor());
+ compilationUnit.createImport(AndroidConstants.CLASS_INTENT,
+ null /* sibling */, new NullProgressMonitor());
+ }
+ } catch (JavaModelException e) {
+ // looks like the class already existed (this happens when the user check to create
+ // inherited abstract methods).
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/ThemeAttributeDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/ThemeAttributeDescriptor.java
new file mode 100644
index 0000000..4219007
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/descriptors/ThemeAttributeDescriptor.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.manifest.descriptors;
+
+import com.android.ide.eclipse.common.resources.ResourceType;
+import com.android.ide.eclipse.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.editors.uimodel.UiResourceAttributeNode;
+
+/**
+ * Describes a Theme/Style XML attribute displayed by a {@link UiResourceAttributeNode}
+ */
+public final class ThemeAttributeDescriptor extends TextAttributeDescriptor {
+
+ public ThemeAttributeDescriptor(String xmlLocalName, String uiName, String nsUri,
+ String tooltip) {
+ super(xmlLocalName, uiName, nsUri, tooltip);
+ }
+
+ /**
+ * @return A new {@link UiResourceAttributeNode} linked to this theme descriptor.
+ */
+ @Override
+ public UiAttributeNode createUiNode(UiElementNode uiParent) {
+ return new UiResourceAttributeNode(ResourceType.STYLE, this, uiParent);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/model/UiClassAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/model/UiClassAttributeNode.java
new file mode 100644
index 0000000..f8aac1d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/model/UiClassAttributeNode.java
@@ -0,0 +1,624 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.manifest.model;
+
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.common.project.AndroidManifestHelper;
+import com.android.ide.eclipse.common.project.BaseProjectHelper;
+import com.android.ide.eclipse.editors.AndroidEditor;
+import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.editors.manifest.descriptors.AndroidManifestDescriptors;
+import com.android.ide.eclipse.editors.ui.SectionHelper;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.editors.uimodel.UiTextAttributeNode;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.jdt.core.IClasspathEntry;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IPackageFragment;
+import org.eclipse.jdt.core.IPackageFragmentRoot;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.ITypeHierarchy;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.core.search.IJavaSearchScope;
+import org.eclipse.jdt.core.search.SearchEngine;
+import org.eclipse.jdt.ui.IJavaElementSearchConstants;
+import org.eclipse.jdt.ui.JavaUI;
+import org.eclipse.jdt.ui.actions.OpenNewClassWizardAction;
+import org.eclipse.jdt.ui.dialogs.ITypeInfoFilterExtension;
+import org.eclipse.jdt.ui.dialogs.ITypeInfoRequestor;
+import org.eclipse.jdt.ui.dialogs.ITypeSelectionComponent;
+import org.eclipse.jdt.ui.dialogs.TypeSelectionExtension;
+import org.eclipse.jdt.ui.wizards.NewClassWizardPage;
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.jface.window.Window;
+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.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.Text;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IFileEditorInput;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.dialogs.SelectionDialog;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.events.HyperlinkAdapter;
+import org.eclipse.ui.forms.events.HyperlinkEvent;
+import org.eclipse.ui.forms.widgets.FormText;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.TableWrapData;
+import org.w3c.dom.Element;
+
+import java.util.ArrayList;
+
+/**
+ * Represents an XML attribute for a class, that can be modified using a simple text field or
+ * a dialog to choose an existing class. Also, there's a link to create a new class.
+ * <p/>
+ * See {@link UiTextAttributeNode} for more information.
+ */
+public class UiClassAttributeNode extends UiTextAttributeNode {
+
+ private String mReferenceClass;
+ private IPostTypeCreationAction mPostCreationAction;
+ private boolean mMandatory;
+
+ private class HierarchyTypeSelection extends TypeSelectionExtension {
+
+ private IJavaProject mJavaProject;
+
+ public HierarchyTypeSelection(IProject project, String referenceClass) {
+ mJavaProject = JavaCore.create(project);
+ mReferenceClass = referenceClass;
+ }
+
+ @Override
+ public ITypeInfoFilterExtension getFilterExtension() {
+ return new ITypeInfoFilterExtension() {
+ public boolean select(ITypeInfoRequestor typeInfoRequestor) {
+ String packageName = typeInfoRequestor.getPackageName();
+ String typeName = typeInfoRequestor.getTypeName();
+ String enclosingType = typeInfoRequestor.getEnclosingName();
+
+ // build the full class name.
+ StringBuilder sb = new StringBuilder(packageName);
+ sb.append('.');
+ if (enclosingType.length() > 0) {
+ sb.append(enclosingType);
+ sb.append('.');
+ }
+ sb.append(typeName);
+
+ String className = sb.toString();
+
+ try {
+ IType type = mJavaProject.findType(className);
+ if (type != null) {
+ // get the type hierarchy
+ ITypeHierarchy hierarchy = type.newSupertypeHierarchy(
+ new NullProgressMonitor());
+
+ // if the super class is not the reference class, it may inherit from
+ // it so we get its supertype. At some point it will be null and we
+ // will return false;
+ IType superType = type;
+ while ((superType = hierarchy.getSuperclass(superType)) != null) {
+ if (mReferenceClass.equals(superType.getFullyQualifiedName())) {
+ return true;
+ }
+ }
+ }
+ } catch (JavaModelException e) {
+ }
+
+ return false;
+ }
+ };
+ }
+ }
+
+ /**
+ * Classes which implement this interface provide a method processing newly created classes.
+ */
+ public static interface IPostTypeCreationAction {
+ /**
+ * Sent to process a newly created class.
+ * @param newType the IType representing the newly created class.
+ */
+ public void processNewType(IType newType);
+ }
+
+ /**
+ * Creates a {@link UiClassAttributeNode} object that will display ui to select or create
+ * classes.
+ * @param referenceClass The allowed supertype of the classes that are to be selected
+ * or created. Can be null.
+ * @param postCreationAction a {@link IPostTypeCreationAction} object handling post creation
+ * modification of the class.
+ * @param mandatory indicates if the class value is mandatory
+ * @param attributeDescriptor the {@link AttributeDescriptor} object linked to the Ui Node.
+ */
+ public UiClassAttributeNode(String referenceClass, IPostTypeCreationAction postCreationAction,
+ boolean mandatory, AttributeDescriptor attributeDescriptor, UiElementNode uiParent) {
+ super(attributeDescriptor, uiParent);
+
+ mReferenceClass = referenceClass;
+ mPostCreationAction = postCreationAction;
+ mMandatory = mandatory;
+ }
+
+ /* (non-java doc)
+ * Creates a label widget and an associated text field.
+ * <p/>
+ * As most other parts of the android manifest editor, this assumes the
+ * parent uses a table layout with 2 columns.
+ */
+ @Override
+ public void createUiControl(final Composite parent, IManagedForm managedForm) {
+ setManagedForm(managedForm);
+ FormToolkit toolkit = managedForm.getToolkit();
+ TextAttributeDescriptor desc = (TextAttributeDescriptor) getDescriptor();
+
+ StringBuilder label = new StringBuilder();
+ label.append("<form><p><a href='unused'>");
+ label.append(desc.getUiName());
+ label.append("</a></p></form>");
+ FormText formText = SectionHelper.createFormText(parent, toolkit, true /* isHtml */,
+ label.toString(), true /* setupLayoutData */);
+ formText.addHyperlinkListener(new HyperlinkAdapter() {
+ @Override
+ public void linkActivated(HyperlinkEvent e) {
+ super.linkActivated(e);
+ handleLabelClick();
+ }
+ });
+ formText.setLayoutData(new TableWrapData(TableWrapData.LEFT, TableWrapData.MIDDLE));
+ SectionHelper.addControlTooltip(formText, desc.getTooltip());
+
+ Composite composite = toolkit.createComposite(parent);
+ composite.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.MIDDLE));
+ GridLayout gl = new GridLayout(2, false);
+ gl.marginHeight = gl.marginWidth = 0;
+ composite.setLayout(gl);
+ // Fixes missing text borders under GTK... also requires adding a 1-pixel margin
+ // for the text field below
+ toolkit.paintBordersFor(composite);
+
+ final Text text = toolkit.createText(composite, getCurrentValue());
+ GridData gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalIndent = 1; // Needed by the fixed composite borders under GTK
+ text.setLayoutData(gd);
+ Button browseButton = toolkit.createButton(composite, "Browse...", SWT.PUSH);
+
+ setTextWidget(text);
+
+ browseButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ super.widgetSelected(e);
+ handleBrowseClick();
+ }
+ });
+ }
+
+ /* (non-java doc)
+ *
+ * Add a modify listener that will check the validity of the class
+ */
+ @Override
+ protected void onAddValidators(final Text text) {
+ ModifyListener listener = new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ try {
+ String textValue = text.getText().trim();
+ if (textValue.length() == 0) {
+ if (mMandatory) {
+ setErrorMessage("Value is mandatory", text);
+ } else {
+ setErrorMessage(null, text);
+ }
+ return;
+ }
+ // first we need the current java package.
+ String javaPackage = getManifestPackage();
+
+ // build the fully qualified name of the class
+ String className = AndroidManifestHelper.combinePackageAndClassName(javaPackage,
+ textValue);
+
+ // only test the vilibility for activities.
+ boolean testVisibility = AndroidConstants.CLASS_ACTIVITY.equals(
+ mReferenceClass);
+
+ // test the class
+ setErrorMessage(BaseProjectHelper.testClassForManifest(
+ BaseProjectHelper.getJavaProject(getProject()), className,
+ mReferenceClass, testVisibility), text);
+ } catch (CoreException ce) {
+ setErrorMessage(ce.getMessage(), text);
+ }
+ }
+ };
+
+ text.addModifyListener(listener);
+
+ // Make sure the validator removes its message(s) when the widget is disposed
+ text.addDisposeListener(new DisposeListener() {
+ public void widgetDisposed(DisposeEvent e) {
+ // we don't want to use setErrorMessage, because we don't want to reset
+ // the error flag in the UiAttributeNode
+ getManagedForm().getMessageManager().removeMessage(text, text);
+ }
+ });
+
+ // Finally call the validator once to make sure the initial value is processed
+ listener.modifyText(null);
+ }
+
+ private void handleBrowseClick() {
+ Text text = getTextWidget();
+
+ // we need to get the project of the manifest.
+ IProject project = getProject();
+ if (project != null) {
+
+ // Create a search scope including only the source folder of the current
+ // project.
+ IJavaSearchScope scope = SearchEngine.createJavaSearchScope(
+ getPackageFragmentRoots(project), false);
+
+ try {
+ SelectionDialog dlg = JavaUI.createTypeDialog(text.getShell(),
+ PlatformUI.getWorkbench().getProgressService(),
+ scope,
+ IJavaElementSearchConstants.CONSIDER_CLASSES, // style
+ false, // no multiple selection
+ "**", //$NON-NLS-1$ //filter
+ new HierarchyTypeSelection(project, mReferenceClass));
+ dlg.setMessage(String.format("Select class name for element %1$s:",
+ getUiParent().getBreadcrumbTrailDescription(false /* include_root */)));
+ if (dlg instanceof ITypeSelectionComponent) {
+ ((ITypeSelectionComponent)dlg).triggerSearch();
+ }
+
+ if (dlg.open() == Window.OK) {
+ Object[] results = dlg.getResult();
+ if (results.length == 1) {
+ handleNewType((IType)results[0]);
+ }
+ }
+ } catch (JavaModelException e1) {
+ }
+ }
+ }
+
+ private void handleLabelClick() {
+ // get the current value
+ String className = getTextWidget().getText().trim();
+
+ // get the package name from the manifest.
+ String packageName = getManifestPackage();
+
+ if (className.length() == 0) {
+ createNewClass(packageName, null /* className */);
+ } else {
+ // build back the fully qualified class name.
+ String fullClassName = className;
+ if (className.startsWith(".")) { //$NON-NLS-1$
+ fullClassName = packageName + className;
+ } else {
+ String[] segments = className.split(AndroidConstants.RE_DOT);
+ if (segments.length == 1) {
+ fullClassName = packageName + "." + className; //$NON-NLS-1$
+ }
+ }
+
+ // in case the type is enclosed, we need to replace the $ with .
+ fullClassName = fullClassName.replaceAll("\\$", "\\."); //$NON-NLS-1$ //$NON-NLS2$
+
+ // now we try to find the file that contains this class and we open it in the editor.
+ IProject project = getProject();
+ IJavaProject javaProject = JavaCore.create(project);
+
+ try {
+ IType result = javaProject.findType(fullClassName);
+ if (result != null) {
+ JavaUI.openInEditor(result);
+ } else {
+ // split the last segment from the fullClassname
+ int index = fullClassName.lastIndexOf('.');
+ if (index != -1) {
+ createNewClass(fullClassName.substring(0, index),
+ fullClassName.substring(index+1));
+ } else {
+ createNewClass(packageName, className);
+ }
+ }
+ } catch (JavaModelException e) {
+ } catch (PartInitException e) {
+ }
+ }
+ }
+
+ private IProject getProject() {
+ UiElementNode uiNode = getUiParent();
+ AndroidEditor editor = uiNode.getEditor();
+ IEditorInput input = editor.getEditorInput();
+ if (input instanceof IFileEditorInput) {
+ // from the file editor we can get the IFile object, and from it, the IProject.
+ IFile file = ((IFileEditorInput)input).getFile();
+ return file.getProject();
+ }
+
+ return null;
+ }
+
+
+ /**
+ * Returns the current value of the /manifest/package attribute.
+ * @return the package or an empty string if not found
+ */
+ private String getManifestPackage() {
+ // get the root uiNode to get the 'package' attribute value.
+ UiElementNode rootNode = getUiParent().getUiRoot();
+
+ Element xmlElement = (Element) rootNode.getXmlNode();
+
+ if (xmlElement != null) {
+ return xmlElement.getAttribute(AndroidManifestDescriptors.PACKAGE_ATTR);
+ }
+ return ""; //$NON-NLS-1$
+ }
+
+
+ /**
+ * Computes and return the {@link IPackageFragmentRoot}s corresponding to the source folders of
+ * the specified project.
+ * @param project the project
+ * @return an array of IPackageFragmentRoot.
+ */
+ private IPackageFragmentRoot[] getPackageFragmentRoots(IProject project) {
+ ArrayList<IPackageFragmentRoot> result = new ArrayList<IPackageFragmentRoot>();
+ try {
+ IJavaProject javaProject = JavaCore.create(project);
+ IPackageFragmentRoot[] roots = javaProject.getPackageFragmentRoots();
+ for (int i = 0; i < roots.length; i++) {
+ IClasspathEntry entry = roots[i].getRawClasspathEntry();
+ if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) {
+ result.add(roots[i]);
+ }
+ }
+ } catch (JavaModelException e) {
+ }
+
+ return result.toArray(new IPackageFragmentRoot[result.size()]);
+ }
+
+ private void handleNewType(IType type) {
+ Text text = getTextWidget();
+
+ // get the fully qualified name with $ to properly detect the enclosing types.
+ String name = type.getFullyQualifiedName('$');
+
+ String packageValue = getManifestPackage();
+
+ // check if the class doesn't start with the package.
+ if (packageValue.length() > 0 && name.startsWith(packageValue)) {
+ // if it does, we remove the package and the first dot.
+ name = name.substring(packageValue.length() + 1);
+
+ // look for how many segments we have left.
+ // if one, just write it that way.
+ // if more than one, write it with a leading dot.
+ String[] packages = name.split(AndroidConstants.RE_DOT);
+ if (packages.length == 1) {
+ text.setText(name);
+ } else {
+ text.setText("." + name); //$NON-NLS-1$
+ }
+ } else {
+ text.setText(name);
+ }
+ }
+
+ private void createNewClass(String packageName, String className) {
+ // create the wizard page for the class creation, and configure it
+ NewClassWizardPage page = new NewClassWizardPage();
+
+ // set the parent class
+ page.setSuperClass(mReferenceClass, true /* canBeModified */);
+
+ // get the source folders as java elements.
+ IPackageFragmentRoot[] roots = getPackageFragmentRoots(getProject());
+
+ IPackageFragmentRoot currentRoot = null;
+ IPackageFragment currentFragment = null;
+ int packageMatchCount = -1;
+
+ for (IPackageFragmentRoot root : roots) {
+ // Get the java element for the package.
+ // This method is said to always return a IPackageFragment even if the
+ // underlying folder doesn't exist...
+ IPackageFragment fragment = root.getPackageFragment(packageName);
+ if (fragment != null && fragment.exists()) {
+ // we have a perfect match! we use it.
+ currentRoot = root;
+ currentFragment = fragment;
+ packageMatchCount = -1;
+ break;
+ } else {
+ // we don't have a match. we look for the fragment with the best match
+ // (ie the closest parent package we can find)
+ try {
+ IJavaElement[] children;
+ children = root.getChildren();
+ for (IJavaElement child : children) {
+ if (child instanceof IPackageFragment) {
+ fragment = (IPackageFragment)child;
+ if (packageName.startsWith(fragment.getElementName())) {
+ // its a match. get the number of segments
+ String[] segments = fragment.getElementName().split("\\."); //$NON-NLS-1$
+ if (segments.length > packageMatchCount) {
+ packageMatchCount = segments.length;
+ currentFragment = fragment;
+ currentRoot = root;
+ }
+ }
+ }
+ }
+ } catch (JavaModelException e) {
+ // Couldn't get the children: we just ignore this package root.
+ }
+ }
+ }
+
+ ArrayList<IPackageFragment> createdFragments = null;
+
+ if (currentRoot != null) {
+ // if we have a perfect match, we set it and we're done.
+ if (packageMatchCount == -1) {
+ page.setPackageFragmentRoot(currentRoot, true /* canBeModified*/);
+ page.setPackageFragment(currentFragment, true /* canBeModified */);
+ } else {
+ // we have a partial match.
+ // create the package. We have to start with the first segment so that we
+ // know what to delete in case of a cancel.
+ try {
+ createdFragments = new ArrayList<IPackageFragment>();
+
+ int totalCount = packageName.split("\\.").length; //$NON-NLS-1$
+ int count = 0;
+ int index = -1;
+ // skip the matching packages
+ while (count < packageMatchCount) {
+ index = packageName.indexOf('.', index+1);
+ count++;
+ }
+
+ // create the rest of the segments, except for the last one as indexOf will
+ // return -1;
+ while (count < totalCount - 1) {
+ index = packageName.indexOf('.', index+1);
+ count++;
+ createdFragments.add(currentRoot.createPackageFragment(
+ packageName.substring(0, index),
+ true /* force*/, new NullProgressMonitor()));
+ }
+
+ // create the last package
+ createdFragments.add(currentRoot.createPackageFragment(
+ packageName, true /* force*/, new NullProgressMonitor()));
+
+ // set the root and fragment in the Wizard page
+ page.setPackageFragmentRoot(currentRoot, true /* canBeModified*/);
+ page.setPackageFragment(createdFragments.get(createdFragments.size()-1),
+ true /* canBeModified */);
+ } catch (JavaModelException e) {
+ // if we can't create the packages, there's a problem. we revert to the default
+ // package
+ for (IPackageFragmentRoot root : roots) {
+ // Get the java element for the package.
+ // This method is said to always return a IPackageFragment even if the
+ // underlying folder doesn't exist...
+ IPackageFragment fragment = root.getPackageFragment(packageName);
+ if (fragment != null && fragment.exists()) {
+ page.setPackageFragmentRoot(root, true /* canBeModified*/);
+ page.setPackageFragment(fragment, true /* canBeModified */);
+ break;
+ }
+ }
+ }
+ }
+ } else if (roots.length > 0) {
+ // if we haven't found a valid fragment, we set the root to the first source folder.
+ page.setPackageFragmentRoot(roots[0], true /* canBeModified*/);
+ }
+
+ // if we have a starting class name we use it
+ if (className != null) {
+ page.setTypeName(className, true /* canBeModified*/);
+ }
+
+ // create the action that will open it the wizard.
+ OpenNewClassWizardAction action = new OpenNewClassWizardAction();
+ action.setConfiguredWizardPage(page);
+ action.run();
+ IJavaElement element = action.getCreatedElement();
+
+ if (element != null) {
+ if (element.getElementType() == IJavaElement.TYPE) {
+
+ IType type = (IType)element;
+
+ if (mPostCreationAction != null) {
+ mPostCreationAction.processNewType(type);
+ }
+
+ handleNewType(type);
+ }
+ } else {
+ // lets delete the packages we created just for this.
+ // we need to start with the leaf and go up
+ if (createdFragments != null) {
+ try {
+ for (int i = createdFragments.size() - 1 ; i >= 0 ; i--) {
+ createdFragments.get(i).delete(true /* force*/, new NullProgressMonitor());
+ }
+ } catch (JavaModelException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ /**
+ * Sets the error messages. If message is <code>null</code>, the message is removed.
+ * @param message the message to set, or <code>null</code> to remove the current message
+ * @param textWidget the {@link Text} widget associated to the message.
+ */
+ private final void setErrorMessage(String message, Text textWidget) {
+ if (message != null) {
+ setHasError(true);
+ getManagedForm().getMessageManager().addMessage(textWidget, message, null /* data */,
+ IMessageProvider.ERROR, textWidget);
+ } else {
+ setHasError(false);
+ getManagedForm().getMessageManager().removeMessage(textWidget, textWidget);
+ }
+ }
+
+ @Override
+ public String[] getPossibleValues() {
+ // TODO: compute a list of existing classes for content assist completion
+ return null;
+ }
+}
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/model/UiManifestElementNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/model/UiManifestElementNode.java
new file mode 100644
index 0000000..fb8f211
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/model/UiManifestElementNode.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.manifest.model;
+
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.editors.manifest.descriptors.AndroidManifestDescriptors;
+import com.android.ide.eclipse.editors.manifest.descriptors.ManifestElementDescriptor;
+import com.android.ide.eclipse.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+import com.android.sdklib.SdkConstants;
+
+import org.w3c.dom.Element;
+
+/**
+ * Represents an XML node that can be modified by the user interface in the XML editor.
+ * <p/>
+ * Each tree viewer used in the application page's parts needs to keep a model representing
+ * each underlying node in the tree. This interface represents the base type for such a node.
+ * <p/>
+ * Each node acts as an intermediary model between the actual XML model (the real data support)
+ * and the tree viewers or the corresponding page parts.
+ * <p/>
+ * Element nodes don't contain data per se. Their data is contained in their attributes
+ * as well as their children's attributes, see {@link UiAttributeNode}.
+ * <p/>
+ * The structure of a given {@link UiElementNode} is declared by a corresponding
+ * {@link ElementDescriptor}.
+ */
+public final class UiManifestElementNode extends UiElementNode {
+
+ /**
+ * Creates a new {@link UiElementNode} described by a given {@link ElementDescriptor}.
+ *
+ * @param elementDescriptor The {@link ElementDescriptor} for the XML node. Cannot be null.
+ */
+ public UiManifestElementNode(ManifestElementDescriptor elementDescriptor) {
+ super(elementDescriptor);
+ }
+
+ /**
+ * Computes a short string describing the UI node suitable for tree views.
+ * Uses the element's attribute "android:name" if present, or the "android:label" one
+ * followed by the element's name.
+ *
+ * @return A short string describing the UI node suitable for tree views.
+ */
+ @Override
+ public String getShortDescription() {
+ if (getXmlNode() != null &&
+ getXmlNode() instanceof Element &&
+ getXmlNode().hasAttributes()) {
+
+ AndroidManifestDescriptors manifestDescriptors =
+ getAndroidTarget().getManifestDescriptors();
+
+ // Application and Manifest nodes have a special treatment: they are unique nodes
+ // so we don't bother trying to differentiate their strings and we fall back to
+ // just using the UI name below.
+ ElementDescriptor desc = getDescriptor();
+ if (desc != manifestDescriptors.getManifestElement() &&
+ desc != manifestDescriptors.getApplicationElement()) {
+ Element elem = (Element) getXmlNode();
+ String attr = elem.getAttributeNS(SdkConstants.NS_RESOURCES,
+ AndroidManifestDescriptors.ANDROID_NAME_ATTR);
+ if (attr == null || attr.length() == 0) {
+ attr = elem.getAttributeNS(SdkConstants.NS_RESOURCES,
+ AndroidManifestDescriptors.ANDROID_LABEL_ATTR);
+ }
+ if (attr != null && attr.length() > 0) {
+ return String.format("%1$s (%2$s)", attr, getDescriptor().getUiName());
+ }
+ }
+ }
+
+ return String.format("%1$s", getDescriptor().getUiName());
+ }
+}
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/model/UiPackageAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/model/UiPackageAttributeNode.java
new file mode 100644
index 0000000..02fb44f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/model/UiPackageAttributeNode.java
@@ -0,0 +1,319 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.manifest.model;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.editors.AndroidEditor;
+import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.editors.ui.SectionHelper;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.editors.uimodel.UiTextAttributeNode;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.jdt.core.IClasspathEntry;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IPackageFragment;
+import org.eclipse.jdt.core.IPackageFragmentRoot;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.ui.JavaUI;
+import org.eclipse.jdt.ui.actions.OpenNewPackageWizardAction;
+import org.eclipse.jdt.ui.actions.ShowInPackageViewAction;
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.jface.window.Window;
+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.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.Text;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IFileEditorInput;
+import org.eclipse.ui.IWorkbenchPartSite;
+import org.eclipse.ui.dialogs.SelectionDialog;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.events.HyperlinkAdapter;
+import org.eclipse.ui.forms.events.HyperlinkEvent;
+import org.eclipse.ui.forms.widgets.FormText;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.TableWrapData;
+
+import java.util.ArrayList;
+
+/**
+ * Represents an XML attribute for a package, that can be modified using a simple text field or
+ * a dialog to choose an existing package. Also, there's a link to create a new package.
+ * <p/>
+ * See {@link UiTextAttributeNode} for more information.
+ */
+public class UiPackageAttributeNode extends UiTextAttributeNode {
+
+ /**
+ * Creates a {@link UiPackageAttributeNode} object that will display ui to select or create
+ * a package.
+ * @param attributeDescriptor the {@link AttributeDescriptor} object linked to the Ui Node.
+ */
+ public UiPackageAttributeNode(AttributeDescriptor attributeDescriptor, UiElementNode uiParent) {
+ super(attributeDescriptor, uiParent);
+ }
+
+ /* (non-java doc)
+ * Creates a label widget and an associated text field.
+ * <p/>
+ * As most other parts of the android manifest editor, this assumes the
+ * parent uses a table layout with 2 columns.
+ */
+ @Override
+ public void createUiControl(final Composite parent, final IManagedForm managedForm) {
+ setManagedForm(managedForm);
+ FormToolkit toolkit = managedForm.getToolkit();
+ TextAttributeDescriptor desc = (TextAttributeDescriptor) getDescriptor();
+
+ StringBuilder label = new StringBuilder();
+ label.append("<form><p><a href='unused'>"); //$NON-NLS-1$
+ label.append(desc.getUiName());
+ label.append("</a></p></form>"); //$NON-NLS-1$
+ FormText formText = SectionHelper.createFormText(parent, toolkit, true /* isHtml */,
+ label.toString(), true /* setupLayoutData */);
+ formText.addHyperlinkListener(new HyperlinkAdapter() {
+ @Override
+ public void linkActivated(HyperlinkEvent e) {
+ super.linkActivated(e);
+ doLabelClick();
+ }
+ });
+ formText.setLayoutData(new TableWrapData(TableWrapData.LEFT, TableWrapData.MIDDLE));
+ SectionHelper.addControlTooltip(formText, desc.getTooltip());
+
+ Composite composite = toolkit.createComposite(parent);
+ composite.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.MIDDLE));
+ GridLayout gl = new GridLayout(2, false);
+ gl.marginHeight = gl.marginWidth = 0;
+ composite.setLayout(gl);
+ // Fixes missing text borders under GTK... also requires adding a 1-pixel margin
+ // for the text field below
+ toolkit.paintBordersFor(composite);
+
+ final Text text = toolkit.createText(composite, getCurrentValue());
+ GridData gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalIndent = 1; // Needed by the fixed composite borders under GTK
+ text.setLayoutData(gd);
+
+ setTextWidget(text);
+
+ Button browseButton = toolkit.createButton(composite, "Browse...", SWT.PUSH);
+
+ browseButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ super.widgetSelected(e);
+ doBrowseClick();
+ }
+ });
+
+ }
+
+ /* (non-java doc)
+ * Adds a validator to the text field that calls managedForm.getMessageManager().
+ */
+ @Override
+ protected void onAddValidators(final Text text) {
+ ModifyListener listener = new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ String package_name = text.getText();
+ if (package_name.indexOf('.') < 1) {
+ getManagedForm().getMessageManager().addMessage(text,
+ "Package name should contain at least two identifiers.",
+ null /* data */, IMessageProvider.ERROR, text);
+ } else {
+ getManagedForm().getMessageManager().removeMessage(text, text);
+ }
+ }
+ };
+
+ text.addModifyListener(listener);
+
+ // Make sure the validator removes its message(s) when the widget is disposed
+ text.addDisposeListener(new DisposeListener() {
+ public void widgetDisposed(DisposeEvent e) {
+ getManagedForm().getMessageManager().removeMessage(text, text);
+ }
+ });
+
+ // Finally call the validator once to make sure the initial value is processed
+ listener.modifyText(null);
+ }
+
+ /**
+ * Handles response to the Browse button by creating a Package dialog.
+ * */
+ private void doBrowseClick() {
+ Text text = getTextWidget();
+
+ // we need to get the project of the manifest.
+ IProject project = getProject();
+ if (project != null) {
+
+ try {
+ SelectionDialog dlg = JavaUI.createPackageDialog(text.getShell(),
+ JavaCore.create(project), 0);
+ dlg.setTitle("Select Android Package");
+ dlg.setMessage("Select the package for the Android project.");
+ SelectionDialog.setDefaultImage(AdtPlugin.getAndroidLogo());
+
+ if (dlg.open() == Window.OK) {
+ Object[] results = dlg.getResult();
+ if (results.length == 1) {
+ setPackageTextField((IPackageFragment)results[0]);
+ }
+ }
+ } catch (JavaModelException e1) {
+ }
+ }
+ }
+
+ /**
+ * Handles response to the Label hyper link being activated.
+ */
+ private void doLabelClick() {
+ // get the current package name
+ String package_name = getTextWidget().getText().trim();
+
+ if (package_name.length() == 0) {
+ createNewPackage();
+ } else {
+ // Try to select the package in the Package Explorer for the current
+ // project and the current editor's site.
+
+ IProject project = getProject();
+ if (project == null) {
+ AdtPlugin.log(IStatus.ERROR, "Failed to get project for UiPackageAttribute"); //$NON-NLS-1$
+ return;
+ }
+
+ IWorkbenchPartSite site = getUiParent().getEditor().getSite();
+ if (site == null) {
+ AdtPlugin.log(IStatus.ERROR, "Failed to get editor site for UiPackageAttribute"); //$NON-NLS-1$
+ return;
+ }
+
+ for (IPackageFragmentRoot root : getPackageFragmentRoots(project)) {
+ IPackageFragment fragment = root.getPackageFragment(package_name);
+ if (fragment != null && fragment.exists()) {
+ ShowInPackageViewAction action = new ShowInPackageViewAction(site);
+ action.run(fragment);
+ // This action's run() doesn't provide the status (although internally it could)
+ // so we just assume it worked.
+ return;
+ }
+ }
+ }
+ }
+
+ /**
+ * Utility method that returns the project for the current file being edited.
+ *
+ * @return The IProject for the current file being edited or null.
+ */
+ private IProject getProject() {
+ UiElementNode uiNode = getUiParent();
+ AndroidEditor editor = uiNode.getEditor();
+ IEditorInput input = editor.getEditorInput();
+ if (input instanceof IFileEditorInput) {
+ // from the file editor we can get the IFile object, and from it, the IProject.
+ IFile file = ((IFileEditorInput)input).getFile();
+ return file.getProject();
+ }
+
+ return null;
+ }
+
+ /**
+ * Utility method that computes and returns the list of {@link IPackageFragmentRoot}
+ * corresponding to the source folder of the specified project.
+ *
+ * @param project the project
+ * @return an array of IPackageFragmentRoot. Can be empty but not null.
+ */
+ private IPackageFragmentRoot[] getPackageFragmentRoots(IProject project) {
+ ArrayList<IPackageFragmentRoot> result = new ArrayList<IPackageFragmentRoot>();
+ try {
+ IJavaProject javaProject = JavaCore.create(project);
+ IPackageFragmentRoot[] roots = javaProject.getPackageFragmentRoots();
+ for (int i = 0; i < roots.length; i++) {
+ IClasspathEntry entry = roots[i].getRawClasspathEntry();
+ if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) {
+ result.add(roots[i]);
+ }
+ }
+ } catch (JavaModelException e) {
+ }
+
+ return result.toArray(new IPackageFragmentRoot[result.size()]);
+ }
+
+ /**
+ * Utility method that sets the package's text field to the package fragment's name.
+ * */
+ private void setPackageTextField(IPackageFragment type) {
+ Text text = getTextWidget();
+
+ String name = type.getElementName();
+
+ text.setText(name);
+ }
+
+
+ /**
+ * Displays and handles a "Create Package Wizard".
+ *
+ * This is invoked by doLabelClick() when clicking on the hyperlink label with an
+ * empty package text field.
+ */
+ private void createNewPackage() {
+ OpenNewPackageWizardAction action = new OpenNewPackageWizardAction();
+
+ IProject project = getProject();
+ action.setSelection(new StructuredSelection(project));
+ action.run();
+
+ IJavaElement element = action.getCreatedElement();
+ if (element != null &&
+ element.exists() &&
+ element.getElementType() == IJavaElement.PACKAGE_FRAGMENT) {
+ setPackageTextField((IPackageFragment) element);
+ }
+ }
+
+ @Override
+ public String[] getPossibleValues() {
+ // TODO: compute a list of existing packages for content assist completion
+ return null;
+ }
+}
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/ApplicationAttributesPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/ApplicationAttributesPart.java
new file mode 100644
index 0000000..01b0f8f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/ApplicationAttributesPart.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.manifest.pages;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.XmlnsAttributeDescriptor;
+import com.android.ide.eclipse.editors.manifest.ManifestEditor;
+import com.android.ide.eclipse.editors.ui.UiElementPart;
+import com.android.ide.eclipse.editors.uimodel.IUiUpdateListener;
+import com.android.ide.eclipse.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.Section;
+
+/**
+ * Application's attributes section part for Application page.
+ * <p/>
+ * This part is displayed at the top of the application page and displays all the possible
+ * attributes of an application node in the AndroidManifest (icon, class name, label, etc.)
+ */
+final class ApplicationAttributesPart extends UiElementPart {
+
+ /** Listen to changes to the UI node for <application> and updates the UI */
+ private AppNodeUpdateListener mAppNodeUpdateListener;
+ /** ManagedForm needed to create the UI controls */
+ private IManagedForm mManagedForm;
+
+ public ApplicationAttributesPart(Composite body, FormToolkit toolkit, ManifestEditor editor,
+ UiElementNode applicationUiNode) {
+ super(body, toolkit, editor, applicationUiNode,
+ "Application Attributes", // section title
+ "Defines the attributes specific to the application.", // section description
+ Section.TWISTIE | Section.EXPANDED);
+ }
+
+ /**
+ * Changes and refreshes the Application UI node handle by the this part.
+ */
+ @Override
+ public void setUiElementNode(UiElementNode uiElementNode) {
+ super.setUiElementNode(uiElementNode);
+
+ createUiAttributes(mManagedForm);
+ }
+
+ /* (non-java doc)
+ * Create the controls to edit the attributes for the given ElementDescriptor.
+ * <p/>
+ * This MUST not be called by the constructor. Instead it must be called from
+ * <code>initialize</code> (i.e. right after the form part is added to the managed form.)
+ * <p/>
+ * Derived classes can override this if necessary.
+ *
+ * @param managedForm The owner managed form
+ */
+ @Override
+ protected void createFormControls(final IManagedForm managedForm) {
+ mManagedForm = managedForm;
+ setTable(createTableLayout(managedForm.getToolkit(), 4 /* numColumns */));
+
+ mAppNodeUpdateListener = new AppNodeUpdateListener();
+ getUiElementNode().addUpdateListener(mAppNodeUpdateListener);
+
+ createUiAttributes(mManagedForm);
+ }
+
+ @Override
+ public void dispose() {
+ super.dispose();
+ if (getUiElementNode() != null && mAppNodeUpdateListener != null) {
+ getUiElementNode().removeUpdateListener(mAppNodeUpdateListener);
+ mAppNodeUpdateListener = null;
+ }
+ }
+
+ @Override
+ protected void createUiAttributes(IManagedForm managedForm) {
+ Composite table = getTable();
+ if (table == null || managedForm == null) {
+ return;
+ }
+
+ // Remove any old UI controls
+ for (Control c : table.getChildren()) {
+ c.dispose();
+ }
+
+ UiElementNode uiElementNode = getUiElementNode();
+ AttributeDescriptor[] attr_desc_list = uiElementNode.getAttributeDescriptors();
+
+ // Display the attributes in 2 columns:
+ // attr 0 | attr 4
+ // attr 1 | attr 5
+ // attr 2 | attr 6
+ // attr 3 | attr 7
+ // that is we have to fill the grid in order 0, 4, 1, 5, 2, 6, 3, 7
+ // thus index = i/2 + (i is odd * n/2)
+ int n = attr_desc_list.length;
+ int n2 = (int) Math.ceil(n / 2.0);
+ for (int i = 0; i < n; i++) {
+ AttributeDescriptor attr_desc = attr_desc_list[i / 2 + (i & 1) * n2];
+ if (attr_desc instanceof XmlnsAttributeDescriptor) {
+ // Do not show hidden attributes
+ continue;
+ }
+
+ UiAttributeNode ui_attr = uiElementNode.findUiAttribute(attr_desc);
+ if (ui_attr != null) {
+ ui_attr.createUiControl(table, managedForm);
+ } else {
+ // The XML has an extra attribute which wasn't declared in
+ // AndroidManifestDescriptors. This is not a problem, we just ignore it.
+ AdtPlugin.log(IStatus.WARNING,
+ "Attribute %1$s not declared in node %2$s, ignored.", //$NON-NLS-1$
+ attr_desc.getXmlLocalName(),
+ uiElementNode.getDescriptor().getXmlName());
+ }
+ }
+
+ if (n == 0) {
+ createLabel(table, managedForm.getToolkit(),
+ "No attributes to display, waiting for SDK to finish loading...",
+ null /* tooltip */ );
+ }
+
+ // Initialize the enabled/disabled state
+ if (mAppNodeUpdateListener != null) {
+ mAppNodeUpdateListener.uiElementNodeUpdated(uiElementNode, null /* state, not used */);
+ }
+
+ // Tell the section that the layout has changed.
+ layoutChanged();
+ }
+
+ /**
+ * This listener synchronizes the UI with the actual presence of the application XML node.
+ */
+ private class AppNodeUpdateListener implements IUiUpdateListener {
+ public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state) {
+ // The UiElementNode for the application XML node always exists, even
+ // if there is no corresponding XML node in the XML file.
+ //
+ // We enable the UI here if the XML node is not null.
+ Composite table = getTable();
+ boolean exists = (ui_node.getXmlNode() != null);
+ if (table != null && table.getEnabled() != exists) {
+ table.setEnabled(exists);
+ for (Control c : table.getChildren()) {
+ c.setEnabled(exists);
+ }
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/ApplicationPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/ApplicationPage.java
new file mode 100644
index 0000000..77527f0
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/ApplicationPage.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.manifest.pages;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.editors.manifest.ManifestEditor;
+import com.android.ide.eclipse.editors.manifest.descriptors.AndroidManifestDescriptors;
+import com.android.ide.eclipse.editors.ui.tree.UiTreeBlock;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.editor.FormPage;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.ScrolledForm;
+
+
+/**
+ * Page for "Application" settings, part of the AndroidManifest form editor.
+ * <p/>
+ * Useful reference:
+ * <a href="http://www.eclipse.org/articles/Article-Forms/article.html">
+ * http://www.eclipse.org/articles/Article-Forms/article.html</a>
+ */
+public final class ApplicationPage extends FormPage {
+ /** Page id used for switching tabs programmatically */
+ public final static String PAGE_ID = "application_page"; //$NON-NLS-1$
+
+ /** Container editor */
+ ManifestEditor mEditor;
+ /** The Application Toogle part */
+ private ApplicationToggle mTooglePart;
+ /** The Application Attributes part */
+ private ApplicationAttributesPart mAttrPart;
+ /** The tree view block */
+ private UiTreeBlock mTreeBlock;
+
+ public ApplicationPage(ManifestEditor editor) {
+ super(editor, PAGE_ID, "Application"); // tab's label, keep it short
+ mEditor = editor;
+ }
+
+ /**
+ * Creates the content in the form hosted in this page.
+ *
+ * @param managedForm the form hosted in this page.
+ */
+ @Override
+ protected void createFormContent(IManagedForm managedForm) {
+ super.createFormContent(managedForm);
+ ScrolledForm form = managedForm.getForm();
+ form.setText("Android Manifest Application");
+ form.setImage(AdtPlugin.getAndroidLogo());
+
+ UiElementNode appUiNode = getUiApplicationNode();
+
+ Composite body = form.getBody();
+ FormToolkit toolkit = managedForm.getToolkit();
+
+ // We usually prefer to have a ColumnLayout here. However
+ // MasterDetailsBlock.createContent() below will reset the body's layout to a grid layout.
+ mTooglePart = new ApplicationToggle(body, toolkit, mEditor, appUiNode);
+ mTooglePart.getSection().setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false));
+ managedForm.addPart(mTooglePart);
+ mAttrPart = new ApplicationAttributesPart(body, toolkit, mEditor, appUiNode);
+ mAttrPart.getSection().setLayoutData(new GridData(SWT.FILL, SWT.TOP, true, false));
+ managedForm.addPart(mAttrPart);
+
+ mTreeBlock = new UiTreeBlock(mEditor, appUiNode,
+ false /* autoCreateRoot */,
+ null /* element filters */,
+ "Application Nodes",
+ "List of all elements in the application");
+ mTreeBlock.createContent(managedForm);
+ }
+
+ /**
+ * Retrieves the application UI node. Since this is a mandatory node, it *always*
+ * exists, even if there is no matching XML node.
+ */
+ private UiElementNode getUiApplicationNode() {
+ AndroidManifestDescriptors manifestDescriptor = mEditor.getManifestDescriptors();
+ if (manifestDescriptor != null) {
+ ElementDescriptor desc = manifestDescriptor.getApplicationElement();
+ return mEditor.getUiRootNode().findUiChildNode(desc.getXmlName());
+ } else {
+ // return the ui root node, as a dummy application root node.
+ return mEditor.getUiRootNode();
+ }
+ }
+
+ /**
+ * Changes and refreshes the Application UI node handled by the sub parts.
+ */
+ public void refreshUiApplicationNode() {
+ UiElementNode appUiNode = getUiApplicationNode();
+ if (mTooglePart != null) {
+ mTooglePart.setUiElementNode(appUiNode);
+ }
+ if (mAttrPart != null) {
+ mAttrPart.setUiElementNode(appUiNode);
+ }
+ if (mTreeBlock != null) {
+ mTreeBlock.changeRootAndDescriptors(appUiNode,
+ null /* element filters */,
+ true /* refresh */);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/ApplicationToggle.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/ApplicationToggle.java
new file mode 100644
index 0000000..139575d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/ApplicationToggle.java
@@ -0,0 +1,313 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.manifest.pages;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.sdk.Sdk;
+import com.android.ide.eclipse.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.editors.manifest.ManifestEditor;
+import com.android.ide.eclipse.editors.ui.UiElementPart;
+import com.android.ide.eclipse.editors.uimodel.IUiUpdateListener;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.editors.uimodel.IUiUpdateListener.UiUpdateState;
+
+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.Composite;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.widgets.FormText;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.Section;
+import org.eclipse.ui.forms.widgets.TableWrapData;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.Text;
+
+/**
+ * Appllication Toogle section part for application page.
+ */
+final class ApplicationToggle extends UiElementPart {
+
+ /** Checkbox indicating whether an application node is present */
+ private Button mCheckbox;
+ /** Listen to changes to the UI node for <application> and updates the checkbox */
+ private AppNodeUpdateListener mAppNodeUpdateListener;
+ /** Internal flag to know where we're programmatically modifying the checkbox and we want to
+ * avoid triggering the checkbox's callback. */
+ public boolean mInternalModification;
+ private FormText mTooltipFormText;
+
+ public ApplicationToggle(Composite body, FormToolkit toolkit, ManifestEditor editor,
+ UiElementNode applicationUiNode) {
+ super(body, toolkit, editor, applicationUiNode,
+ "Application Toggle",
+ null, /* description */
+ Section.TWISTIE | Section.EXPANDED);
+ }
+
+ @Override
+ public void dispose() {
+ super.dispose();
+ if (getUiElementNode() != null && mAppNodeUpdateListener != null) {
+ getUiElementNode().removeUpdateListener(mAppNodeUpdateListener);
+ mAppNodeUpdateListener = null;
+ }
+ }
+
+ /**
+ * Changes and refreshes the Application UI node handle by the this part.
+ */
+ @Override
+ public void setUiElementNode(UiElementNode uiElementNode) {
+ super.setUiElementNode(uiElementNode);
+
+ updateTooltip();
+
+ // Set the state of the checkbox
+ mAppNodeUpdateListener.uiElementNodeUpdated(getUiElementNode(),
+ UiUpdateState.CHILDREN_CHANGED);
+ }
+
+ /**
+ * Create the controls to edit the attributes for the given ElementDescriptor.
+ * <p/>
+ * This MUST not be called by the constructor. Instead it must be called from
+ * <code>initialize</code> (i.e. right after the form part is added to the managed form.)
+ *
+ * @param managedForm The owner managed form
+ */
+ @Override
+ protected void createFormControls(IManagedForm managedForm) {
+ FormToolkit toolkit = managedForm.getToolkit();
+ Composite table = createTableLayout(toolkit, 1 /* numColumns */);
+
+ mTooltipFormText = createFormText(table, toolkit, true, "<form></form>",
+ false /* setupLayoutData */);
+ updateTooltip();
+
+ mCheckbox = toolkit.createButton(table,
+ "Define an <application> tag in the AndroidManifest.xml",
+ SWT.CHECK);
+ mCheckbox.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.TOP));
+ mCheckbox.setSelection(false);
+ mCheckbox.addSelectionListener(new CheckboxSelectionListener());
+
+ mAppNodeUpdateListener = new AppNodeUpdateListener();
+ getUiElementNode().addUpdateListener(mAppNodeUpdateListener);
+
+ // Initialize the state of the checkbox
+ mAppNodeUpdateListener.uiElementNodeUpdated(getUiElementNode(),
+ UiUpdateState.CHILDREN_CHANGED);
+
+ // Tell the section that the layout has changed.
+ layoutChanged();
+ }
+
+ /**
+ * Updates the application tooltip in the form text.
+ * If there is no tooltip, the form text is hidden.
+ */
+ private void updateTooltip() {
+ boolean isVisible = false;
+
+ String tooltip = getUiElementNode().getDescriptor().getTooltip();
+ if (tooltip != null) {
+ tooltip = DescriptorsUtils.formatFormText(tooltip,
+ getUiElementNode().getDescriptor(),
+ Sdk.getCurrent().getDocumentationBaseUrl());
+
+ mTooltipFormText.setText(tooltip, true /* parseTags */, true /* expandURLs */);
+ mTooltipFormText.setImage(DescriptorsUtils.IMAGE_KEY, AdtPlugin.getAndroidLogo());
+ mTooltipFormText.addHyperlinkListener(getEditor().createHyperlinkListener());
+ isVisible = true;
+ }
+
+ mTooltipFormText.setVisible(isVisible);
+ }
+
+ /**
+ * This listener synchronizes the XML application node when the checkbox
+ * is changed by the user.
+ */
+ private class CheckboxSelectionListener extends SelectionAdapter {
+ private Node mUndoXmlNode;
+ private Node mUndoXmlParent;
+ private Node mUndoXmlNextNode;
+ private Node mUndoXmlNextElement;
+ private Document mUndoXmlDocument;
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ super.widgetSelected(e);
+ if (!mInternalModification && getUiElementNode() != null) {
+ getUiElementNode().getEditor().wrapUndoRecording(
+ mCheckbox.getSelection()
+ ? "Create or restore Application node"
+ : "Remove Application node",
+ new Runnable() {
+ public void run() {
+ getUiElementNode().getEditor().editXmlModel(new Runnable() {
+ public void run() {
+ if (mCheckbox.getSelection()) {
+ // The user wants an <application> node.
+ // Either restore a previous one
+ // or create a full new one.
+ boolean create = true;
+ if (mUndoXmlNode != null) {
+ create = !restoreApplicationNode();
+ }
+ if (create) {
+ getUiElementNode().createXmlNode();
+ }
+ } else {
+ // Users no longer wants the <application> node.
+ removeApplicationNode();
+ }
+ }
+ });
+ }
+ });
+ }
+ }
+
+ /**
+ * Restore a previously "saved" application node.
+ *
+ * @return True if the node could be restored, false otherwise.
+ */
+ private boolean restoreApplicationNode() {
+ if (mUndoXmlDocument == null || mUndoXmlNode == null) {
+ return false;
+ }
+
+ // Validate node references...
+ mUndoXmlParent = validateNode(mUndoXmlDocument, mUndoXmlParent);
+ mUndoXmlNextNode = validateNode(mUndoXmlDocument, mUndoXmlNextNode);
+ mUndoXmlNextElement = validateNode(mUndoXmlDocument, mUndoXmlNextElement);
+
+ if (mUndoXmlParent == null){
+ // If the parent node doesn't exist, try to find a new manifest node.
+ // If it doesn't exist, create it.
+ mUndoXmlParent = getUiElementNode().getUiParent().prepareCommit();
+ mUndoXmlNextNode = null;
+ mUndoXmlNextElement = null;
+ }
+
+ boolean success = false;
+ if (mUndoXmlParent != null) {
+ // If the parent is still around, reuse the same node.
+
+ // Ideally we want to insert the node before what used to be its next sibling.
+ // If that's not possible, we try to insert it before its next sibling element.
+ // If that's not possible either, it will be inserted at the end of the parent's.
+ Node next = mUndoXmlNextNode;
+ if (next == null) {
+ next = mUndoXmlNextElement;
+ }
+ mUndoXmlParent.insertBefore(mUndoXmlNode, next);
+ if (next == null) {
+ Text sep = mUndoXmlDocument.createTextNode("\n"); //$NON-NLS-1$
+ mUndoXmlParent.insertBefore(sep, null); // insert separator before end tag
+ }
+ success = true;
+ }
+
+ // Remove internal references to avoid using them twice
+ mUndoXmlParent = null;
+ mUndoXmlNextNode = null;
+ mUndoXmlNextElement = null;
+ mUndoXmlNode = null;
+ mUndoXmlDocument = null;
+ return success;
+ }
+
+ /**
+ * Validates that the given xml_node is still either the root node or one of its
+ * direct descendants.
+ *
+ * @param root_node The root of the node hierarchy to examine.
+ * @param xml_node The XML node to find.
+ * @return Returns xml_node if it is, otherwise returns null.
+ */
+ private Node validateNode(Node root_node, Node xml_node) {
+ if (root_node == xml_node) {
+ return xml_node;
+ } else {
+ for (Node node = root_node.getFirstChild(); node != null;
+ node = node.getNextSibling()) {
+ if (root_node == xml_node || validateNode(node, xml_node) != null) {
+ return xml_node;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Removes the <application> node from the hierarchy.
+ * Before doing that, we try to remember where it was so that we can put it back
+ * in the same place.
+ */
+ private void removeApplicationNode() {
+ // Make sure the node actually exists...
+ Node xml_node = getUiElementNode().getXmlNode();
+ if (xml_node == null) {
+ return;
+ }
+
+ // Save its parent, next sibling and next element
+ mUndoXmlDocument = xml_node.getOwnerDocument();
+ mUndoXmlParent = xml_node.getParentNode();
+ mUndoXmlNextNode = xml_node.getNextSibling();
+ mUndoXmlNextElement = mUndoXmlNextNode;
+ while (mUndoXmlNextElement != null &&
+ mUndoXmlNextElement.getNodeType() != Node.ELEMENT_NODE) {
+ mUndoXmlNextElement = mUndoXmlNextElement.getNextSibling();
+ }
+
+ // Actually remove the node from the hierarchy and keep it here.
+ // The returned node looses its parents/siblings pointers.
+ mUndoXmlNode = getUiElementNode().deleteXmlNode();
+ }
+ }
+
+ /**
+ * This listener synchronizes the UI (i.e. the checkbox) with the
+ * actual presence of the application XML node.
+ */
+ private class AppNodeUpdateListener implements IUiUpdateListener {
+ public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state) {
+ // The UiElementNode for the application XML node always exists, even
+ // if there is no corresponding XML node in the XML file.
+ //
+ // To update the checkbox to reflect the actual state, we just need
+ // to check if the XML node is null.
+ try {
+ mInternalModification = true;
+ boolean exists = ui_node.getXmlNode() != null;
+ if (mCheckbox.getSelection() != exists) {
+ mCheckbox.setSelection(exists);
+ }
+ } finally {
+ mInternalModification = false;
+ }
+
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/InstrumentationPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/InstrumentationPage.java
new file mode 100644
index 0000000..86d0dd1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/InstrumentationPage.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.manifest.pages;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.editors.manifest.ManifestEditor;
+import com.android.ide.eclipse.editors.manifest.descriptors.AndroidManifestDescriptors;
+import com.android.ide.eclipse.editors.ui.tree.UiTreeBlock;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.editor.FormPage;
+import org.eclipse.ui.forms.widgets.ScrolledForm;
+
+/**
+ * Page for instrumentation settings, part of the AndroidManifest form editor.
+ */
+public final class InstrumentationPage extends FormPage {
+ /** Page id used for switching tabs programmatically */
+ public final static String PAGE_ID = "instrumentation_page"; //$NON-NLS-1$
+
+ /** Container editor */
+ ManifestEditor mEditor;
+
+ private UiTreeBlock mTreeBlock;
+
+ public InstrumentationPage(ManifestEditor editor) {
+ super(editor, PAGE_ID, "Instrumentation"); // tab's label, keep it short
+ mEditor = editor;
+ }
+
+ /**
+ * Creates the content in the form hosted in this page.
+ *
+ * @param managedForm the form hosted in this page.
+ */
+ @Override
+ protected void createFormContent(IManagedForm managedForm) {
+ super.createFormContent(managedForm);
+ ScrolledForm form = managedForm.getForm();
+ form.setText("Android Manifest Instrumentation");
+ form.setImage(AdtPlugin.getAndroidLogo());
+
+ UiElementNode manifest = mEditor.getUiRootNode();
+ AndroidManifestDescriptors manifestDescriptor = mEditor.getManifestDescriptors();
+
+ ElementDescriptor[] descriptorFilters = null;
+ if (manifestDescriptor != null) {
+ descriptorFilters = new ElementDescriptor[] {
+ manifestDescriptor.getInstrumentationElement(),
+ };
+ }
+
+ mTreeBlock = new UiTreeBlock(mEditor, manifest,
+ true /* autoCreateRoot */,
+ descriptorFilters,
+ "Instrumentation",
+ "List of instrumentations defined in the manifest");
+ mTreeBlock.createContent(managedForm);
+ }
+
+ /**
+ * Changes and refreshes the Application UI node handled by the sub parts.
+ */
+ public void refreshUiNode() {
+ if (mTreeBlock != null) {
+ UiElementNode manifest = mEditor.getUiRootNode();
+ AndroidManifestDescriptors manifestDescriptor = mEditor.getManifestDescriptors();
+
+ mTreeBlock.changeRootAndDescriptors(manifest,
+ new ElementDescriptor[] {
+ manifestDescriptor.getInstrumentationElement()
+ },
+ true /* refresh */);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/OverviewExportPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/OverviewExportPart.java
new file mode 100644
index 0000000..66af84c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/OverviewExportPart.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.manifest.pages;
+
+import com.android.ide.eclipse.common.project.ExportHelper;
+import com.android.ide.eclipse.editors.manifest.ManifestEditor;
+import com.android.ide.eclipse.editors.ui.SectionHelper.ManifestSectionPart;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.forms.events.HyperlinkAdapter;
+import org.eclipse.ui.forms.events.HyperlinkEvent;
+import org.eclipse.ui.forms.widgets.FormText;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.Section;
+import org.eclipse.ui.part.FileEditorInput;
+
+/**
+ * Export section part for overview page.
+ */
+final class OverviewExportPart extends ManifestSectionPart {
+
+ private final OverviewPage mOverviewPage;
+
+ public OverviewExportPart(OverviewPage overviewPage, Composite body, FormToolkit toolkit, ManifestEditor editor) {
+ super(body, toolkit, Section.TWISTIE | Section.EXPANDED, true /* description */);
+ mOverviewPage = overviewPage;
+ Section section = getSection();
+ section.setText("Exporting");
+ section.setDescription("To export the application for distribution, you have the following options:");
+
+ Composite table = createTableLayout(toolkit, 2 /* numColumns */);
+
+ StringBuffer buf = new StringBuffer();
+ buf.append("<form><li><a href=\"wizard\">"); //$NON-NLS-1$
+ buf.append("Use the Export Wizard");
+ buf.append("</a>"); //$NON-NLS-1$
+ buf.append(" to export and sign an APK");
+ buf.append("</li>"); //$NON-NLS-1$
+ buf.append("<li><a href=\"manual\">"); //$NON-NLS-1$
+ buf.append("Export an unsigned APK");
+ buf.append("</a>"); //$NON-NLS-1$
+ buf.append(" and sign it manually");
+ buf.append("</li></form>"); //$NON-NLS-1$
+
+ FormText text = createFormText(table, toolkit, true, buf.toString(),
+ false /* setupLayoutData */);
+ text.addHyperlinkListener(new HyperlinkAdapter() {
+ @Override
+ public void linkActivated(HyperlinkEvent e) {
+ // get the project from the editor
+ IEditorInput input = mOverviewPage.mEditor.getEditorInput();
+ if (input instanceof FileEditorInput) {
+ FileEditorInput fileInput = (FileEditorInput)input;
+ IFile file = fileInput.getFile();
+ IProject project = file.getProject();
+
+ if ("manual".equals(e.data)) { //$NON-NLS-1$
+ // now we can export an unsigned apk for the project.
+ ExportHelper.exportProject(project);
+ } else {
+ // call the export wizard
+ ExportHelper.startExportWizard(project);
+ }
+ }
+ }
+ });
+
+ layoutChanged();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/OverviewInfoPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/OverviewInfoPart.java
new file mode 100644
index 0000000..026b760
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/OverviewInfoPart.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.manifest.pages;
+
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.editors.manifest.ManifestEditor;
+import com.android.ide.eclipse.editors.manifest.descriptors.AndroidManifestDescriptors;
+import com.android.ide.eclipse.editors.ui.UiElementPart;
+import com.android.ide.eclipse.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.Section;
+import org.w3c.dom.Node;
+
+/**
+ * Generic info section part for overview page
+ */
+final class OverviewInfoPart extends UiElementPart {
+
+ private IManagedForm mManagedForm;
+
+ public OverviewInfoPart(Composite body, FormToolkit toolkit, ManifestEditor editor) {
+ super(body, toolkit, editor,
+ null, // uiElementNode
+ "General Information", // section title
+ "Defines general information about the AndroidManifest.xml", // section description
+ Section.TWISTIE | Section.EXPANDED);
+ }
+
+ /**
+ * Retrieves the UiElementNode that this part will edit. The node must exist
+ * and can't be null, by design, because it's a mandatory node.
+ */
+ private static UiElementNode getManifestUiNode(ManifestEditor editor) {
+ AndroidManifestDescriptors manifestDescriptors = editor.getManifestDescriptors();
+ if (manifestDescriptors != null) {
+ ElementDescriptor desc = manifestDescriptors.getManifestElement();
+ if (editor.getUiRootNode().getDescriptor() == desc) {
+ return editor.getUiRootNode();
+ } else {
+ return editor.getUiRootNode().findUiChildNode(desc.getXmlName());
+ }
+ }
+
+ // No manifest descriptor: we have a dummy UiRootNode, so we return that.
+ // The editor will be reloaded once we have the proper descriptors anyway.
+ return editor.getUiRootNode();
+ }
+
+ /**
+ * Retrieves the uses-sdk UI node. Since this is a mandatory node, it *always*
+ * exists, even if there is no matching XML node.
+ */
+ private UiElementNode getUsesSdkUiNode(ManifestEditor editor) {
+ AndroidManifestDescriptors manifestDescriptors = editor.getManifestDescriptors();
+ if (manifestDescriptors != null) {
+ ElementDescriptor desc = manifestDescriptors.getUsesSdkElement();
+ return editor.getUiRootNode().findUiChildNode(desc.getXmlName());
+ }
+
+ // No manifest descriptor: we have a dummy UiRootNode, so we return that.
+ // The editor will be reloaded once we have the proper descriptors anyway.
+ return editor.getUiRootNode();
+ }
+
+ /**
+ * Overridden in order to capture the current managed form.
+ *
+ * {@inheritDoc}
+ */
+ @Override
+ protected void createFormControls(final IManagedForm managedForm) {
+ mManagedForm = managedForm;
+ super.createFormControls(managedForm);
+ }
+
+ /**
+ * Removes any existing Attribute UI widgets and recreate them if the SDK has changed.
+ * <p/>
+ * This is called by {@link OverviewPage#refreshUiApplicationNode()} when the
+ * SDK has changed.
+ */
+ public void onSdkChanged() {
+ createUiAttributes(mManagedForm);
+ }
+
+ /**
+ * Overridden to add the description and the ui attributes of both the
+ * manifest and uses-sdk UI nodes.
+ * <p/>
+ * {@inheritDoc}
+ */
+ @Override
+ protected void fillTable(Composite table, IManagedForm managedForm) {
+ int n = 0;
+
+ UiElementNode uiNode = getManifestUiNode(getEditor());
+ n += insertUiAttributes(uiNode, table, managedForm);
+
+ uiNode = getUsesSdkUiNode(getEditor());
+ n += insertUiAttributes(uiNode, table, managedForm);
+
+ if (n == 0) {
+ createLabel(table, managedForm.getToolkit(),
+ "No attributes to display, waiting for SDK to finish loading...",
+ null /* tooltip */ );
+ }
+
+ layoutChanged();
+ }
+
+ /**
+ * Overridden to tests whether either the manifest or uses-sdk nodes parts are dirty.
+ * <p/>
+ * {@inheritDoc}
+ *
+ * @return <code>true</code> if the part is dirty, <code>false</code>
+ * otherwise.
+ */
+ @Override
+ public boolean isDirty() {
+ boolean dirty = super.isDirty();
+
+ if (!dirty) {
+ UiElementNode uiNode = getManifestUiNode(getEditor());
+ if (uiNode != null) {
+ for (UiAttributeNode ui_attr : uiNode.getUiAttributes()) {
+ if (ui_attr.isDirty()) {
+ markDirty();
+ dirty = true;
+ break;
+ }
+ }
+ }
+ }
+
+ if (!dirty) {
+ UiElementNode uiNode = getUsesSdkUiNode(getEditor());
+ if (uiNode != null) {
+ for (UiAttributeNode ui_attr : uiNode.getUiAttributes()) {
+ if (ui_attr.isDirty()) {
+ markDirty();
+ dirty = true;
+ break;
+ }
+ }
+ }
+ }
+
+ return dirty;
+ }
+
+ /**
+ * Overridden to save both the manifest or uses-sdk nodes.
+ * <p/>
+ * {@inheritDoc}
+ */
+ @Override
+ public void commit(boolean onSave) {
+ final UiElementNode manifestUiNode = getManifestUiNode(getEditor());
+ final UiElementNode usesSdkUiNode = getUsesSdkUiNode(getEditor());
+
+ getEditor().editXmlModel(new Runnable() {
+ public void run() {
+ if (manifestUiNode != null && manifestUiNode.isDirty()) {
+ for (UiAttributeNode ui_attr : manifestUiNode.getUiAttributes()) {
+ ui_attr.commit();
+ }
+ }
+
+ if (usesSdkUiNode != null && usesSdkUiNode.isDirty()) {
+ for (UiAttributeNode ui_attr : usesSdkUiNode.getUiAttributes()) {
+ ui_attr.commit();
+ }
+
+ if (!usesSdkUiNode.isDirty()) {
+ // Remove the <uses-sdk> XML element if it is empty.
+ // Rather than rely on the internal UI state, actually check that the
+ // XML element has no attributes and no child element so that we don't
+ // trash some user-generated content.
+ Node element = usesSdkUiNode.prepareCommit();
+ if (element != null &&
+ !element.hasAttributes() &&
+ !element.hasChildNodes()) {
+ // Important note: we MUST NOT use usesSdkUiNode.deleteXmlNode()
+ // here, as it would clear the UiAttribute list and thus break the
+ // link between the controls and the ui attribute field.
+ // Instead what we want is simply to remove the XML node and let the
+ // UiElementNode node know.
+ Node parent = element.getParentNode();
+ if (parent != null) {
+ parent.removeChild(element);
+ usesSdkUiNode.loadFromXmlNode(null /*xml_node*/);
+ }
+ }
+ }
+ }
+ }
+ });
+
+ // We need to call super's commit after we synchronized the nodes to make sure we
+ // reset the dirty flag after all the side effects from committing have occurred.
+ super.commit(onSave);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/OverviewLinksPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/OverviewLinksPart.java
new file mode 100644
index 0000000..d637a8f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/OverviewLinksPart.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.manifest.pages;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.editors.manifest.ManifestEditor;
+import com.android.ide.eclipse.editors.manifest.descriptors.AndroidManifestDescriptors;
+import com.android.ide.eclipse.editors.ui.SectionHelper.ManifestSectionPart;
+
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.ui.forms.widgets.FormText;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.Section;
+
+/**
+ * Links section part for overview page.
+ */
+final class OverviewLinksPart extends ManifestSectionPart {
+
+ private final ManifestEditor mEditor;
+ private FormText mFormText;
+
+ public OverviewLinksPart(Composite body, FormToolkit toolkit, ManifestEditor editor) {
+ super(body, toolkit, Section.TWISTIE | Section.EXPANDED, true /* description */);
+ mEditor = editor;
+ Section section = getSection();
+ section.setText("Links");
+ section.setDescription("The content of the Android Manifest is made up of three sections. You can also edit the XML directly.");
+
+ Composite table = createTableLayout(toolkit, 2 /* numColumns */);
+
+ StringBuffer buf = new StringBuffer();
+ buf.append(String.format("<form><li style=\"image\" value=\"app_img\"><a href=\"page:%1$s\">", // $NON-NLS-1$
+ ApplicationPage.PAGE_ID));
+ buf.append("Application");
+ buf.append("</a>"); //$NON-NLS-1$
+ buf.append(": Activities, intent filters, providers, services and receivers.");
+ buf.append("</li>"); //$NON-NLS-1$
+
+ buf.append(String.format("<li style=\"image\" value=\"perm_img\"><a href=\"page:%1$s\">", // $NON-NLS-1$
+ PermissionPage.PAGE_ID));
+ buf.append("Permission");
+ buf.append("</a>"); //$NON-NLS-1$
+ buf.append(": Permissions defined and permissions used.");
+ buf.append("</li>"); //$NON-NLS-1$
+
+ buf.append(String.format("<li style=\"image\" value=\"inst_img\"><a href=\"page:%1$s\">", // $NON-NLS-1$
+ InstrumentationPage.PAGE_ID));
+ buf.append("Instrumentation");
+ buf.append("</a>"); //$NON-NLS-1$
+ buf.append(": Instrumentation defined.");
+ buf.append("</li>"); //$NON-NLS-1$
+
+ buf.append(String.format("<li style=\"image\" value=\"android_img\"><a href=\"page:%1$s\">", // $NON-NLS-1$
+ ManifestEditor.TEXT_EDITOR_ID));
+ buf.append("XML Source");
+ buf.append("</a>"); //$NON-NLS-1$
+ buf.append(": Directly edit the AndroidManifest.xml file.");
+ buf.append("</li>"); //$NON-NLS-1$
+
+ buf.append("<li style=\"image\" value=\"android_img\">"); // $NON-NLS-1$
+ buf.append("<a href=\"http://code.google.com/android/devel/bblocks-manifest.html\">Documentation</a>: Documentation from the Android SDK for AndroidManifest.xml."); // $NON-NLS-1$
+ buf.append("</li>"); //$NON-NLS-1$
+ buf.append("</form>"); //$NON-NLS-1$
+
+ mFormText = createFormText(table, toolkit, true, buf.toString(),
+ false /* setupLayoutData */);
+
+ AndroidManifestDescriptors manifestDescriptor = editor.getManifestDescriptors();
+
+ Image androidLogo = AdtPlugin.getAndroidLogo();
+ mFormText.setImage("android_img", androidLogo); //$NON-NLS-1$
+
+ if (manifestDescriptor != null) {
+ mFormText.setImage("app_img", getIcon(manifestDescriptor.getApplicationElement())); //$NON-NLS-1$
+ mFormText.setImage("perm_img", getIcon(manifestDescriptor.getPermissionElement())); //$NON-NLS-1$
+ mFormText.setImage("inst_img", getIcon(manifestDescriptor.getInstrumentationElement())); //$NON-NLS-1$
+ } else {
+ mFormText.setImage("app_img", androidLogo); //$NON-NLS-1$
+ mFormText.setImage("perm_img", androidLogo); //$NON-NLS-1$
+ mFormText.setImage("inst_img", androidLogo); //$NON-NLS-1$
+ }
+ mFormText.addHyperlinkListener(editor.createHyperlinkListener());
+ }
+
+ /**
+ * Update the UI with information from the new descriptors.
+ * <p/>At this point, this only refreshes the icons.
+ * <p/>
+ * This is called by {@link OverviewPage#refreshUiApplicationNode()} when the
+ * SDK has changed.
+ */
+ public void onSdkChanged() {
+ AndroidManifestDescriptors manifestDescriptor = mEditor.getManifestDescriptors();
+ if (manifestDescriptor != null) {
+ mFormText.setImage("app_img", getIcon(manifestDescriptor.getApplicationElement())); //$NON-NLS-1$
+ mFormText.setImage("perm_img", getIcon(manifestDescriptor.getPermissionElement())); //$NON-NLS-1$
+ mFormText.setImage("inst_img", getIcon(manifestDescriptor.getInstrumentationElement())); //$NON-NLS-1$
+ }
+ }
+
+ private Image getIcon(ElementDescriptor desc) {
+ if (desc != null && desc.getIcon() != null) {
+ return desc.getIcon();
+ }
+
+ return AdtPlugin.getAndroidLogo();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/OverviewPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/OverviewPage.java
new file mode 100644
index 0000000..62954bd
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/OverviewPage.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.manifest.pages;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.editors.manifest.ManifestEditor;
+
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.editor.FormPage;
+import org.eclipse.ui.forms.widgets.ColumnLayout;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.ScrolledForm;
+
+
+/**
+ * Page for overview settings, part of the AndroidManifest form editor.
+ * <p/>
+ * Useful reference:
+ * <a href="http://www.eclipse.org/articles/Article-Forms/article.html">
+ * http://www.eclipse.org/articles/Article-Forms/article.html</a>
+ */
+public final class OverviewPage extends FormPage {
+
+ /** Page id used for switching tabs programmatically */
+ final static String PAGE_ID = "overview_page"; //$NON-NLS-1$
+
+ /** Container editor */
+ ManifestEditor mEditor;
+ /** Overview part (attributes for manifest) */
+ private OverviewInfoPart mOverviewPart;
+ /** Overview link part */
+ private OverviewLinksPart mOverviewLinkPart;
+
+ public OverviewPage(ManifestEditor editor) {
+ super(editor, PAGE_ID, "Overview"); // tab's label, user visible, keep it short
+ mEditor = editor;
+ }
+
+ /**
+ * Creates the content in the form hosted in this page.
+ *
+ * @param managedForm the form hosted in this page.
+ */
+ @Override
+ protected void createFormContent(IManagedForm managedForm) {
+ super.createFormContent(managedForm);
+ ScrolledForm form = managedForm.getForm();
+ form.setText("Android Manifest Overview");
+ form.setImage(AdtPlugin.getAndroidLogo());
+
+ Composite body = form.getBody();
+ FormToolkit toolkit = managedForm.getToolkit();
+ ColumnLayout cl = new ColumnLayout();
+ cl.minNumColumns = cl.maxNumColumns = 1;
+ body.setLayout(cl);
+ mOverviewPart = new OverviewInfoPart(body, toolkit, mEditor);
+ managedForm.addPart(mOverviewPart);
+ managedForm.addPart(new OverviewExportPart(this, body, toolkit, mEditor));
+ mOverviewLinkPart = new OverviewLinksPart(body, toolkit, mEditor);
+ managedForm.addPart(mOverviewLinkPart);
+ }
+
+ /**
+ * Changes and refreshes the Application UI node handle by the sub parts.
+ */
+ public void refreshUiApplicationNode() {
+ if (mOverviewPart != null) {
+ mOverviewPart.onSdkChanged();
+ }
+
+ if (mOverviewLinkPart != null) {
+ mOverviewLinkPart.onSdkChanged();
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/PermissionPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/PermissionPage.java
new file mode 100644
index 0000000..41ba22e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/manifest/pages/PermissionPage.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.manifest.pages;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.editors.manifest.ManifestEditor;
+import com.android.ide.eclipse.editors.manifest.descriptors.AndroidManifestDescriptors;
+import com.android.ide.eclipse.editors.ui.tree.UiTreeBlock;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.editor.FormPage;
+import org.eclipse.ui.forms.widgets.ScrolledForm;
+
+/**
+ * Page for permissions settings, part of the AndroidManifest form editor.
+ * <p/>
+ * Useful reference:
+ * <a href="http://www.eclipse.org/articles/Article-Forms/article.html">
+ * http://www.eclipse.org/articles/Article-Forms/article.html</a>
+ */
+public final class PermissionPage extends FormPage {
+ /** Page id used for switching tabs programmatically */
+ public final static String PAGE_ID = "permission_page"; //$NON-NLS-1$
+
+ /** Container editor */
+ ManifestEditor mEditor;
+
+ private UiTreeBlock mTreeBlock;
+
+ public PermissionPage(ManifestEditor editor) {
+ super(editor, PAGE_ID, "Permissions"); // tab label, keep it short
+ mEditor = editor;
+ }
+
+ /**
+ * Creates the content in the form hosted in this page.
+ *
+ * @param managedForm the form hosted in this page.
+ */
+ @Override
+ protected void createFormContent(IManagedForm managedForm) {
+ super.createFormContent(managedForm);
+ ScrolledForm form = managedForm.getForm();
+ form.setText("Android Manifest Permissions");
+ form.setImage(AdtPlugin.getAndroidLogo());
+
+ UiElementNode manifest = mEditor.getUiRootNode();
+ AndroidManifestDescriptors manifestDescriptor = mEditor.getManifestDescriptors();
+
+ ElementDescriptor[] descriptorFilters = null;
+ if (manifestDescriptor != null) {
+ descriptorFilters = new ElementDescriptor[] {
+ manifestDescriptor.getPermissionElement(),
+ manifestDescriptor.getUsesPermissionElement(),
+ manifestDescriptor.getPermissionGroupElement(),
+ manifestDescriptor.getPermissionTreeElement()
+ };
+ }
+ mTreeBlock = new UiTreeBlock(mEditor, manifest,
+ true /* autoCreateRoot */,
+ descriptorFilters,
+ "Permissions",
+ "List of permissions defined and used by the manifest");
+ mTreeBlock.createContent(managedForm);
+ }
+
+ /**
+ * Changes and refreshes the Application UI node handled by the sub parts.
+ */
+ public void refreshUiNode() {
+ if (mTreeBlock != null) {
+ UiElementNode manifest = mEditor.getUiRootNode();
+ AndroidManifestDescriptors manifestDescriptor = mEditor.getManifestDescriptors();
+
+ mTreeBlock.changeRootAndDescriptors(manifest,
+ new ElementDescriptor[] {
+ manifestDescriptor.getPermissionElement(),
+ manifestDescriptor.getUsesPermissionElement(),
+ manifestDescriptor.getPermissionGroupElement(),
+ manifestDescriptor.getPermissionTreeElement()
+ },
+ true /* refresh */);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/MenuContentAssist.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/MenuContentAssist.java
new file mode 100644
index 0000000..bf76d53
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/MenuContentAssist.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.menu;
+
+import com.android.ide.eclipse.adt.sdk.AndroidTargetData;
+import com.android.ide.eclipse.editors.AndroidContentAssist;
+
+/**
+ * Content Assist Processor for /res/menu XML files
+ */
+class MenuContentAssist extends AndroidContentAssist {
+
+ /**
+ * Constructor for LayoutContentAssist
+ */
+ public MenuContentAssist() {
+ super(AndroidTargetData.DESCRIPTOR_MENU);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/MenuEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/MenuEditor.java
new file mode 100644
index 0000000..cff1746
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/MenuEditor.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.menu;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.sdk.AndroidTargetData;
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.common.project.AndroidXPathFactory;
+import com.android.ide.eclipse.editors.AndroidEditor;
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.part.FileEditorInput;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpressionException;
+
+/**
+ * Multi-page form editor for /res/menu XML files.
+ */
+public class MenuEditor extends AndroidEditor {
+
+ public static final String ID = AndroidConstants.EDITORS_NAMESPACE + ".menu.MenuEditor"; //$NON-NLS-1$
+
+ /** Root node of the UI element hierarchy */
+ private UiElementNode mUiRootNode;
+
+ /**
+ * Creates the form editor for resources XML files.
+ */
+ public MenuEditor() {
+ super();
+ }
+
+ /**
+ * Returns the root node of the UI element hierarchy, which here is
+ * the "menu" node.
+ */
+ @Override
+ public UiElementNode getUiRootNode() {
+ return mUiRootNode;
+ }
+
+ // ---- Base Class Overrides ----
+
+ /**
+ * Returns whether the "save as" operation is supported by this editor.
+ * <p/>
+ * Save-As is a valid operation for the ManifestEditor since it acts on a
+ * single source file.
+ *
+ * @see IEditorPart
+ */
+ @Override
+ public boolean isSaveAsAllowed() {
+ return true;
+ }
+
+ /**
+ * Create the various form pages.
+ */
+ @Override
+ protected void createFormPages() {
+ try {
+ addPage(new MenuTreePage(this));
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, "Error creating nested page"); //$NON-NLS-1$
+ }
+
+ }
+
+ /* (non-java doc)
+ * Change the tab/title name to include the project name.
+ */
+ @Override
+ protected void setInput(IEditorInput input) {
+ super.setInput(input);
+ if (input instanceof FileEditorInput) {
+ FileEditorInput fileInput = (FileEditorInput) input;
+ IFile file = fileInput.getFile();
+ setPartName(String.format("%1$s", file.getName()));
+ }
+ }
+
+ /**
+ * Processes the new XML Model, which XML root node is given.
+ *
+ * @param xml_doc The XML document, if available, or null if none exists.
+ */
+ @Override
+ protected void xmlModelChanged(Document xml_doc) {
+ // init the ui root on demand
+ initUiRootNode(false /*force*/);
+
+ mUiRootNode.setXmlDocument(xml_doc);
+ if (xml_doc != null) {
+ ElementDescriptor root_desc = mUiRootNode.getDescriptor();
+ try {
+ XPath xpath = AndroidXPathFactory.newXPath();
+ Node node = (Node) xpath.evaluate("/" + root_desc.getXmlName(), //$NON-NLS-1$
+ xml_doc,
+ XPathConstants.NODE);
+ if (node == null && root_desc.isMandatory()) {
+ // Create the root element if it doesn't exist yet (for empty new documents)
+ node = mUiRootNode.createXmlNode();
+ }
+
+ // Refresh the manifest UI node and all its descendants
+ mUiRootNode.loadFromXmlNode(node);
+
+ // TODO ? startMonitoringMarkers();
+ } catch (XPathExpressionException e) {
+ AdtPlugin.log(e, "XPath error when trying to find '%s' element in XML.", //$NON-NLS-1$
+ root_desc.getXmlName());
+ }
+ }
+
+ super.xmlModelChanged(xml_doc);
+ }
+
+ /**
+ * Creates the initial UI Root Node, including the known mandatory elements.
+ * @param force if true, a new UiRootNode is recreated even if it already exists.
+ */
+ @Override
+ protected void initUiRootNode(boolean force) {
+ // The root UI node is always created, even if there's no corresponding XML node.
+ if (mUiRootNode == null || force) {
+ Document doc = null;
+ if (mUiRootNode != null) {
+ doc = mUiRootNode.getXmlDocument();
+ }
+
+ // get the target data from the opened file (and its project)
+ AndroidTargetData data = getTargetData();
+
+ ElementDescriptor desc;
+ if (data == null) {
+ desc = new ElementDescriptor("temp", null /*children*/);
+ } else {
+ desc = data.getMenuDescriptors().getDescriptor();
+ }
+
+ mUiRootNode = desc.createUiNode();
+ mUiRootNode.setEditor(this);
+
+ onDescriptorsChanged(doc);
+ }
+ }
+
+ // ---- Local Methods ----
+
+ /**
+ * Reloads the UI manifest node from the XML, and calls the pages to update.
+ */
+ private void onDescriptorsChanged(Document document) {
+ if (document != null) {
+ mUiRootNode.loadFromXmlNode(document);
+ } else {
+ mUiRootNode.reloadFromXmlNode(mUiRootNode.getXmlNode());
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/MenuSourceViewerConfig.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/MenuSourceViewerConfig.java
new file mode 100644
index 0000000..a5e3b09
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/MenuSourceViewerConfig.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.menu;
+
+
+import com.android.ide.eclipse.editors.AndroidSourceViewerConfig;
+
+/**
+ * Source Viewer Configuration that calls in MenuContentAssist.
+ */
+public class MenuSourceViewerConfig extends AndroidSourceViewerConfig {
+
+ public MenuSourceViewerConfig() {
+ super(new MenuContentAssist());
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/MenuTreePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/MenuTreePage.java
new file mode 100644
index 0000000..edbfa5e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/MenuTreePage.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.menu;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.editors.ui.tree.UiTreeBlock;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.editor.FormPage;
+import org.eclipse.ui.forms.widgets.ScrolledForm;
+
+/**
+ * Page for the menu form editor.
+ */
+public final class MenuTreePage extends FormPage {
+ /** Page id used for switching tabs programmatically */
+ public final static String PAGE_ID = "layout_tree_page"; //$NON-NLS-1$
+
+ /** Container editor */
+ MenuEditor mEditor;
+
+ public MenuTreePage(MenuEditor editor) {
+ super(editor, PAGE_ID, "Layout"); // tab's label, keep it short
+ mEditor = editor;
+ }
+
+ /**
+ * Creates the content in the form hosted in this page.
+ *
+ * @param managedForm the form hosted in this page.
+ */
+ @Override
+ protected void createFormContent(IManagedForm managedForm) {
+ super.createFormContent(managedForm);
+ ScrolledForm form = managedForm.getForm();
+ form.setText("Android Menu");
+ form.setImage(AdtPlugin.getAndroidLogo());
+
+ UiElementNode rootNode = mEditor.getUiRootNode();
+ UiTreeBlock block = new UiTreeBlock(mEditor, rootNode,
+ true /* autoCreateRoot */,
+ null /* no element filters */,
+ "Menu Elements",
+ "List of all menu elements in this XML file.");
+ block.createContent(managedForm);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/descriptors/MenuDescriptors.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/descriptors/MenuDescriptors.java
new file mode 100644
index 0000000..40a8f16
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/menu/descriptors/MenuDescriptors.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.menu.descriptors;
+
+import com.android.ide.eclipse.common.resources.DeclareStyleableInfo;
+import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.editors.descriptors.IDescriptorProvider;
+import com.android.ide.eclipse.editors.descriptors.XmlnsAttributeDescriptor;
+import com.android.sdklib.SdkConstants;
+
+import java.util.ArrayList;
+import java.util.Map;
+
+
+/**
+ * Complete description of the menu structure.
+ */
+public final class MenuDescriptors implements IDescriptorProvider {
+
+ public static final String MENU_ROOT_ELEMENT = "menu"; //$NON-NLS-1$
+
+ /** The root element descriptor. */
+ private ElementDescriptor mDescriptor = null;
+
+ /** @return the root descriptor. */
+ public ElementDescriptor getDescriptor() {
+ return mDescriptor;
+ }
+
+ public ElementDescriptor[] getRootElementDescriptors() {
+ return mDescriptor.getChildren();
+ }
+
+ /**
+ * Updates the document descriptor.
+ * <p/>
+ * It first computes the new children of the descriptor and then updates them
+ * all at once.
+ *
+ * @param styleMap The map style => attributes from the attrs.xml file
+ */
+ public synchronized void updateDescriptors(Map<String, DeclareStyleableInfo> styleMap) {
+
+ // There are 3 elements: menu, item and group.
+ // The root element MUST be a menu.
+ // A top menu can contain items or group:
+ // - top groups can contain top items
+ // - top items can contain sub-menus
+ // A sub menu can contains sub items or sub groups:
+ // - sub groups can contain sub items
+ // - sub items cannot contain anything
+
+ if (mDescriptor == null) {
+ mDescriptor = createElement(styleMap,
+ MENU_ROOT_ELEMENT, // xmlName
+ "Menu", // uiName,
+ null, // TODO SDK URL
+ null, // extraAttribute
+ null, // childrenElements,
+ true /* mandatory */);
+ }
+
+ // -- sub menu can have sub_items, sub_groups but not sub_menus
+
+ ElementDescriptor sub_item = createElement(styleMap,
+ "item", // xmlName //$NON-NLS-1$
+ "Item", // uiName,
+ null, // TODO SDK URL
+ null, // extraAttribute
+ null, // childrenElements,
+ false /* mandatory */);
+
+ ElementDescriptor sub_group = createElement(styleMap,
+ "group", // xmlName //$NON-NLS-1$
+ "Group", // uiName,
+ null, // TODO SDK URL
+ null, // extraAttribute
+ new ElementDescriptor[] { sub_item }, // childrenElements,
+ false /* mandatory */);
+
+ ElementDescriptor sub_menu = createElement(styleMap,
+ MENU_ROOT_ELEMENT, // xmlName //$NON-NLS-1$
+ "Sub-Menu", // uiName,
+ null, // TODO SDK URL
+ null, // extraAttribute
+ new ElementDescriptor[] { sub_item, sub_group }, // childrenElements,
+ true /* mandatory */);
+
+ // -- top menu can have all top groups and top items (which can have sub menus)
+
+ ElementDescriptor top_item = createElement(styleMap,
+ "item", // xmlName //$NON-NLS-1$
+ "Item", // uiName,
+ null, // TODO SDK URL
+ null, // extraAttribute
+ new ElementDescriptor[] { sub_menu }, // childrenElements,
+ false /* mandatory */);
+
+ ElementDescriptor top_group = createElement(styleMap,
+ "group", // xmlName //$NON-NLS-1$
+ "Group", // uiName,
+ null, // TODO SDK URL
+ null, // extraAttribute
+ new ElementDescriptor[] { top_item }, // childrenElements,
+ false /* mandatory */);
+
+ XmlnsAttributeDescriptor xmlns = new XmlnsAttributeDescriptor("android", //$NON-NLS-1$
+ SdkConstants.NS_RESOURCES);
+
+ updateElement(mDescriptor, styleMap, "Menu", xmlns); //$NON-NLS-1$
+ mDescriptor.setChildren(new ElementDescriptor[] { top_item, top_group });
+ }
+
+ /**
+ * Returns a new ElementDescriptor constructed from the information given here
+ * and the javadoc & attributes extracted from the style map if any.
+ */
+ private ElementDescriptor createElement(
+ Map<String, DeclareStyleableInfo> styleMap,
+ String xmlName, String uiName, String sdkUrl,
+ AttributeDescriptor extraAttribute,
+ ElementDescriptor[] childrenElements, boolean mandatory) {
+
+ ElementDescriptor element = new ElementDescriptor(xmlName, uiName, null, sdkUrl,
+ null, childrenElements, mandatory);
+
+ return updateElement(element, styleMap,
+ getStyleName(xmlName),
+ extraAttribute);
+ }
+
+ /**
+ * Updates an ElementDescriptor with the javadoc & attributes extracted from the style
+ * map if any.
+ */
+ private ElementDescriptor updateElement(ElementDescriptor element,
+ Map<String, DeclareStyleableInfo> styleMap,
+ String styleName,
+ AttributeDescriptor extraAttribute) {
+ ArrayList<AttributeDescriptor> descs = new ArrayList<AttributeDescriptor>();
+
+ DeclareStyleableInfo style = styleMap != null ? styleMap.get(styleName) : null;
+ if (style != null) {
+ DescriptorsUtils.appendAttributes(descs,
+ null, // elementName
+ SdkConstants.NS_RESOURCES,
+ style.getAttributes(),
+ null, // requiredAttributes
+ null); // overrides
+ element.setTooltip(style.getJavaDoc());
+ }
+
+ if (extraAttribute != null) {
+ descs.add(extraAttribute);
+ }
+
+ element.setAttributes(descs.toArray(new AttributeDescriptor[descs.size()]));
+ return element;
+ }
+
+ /**
+ * Returns the style name (i.e. the <declare-styleable> name found in attrs.xml)
+ * for a given XML element name.
+ * <p/>
+ * The rule is that all elements have for style name:
+ * - their xml name capitalized
+ * - a "Menu" prefix, except for <menu> itself which is just "Menu".
+ */
+ private String getStyleName(String xmlName) {
+ String styleName = DescriptorsUtils.capitalize(xmlName);
+
+ // This is NOT the UI Name but the expected internal style name
+ final String MENU_STYLE_BASE_NAME = "Menu"; //$NON-NLS-1$
+
+ if (!styleName.equals(MENU_STYLE_BASE_NAME)) {
+ styleName = MENU_STYLE_BASE_NAME + styleName;
+ }
+ return styleName;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/ResourcesContentAssist.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/ResourcesContentAssist.java
new file mode 100644
index 0000000..c9c8e17
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/ResourcesContentAssist.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources;
+
+import com.android.ide.eclipse.adt.sdk.AndroidTargetData;
+import com.android.ide.eclipse.editors.AndroidContentAssist;
+
+/**
+ * Content Assist Processor for /res/values and /res/drawable XML files
+ */
+class ResourcesContentAssist extends AndroidContentAssist {
+
+ /**
+ * Constructor for ResourcesContentAssist
+ */
+ public ResourcesContentAssist() {
+ super(AndroidTargetData.DESCRIPTOR_RESOURCES);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/ResourcesEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/ResourcesEditor.java
new file mode 100644
index 0000000..46a9112
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/ResourcesEditor.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.common.project.AndroidXPathFactory;
+import com.android.ide.eclipse.editors.AndroidEditor;
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.editors.resources.descriptors.ResourcesDescriptors;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.part.FileEditorInput;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpressionException;
+
+/**
+ * Multi-page form editor for /res/values and /res/drawable XML files.
+ */
+public class ResourcesEditor extends AndroidEditor {
+
+ public static final String ID = AndroidConstants.EDITORS_NAMESPACE + ".resources.ResourcesEditor"; //$NON-NLS-1$
+
+ /** Root node of the UI element hierarchy */
+ private UiElementNode mUiResourcesNode;
+
+
+ /**
+ * Creates the form editor for resources XML files.
+ */
+ public ResourcesEditor() {
+ super();
+ }
+
+ /**
+ * Returns the root node of the UI element hierarchy, which
+ * here is the "resources" node.
+ */
+ @Override
+ public UiElementNode getUiRootNode() {
+ return mUiResourcesNode;
+ }
+
+ // ---- Base Class Overrides ----
+
+ /**
+ * Returns whether the "save as" operation is supported by this editor.
+ * <p/>
+ * Save-As is a valid operation for the ManifestEditor since it acts on a
+ * single source file.
+ *
+ * @see IEditorPart
+ */
+ @Override
+ public boolean isSaveAsAllowed() {
+ return true;
+ }
+
+ /**
+ * Create the various form pages.
+ */
+ @Override
+ protected void createFormPages() {
+ try {
+ addPage(new ResourcesTreePage(this));
+ } catch (PartInitException e) {
+ AdtPlugin.log(IStatus.ERROR, "Error creating nested page"); //$NON-NLS-1$
+ AdtPlugin.getDefault().getLog().log(e.getStatus());
+ }
+ }
+
+ /* (non-java doc)
+ * Change the tab/title name to include the project name.
+ */
+ @Override
+ protected void setInput(IEditorInput input) {
+ super.setInput(input);
+ if (input instanceof FileEditorInput) {
+ FileEditorInput fileInput = (FileEditorInput) input;
+ IFile file = fileInput.getFile();
+ setPartName(String.format("%1$s",
+ file.getName()));
+ }
+ }
+
+ /**
+ * Processes the new XML Model, which XML root node is given.
+ *
+ * @param xml_doc The XML document, if available, or null if none exists.
+ */
+ @Override
+ protected void xmlModelChanged(Document xml_doc) {
+ // init the ui root on demand
+ initUiRootNode(false /*force*/);
+
+ mUiResourcesNode.setXmlDocument(xml_doc);
+ if (xml_doc != null) {
+ ElementDescriptor resources_desc =
+ ResourcesDescriptors.getInstance().getElementDescriptor();
+ try {
+ XPath xpath = AndroidXPathFactory.newXPath();
+ Node node = (Node) xpath.evaluate("/" + resources_desc.getXmlName(), //$NON-NLS-1$
+ xml_doc,
+ XPathConstants.NODE);
+ assert node != null && node.getNodeName().equals(resources_desc.getXmlName());
+
+ // Refresh the manifest UI node and all its descendants
+ mUiResourcesNode.loadFromXmlNode(node);
+ } catch (XPathExpressionException e) {
+ AdtPlugin.log(e, "XPath error when trying to find '%s' element in XML.", //$NON-NLS-1$
+ resources_desc.getXmlName());
+ }
+ }
+
+ super.xmlModelChanged(xml_doc);
+ }
+
+ /**
+ * Creates the initial UI Root Node, including the known mandatory elements.
+ * @param force if true, a new UiRootNode is recreated even if it already exists.
+ */
+ @Override
+ protected void initUiRootNode(boolean force) {
+ // The manifest UI node is always created, even if there's no corresponding XML node.
+ if (mUiResourcesNode == null || force) {
+ ElementDescriptor resources_desc =
+ ResourcesDescriptors.getInstance().getElementDescriptor();
+ mUiResourcesNode = resources_desc.createUiNode();
+ mUiResourcesNode.setEditor(this);
+
+ onDescriptorsChanged();
+ }
+ }
+
+ // ---- Local Methods ----
+
+ private void onDescriptorsChanged() {
+ // nothing to be done, as the descriptor are static for now.
+ // FIXME Update when the descriptors are not static
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/ResourcesSourceViewerConfig.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/ResourcesSourceViewerConfig.java
new file mode 100644
index 0000000..1804312
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/ResourcesSourceViewerConfig.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources;
+
+
+import com.android.ide.eclipse.editors.AndroidSourceViewerConfig;
+
+/**
+ * Source Viewer Configuration that calls in ResourcesContentAssist.
+ */
+public class ResourcesSourceViewerConfig extends AndroidSourceViewerConfig {
+
+ public ResourcesSourceViewerConfig() {
+ super(new ResourcesContentAssist());
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/ResourcesTreePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/ResourcesTreePage.java
new file mode 100644
index 0000000..5c1b0e1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/ResourcesTreePage.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.editors.resources.manager.ResourceFolder;
+import com.android.ide.eclipse.editors.resources.manager.ResourceManager;
+import com.android.ide.eclipse.editors.ui.tree.UiTreeBlock;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.editor.FormPage;
+import org.eclipse.ui.forms.widgets.ScrolledForm;
+import org.eclipse.ui.part.FileEditorInput;
+
+/**
+ * Page for instrumentation settings, part of the AndroidManifest form editor.
+ */
+public final class ResourcesTreePage extends FormPage {
+ /** Page id used for switching tabs programmatically */
+ public final static String PAGE_ID = "res_tree_page"; //$NON-NLS-1$
+
+ /** Container editor */
+ ResourcesEditor mEditor;
+
+ public ResourcesTreePage(ResourcesEditor editor) {
+ super(editor, PAGE_ID, "Resources"); // tab's label, keep it short
+ mEditor = editor;
+ }
+
+ /**
+ * Creates the content in the form hosted in this page.
+ *
+ * @param managedForm the form hosted in this page.
+ */
+ @Override
+ protected void createFormContent(IManagedForm managedForm) {
+ super.createFormContent(managedForm);
+ ScrolledForm form = managedForm.getForm();
+
+ String configText = null;
+ IEditorInput input = mEditor.getEditorInput();
+ if (input instanceof FileEditorInput) {
+ FileEditorInput fileInput = (FileEditorInput)input;
+ IFile iFile = fileInput.getFile();
+
+ ResourceFolder resFolder = ResourceManager.getInstance().getResourceFolder(iFile);
+ if (resFolder != null) {
+ configText = resFolder.getConfiguration().toDisplayString();
+ }
+ }
+
+ if (configText != null) {
+ form.setText(String.format("Android Resources (%1$s)", configText));
+ } else {
+ form.setText("Android Resources");
+ }
+
+ form.setImage(AdtPlugin.getAndroidLogo());
+
+ UiElementNode resources = mEditor.getUiRootNode();
+ UiTreeBlock block = new UiTreeBlock(mEditor, resources,
+ true /* autoCreateRoot */,
+ null /* no element filters */,
+ "Resources Elements",
+ "List of all resources elements in this XML file.");
+ block.createContent(managedForm);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/CountryCodeQualifier.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/CountryCodeQualifier.java
new file mode 100644
index 0000000..9a61d17
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/CountryCodeQualifier.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.configurations;
+
+import com.android.ide.eclipse.editors.IconFactory;
+
+import org.eclipse.swt.graphics.Image;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Resource Qualifier for Mobile Country Code.
+ */
+public final class CountryCodeQualifier extends ResourceQualifier {
+ /** Default pixel density value. This means the property is not set. */
+ private final static int DEFAULT_CODE = -1;
+
+ private final static Pattern sCountryCodePattern = Pattern.compile("^mcc(\\d{3})$");//$NON-NLS-1$
+
+ private int mCode = DEFAULT_CODE;
+
+ public static final String NAME = "Mobile Country Code";
+
+ /**
+ * Creates and returns a qualifier from the given folder segment. If the segment is incorrect,
+ * <code>null</code> is returned.
+ * @param segment the folder segment from which to create a qualifier.
+ * @return a new {@link CountryCodeQualifier} object or <code>null</code>
+ */
+ public static CountryCodeQualifier getQualifier(String segment) {
+ Matcher m = sCountryCodePattern.matcher(segment);
+ if (m.matches()) {
+ String v = m.group(1);
+
+ int code = -1;
+ try {
+ code = Integer.parseInt(v);
+ } catch (NumberFormatException e) {
+ // looks like the string we extracted wasn't a valid number.
+ return null;
+ }
+
+ CountryCodeQualifier qualifier = new CountryCodeQualifier();
+ qualifier.mCode = code;
+ return qualifier;
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the folder name segment for the given value. This is equivalent to calling
+ * {@link #toString()} on a {@link CountryCodeQualifier} object.
+ * @param code the value of the qualifier, as returned by {@link #getCode()}.
+ */
+ public static String getFolderSegment(int code) {
+ if (code != DEFAULT_CODE && code >= 100 && code <=999) { // code is 3 digit.) {
+ return String.format("mcc%1$d", code); //$NON-NLS-1$
+ }
+
+ return ""; //$NON-NLS-1$
+ }
+
+ public int getCode() {
+ return mCode;
+ }
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+
+ @Override
+ public String getShortName() {
+ return "Country Code";
+ }
+
+ @Override
+ public Image getIcon() {
+ return IconFactory.getInstance().getIcon("mcc"); //$NON-NLS-1$
+ }
+
+ @Override
+ public boolean isValid() {
+ return mCode != DEFAULT_CODE;
+ }
+
+ @Override
+ public boolean checkAndSet(String value, FolderConfiguration config) {
+ CountryCodeQualifier qualifier = getQualifier(value);
+ if (qualifier != null) {
+ config.setCountryCodeQualifier(qualifier);
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean equals(Object qualifier) {
+ if (qualifier instanceof CountryCodeQualifier) {
+ return mCode == ((CountryCodeQualifier)qualifier).mCode;
+ }
+
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return mCode;
+ }
+
+ /**
+ * Returns the string used to represent this qualifier in the folder name.
+ */
+ @Override
+ public String toString() {
+ return getFolderSegment(mCode);
+ }
+
+ @Override
+ public String getStringValue() {
+ if (mCode != DEFAULT_CODE) {
+ return String.format("MCC %1$d", mCode);
+ }
+
+ return ""; //$NON-NLS-1$
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/FolderConfiguration.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/FolderConfiguration.java
new file mode 100644
index 0000000..3c3e11f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/FolderConfiguration.java
@@ -0,0 +1,499 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.configurations;
+
+import com.android.ide.eclipse.editors.resources.manager.ResourceFolderType;
+
+
+/**
+ * Represents the configuration for Resource Folders. All the properties have a default
+ * value which means that the property is not set.
+ */
+public final class FolderConfiguration implements Comparable<FolderConfiguration> {
+ public final static String QUALIFIER_SEP = "-"; //$NON-NLS-1$
+
+ private final ResourceQualifier[] mQualifiers = new ResourceQualifier[INDEX_COUNT];
+
+ private final static int INDEX_COUNTRY_CODE = 0;
+ private final static int INDEX_NETWORK_CODE = 1;
+ private final static int INDEX_LANGUAGE = 2;
+ private final static int INDEX_REGION = 3;
+ private final static int INDEX_SCREEN_ORIENTATION = 4;
+ private final static int INDEX_PIXEL_DENSITY = 5;
+ private final static int INDEX_TOUCH_TYPE = 6;
+ private final static int INDEX_KEYBOARD_STATE = 7;
+ private final static int INDEX_TEXT_INPUT_METHOD = 8;
+ private final static int INDEX_NAVIGATION_METHOD = 9;
+ private final static int INDEX_SCREEN_DIMENSION = 10;
+ private final static int INDEX_COUNT = 11;
+
+ /**
+ * Sets the config from the qualifiers of a given <var>config</var>.
+ * @param config
+ */
+ public void set(FolderConfiguration config) {
+ for (int i = 0 ; i < INDEX_COUNT ; i++) {
+ mQualifiers[i] = config.mQualifiers[i];
+ }
+ }
+
+ /**
+ * Removes the qualifiers from the receiver if they are present (and valid)
+ * in the given configuration.
+ */
+ public void substract(FolderConfiguration config) {
+ for (int i = 0 ; i < INDEX_COUNT ; i++) {
+ if (config.mQualifiers[i] != null && config.mQualifiers[i].isValid()) {
+ mQualifiers[i] = null;
+ }
+ }
+ }
+
+ /**
+ * Returns the first invalid qualifier, or <code>null<code> if they are all valid (or if none
+ * exists).
+ */
+ public ResourceQualifier getInvalidQualifier() {
+ for (int i = 0 ; i < INDEX_COUNT ; i++) {
+ if (mQualifiers[i] != null && mQualifiers[i].isValid() == false) {
+ return mQualifiers[i];
+ }
+ }
+
+ // all allocated qualifiers are valid, we return null.
+ return null;
+ }
+
+ /**
+ * Returns whether the Region qualifier is valid. Region qualifier can only be present if a
+ * Language qualifier is present as well.
+ * @return true if the Region qualifier is valid.
+ */
+ public boolean checkRegion() {
+ if (mQualifiers[INDEX_LANGUAGE] == null && mQualifiers[INDEX_REGION] != null) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Adds a qualifier to the {@link FolderConfiguration}
+ * @param qualifier the {@link ResourceQualifier} to add.
+ */
+ public void addQualifier(ResourceQualifier qualifier) {
+ if (qualifier instanceof CountryCodeQualifier) {
+ mQualifiers[INDEX_COUNTRY_CODE] = qualifier;
+ } else if (qualifier instanceof NetworkCodeQualifier) {
+ mQualifiers[INDEX_NETWORK_CODE] = qualifier;
+ } else if (qualifier instanceof LanguageQualifier) {
+ mQualifiers[INDEX_LANGUAGE] = qualifier;
+ } else if (qualifier instanceof RegionQualifier) {
+ mQualifiers[INDEX_REGION] = qualifier;
+ } else if (qualifier instanceof ScreenOrientationQualifier) {
+ mQualifiers[INDEX_SCREEN_ORIENTATION] = qualifier;
+ } else if (qualifier instanceof PixelDensityQualifier) {
+ mQualifiers[INDEX_PIXEL_DENSITY] = qualifier;
+ } else if (qualifier instanceof TouchScreenQualifier) {
+ mQualifiers[INDEX_TOUCH_TYPE] = qualifier;
+ } else if (qualifier instanceof KeyboardStateQualifier) {
+ mQualifiers[INDEX_KEYBOARD_STATE] = qualifier;
+ } else if (qualifier instanceof TextInputMethodQualifier) {
+ mQualifiers[INDEX_TEXT_INPUT_METHOD] = qualifier;
+ } else if (qualifier instanceof NavigationMethodQualifier) {
+ mQualifiers[INDEX_NAVIGATION_METHOD] = qualifier;
+ } else if (qualifier instanceof ScreenDimensionQualifier) {
+ mQualifiers[INDEX_SCREEN_DIMENSION] = qualifier;
+ }
+ }
+
+ /**
+ * Removes a given qualifier from the {@link FolderConfiguration}.
+ * @param qualifier the {@link ResourceQualifier} to remove.
+ */
+ public void removeQualifier(ResourceQualifier qualifier) {
+ for (int i = 0 ; i < INDEX_COUNT ; i++) {
+ if (mQualifiers[i] == qualifier) {
+ mQualifiers[i] = null;
+ return;
+ }
+ }
+ }
+
+ public void setCountryCodeQualifier(CountryCodeQualifier qualifier) {
+ mQualifiers[INDEX_COUNTRY_CODE] = qualifier;
+ }
+
+ public CountryCodeQualifier getCountryCodeQualifier() {
+ return (CountryCodeQualifier)mQualifiers[INDEX_COUNTRY_CODE];
+ }
+
+ public void setNetworkCodeQualifier(NetworkCodeQualifier qualifier) {
+ mQualifiers[INDEX_NETWORK_CODE] = qualifier;
+ }
+
+ public NetworkCodeQualifier getNetworkCodeQualifier() {
+ return (NetworkCodeQualifier)mQualifiers[INDEX_NETWORK_CODE];
+ }
+
+ public void setLanguageQualifier(LanguageQualifier qualifier) {
+ mQualifiers[INDEX_LANGUAGE] = qualifier;
+ }
+
+ public LanguageQualifier getLanguageQualifier() {
+ return (LanguageQualifier)mQualifiers[INDEX_LANGUAGE];
+ }
+
+ public void setRegionQualifier(RegionQualifier qualifier) {
+ mQualifiers[INDEX_REGION] = qualifier;
+ }
+
+ public RegionQualifier getRegionQualifier() {
+ return (RegionQualifier)mQualifiers[INDEX_REGION];
+ }
+
+ public void setScreenOrientationQualifier(ScreenOrientationQualifier qualifier) {
+ mQualifiers[INDEX_SCREEN_ORIENTATION] = qualifier;
+ }
+
+ public ScreenOrientationQualifier getScreenOrientationQualifier() {
+ return (ScreenOrientationQualifier)mQualifiers[INDEX_SCREEN_ORIENTATION];
+ }
+
+ public void setPixelDensityQualifier(PixelDensityQualifier qualifier) {
+ mQualifiers[INDEX_PIXEL_DENSITY] = qualifier;
+ }
+
+ public PixelDensityQualifier getPixelDensityQualifier() {
+ return (PixelDensityQualifier)mQualifiers[INDEX_PIXEL_DENSITY];
+ }
+
+ public void setTouchTypeQualifier(TouchScreenQualifier qualifier) {
+ mQualifiers[INDEX_TOUCH_TYPE] = qualifier;
+ }
+
+ public TouchScreenQualifier getTouchTypeQualifier() {
+ return (TouchScreenQualifier)mQualifiers[INDEX_TOUCH_TYPE];
+ }
+
+ public void setKeyboardStateQualifier(KeyboardStateQualifier qualifier) {
+ mQualifiers[INDEX_KEYBOARD_STATE] = qualifier;
+ }
+
+ public KeyboardStateQualifier getKeyboardStateQualifier() {
+ return (KeyboardStateQualifier)mQualifiers[INDEX_KEYBOARD_STATE];
+ }
+
+ public void setTextInputMethodQualifier(TextInputMethodQualifier qualifier) {
+ mQualifiers[INDEX_TEXT_INPUT_METHOD] = qualifier;
+ }
+
+ public TextInputMethodQualifier getTextInputMethodQualifier() {
+ return (TextInputMethodQualifier)mQualifiers[INDEX_TEXT_INPUT_METHOD];
+ }
+
+ public void setNavigationMethodQualifier(NavigationMethodQualifier qualifier) {
+ mQualifiers[INDEX_NAVIGATION_METHOD] = qualifier;
+ }
+
+ public NavigationMethodQualifier getNavigationMethodQualifier() {
+ return (NavigationMethodQualifier)mQualifiers[INDEX_NAVIGATION_METHOD];
+ }
+
+ public void setScreenDimensionQualifier(ScreenDimensionQualifier qualifier) {
+ mQualifiers[INDEX_SCREEN_DIMENSION] = qualifier;
+ }
+
+ public ScreenDimensionQualifier getScreenDimensionQualifier() {
+ return (ScreenDimensionQualifier)mQualifiers[INDEX_SCREEN_DIMENSION];
+ }
+
+ /**
+ * Returns whether an object is equals to the receiver.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+
+ if (obj instanceof FolderConfiguration) {
+ FolderConfiguration fc = (FolderConfiguration)obj;
+ for (int i = 0 ; i < INDEX_COUNT ; i++) {
+ ResourceQualifier qualifier = mQualifiers[i];
+ ResourceQualifier fcQualifier = fc.mQualifiers[i];
+ if (qualifier != null) {
+ if (qualifier.equals(fcQualifier) == false) {
+ return false;
+ }
+ } else if (fcQualifier != null) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return toString().hashCode();
+ }
+
+ /**
+ * Returns whether the Configuration has only default values.
+ */
+ public boolean isDefault() {
+ for (ResourceQualifier irq : mQualifiers) {
+ if (irq != null) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the name of a folder with the configuration.
+ */
+ public String getFolderName(ResourceFolderType folder) {
+ StringBuilder result = new StringBuilder(folder.getName());
+
+ for (ResourceQualifier qualifier : mQualifiers) {
+ if (qualifier != null) {
+ result.append(QUALIFIER_SEP);
+ result.append(qualifier.toString());
+ }
+ }
+
+ return result.toString();
+ }
+
+ /**
+ * Returns a string valid for usage in a folder name, or <code>null</code> if the configuration
+ * is default.
+ */
+ @Override
+ public String toString() {
+ StringBuilder result = null;
+
+ for (ResourceQualifier irq : mQualifiers) {
+ if (irq != null) {
+ if (result == null) {
+ result = new StringBuilder();
+ } else {
+ result.append(QUALIFIER_SEP);
+ }
+ result.append(irq.toString());
+ }
+ }
+
+ if (result != null) {
+ return result.toString();
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns a string valid for display purpose.
+ */
+ public String toDisplayString() {
+ if (isDefault()) {
+ return "default";
+ }
+
+ StringBuilder result = null;
+ int index = 0;
+ ResourceQualifier qualifier = null;
+
+ // pre- language/region qualifiers
+ while (index < INDEX_LANGUAGE) {
+ qualifier = mQualifiers[index++];
+ if (qualifier != null) {
+ if (result == null) {
+ result = new StringBuilder();
+ } else {
+ result.append(", "); //$NON-NLS-1$
+ }
+ result.append(qualifier.getStringValue());
+
+ }
+ }
+
+ // process the language/region qualifier in a custom way, if there are both non null.
+ if (mQualifiers[INDEX_LANGUAGE] != null && mQualifiers[INDEX_REGION] != null) {
+ String language = mQualifiers[INDEX_LANGUAGE].getStringValue();
+ String region = mQualifiers[INDEX_REGION].getStringValue();
+
+ if (result == null) {
+ result = new StringBuilder();
+ } else {
+ result.append(", "); //$NON-NLS-1$
+ }
+ result.append(String.format("%s_%s", language, region)); //$NON-NLS-1$
+
+ index += 2;
+ }
+
+ // post language/region qualifiers.
+ while (index < INDEX_COUNT) {
+ qualifier = mQualifiers[index++];
+ if (qualifier != null) {
+ if (result == null) {
+ result = new StringBuilder();
+ } else {
+ result.append(", "); //$NON-NLS-1$
+ }
+ result.append(qualifier.getStringValue());
+
+ }
+ }
+
+ return result.toString();
+ }
+
+ public int compareTo(FolderConfiguration folderConfig) {
+ // default are always at the top.
+ if (isDefault()) {
+ if (folderConfig.isDefault()) {
+ return 0;
+ }
+ return -1;
+ }
+
+ // now we compare the qualifiers
+ for (int i = 0 ; i < INDEX_COUNT; i++) {
+ ResourceQualifier qualifier1 = mQualifiers[i];
+ ResourceQualifier qualifier2 = folderConfig.mQualifiers[i];
+
+ if (qualifier1 == null) {
+ if (qualifier2 == null) {
+ continue;
+ } else {
+ return -1;
+ }
+ } else {
+ if (qualifier2 == null) {
+ return 1;
+ } else {
+ int result = qualifier1.compareTo(qualifier2);
+
+ if (result == 0) {
+ continue;
+ }
+
+ return result;
+ }
+ }
+ }
+
+ // if we arrive here, all the qualifier matches
+ return 0;
+ }
+
+ /**
+ * Returns whether the configuration match the given reference config.
+ * <p/>A match means that:
+ * <ul>
+ * <li>This config does not use any qualifier not used by the reference config</li>
+ * <li>The qualifier used by this config have the same values as the qualifiers of
+ * the reference config.</li>
+ * </ul>
+ * @param referenceConfig The reference configuration to test against.
+ * @return the number of matching qualifiers or -1 if the configurations are not compatible.
+ */
+ public int match(FolderConfiguration referenceConfig) {
+ int matchCount = 0;
+
+ for (int i = 0 ; i < INDEX_COUNT ; i++) {
+ ResourceQualifier testQualifier = mQualifiers[i];
+ ResourceQualifier referenceQualifier = referenceConfig.mQualifiers[i];
+
+ // we only care if testQualifier is non null. If it's null, it's a match but
+ // without increasing the matchCount.
+ if (testQualifier != null) {
+ if (referenceQualifier == null) {
+ return -1;
+ } else if (testQualifier.equals(referenceQualifier) == false) {
+ return -1;
+ }
+
+ // the qualifier match, increment the count
+ matchCount++;
+ }
+ }
+ return matchCount;
+ }
+
+ /**
+ * Returns the index of the first non null {@link ResourceQualifier} starting at index
+ * <var>startIndex</var>
+ * @param startIndex
+ * @return -1 if no qualifier was found.
+ */
+ public int getHighestPriorityQualifier(int startIndex) {
+ for (int i = startIndex ; i < INDEX_COUNT ; i++) {
+ if (mQualifiers[i] != null) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Create default qualifiers.
+ */
+ public void createDefault() {
+ mQualifiers[INDEX_COUNTRY_CODE] = new CountryCodeQualifier();
+ mQualifiers[INDEX_NETWORK_CODE] = new NetworkCodeQualifier();
+ mQualifiers[INDEX_LANGUAGE] = new LanguageQualifier();
+ mQualifiers[INDEX_REGION] = new RegionQualifier();
+ mQualifiers[INDEX_SCREEN_ORIENTATION] = new ScreenOrientationQualifier();
+ mQualifiers[INDEX_PIXEL_DENSITY] = new PixelDensityQualifier();
+ mQualifiers[INDEX_TOUCH_TYPE] = new TouchScreenQualifier();
+ mQualifiers[INDEX_KEYBOARD_STATE] = new KeyboardStateQualifier();
+ mQualifiers[INDEX_TEXT_INPUT_METHOD] = new TextInputMethodQualifier();
+ mQualifiers[INDEX_NAVIGATION_METHOD] = new NavigationMethodQualifier();
+ mQualifiers[INDEX_SCREEN_DIMENSION] = new ScreenDimensionQualifier();
+ }
+
+ /**
+ * Returns an array of all the non null qualifiers.
+ */
+ public ResourceQualifier[] getQualifiers() {
+ int count = 0;
+ for (int i = 0 ; i < INDEX_COUNT ; i++) {
+ if (mQualifiers[i] != null) {
+ count++;
+ }
+ }
+
+ ResourceQualifier[] array = new ResourceQualifier[count];
+ int index = 0;
+ for (int i = 0 ; i < INDEX_COUNT ; i++) {
+ if (mQualifiers[i] != null) {
+ array[index++] = mQualifiers[i];
+ }
+ }
+
+ return array;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/KeyboardStateQualifier.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/KeyboardStateQualifier.java
new file mode 100644
index 0000000..ad232ed
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/KeyboardStateQualifier.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.configurations;
+
+import com.android.ide.eclipse.editors.IconFactory;
+
+import org.eclipse.swt.graphics.Image;
+
+
+
+/**
+ * Resource Qualifier for keyboard state.
+ */
+public final class KeyboardStateQualifier extends ResourceQualifier {
+
+ public static final String NAME = "Keyboard State";
+
+ private KeyboardState mValue = null;
+
+ /**
+ * Screen Orientation enum.
+ */
+ public static enum KeyboardState {
+ EXPOSED("keysexposed", "Exposed"), //$NON-NLS-1$
+ HIDDEN("keyshidden", "Hidden"); //$NON-NLS-1$
+
+ private String mValue;
+ private String mDisplayValue;
+
+ private KeyboardState(String value, String displayValue) {
+ mValue = value;
+ mDisplayValue = displayValue;
+ }
+
+ /**
+ * Returns the enum for matching the provided qualifier value.
+ * @param value The qualifier value.
+ * @return the enum for the qualifier value or null if no matching was found.
+ */
+ static KeyboardState getEnum(String value) {
+ for (KeyboardState orient : values()) {
+ if (orient.mValue.equals(value)) {
+ return orient;
+ }
+ }
+
+ return null;
+ }
+
+ public String getValue() {
+ return mValue;
+ }
+
+ public String getDisplayValue() {
+ return mDisplayValue;
+ }
+
+ public static int getIndex(KeyboardState value) {
+ int i = 0;
+ for (KeyboardState input : values()) {
+ if (value == input) {
+ return i;
+ }
+
+ i++;
+ }
+
+ return -1;
+ }
+
+ public static KeyboardState getByIndex(int index) {
+ int i = 0;
+ for (KeyboardState value : values()) {
+ if (i == index) {
+ return value;
+ }
+ i++;
+ }
+ return null;
+ }
+ }
+
+ public KeyboardStateQualifier() {
+ // pass
+ }
+
+ public KeyboardStateQualifier(KeyboardState value) {
+ mValue = value;
+ }
+
+ public KeyboardState getValue() {
+ return mValue;
+ }
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+
+ @Override
+ public String getShortName() {
+ return "Keyboard";
+ }
+
+ @Override
+ public Image getIcon() {
+ return IconFactory.getInstance().getIcon("keyboard"); //$NON-NLS-1$
+ }
+
+ @Override
+ public boolean isValid() {
+ return mValue != null;
+ }
+
+ @Override
+ public boolean checkAndSet(String value, FolderConfiguration config) {
+ KeyboardState orientation = KeyboardState.getEnum(value);
+ if (orientation != null) {
+ KeyboardStateQualifier qualifier = new KeyboardStateQualifier();
+ qualifier.mValue = orientation;
+ config.setKeyboardStateQualifier(qualifier);
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean equals(Object qualifier) {
+ if (qualifier instanceof KeyboardStateQualifier) {
+ return mValue == ((KeyboardStateQualifier)qualifier).mValue;
+ }
+
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ if (mValue != null) {
+ return mValue.hashCode();
+ }
+
+ return 0;
+ }
+
+ /**
+ * Returns the string used to represent this qualifier in the folder name.
+ */
+ @Override
+ public String toString() {
+ if (mValue != null) {
+ return mValue.getValue();
+ }
+
+ return ""; //$NON-NLS-1$
+ }
+
+ @Override
+ public String getStringValue() {
+ if (mValue != null) {
+ return mValue.getDisplayValue();
+ }
+
+ return ""; //$NON-NLS-1$
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/LanguageQualifier.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/LanguageQualifier.java
new file mode 100644
index 0000000..99c3a43
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/LanguageQualifier.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.configurations;
+
+import com.android.ide.eclipse.editors.IconFactory;
+
+import org.eclipse.swt.graphics.Image;
+
+import java.util.regex.Pattern;
+
+/**
+ * Resource Qualifier for Language.
+ */
+public final class LanguageQualifier extends ResourceQualifier {
+ private final static Pattern sLanguagePattern = Pattern.compile("^[a-z]{2}$"); //$NON-NLS-1$
+
+ public static final String NAME = "Language";
+
+ private String mValue;
+
+ /**
+ * Creates and returns a qualifier from the given folder segment. If the segment is incorrect,
+ * <code>null</code> is returned.
+ * @param segment the folder segment from which to create a qualifier.
+ * @return a new {@link LanguageQualifier} object or <code>null</code>
+ */
+ public static LanguageQualifier getQualifier(String segment) {
+ if (sLanguagePattern.matcher(segment).matches()) {
+ LanguageQualifier qualifier = new LanguageQualifier();
+ qualifier.mValue = segment;
+
+ return qualifier;
+ }
+ return null;
+ }
+
+ /**
+ * Returns the folder name segment for the given value. This is equivalent to calling
+ * {@link #toString()} on a {@link LanguageQualifier} object.
+ * @param value the value of the qualifier, as returned by {@link #getValue()}.
+ */
+ public static String getFolderSegment(String value) {
+ String segment = value.toLowerCase();
+ if (sLanguagePattern.matcher(segment).matches()) {
+ return segment;
+ }
+
+ return null;
+ }
+
+ public String getValue() {
+ if (mValue != null) {
+ return mValue;
+ }
+
+ return ""; //$NON-NLS-1$
+ }
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+
+ @Override
+ public String getShortName() {
+ return NAME;
+ }
+
+ @Override
+ public Image getIcon() {
+ return IconFactory.getInstance().getIcon("language"); //$NON-NLS-1$
+ }
+
+ @Override
+ public boolean isValid() {
+ return mValue != null;
+ }
+
+ @Override
+ public boolean checkAndSet(String value, FolderConfiguration config) {
+ LanguageQualifier qualifier = getQualifier(value);
+ if (qualifier != null) {
+ config.setLanguageQualifier(qualifier);
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean equals(Object qualifier) {
+ if (qualifier instanceof LanguageQualifier) {
+ if (mValue == null) {
+ return ((LanguageQualifier)qualifier).mValue == null;
+ }
+ return mValue.equals(((LanguageQualifier)qualifier).mValue);
+ }
+
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ if (mValue != null) {
+ return mValue.hashCode();
+ }
+
+ return 0;
+ }
+
+ /**
+ * Returns the string used to represent this qualifier in the folder name.
+ */
+ @Override
+ public String toString() {
+ if (mValue != null) {
+ return getFolderSegment(mValue);
+ }
+
+ return ""; //$NON-NLS-1$
+ }
+
+ @Override
+ public String getStringValue() {
+ if (mValue != null) {
+ return mValue;
+ }
+
+ return ""; //$NON-NLS-1$
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/NavigationMethodQualifier.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/NavigationMethodQualifier.java
new file mode 100644
index 0000000..1a2cf53
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/NavigationMethodQualifier.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.configurations;
+
+import com.android.ide.eclipse.editors.IconFactory;
+
+import org.eclipse.swt.graphics.Image;
+
+
+
+/**
+ * Resource Qualifier for Navigation Method.
+ */
+public final class NavigationMethodQualifier extends ResourceQualifier {
+
+ public static final String NAME = "Navigation Method";
+
+ private NavigationMethod mValue;
+
+ /**
+ * Navigation Method enum.
+ */
+ public static enum NavigationMethod {
+ DPAD("dpad", "D-pad"), //$NON-NLS-1$
+ TRACKBALL("trackball", "Trackball"), //$NON-NLS-1$
+ WHEEL("wheel", "Wheel"), //$NON-NLS-1$
+ NONAV("nonav", "No Navigation"); //$NON-NLS-1$
+
+ private String mValue;
+ private String mDisplay;
+
+ private NavigationMethod(String value, String display) {
+ mValue = value;
+ mDisplay = display;
+ }
+
+ /**
+ * Returns the enum for matching the provided qualifier value.
+ * @param value The qualifier value.
+ * @return the enum for the qualifier value or null if no matching was found.
+ */
+ static NavigationMethod getEnum(String value) {
+ for (NavigationMethod orient : values()) {
+ if (orient.mValue.equals(value)) {
+ return orient;
+ }
+ }
+
+ return null;
+ }
+
+ public String getValue() {
+ return mValue;
+ }
+
+ public String getDisplayValue() {
+ return mDisplay;
+ }
+
+ public static int getIndex(NavigationMethod value) {
+ int i = 0;
+ for (NavigationMethod nav : values()) {
+ if (nav == value) {
+ return i;
+ }
+
+ i++;
+ }
+
+ return -1;
+ }
+
+ public static NavigationMethod getByIndex(int index) {
+ int i = 0;
+ for (NavigationMethod value : values()) {
+ if (i == index) {
+ return value;
+ }
+ i++;
+ }
+ return null;
+ }
+ }
+
+ public NavigationMethodQualifier() {
+ // pass
+ }
+
+ public NavigationMethodQualifier(NavigationMethod value) {
+ mValue = value;
+ }
+
+ public NavigationMethod getValue() {
+ return mValue;
+ }
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+
+ @Override
+ public String getShortName() {
+ return "Navigation";
+ }
+
+
+ @Override
+ public Image getIcon() {
+ return IconFactory.getInstance().getIcon("navpad"); //$NON-NLS-1$
+ }
+
+ @Override
+ public boolean isValid() {
+ return mValue != null;
+ }
+
+ @Override
+ public boolean checkAndSet(String value, FolderConfiguration config) {
+ NavigationMethod method = NavigationMethod.getEnum(value);
+ if (method != null) {
+ NavigationMethodQualifier qualifier = new NavigationMethodQualifier();
+ qualifier.mValue = method;
+ config.setNavigationMethodQualifier(qualifier);
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean equals(Object qualifier) {
+ if (qualifier instanceof NavigationMethodQualifier) {
+ return mValue == ((NavigationMethodQualifier)qualifier).mValue;
+ }
+
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ if (mValue != null) {
+ return mValue.hashCode();
+ }
+
+ return 0;
+ }
+
+ /**
+ * Returns the string used to represent this qualifier in the folder name.
+ */
+ @Override
+ public String toString() {
+ if (mValue != null) {
+ return mValue.getValue();
+ }
+
+ return ""; //$NON-NLS-1$
+ }
+
+ @Override
+ public String getStringValue() {
+ if (mValue != null) {
+ return mValue.getDisplayValue();
+ }
+
+ return ""; //$NON-NLS-1$
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/NetworkCodeQualifier.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/NetworkCodeQualifier.java
new file mode 100644
index 0000000..7e30901
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/NetworkCodeQualifier.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.configurations;
+
+import com.android.ide.eclipse.editors.IconFactory;
+
+import org.eclipse.swt.graphics.Image;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Resource Qualifier for Mobile Network Code Pixel Density.
+ */
+public final class NetworkCodeQualifier extends ResourceQualifier {
+ /** Default pixel density value. This means the property is not set. */
+ private final static int DEFAULT_CODE = -1;
+
+ private final static Pattern sNetworkCodePattern = Pattern.compile("^mnc(\\d{1,3})$"); //$NON-NLS-1$
+
+ private int mCode = DEFAULT_CODE;
+
+ public final static String NAME = "Mobile Network Code";
+
+ /**
+ * Creates and returns a qualifier from the given folder segment. If the segment is incorrect,
+ * <code>null</code> is returned.
+ * @param segment the folder segment from which to create a qualifier.
+ * @return a new {@link CountryCodeQualifier} object or <code>null</code>
+ */
+ public static NetworkCodeQualifier getQualifier(String segment) {
+ Matcher m = sNetworkCodePattern.matcher(segment);
+ if (m.matches()) {
+ String v = m.group(1);
+
+ int code = -1;
+ try {
+ code = Integer.parseInt(v);
+ } catch (NumberFormatException e) {
+ // looks like the string we extracted wasn't a valid number.
+ return null;
+ }
+
+ NetworkCodeQualifier qualifier = new NetworkCodeQualifier();
+ qualifier.mCode = code;
+ return qualifier;
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the folder name segment for the given value. This is equivalent to calling
+ * {@link #toString()} on a {@link NetworkCodeQualifier} object.
+ * @param code the value of the qualifier, as returned by {@link #getCode()}.
+ */
+ public static String getFolderSegment(int code) {
+ if (code != DEFAULT_CODE && code >= 1 && code <= 999) { // code is 1-3 digit.
+ return String.format("mnc%1$d", code); //$NON-NLS-1$
+ }
+
+ return ""; //$NON-NLS-1$
+ }
+
+ public int getCode() {
+ return mCode;
+ }
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+
+ @Override
+ public String getShortName() {
+ return "Network Code";
+ }
+
+ @Override
+ public Image getIcon() {
+ return IconFactory.getInstance().getIcon("mnc"); //$NON-NLS-1$
+ }
+
+ @Override
+ public boolean isValid() {
+ return mCode != DEFAULT_CODE;
+ }
+
+ @Override
+ public boolean checkAndSet(String value, FolderConfiguration config) {
+ Matcher m = sNetworkCodePattern.matcher(value);
+ if (m.matches()) {
+ String v = m.group(1);
+
+ int code = -1;
+ try {
+ code = Integer.parseInt(v);
+ } catch (NumberFormatException e) {
+ // looks like the string we extracted wasn't a valid number.
+ return false;
+ }
+
+ NetworkCodeQualifier qualifier = new NetworkCodeQualifier();
+ qualifier.mCode = code;
+ config.setNetworkCodeQualifier(qualifier);
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean equals(Object qualifier) {
+ if (qualifier instanceof NetworkCodeQualifier) {
+ return mCode == ((NetworkCodeQualifier)qualifier).mCode;
+ }
+
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return mCode;
+ }
+
+ /**
+ * Returns the string used to represent this qualifier in the folder name.
+ */
+ @Override
+ public String toString() {
+ return getFolderSegment(mCode);
+ }
+
+ @Override
+ public String getStringValue() {
+ if (mCode != DEFAULT_CODE) {
+ return String.format("MNC %1$d", mCode);
+ }
+
+ return ""; //$NON-NLS-1$
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/PixelDensityQualifier.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/PixelDensityQualifier.java
new file mode 100644
index 0000000..c47bb83
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/PixelDensityQualifier.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.configurations;
+
+import com.android.ide.eclipse.editors.IconFactory;
+
+import org.eclipse.swt.graphics.Image;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Resource Qualifier for Screen Pixel Density.
+ */
+public final class PixelDensityQualifier extends ResourceQualifier {
+ /** Default pixel density value. This means the property is not set. */
+ private final static int DEFAULT_DENSITY = -1;
+
+ private final static Pattern sPixelDensityPattern = Pattern.compile("^(\\d+)dpi$");//$NON-NLS-1$
+
+ public static final String NAME = "Pixel Density";
+
+ private int mValue = DEFAULT_DENSITY;
+
+ /**
+ * Creates and returns a qualifier from the given folder segment. If the segment is incorrect,
+ * <code>null</code> is returned.
+ * @param folderSegment the folder segment from which to create a qualifier.
+ * @return a new {@link CountryCodeQualifier} object or <code>null</code>
+ */
+ public static PixelDensityQualifier getQualifier(String folderSegment) {
+ Matcher m = sPixelDensityPattern.matcher(folderSegment);
+ if (m.matches()) {
+ String v = m.group(1);
+
+ int density = -1;
+ try {
+ density = Integer.parseInt(v);
+ } catch (NumberFormatException e) {
+ // looks like the string we extracted wasn't a valid number.
+ return null;
+ }
+
+ PixelDensityQualifier qualifier = new PixelDensityQualifier();
+ qualifier.mValue = density;
+
+ return qualifier;
+ }
+ return null;
+ }
+
+ /**
+ * Returns the folder name segment for the given value. This is equivalent to calling
+ * {@link #toString()} on a {@link NetworkCodeQualifier} object.
+ * @param value the value of the qualifier, as returned by {@link #getValue()}.
+ */
+ public static String getFolderSegment(int value) {
+ if (value != DEFAULT_DENSITY) {
+ return String.format("%1$ddpi", value); //$NON-NLS-1$
+ }
+
+ return ""; //$NON-NLS-1$
+ }
+
+ public int getValue() {
+ return mValue;
+ }
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+
+ @Override
+ public String getShortName() {
+ return NAME;
+ }
+
+ @Override
+ public Image getIcon() {
+ return IconFactory.getInstance().getIcon("dpi"); //$NON-NLS-1$
+ }
+
+ @Override
+ public boolean isValid() {
+ return mValue != DEFAULT_DENSITY;
+ }
+
+ @Override
+ public boolean checkAndSet(String value, FolderConfiguration config) {
+ PixelDensityQualifier qualifier = getQualifier(value);
+ if (qualifier != null) {
+ config.setPixelDensityQualifier(qualifier);
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean equals(Object qualifier) {
+ if (qualifier instanceof PixelDensityQualifier) {
+ return mValue == ((PixelDensityQualifier)qualifier).mValue;
+ }
+
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return mValue;
+ }
+
+ /**
+ * Returns the string used to represent this qualifier in the folder name.
+ */
+ @Override
+ public String toString() {
+ return getFolderSegment(mValue);
+ }
+
+ @Override
+ public String getStringValue() {
+ if (mValue != DEFAULT_DENSITY) {
+ return String.format("%1$d dpi", mValue);
+ }
+
+ return ""; //$NON-NLS-1$
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/RegionQualifier.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/RegionQualifier.java
new file mode 100644
index 0000000..dc4d5fa
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/RegionQualifier.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.configurations;
+
+import com.android.ide.eclipse.editors.IconFactory;
+
+import org.eclipse.swt.graphics.Image;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Resource Qualifier for Region.
+ */
+public final class RegionQualifier extends ResourceQualifier {
+ private final static Pattern sRegionPattern = Pattern.compile("^r([A-Z]{2})$"); //$NON-NLS-1$
+
+ public static final String NAME = "Region";
+
+ private String mValue;
+
+ /**
+ * Creates and returns a qualifier from the given folder segment. If the segment is incorrect,
+ * <code>null</code> is returned.
+ * @param segment the folder segment from which to create a qualifier.
+ * @return a new {@link RegionQualifier} object or <code>null</code>
+ */
+ public static RegionQualifier getQualifier(String segment) {
+ Matcher m = sRegionPattern.matcher(segment);
+ if (m.matches()) {
+ RegionQualifier qualifier = new RegionQualifier();
+ qualifier.mValue = m.group(1);
+
+ return qualifier;
+ }
+ return null;
+ }
+
+ /**
+ * Returns the folder name segment for the given value. This is equivalent to calling
+ * {@link #toString()} on a {@link RegionQualifier} object.
+ * @param value the value of the qualifier, as returned by {@link #getValue()}.
+ */
+ public static String getFolderSegment(String value) {
+ if (value != null) {
+ String segment = "r" + value.toUpperCase(); //$NON-NLS-1$
+ if (sRegionPattern.matcher(segment).matches()) {
+ return segment;
+ }
+ }
+
+ return ""; //$NON-NLS-1$
+ }
+
+ public String getValue() {
+ if (mValue != null) {
+ return mValue;
+ }
+
+ return ""; //$NON-NLS-1$
+ }
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+
+ @Override
+ public String getShortName() {
+ return NAME;
+ }
+
+ @Override
+ public Image getIcon() {
+ return IconFactory.getInstance().getIcon("region"); //$NON-NLS-1$
+ }
+
+ @Override
+ public boolean isValid() {
+ return mValue != null;
+ }
+
+ @Override
+ public boolean checkAndSet(String value, FolderConfiguration config) {
+ RegionQualifier qualifier = getQualifier(value);
+ if (qualifier != null) {
+ config.setRegionQualifier(qualifier);
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean equals(Object qualifier) {
+ if (qualifier instanceof RegionQualifier) {
+ if (mValue == null) {
+ return ((RegionQualifier)qualifier).mValue == null;
+ }
+ return mValue.equals(((RegionQualifier)qualifier).mValue);
+ }
+
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ if (mValue != null) {
+ return mValue.hashCode();
+ }
+
+ return 0;
+ }
+
+ /**
+ * Returns the string used to represent this qualifier in the folder name.
+ */
+ @Override
+ public String toString() {
+ return getFolderSegment(mValue);
+ }
+
+ @Override
+ public String getStringValue() {
+ if (mValue != null) {
+ return mValue;
+ }
+
+ return ""; //$NON-NLS-1$
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/ResourceQualifier.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/ResourceQualifier.java
new file mode 100644
index 0000000..0257afa
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/ResourceQualifier.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.configurations;
+
+import org.eclipse.swt.graphics.Image;
+
+/**
+ * Base class for resource qualifiers.
+ * <p/>The resource qualifier classes are designed as immutable.
+ */
+public abstract class ResourceQualifier implements Comparable<ResourceQualifier> {
+
+ /**
+ * Returns the human readable name of the qualifier.
+ */
+ public abstract String getName();
+
+ /**
+ * Returns a shorter human readable name for the qualifier.
+ * @see #getName()
+ */
+ public abstract String getShortName();
+
+ /**
+ * Returns the icon for the qualifier.
+ */
+ public abstract Image getIcon();
+
+ /**
+ * Returns whether the qualifier has a valid filter value.
+ */
+ public abstract boolean isValid();
+
+ /**
+ * Check if the value is valid for this qualifier, and if so sets the value
+ * into a Folder Configuration.
+ * @param value The value to check and set. Must not be null.
+ * @param config The folder configuration to receive the value. Must not be null.
+ * @return true if the value was valid and was set.
+ */
+ public abstract boolean checkAndSet(String value, FolderConfiguration config);
+
+ /**
+ * Returns a string formated to be used in a folder name.
+ * <p/>This is declared as abstract to force children classes to implement it.
+ */
+ @Override
+ public abstract String toString();
+
+ /**
+ * Returns a string formatted for display purpose.
+ */
+ public abstract String getStringValue();
+
+ /**
+ * Returns <code>true</code> if both objects are equal.
+ * <p/>This is declared as abstract to force children classes to implement it.
+ */
+ @Override
+ public abstract boolean equals(Object object);
+
+ /**
+ * Returns a hash code value for the object.
+ * <p/>This is declared as abstract to force children classes to implement it.
+ */
+ @Override
+ public abstract int hashCode();
+
+ public final int compareTo(ResourceQualifier o) {
+ return toString().compareTo(o.toString());
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/ScreenDimensionQualifier.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/ScreenDimensionQualifier.java
new file mode 100644
index 0000000..a2cc789
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/ScreenDimensionQualifier.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.configurations;
+
+import com.android.ide.eclipse.editors.IconFactory;
+
+import org.eclipse.swt.graphics.Image;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Resource Qualifier for Screen Dimension.
+ */
+public final class ScreenDimensionQualifier extends ResourceQualifier {
+ /** Default screen size value. This means the property is not set */
+ final static int DEFAULT_SIZE = -1;
+
+ private final static Pattern sDimensionPattern = Pattern.compile(
+ "^(\\d+)x(\\d+)$"); //$NON-NLS-1$
+
+ public static final String NAME = "Screen Dimension";
+
+ /** Screen size 1 value. This is not size X or Y because the folder name always
+ * contains the biggest size first. So if the qualifier is 400x200, size 1 will always be
+ * 400 but that'll be X in landscape and Y in portrait.
+ * Default value is <code>DEFAULT_SIZE</code> */
+ private int mValue1 = DEFAULT_SIZE;
+
+ /** Screen size 2 value. This is not size X or Y because the folder name always
+ * contains the biggest size first. So if the qualifier is 400x200, size 2 will always be
+ * 200 but that'll be Y in landscape and X in portrait.
+ * Default value is <code>DEFAULT_SIZE</code> */
+ private int mValue2 = DEFAULT_SIZE;
+
+ public int getValue1() {
+ return mValue1;
+ }
+
+ public int getValue2() {
+ return mValue2;
+ }
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+
+ @Override
+ public String getShortName() {
+ return "Dimension";
+ }
+
+ @Override
+ public Image getIcon() {
+ return IconFactory.getInstance().getIcon("dimension"); //$NON-NLS-1$
+ }
+
+ @Override
+ public boolean isValid() {
+ return mValue1 != DEFAULT_SIZE && mValue2 != DEFAULT_SIZE;
+ }
+
+ @Override
+ public boolean checkAndSet(String value, FolderConfiguration config) {
+ Matcher m = sDimensionPattern.matcher(value);
+ if (m.matches()) {
+ String d1 = m.group(1);
+ String d2 = m.group(2);
+
+ ScreenDimensionQualifier qualifier = getQualifier(d1, d2);
+ if (qualifier != null) {
+ config.setScreenDimensionQualifier(qualifier);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean equals(Object qualifier) {
+ if (qualifier instanceof ScreenDimensionQualifier) {
+ ScreenDimensionQualifier q = (ScreenDimensionQualifier)qualifier;
+ return (mValue1 == q.mValue1 && mValue2 == q.mValue2);
+ }
+
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return toString().hashCode();
+ }
+
+ public static ScreenDimensionQualifier getQualifier(String size1, String size2) {
+ try {
+ int s1 = Integer.parseInt(size1);
+ int s2 = Integer.parseInt(size2);
+
+ ScreenDimensionQualifier qualifier = new ScreenDimensionQualifier();
+
+ if (s1 > s2) {
+ qualifier.mValue1 = s1;
+ qualifier.mValue2 = s2;
+ } else {
+ qualifier.mValue1 = s2;
+ qualifier.mValue2 = s1;
+ }
+
+ return qualifier;
+ } catch (NumberFormatException e) {
+ // looks like the string we extracted wasn't a valid number.
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the string used to represent this qualifier in the folder name.
+ */
+ @Override
+ public String toString() {
+ return String.format("%1$dx%2$d", mValue1, mValue2); //$NON-NLS-1$
+ }
+
+ @Override
+ public String getStringValue() {
+ if (mValue1 != -1 && mValue2 != -1) {
+ return String.format("%1$dx%2$d", mValue1, mValue2);
+ }
+
+ return ""; //$NON-NLS-1$
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/ScreenOrientationQualifier.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/ScreenOrientationQualifier.java
new file mode 100644
index 0000000..e30930f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/ScreenOrientationQualifier.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.configurations;
+
+import com.android.ide.eclipse.editors.IconFactory;
+
+import org.eclipse.swt.graphics.Image;
+
+/**
+ * Resource Qualifier for Screen Orientation.
+ */
+public final class ScreenOrientationQualifier extends ResourceQualifier {
+
+ public static final String NAME = "Screen Orientation";
+
+ private ScreenOrientation mValue = null;
+
+ /**
+ * Screen Orientation enum.
+ */
+ public static enum ScreenOrientation {
+ PORTRAIT("port", "Portrait"), //$NON-NLS-1$
+ LANDSCAPE("land", "Landscape"), //$NON-NLS-1$
+ SQUARE("square", "Square"); //$NON-NLS-1$
+
+ private String mValue;
+ private String mDisplayValue;
+
+ private ScreenOrientation(String value, String displayValue) {
+ mValue = value;
+ mDisplayValue = displayValue;
+ }
+
+ /**
+ * Returns the enum for matching the provided qualifier value.
+ * @param value The qualifier value.
+ * @return the enum for the qualifier value or null if no matching was found.
+ */
+ static ScreenOrientation getEnum(String value) {
+ for (ScreenOrientation orient : values()) {
+ if (orient.mValue.equals(value)) {
+ return orient;
+ }
+ }
+
+ return null;
+ }
+
+ public String getValue() {
+ return mValue;
+ }
+
+ public String getDisplayValue() {
+ return mDisplayValue;
+ }
+
+ public static int getIndex(ScreenOrientation orientation) {
+ int i = 0;
+ for (ScreenOrientation orient : values()) {
+ if (orient == orientation) {
+ return i;
+ }
+
+ i++;
+ }
+
+ return -1;
+ }
+
+ public static ScreenOrientation getByIndex(int index) {
+ int i = 0;
+ for (ScreenOrientation orient : values()) {
+ if (i == index) {
+ return orient;
+ }
+ i++;
+ }
+
+ return null;
+ }
+ }
+
+ public ScreenOrientationQualifier() {
+ }
+
+ public ScreenOrientationQualifier(ScreenOrientation value) {
+ mValue = value;
+ }
+
+ public ScreenOrientation getValue() {
+ return mValue;
+ }
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+
+ @Override
+ public String getShortName() {
+ return "Orientation";
+ }
+
+ @Override
+ public Image getIcon() {
+ return IconFactory.getInstance().getIcon("orientation"); //$NON-NLS-1$
+ }
+
+ @Override
+ public boolean isValid() {
+ return mValue != null;
+ }
+
+ @Override
+ public boolean checkAndSet(String value, FolderConfiguration config) {
+ ScreenOrientation orientation = ScreenOrientation.getEnum(value);
+ if (orientation != null) {
+ ScreenOrientationQualifier qualifier = new ScreenOrientationQualifier(orientation);
+ config.setScreenOrientationQualifier(qualifier);
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean equals(Object qualifier) {
+ if (qualifier instanceof ScreenOrientationQualifier) {
+ return mValue == ((ScreenOrientationQualifier)qualifier).mValue;
+ }
+
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ if (mValue != null) {
+ return mValue.hashCode();
+ }
+
+ return 0;
+ }
+
+ /**
+ * Returns the string used to represent this qualifier in the folder name.
+ */
+ @Override
+ public String toString() {
+ if (mValue != null) {
+ return mValue.getValue();
+ }
+
+ return ""; //$NON-NLS-1$
+ }
+
+ @Override
+ public String getStringValue() {
+ if (mValue != null) {
+ return mValue.getDisplayValue();
+ }
+
+ return ""; //$NON-NLS-1$
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/TextInputMethodQualifier.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/TextInputMethodQualifier.java
new file mode 100644
index 0000000..de40138
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/TextInputMethodQualifier.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.configurations;
+
+import com.android.ide.eclipse.editors.IconFactory;
+
+import org.eclipse.swt.graphics.Image;
+
+
+
+
+/**
+ * Resource Qualifier for Text Input Method.
+ */
+public final class TextInputMethodQualifier extends ResourceQualifier {
+
+ public static final String NAME = "Text Input Method";
+
+ private TextInputMethod mValue;
+
+ /**
+ * Screen Orientation enum.
+ */
+ public static enum TextInputMethod {
+ NOKEY("nokeys", "No Keys"), //$NON-NLS-1$
+ QWERTY("qwerty", "Qwerty"), //$NON-NLS-1$
+ TWELVEKEYS("12key", "12 Key"); //$NON-NLS-1$
+
+ private String mValue;
+ private String mDisplayValue;
+
+ private TextInputMethod(String value, String displayValue) {
+ mValue = value;
+ mDisplayValue = displayValue;
+ }
+
+ /**
+ * Returns the enum for matching the provided qualifier value.
+ * @param value The qualifier value.
+ * @return the enum for the qualifier value or null if no matching was found.
+ */
+ static TextInputMethod getEnum(String value) {
+ for (TextInputMethod orient : values()) {
+ if (orient.mValue.equals(value)) {
+ return orient;
+ }
+ }
+
+ return null;
+ }
+
+ public String getValue() {
+ return mValue;
+ }
+
+ public String getDisplayValue() {
+ return mDisplayValue;
+ }
+
+ public static int getIndex(TextInputMethod value) {
+ int i = 0;
+ for (TextInputMethod input : values()) {
+ if (value == input) {
+ return i;
+ }
+
+ i++;
+ }
+
+ return -1;
+ }
+
+ public static TextInputMethod getByIndex(int index) {
+ int i = 0;
+ for (TextInputMethod value : values()) {
+ if (i == index) {
+ return value;
+ }
+ i++;
+ }
+ return null;
+ }
+ }
+
+ public TextInputMethodQualifier() {
+ // pass
+ }
+
+ public TextInputMethodQualifier(TextInputMethod value) {
+ mValue = value;
+ }
+
+ public TextInputMethod getValue() {
+ return mValue;
+ }
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+
+ @Override
+ public String getShortName() {
+ return "Text Input";
+ }
+
+ @Override
+ public Image getIcon() {
+ return IconFactory.getInstance().getIcon("text_input"); //$NON-NLS-1$
+ }
+
+ @Override
+ public boolean isValid() {
+ return mValue != null;
+ }
+
+ @Override
+ public boolean checkAndSet(String value, FolderConfiguration config) {
+ TextInputMethod method = TextInputMethod.getEnum(value);
+ if (method != null) {
+ TextInputMethodQualifier qualifier = new TextInputMethodQualifier();
+ qualifier.mValue = method;
+ config.setTextInputMethodQualifier(qualifier);
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean equals(Object qualifier) {
+ if (qualifier instanceof TextInputMethodQualifier) {
+ return mValue == ((TextInputMethodQualifier)qualifier).mValue;
+ }
+
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ if (mValue != null) {
+ return mValue.hashCode();
+ }
+
+ return 0;
+ }
+
+ /**
+ * Returns the string used to represent this qualifier in the folder name.
+ */
+ @Override
+ public String toString() {
+ if (mValue != null) {
+ return mValue.getValue();
+ }
+
+ return ""; //$NON-NLS-1$
+ }
+
+ @Override
+ public String getStringValue() {
+ if (mValue != null) {
+ return mValue.getDisplayValue();
+ }
+
+ return ""; //$NON-NLS-1$
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/TouchScreenQualifier.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/TouchScreenQualifier.java
new file mode 100644
index 0000000..2390e2c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/configurations/TouchScreenQualifier.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.configurations;
+
+import com.android.ide.eclipse.editors.IconFactory;
+
+import org.eclipse.swt.graphics.Image;
+
+
+/**
+ * Resource Qualifier for Touch Screen type.
+ */
+public final class TouchScreenQualifier extends ResourceQualifier {
+
+ public static final String NAME = "Touch Screen";
+
+ private TouchScreenType mValue;
+
+ /**
+ * Screen Orientation enum.
+ */
+ public static enum TouchScreenType {
+ NOTOUCH("notouch", "No Touch"), //$NON-NLS-1$
+ STYLUS("stylus", "Stylus"), //$NON-NLS-1$
+ FINGER("finger", "Finger"); //$NON-NLS-1$
+
+ private String mValue;
+ private String mDisplayValue;
+
+ private TouchScreenType(String value, String displayValue) {
+ mValue = value;
+ mDisplayValue = displayValue;
+ }
+
+ /**
+ * Returns the enum for matching the provided qualifier value.
+ * @param value The qualifier value.
+ * @return the enum for the qualifier value or null if no matching was found.
+ */
+ static TouchScreenType getEnum(String value) {
+ for (TouchScreenType orient : values()) {
+ if (orient.mValue.equals(value)) {
+ return orient;
+ }
+ }
+
+ return null;
+ }
+
+ public String getValue() {
+ return mValue;
+ }
+
+ public String getDisplayValue() {
+ return mDisplayValue;
+ }
+
+ public static int getIndex(TouchScreenType touch) {
+ int i = 0;
+ for (TouchScreenType t : values()) {
+ if (t == touch) {
+ return i;
+ }
+
+ i++;
+ }
+
+ return -1;
+ }
+
+ public static TouchScreenType getByIndex(int index) {
+ int i = 0;
+ for (TouchScreenType value : values()) {
+ if (i == index) {
+ return value;
+ }
+ i++;
+ }
+
+ return null;
+ }
+ }
+
+ public TouchScreenQualifier() {
+ // pass
+ }
+
+ public TouchScreenQualifier(TouchScreenType touchValue) {
+ mValue = touchValue;
+ }
+
+ public TouchScreenType getValue() {
+ return mValue;
+ }
+
+ @Override
+ public String getName() {
+ return NAME;
+ }
+
+ @Override
+ public String getShortName() {
+ return NAME;
+ }
+
+ @Override
+ public Image getIcon() {
+ return IconFactory.getInstance().getIcon("touch"); //$NON-NLS-1$
+ }
+
+ @Override
+ public boolean isValid() {
+ return mValue != null;
+ }
+
+ @Override
+ public boolean checkAndSet(String value, FolderConfiguration config) {
+ TouchScreenType type = TouchScreenType.getEnum(value);
+ if (type != null) {
+ TouchScreenQualifier qualifier = new TouchScreenQualifier();
+ qualifier.mValue = type;
+ config.setTouchTypeQualifier(qualifier);
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean equals(Object qualifier) {
+ if (qualifier instanceof TouchScreenQualifier) {
+ return mValue == ((TouchScreenQualifier)qualifier).mValue;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ if (mValue != null) {
+ return mValue.hashCode();
+ }
+
+ return 0;
+ }
+
+ /**
+ * Returns the string used to represent this qualifier in the folder name.
+ */
+ @Override
+ public String toString() {
+ if (mValue != null) {
+ return mValue.getValue();
+ }
+
+ return ""; //$NON-NLS-1$
+ }
+
+ @Override
+ public String getStringValue() {
+ if (mValue != null) {
+ return mValue.getDisplayValue();
+ }
+
+ return ""; //$NON-NLS-1$
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/descriptors/ColorValueDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/descriptors/ColorValueDescriptor.java
new file mode 100644
index 0000000..92288ba
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/descriptors/ColorValueDescriptor.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.descriptors;
+
+import com.android.ide.eclipse.editors.descriptors.TextValueDescriptor;
+import com.android.ide.eclipse.editors.resources.uimodel.UiColorValueNode;
+import com.android.ide.eclipse.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.editors.uimodel.UiResourceAttributeNode;
+
+/**
+ * Describes a Color XML element value displayed by an {@link UiColorValueNode}.
+ */
+public final class ColorValueDescriptor extends TextValueDescriptor {
+
+ public ColorValueDescriptor(String uiName, String tooltip) {
+ super(uiName, tooltip);
+ }
+
+ /**
+ * @return A new {@link UiResourceAttributeNode} linked to this theme descriptor.
+ */
+ @Override
+ public UiAttributeNode createUiNode(UiElementNode uiParent) {
+ return new UiColorValueNode(this, uiParent);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/descriptors/ItemElementDescriptor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/descriptors/ItemElementDescriptor.java
new file mode 100644
index 0000000..bf83d52
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/descriptors/ItemElementDescriptor.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.descriptors;
+
+import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.editors.resources.uimodel.UiItemElementNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+/**
+ * {@link ItemElementDescriptor} is a special version of {@link ElementDescriptor} that
+ * uses a specialized {@link UiItemElementNode} for display.
+ */
+public class ItemElementDescriptor extends ElementDescriptor {
+
+ /**
+ * Constructs a new {@link ItemElementDescriptor} based on its XML name, UI name,
+ * tooltip, SDK url, attributes list, children list and mandatory.
+ *
+ * @param xml_name The XML element node name. Case sensitive.
+ * @param ui_name The XML element name for the user interface, typically capitalized.
+ * @param tooltip An optional tooltip. Can be null or empty.
+ * @param sdk_url An optional SKD URL. Can be null or empty.
+ * @param attributes The list of allowed attributes. Can be null or empty.
+ * @param children The list of allowed children. Can be null or empty.
+ * @param mandatory Whether this node must always exist (even for empty models). A mandatory
+ * UI node is never deleted and it may lack an actual XML node attached. A non-mandatory
+ * UI node MUST have an XML node attached and it will cease to exist when the XML node
+ * ceases to exist.
+ */
+ public ItemElementDescriptor(String xml_name, String ui_name,
+ String tooltip, String sdk_url, AttributeDescriptor[] attributes,
+ ElementDescriptor[] children, boolean mandatory) {
+ super(xml_name, ui_name, tooltip, sdk_url, attributes, children, mandatory);
+ }
+
+ @Override
+ public UiElementNode createUiNode() {
+ return new UiItemElementNode(this);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/descriptors/ResourcesDescriptors.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/descriptors/ResourcesDescriptors.java
new file mode 100644
index 0000000..1075897
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/descriptors/ResourcesDescriptors.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.descriptors;
+
+import com.android.ide.eclipse.common.resources.ResourceType;
+import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.editors.descriptors.FlagAttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.IDescriptorProvider;
+import com.android.ide.eclipse.editors.descriptors.ListAttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.TextValueDescriptor;
+
+
+/**
+ * Complete description of the structure for resources XML files (under res/values/)
+ */
+public class ResourcesDescriptors implements IDescriptorProvider {
+
+ // Public attributes names, attributes descriptors and elements descriptors
+
+ public static final String ROOT_ELEMENT = "resources"; //$NON-NLS-1$
+
+ public static final String NAME_ATTR = "name"; //$NON-NLS-1$
+ public static final String TYPE_ATTR = "type"; //$NON-NLS-1$
+
+ private static final ResourcesDescriptors sThis = new ResourcesDescriptors();
+
+ /** The {@link ElementDescriptor} for the root Resources element. */
+ public final ElementDescriptor mResourcesElement;
+
+ public static ResourcesDescriptors getInstance() {
+ return sThis;
+ }
+
+ /*
+ * @see com.android.ide.eclipse.editors.descriptors.IDescriptorProvider#getRootElementDescriptors()
+ */
+ public ElementDescriptor[] getRootElementDescriptors() {
+ return new ElementDescriptor[] { mResourcesElement };
+ }
+
+ public ElementDescriptor getDescriptor() {
+ return mResourcesElement;
+ }
+
+ public ElementDescriptor getElementDescriptor() {
+ return mResourcesElement;
+ }
+
+ private ResourcesDescriptors() {
+
+ // Common attributes used in many placed
+
+ // Elements
+
+ ElementDescriptor color_element = new ElementDescriptor(
+ "color", //$NON-NLS-1$
+ "Color",
+ "A @color@ value specifies an RGB value with an alpha channel, which can be used in various places such as specifying a solid color for a Drawable or the color to use for text. It always begins with a # character and then is followed by the alpha-red-green-blue information in one of the following formats: #RGB, #ARGB, #RRGGBB or #AARRGGBB.",
+ "http://code.google.com/android/reference/available-resources.html#colorvals", //$NON-NLS-1$
+ new AttributeDescriptor[] {
+ new TextAttributeDescriptor(NAME_ATTR,
+ "Name*",
+ null /* nsUri */,
+ "The mandatory name used in referring to this color."),
+ new ColorValueDescriptor(
+ "Value*",
+ "A mandatory color value.")
+ },
+ null, // no child nodes
+ false /* not mandatory */);
+
+ ElementDescriptor string_element = new ElementDescriptor(
+ "string", //$NON-NLS-1$
+ "String",
+ "@Strings@, with optional simple formatting, can be stored and retrieved as resources. You can add formatting to your string by using three standard HTML tags: b, i, and u. If you use an apostrophe or a quote in your string, you must either escape it or enclose the whole string in the other kind of enclosing quotes.",
+ "http://code.google.com/android/reference/available-resources.html#stringresources", //$NON-NLS-1$
+ new AttributeDescriptor[] {
+ new TextAttributeDescriptor(NAME_ATTR,
+ "Name*",
+ null /* nsUri */,
+ "The mandatory name used in referring to this string."),
+ new TextValueDescriptor(
+ "Value*",
+ "A mandatory string value.")
+ },
+ null, // no child nodes
+ false /* not mandatory */);
+
+ ElementDescriptor item_element = new ItemElementDescriptor(
+ "item", //$NON-NLS-1$
+ "Item",
+ null, // TODO find javadoc
+ null, // TODO find link to javadoc
+ new AttributeDescriptor[] {
+ new TextAttributeDescriptor(NAME_ATTR,
+ "Name*",
+ null /* nsUri */,
+ "The mandatory name used in referring to this resource."),
+ new ListAttributeDescriptor(TYPE_ATTR,
+ "Type*",
+ null /* nsUri */,
+ "The mandatory type of this resource.",
+ ResourceType.getNames()
+ ),
+ new FlagAttributeDescriptor("format",
+ "Format",
+ null /* nsUri */,
+ "The optional format of this resource.",
+ new String[] {
+ "boolean", //$NON-NLS-1$
+ "color", //$NON-NLS-1$
+ "dimension", //$NON-NLS-1$
+ "float", //$NON-NLS-1$
+ "fraction", //$NON-NLS-1$
+ "integer", //$NON-NLS-1$
+ "reference", //$NON-NLS-1$
+ "string" //$NON-NLS-1$
+ }),
+ new TextValueDescriptor(
+ "Value",
+ "A standard string, hex color value, or reference to any other resource type.")
+ },
+ null, // no child nodes
+ false /* not mandatory */);
+
+ ElementDescriptor drawable_element = new ElementDescriptor(
+ "drawable", //$NON-NLS-1$
+ "Drawable",
+ "A @drawable@ defines a rectangle of color. Android accepts color values written in various web-style formats -- a hexadecimal constant in any of the following forms: #RGB, #ARGB, #RRGGBB, #AARRGGBB. Zero in the alpha channel means transparent. The default value is opaque.",
+ "http://code.google.com/android/reference/available-resources.html#colordrawableresources", //$NON-NLS-1$
+ new AttributeDescriptor[] {
+ new TextAttributeDescriptor(NAME_ATTR,
+ "Name*",
+ null /* nsUri */,
+ "The mandatory name used in referring to this drawable."),
+ new TextValueDescriptor(
+ "Value*",
+ "A mandatory color value in the form #RGB, #ARGB, #RRGGBB or #AARRGGBB.")
+ },
+ null, // no child nodes
+ false /* not mandatory */);
+
+ ElementDescriptor dimen_element = new ElementDescriptor(
+ "dimen", //$NON-NLS-1$
+ "Dimension",
+ "You can create common dimensions to use for various screen elements by defining @dimension@ values in XML. A dimension resource is a number followed by a unit of measurement. Supported units are px (pixels), in (inches), mm (millimeters), pt (points at 72 DPI), dp (density-independent pixels) and sp (scale-independent pixels)",
+ "http://code.google.com/android/reference/available-resources.html#dimension", //$NON-NLS-1$
+ new AttributeDescriptor[] {
+ new TextAttributeDescriptor(NAME_ATTR,
+ "Name*",
+ null /* nsUri */,
+ "The mandatory name used in referring to this dimension."),
+ new TextValueDescriptor(
+ "Value*",
+ "A mandatory dimension value is a number followed by a unit of measurement. For example: 10px, 2in, 5sp.")
+ },
+ null, // no child nodes
+ false /* not mandatory */);
+
+ ElementDescriptor style_element = new ElementDescriptor(
+ "style", //$NON-NLS-1$
+ "Style/Theme",
+ "Both @styles and themes@ are defined in a style block containing one or more string or numerical values (typically color values), or references to other resources (drawables and so on).",
+ "http://code.google.com/android/reference/available-resources.html#stylesandthemes", //$NON-NLS-1$
+ new AttributeDescriptor[] {
+ new TextAttributeDescriptor(NAME_ATTR,
+ "Name*",
+ null /* nsUri */,
+ "The mandatory name used in referring to this theme."),
+ new TextAttributeDescriptor("parent", // $NON-NLS-1$
+ "Parent",
+ null /* nsUri */,
+ "An optional parent theme. All values from the specified theme will be inherited into this theme. Any values with identical names that you specify will override inherited values."),
+ },
+ new ElementDescriptor[] {
+ new ElementDescriptor(
+ "item", //$NON-NLS-1$
+ "Item",
+ "A value to use in this @theme@. It can be a standard string, a hex color value, or a reference to any other resource type.",
+ "http://code.google.com/android/reference/available-resources.html#stylesandthemes", //$NON-NLS-1$
+ new AttributeDescriptor[] {
+ new TextAttributeDescriptor(NAME_ATTR,
+ "Name*",
+ null /* nsUri */,
+ "The mandatory name used in referring to this item."),
+ new TextValueDescriptor(
+ "Value*",
+ "A mandatory standard string, hex color value, or reference to any other resource type.")
+ },
+ null, // no child nodes
+ false /* not mandatory */)
+ },
+ false /* not mandatory */);
+
+ ElementDescriptor string_array_element = new ElementDescriptor(
+ "string-array", //$NON-NLS-1$
+ "String Array",
+ "An array of strings. Strings are added as underlying item elements to the array.",
+ null, // tooltips
+ new AttributeDescriptor[] {
+ new TextAttributeDescriptor(NAME_ATTR,
+ "Name*",
+ null /* nsUri */,
+ "The mandatory name used in referring to this string array."),
+ },
+ new ElementDescriptor[] {
+ new ElementDescriptor(
+ "item", //$NON-NLS-1$
+ "Item",
+ "A string value to use in this string array.",
+ null, // tooltip
+ new AttributeDescriptor[] {
+ new TextValueDescriptor(
+ "Value*",
+ "A mandatory string.")
+ },
+ null, // no child nodes
+ false /* not mandatory */)
+ },
+ false /* not mandatory */);
+
+ ElementDescriptor integer_array_element = new ElementDescriptor(
+ "integer-array", //$NON-NLS-1$
+ "Integer Array",
+ "An array of integers. Integers are added as underlying item elements to the array.",
+ null, // tooltips
+ new AttributeDescriptor[] {
+ new TextAttributeDescriptor(NAME_ATTR,
+ "Name*",
+ null /* nsUri */,
+ "The mandatory name used in referring to this integer array."),
+ },
+ new ElementDescriptor[] {
+ new ElementDescriptor(
+ "item", //$NON-NLS-1$
+ "Item",
+ "An integer value to use in this integer array.",
+ null, // tooltip
+ new AttributeDescriptor[] {
+ new TextValueDescriptor(
+ "Value*",
+ "A mandatory integer.")
+ },
+ null, // no child nodes
+ false /* not mandatory */)
+ },
+ false /* not mandatory */);
+
+ mResourcesElement = new ElementDescriptor(
+ ROOT_ELEMENT,
+ "Resources",
+ null,
+ "http://code.google.com/android/reference/available-resources.html", //$NON-NLS-1$
+ null, // no attributes
+ new ElementDescriptor[] {
+ string_element,
+ color_element,
+ dimen_element,
+ drawable_element,
+ style_element,
+ item_element,
+ string_array_element,
+ integer_array_element,
+ },
+ true /* mandatory */);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/explorer/ResourceExplorerView.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/explorer/ResourceExplorerView.java
new file mode 100644
index 0000000..d1d8891
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/explorer/ResourceExplorerView.java
@@ -0,0 +1,340 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.explorer;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.editors.resources.manager.ProjectResourceItem;
+import com.android.ide.eclipse.editors.resources.manager.ProjectResources;
+import com.android.ide.eclipse.editors.resources.manager.ResourceFile;
+import com.android.ide.eclipse.editors.resources.manager.ResourceManager;
+import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IResourceEventListener;
+import com.android.ide.eclipse.editors.wizards.ResourceContentProvider;
+import com.android.ide.eclipse.editors.wizards.ResourceLabelProvider;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IAdaptable;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IJavaProject;
+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.IStructuredSelection;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.ControlListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeColumn;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IFileEditorInput;
+import org.eclipse.ui.ISelectionListener;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchPart;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.ide.IDE;
+import org.eclipse.ui.part.ViewPart;
+
+import java.util.Iterator;
+
+/**
+ * Resource Explorer View.
+ * <p/>
+ * This contains a basic Tree view, and uses a TreeViewer to handle the data.
+ * <p/>
+ * The view listener to change in selection in the workbench, and update to show the resource
+ * of the project of the current selected item (either item in the package explorer, or of the
+ * current editor).
+ *
+ * @see ResourceContentProvider
+ */
+public class ResourceExplorerView extends ViewPart implements ISelectionListener,
+ IResourceEventListener {
+
+ // Note: keep using the obsolete AndroidConstants.EDITORS_NAMESPACE (which used
+ // to be the Editors Plugin ID) to keep existing preferences functional.
+ private final static String PREFS_COLUMN_RES =
+ AndroidConstants.EDITORS_NAMESPACE + "ResourceExplorer.Col1"; //$NON-NLS-1$
+ private final static String PREFS_COLUMN_2 =
+ AndroidConstants.EDITORS_NAMESPACE + "ResourceExplorer.Col2"; //$NON-NLS-1$
+
+ private Tree mTree;
+ private TreeViewer mTreeViewer;
+
+ private IProject mCurrentProject;
+
+ public ResourceExplorerView() {
+ }
+
+ @Override
+ public void createPartControl(Composite parent) {
+ mTree = new Tree(parent, SWT.SINGLE | SWT.VIRTUAL);
+ mTree.setLayoutData(new GridData(GridData.FILL_BOTH));
+ mTree.setHeaderVisible(true);
+ mTree.setLinesVisible(true);
+
+ final IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore();
+
+ // create 2 columns. The main one with the resources, and an "info" column.
+ createTreeColumn(mTree, "Resources", SWT.LEFT,
+ "abcdefghijklmnopqrstuvwxz", -1, PREFS_COLUMN_RES, store); //$NON-NLS-1$
+ createTreeColumn(mTree, "Info", SWT.LEFT,
+ "0123456789", -1, PREFS_COLUMN_2, store); //$NON-NLS-1$
+
+ // create the jface wrapper
+ mTreeViewer = new TreeViewer(mTree);
+
+ mTreeViewer.setContentProvider(new ResourceContentProvider(true /* fullLevels */));
+ mTreeViewer.setLabelProvider(new ResourceLabelProvider());
+
+ // listen to selection change in the workbench.
+ IWorkbenchPage page = getSite().getPage();
+
+ page.addSelectionListener(this);
+
+ // init with current selection
+ selectionChanged(getSite().getPart(), page.getSelection());
+
+ // add support for double click.
+ mTreeViewer.addDoubleClickListener(new IDoubleClickListener() {
+ public void doubleClick(DoubleClickEvent event) {
+ ISelection sel = event.getSelection();
+
+ if (sel instanceof IStructuredSelection) {
+ IStructuredSelection selection = (IStructuredSelection) sel;
+
+ if (selection.size() == 1) {
+ Object element = selection.getFirstElement();
+
+ // if it's a resourceFile, we directly open it.
+ if (element instanceof ResourceFile) {
+ try {
+ IDE.openEditor(getSite().getWorkbenchWindow().getActivePage(),
+ ((ResourceFile)element).getFile().getIFile());
+ } catch (PartInitException e) {
+ }
+ } else if (element instanceof ProjectResourceItem) {
+ // if it's a ResourceItem, we open the first file, but only if
+ // there's no alternate files.
+ ProjectResourceItem item = (ProjectResourceItem)element;
+
+ if (item.isEditableDirectly()) {
+ ResourceFile[] files = item.getSourceFileArray();
+ if (files[0] != null) {
+ try {
+ IDE.openEditor(
+ getSite().getWorkbenchWindow().getActivePage(),
+ files[0].getFile().getIFile());
+ } catch (PartInitException e) {
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ });
+
+ // set up the resource manager to send us resource change notification
+ AdtPlugin.getDefault().getResourceMonitor().addResourceEventListener(this);
+ }
+
+ @Override
+ public void dispose() {
+ AdtPlugin.getDefault().getResourceMonitor().removeResourceEventListener(this);
+
+ super.dispose();
+ }
+
+ @Override
+ public void setFocus() {
+ mTree.setFocus();
+ }
+
+ /**
+ * Processes a new selection.
+ */
+ public void selectionChanged(IWorkbenchPart part, ISelection selection) {
+ // first we test if the part is an editor.
+ if (part instanceof IEditorPart) {
+ // if it is, we check if it's a file editor.
+ IEditorInput input = ((IEditorPart)part).getEditorInput();
+
+ if (input instanceof IFileEditorInput) {
+ // from the file editor we can get the IFile object, and from it, the IProject.
+ IFile file = ((IFileEditorInput)input).getFile();
+
+ // get the file project
+ IProject project = file.getProject();
+
+ handleProjectSelection(project);
+ }
+ } else if (selection instanceof IStructuredSelection) {
+ // if it's not an editor, we look for structured selection.
+ for (Iterator<?> it = ((IStructuredSelection) selection).iterator();
+ it.hasNext();) {
+ Object element = it.next();
+ IProject project = null;
+
+ // if we are in the navigator or package explorer, the selection could contain a
+ // IResource object.
+ if (element instanceof IResource) {
+ project = ((IResource) element).getProject();
+ } else if (element instanceof IJavaElement) {
+ // if we are in the package explorer on a java element, we handle that too.
+ IJavaElement javaElement = (IJavaElement)element;
+ IJavaProject javaProject = javaElement.getJavaProject();
+ if (javaProject != null) {
+ project = javaProject.getProject();
+ }
+ } else if (element instanceof IAdaptable) {
+ // finally we try to get a project object from IAdaptable.
+ project = (IProject) ((IAdaptable) element)
+ .getAdapter(IProject.class);
+ }
+
+ // if we found a project, handle it, and return.
+ if (project != null) {
+ if (handleProjectSelection(project)) {
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Handles a project selection.
+ * @param project the new selected project
+ * @return true if the project could be processed.
+ */
+ private boolean handleProjectSelection(IProject project) {
+ try {
+ // if it's an android project, then we get its resources, and feed them
+ // to the tree viewer.
+ if (project.hasNature(AndroidConstants.NATURE)) {
+ if (mCurrentProject != project) {
+ ProjectResources projRes = ResourceManager.getInstance().getProjectResources(
+ project);
+ if (projRes != null) {
+ mTreeViewer.setInput(projRes);
+ mCurrentProject = project;
+ return true;
+ }
+ }
+ }
+ } catch (CoreException e) {
+ }
+
+ return false;
+ }
+
+ /**
+ * Create a TreeColumn with the specified parameters. If a
+ * <code>PreferenceStore</code> 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 fixedSize a fixed size. If != -1 the column is non resizable
+ * @param pref_name The preference entry name for column width
+ * @param prefs The preference store
+ */
+ public void createTreeColumn(Tree parent, String header, int style,
+ String sample_text, int fixedSize, final String pref_name,
+ final IPreferenceStore prefs) {
+
+ // create the column
+ TreeColumn col = new TreeColumn(parent, style);
+
+ if (fixedSize != -1) {
+ col.setWidth(fixedSize);
+ col.setResizable(false);
+ } else {
+ // 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));
+ }
+
+ // if there is a pref store and a pref entry name, then we setup a
+ // listener to catch column resize to put the new width value into the store.
+ if (prefs != null && pref_name != null) {
+ col.addControlListener(new ControlListener() {
+ public void controlMoved(ControlEvent e) {
+ }
+
+ public void controlResized(ControlEvent e) {
+ // get the new width
+ int w = ((TreeColumn)e.widget).getWidth();
+
+ // store in pref store
+ prefs.setValue(pref_name, w);
+ }
+ });
+ }
+ }
+
+ // set the header
+ col.setText(header);
+ }
+
+ /**
+ * Processes a start in a resource event change.
+ */
+ public void resourceChangeEventStart() {
+ // pass
+ }
+
+ /**
+ * Processes the end of a resource change event.
+ */
+ public void resourceChangeEventEnd() {
+ try {
+ mTree.getDisplay().asyncExec(new Runnable() {
+ public void run() {
+ if (mTree.isDisposed() == false) {
+ mTreeViewer.refresh();
+ }
+ }
+ });
+ } catch (SWTException e) {
+ // display is disposed. nothing to do.
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/CompiledResourcesMonitor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/CompiledResourcesMonitor.java
new file mode 100644
index 0000000..455c825
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/CompiledResourcesMonitor.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.manager;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.common.project.AndroidManifestHelper;
+import com.android.ide.eclipse.common.resources.ResourceType;
+import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IFileListener;
+import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IProjectListener;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IMarkerDelta;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResourceDelta;
+import org.eclipse.core.runtime.CoreException;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A monitor for the compiled resources. This only monitors changes in the resources of type
+ * {@link ResourceType#ID}.
+ */
+public final class CompiledResourcesMonitor implements IFileListener, IProjectListener {
+
+ private final static CompiledResourcesMonitor sThis = new CompiledResourcesMonitor();
+
+ /**
+ * Sets up the monitoring system.
+ * @param monitor The main Resource Monitor.
+ */
+ public static void setupMonitor(ResourceMonitor monitor) {
+ monitor.addFileListener(sThis, IResourceDelta.ADDED | IResourceDelta.CHANGED);
+ monitor.addProjectListener(sThis);
+ }
+
+ /**
+ * private constructor to prevent construction.
+ */
+ private CompiledResourcesMonitor() {
+ }
+
+
+ /* (non-Javadoc)
+ * Sent when a file changed : if the file is the R class, then it is parsed again to update
+ * the internal data.
+ *
+ * @param file The file that changed.
+ * @param markerDeltas The marker deltas for the file.
+ * @param kind The change kind. This is equivalent to
+ * {@link IResourceDelta#accept(IResourceDeltaVisitor)}
+ *
+ * @see IFileListener#fileChanged
+ */
+ public void fileChanged(IFile file, IMarkerDelta[] markerDeltas, int kind) {
+ if (file.getName().equals(AndroidConstants.FN_COMPILED_RESOURCE_CLASS)) {
+ loadAndParseRClass(file.getProject());
+ }
+ }
+
+ /**
+ * Processes project close event.
+ */
+ public void projectClosed(IProject project) {
+ // the ProjectResources object will be removed by the ResourceManager.
+ }
+
+ /**
+ * Processes project delete event.
+ */
+ public void projectDeleted(IProject project) {
+ // the ProjectResources object will be removed by the ResourceManager.
+ }
+
+ /**
+ * Processes project open event.
+ */
+ public void projectOpened(IProject project) {
+ // when the project is opened, we get an ADDED event for each file, so we don't
+ // need to do anything here.
+ }
+
+ /**
+ * Processes existing project at init time.
+ */
+ public void projectOpenedWithWorkspace(IProject project) {
+ try {
+ // check this is an android project
+ if (project.hasNature(AndroidConstants.NATURE)) {
+ loadAndParseRClass(project);
+ }
+ } catch (CoreException e) {
+ // pass
+ }
+ }
+
+ private void loadAndParseRClass(IProject project) {
+ try {
+ // first check there's a ProjectResources to store the content
+ ProjectResources projectResources = ResourceManager.getInstance().getProjectResources(
+ project);
+
+ if (projectResources != null) {
+ // create the classname
+ String className = getRClassName(project);
+
+ // create a temporary class loader to load it.
+ ProjectClassLoader loader = new ProjectClassLoader(null /* parentClassLoader */,
+ project);
+
+ try {
+ Class<?> clazz = loader.loadClass(className);
+
+ if (clazz != null) {
+ // create the maps to store the result of the parsing
+ Map<String, Map<String, Integer>> resourceValueMap =
+ new HashMap<String, Map<String, Integer>>();
+ Map<Integer, String[]> genericValueToNameMap =
+ new HashMap<Integer, String[]>();
+ Map<IntArrayWrapper, String> styleableValueToNameMap =
+ new HashMap<IntArrayWrapper, String>();
+
+ // parse the class
+ if (parseClass(clazz, genericValueToNameMap, styleableValueToNameMap,
+ resourceValueMap)) {
+ // now we associate the maps to the project.
+ projectResources.setCompiledResources(genericValueToNameMap,
+ styleableValueToNameMap, resourceValueMap);
+ }
+ }
+ } catch (Error e) {
+ // Log this error with the class name we're trying to load and abort.
+ AdtPlugin.log(e, "loadAndParseRClass failed to find class %1$s", className); //$NON-NLS-1$
+ }
+ }
+ } catch (ClassNotFoundException e) {
+ // pass
+ }
+ }
+
+ /**
+ * Parses a R class, and fills maps.
+ * @param rClass the class to parse
+ * @param genericValueToNameMap
+ * @param styleableValueToNameMap
+ * @param resourceValueMap
+ * @return True if we managed to parse the R class.
+ */
+ private boolean parseClass(Class<?> rClass, Map<Integer, String[]> genericValueToNameMap,
+ Map<IntArrayWrapper, String> styleableValueToNameMap, Map<String,
+ Map<String, Integer>> resourceValueMap) {
+ try {
+ for (Class<?> inner : rClass.getDeclaredClasses()) {
+ String resType = inner.getSimpleName();
+
+ Map<String, Integer> fullMap = new HashMap<String, Integer>();
+ resourceValueMap.put(resType, fullMap);
+
+ for (Field f : inner.getDeclaredFields()) {
+ // only process static final fields.
+ int modifiers = f.getModifiers();
+ if (Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers)) {
+ Class<?> type = f.getType();
+ if (type.isArray() && type.getComponentType() == int.class) {
+ // if the object is an int[] we put it in the styleable map
+ styleableValueToNameMap.put(new IntArrayWrapper((int[]) f.get(null)),
+ f.getName());
+ } else if (type == int.class) {
+ Integer value = (Integer) f.get(null);
+ genericValueToNameMap.put(value, new String[] { f.getName(), resType });
+ fullMap.put(f.getName(), value);
+ } else {
+ assert false;
+ }
+ }
+ }
+ }
+
+ return true;
+ } catch (IllegalArgumentException e) {
+ } catch (IllegalAccessException e) {
+ }
+ return false;
+ }
+
+ private String getRClassName(IProject project) {
+ // create the classname
+ AndroidManifestHelper manifest = new AndroidManifestHelper(project);
+ String javaPackage = manifest.getPackageName();
+
+ return javaPackage + ".R"; //$NON-NLS-1$
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ConfigurableResourceItem.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ConfigurableResourceItem.java
new file mode 100644
index 0000000..57c17fc
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ConfigurableResourceItem.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.manager;
+
+/**
+ * Represents a resource item that can exist in multiple "alternate" versions.
+ */
+public class ConfigurableResourceItem extends ProjectResourceItem {
+
+ /**
+ * Constructs a new Resource Item.
+ * @param name the name of the resource as it appears in the XML and R.java files.
+ */
+ public ConfigurableResourceItem(String name) {
+ super(name);
+ }
+
+ /**
+ * Returns if the resource item has at least one non-default configuration.
+ */
+ public boolean hasAlternates() {
+ for (ResourceFile file : mFiles) {
+ if (file.getFolder().getConfiguration().isDefault() == false) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns whether the resource has a default version, with no qualifier.
+ */
+ public boolean hasDefault() {
+ for (ResourceFile file : mFiles) {
+ if (file.getFolder().getConfiguration().isDefault()) {
+ return true;
+ }
+ }
+
+ // We only want to return false if there's no default and more than 0 items.
+ return (mFiles.size() == 0);
+ }
+
+ /**
+ * Returns the number of alternate versions of this resource.
+ */
+ public int getAlternateCount() {
+ int count = 0;
+ for (ResourceFile file : mFiles) {
+ if (file.getFolder().getConfiguration().isDefault() == false) {
+ count++;
+ }
+ }
+
+ return count;
+ }
+
+ /*
+ * (non-Javadoc)
+ * Returns whether the item can be edited directly (ie it does not have alternate versions).
+ */
+ @Override
+ public boolean isEditableDirectly() {
+ return hasAlternates() == false;
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/FolderTypeRelationship.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/FolderTypeRelationship.java
new file mode 100644
index 0000000..a9f80bd
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/FolderTypeRelationship.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.manager;
+
+import com.android.ide.eclipse.common.resources.ResourceType;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * This class gives access to the bi directional relationship between {@link ResourceType} and
+ * {@link ResourceFolderType}.
+ */
+public final class FolderTypeRelationship {
+
+ private final static HashMap<ResourceType, ResourceFolderType[]> mTypeToFolderMap =
+ new HashMap<ResourceType, ResourceFolderType[]>();
+
+ private final static HashMap<ResourceFolderType, ResourceType[]> mFolderToTypeMap =
+ new HashMap<ResourceFolderType, ResourceType[]>();
+
+ // generate the relationships.
+ static {
+ HashMap<ResourceType, List<ResourceFolderType>> typeToFolderMap =
+ new HashMap<ResourceType, List<ResourceFolderType>>();
+
+ HashMap<ResourceFolderType, List<ResourceType>> folderToTypeMap =
+ new HashMap<ResourceFolderType, List<ResourceType>>();
+
+ add(ResourceType.ANIM, ResourceFolderType.ANIM, typeToFolderMap, folderToTypeMap);
+ add(ResourceType.ARRAY, ResourceFolderType.VALUES, typeToFolderMap, folderToTypeMap);
+ add(ResourceType.COLOR, ResourceFolderType.VALUES, typeToFolderMap, folderToTypeMap);
+ add(ResourceType.COLOR, ResourceFolderType.COLOR, typeToFolderMap, folderToTypeMap);
+ add(ResourceType.DIMEN, ResourceFolderType.VALUES, typeToFolderMap, folderToTypeMap);
+ add(ResourceType.DRAWABLE, ResourceFolderType.VALUES, typeToFolderMap, folderToTypeMap);
+ add(ResourceType.DRAWABLE, ResourceFolderType.DRAWABLE, typeToFolderMap, folderToTypeMap);
+ add(ResourceType.ID, ResourceFolderType.VALUES, typeToFolderMap, folderToTypeMap);
+ add(ResourceType.LAYOUT, ResourceFolderType.LAYOUT, typeToFolderMap, folderToTypeMap);
+ add(ResourceType.MENU, ResourceFolderType.MENU, typeToFolderMap, folderToTypeMap);
+ add(ResourceType.RAW, ResourceFolderType.RAW, typeToFolderMap, folderToTypeMap);
+ add(ResourceType.STRING, ResourceFolderType.VALUES, typeToFolderMap, folderToTypeMap);
+ add(ResourceType.STYLE, ResourceFolderType.VALUES, typeToFolderMap, folderToTypeMap);
+ add(ResourceType.XML, ResourceFolderType.XML, typeToFolderMap, folderToTypeMap);
+
+ optimize(typeToFolderMap, folderToTypeMap);
+ }
+
+ /**
+ * Returns a list of {@link ResourceType}s that can be generated from files inside a folder
+ * of the specified type.
+ * @param folderType The folder type.
+ * @return an array of {@link ResourceType}
+ */
+ public static ResourceType[] getRelatedResourceTypes(ResourceFolderType folderType) {
+ ResourceType[] array = mFolderToTypeMap.get(folderType);
+ if (array != null) {
+ return array;
+ }
+ return new ResourceType[0];
+ }
+
+ /**
+ * Returns a list of {@link ResourceFolderType} that can contain files generating resources
+ * of the specified type.
+ * @param resType the type of resource.
+ * @return an array of {@link ResourceFolderType}
+ */
+ public static ResourceFolderType[] getRelatedFolders(ResourceType resType) {
+ ResourceFolderType[] array = mTypeToFolderMap.get(resType);
+ if (array != null) {
+ return array;
+ }
+ return new ResourceFolderType[0];
+ }
+
+ /**
+ * Returns true if the {@link ResourceType} and the {@link ResourceFolderType} values match.
+ * @param resType the resource type.
+ * @param folderType the folder type.
+ * @return true if files inside the folder of the specified {@link ResourceFolderType}
+ * could generate a resource of the specified {@link ResourceType}
+ */
+ public static boolean match(ResourceType resType, ResourceFolderType folderType) {
+ ResourceFolderType[] array = mTypeToFolderMap.get(resType);
+
+ if (array != null && array.length > 0) {
+ for (ResourceFolderType fType : array) {
+ if (fType == folderType) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Adds a {@link ResourceType} - {@link ResourceFolderType} relationship. this indicates that
+ * a file in the folder can generate a resource of the specified type.
+ * @param type The resourceType
+ * @param folder The {@link ResourceFolderType}
+ * @param folderToTypeMap
+ * @param typeToFolderMap
+ */
+ private static void add(ResourceType type, ResourceFolderType folder,
+ HashMap<ResourceType, List<ResourceFolderType>> typeToFolderMap,
+ HashMap<ResourceFolderType, List<ResourceType>> folderToTypeMap) {
+ // first we add the folder to the list associated with the type.
+ List<ResourceFolderType> folderList = typeToFolderMap.get(type);
+ if (folderList == null) {
+ folderList = new ArrayList<ResourceFolderType>();
+ typeToFolderMap.put(type, folderList);
+ }
+ if (folderList.indexOf(folder) == -1) {
+ folderList.add(folder);
+ }
+
+ // now we add the type to the list associated with the folder.
+ List<ResourceType> typeList = folderToTypeMap.get(folder);
+ if (typeList == null) {
+ typeList = new ArrayList<ResourceType>();
+ folderToTypeMap.put(folder, typeList);
+ }
+ if (typeList.indexOf(type) == -1) {
+ typeList.add(type);
+ }
+ }
+
+ /**
+ * Optimize the map to contains array instead of lists (since the api returns arrays)
+ * @param typeToFolderMap
+ * @param folderToTypeMap
+ */
+ private static void optimize(HashMap<ResourceType, List<ResourceFolderType>> typeToFolderMap,
+ HashMap<ResourceFolderType, List<ResourceType>> folderToTypeMap) {
+ Set<ResourceType> types = typeToFolderMap.keySet();
+ for (ResourceType type : types) {
+ List<ResourceFolderType> list = typeToFolderMap.get(type);
+ mTypeToFolderMap.put(type, list.toArray(new ResourceFolderType[list.size()]));
+ }
+
+ Set<ResourceFolderType> folders = folderToTypeMap.keySet();
+ for (ResourceFolderType folder : folders) {
+ List<ResourceType> list = folderToTypeMap.get(folder);
+ mFolderToTypeMap.put(folder, list.toArray(new ResourceType[list.size()]));
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/IdResourceItem.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/IdResourceItem.java
new file mode 100644
index 0000000..552aec9
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/IdResourceItem.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.manager;
+
+import com.android.ide.eclipse.common.resources.IIdResourceItem;
+import com.android.ide.eclipse.common.resources.ResourceType;
+
+/**
+ * Represents a resource item of type {@link ResourceType#ID}
+ */
+public class IdResourceItem extends ProjectResourceItem implements IIdResourceItem {
+
+ private final boolean mIsDeclaredInline;
+
+ /**
+ * Constructs a new ResourceItem.
+ * @param name the name of the resource as it appears in the XML and R.java files.
+ * @param isDeclaredInline Whether this id was declared inline.
+ */
+ IdResourceItem(String name, boolean isDeclaredInline) {
+ super(name);
+ mIsDeclaredInline = isDeclaredInline;
+ }
+
+ /*
+ * (non-Javadoc)
+ * Returns whether the ID resource has been declared inline inside another resource XML file.
+ */
+ public boolean isDeclaredInline() {
+ return mIsDeclaredInline;
+ }
+
+ /* (non-Javadoc)
+ * Returns whether the item can be edited (ie, the id was not declared inline).
+ */
+ @Override
+ public boolean isEditableDirectly() {
+ return !mIsDeclaredInline;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/IntArrayWrapper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/IntArrayWrapper.java
new file mode 100644
index 0000000..25eb112
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/IntArrayWrapper.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.manager;
+
+import java.util.Arrays;
+
+
+/**
+ * Wrapper around a int[] to provide hashCode/equals support.
+ */
+public final class IntArrayWrapper {
+
+ private int[] mData;
+
+ public IntArrayWrapper(int[] data) {
+ mData = data;
+ }
+
+ public void set(int[] data) {
+ mData = data;
+ }
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(mData);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (getClass().equals(obj.getClass())) {
+ return Arrays.equals(mData, ((IntArrayWrapper)obj).mData);
+ }
+
+ return super.equals(obj);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/MultiResourceFile.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/MultiResourceFile.java
new file mode 100644
index 0000000..3812791
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/MultiResourceFile.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.manager;
+
+import com.android.ide.eclipse.common.resources.ResourceType;
+import com.android.ide.eclipse.editors.resources.manager.files.IAbstractFile;
+import com.android.layoutlib.api.IResourceValue;
+import com.android.layoutlib.utils.ResourceValue;
+import com.android.layoutlib.utils.ValueResourceParser;
+import com.android.layoutlib.utils.ValueResourceParser.IValueResourceRepository;
+
+import org.eclipse.core.runtime.CoreException;
+import org.xml.sax.SAXException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Set;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+/**
+ * Represents a resource file able to declare multiple resources, which could be of
+ * different {@link ResourceType}.
+ * <p/>
+ * This is typically an XML file inside res/values.
+ */
+public final class MultiResourceFile extends ResourceFile implements IValueResourceRepository {
+
+ private final static SAXParserFactory sParserFactory = SAXParserFactory.newInstance();
+
+ private final HashMap<ResourceType, HashMap<String, ResourceValue>> mResourceItems =
+ new HashMap<ResourceType, HashMap<String, ResourceValue>>();
+
+ public MultiResourceFile(IAbstractFile file, ResourceFolder folder) {
+ super(file, folder);
+ }
+
+ @Override
+ public ResourceType[] getResourceTypes() {
+ update();
+
+ Set<ResourceType> keys = mResourceItems.keySet();
+
+ return keys.toArray(new ResourceType[keys.size()]);
+ }
+
+ @Override
+ public boolean hasResources(ResourceType type) {
+ update();
+
+ HashMap<String, ResourceValue> list = mResourceItems.get(type);
+ return (list != null && list.size() > 0);
+ }
+
+ @Override
+ public Collection<ProjectResourceItem> getResources(ResourceType type,
+ ProjectResources projectResources) {
+ update();
+
+ HashMap<String, ResourceValue> list = mResourceItems.get(type);
+
+ ArrayList<ProjectResourceItem> items = new ArrayList<ProjectResourceItem>();
+
+ if (list != null) {
+ Collection<ResourceValue> values = list.values();
+ for (ResourceValue res : values) {
+ ProjectResourceItem item = projectResources.findResourceItem(type, res.getName());
+
+ if (item == null) {
+ if (type == ResourceType.ID) {
+ item = new IdResourceItem(res.getName(), false /* isDeclaredInline */);
+ } else {
+ item = new ConfigurableResourceItem(res.getName());
+ }
+ items.add(item);
+ }
+
+ item.add(this);
+ }
+ }
+
+ return items;
+ }
+
+ /**
+ * Updates the Resource items if necessary.
+ */
+ private void update() {
+ if (isTouched() == true) {
+ // reset current content.
+ mResourceItems.clear();
+
+ // need to parse the file and find the content.
+ parseFile();
+
+ resetTouch();
+ }
+ }
+
+ /**
+ * Parses the file and creates a list of {@link ResourceType}.
+ */
+ private void parseFile() {
+ try {
+ SAXParser parser = sParserFactory.newSAXParser();
+ parser.parse(getFile().getContents(), new ValueResourceParser(this, isFramework()));
+ } catch (ParserConfigurationException e) {
+ } catch (SAXException e) {
+ } catch (IOException e) {
+ } catch (CoreException e) {
+ }
+ }
+
+ /**
+ * Adds a resource item to the list
+ * @param resType The type of the resource
+ * @param value The value of the resource.
+ */
+ public void addResourceValue(String resType, ResourceValue value) {
+ ResourceType type = ResourceType.getEnum(resType);
+ if (type != null) {
+ HashMap<String, ResourceValue> list = mResourceItems.get(type);
+
+ // if the list does not exist, create it.
+ if (list == null) {
+ list = new HashMap<String, ResourceValue>();
+ mResourceItems.put(type, list);
+ } else {
+ // look for a possible value already existing.
+ ResourceValue oldValue = list.get(value.getName());
+
+ if (oldValue != null) {
+ oldValue.replaceWith(value);
+ return;
+ }
+ }
+
+ // empty list or no match found? add the given resource
+ list.put(value.getName(), value);
+ }
+ }
+
+ @Override
+ public IResourceValue getValue(ResourceType type, String name) {
+ update();
+
+ // get the list for the given type
+ HashMap<String, ResourceValue> list = mResourceItems.get(type);
+
+ if (list != null) {
+ return list.get(name);
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ProjectClassLoader.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ProjectClassLoader.java
new file mode 100644
index 0000000..8b6c3c1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ProjectClassLoader.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.manager;
+
+import com.android.ide.eclipse.common.AndroidConstants;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.jdt.core.IClasspathEntry;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+
+/**
+ * ClassLoader able to load class from output of an Eclipse project.
+ */
+public final class ProjectClassLoader extends ClassLoader {
+
+ private final IJavaProject mJavaProject;
+ private URLClassLoader mJarClassLoader;
+ private boolean mInsideJarClassLoader = false;
+
+ public ProjectClassLoader(ClassLoader parentClassLoader, IProject project) {
+ super(parentClassLoader);
+ mJavaProject = JavaCore.create(project);
+ }
+
+ @Override
+ protected Class<?> findClass(String name) throws ClassNotFoundException {
+ try {
+ // get the project output folder.
+ IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
+ IPath outputLocation = mJavaProject.getOutputLocation();
+ IResource outRes = root.findMember(outputLocation);
+ if (outRes == null) {
+ throw new ClassNotFoundException(name);
+ }
+
+ File outFolder = new File(outRes.getLocation().toOSString());
+
+ // get the class name segments
+ String[] segments = name.split("\\."); //$NON-NLS-1$
+
+ File classFile = getFile(outFolder, segments, 0);
+ if (classFile == null) {
+ if (mInsideJarClassLoader == false) {
+ // if no file matching the class name was found, look in the 3rd party jars
+ return loadClassFromJar(name);
+ } else {
+ throw new ClassNotFoundException(name);
+ }
+ }
+
+ // load the content of the file and create the class.
+ FileInputStream fis = new FileInputStream(classFile);
+ byte[] data = new byte[(int)classFile.length()];
+ int read = 0;
+ try {
+ read = fis.read(data);
+ } catch (IOException e) {
+ data = null;
+ }
+ fis.close();
+
+ if (data != null) {
+ Class<?> clazz = defineClass(null, data, 0, read);
+ if (clazz != null) {
+ return clazz;
+ }
+ }
+ } catch (Exception e) {
+ throw new ClassNotFoundException(e.getMessage());
+ }
+
+ throw new ClassNotFoundException(name);
+ }
+
+ /**
+ * Returns the File matching the a certain path from a root {@link File}.
+ * <p/>The methods checks that the file ends in .class even though the last segment
+ * does not.
+ * @param parent the root of the file.
+ * @param segments the segments containing the path of the file
+ * @param index the offset at which to start looking into segments.
+ * @throws FileNotFoundException
+ */
+ private File getFile(File parent, String[] segments, int index)
+ throws FileNotFoundException {
+ // reached the end with no match?
+ if (index == segments.length) {
+ throw new FileNotFoundException();
+ }
+
+ String toMatch = segments[index];
+ File[] files = parent.listFiles();
+
+ // we're at the last segments. we look for a matching <file>.class
+ if (index == segments.length - 1) {
+ toMatch = toMatch + ".class";
+
+ if (files != null) {
+ for (File file : files) {
+ if (file.isFile() && file.getName().equals(toMatch)) {
+ return file;
+ }
+ }
+ }
+
+ // no match? abort.
+ throw new FileNotFoundException();
+ }
+
+ String innerClassName = null;
+
+ if (files != null) {
+ for (File file : files) {
+ if (file.isDirectory()) {
+ if (toMatch.equals(file.getName())) {
+ return getFile(file, segments, index+1);
+ }
+ } else if (file.getName().startsWith(toMatch)) {
+ if (innerClassName == null) {
+ StringBuilder sb = new StringBuilder(segments[index]);
+ for (int i = index + 1 ; i < segments.length ; i++) {
+ sb.append('$');
+ sb.append(segments[i]);
+ }
+ sb.append(".class");
+
+ innerClassName = sb.toString();
+ }
+
+ if (file.getName().equals(innerClassName)) {
+ return file;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Loads a class from the 3rd party jar present in the project
+ * @throws ClassNotFoundException
+ */
+ private Class<?> loadClassFromJar(String name) throws ClassNotFoundException {
+ if (mJarClassLoader == null) {
+ // get the OS path to all the external jars
+ URL[] jars = getExternalJars();
+
+ mJarClassLoader = new URLClassLoader(jars, this /* parent */);
+ }
+
+ try {
+ // because a class loader always look in its parent loader first, we need to know
+ // that we are querying the jar classloader. This will let us know to not query
+ // it again for classes we don't find, or this would create an infinite loop.
+ mInsideJarClassLoader = true;
+ return mJarClassLoader.loadClass(name);
+ } finally {
+ mInsideJarClassLoader = false;
+ }
+ }
+
+ /**
+ * Returns an array of external jar files used by the project.
+ * @return an array of OS-specific absolute file paths
+ */
+ private final URL[] getExternalJars() {
+ // get a java project from it
+ IJavaProject javaProject = JavaCore.create(mJavaProject.getProject());
+
+ IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot();
+
+ ArrayList<URL> oslibraryList = new ArrayList<URL>();
+ IClasspathEntry[] classpaths = javaProject.readRawClasspath();
+ if (classpaths != null) {
+ for (IClasspathEntry e : classpaths) {
+ if (e.getEntryKind() == IClasspathEntry.CPE_LIBRARY ||
+ e.getEntryKind() == IClasspathEntry.CPE_VARIABLE) {
+ // if this is a classpath variable reference, we resolve it.
+ if (e.getEntryKind() == IClasspathEntry.CPE_VARIABLE) {
+ e = JavaCore.getResolvedClasspathEntry(e);
+ }
+
+ // get the IPath
+ IPath path = e.getPath();
+
+ // check the name ends with .jar
+ if (AndroidConstants.EXT_JAR.equalsIgnoreCase(path.getFileExtension())) {
+ boolean local = false;
+ IResource resource = wsRoot.findMember(path);
+ if (resource != null && resource.exists() &&
+ resource.getType() == IResource.FILE) {
+ local = true;
+ try {
+ oslibraryList.add(
+ new File(resource.getLocation().toOSString()).toURL());
+ } catch (MalformedURLException mue) {
+ // pass
+ }
+ }
+
+ if (local == false) {
+ // if the jar path doesn't match a workspace resource,
+ // then we get an OSString and check if this links to a valid file.
+ String osFullPath = path.toOSString();
+
+ File f = new File(osFullPath);
+ if (f.exists()) {
+ try {
+ oslibraryList.add(f.toURL());
+ } catch (MalformedURLException mue) {
+ // pass
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return oslibraryList.toArray(new URL[oslibraryList.size()]);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ProjectResourceItem.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ProjectResourceItem.java
new file mode 100644
index 0000000..ba770b2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ProjectResourceItem.java
@@ -0,0 +1,91 @@
+package com.android.ide.eclipse.editors.resources.manager;
+
+import com.android.ide.eclipse.common.resources.ResourceItem;
+import com.android.ide.eclipse.common.resources.ResourceType;
+import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Base class for Resource Item coming from an Android Project.
+ */
+public abstract class ProjectResourceItem extends ResourceItem {
+
+ private final static Comparator<ResourceFile> sComparator = new Comparator<ResourceFile>() {
+ public int compare(ResourceFile file1, ResourceFile file2) {
+ // get both FolderConfiguration and compare them
+ FolderConfiguration fc1 = file1.getFolder().getConfiguration();
+ FolderConfiguration fc2 = file2.getFolder().getConfiguration();
+
+ return fc1.compareTo(fc2);
+ }
+ };
+
+ /**
+ * List of files generating this ResourceItem.
+ */
+ protected final ArrayList<ResourceFile> mFiles = new ArrayList<ResourceFile>();
+
+ /**
+ * Constructs a new ResourceItem.
+ * @param name the name of the resource as it appears in the XML and R.java files.
+ */
+ public ProjectResourceItem(String name) {
+ super(name);
+ }
+
+ /**
+ * Returns whether the resource item is editable directly.
+ * <p/>
+ * This is typically the case for resources that don't have alternate versions, or resources
+ * of type {@link ResourceType#ID} that aren't declared inline.
+ */
+ public abstract boolean isEditableDirectly();
+
+ /**
+ * Adds a new version of this resource item, by adding its {@link ResourceFile}.
+ * @param file the {@link ResourceFile} object.
+ */
+ protected void add(ResourceFile file) {
+ mFiles.add(file);
+ }
+
+ /**
+ * Reset the item by emptying its version list.
+ */
+ protected void reset() {
+ mFiles.clear();
+ }
+
+ /**
+ * Returns the sorted list of {@link ResourceItem} objects for this resource item.
+ */
+ public ResourceFile[] getSourceFileArray() {
+ ArrayList<ResourceFile> list = new ArrayList<ResourceFile>();
+ list.addAll(mFiles);
+
+ Collections.sort(list, sComparator);
+
+ return list.toArray(new ResourceFile[list.size()]);
+ }
+
+ /**
+ * Returns the list of {@link ResourceItem} objects for this resource item.
+ */
+ public List<ResourceFile> getSourceFileList() {
+ return Collections.unmodifiableList(mFiles);
+ }
+
+
+ /**
+ * Replaces the content of the receiver with the ResourceItem received as parameter.
+ * @param item
+ */
+ protected void replaceWith(ProjectResourceItem item) {
+ mFiles.clear();
+ mFiles.addAll(item.mFiles);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ProjectResources.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ProjectResources.java
new file mode 100644
index 0000000..40e4e3b
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ProjectResources.java
@@ -0,0 +1,804 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.manager;
+
+import com.android.ide.eclipse.common.resources.IResourceRepository;
+import com.android.ide.eclipse.common.resources.ResourceItem;
+import com.android.ide.eclipse.common.resources.ResourceType;
+import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration;
+import com.android.ide.eclipse.editors.resources.configurations.LanguageQualifier;
+import com.android.ide.eclipse.editors.resources.configurations.RegionQualifier;
+import com.android.ide.eclipse.editors.resources.manager.files.IAbstractFolder;
+import com.android.layoutlib.api.IResourceValue;
+import com.android.layoutlib.utils.ResourceValue;
+
+import org.eclipse.core.resources.IFolder;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Represents the resources of a project. This is a file view of the resources, with handling
+ * for the alternate resource types. For a compiled view use CompiledResources.
+ */
+public class ProjectResources implements IResourceRepository {
+ private final HashMap<ResourceFolderType, List<ResourceFolder>> mFolderMap =
+ new HashMap<ResourceFolderType, List<ResourceFolder>>();
+
+ private final HashMap<ResourceType, List<ProjectResourceItem>> mResourceMap =
+ new HashMap<ResourceType, List<ProjectResourceItem>>();
+
+ /** Map of (name, id) for resources of type {@link ResourceType#ID} coming from R.java */
+ private Map<String, Map<String, Integer>> mResourceValueMap;
+ /** Map of (id, [name, resType]) for all resources coming from R.java */
+ private Map<Integer, String[]> mResIdValueToNameMap;
+ /** Map of (int[], name) for styleable resources coming from R.java */
+ private Map<IntArrayWrapper, String> mStyleableValueToNameMap;
+
+ /** Cached list of {@link IdResourceItem}. This is mix of IdResourceItem created by
+ * {@link MultiResourceFile} for ids coming from XML files under res/values and
+ * {@link IdResourceItem} created manually, from the list coming from R.java */
+ private final ArrayList<IdResourceItem> mIdResourceList = new ArrayList<IdResourceItem>();
+
+ private final boolean mIsFrameworkRepository;
+
+ private final IntArrayWrapper mWrapper = new IntArrayWrapper(null);
+
+ public ProjectResources(boolean isFrameworkRepository) {
+ mIsFrameworkRepository = isFrameworkRepository;
+ }
+
+ public boolean isSystemRepository() {
+ return mIsFrameworkRepository;
+ }
+
+ /**
+ * Adds a Folder Configuration to the project.
+ * @param type The resource type.
+ * @param config The resource configuration.
+ * @param folder The workspace folder object.
+ * @return the {@link ResourceFolder} object associated to this folder.
+ */
+ protected ResourceFolder add(ResourceFolderType type, FolderConfiguration config,
+ IAbstractFolder folder) {
+ // get the list for the resource type
+ List<ResourceFolder> list = mFolderMap.get(type);
+
+ if (list == null) {
+ list = new ArrayList<ResourceFolder>();
+
+ ResourceFolder cf = new ResourceFolder(type, config, folder, mIsFrameworkRepository);
+ list.add(cf);
+
+ mFolderMap.put(type, list);
+
+ return cf;
+ }
+
+ // look for an already existing folder configuration.
+ for (ResourceFolder cFolder : list) {
+ if (cFolder.mConfiguration.equals(config)) {
+ // config already exist. Nothing to be done really, besides making sure
+ // the IFolder object is up to date.
+ cFolder.mFolder = folder;
+ return cFolder;
+ }
+ }
+
+ // If we arrive here, this means we didn't find a matching configuration.
+ // So we add one.
+ ResourceFolder cf = new ResourceFolder(type, config, folder, mIsFrameworkRepository);
+ list.add(cf);
+
+ return cf;
+ }
+
+ /**
+ * Removes a {@link ResourceFolder} associated with the specified {@link IAbstractFolder}.
+ * @param type The type of the folder
+ * @param folder the IFolder object.
+ */
+ protected void removeFolder(ResourceFolderType type, IFolder folder) {
+ // get the list of folders for the resource type.
+ List<ResourceFolder> list = mFolderMap.get(type);
+
+ if (list != null) {
+ int count = list.size();
+ for (int i = 0 ; i < count ; i++) {
+ ResourceFolder resFolder = list.get(i);
+ if (resFolder.getFolder().getIFolder().equals(folder)) {
+ // we found the matching ResourceFolder. we need to remove it.
+ list.remove(i);
+
+ // we now need to invalidate this resource type.
+ // The easiest way is to touch one of the other folders of the same type.
+ if (list.size() > 0) {
+ list.get(0).touch();
+ } else {
+ // if the list is now empty, and we have a single ResouceType out of this
+ // ResourceFolderType, then we are done.
+ // However, if another ResourceFolderType can generate similar ResourceType
+ // than this, we need to update those ResourceTypes as well.
+ // For instance, if the last "drawable-*" folder is deleted, we need to
+ // refresh the ResourceItem associated with ResourceType.DRAWABLE.
+ // Those can be found in ResourceFolderType.DRAWABLE but also in
+ // ResourceFolderType.VALUES.
+ // If we don't find a single folder to touch, then it's fine, as the top
+ // level items (the list of generated resource types) is not cached
+ // (for now)
+
+ // get the lists of ResourceTypes generated by this ResourceFolderType
+ ResourceType[] resTypes = FolderTypeRelationship.getRelatedResourceTypes(
+ type);
+
+ // for each of those, make sure to find one folder to touch so that the
+ // list of ResourceItem associated with the type is rebuilt.
+ for (ResourceType resType : resTypes) {
+ // get the list of folder that can generate this type
+ ResourceFolderType[] folderTypes =
+ FolderTypeRelationship.getRelatedFolders(resType);
+
+ // we only need to touch one folder in any of those (since it's one
+ // folder per type, not per folder type).
+ for (ResourceFolderType folderType : folderTypes) {
+ List<ResourceFolder> resFolders = mFolderMap.get(folderType);
+
+ if (resFolders != null && resFolders.size() > 0) {
+ resFolders.get(0).touch();
+ break;
+ }
+ }
+ }
+ }
+
+ // we're done updating/touching, we can stop
+ break;
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Returns a list of {@link ResourceFolder} for a specific {@link ResourceFolderType}.
+ * @param type The {@link ResourceFolderType}
+ */
+ public List<ResourceFolder> getFolders(ResourceFolderType type) {
+ return mFolderMap.get(type);
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ide.eclipse.editors.resources.IResourceRepository#getAvailableResourceTypes()
+ */
+ public ResourceType[] getAvailableResourceTypes() {
+ ArrayList<ResourceType> list = new ArrayList<ResourceType>();
+
+ // For each key, we check if there's a single ResourceType match.
+ // If not, we look for the actual content to give us the resource type.
+
+ for (ResourceFolderType folderType : mFolderMap.keySet()) {
+ ResourceType types[] = FolderTypeRelationship.getRelatedResourceTypes(folderType);
+ if (types.length == 1) {
+ // before we add it we check if it's not already present, since a ResourceType
+ // could be created from multiple folders, even for the folders that only create
+ // one type of resource (drawable for instance, can be created from drawable/ and
+ // values/)
+ if (list.indexOf(types[0]) == -1) {
+ list.add(types[0]);
+ }
+ } else {
+ // there isn't a single resource type out of this folder, so we look for all
+ // content.
+ List<ResourceFolder> folders = mFolderMap.get(folderType);
+ if (folders != null) {
+ for (ResourceFolder folder : folders) {
+ Collection<ResourceType> folderContent = folder.getResourceTypes();
+
+ // then we add them, but only if they aren't already in the list.
+ for (ResourceType folderResType : folderContent) {
+ if (list.indexOf(folderResType) == -1) {
+ list.add(folderResType);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // in case ResourceType.ID haven't been added yet because there's no id defined
+ // in XML, we check on the list of compiled id resources.
+ if (list.indexOf(ResourceType.ID) == -1 && mResourceValueMap != null) {
+ Map<String, Integer> map = mResourceValueMap.get(ResourceType.ID.getName());
+ if (map != null && map.size() > 0) {
+ list.add(ResourceType.ID);
+ }
+ }
+
+ // at this point the list is full of ResourceType defined in the files.
+ // We need to sort it.
+ Collections.sort(list);
+
+ return list.toArray(new ResourceType[list.size()]);
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ide.eclipse.editors.resources.IResourceRepository#getResources(com.android.ide.eclipse.common.resources.ResourceType)
+ */
+ public ProjectResourceItem[] getResources(ResourceType type) {
+ checkAndUpdate(type);
+
+ if (type == ResourceType.ID) {
+ synchronized (mIdResourceList) {
+ return mIdResourceList.toArray(new ProjectResourceItem[mIdResourceList.size()]);
+ }
+ }
+
+ List<ProjectResourceItem> items = mResourceMap.get(type);
+
+ return items.toArray(new ProjectResourceItem[items.size()]);
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ide.eclipse.editors.resources.IResourceRepository#hasResources(com.android.ide.eclipse.common.resources.ResourceType)
+ */
+ public boolean hasResources(ResourceType type) {
+ checkAndUpdate(type);
+
+ if (type == ResourceType.ID) {
+ synchronized (mIdResourceList) {
+ return mIdResourceList.size() > 0;
+ }
+ }
+
+ List<ProjectResourceItem> items = mResourceMap.get(type);
+ return (items != null && items.size() > 0);
+ }
+
+ /**
+ * Returns the {@link ResourceFolder} associated with a {@link IFolder}.
+ * @param folder The {@link IFolder} object.
+ * @return the {@link ResourceFolder} or null if it was not found.
+ */
+ public ResourceFolder getResourceFolder(IFolder folder) {
+ for (List<ResourceFolder> list : mFolderMap.values()) {
+ for (ResourceFolder resFolder : list) {
+ if (resFolder.getFolder().getIFolder().equals(folder)) {
+ return resFolder;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the {@link ResourceFile} matching the given name, {@link ResourceFolderType} and
+ * configuration.
+ * <p/>This only works with files generating one resource named after the file (for instance,
+ * layouts, bitmap based drawable, xml, anims).
+ * @return the matching file or <code>null</code> if no match was found.
+ */
+ public ResourceFile getMatchingFile(String name, ResourceFolderType type,
+ FolderConfiguration config) {
+ // get the folders for the given type
+ List<ResourceFolder> folders = mFolderMap.get(type);
+
+ // look for folders containing a file with the given name.
+ ArrayList<ResourceFolder> matchingFolders = new ArrayList<ResourceFolder>();
+
+ // remove the folders that do not have a file with the given name, or if their config
+ // is incompatible.
+ for (int i = 0 ; i < folders.size(); i++) {
+ ResourceFolder folder = folders.get(i);
+
+ if (folder.hasFile(name) == true) {
+ matchingFolders.add(folder);
+ }
+ }
+
+ // from those, get the folder with a config matching the given reference configuration.
+ Resource match = findMatchingConfiguredResource(matchingFolders, config);
+
+ // do we have a matching folder?
+ if (match instanceof ResourceFolder) {
+ // get the ResourceFile from the filename
+ return ((ResourceFolder)match).getFile(name);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the resources values matching a given {@link FolderConfiguration}.
+ * @param referenceConfig the configuration that each value must match.
+ */
+ public Map<String, Map<String, IResourceValue>> getConfiguredResources(
+ FolderConfiguration referenceConfig) {
+
+ Map<String, Map<String, IResourceValue>> map =
+ new HashMap<String, Map<String, IResourceValue>>();
+
+ // special case for Id since there's a mix of compiled id (declared inline) and id declared
+ // in the XML files.
+ if (mIdResourceList.size() > 0) {
+ Map<String, IResourceValue> idMap = new HashMap<String, IResourceValue>();
+ String idType = ResourceType.ID.getName();
+ for (IdResourceItem id : mIdResourceList) {
+ // FIXME: cache the ResourceValue!
+ idMap.put(id.getName(), new ResourceValue(idType, id.getName(),
+ mIsFrameworkRepository));
+ }
+
+ map.put(ResourceType.ID.getName(), idMap);
+ }
+
+ Set<ResourceType> keys = mResourceMap.keySet();
+ for (ResourceType key : keys) {
+ // we don't process ID resources since we already did it above.
+ if (key != ResourceType.ID) {
+ map.put(key.getName(), getConfiguredResource(key, referenceConfig));
+ }
+ }
+
+ return map;
+ }
+
+ /**
+ * Loads all the resources. Essentially this forces to load the values from the
+ * {@link ResourceFile} objects to make sure they are up to date and loaded
+ * in {@link #mResourceMap}.
+ */
+ public void loadAll() {
+ // gets all the resource types available.
+ ResourceType[] types = getAvailableResourceTypes();
+
+ // loop on them and load them
+ for (ResourceType type: types) {
+ checkAndUpdate(type);
+ }
+ }
+
+ /**
+ * Resolves a compiled resource id into the resource name and type
+ * @param id
+ * @return an array of 2 strings { name, type } or null if the id could not be resolved
+ */
+ public String[] resolveResourceValue(int id) {
+ if (mResIdValueToNameMap != null) {
+ return mResIdValueToNameMap.get(id);
+ }
+
+ return null;
+ }
+
+ /**
+ * Resolves a compiled resource id of type int[] into the resource name.
+ */
+ public String resolveResourceValue(int[] id) {
+ if (mStyleableValueToNameMap != null) {
+ mWrapper.set(id);
+ return mStyleableValueToNameMap.get(mWrapper);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the value of a resource by its type and name.
+ */
+ public Integer getResourceValue(String type, String name) {
+ if (mResourceValueMap != null) {
+ Map<String, Integer> map = mResourceValueMap.get(type);
+ if (map != null) {
+ return map.get(name);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the list of languages used in the resources.
+ */
+ public Set<String> getLanguages() {
+ Set<String> set = new HashSet<String>();
+
+ Collection<List<ResourceFolder>> folderList = mFolderMap.values();
+ for (List<ResourceFolder> folderSubList : folderList) {
+ for (ResourceFolder folder : folderSubList) {
+ FolderConfiguration config = folder.getConfiguration();
+ LanguageQualifier lang = config.getLanguageQualifier();
+ if (lang != null) {
+ set.add(lang.getStringValue());
+ }
+ }
+ }
+
+ return set;
+ }
+
+ /**
+ * Returns the list of regions used in the resources with the given language.
+ * @param currentLanguage the current language the region must be associated with.
+ */
+ public Set<String> getRegions(String currentLanguage) {
+ Set<String> set = new HashSet<String>();
+
+ Collection<List<ResourceFolder>> folderList = mFolderMap.values();
+ for (List<ResourceFolder> folderSubList : folderList) {
+ for (ResourceFolder folder : folderSubList) {
+ FolderConfiguration config = folder.getConfiguration();
+
+ // get the language
+ LanguageQualifier lang = config.getLanguageQualifier();
+ if (lang != null && lang.getStringValue().equals(currentLanguage)) {
+ RegionQualifier region = config.getRegionQualifier();
+ if (region != null) {
+ set.add(region.getStringValue());
+ }
+ }
+ }
+ }
+
+ return set;
+ }
+
+ /**
+ * Returns a map of (resource name, resource value) for the given {@link ResourceType}.
+ * <p/>The values returned are taken from the resource files best matching a given
+ * {@link FolderConfiguration}.
+ * @param type the type of the resources.
+ * @param referenceConfig the configuration to best match.
+ */
+ private Map<String, IResourceValue> getConfiguredResource(ResourceType type,
+ FolderConfiguration referenceConfig) {
+ // get the resource item for the given type
+ List<ProjectResourceItem> items = mResourceMap.get(type);
+
+ // create the map
+ HashMap<String, IResourceValue> map = new HashMap<String, IResourceValue>();
+
+ for (ProjectResourceItem item : items) {
+ // get the source files generating this resource
+ List<ResourceFile> list = item.getSourceFileList();
+
+ // look for the best match for the given configuration
+ Resource match = findMatchingConfiguredResource(list, referenceConfig);
+
+ if (match instanceof ResourceFile) {
+ ResourceFile matchResFile = (ResourceFile)match;
+
+ // get the value of this configured resource.
+ IResourceValue value = matchResFile.getValue(type, item.getName());
+
+ if (value != null) {
+ map.put(item.getName(), value);
+ }
+ }
+ }
+
+ return map;
+ }
+
+ /**
+ * Returns the best matching {@link Resource}.
+ * @param resources the list of {@link Resource} to choose from.
+ * @param referenceConfig the {@link FolderConfiguration} to match.
+ */
+ private Resource findMatchingConfiguredResource(List<? extends Resource> resources,
+ FolderConfiguration referenceConfig) {
+ // look for resources with the maximum number of qualifier match.
+ int currentMax = -1;
+ ArrayList<Resource> matchingResources = new ArrayList<Resource>();
+ for (int i = 0 ; i < resources.size(); i++) {
+ Resource res = resources.get(i);
+
+ int count = res.getConfiguration().match(referenceConfig);
+ if (count > currentMax) {
+ matchingResources.clear();
+ matchingResources.add(res);
+ currentMax = count;
+ } else if (count != -1 && count == currentMax) {
+ matchingResources.add(res);
+ }
+ }
+
+ // if we have more than one match, we look for the match with the qualifiers with the
+ // highest priority.
+ Resource resMatch = null;
+ if (matchingResources.size() == 1) {
+ resMatch = matchingResources.get(0);
+ } else if (matchingResources.size() > 1) {
+ // More than one resource with the same number of qualifier match.
+ // We loop, looking for the resource with the highest priority qualifiers.
+ ArrayList<Resource> tmpResources = new ArrayList<Resource>();
+ int startIndex = 0;
+ while (matchingResources.size() > 1) {
+ int highest = -1;
+ for (int i = 0 ; i < matchingResources.size() ; i++) {
+ Resource folder = matchingResources.get(i);
+
+ // get highest priority qualifiers.
+ int m = folder.getConfiguration().getHighestPriorityQualifier(startIndex);
+
+ // add to the list if highest.
+ if (m != -1) {
+ if (highest == -1 || m == highest) {
+ tmpResources.add(folder);
+ highest = m;
+ } else if (m < highest) { // highest priority == lowest index.
+ tmpResources.clear();
+ tmpResources.add(folder);
+ }
+ }
+ }
+
+ // at this point, we have a list with 1+ resources that all have the same highest
+ // priority qualifiers. Go through the list again looking for the next highest
+ // priority qualifier.
+ startIndex = highest + 1;
+
+ // this should not happen, but it's better to check.
+ if (matchingResources.size() == tmpResources.size() && highest == -1) {
+ // this means all the resources match with the same qualifiers
+ // (highest == -1 means we reached the end of the qualifier list)
+ // In this case, we arbitrarily take the first resource.
+ matchingResources.clear();
+ matchingResources.add(tmpResources.get(0));
+ } else {
+ matchingResources.clear();
+ matchingResources.addAll(tmpResources);
+ }
+ tmpResources.clear();
+ }
+
+ // we should have only one match here.
+ resMatch = matchingResources.get(0);
+ }
+
+ return resMatch;
+ }
+
+ /**
+ * Checks if the list of {@link ResourceItem}s for the specified {@link ResourceType} needs
+ * to be updated.
+ * @param type the Resource Type.
+ */
+ private void checkAndUpdate(ResourceType type) {
+ // get the list of folder that can output this type
+ ResourceFolderType[] folderTypes = FolderTypeRelationship.getRelatedFolders(type);
+
+ for (ResourceFolderType folderType : folderTypes) {
+ List<ResourceFolder> folders = mFolderMap.get(folderType);
+
+ if (folders != null) {
+ for (ResourceFolder folder : folders) {
+ if (folder.isTouched()) {
+ // if this folder is touched we need to update all the types that can
+ // be generated from a file in this folder.
+ // This will include 'type' obviously.
+ ResourceType[] resTypes = FolderTypeRelationship.getRelatedResourceTypes(
+ folderType);
+ for (ResourceType resType : resTypes) {
+ update(resType);
+ }
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Updates the list of {@link ResourceItem} objects associated with a {@link ResourceType}.
+ * This will reset the touch status of all the folders that can generate this resource type.
+ * @param type the Resource Type.
+ */
+ private void update(ResourceType type) {
+ // get the cache list, and lets make a backup
+ List<ProjectResourceItem> items = mResourceMap.get(type);
+ List<ProjectResourceItem> backup = new ArrayList<ProjectResourceItem>();
+
+ if (items == null) {
+ items = new ArrayList<ProjectResourceItem>();
+ mResourceMap.put(type, items);
+ } else {
+ // backup the list
+ backup.addAll(items);
+
+ // we reset the list itself.
+ items.clear();
+ }
+
+ // get the list of folder that can output this type
+ ResourceFolderType[] folderTypes = FolderTypeRelationship.getRelatedFolders(type);
+
+ for (ResourceFolderType folderType : folderTypes) {
+ List<ResourceFolder> folders = mFolderMap.get(folderType);
+
+ if (folders != null) {
+ for (ResourceFolder folder : folders) {
+ items.addAll(folder.getResources(type, this));
+ folder.resetTouch();
+ }
+ }
+ }
+
+ // now items contains the new list. We "merge" it with the backup list.
+ // Basically, we need to keep the old instances of ResourceItem (where applicable),
+ // but replace them by the content of the new items.
+ // This will let the resource explorer keep the expanded state of the nodes whose data
+ // is a ResourceItem object.
+ if (backup.size() > 0) {
+ // this is not going to change as we're only replacing instances.
+ int count = items.size();
+
+ for (int i = 0 ; i < count;) {
+ // get the "new" item
+ ProjectResourceItem item = items.get(i);
+
+ // look for a similar item in the old list.
+ ProjectResourceItem foundOldItem = null;
+ for (ProjectResourceItem oldItem : backup) {
+ if (oldItem.getName().equals(item.getName())) {
+ foundOldItem = oldItem;
+ break;
+ }
+ }
+
+ if (foundOldItem != null) {
+ // erase the data of the old item with the data from the new one.
+ foundOldItem.replaceWith(item);
+
+ // remove the old and new item from their respective lists
+ items.remove(i);
+ backup.remove(foundOldItem);
+
+ // add the old item to the new list
+ items.add(foundOldItem);
+ } else {
+ // this is a new item, we skip to the next object
+ i++;
+ }
+ }
+ }
+
+ // if this is the ResourceType.ID, we create the actual list, from this list and
+ // the compiled resource list.
+ if (type == ResourceType.ID) {
+ mergeIdResources();
+ } else {
+ // else this is the list that will actually be displayed, so we sort it.
+ Collections.sort(items);
+ }
+ }
+
+ /**
+ * Looks up an existing {@link ProjectResourceItem} by {@link ResourceType} and name.
+ * @param type the Resource Type.
+ * @param name the Resource name.
+ * @return the existing ResourceItem or null if no match was found.
+ */
+ protected ProjectResourceItem findResourceItem(ResourceType type, String name) {
+ List<ProjectResourceItem> list = mResourceMap.get(type);
+
+ for (ProjectResourceItem item : list) {
+ if (name.equals(item.getName())) {
+ return item;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Sets compiled resource information.
+ * @param resIdValueToNameMap a map of compiled resource id to resource name.
+ * The map is acquired by the {@link ProjectResources} object.
+ * @param styleableValueMap
+ * @param resourceValueMap a map of (name, id) for resources of type {@link ResourceType#ID}.
+ * The list is acquired by the {@link ProjectResources} object.
+ */
+ void setCompiledResources(Map<Integer, String[]> resIdValueToNameMap,
+ Map<IntArrayWrapper, String> styleableValueMap,
+ Map<String, Map<String, Integer>> resourceValueMap) {
+ mResourceValueMap = resourceValueMap;
+ mResIdValueToNameMap = resIdValueToNameMap;
+ mStyleableValueToNameMap = styleableValueMap;
+ mergeIdResources();
+ }
+
+ /**
+ * Merges the list of ID resource coming from R.java and the list of ID resources
+ * coming from XML declaration into the cached list {@link #mIdResourceList}.
+ */
+ void mergeIdResources() {
+ // get the list of IDs coming from XML declaration. Those ids are present in
+ // mCompiledIdResources already, so we'll need to use those instead of creating
+ // new IdResourceItem
+ List<ProjectResourceItem> xmlIdResources = mResourceMap.get(ResourceType.ID);
+
+ synchronized (mIdResourceList) {
+ // copy the currently cached items.
+ ArrayList<IdResourceItem> oldItems = new ArrayList<IdResourceItem>();
+ oldItems.addAll(mIdResourceList);
+
+ // empty the current list
+ mIdResourceList.clear();
+
+ // get the list of compile id resources.
+ Map<String, Integer> idMap = null;
+ if (mResourceValueMap != null) {
+ idMap = mResourceValueMap.get(ResourceType.ID.getName());
+ }
+
+ if (idMap == null) {
+ if (xmlIdResources != null) {
+ for (ProjectResourceItem resourceItem : xmlIdResources) {
+ // check the actual class just for safety.
+ if (resourceItem instanceof IdResourceItem) {
+ mIdResourceList.add((IdResourceItem)resourceItem);
+ }
+ }
+ }
+ } else {
+ // loop on the full list of id, and look for a match in the old list,
+ // in the list coming from XML (in case a new XML item was created.)
+
+ Set<String> idSet = idMap.keySet();
+
+ idLoop: for (String idResource : idSet) {
+ // first look in the XML list in case an id went from inline to XML declared.
+ if (xmlIdResources != null) {
+ for (ProjectResourceItem resourceItem : xmlIdResources) {
+ if (resourceItem instanceof IdResourceItem &&
+ resourceItem.getName().equals(idResource)) {
+ mIdResourceList.add((IdResourceItem)resourceItem);
+ continue idLoop;
+ }
+ }
+ }
+
+ // if we haven't found it, look in the old items.
+ int count = oldItems.size();
+ for (int i = 0 ; i < count ; i++) {
+ IdResourceItem resourceItem = oldItems.get(i);
+ if (resourceItem.getName().equals(idResource)) {
+ oldItems.remove(i);
+ mIdResourceList.add(resourceItem);
+ continue idLoop;
+ }
+ }
+
+ // if we haven't found it, it looks like it's a new id that was
+ // declared inline.
+ mIdResourceList.add(new IdResourceItem(idResource,
+ true /* isDeclaredInline */));
+ }
+ }
+
+ // now we sort the list
+ Collections.sort(mIdResourceList);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/Resource.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/Resource.java
new file mode 100644
index 0000000..dd8d080
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/Resource.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.manager;
+
+import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration;
+
+/**
+ * Base class for file system resource items (Folders, Files).
+ */
+public abstract class Resource {
+ private boolean mTouched = true;
+
+ /**
+ * Returns the {@link FolderConfiguration} for this object.
+ */
+ public abstract FolderConfiguration getConfiguration();
+
+ /**
+ * Indicates that the underlying file was changed.
+ */
+ public final void touch() {
+ mTouched = true;
+ }
+
+ public final boolean isTouched() {
+ return mTouched;
+ }
+
+ public final void resetTouch() {
+ mTouched = false;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceFile.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceFile.java
new file mode 100644
index 0000000..f927a9a
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceFile.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.manager;
+
+import com.android.ide.eclipse.common.resources.ResourceType;
+import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration;
+import com.android.ide.eclipse.editors.resources.manager.files.IAbstractFile;
+import com.android.layoutlib.api.IResourceValue;
+
+import java.util.Collection;
+
+/**
+ * Represents a Resource file (a file under $Project/res/)
+ */
+public abstract class ResourceFile extends Resource {
+
+ private final IAbstractFile mFile;
+ private final ResourceFolder mFolder;
+
+ protected ResourceFile(IAbstractFile file, ResourceFolder folder) {
+ mFile = file;
+ mFolder = folder;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ide.eclipse.editors.resources.manager.Resource#getConfiguration()
+ */
+ @Override
+ public FolderConfiguration getConfiguration() {
+ return mFolder.getConfiguration();
+ }
+
+ /**
+ * Returns the IFile associated with the ResourceFile.
+ */
+ public final IAbstractFile getFile() {
+ return mFile;
+ }
+
+ /**
+ * Returns the parent folder as a {@link ResourceFolder}.
+ */
+ public final ResourceFolder getFolder() {
+ return mFolder;
+ }
+
+ /**
+ * Returns whether the resource is a framework resource.
+ */
+ public final boolean isFramework() {
+ return mFolder.isFramework();
+ }
+
+ /**
+ * Returns the list of {@link ResourceType} generated by the file.
+ */
+ public abstract ResourceType[] getResourceTypes();
+
+ /**
+ * Returns whether the file generated a resource of a specific type.
+ * @param type The {@link ResourceType}
+ */
+ public abstract boolean hasResources(ResourceType type);
+
+ /**
+ * Get the list of {@link ProjectResourceItem} of a specific type generated by the file.
+ * This method must make sure not to create duplicate.
+ * @param type The type of {@link ProjectResourceItem} to return.
+ * @param projectResources The global Project Resource object, allowing the implementation to
+ * query for already existing {@link ProjectResourceItem}
+ * @return The list of <b>new</b> {@link ProjectResourceItem}
+ * @see ProjectResources#findResourceItem(ResourceType, String)
+ */
+ public abstract Collection<ProjectResourceItem> getResources(ResourceType type,
+ ProjectResources projectResources);
+
+ /**
+ * Returns the value of a resource generated by this file by {@link ResourceType} and name.
+ * <p/>If no resource match, <code>null</code> is returned.
+ * @param type the type of the resource.
+ * @param name the name of the resource.
+ */
+ public abstract IResourceValue getValue(ResourceType type, String name);
+}
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceFolder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceFolder.java
new file mode 100644
index 0000000..98f5b39
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceFolder.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.manager;
+
+import com.android.ide.eclipse.common.resources.ResourceItem;
+import com.android.ide.eclipse.common.resources.ResourceType;
+import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration;
+import com.android.ide.eclipse.editors.resources.manager.files.IAbstractFile;
+import com.android.ide.eclipse.editors.resources.manager.files.IAbstractFolder;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * Resource Folder class. Contains list of {@link ResourceFile}s,
+ * the {@link FolderConfiguration}, and a link to the workspace {@link IFolder} object.
+ */
+public final class ResourceFolder extends Resource {
+ ResourceFolderType mType;
+ FolderConfiguration mConfiguration;
+ IAbstractFolder mFolder;
+ ArrayList<ResourceFile> mFiles = null;
+ private final boolean mIsFramework;
+
+ /**
+ * Creates a new {@link ResourceFolder}
+ * @param type The type of the folder
+ * @param config The configuration of the folder
+ * @param folder The associated {@link IAbstractFolder} object.
+ * @param isFrameworkRepository
+ */
+ public ResourceFolder(ResourceFolderType type, FolderConfiguration config,
+ IAbstractFolder folder, boolean isFrameworkRepository) {
+ mType = type;
+ mConfiguration = config;
+ mFolder = folder;
+ mIsFramework = isFrameworkRepository;
+ }
+
+ /**
+ * Adds a {@link ResourceFile} to the folder.
+ * @param file The {@link ResourceFile}.
+ */
+ public void addFile(ResourceFile file) {
+ if (mFiles == null) {
+ mFiles = new ArrayList<ResourceFile>();
+ }
+
+ mFiles.add(file);
+ }
+
+ /**
+ * Attempts to remove the {@link ResourceFile} associated with a specified {@link IFile}.
+ * @param file the IFile object.
+ */
+ public void removeFile(IFile file) {
+ if (mFiles != null) {
+ int count = mFiles.size();
+ for (int i = 0 ; i < count ; i++) {
+ ResourceFile resFile = mFiles.get(i);
+ if (resFile != null) {
+ IFile iFile = resFile.getFile().getIFile();
+ if (iFile != null && iFile.equals(file)) {
+ mFiles.remove(i);
+ touch();
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the {@link IFolder} associated with this object.
+ */
+ public IAbstractFolder getFolder() {
+ return mFolder;
+ }
+
+ /**
+ * Returns the {@link ResourceFolderType} of this object.
+ */
+ public ResourceFolderType getType() {
+ return mType;
+ }
+
+ /**
+ * Returns whether the folder is a framework resource folder.
+ */
+ public boolean isFramework() {
+ return mIsFramework;
+ }
+
+ /**
+ * Returns the list of {@link ResourceType}s generated by the files inside this folder.
+ */
+ public Collection<ResourceType> getResourceTypes() {
+ ArrayList<ResourceType> list = new ArrayList<ResourceType>();
+
+ if (mFiles != null) {
+ for (ResourceFile file : mFiles) {
+ ResourceType[] types = file.getResourceTypes();
+
+ // loop through those and add them to the main list,
+ // if they are not already present
+ if (types != null) {
+ for (ResourceType resType : types) {
+ if (list.indexOf(resType) == -1) {
+ list.add(resType);
+ }
+ }
+ }
+ }
+ }
+
+ return list;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ide.eclipse.editors.resources.manager.Resource#getConfiguration()
+ */
+ @Override
+ public FolderConfiguration getConfiguration() {
+ return mConfiguration;
+ }
+
+ /**
+ * Returns whether the folder contains a file with the given name.
+ * @param name the name of the file.
+ */
+ public boolean hasFile(String name) {
+ return mFolder.hasFile(name);
+ }
+
+ /**
+ * Returns the {@link ResourceFile} matching a {@link IAbstractFile} object.
+ * @param file The {@link IFile} object.
+ * @return the {@link ResourceFile} or null if no match was found.
+ */
+ public ResourceFile getFile(IAbstractFile file) {
+ if (mFiles != null) {
+ for (ResourceFile f : mFiles) {
+ if (f.getFile().equals(file)) {
+ return f;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the {@link ResourceFile} matching a {@link IFile} object.
+ * @param file The {@link IFile} object.
+ * @return the {@link ResourceFile} or null if no match was found.
+ */
+ public ResourceFile getFile(IFile file) {
+ if (mFiles != null) {
+ for (ResourceFile f : mFiles) {
+ IFile iFile = f.getFile().getIFile();
+ if (iFile != null && iFile.equals(file)) {
+ return f;
+ }
+ }
+ }
+ return null;
+ }
+
+
+ /**
+ * Returns the {@link ResourceFile} matching a given name.
+ * @param filename The name of the file to return.
+ * @return the {@link ResourceFile} or <code>null</code> if no match was found.
+ */
+ public ResourceFile getFile(String filename) {
+ if (mFiles != null) {
+ for (ResourceFile f : mFiles) {
+ if (f.getFile().getName().equals(filename)) {
+ return f;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns whether a file in the folder is generating a resource of a specified type.
+ * @param type The {@link ResourceType} being looked up.
+ */
+ public boolean hasResources(ResourceType type) {
+ // Check if the folder type is able to generate resource of the type that was asked.
+ // this is a first check to avoid going through the files.
+ ResourceFolderType[] folderTypes = FolderTypeRelationship.getRelatedFolders(type);
+
+ boolean valid = false;
+ for (ResourceFolderType rft : folderTypes) {
+ if (rft == mType) {
+ valid = true;
+ break;
+ }
+ }
+
+ if (valid) {
+ if (mFiles != null) {
+ for (ResourceFile f : mFiles) {
+ if (f.hasResources(type)) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Get the list of {@link ResourceItem} of a specific type generated by all the files
+ * in the folder.
+ * This method must make sure not to create duplicates.
+ * @param type The type of {@link ResourceItem} to return.
+ * @param projectResources The global Project Resource object, allowing the implementation to
+ * query for already existing {@link ResourceItem}
+ * @return The list of <b>new</b> {@link ResourceItem}
+ * @see ProjectResources#findResourceItem(ResourceType, String)
+ */
+ public Collection<ProjectResourceItem> getResources(ResourceType type,
+ ProjectResources projectResources) {
+ Collection<ProjectResourceItem> list = new ArrayList<ProjectResourceItem>();
+ if (mFiles != null) {
+ for (ResourceFile f : mFiles) {
+ list.addAll(f.getResources(type, projectResources));
+ }
+ }
+ return list;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceFolderType.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceFolderType.java
new file mode 100644
index 0000000..5fc7393
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceFolderType.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.manager;
+
+import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration;
+import com.android.sdklib.SdkConstants;
+
+/**
+ * Enum representing a type of resource folder.
+ */
+public enum ResourceFolderType {
+ ANIM(SdkConstants.FD_ANIM),
+ COLOR(SdkConstants.FD_COLOR),
+ DRAWABLE(SdkConstants.FD_DRAWABLE),
+ LAYOUT(SdkConstants.FD_LAYOUT),
+ MENU(SdkConstants.FD_MENU),
+ RAW(SdkConstants.FD_RAW),
+ VALUES(SdkConstants.FD_VALUES),
+ XML(SdkConstants.FD_XML);
+
+ private final String mName;
+
+ ResourceFolderType(String name) {
+ mName = name;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Returns the enum by name.
+ * @param name The enum string value.
+ * @return the enum or null if not found.
+ */
+ public static ResourceFolderType getTypeByName(String name) {
+ for (ResourceFolderType rType : values()) {
+ if (rType.mName.equals(name)) {
+ return rType;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the {@link ResourceFolderType} from the folder name
+ * @param folderName The name of the folder. This must be a valid folder name in the format
+ * <code>resType[-resqualifiers[-resqualifiers[...]]</code>
+ * @return the <code>ResourceFolderType</code> representing the type of the folder, or
+ * <code>null</code> if no matching type was found.
+ */
+ public static ResourceFolderType getFolderType(String folderName) {
+ // split the name of the folder in segments.
+ String[] folderSegments = folderName.split(FolderConfiguration.QUALIFIER_SEP);
+
+ // get the enum for the resource type.
+ return getTypeByName(folderSegments[0]);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceManager.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceManager.java
new file mode 100644
index 0000000..6099008
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceManager.java
@@ -0,0 +1,493 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.manager;
+
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.common.resources.ResourceType;
+import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration;
+import com.android.ide.eclipse.editors.resources.configurations.ResourceQualifier;
+import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IFileListener;
+import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IFolderListener;
+import com.android.ide.eclipse.editors.resources.manager.ResourceMonitor.IProjectListener;
+import com.android.ide.eclipse.editors.resources.manager.files.FileWrapper;
+import com.android.ide.eclipse.editors.resources.manager.files.FolderWrapper;
+import com.android.ide.eclipse.editors.resources.manager.files.IAbstractFile;
+import com.android.ide.eclipse.editors.resources.manager.files.IAbstractFolder;
+import com.android.ide.eclipse.editors.resources.manager.files.IFileWrapper;
+import com.android.ide.eclipse.editors.resources.manager.files.IFolderWrapper;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.SdkConstants;
+
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IMarkerDelta;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IResourceDelta;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+
+public final class ResourceManager implements IProjectListener, IFolderListener, IFileListener {
+
+ private final static ResourceManager sThis = new ResourceManager();
+
+ /** List of the qualifier object helping for the parsing of folder names */
+ private final ResourceQualifier[] mQualifiers;
+
+ /**
+ * Map associating project resource with project objects.
+ */
+ private final HashMap<IProject, ProjectResources> mMap =
+ new HashMap<IProject, ProjectResources>();
+
+ /**
+ * Sets up the resource manager with the global resource monitor.
+ * @param monitor The global resource monitor
+ */
+ public static void setup(ResourceMonitor monitor) {
+ monitor.addProjectListener(sThis);
+ int mask = IResourceDelta.ADDED | IResourceDelta.REMOVED | IResourceDelta.CHANGED;
+ monitor.addFolderListener(sThis, mask);
+ monitor.addFileListener(sThis, mask);
+
+ CompiledResourcesMonitor.setupMonitor(monitor);
+ }
+
+ /**
+ * Returns the singleton instance.
+ */
+ public static ResourceManager getInstance() {
+ return sThis;
+ }
+
+ /**
+ * Returns the resources of a project.
+ * @param project The project
+ * @return a ProjectResources object or null if none was found.
+ */
+ public ProjectResources getProjectResources(IProject project) {
+ return mMap.get(project);
+ }
+
+ /**
+ * Processes folder event.
+ */
+ public void folderChanged(IFolder folder, int kind) {
+ ProjectResources resources;
+
+ final IProject project = folder.getProject();
+
+ try {
+ if (project.hasNature(AndroidConstants.NATURE) == false) {
+ return;
+ }
+ } catch (CoreException e) {
+ // can't get the project nature? return!
+ return;
+ }
+
+ switch (kind) {
+ case IResourceDelta.ADDED:
+ // checks if the folder is under res.
+ IPath path = folder.getFullPath();
+
+ // the path will be project/res/<something>
+ if (path.segmentCount() == 3) {
+ if (isInResFolder(path)) {
+ // get the project and its resource object.
+ resources = mMap.get(project);
+
+ // if it doesn't exist, we create it.
+ if (resources == null) {
+ resources = new ProjectResources(false /* isFrameworkRepository */);
+ mMap.put(project, resources);
+ }
+
+ processFolder(new IFolderWrapper(folder), resources);
+ }
+ }
+ break;
+ case IResourceDelta.CHANGED:
+ resources = mMap.get(folder.getProject());
+ if (resources != null) {
+ ResourceFolder resFolder = resources.getResourceFolder(folder);
+ if (resFolder != null) {
+ resFolder.touch();
+ }
+ }
+ break;
+ case IResourceDelta.REMOVED:
+ resources = mMap.get(folder.getProject());
+ if (resources != null) {
+ // lets get the folder type
+ ResourceFolderType type = ResourceFolderType.getFolderType(folder.getName());
+
+ resources.removeFolder(type, folder);
+ }
+ break;
+ }
+ }
+
+ /* (non-Javadoc)
+ * Sent when a file changed. Depending on the file being changed, and the type of change (ADDED,
+ * REMOVED, CHANGED), the file change is processed to update the resource manager data.
+ *
+ * @param file The file that changed.
+ * @param markerDeltas The marker deltas for the file.
+ * @param kind The change kind. This is equivalent to
+ * {@link IResourceDelta#accept(IResourceDeltaVisitor)}
+ *
+ * @see IFileListener#fileChanged
+ */
+ public void fileChanged(IFile file, IMarkerDelta[] markerDeltas, int kind) {
+ ProjectResources resources;
+
+ final IProject project = file.getProject();
+
+ try {
+ if (project.hasNature(AndroidConstants.NATURE) == false) {
+ return;
+ }
+ } catch (CoreException e) {
+ // can't get the project nature? return!
+ return;
+ }
+
+ switch (kind) {
+ case IResourceDelta.ADDED:
+ // checks if the file is under res/something.
+ IPath path = file.getFullPath();
+
+ if (path.segmentCount() == 4) {
+ if (isInResFolder(path)) {
+ // get the project and its resources
+ resources = mMap.get(project);
+
+ IContainer container = file.getParent();
+ if (container instanceof IFolder && resources != null) {
+
+ ResourceFolder folder = resources.getResourceFolder((IFolder)container);
+
+ if (folder != null) {
+ processFile(new IFileWrapper(file), folder);
+ }
+ }
+ }
+ }
+ break;
+ case IResourceDelta.CHANGED:
+ // try to find a matching ResourceFile
+ resources = mMap.get(project);
+ if (resources != null) {
+ IContainer container = file.getParent();
+ if (container instanceof IFolder) {
+ ResourceFolder resFolder = resources.getResourceFolder((IFolder)container);
+
+ // we get the delete on the folder before the file, so it is possible
+ // the associated ResourceFolder doesn't exist anymore.
+ if (resFolder != null) {
+ // get the resourceFile, and touch it.
+ ResourceFile resFile = resFolder.getFile(file);
+ if (resFile != null) {
+ resFile.touch();
+ }
+ }
+ }
+ }
+ break;
+ case IResourceDelta.REMOVED:
+ // try to find a matching ResourceFile
+ resources = mMap.get(project);
+ if (resources != null) {
+ IContainer container = file.getParent();
+ if (container instanceof IFolder) {
+ ResourceFolder resFolder = resources.getResourceFolder((IFolder)container);
+
+ // we get the delete on the folder before the file, so it is possible
+ // the associated ResourceFolder doesn't exist anymore.
+ if (resFolder != null) {
+ // remove the file
+ resFolder.removeFile(file);
+ }
+ }
+ }
+ break;
+ }
+ }
+
+ public void projectClosed(IProject project) {
+ mMap.remove(project);
+ }
+
+ public void projectDeleted(IProject project) {
+ mMap.remove(project);
+ }
+
+ public void projectOpened(IProject project) {
+ createProject(project);
+ }
+
+ public void projectOpenedWithWorkspace(IProject project) {
+ createProject(project);
+ }
+
+ /**
+ * Returns the {@link ResourceFolder} for the given file or <code>null</code> if none exists.
+ */
+ public ResourceFolder getResourceFolder(IFile file) {
+ IContainer container = file.getParent();
+ if (container.getType() == IResource.FOLDER) {
+ IFolder parent = (IFolder)container;
+ IProject project = file.getProject();
+
+ ProjectResources resources = getProjectResources(project);
+ if (resources != null) {
+ return resources.getResourceFolder(parent);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Loads and returns the resources for a given {@link IAndroidTarget}
+ * @param androidTarget the target from which to load the framework resources
+ */
+ public ProjectResources loadFrameworkResources(IAndroidTarget androidTarget) {
+ String osResourcesPath = androidTarget.getPath(IAndroidTarget.RESOURCES);
+
+ File frameworkRes = new File(osResourcesPath);
+ if (frameworkRes.isDirectory()) {
+ ProjectResources resources = new ProjectResources(true /* isFrameworkRepository */);
+
+ try {
+ File[] files = frameworkRes.listFiles();
+ for (File file : files) {
+ if (file.isDirectory()) {
+ ResourceFolder resFolder = processFolder(new FolderWrapper(file),
+ resources);
+
+ if (resFolder != null) {
+ // now we process the content of the folder
+ File[] children = file.listFiles();
+
+ for (File childRes : children) {
+ if (childRes.isFile()) {
+ processFile(new FileWrapper(childRes), resFolder);
+ }
+ }
+ }
+
+ }
+ }
+
+ // now that we have loaded the files, we need to force load the resources from them
+ resources.loadAll();
+
+ return resources;
+
+ } catch (IOException e) {
+ // since we test that folders are folders, and files are files, this shouldn't
+ // happen. We can ignore it.
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Initial project parsing to gather resource info.
+ * @param project
+ */
+ private void createProject(IProject project) {
+ if (project.isOpen()) {
+ try {
+ if (project.hasNature(AndroidConstants.NATURE) == false) {
+ return;
+ }
+ } catch (CoreException e1) {
+ // can't check the nature of the project? ignore it.
+ return;
+ }
+
+ IFolder resourceFolder = project.getFolder(SdkConstants.FD_RESOURCES);
+
+ ProjectResources projectResources = mMap.get(project);
+ if (projectResources == null) {
+ projectResources = new ProjectResources(false /* isFrameworkRepository */);
+ mMap.put(project, projectResources);
+ }
+
+ if (resourceFolder != null && resourceFolder.exists()) {
+ try {
+ IResource[] resources = resourceFolder.members();
+
+ for (IResource res : resources) {
+ if (res.getType() == IResource.FOLDER) {
+ IFolder folder = (IFolder)res;
+ ResourceFolder resFolder = processFolder(new IFolderWrapper(folder),
+ projectResources);
+
+ if (resFolder != null) {
+ // now we process the content of the folder
+ IResource[] files = folder.members();
+
+ for (IResource fileRes : files) {
+ if (fileRes.getType() == IResource.FILE) {
+ IFile file = (IFile)fileRes;
+
+ processFile(new IFileWrapper(file), resFolder);
+ }
+ }
+ }
+ }
+ }
+ } catch (CoreException e) {
+ // This happens if the project is closed or if the folder doesn't exist.
+ // Since we already test for that, we can ignore this exception.
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates a {@link FolderConfiguration} matching the folder segments.
+ * @param folderSegments The segments of the folder name. The first segments should contain
+ * the name of the folder
+ * @return a FolderConfiguration object, or null if the folder name isn't valid..
+ */
+ public FolderConfiguration getConfig(String[] folderSegments) {
+ FolderConfiguration config = new FolderConfiguration();
+
+ // we are going to loop through the segments, and match them with the first
+ // available qualifier. If the segment doesn't match we try with the next qualifier.
+ // Because the order of the qualifier is fixed, we do not reset the first qualifier
+ // after each sucessful segment.
+ // If we run out of qualifier before processing all the segments, we fail.
+
+ int qualifierIndex = 0;
+ int qualifierCount = mQualifiers.length;
+
+ for (int i = 1 ; i < folderSegments.length; i++) {
+ String seg = folderSegments[i];
+ if (seg.length() > 0) {
+ while (qualifierIndex < qualifierCount &&
+ mQualifiers[qualifierIndex].checkAndSet(seg, config) == false) {
+ qualifierIndex++;
+ }
+
+ // if we reached the end of the qualifier we didn't find a matching qualifier.
+ if (qualifierIndex == qualifierCount) {
+ return null;
+ }
+
+ } else {
+ return null;
+ }
+ }
+
+ return config;
+ }
+
+ /**
+ * Processes a folder and adds it to the list of the project resources.
+ * @param folder the folder to process
+ * @param project the folder's project.
+ * @return the ConfiguredFolder created from this folder, or null if the process failed.
+ */
+ private ResourceFolder processFolder(IAbstractFolder folder, ProjectResources project) {
+ // split the name of the folder in segments.
+ String[] folderSegments = folder.getName().split(FolderConfiguration.QUALIFIER_SEP);
+
+ // get the enum for the resource type.
+ ResourceFolderType type = ResourceFolderType.getTypeByName(folderSegments[0]);
+
+ if (type != null) {
+ // get the folder configuration.
+ FolderConfiguration config = getConfig(folderSegments);
+
+ if (config != null) {
+ ResourceFolder configuredFolder = project.add(type, config, folder);
+
+ return configuredFolder;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Processes a file and adds it to its parent folder resource.
+ * @param file
+ * @param folder
+ */
+ private void processFile(IAbstractFile file, ResourceFolder folder) {
+ // get the type of the folder
+ ResourceFolderType type = folder.getType();
+
+ // look for this file if it's already been created
+ ResourceFile resFile = folder.getFile(file);
+
+ if (resFile != null) {
+ // invalidate the file
+ resFile.touch();
+ } else {
+ // create a ResourceFile for it.
+
+ // check if that's a single or multi resource type folder. For now we define this by
+ // the number of possible resource type output by files in the folder. This does
+ // not make the difference between several resource types from a single file or
+ // the ability to have 2 files in the same folder generating 2 different types of
+ // resource. The former is handled by MultiResourceFile properly while we don't
+ // handle the latter. If we were to add this behavior we'd have to change this call.
+ ResourceType[] types = FolderTypeRelationship.getRelatedResourceTypes(type);
+
+ if (types.length == 1) {
+ resFile = new SingleResourceFile(file, folder);
+ } else {
+ resFile = new MultiResourceFile(file, folder);
+ }
+
+ // add it to the folder
+ folder.addFile(resFile);
+ }
+ }
+
+ /**
+ * Returns true if the path is under /project/res/
+ * @param path a workspace relative path
+ * @return true if the path is under /project res/
+ */
+ private boolean isInResFolder(IPath path) {
+ return SdkConstants.FD_RESOURCES.equalsIgnoreCase(path.segment(1));
+ }
+
+ /**
+ * Private constructor to enforce singleton design.
+ */
+ ResourceManager() {
+ // get the default qualifiers.
+ FolderConfiguration defaultConfig = new FolderConfiguration();
+ defaultConfig.createDefault();
+ mQualifiers = defaultConfig.getQualifiers();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceMonitor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceMonitor.java
new file mode 100644
index 0000000..59a72fb
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/ResourceMonitor.java
@@ -0,0 +1,377 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.manager;
+
+import com.android.ide.eclipse.common.project.BaseProjectHelper;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IMarkerDelta;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IResourceChangeEvent;
+import org.eclipse.core.resources.IResourceChangeListener;
+import org.eclipse.core.resources.IResourceDelta;
+import org.eclipse.core.resources.IResourceDeltaVisitor;
+import org.eclipse.core.resources.IWorkspace;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.jdt.core.IJavaModel;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+
+import java.util.ArrayList;
+
+/**
+ * Resource Monitor for the whole editor plugin. Other, more simple, listeners can register to
+ * that one.
+ */
+public class ResourceMonitor implements IResourceChangeListener {
+
+ private final static ResourceMonitor sThis = new ResourceMonitor();
+
+ /**
+ * Classes which implement this interface provide a method that deals
+ * with file change events.
+ */
+ public interface IFileListener {
+ /**
+ * Sent when a file changed.
+ * @param file The file that changed.
+ * @param markerDeltas The marker deltas for the file.
+ * @param kind The change kind. This is equivalent to
+ * {@link IResourceDelta#accept(IResourceDeltaVisitor)}
+ */
+ public void fileChanged(IFile file, IMarkerDelta[] markerDeltas, int kind);
+ }
+
+ /**
+ * Classes which implements this interface provide methods dealing with project events.
+ */
+ public interface IProjectListener {
+ /**
+ * Sent for each opened android project at the time the listener is put in place.
+ * @param project the opened project.
+ */
+ public void projectOpenedWithWorkspace(IProject project);
+ /**
+ * Sent when a project is opened.
+ * @param project the project being opened.
+ */
+ public void projectOpened(IProject project);
+ /**
+ * Sent when a project is closed.
+ * @param project the project being closed.
+ */
+ public void projectClosed(IProject project);
+ /**
+ * Sent when a project is deleted.
+ * @param project the project about to be deleted.
+ */
+ public void projectDeleted(IProject project);
+ }
+
+ /**
+ * Classes which implement this interface provide a method that deals
+ * with folder change events
+ */
+ public interface IFolderListener {
+ /**
+ * Sent when a folder changed.
+ * @param folder The file that was changed
+ * @param kind The change kind. This is equivalent to {@link IResourceDelta#getKind()}
+ */
+ public void folderChanged(IFolder folder, int kind);
+ }
+
+ /**
+ * Interface for a listener to be notified when resource change event starts and ends.
+ */
+ public interface IResourceEventListener {
+ public void resourceChangeEventStart();
+ public void resourceChangeEventEnd();
+ }
+
+ /**
+ * Base listener bundle to associate a listener to an event mask.
+ */
+ private static class ListenerBundle {
+ /** Mask value to accept all events */
+ public final static int MASK_NONE = -1;
+
+ /**
+ * Event mask. Values accepted are IResourceDelta.###
+ * @see IResourceDelta#ADDED
+ * @see IResourceDelta#REMOVED
+ * @see IResourceDelta#CHANGED
+ * @see IResourceDelta#ADDED_PHANTOM
+ * @see IResourceDelta#REMOVED_PHANTOM
+ * */
+ int kindMask;
+ }
+
+ /**
+ * Listener bundle for file event.
+ */
+ private static class FileListenerBundle extends ListenerBundle {
+
+ /** The file listener */
+ IFileListener listener;
+ }
+
+ /**
+ * Listener bundle for folder event.
+ */
+ private static class FolderListenerBundle extends ListenerBundle {
+ /** The file listener */
+ IFolderListener listener;
+ }
+
+ private final ArrayList<FileListenerBundle> mFileListeners =
+ new ArrayList<FileListenerBundle>();
+
+ private final ArrayList<FolderListenerBundle> mFolderListeners =
+ new ArrayList<FolderListenerBundle>();
+
+ private final ArrayList<IProjectListener> mProjectListeners = new ArrayList<IProjectListener>();
+
+ private final ArrayList<IResourceEventListener> mEventListeners =
+ new ArrayList<IResourceEventListener>();
+
+ private IWorkspace mWorkspace;
+
+ /**
+ * Delta visitor for resource changes.
+ */
+ private final class DeltaVisitor implements IResourceDeltaVisitor {
+
+ public boolean visit(IResourceDelta delta) {
+ IResource r = delta.getResource();
+ int type = r.getType();
+ if (type == IResource.FILE) {
+ int kind = delta.getKind();
+ // notify the listeners.
+ for (FileListenerBundle bundle : mFileListeners) {
+ if (bundle.kindMask == ListenerBundle.MASK_NONE
+ || (bundle.kindMask & kind) != 0) {
+ bundle.listener.fileChanged((IFile)r, delta.getMarkerDeltas(), kind);
+ }
+ }
+ return false;
+ } else if (type == IResource.FOLDER) {
+ int kind = delta.getKind();
+ // notify the listeners.
+ for (FolderListenerBundle bundle : mFolderListeners) {
+ if (bundle.kindMask == ListenerBundle.MASK_NONE
+ || (bundle.kindMask & kind) != 0) {
+ bundle.listener.folderChanged((IFolder)r, kind);
+ }
+ }
+ return true;
+ } else if (type == IResource.PROJECT) {
+ int flags = delta.getFlags();
+
+ if (flags == IResourceDelta.OPEN) {
+ // the project is opening or closing.
+ IProject project = (IProject)r;
+
+ if (project.isOpen()) {
+ // notify the listeners.
+ for (IProjectListener pl : mProjectListeners) {
+ pl.projectOpened(project);
+ }
+ } else {
+ // notify the listeners.
+ for (IProjectListener pl : mProjectListeners) {
+ pl.projectClosed(project);
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+ }
+
+ public static ResourceMonitor getMonitor() {
+ return sThis;
+ }
+
+
+ /**
+ * Starts the resource monitoring.
+ * @param ws The current workspace.
+ * @return The monitor object.
+ */
+ public static ResourceMonitor startMonitoring(IWorkspace ws) {
+ if (sThis != null) {
+ ws.addResourceChangeListener(sThis,
+ IResourceChangeEvent.POST_CHANGE | IResourceChangeEvent.PRE_DELETE);
+ sThis.mWorkspace = ws;
+ }
+ return sThis;
+ }
+
+ /**
+ * Stops the resource monitoring.
+ * @param ws The current workspace.
+ */
+ public static void stopMonitoring(IWorkspace ws) {
+ if (sThis != null) {
+ ws.removeResourceChangeListener(sThis);
+
+ sThis.mFileListeners.clear();
+ sThis.mProjectListeners.clear();
+ }
+ }
+
+ /**
+ * Adds a file listener.
+ * @param listener The listener to receive the events.
+ * @param kindMask The event mask to filter out specific events.
+ * {@link ListenerBundle#MASK_NONE} will forward all events.
+ */
+ public synchronized void addFileListener(IFileListener listener, int kindMask) {
+ FileListenerBundle bundle = new FileListenerBundle();
+ bundle.listener = listener;
+ bundle.kindMask = kindMask;
+
+ mFileListeners.add(bundle);
+ }
+
+ /**
+ * Removes an existing file listener.
+ * @param listener the listener to remove.
+ */
+ public synchronized void removeFileListener(IFileListener listener) {
+ for (int i = 0 ; i < mFileListeners.size() ; i++) {
+ FileListenerBundle bundle = mFileListeners.get(i);
+ if (bundle.listener == listener) {
+ mFileListeners.remove(i);
+ return;
+ }
+ }
+ }
+
+ /**
+ * Adds a folder listener.
+ * @param listener The listener to receive the events.
+ * @param kindMask The event mask to filter out specific events.
+ * {@link ListenerBundle#MASK_NONE} will forward all events.
+ */
+ public synchronized void addFolderListener(IFolderListener listener, int kindMask) {
+ FolderListenerBundle bundle = new FolderListenerBundle();
+ bundle.listener = listener;
+ bundle.kindMask = kindMask;
+
+ mFolderListeners.add(bundle);
+ }
+
+ /**
+ * Removes an existing folder listener.
+ * @param listener the listener to remove.
+ */
+ public synchronized void removeFolderListener(IFolderListener listener) {
+ for (int i = 0 ; i < mFolderListeners.size() ; i++) {
+ FolderListenerBundle bundle = mFolderListeners.get(i);
+ if (bundle.listener == listener) {
+ mFolderListeners.remove(i);
+ return;
+ }
+ }
+ }
+
+ /**
+ * Adds a project listener.
+ * @param listener The listener to receive the events.
+ */
+ public synchronized void addProjectListener(IProjectListener listener) {
+ mProjectListeners.add(listener);
+
+ // we need to look at the opened projects and give them to the listener.
+
+ // get the list of opened android projects.
+ IWorkspaceRoot workspaceRoot = mWorkspace.getRoot();
+ IJavaModel javaModel = JavaCore.create(workspaceRoot);
+ IJavaProject[] androidProjects = BaseProjectHelper.getAndroidProjects(javaModel);
+
+ for (IJavaProject androidProject : androidProjects) {
+ listener.projectOpenedWithWorkspace(androidProject.getProject());
+ }
+ }
+
+ /**
+ * Removes an existing project listener.
+ * @param listener the listener to remove.
+ */
+ public synchronized void removeProjectListener(IProjectListener listener) {
+ mProjectListeners.remove(listener);
+ }
+
+ /**
+ * Adds a resource event listener.
+ * @param listener The listener to receive the events.
+ */
+ public synchronized void addResourceEventListener(IResourceEventListener listener) {
+ mEventListeners.add(listener);
+ }
+
+ /**
+ * Removes an existing Resource Event listener.
+ * @param listener the listener to remove.
+ */
+ public synchronized void removeResourceEventListener(IResourceEventListener listener) {
+ mEventListeners.remove(listener);
+ }
+
+ /**
+ * Processes the workspace resource change events.
+ */
+ public void resourceChanged(IResourceChangeEvent event) {
+ // notify the event listeners of a start.
+ for (IResourceEventListener listener : mEventListeners) {
+ listener.resourceChangeEventStart();
+ }
+
+ if (event.getType() == IResourceChangeEvent.PRE_DELETE) {
+ // a project is being deleted. Lets get the project object and remove
+ // its compiled resource list.
+ IResource r = event.getResource();
+ IProject project = r.getProject();
+
+ // notify the listeners.
+ for (IProjectListener pl : mProjectListeners) {
+ pl.projectDeleted(project);
+ }
+ } else {
+ // this a regular resource change. We get the delta and go through it with a visitor.
+ IResourceDelta delta = event.getDelta();
+
+ DeltaVisitor visitor = new DeltaVisitor();
+ try {
+ delta.accept(visitor);
+ } catch (CoreException e) {
+ }
+ }
+
+ // we're done, notify the event listeners.
+ for (IResourceEventListener listener : mEventListeners) {
+ listener.resourceChangeEventEnd();
+ }
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/SingleResourceFile.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/SingleResourceFile.java
new file mode 100644
index 0000000..32b1107
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/SingleResourceFile.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.manager;
+
+import com.android.ide.eclipse.common.resources.ResourceType;
+import com.android.ide.eclipse.editors.resources.manager.files.IAbstractFile;
+import com.android.layoutlib.api.IResourceValue;
+import com.android.layoutlib.utils.ResourceValue;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.xml.parsers.SAXParserFactory;
+
+/**
+ * Represents a resource file describing a single resource.
+ * <p/>
+ * This is typically an XML file inside res/anim, res/layout, or res/menu or an image file
+ * under res/drawable.
+ */
+public class SingleResourceFile extends ResourceFile {
+
+ private final static SAXParserFactory sParserFactory = SAXParserFactory.newInstance();
+ static {
+ sParserFactory.setNamespaceAware(true);
+ }
+
+ private final static Pattern sXmlPattern = Pattern.compile("^(.+)\\.xml", //$NON-NLS-1$
+ Pattern.CASE_INSENSITIVE);
+
+ private final static Pattern[] sDrawablePattern = new Pattern[] {
+ Pattern.compile("^(.+)\\.9\\.png", Pattern.CASE_INSENSITIVE), //$NON-NLS-1$
+ Pattern.compile("^(.+)\\.png", Pattern.CASE_INSENSITIVE), //$NON-NLS-1$
+ Pattern.compile("^(.+)\\.jpg", Pattern.CASE_INSENSITIVE), //$NON-NLS-1$
+ Pattern.compile("^(.+)\\.gif", Pattern.CASE_INSENSITIVE), //$NON-NLS-1$
+ };
+
+ private String mResourceName;
+ private ResourceType mType;
+ private IResourceValue mValue;
+
+ public SingleResourceFile(IAbstractFile file, ResourceFolder folder) {
+ super(file, folder);
+
+ // we need to infer the type of the resource from the folder type.
+ // This is easy since this is a single Resource file.
+ ResourceType[] types = FolderTypeRelationship.getRelatedResourceTypes(folder.getType());
+ mType = types[0];
+
+ // compute the resource name
+ mResourceName = getResourceName(mType);
+
+ mValue = new ResourceValue(mType.getName(), getResourceName(mType), file.getOsLocation(),
+ isFramework());
+ }
+
+ @Override
+ public ResourceType[] getResourceTypes() {
+ return FolderTypeRelationship.getRelatedResourceTypes(getFolder().getType());
+ }
+
+ @Override
+ public boolean hasResources(ResourceType type) {
+ return FolderTypeRelationship.match(type, getFolder().getType());
+ }
+
+ @Override
+ public Collection<ProjectResourceItem> getResources(ResourceType type,
+ ProjectResources projectResources) {
+
+ // looking for an existing ResourceItem with this name and type
+ ProjectResourceItem item = projectResources.findResourceItem(type, mResourceName);
+
+ ArrayList<ProjectResourceItem> items = new ArrayList<ProjectResourceItem>();
+
+ if (item == null) {
+ item = new ConfigurableResourceItem(mResourceName);
+ items.add(item);
+ }
+
+ // add this ResourceFile to the ResourceItem
+ item.add(this);
+
+ return items;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see com.android.ide.eclipse.editors.resources.manager.ResourceFile#getValue(com.android.ide.eclipse.common.resources.ResourceType, java.lang.String)
+ *
+ * This particular implementation does not care about the type or name since a
+ * SingleResourceFile represents a file generating only one resource.
+ * The value returned is the full absolute path of the file in OS form.
+ */
+ @Override
+ public IResourceValue getValue(ResourceType type, String name) {
+ return mValue;
+ }
+
+ /**
+ * Returns the name of the resources.
+ */
+ private String getResourceName(ResourceType type) {
+ // get the name from the filename.
+ String name = getFile().getName();
+
+ if (type == ResourceType.ANIM || type == ResourceType.LAYOUT || type == ResourceType.MENU ||
+ type == ResourceType.COLOR || type == ResourceType.XML) {
+ Matcher m = sXmlPattern.matcher(name);
+ if (m.matches()) {
+ return m.group(1);
+ }
+ } else if (type == ResourceType.DRAWABLE) {
+ for (Pattern p : sDrawablePattern) {
+ Matcher m = p.matcher(name);
+ if (m.matches()) {
+ return m.group(1);
+ }
+ }
+
+ // also try the Xml pattern for selector/shape based drawable.
+ Matcher m = sXmlPattern.matcher(name);
+ if (m.matches()) {
+ return m.group(1);
+ }
+ }
+ return name;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/FileWrapper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/FileWrapper.java
new file mode 100644
index 0000000..d99cb13
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/FileWrapper.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.manager.files;
+
+import org.eclipse.core.resources.IFile;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * An implementation of {@link IAbstractFile} on top of a {@link File} object.
+ *
+ */
+public class FileWrapper implements IAbstractFile {
+
+ private File mFile;
+
+ /**
+ * Constructs a {@link FileWrapper} object. If {@link File#isFile()} returns <code>false</code>
+ * then an {@link IOException} is thrown.
+ */
+ public FileWrapper(File file) throws IOException {
+ if (file.isFile() == false) {
+ throw new IOException("FileWrapper must wrap a File object representing an existing file!"); //$NON-NLS-1$
+ }
+
+ mFile = file;
+ }
+
+ public InputStream getContents() {
+ try {
+ return new FileInputStream(mFile);
+ } catch (FileNotFoundException e) {
+ // we'll return null below.
+ }
+
+ return null;
+ }
+
+ public IFile getIFile() {
+ return null;
+ }
+
+ public String getOsLocation() {
+ return mFile.getAbsolutePath();
+ }
+
+ public String getName() {
+ return mFile.getName();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof FileWrapper) {
+ return mFile.equals(((FileWrapper)obj).mFile);
+ }
+
+ if (obj instanceof File) {
+ return mFile.equals(obj);
+ }
+
+ return super.equals(obj);
+ }
+
+ @Override
+ public int hashCode() {
+ return mFile.hashCode();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/FolderWrapper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/FolderWrapper.java
new file mode 100644
index 0000000..9ad7460
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/FolderWrapper.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.manager.files;
+
+import org.eclipse.core.resources.IFolder;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * An implementation of {@link IAbstractFolder} on top of a {@link File} object.
+ */
+public class FolderWrapper implements IAbstractFolder {
+
+ private File mFolder;
+
+ /**
+ * Constructs a {@link FileWrapper} object. If {@link File#isDirectory()} returns
+ * <code>false</code> then an {@link IOException} is thrown.
+ */
+ public FolderWrapper(File folder) throws IOException {
+ if (folder.isDirectory() == false) {
+ throw new IOException("FileWrapper must wrap a File object representing an existing folder!"); //$NON-NLS-1$
+ }
+
+ mFolder = folder;
+ }
+
+ public boolean hasFile(String name) {
+ return false;
+ }
+
+ public String getName() {
+ return mFolder.getName();
+ }
+
+ public IFolder getIFolder() {
+ return null;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof FolderWrapper) {
+ return mFolder.equals(((FolderWrapper)obj).mFolder);
+ }
+
+ if (obj instanceof File) {
+ return mFolder.equals(obj);
+ }
+
+ return super.equals(obj);
+ }
+
+ @Override
+ public int hashCode() {
+ return mFolder.hashCode();
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IAbstractFile.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IAbstractFile.java
new file mode 100644
index 0000000..7e807f9
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IAbstractFile.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.manager.files;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.runtime.CoreException;
+
+import java.io.InputStream;
+
+/**
+ * A file.
+ */
+public interface IAbstractFile extends IAbstractResource {
+
+ /**
+ * Returns an {@link InputStream} object on the file content.
+ * @throws CoreException
+ */
+ InputStream getContents() throws CoreException;
+
+ /**
+ * Returns the OS path of the file location.
+ */
+ String getOsLocation();
+
+ /**
+ * Returns the {@link IFile} object that the receiver could represent. Can be <code>null</code>
+ */
+ IFile getIFile();
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IAbstractFolder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IAbstractFolder.java
new file mode 100644
index 0000000..b35283d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IAbstractFolder.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.manager.files;
+
+import org.eclipse.core.resources.IFolder;
+
+/**
+ * A folder.
+ */
+public interface IAbstractFolder extends IAbstractResource {
+
+ /**
+ * Returns true if the receiver contains a file with a given name
+ * @param name the name of the file. This is the name without the path leading to the
+ * parent folder.
+ */
+ boolean hasFile(String name);
+
+ /**
+ * Returns the {@link IFolder} object that the receiver could represent.
+ * Can be <code>null</code>
+ */
+ IFolder getIFolder();
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IAbstractResource.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IAbstractResource.java
new file mode 100644
index 0000000..daf243d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IAbstractResource.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.manager.files;
+
+import org.eclipse.core.resources.IFile;
+
+import java.io.File;
+
+/**
+ * Base representation of a file system resource.<p/>
+ * This somewhat limited interface is designed to let classes use file-system resources, without
+ * having the manually handle {@link IFile} and/or {@link File} manually.
+ */
+public interface IAbstractResource {
+
+ /**
+ * Returns the name of the resource.
+ */
+ String getName();
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IFileWrapper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IFileWrapper.java
new file mode 100644
index 0000000..f0f5f2d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IFileWrapper.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.manager.files;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.runtime.CoreException;
+
+import java.io.InputStream;
+
+/**
+ * An implementation of {@link IAbstractFile} on top of an {@link IFile} object.
+ */
+public class IFileWrapper implements IAbstractFile {
+
+ private IFile mFile;
+
+ public IFileWrapper(IFile file) {
+ mFile = file;
+ }
+
+ public InputStream getContents() throws CoreException {
+ return mFile.getContents();
+ }
+
+ public String getOsLocation() {
+ return mFile.getLocation().toOSString();
+ }
+
+ public String getName() {
+ return mFile.getName();
+ }
+
+ public IFile getIFile() {
+ return mFile;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof IFileWrapper) {
+ return mFile.equals(((IFileWrapper)obj).mFile);
+ }
+
+ if (obj instanceof IFile) {
+ return mFile.equals(obj);
+ }
+
+ return super.equals(obj);
+ }
+
+ @Override
+ public int hashCode() {
+ return mFile.hashCode();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IFolderWrapper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IFolderWrapper.java
new file mode 100644
index 0000000..b1fa3ef
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/manager/files/IFolderWrapper.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.manager.files;
+
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+
+/**
+ * An implementation of {@link IAbstractFolder} on top of an {@link IFolder} object.
+ */
+public class IFolderWrapper implements IAbstractFolder {
+
+ private IFolder mFolder;
+
+ public IFolderWrapper(IFolder folder) {
+ mFolder = folder;
+ }
+
+ public String getName() {
+ return mFolder.getName();
+ }
+
+ public boolean hasFile(String name) {
+ try {
+ IResource[] files = mFolder.members();
+ for (IResource file : files) {
+ if (name.equals(file.getName())) {
+ return true;
+ }
+ }
+ } catch (CoreException e) {
+ // we'll return false below.
+ }
+
+ return false;
+ }
+
+ public IFolder getIFolder() {
+ return mFolder;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof IFolderWrapper) {
+ return mFolder.equals(((IFolderWrapper)obj).mFolder);
+ }
+
+ if (obj instanceof IFolder) {
+ return mFolder.equals(obj);
+ }
+
+ return super.equals(obj);
+ }
+
+ @Override
+ public int hashCode() {
+ return mFolder.hashCode();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/uimodel/UiColorValueNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/uimodel/UiColorValueNode.java
new file mode 100644
index 0000000..29453e9
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/uimodel/UiColorValueNode.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.uimodel;
+
+import com.android.ide.eclipse.editors.descriptors.TextValueDescriptor;
+import com.android.ide.eclipse.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+import com.android.ide.eclipse.editors.uimodel.UiTextValueNode;
+
+import org.eclipse.jface.dialogs.IMessageProvider;
+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.widgets.Text;
+
+import java.util.regex.Pattern;
+
+/**
+ * Displays and edits a color XML element value with a custom validator.
+ * <p/>
+ * See {@link UiAttributeNode} for more information.
+ */
+public class UiColorValueNode extends UiTextValueNode {
+
+ /** Accepted RGBA formats are one of #RGB, #ARGB, #RRGGBB or #AARRGGBB. */
+ private static final Pattern RGBA_REGEXP = Pattern.compile(
+ "#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})"); //$NON-NLS-1$
+
+ public UiColorValueNode(TextValueDescriptor attributeDescriptor, UiElementNode uiParent) {
+ super(attributeDescriptor, uiParent);
+ }
+
+ /* (non-java doc)
+ *
+ * Add a modify listener that will check colors have the proper format,
+ * that is one of #RGB, #ARGB, #RRGGBB or #AARRGGBB.
+ */
+ @Override
+ protected void onAddValidators(final Text text) {
+ ModifyListener listener = new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ String color = text.getText();
+ if (RGBA_REGEXP.matcher(color).matches()) {
+ getManagedForm().getMessageManager().removeMessage(text, text);
+ } else {
+ getManagedForm().getMessageManager().addMessage(text,
+ "Accepted color formats are one of #RGB, #ARGB, #RRGGBB or #AARRGGBB.",
+ null /* data */, IMessageProvider.ERROR, text);
+ }
+ }
+ };
+
+ text.addModifyListener(listener);
+
+ // Make sure the validator removes its message(s) when the widget is disposed
+ text.addDisposeListener(new DisposeListener() {
+ public void widgetDisposed(DisposeEvent e) {
+ getManagedForm().getMessageManager().removeMessage(text, text);
+ }
+ });
+
+ // Finally call the validator once to make sure the initial value is processed
+ listener.modifyText(null);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/uimodel/UiItemElementNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/uimodel/UiItemElementNode.java
new file mode 100644
index 0000000..89649f5
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/resources/uimodel/UiItemElementNode.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.uimodel;
+
+import com.android.ide.eclipse.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.editors.resources.descriptors.ItemElementDescriptor;
+import com.android.ide.eclipse.editors.resources.descriptors.ResourcesDescriptors;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+/**
+ * {@link UiItemElementNode} is apecial version of {@link UiElementNode} that
+ * customizes the element display to include the item type attribute if present.
+ */
+public class UiItemElementNode extends UiElementNode {
+
+ /**
+ * Creates a new {@link UiElementNode} described by a given {@link ItemElementDescriptor}.
+ *
+ * @param elementDescriptor The {@link ItemElementDescriptor} for the XML node. Cannot be null.
+ */
+ public UiItemElementNode(ItemElementDescriptor elementDescriptor) {
+ super(elementDescriptor);
+ }
+
+ @Override
+ public String getShortDescription() {
+ Node xmlNode = getXmlNode();
+ if (xmlNode != null && xmlNode instanceof Element && xmlNode.hasAttributes()) {
+
+ Element elem = (Element) xmlNode;
+ String type = elem.getAttribute(ResourcesDescriptors.TYPE_ATTR);
+ String name = elem.getAttribute(ResourcesDescriptors.NAME_ATTR);
+ if (type != null && name != null && type.length() > 0 && name.length() > 0) {
+ type = DescriptorsUtils.capitalize(type);
+ return String.format("%1$s (%2$s %3$s)", name, type, getDescriptor().getUiName());
+ }
+ }
+
+ return super.getShortDescription();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/EditableDialogCellEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/EditableDialogCellEditor.java
new file mode 100644
index 0000000..5fb479f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/EditableDialogCellEditor.java
@@ -0,0 +1,458 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.ui;
+
+import org.eclipse.core.runtime.Assert;
+import org.eclipse.jface.viewers.DialogCellEditor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.FocusAdapter;
+import org.eclipse.swt.events.FocusEvent;
+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.MouseAdapter;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.TraverseEvent;
+import org.eclipse.swt.events.TraverseListener;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Text;
+
+import java.text.MessageFormat;
+
+/**
+ * Custom DialogCellEditor, replacing the Label with an editable {@link Text} widget.
+ * <p/>Also set the button to {@link SWT#FLAT} to make sure it looks good on MacOS X.
+ * <p/>Most of the code comes from TextCellEditor.
+ */
+public abstract class EditableDialogCellEditor extends DialogCellEditor {
+
+ private Text text;
+
+ private ModifyListener modifyListener;
+
+ /**
+ * State information for updating action enablement
+ */
+ private boolean isSelection = false;
+
+ private boolean isDeleteable = false;
+
+ private boolean isSelectable = false;
+
+ EditableDialogCellEditor(Composite parent) {
+ super(parent);
+ }
+
+ /*
+ * Re-implement this method only to properly set the style in the button, or it won't look
+ * good in MacOS X
+ */
+ @Override
+ protected Button createButton(Composite parent) {
+ Button result = new Button(parent, SWT.DOWN | SWT.FLAT);
+ result.setText("..."); //$NON-NLS-1$
+ return result;
+ }
+
+
+ @Override
+ protected Control createContents(Composite cell) {
+ text = new Text(cell, SWT.SINGLE);
+ text.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetDefaultSelected(SelectionEvent e) {
+ handleDefaultSelection(e);
+ }
+ });
+ text.addKeyListener(new KeyAdapter() {
+ // hook key pressed - see PR 14201
+ @Override
+ public void keyPressed(KeyEvent e) {
+ keyReleaseOccured(e);
+
+ // as a result of processing the above call, clients may have
+ // disposed this cell editor
+ if ((getControl() == null) || getControl().isDisposed()) {
+ return;
+ }
+ checkSelection(); // see explanation below
+ checkDeleteable();
+ checkSelectable();
+ }
+ });
+ text.addTraverseListener(new TraverseListener() {
+ public void keyTraversed(TraverseEvent e) {
+ if (e.detail == SWT.TRAVERSE_ESCAPE
+ || e.detail == SWT.TRAVERSE_RETURN) {
+ e.doit = false;
+ }
+ }
+ });
+ // We really want a selection listener but it is not supported so we
+ // use a key listener and a mouse listener to know when selection changes
+ // may have occurred
+ text.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseUp(MouseEvent e) {
+ checkSelection();
+ checkDeleteable();
+ checkSelectable();
+ }
+ });
+ text.addFocusListener(new FocusAdapter() {
+ @Override
+ public void focusLost(FocusEvent e) {
+ EditableDialogCellEditor.this.focusLost();
+ }
+ });
+ text.setFont(cell.getFont());
+ text.setBackground(cell.getBackground());
+ text.setText("");//$NON-NLS-1$
+ text.addModifyListener(getModifyListener());
+ return text;
+ }
+
+ /**
+ * Checks to see if the "deletable" state (can delete/
+ * nothing to delete) has changed and if so fire an
+ * enablement changed notification.
+ */
+ private void checkDeleteable() {
+ boolean oldIsDeleteable = isDeleteable;
+ isDeleteable = isDeleteEnabled();
+ if (oldIsDeleteable != isDeleteable) {
+ fireEnablementChanged(DELETE);
+ }
+ }
+
+ /**
+ * Checks to see if the "selectable" state (can select)
+ * has changed and if so fire an enablement changed notification.
+ */
+ private void checkSelectable() {
+ boolean oldIsSelectable = isSelectable;
+ isSelectable = isSelectAllEnabled();
+ if (oldIsSelectable != isSelectable) {
+ fireEnablementChanged(SELECT_ALL);
+ }
+ }
+
+ /**
+ * Checks to see if the selection state (selection /
+ * no selection) has changed and if so fire an
+ * enablement changed notification.
+ */
+ private void checkSelection() {
+ boolean oldIsSelection = isSelection;
+ isSelection = text.getSelectionCount() > 0;
+ if (oldIsSelection != isSelection) {
+ fireEnablementChanged(COPY);
+ fireEnablementChanged(CUT);
+ }
+ }
+
+ /* (non-Javadoc)
+ * Method declared on CellEditor.
+ */
+ @Override
+ protected void doSetFocus() {
+ if (text != null) {
+ text.selectAll();
+ text.setFocus();
+ checkSelection();
+ checkDeleteable();
+ checkSelectable();
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.eclipse.jface.viewers.DialogCellEditor#updateContents(java.lang.Object)
+ */
+ @Override
+ protected void updateContents(Object value) {
+ Assert.isTrue(text != null && (value == null || (value instanceof String)));
+ if (value != null) {
+ text.removeModifyListener(getModifyListener());
+ text.setText((String) value);
+ text.addModifyListener(getModifyListener());
+ }
+ }
+
+ /**
+ * The <code>TextCellEditor</code> implementation of
+ * this <code>CellEditor</code> framework method returns
+ * the text string.
+ *
+ * @return the text string
+ */
+ @Override
+ protected Object doGetValue() {
+ return text.getText();
+ }
+
+
+ /**
+ * Processes a modify event that occurred in this text cell editor.
+ * This framework method performs validation and sets the error message
+ * accordingly, and then reports a change via <code>fireEditorValueChanged</code>.
+ * Subclasses should call this method at appropriate times. Subclasses
+ * may extend or reimplement.
+ *
+ * @param e the SWT modify event
+ */
+ protected void editOccured(ModifyEvent e) {
+ String value = text.getText();
+ if (value == null) {
+ value = "";//$NON-NLS-1$
+ }
+ Object typedValue = value;
+ boolean oldValidState = isValueValid();
+ boolean newValidState = isCorrect(typedValue);
+
+ if (!newValidState) {
+ // try to insert the current value into the error message.
+ setErrorMessage(MessageFormat.format(getErrorMessage(),
+ new Object[] { value }));
+ }
+ valueChanged(oldValidState, newValidState);
+ }
+
+ /**
+ * Return the modify listener.
+ */
+ private ModifyListener getModifyListener() {
+ if (modifyListener == null) {
+ modifyListener = new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ editOccured(e);
+ }
+ };
+ }
+ return modifyListener;
+ }
+
+ /**
+ * Handles a default selection event from the text control by applying the editor
+ * value and deactivating this cell editor.
+ *
+ * @param event the selection event
+ *
+ * @since 3.0
+ */
+ protected void handleDefaultSelection(SelectionEvent event) {
+ // same with enter-key handling code in keyReleaseOccured(e);
+ fireApplyEditorValue();
+ deactivate();
+ }
+
+ /**
+ * The <code>TextCellEditor</code> implementation of this
+ * <code>CellEditor</code> method returns <code>true</code> if
+ * the current selection is not empty.
+ */
+ @Override
+ public boolean isCopyEnabled() {
+ if (text == null || text.isDisposed()) {
+ return false;
+ }
+ return text.getSelectionCount() > 0;
+ }
+
+ /**
+ * The <code>TextCellEditor</code> implementation of this
+ * <code>CellEditor</code> method returns <code>true</code> if
+ * the current selection is not empty.
+ */
+ @Override
+ public boolean isCutEnabled() {
+ if (text == null || text.isDisposed()) {
+ return false;
+ }
+ return text.getSelectionCount() > 0;
+ }
+
+ /**
+ * The <code>TextCellEditor</code> implementation of this
+ * <code>CellEditor</code> method returns <code>true</code>
+ * if there is a selection or if the caret is not positioned
+ * at the end of the text.
+ */
+ @Override
+ public boolean isDeleteEnabled() {
+ if (text == null || text.isDisposed()) {
+ return false;
+ }
+ return text.getSelectionCount() > 0
+ || text.getCaretPosition() < text.getCharCount();
+ }
+
+ /**
+ * The <code>TextCellEditor</code> implementation of this
+ * <code>CellEditor</code> method always returns <code>true</code>.
+ */
+ @Override
+ public boolean isPasteEnabled() {
+ if (text == null || text.isDisposed()) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Check if save all is enabled
+ * @return true if it is
+ */
+ public boolean isSaveAllEnabled() {
+ if (text == null || text.isDisposed()) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Returns <code>true</code> if this cell editor is
+ * able to perform the select all action.
+ * <p>
+ * This default implementation always returns
+ * <code>false</code>.
+ * </p>
+ * <p>
+ * Subclasses may override
+ * </p>
+ * @return <code>true</code> if select all is possible,
+ * <code>false</code> otherwise
+ */
+ @Override
+ public boolean isSelectAllEnabled() {
+ if (text == null || text.isDisposed()) {
+ return false;
+ }
+ return text.getCharCount() > 0;
+ }
+
+ /**
+ * Processes a key release event that occurred in this cell editor.
+ * <p>
+ * The <code>TextCellEditor</code> implementation of this framework method
+ * ignores when the RETURN key is pressed since this is handled in
+ * <code>handleDefaultSelection</code>.
+ * An exception is made for Ctrl+Enter for multi-line texts, since
+ * a default selection event is not sent in this case.
+ * </p>
+ *
+ * @param keyEvent the key event
+ */
+ @Override
+ protected void keyReleaseOccured(KeyEvent keyEvent) {
+ if (keyEvent.character == '\r') { // Return key
+ // Enter is handled in handleDefaultSelection.
+ // Do not apply the editor value in response to an Enter key event
+ // since this can be received from the IME when the intent is -not-
+ // to apply the value.
+ // See bug 39074 [CellEditors] [DBCS] canna input mode fires bogus event from Text Control
+ //
+ // An exception is made for Ctrl+Enter for multi-line texts, since
+ // a default selection event is not sent in this case.
+ if (text != null && !text.isDisposed()
+ && (text.getStyle() & SWT.MULTI) != 0) {
+ if ((keyEvent.stateMask & SWT.CTRL) != 0) {
+ super.keyReleaseOccured(keyEvent);
+ }
+ }
+ return;
+ }
+ super.keyReleaseOccured(keyEvent);
+ }
+
+ /**
+ * The <code>TextCellEditor</code> implementation of this
+ * <code>CellEditor</code> method copies the
+ * current selection to the clipboard.
+ */
+ @Override
+ public void performCopy() {
+ text.copy();
+ }
+
+ /**
+ * The <code>TextCellEditor</code> implementation of this
+ * <code>CellEditor</code> method cuts the
+ * current selection to the clipboard.
+ */
+ @Override
+ public void performCut() {
+ text.cut();
+ checkSelection();
+ checkDeleteable();
+ checkSelectable();
+ }
+
+ /**
+ * The <code>TextCellEditor</code> implementation of this
+ * <code>CellEditor</code> method deletes the
+ * current selection or, if there is no selection,
+ * the character next character from the current position.
+ */
+ @Override
+ public void performDelete() {
+ if (text.getSelectionCount() > 0) {
+ // remove the contents of the current selection
+ text.insert(""); //$NON-NLS-1$
+ } else {
+ // remove the next character
+ int pos = text.getCaretPosition();
+ if (pos < text.getCharCount()) {
+ text.setSelection(pos, pos + 1);
+ text.insert(""); //$NON-NLS-1$
+ }
+ }
+ checkSelection();
+ checkDeleteable();
+ checkSelectable();
+ }
+
+ /**
+ * The <code>TextCellEditor</code> implementation of this
+ * <code>CellEditor</code> method pastes the
+ * the clipboard contents over the current selection.
+ */
+ @Override
+ public void performPaste() {
+ text.paste();
+ checkSelection();
+ checkDeleteable();
+ checkSelectable();
+ }
+
+ /**
+ * The <code>TextCellEditor</code> implementation of this
+ * <code>CellEditor</code> method selects all of the
+ * current text.
+ */
+ @Override
+ public void performSelectAll() {
+ text.selectAll();
+ checkSelection();
+ checkDeleteable();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/ErrorImageComposite.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/ErrorImageComposite.java
new file mode 100644
index 0000000..d095376
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/ErrorImageComposite.java
@@ -0,0 +1,47 @@
+package com.android.ide.eclipse.editors.ui;
+
+import org.eclipse.jface.resource.CompositeImageDescriptor;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.viewers.DecorationOverlayIcon;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.ui.ISharedImages;
+import org.eclipse.ui.PlatformUI;
+
+/**
+ * ImageDescriptor that adds a error marker.
+ * Based on {@link DecorationOverlayIcon} only available in Eclipse 3.3
+ */
+public class ErrorImageComposite extends CompositeImageDescriptor {
+
+ private Image mBaseImage;
+ private ImageDescriptor mErrorImageDescriptor;
+ private Point mSize;
+
+ public ErrorImageComposite(Image baseImage) {
+ mBaseImage = baseImage;
+ mErrorImageDescriptor = PlatformUI.getWorkbench().getSharedImages().getImageDescriptor(
+ ISharedImages.IMG_OBJS_ERROR_TSK);
+ mSize = new Point(baseImage.getBounds().width, baseImage.getBounds().height);
+ }
+
+ @Override
+ protected void drawCompositeImage(int width, int height) {
+ ImageData baseData = mBaseImage.getImageData();
+ drawImage(baseData, 0, 0);
+
+ ImageData overlayData = mErrorImageDescriptor.getImageData();
+ if (overlayData.width == baseData.width && baseData.height == baseData.height) {
+ overlayData = overlayData.scaledTo(14, 14);
+ drawImage(overlayData, -3, mSize.y - overlayData.height + 3);
+ } else {
+ drawImage(overlayData, 0, mSize.y - overlayData.height);
+ }
+ }
+
+ @Override
+ protected Point getSize() {
+ return mSize;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/FlagValueCellEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/FlagValueCellEditor.java
new file mode 100644
index 0000000..ccae099
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/FlagValueCellEditor.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.ui;
+
+import com.android.ide.eclipse.editors.uimodel.UiFlagAttributeNode;
+
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+
+/**
+ * DialogCellEditor able to receive a {@link UiFlagAttributeNode} in the {@link #setValue(Object)}
+ * method.
+ * <p/>The dialog box opened is the same as the one in the ui created by
+ * {@link UiFlagAttributeNode#createUiControl(Composite, org.eclipse.ui.forms.IManagedForm)}
+ */
+public class FlagValueCellEditor extends EditableDialogCellEditor {
+
+ private UiFlagAttributeNode mUiFlagAttribute;
+
+ public FlagValueCellEditor(Composite parent) {
+ super(parent);
+ }
+
+ @Override
+ protected Object openDialogBox(Control cellEditorWindow) {
+ if (mUiFlagAttribute != null) {
+ String currentValue = (String)getValue();
+ return mUiFlagAttribute.showDialog(cellEditorWindow.getShell(), currentValue);
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void doSetValue(Object value) {
+ if (value instanceof UiFlagAttributeNode) {
+ mUiFlagAttribute = (UiFlagAttributeNode)value;
+ super.doSetValue(mUiFlagAttribute.getCurrentValue());
+ return;
+ }
+
+ super.doSetValue(value);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/ListValueCellEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/ListValueCellEditor.java
new file mode 100644
index 0000000..304dd14
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/ListValueCellEditor.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.ui;
+
+import com.android.ide.eclipse.editors.uimodel.UiListAttributeNode;
+
+import org.eclipse.jface.viewers.ComboBoxCellEditor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.CCombo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+
+/**
+ * ComboBoxCellEditor able to receive a {@link UiListAttributeNode} in the {@link #setValue(Object)}
+ * method, and returning a {@link String} in {@link #getValue()} instead of an {@link Integer}.
+ */
+public class ListValueCellEditor extends ComboBoxCellEditor {
+ private String[] mItems;
+ private CCombo mCombo;
+
+ public ListValueCellEditor(Composite parent) {
+ super(parent, new String[0], SWT.DROP_DOWN);
+ }
+
+ @Override
+ protected Control createControl(Composite parent) {
+ mCombo = (CCombo) super.createControl(parent);
+ return mCombo;
+ }
+
+ @Override
+ protected void doSetValue(Object value) {
+ if (value instanceof UiListAttributeNode) {
+ UiListAttributeNode uiListAttribute = (UiListAttributeNode)value;
+
+ // set the possible values in the combo
+ String[] items = uiListAttribute.getPossibleValues();
+ mItems = new String[items.length];
+ System.arraycopy(items, 0, mItems, 0, items.length);
+ setItems(mItems);
+
+ // now edit the current value of the attribute
+ String attrValue = uiListAttribute.getCurrentValue();
+ mCombo.setText(attrValue);
+
+ return;
+ }
+
+ // default behavior
+ super.doSetValue(value);
+ }
+
+ @Override
+ protected Object doGetValue() {
+ String comboText = mCombo.getText();
+ if (comboText == null) {
+ return ""; //$NON-NLS-1$
+ }
+ return comboText;
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/ResourceValueCellEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/ResourceValueCellEditor.java
new file mode 100644
index 0000000..4fc0ab3
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/ResourceValueCellEditor.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.ui;
+
+import com.android.ide.eclipse.editors.uimodel.UiFlagAttributeNode;
+import com.android.ide.eclipse.editors.uimodel.UiResourceAttributeNode;
+
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+
+/**
+ * DialogCellEditor able to receive a {@link UiFlagAttributeNode} in the {@link #setValue(Object)}
+ * method.
+ * <p/>The dialog box opened is the same as the one in the ui created by
+ * {@link UiFlagAttributeNode#createUiControl(Composite, org.eclipse.ui.forms.IManagedForm)}
+ */
+public class ResourceValueCellEditor extends EditableDialogCellEditor {
+
+ private UiResourceAttributeNode mUiResourceAttribute;
+
+ public ResourceValueCellEditor(Composite parent) {
+ super(parent);
+ }
+
+ @Override
+ protected Object openDialogBox(Control cellEditorWindow) {
+ if (mUiResourceAttribute != null) {
+ String currentValue = (String)getValue();
+ return mUiResourceAttribute.showDialog(cellEditorWindow.getShell(), currentValue);
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void doSetValue(Object value) {
+ if (value instanceof UiResourceAttributeNode) {
+ mUiResourceAttribute = (UiResourceAttributeNode)value;
+ super.doSetValue(mUiResourceAttribute.getCurrentValue());
+ return;
+ }
+
+ super.doSetValue(value);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/SectionHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/SectionHelper.java
new file mode 100644
index 0000000..409e92f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/SectionHelper.java
@@ -0,0 +1,348 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.ui;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.editors.AndroidEditor;
+
+import org.eclipse.jface.text.DefaultInformationControl;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseTrackListener;
+import org.eclipse.swt.graphics.Point;
+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.Text;
+import org.eclipse.ui.forms.SectionPart;
+import org.eclipse.ui.forms.widgets.FormText;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.Section;
+import org.eclipse.ui.forms.widgets.TableWrapData;
+import org.eclipse.ui.forms.widgets.TableWrapLayout;
+
+import java.lang.reflect.Method;
+
+/**
+ * Helper for the AndroidManifest form editor.
+ *
+ * Helps create a new SectionPart with sensible default parameters,
+ * create default layout or add typical widgets.
+ *
+ * IMPORTANT: This is NOT a generic class. It makes a lot of assumptions on the
+ * UI as used by the form editor for the AndroidManifest.
+ *
+ * TODO: Consider moving to a common package.
+ */
+public final class SectionHelper {
+
+ /**
+ * Utility class that derives from SectionPart, constructs the Section with
+ * sensible defaults (with a title and a description) and provide some shorthand
+ * methods for creating typically UI (label and text, form text.)
+ */
+ static public class ManifestSectionPart extends SectionPart {
+
+ /**
+ * Construct a SectionPart that uses a title bar and a description.
+ * It's up to the caller to call setText() and setDescription().
+ * <p/>
+ * The section style includes a description and a title bar by default.
+ *
+ * @param body The parent (e.g. FormPage body)
+ * @param toolkit Form Toolkit
+ */
+ public ManifestSectionPart(Composite body, FormToolkit toolkit) {
+ this(body, toolkit, 0, false);
+ }
+
+ /**
+ * Construct a SectionPart that uses a title bar and a description.
+ * It's up to the caller to call setText() and setDescription().
+ * <p/>
+ * The section style includes a description and a title bar by default.
+ * You can add extra styles, like Section.TWISTIE.
+ *
+ * @param body The parent (e.g. FormPage body).
+ * @param toolkit Form Toolkit.
+ * @param extra_style Extra styles (on top of description and title bar).
+ * @param use_description True if the Section.DESCRIPTION style should be added.
+ */
+ public ManifestSectionPart(Composite body, FormToolkit toolkit,
+ int extra_style, boolean use_description) {
+ super(body, toolkit, extra_style |
+ Section.TITLE_BAR |
+ (use_description ? Section.DESCRIPTION : 0));
+ }
+
+ // Create non-static methods of helpers just for convenience
+
+ /**
+ * Creates a new composite with a TableWrapLayout set with a given number of columns.
+ *
+ * If the parent composite is a Section, the new composite is set as a client.
+ *
+ * @param toolkit Form Toolkit
+ * @param numColumns Desired number of columns.
+ * @return The new composite.
+ */
+ public Composite createTableLayout(FormToolkit toolkit, int numColumns) {
+ return SectionHelper.createTableLayout(getSection(), toolkit, numColumns);
+ }
+
+ /**
+ * Creates a label widget.
+ * If the parent layout if a TableWrapLayout, maximize it to span over all columns.
+ *
+ * @param parent The parent (e.g. composite from CreateTableLayout())
+ * @param toolkit Form Toolkit
+ * @param label The string for the label.
+ * @param tooltip An optional tooltip for the label and text. Can be null.
+ * @return The new created label
+ */
+ public Label createLabel(Composite parent, FormToolkit toolkit, String label,
+ String tooltip) {
+ return SectionHelper.createLabel(parent, toolkit, label, tooltip);
+ }
+
+ /**
+ * Creates two widgets: a label and a text field.
+ *
+ * This expects the parent composite to have a TableWrapLayout with 2 columns.
+ *
+ * @param parent The parent (e.g. composite from CreateTableLayout())
+ * @param toolkit Form Toolkit
+ * @param label The string for the label.
+ * @param value The initial value of the text field. Can be null.
+ * @param tooltip An optional tooltip for the label and text. Can be null.
+ * @return The new created Text field (the label is not returned)
+ */
+ public Text createLabelAndText(Composite parent, FormToolkit toolkit, String label,
+ String value, String tooltip) {
+ return SectionHelper.createLabelAndText(parent, toolkit, label, value, tooltip);
+ }
+
+ /**
+ * Creates a FormText widget.
+ *
+ * This expects the parent composite to have a TableWrapLayout with 2 columns.
+ *
+ * @param parent The parent (e.g. composite from CreateTableLayout())
+ * @param toolkit Form Toolkit
+ * @param isHtml True if the form text will contain HTML that must be interpreted as
+ * rich text (i.e. parse tags & expand URLs).
+ * @param label The string for the label.
+ * @param setupLayoutData indicates whether the created form text receives a TableWrapData
+ * through the setLayoutData method. In some case, creating it will make the table parent
+ * huge, which we don't want.
+ * @return The new created FormText.
+ */
+ public FormText createFormText(Composite parent, FormToolkit toolkit, boolean isHtml,
+ String label, boolean setupLayoutData) {
+ return SectionHelper.createFormText(parent, toolkit, isHtml, label, setupLayoutData);
+ }
+
+ /**
+ * Forces the section to recompute its layout and redraw.
+ * This is needed after the content of the section has been changed at runtime.
+ */
+ public void layoutChanged() {
+ Section section = getSection();
+
+ // Calls getSection().reflow(), which is the same that Section calls
+ // when the expandable state is changed and the height changes.
+ // Since this is protected, some reflection is needed to invoke it.
+ try {
+ Method reflow;
+ reflow = section.getClass().getDeclaredMethod("reflow", (Class<?>[])null);
+ reflow.setAccessible(true);
+ reflow.invoke(section);
+ } catch (Exception e) {
+ AdtPlugin.log(e, "Error when invoking Section.reflow");
+ }
+
+ section.layout(true /* changed */, true /* all */);
+ }
+ }
+
+ /**
+ * Creates a new composite with a TableWrapLayout set with a given number of columns.
+ *
+ * If the parent composite is a Section, the new composite is set as a client.
+ *
+ * @param composite The parent (e.g. a Section or SectionPart)
+ * @param toolkit Form Toolkit
+ * @param numColumns Desired number of columns.
+ * @return The new composite.
+ */
+ static public Composite createTableLayout(Composite composite, FormToolkit toolkit,
+ int numColumns) {
+ Composite table = toolkit.createComposite(composite);
+ TableWrapLayout layout = new TableWrapLayout();
+ layout.numColumns = numColumns;
+ table.setLayout(layout);
+ toolkit.paintBordersFor(table);
+ if (composite instanceof Section) {
+ ((Section) composite).setClient(table);
+ }
+ return table;
+ }
+
+ /**
+ * Creates a new composite with a GridLayout set with a given number of columns.
+ *
+ * If the parent composite is a Section, the new composite is set as a client.
+ *
+ * @param composite The parent (e.g. a Section or SectionPart)
+ * @param toolkit Form Toolkit
+ * @param numColumns Desired number of columns.
+ * @return The new composite.
+ */
+ static public Composite createGridLayout(Composite composite, FormToolkit toolkit,
+ int numColumns) {
+ Composite grid = toolkit.createComposite(composite);
+ GridLayout layout = new GridLayout();
+ layout.numColumns = numColumns;
+ grid.setLayout(layout);
+ toolkit.paintBordersFor(grid);
+ if (composite instanceof Section) {
+ ((Section) composite).setClient(grid);
+ }
+ return grid;
+ }
+
+ /**
+ * Creates two widgets: a label and a text field.
+ *
+ * This expects the parent composite to have a TableWrapLayout with 2 columns.
+ *
+ * @param parent The parent (e.g. composite from CreateTableLayout())
+ * @param toolkit Form Toolkit
+ * @param label_text The string for the label.
+ * @param value The initial value of the text field. Can be null.
+ * @param tooltip An optional tooltip for the label and text. Can be null.
+ * @return The new created Text field (the label is not returned)
+ */
+ static public Text createLabelAndText(Composite parent, FormToolkit toolkit, String label_text,
+ String value, String tooltip) {
+ Label label = toolkit.createLabel(parent, label_text);
+ label.setLayoutData(new TableWrapData(TableWrapData.LEFT, TableWrapData.MIDDLE));
+ Text text = toolkit.createText(parent, value);
+ text.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.MIDDLE));
+
+ addControlTooltip(label, tooltip);
+ return text;
+ }
+
+ /**
+ * Creates a label widget.
+ * If the parent layout if a TableWrapLayout, maximize it to span over all columns.
+ *
+ * @param parent The parent (e.g. composite from CreateTableLayout())
+ * @param toolkit Form Toolkit
+ * @param label_text The string for the label.
+ * @param tooltip An optional tooltip for the label and text. Can be null.
+ * @return The new created label
+ */
+ static public Label createLabel(Composite parent, FormToolkit toolkit, String label_text,
+ String tooltip) {
+ Label label = toolkit.createLabel(parent, label_text);
+
+ TableWrapData twd = new TableWrapData(TableWrapData.FILL_GRAB);
+ if (parent.getLayout() instanceof TableWrapLayout) {
+ twd.colspan = ((TableWrapLayout) parent.getLayout()).numColumns;
+ }
+ label.setLayoutData(twd);
+
+ addControlTooltip(label, tooltip);
+ return label;
+ }
+
+ /**
+ * Associates a tooltip with a control.
+ *
+ * This mirrors the behavior from org.eclipse.pde.internal.ui.editor.text.PDETextHover
+ *
+ * @param control The control to which associate the tooltip.
+ * @param tooltip The tooltip string. Can use \n for multi-lines. Will not display if null.
+ */
+ static public void addControlTooltip(final Control control, String tooltip) {
+ if (control == null || tooltip == null || tooltip.length() == 0) {
+ return;
+ }
+
+ // Some kinds of controls already properly implement tooltip display.
+ if (control instanceof Button) {
+ control.setToolTipText(tooltip);
+ return;
+ }
+
+ control.setToolTipText(null);
+
+ final DefaultInformationControl ic = new DefaultInformationControl(control.getShell());
+ ic.setInformation(tooltip);
+ Point sz = ic.computeSizeHint();
+ ic.setSize(sz.x, sz.y);
+ ic.setVisible(false); // initially hidden
+
+ control.addMouseTrackListener(new MouseTrackListener() {
+ public void mouseEnter(MouseEvent e) {
+ }
+
+ public void mouseExit(MouseEvent e) {
+ ic.setVisible(false);
+ }
+
+ public void mouseHover(MouseEvent e) {
+ ic.setLocation(control.toDisplay(10, 25)); // same offset as in PDETextHover
+ ic.setVisible(true);
+ }
+ });
+ }
+
+ /**
+ * Creates a FormText widget.
+ *
+ * This expects the parent composite to have a TableWrapLayout with 2 columns.
+ *
+ * @param parent The parent (e.g. composite from CreateTableLayout())
+ * @param toolkit Form Toolkit
+ * @param isHtml True if the form text will contain HTML that must be interpreted as
+ * rich text (i.e. parse tags & expand URLs).
+ * @param label The string for the label.
+ * @param setupLayoutData indicates whether the created form text receives a TableWrapData
+ * through the setLayoutData method. In some case, creating it will make the table parent
+ * huge, which we don't want.
+ * @return The new created FormText.
+ */
+ static public FormText createFormText(Composite parent, FormToolkit toolkit,
+ boolean isHtml, String label, boolean setupLayoutData) {
+ FormText text = toolkit.createFormText(parent, true /* track focus */);
+ if (setupLayoutData) {
+ TableWrapData twd = new TableWrapData(TableWrapData.FILL_GRAB);
+ twd.maxWidth = AndroidEditor.TEXT_WIDTH_HINT;
+ if (parent.getLayout() instanceof TableWrapLayout) {
+ twd.colspan = ((TableWrapLayout) parent.getLayout()).numColumns;
+ }
+ text.setLayoutData(twd);
+ }
+ text.setWhitespaceNormalized(true);
+ text.setText(label, isHtml /* parseTags */, isHtml /* expandURLs */);
+ return text;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/TextValueCellEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/TextValueCellEditor.java
new file mode 100644
index 0000000..2fe5783
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/TextValueCellEditor.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.ui;
+
+import com.android.ide.eclipse.editors.uimodel.UiAttributeNode;
+
+import org.eclipse.jface.viewers.TextCellEditor;
+import org.eclipse.swt.widgets.Composite;
+
+/**
+ * TextCellEditor able to receive a {@link UiAttributeNode} in the {@link #setValue(Object)}
+ * method.
+ */
+public class TextValueCellEditor extends TextCellEditor {
+
+ public TextValueCellEditor(Composite parent) {
+ super(parent);
+ }
+
+ @Override
+ protected void doSetValue(Object value) {
+ if (value instanceof UiAttributeNode) {
+ super.doSetValue(((UiAttributeNode)value).getCurrentValue());
+ return;
+ }
+
+ super.doSetValue(value);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/UiElementPart.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/UiElementPart.java
new file mode 100644
index 0000000..66773bd
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/UiElementPart.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.ui;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.XmlnsAttributeDescriptor;
+import com.android.ide.eclipse.editors.manifest.ManifestEditor;
+import com.android.ide.eclipse.editors.ui.SectionHelper.ManifestSectionPart;
+import com.android.ide.eclipse.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.Section;
+
+/**
+ * Generic page's section part that displays all attributes of a given {@link UiElementNode}.
+ * <p/>
+ * This part is designed to be displayed in a page that has a table column layout.
+ * It is linked to a specific {@link UiElementNode} and automatically displays all of its
+ * attributes, manages its dirty state and commits the attributes when necessary.
+ * <p/>
+ * No derivation is needed unless the UI or workflow needs to be extended.
+ */
+public class UiElementPart extends ManifestSectionPart {
+
+ /** A reference to the container editor */
+ private ManifestEditor mEditor;
+ /** The {@link UiElementNode} manipulated by this SectionPart. It can be null. */
+ private UiElementNode mUiElementNode;
+ /** Table that contains all the attributes */
+ private Composite mTable;
+
+ public UiElementPart(Composite body, FormToolkit toolkit, ManifestEditor editor,
+ UiElementNode uiElementNode, String sectionTitle, String sectionDescription,
+ int extra_style) {
+ super(body, toolkit, extra_style, sectionDescription != null);
+ mEditor = editor;
+ mUiElementNode = uiElementNode;
+ setupSection(sectionTitle, sectionDescription);
+
+ if (uiElementNode == null) {
+ // This is serious and should never happen. Instead of crashing, simply abort.
+ // There will be no UI, which will prevent further damage.
+ AdtPlugin.log(IStatus.ERROR, "Missing node to edit!"); //$NON-NLS-1$
+ return;
+ }
+ }
+
+ /**
+ * Returns the Editor associated with this part.
+ */
+ public ManifestEditor getEditor() {
+ return mEditor;
+ }
+
+ /**
+ * Returns the {@link UiElementNode} associated with this part.
+ */
+ public UiElementNode getUiElementNode() {
+ return mUiElementNode;
+ }
+
+ /**
+ * Changes the element node handled by this part.
+ *
+ * @param uiElementNode The new element node for the part.
+ */
+ public void setUiElementNode(UiElementNode uiElementNode) {
+ mUiElementNode = uiElementNode;
+ }
+
+ /**
+ * Initializes the form part.
+ * <p/>
+ * This is called by the owning managed form as soon as the part is added to the form,
+ * which happens right after the part is actually created.
+ */
+ @Override
+ public void initialize(IManagedForm form) {
+ super.initialize(form);
+ createFormControls(form);
+ }
+
+ /**
+ * Setup the section that contains this part.
+ * <p/>
+ * This is called by the constructor to set the section's title and description
+ * with parameters given in the constructor.
+ * <br/>
+ * Derived class override this if needed, however in most cases the default
+ * implementation should be enough.
+ *
+ * @param sectionTitle The section part's title
+ * @param sectionDescription The section part's description
+ */
+ protected void setupSection(String sectionTitle, String sectionDescription) {
+ Section section = getSection();
+ section.setText(sectionTitle);
+ section.setDescription(sectionDescription);
+ }
+
+ /**
+ * Create the controls to edit the attributes for the given ElementDescriptor.
+ * <p/>
+ * This MUST not be called by the constructor. Instead it must be called from
+ * <code>initialize</code> (i.e. right after the form part is added to the managed form.)
+ * <p/>
+ * Derived classes can override this if necessary.
+ *
+ * @param managedForm The owner managed form
+ */
+ protected void createFormControls(IManagedForm managedForm) {
+ setTable(createTableLayout(managedForm.getToolkit(), 2 /* numColumns */));
+
+ createUiAttributes(managedForm);
+ }
+
+ /**
+ * Sets the table where the attribute UI needs to be created.
+ */
+ protected void setTable(Composite table) {
+ mTable = table;
+ }
+
+ /**
+ * Returns the table where the attribute UI needs to be created.
+ */
+ protected Composite getTable() {
+ return mTable;
+ }
+
+ /**
+ * Add all the attribute UI widgets into the underlying table layout.
+ *
+ * @param managedForm The owner managed form
+ */
+ protected void createUiAttributes(IManagedForm managedForm) {
+ Composite table = getTable();
+ if (table == null || managedForm == null) {
+ return;
+ }
+
+ // Remove any old UI controls
+ for (Control c : table.getChildren()) {
+ c.dispose();
+ }
+
+ fillTable(table, managedForm);
+
+ // Tell the section that the layout has changed.
+ layoutChanged();
+ }
+
+ /**
+ * Actually fills the table.
+ * This is called by {@link #createUiAttributes(IManagedForm)} to populate the new
+ * table. The default implementation is to use
+ * {@link #insertUiAttributes(UiElementNode, Composite, IManagedForm)} to actually
+ * place the attributes of the default {@link UiElementNode} in the table.
+ * <p/>
+ * Derived classes can override this to add controls in the table before or after.
+ *
+ * @param table The table to fill. It must have 2 columns.
+ * @param managedForm The managed form for new controls.
+ */
+ protected void fillTable(Composite table, IManagedForm managedForm) {
+ int inserted = insertUiAttributes(mUiElementNode, table, managedForm);
+
+ if (inserted == 0) {
+ createLabel(table, managedForm.getToolkit(),
+ "No attributes to display, waiting for SDK to finish loading...",
+ null /* tooltip */ );
+ }
+ }
+
+ /**
+ * Insert the UI attributes of the given {@link UiElementNode} in the given table.
+ *
+ * @param uiNode The {@link UiElementNode} that contains the attributes to display.
+ * Must not be null.
+ * @param table The table to fill. It must have 2 columns.
+ * @param managedForm The managed form for new controls.
+ * @return The number of UI attributes inserted. It is >= 0.
+ */
+ protected int insertUiAttributes(UiElementNode uiNode, Composite table, IManagedForm managedForm) {
+ if (uiNode == null || table == null || managedForm == null) {
+ return 0;
+ }
+
+ // To iterate over all attributes, we use the {@link ElementDescriptor} instead
+ // of the {@link UiElementNode} because the attributes' order is guaranteed in the
+ // descriptor but not in the node itself.
+ AttributeDescriptor[] attr_desc_list = uiNode.getAttributeDescriptors();
+ for (AttributeDescriptor attr_desc : attr_desc_list) {
+ if (attr_desc instanceof XmlnsAttributeDescriptor) {
+ // Do not show hidden attributes
+ continue;
+ }
+
+ UiAttributeNode ui_attr = uiNode.findUiAttribute(attr_desc);
+ if (ui_attr != null) {
+ ui_attr.createUiControl(table, managedForm);
+ } else {
+ // The XML has an extra attribute which wasn't declared in
+ // AndroidManifestDescriptors. This is not a problem, we just ignore it.
+ AdtPlugin.log(IStatus.WARNING,
+ "Attribute %1$s not declared in node %2$s, ignored.", //$NON-NLS-1$
+ attr_desc.getXmlLocalName(),
+ uiNode.getDescriptor().getXmlName());
+ }
+ }
+ return attr_desc_list.length;
+ }
+
+ /**
+ * Tests whether the part is dirty i.e. its widgets have state that is
+ * newer than the data in the model.
+ * <p/>
+ * This is done by iterating over all attributes and updating the super's
+ * internal dirty flag. Stop once at least one attribute is dirty.
+ *
+ * @return <code>true</code> if the part is dirty, <code>false</code>
+ * otherwise.
+ */
+ @Override
+ public boolean isDirty() {
+ if (mUiElementNode != null && !super.isDirty()) {
+ for (UiAttributeNode ui_attr : mUiElementNode.getUiAttributes()) {
+ if (ui_attr.isDirty()) {
+ markDirty();
+ break;
+ }
+ }
+ }
+ return super.isDirty();
+ }
+
+ /**
+ * If part is displaying information loaded from a model, this method
+ * instructs it to commit the new (modified) data back into the model.
+ *
+ * @param onSave
+ * indicates if commit is called during 'save' operation or for
+ * some other reason (for example, if form is contained in a
+ * wizard or a multi-page editor and the user is about to leave
+ * the page).
+ */
+ @Override
+ public void commit(boolean onSave) {
+ if (mUiElementNode != null) {
+ mEditor.editXmlModel(new Runnable() {
+ public void run() {
+ for (UiAttributeNode ui_attr : mUiElementNode.getUiAttributes()) {
+ ui_attr.commit();
+ }
+ }
+ });
+ }
+
+ // We need to call super's commit after we synchronized the nodes to make sure we
+ // reset the dirty flag after all the side effects from committing have occurred.
+ super.commit(onSave);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/CopyCutAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/CopyCutAction.java
new file mode 100644
index 0000000..2aad217
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/CopyCutAction.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.ui.tree;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.editors.AndroidEditor;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+import org.apache.xml.serialize.Method;
+import org.apache.xml.serialize.OutputFormat;
+import org.apache.xml.serialize.XMLSerializer;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.ui.ISharedImages;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+import org.eclipse.wst.xml.core.internal.document.NodeContainer;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * Provides Cut and Copy actions for the tree nodes.
+ */
+public class CopyCutAction extends Action {
+ private List<UiElementNode> mUiNodes;
+ private boolean mPerformCut;
+ private final AndroidEditor mEditor;
+ private final Clipboard mClipboard;
+ private final ICommitXml mXmlCommit;
+
+ /**
+ * Creates a new Copy or Cut action.
+ *
+ * @param selected The UI node to cut or copy. It *must* have a non-null XML node.
+ * @param perform_cut True if the operation is cut, false if it is copy.
+ */
+ public CopyCutAction(AndroidEditor editor, Clipboard clipboard, ICommitXml xmlCommit,
+ UiElementNode selected, boolean perform_cut) {
+ this(editor, clipboard, xmlCommit, toList(selected), perform_cut);
+ }
+
+ /**
+ * Creates a new Copy or Cut action.
+ *
+ * @param selected The UI nodes to cut or copy. They *must* have a non-null XML node.
+ * The list becomes owned by the {@link CopyCutAction}.
+ * @param perform_cut True if the operation is cut, false if it is copy.
+ */
+ public CopyCutAction(AndroidEditor editor, Clipboard clipboard, ICommitXml xmlCommit,
+ List<UiElementNode> selected, boolean perform_cut) {
+ super(perform_cut ? "Cut" : "Copy");
+ mEditor = editor;
+ mClipboard = clipboard;
+ mXmlCommit = xmlCommit;
+
+ ISharedImages images = PlatformUI.getWorkbench().getSharedImages();
+ if (perform_cut) {
+ setImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_TOOL_CUT));
+ setHoverImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_TOOL_CUT));
+ setDisabledImageDescriptor(
+ images.getImageDescriptor(ISharedImages.IMG_TOOL_CUT_DISABLED));
+ } else {
+ setImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_TOOL_COPY));
+ setHoverImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_TOOL_COPY));
+ setDisabledImageDescriptor(
+ images.getImageDescriptor(ISharedImages.IMG_TOOL_COPY_DISABLED));
+ }
+
+ mUiNodes = selected;
+ mPerformCut = perform_cut;
+ }
+
+ /**
+ * Performs the cut or copy action.
+ * First an XML serializer is used to turn the existing XML node into a valid
+ * XML fragment, which is added as text to the clipboard.
+ */
+ @Override
+ public void run() {
+ super.run();
+ if (mUiNodes == null || mUiNodes.size() < 1) {
+ return;
+ }
+
+ // Commit the current pages first, to make sure the XML is in sync.
+ // Committing may change the XML structure.
+ if (mXmlCommit != null) {
+ mXmlCommit.commitPendingXmlChanges();
+ }
+
+ StringBuilder allText = new StringBuilder();
+ ArrayList<UiElementNode> nodesToCut = mPerformCut ? new ArrayList<UiElementNode>() : null;
+
+ for (UiElementNode uiNode : mUiNodes) {
+ try {
+ Node xml_node = uiNode.getXmlNode();
+ if (xml_node == null) {
+ return;
+ }
+
+ String data = getXmlTextFromEditor(xml_node);
+
+ // In the unlikely event that IStructuredDocument failed to extract the text
+ // directly from the editor, try to fall back on a direct XML serialization
+ // of the XML node. This uses the generic Node interface with no SSE tricks.
+ if (data == null) {
+ data = getXmlTextFromSerialization(xml_node);
+ }
+
+ if (data != null) {
+ allText.append(data);
+ if (mPerformCut) {
+ // only remove notes to cut if we actually got some XML text from them
+ nodesToCut.add(uiNode);
+ }
+ }
+
+ } catch (Exception e) {
+ AdtPlugin.log(e, "CopyCutAction failed for UI node %1$s", //$NON-NLS-1$
+ uiNode.getBreadcrumbTrailDescription(true));
+ }
+ } // for uiNode
+
+ if (allText != null && allText.length() > 0) {
+ mClipboard.setContents(
+ new Object[] { allText.toString() },
+ new Transfer[] { TextTransfer.getInstance() });
+ if (mPerformCut) {
+ for (UiElementNode uiNode : nodesToCut) {
+ uiNode.deleteXmlNode();
+ }
+ }
+ }
+ }
+
+ /** Get the data directly from the editor. */
+ private String getXmlTextFromEditor(Node xml_node) {
+ String data = null;
+ IStructuredModel model = mEditor.getModelForRead();
+ try {
+ IStructuredDocument sse_doc = mEditor.getStructuredDocument();
+ if (xml_node instanceof NodeContainer) {
+ // The easy way to get the source of an SSE XML node.
+ data = ((NodeContainer) xml_node).getSource();
+ } else if (xml_node instanceof IndexedRegion && sse_doc != null) {
+ // Try harder.
+ IndexedRegion region = (IndexedRegion) xml_node;
+ int start = region.getStartOffset();
+ int end = region.getEndOffset();
+
+ if (end > start) {
+ data = sse_doc.get(start, end - start);
+ }
+ }
+ } catch (BadLocationException e) {
+ // the region offset was invalid. ignore.
+ } finally {
+ model.releaseFromRead();
+ }
+ return data;
+ }
+
+ /**
+ * Direct XML serialization of the XML node.
+ * <p/>
+ * This uses the generic Node interface with no SSE tricks. It's however slower
+ * and doesn't respect formatting (since serialization is involved instead of reading
+ * the actual text buffer.)
+ */
+ private String getXmlTextFromSerialization(Node xml_node) throws IOException {
+ String data;
+ StringWriter sw = new StringWriter();
+ XMLSerializer serializer = new XMLSerializer(sw,
+ new OutputFormat(Method.XML,
+ OutputFormat.Defaults.Encoding /* utf-8 */,
+ true /* indent */));
+ // Serialize will throw an IOException if it fails.
+ serializer.serialize((Element) xml_node);
+ data = sw.toString();
+ return data;
+ }
+
+ /**
+ * Static helper class to wrap on node into a list for the constructors.
+ */
+ private static ArrayList<UiElementNode> toList(UiElementNode selected) {
+ ArrayList<UiElementNode> list = null;
+ if (selected != null) {
+ list = new ArrayList<UiElementNode>(1);
+ list.add(selected);
+ }
+ return list;
+ }
+}
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/ICommitXml.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/ICommitXml.java
new file mode 100644
index 0000000..8b6aa0e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/ICommitXml.java
@@ -0,0 +1,28 @@
+/*
+ * 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.ide.eclipse.editors.ui.tree;
+
+/**
+ * Interface for an object that can commit its changes to the underlying XML model
+ */
+public interface ICommitXml {
+
+ /** Commits pending data to the underlying XML model. */
+ public void commitPendingXmlChanges();
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/NewItemSelectionDialog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/NewItemSelectionDialog.java
new file mode 100644
index 0000000..0729881
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/NewItemSelectionDialog.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.ui.tree;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.viewers.ILabelProvider;
+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.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.dialogs.AbstractElementListSelectionDialog;
+import org.eclipse.ui.dialogs.ISelectionStatusValidator;
+
+import java.util.Arrays;
+
+/**
+ * A selection dialog to select the type of the new element node to
+ * created, either in the application node or the selected sub node.
+ */
+public class NewItemSelectionDialog extends AbstractElementListSelectionDialog {
+
+ /** The UI node selected in the tree view before creating the new item selection dialog.
+ * Can be null -- which means new items must be created in the root_node. */
+ private UiElementNode mSelectedUiNode;
+ /** The root node chosen by the user, either root_node or the one passed
+ * to the constructor if not null */
+ private UiElementNode mChosenRootNode;
+ private UiElementNode mLocalRootNode;
+ /** The descriptor of the elements to be displayed as root in this tree view. All elements
+ * of the same type in the root will be displayed. */
+ private ElementDescriptor[] mDescriptorFilters;
+
+ /**
+ * Creates the new item selection dialog.
+ *
+ * @param shell The parent shell for the list.
+ * @param labelProvider ILabelProvider for the list.
+ * @param descriptorFilters The element allows at the root of the tree
+ * @param ui_node The selected node, or null if none is selected.
+ * @param root_node The root of the Ui Tree, either the UiDocumentNode or a sub-node.
+ */
+ public NewItemSelectionDialog(Shell shell, ILabelProvider labelProvider,
+ ElementDescriptor[] descriptorFilters,
+ UiElementNode ui_node,
+ UiElementNode root_node) {
+ super(shell, labelProvider);
+ mDescriptorFilters = descriptorFilters;
+ mLocalRootNode = root_node;
+
+ // Only accept the UI node if it is not the UI root node and it can have children.
+ // If the node cannot have children, select its parent as a potential target.
+ if (ui_node != null && ui_node != mLocalRootNode) {
+ if (ui_node.getDescriptor().hasChildren()) {
+ mSelectedUiNode = ui_node;
+ } else {
+ UiElementNode parent = ui_node.getUiParent();
+ if (parent != null && parent != mLocalRootNode) {
+ mSelectedUiNode = parent;
+ }
+ }
+ }
+
+ setHelpAvailable(false);
+ setMultipleSelection(false);
+
+ setValidator(new ISelectionStatusValidator() {
+ public IStatus validate(Object[] selection) {
+ if (selection.length == 1 && selection[0] instanceof ViewElementDescriptor) {
+ return new Status(IStatus.OK, // severity
+ AdtPlugin.PLUGIN_ID, //plugin id
+ IStatus.OK, // code
+ ((ViewElementDescriptor) selection[0]).getCanonicalClassName(), //msg
+ null); // exception
+ } else if (selection.length == 1 && selection[0] instanceof ElementDescriptor) {
+ return new Status(IStatus.OK, // severity
+ AdtPlugin.PLUGIN_ID, //plugin id
+ IStatus.OK, // code
+ "", //$NON-NLS-1$ // msg
+ null); // exception
+ } else {
+ return new Status(IStatus.ERROR, // severity
+ AdtPlugin.PLUGIN_ID, //plugin id
+ IStatus.ERROR, // code
+ "Invalid selection", // msg, translatable
+ null); // exception
+ }
+ }
+ });
+ }
+
+ /**
+ * @return The root node selected by the user, either root node or the
+ * one passed to the constructor if not null.
+ */
+ public UiElementNode getChosenRootNode() {
+ return mChosenRootNode;
+ }
+
+ /**
+ * Internal helper to compute the result. Returns the selection from
+ * the list view, if any.
+ */
+ @Override
+ protected void computeResult() {
+ setResult(Arrays.asList(getSelectedElements()));
+ }
+
+ /**
+ * Creates the dialog area.
+ *
+ * First add a radio area, which may be either 2 radio controls or
+ * just a message area if there's only one choice (the app root node).
+ *
+ * Then uses the default from the AbstractElementListSelectionDialog
+ * which is to add both a filter text and a filtered list. Adding both
+ * is necessary (since the base class accesses both internal directly
+ * fields without checking for null pointers.)
+ *
+ * Finally sets the initial selection list.
+ */
+ @Override
+ protected Control createDialogArea(Composite parent) {
+ Composite contents = (Composite) super.createDialogArea(parent);
+
+ createRadioControl(contents);
+ createFilterText(contents);
+ createFilteredList(contents);
+
+ // Initialize the list state.
+ // This must be done after the filtered list as been created.
+ chooseNode(mChosenRootNode);
+ setSelection(getInitialElementSelections().toArray());
+ return contents;
+ }
+
+ /**
+ * Creates the message text widget and sets layout data.
+ * @param content the parent composite of the message area.
+ */
+ private Composite createRadioControl(Composite content) {
+
+ if (mSelectedUiNode != null) {
+ Button radio1 = new Button(content, SWT.RADIO);
+ radio1.setText(String.format("Create a new element at the top level, in %1$s.",
+ mLocalRootNode.getShortDescription()));
+
+ Button radio2 = new Button(content, SWT.RADIO);
+ radio2.setText(String.format("Create a new element in the selected element, %1$s.",
+ mSelectedUiNode.getBreadcrumbTrailDescription(false /* include_root */)));
+
+ // Set the initial selection before adding the listeners
+ // (they can't be run till the filtered list has been created)
+ radio1.setSelection(false);
+ radio2.setSelection(true);
+ mChosenRootNode = mSelectedUiNode;
+
+ radio1.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ super.widgetSelected(e);
+ chooseNode(mLocalRootNode);
+ }
+ });
+
+ radio2.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ super.widgetSelected(e);
+ chooseNode(mSelectedUiNode);
+ }
+ });
+ } else {
+ setMessage(String.format("Create a new element at the top level, in %1$s.",
+ mLocalRootNode.getShortDescription()));
+ createMessageArea(content);
+
+ mChosenRootNode = mLocalRootNode;
+ }
+
+ return content;
+ }
+
+ /**
+ * Internal helper to remember the root node choosen by the user.
+ * It also sets the list view to the adequate list of children that can
+ * be added to the chosen root node.
+ *
+ * If the chosen root node is mLocalRootNode and a descriptor filter was specified
+ * when creating the master-detail part, we use this as the set of nodes that
+ * can be created on the root node.
+ *
+ * @param ui_node The chosen root node, either mLocalRootNode or
+ * mSelectedUiNode.
+ */
+ private void chooseNode(UiElementNode ui_node) {
+ mChosenRootNode = ui_node;
+
+ if (ui_node == mLocalRootNode &&
+ mDescriptorFilters != null &&
+ mDescriptorFilters.length != 0) {
+ setListElements(mDescriptorFilters);
+ } else {
+ setListElements(ui_node.getDescriptor().getChildren());
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/PasteAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/PasteAction.java
new file mode 100644
index 0000000..8bb4ad2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/PasteAction.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.ui.tree;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.editors.AndroidEditor;
+import com.android.ide.eclipse.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.dnd.TextTransfer;
+import org.eclipse.ui.ISharedImages;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument;
+import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocumentRegion;
+import org.eclipse.wst.xml.core.internal.document.NodeContainer;
+import org.w3c.dom.Node;
+
+
+/**
+ * Provides Paste operation for the tree nodes
+ */
+public class PasteAction extends Action {
+ private UiElementNode mUiNode;
+ private final AndroidEditor mEditor;
+ private final Clipboard mClipboard;
+
+ public PasteAction(AndroidEditor editor, Clipboard clipboard, UiElementNode ui_node) {
+ super("Paste");
+ mEditor = editor;
+ mClipboard = clipboard;
+
+ ISharedImages images = PlatformUI.getWorkbench().getSharedImages();
+ setImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_TOOL_PASTE));
+ setHoverImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_TOOL_PASTE));
+ setDisabledImageDescriptor(
+ images.getImageDescriptor(ISharedImages.IMG_TOOL_PASTE_DISABLED));
+
+ mUiNode = ui_node;
+ }
+
+ /**
+ * Performs the paste operation.
+ */
+ @Override
+ public void run() {
+ super.run();
+
+ final String data = (String) mClipboard.getContents(TextTransfer.getInstance());
+ if (data != null) {
+ IStructuredModel model = mEditor.getModelForEdit();
+ try {
+ IStructuredDocument sse_doc = mEditor.getStructuredDocument();
+ if (sse_doc != null) {
+ if (mUiNode.getDescriptor().hasChildren()) {
+ // This UI Node can have children. The new XML is
+ // inserted as the first child.
+
+ if (mUiNode.getUiChildren().size() > 0) {
+ // There's already at least one child, so insert right before it.
+ Node xml_node = mUiNode.getUiChildren().get(0).getXmlNode();
+ if (xml_node instanceof IndexedRegion) { // implies xml_node != null
+ IndexedRegion region = (IndexedRegion) xml_node;
+ sse_doc.replace(region.getStartOffset(), 0, data);
+ return; // we're done, no need to try the other cases
+ }
+ }
+
+ // If there's no first XML node child. Create one by
+ // inserting at the end of the *start* tag.
+ Node xml_node = mUiNode.getXmlNode();
+ if (xml_node instanceof NodeContainer) {
+ NodeContainer container = (NodeContainer) xml_node;
+ IStructuredDocumentRegion start_tag =
+ container.getStartStructuredDocumentRegion();
+ if (start_tag != null) {
+ sse_doc.replace(start_tag.getEndOffset(), 0, data);
+ return; // we're done, no need to try the other case
+ }
+ }
+ }
+
+ // This UI Node doesn't accept children. The new XML is inserted as the
+ // next sibling. This also serves as a fallback if all the previous
+ // attempts failed. However, this is not possible if the current node
+ // has for parent a document -- an XML document can only have one root,
+ // with no siblings.
+ if (!(mUiNode.getUiParent() instanceof UiDocumentNode)) {
+ Node xml_node = mUiNode.getXmlNode();
+ if (xml_node instanceof IndexedRegion) {
+ IndexedRegion region = (IndexedRegion) xml_node;
+ sse_doc.replace(region.getEndOffset(), 0, data);
+ }
+ }
+ }
+
+ } catch (BadLocationException e) {
+ AdtPlugin.log(e, "ParseAction failed for UI Node %2$s, content '%1$s'", //$NON-NLS-1$
+ mUiNode.getBreadcrumbTrailDescription(true), data);
+ } finally {
+ model.releaseFromEdit();
+ }
+ }
+ }
+}
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiActions.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiActions.java
new file mode 100644
index 0000000..21180b1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiActions.java
@@ -0,0 +1,385 @@
+/*
+ * 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.ide.eclipse.editors.ui.tree;
+
+import com.android.ide.eclipse.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.swt.widgets.Shell;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+
+import java.util.List;
+
+/**
+ * Performs basic actions on an XML tree: add node, remove node, move up/down.
+ */
+public abstract class UiActions implements ICommitXml {
+
+ public UiActions() {
+ }
+
+ //---------------------
+ // Actual implementations must override these to provide specific hooks
+
+ /** Returns the UiDocumentNode for the current model. */
+ abstract protected UiElementNode getRootNode();
+
+ /** Commits pending data before the XML model is modified. */
+ abstract public void commitPendingXmlChanges();
+
+ /**
+ * Utility method to select an outline item based on its model node
+ *
+ * @param uiNode The node to select. Can be null (in which case nothing should happen)
+ */
+ abstract protected void selectUiNode(UiElementNode uiNode);
+
+ //---------------------
+
+ /**
+ * Called when the "Add..." button next to the tree view is selected.
+ * <p/>
+ * This simplified version of doAdd does not support descriptor filters and creates
+ * a new {@link UiModelTreeLabelProvider} for each call.
+ */
+ public void doAdd(UiElementNode uiNode, Shell shell) {
+ doAdd(uiNode, null /* descriptorFilters */, shell, new UiModelTreeLabelProvider());
+ }
+
+ /**
+ * Called when the "Add..." button next to the tree view is selected.
+ *
+ * Displays a selection dialog that lets the user select which kind of node
+ * to create, depending on the current selection.
+ */
+ public void doAdd(UiElementNode uiNode,
+ ElementDescriptor[] descriptorFilters,
+ Shell shell, ILabelProvider labelProvider) {
+ // If the root node is a document with already a root, use it as the root node
+ UiElementNode rootNode = getRootNode();
+ if (rootNode instanceof UiDocumentNode && rootNode.getUiChildren().size() > 0) {
+ rootNode = rootNode.getUiChildren().get(0);
+ }
+
+ NewItemSelectionDialog dlg = new NewItemSelectionDialog(
+ shell,
+ labelProvider,
+ descriptorFilters,
+ uiNode, rootNode);
+ dlg.open();
+ Object[] results = dlg.getResult();
+ if (results != null && results.length > 0) {
+ addElement(dlg.getChosenRootNode(), null, (ElementDescriptor) results[0],
+ true /*updateLayout*/);
+ }
+ }
+
+ /**
+ * Adds a new XML element based on the {@link ElementDescriptor} to the given parent
+ * {@link UiElementNode}, and then select it.
+ * <p/>
+ * If the parent is a document root which already contains a root element, the inner
+ * root element is used as the actual parent. This ensure you can't create a broken
+ * XML file with more than one root element.
+ * <p/>
+ * If a sibling is given and that sibling has the same parent, the new node is added
+ * right after that sibling. Otherwise the new node is added at the end of the parent
+ * child list.
+ *
+ * @param uiParent An existing UI node or null to add to the tree root
+ * @param uiSibling An existing UI node before which to insert the new node. Can be null.
+ * @param descriptor The descriptor of the element to add
+ * @param updateLayout True if layout attributes should be set
+ * @return The new {@link UiElementNode} or null.
+ */
+ public UiElementNode addElement(UiElementNode uiParent,
+ UiElementNode uiSibling,
+ ElementDescriptor descriptor,
+ boolean updateLayout) {
+ if (uiParent instanceof UiDocumentNode && uiParent.getUiChildren().size() > 0) {
+ uiParent = uiParent.getUiChildren().get(0);
+ }
+ if (uiSibling != null && uiSibling.getUiParent() != uiParent) {
+ uiSibling = null;
+ }
+
+ UiElementNode uiNew = addNewTreeElement(uiParent, uiSibling, descriptor, updateLayout);
+ selectUiNode(uiNew);
+
+ return uiNew;
+ }
+
+ /**
+ * Called when the "Remove" button is selected.
+ *
+ * If the tree has a selection, remove it.
+ * This simply deletes the XML node attached to the UI node: when the XML model fires the
+ * update event, the tree will get refreshed.
+ */
+ public void doRemove(final List<UiElementNode> nodes, Shell shell) {
+
+ if (nodes == null || nodes.size() == 0) {
+ return;
+ }
+
+ final int len = nodes.size();
+
+ StringBuilder sb = new StringBuilder();
+ for (UiElementNode node : nodes) {
+ sb.append("\n- "); //$NON-NLS-1$
+ sb.append(node.getBreadcrumbTrailDescription(false /* include_root */));
+ }
+
+ if (MessageDialog.openQuestion(shell,
+ len > 1 ? "Remove elements from Android XML" // title
+ : "Remove element from Android XML",
+ String.format("Do you really want to remove %1$s?", sb.toString()))) {
+ commitPendingXmlChanges();
+ getRootNode().getEditor().editXmlModel(new Runnable() {
+ public void run() {
+ UiElementNode previous = null;
+ UiElementNode parent = null;
+
+ for (int i = len - 1; i >= 0; i--) {
+ UiElementNode node = nodes.get(i);
+ previous = node.getUiPreviousSibling();
+ parent = node.getUiParent();
+
+ // delete node
+ node.deleteXmlNode();
+ }
+
+ // try to select the last previous sibling or the last parent
+ if (previous != null) {
+ selectUiNode(previous);
+ } else if (parent != null) {
+ selectUiNode(parent);
+ }
+ }
+ });
+ }
+ }
+
+ /**
+ * Called when the "Up" button is selected.
+ * <p/>
+ * If the tree has a selection, move it up, either in the child list or as the last child
+ * of the previous parent.
+ */
+ public void doUp(final List<UiElementNode> nodes) {
+ if (nodes == null || nodes.size() < 1) {
+ return;
+ }
+
+ final Node[] select_xml_node = { null };
+ UiElementNode last_node = null;
+ UiElementNode search_root = null;
+
+ for (int i = 0; i < nodes.size(); i++) {
+ final UiElementNode node = last_node = nodes.get(i);
+
+ // the node will move either up to its parent or grand-parent
+ search_root = node.getUiParent();
+ if (search_root != null && search_root.getUiParent() != null) {
+ search_root = search_root.getUiParent();
+ }
+
+ commitPendingXmlChanges();
+ getRootNode().getEditor().editXmlModel(new Runnable() {
+ public void run() {
+ Node xml_node = node.getXmlNode();
+ if (xml_node != null) {
+ Node xml_parent = xml_node.getParentNode();
+ if (xml_parent != null) {
+ UiElementNode ui_prev = node.getUiPreviousSibling();
+ if (ui_prev != null && ui_prev.getXmlNode() != null) {
+ // This node is not the first one of the parent, so it can be
+ // removed and then inserted before its previous sibling.
+ // If the previous sibling can have children, though, then it
+ // is inserted at the end of the children list.
+ Node xml_prev = ui_prev.getXmlNode();
+ if (ui_prev.getDescriptor().hasChildren()) {
+ xml_prev.appendChild(xml_parent.removeChild(xml_node));
+ select_xml_node[0] = xml_node;
+ } else {
+ xml_parent.insertBefore(
+ xml_parent.removeChild(xml_node),
+ xml_prev);
+ select_xml_node[0] = xml_node;
+ }
+ } else if (!(xml_parent instanceof Document) &&
+ xml_parent.getParentNode() != null &&
+ !(xml_parent.getParentNode() instanceof Document)) {
+ // If the node is the first one of the child list of its
+ // parent, move it up in the hierarchy as previous sibling
+ // to the parent. This is only possible if the parent of the
+ // parent is not a document.
+ Node grand_parent = xml_parent.getParentNode();
+ grand_parent.insertBefore(xml_parent.removeChild(xml_node),
+ xml_parent);
+ select_xml_node[0] = xml_node;
+ }
+ }
+ }
+ }
+ });
+ }
+
+ if (select_xml_node[0] == null) {
+ // The XML node has not been moved, we can just select the same UI node
+ selectUiNode(last_node);
+ } else {
+ // The XML node has moved. At this point the UI model has been reloaded
+ // and the XML node has been affected to a new UI node. Find that new UI
+ // node and select it.
+ if (search_root == null) {
+ search_root = last_node.getUiRoot();
+ }
+ if (search_root != null) {
+ selectUiNode(search_root.findXmlNode(select_xml_node[0]));
+ }
+ }
+ }
+
+ /**
+ * Called when the "Down" button is selected.
+ *
+ * If the tree has a selection, move it down, either in the same child list or as the
+ * first child of the next parent.
+ */
+ public void doDown(final List<UiElementNode> nodes) {
+ if (nodes == null || nodes.size() < 1) {
+ return;
+ }
+
+ final Node[] select_xml_node = { null };
+ UiElementNode last_node = null;
+ UiElementNode search_root = null;
+
+ for (int i = nodes.size() - 1; i >= 0; i--) {
+ final UiElementNode node = last_node = nodes.get(i);
+ // the node will move either down to its parent or grand-parent
+ search_root = node.getUiParent();
+ if (search_root != null && search_root.getUiParent() != null) {
+ search_root = search_root.getUiParent();
+ }
+
+ commitPendingXmlChanges();
+ getRootNode().getEditor().editXmlModel(new Runnable() {
+ public void run() {
+ Node xml_node = node.getXmlNode();
+ if (xml_node != null) {
+ Node xml_parent = xml_node.getParentNode();
+ if (xml_parent != null) {
+ UiElementNode uiNext = node.getUiNextSibling();
+ if (uiNext != null && uiNext.getXmlNode() != null) {
+ // This node is not the last one of the parent, so it can be
+ // removed and then inserted after its next sibling.
+ // If the next sibling is a node that can have children, though,
+ // then the node is inserted as the first child.
+ Node xml_next = uiNext.getXmlNode();
+ if (uiNext.getDescriptor().hasChildren()) {
+ // Note: insertBefore works as append if the ref node is
+ // null, i.e. when the node doesn't have children yet.
+ xml_next.insertBefore(xml_parent.removeChild(xml_node),
+ xml_next.getFirstChild());
+ select_xml_node[0] = xml_node;
+ } else {
+ // Insert "before after next" ;-)
+ xml_parent.insertBefore(xml_parent.removeChild(xml_node),
+ xml_next.getNextSibling());
+ select_xml_node[0] = xml_node;
+ }
+ } else if (!(xml_parent instanceof Document) &&
+ xml_parent.getParentNode() != null &&
+ !(xml_parent.getParentNode() instanceof Document)) {
+ // This node is the last node of its parent.
+ // If neither the parent nor the grandparent is a document,
+ // then the node can be insert right after the parent.
+ Node grand_parent = xml_parent.getParentNode();
+ grand_parent.insertBefore(xml_parent.removeChild(xml_node),
+ xml_parent.getNextSibling());
+ select_xml_node[0] = xml_node;
+ }
+ }
+ }
+ }
+ });
+ }
+
+ if (select_xml_node[0] == null) {
+ // The XML node has not been moved, we can just select the same UI node
+ selectUiNode(last_node);
+ } else {
+ // The XML node has moved. At this point the UI model has been reloaded
+ // and the XML node has been affected to a new UI node. Find that new UI
+ // node and select it.
+ if (search_root == null) {
+ search_root = last_node.getUiRoot();
+ }
+ if (search_root != null) {
+ selectUiNode(search_root.findXmlNode(select_xml_node[0]));
+ }
+ }
+ }
+
+ //---------------------
+
+ /**
+ * Adds a new element of the given descriptor's type to the given UI parent node.
+ *
+ * This actually creates the corresponding XML node in the XML model, which in turn
+ * will refresh the current tree view.
+ *
+ * @param uiParent An existing UI node or null to add to the tree root
+ * @param uiSibling An existing UI node to insert right before. Can be null.
+ * @param descriptor The descriptor of the element to add
+ * @param updateLayout True if layout attributes should be set
+ * @return The {@link UiElementNode} that has been added to the UI tree.
+ */
+ private UiElementNode addNewTreeElement(UiElementNode uiParent,
+ final UiElementNode uiSibling,
+ ElementDescriptor descriptor,
+ final boolean updateLayout) {
+ commitPendingXmlChanges();
+
+ int index = 0;
+ for (UiElementNode uiChild : uiParent.getUiChildren()) {
+ if (uiChild == uiSibling) {
+ break;
+ }
+ index++;
+ }
+
+ final UiElementNode uiNew = uiParent.insertNewUiChild(index, descriptor);
+ UiElementNode rootNode = getRootNode();
+
+ rootNode.getEditor().editXmlModel(new Runnable() {
+ public void run() {
+ DescriptorsUtils.setDefaultLayoutAttributes(uiNew, updateLayout);
+ Node xmlNode = uiNew.createXmlNode();
+ }
+ });
+ return uiNew;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiElementDetail.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiElementDetail.java
new file mode 100644
index 0000000..15c67c3
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiElementDetail.java
@@ -0,0 +1,486 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.ui.tree;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.sdk.Sdk;
+import com.android.ide.eclipse.editors.AndroidEditor;
+import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.editors.descriptors.SeparatorAttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.XmlnsAttributeDescriptor;
+import com.android.ide.eclipse.editors.ui.SectionHelper;
+import com.android.ide.eclipse.editors.ui.SectionHelper.ManifestSectionPart;
+import com.android.ide.eclipse.editors.uimodel.IUiUpdateListener;
+import com.android.ide.eclipse.editors.uimodel.UiAttributeNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ITreeSelection;
+import org.eclipse.swt.events.DisposeEvent;
+import org.eclipse.swt.events.DisposeListener;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.ui.forms.IDetailsPage;
+import org.eclipse.ui.forms.IFormPart;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.events.ExpansionEvent;
+import org.eclipse.ui.forms.events.IExpansionListener;
+import org.eclipse.ui.forms.widgets.FormText;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.Section;
+import org.eclipse.ui.forms.widgets.SharedScrolledComposite;
+import org.eclipse.ui.forms.widgets.TableWrapData;
+import org.eclipse.ui.forms.widgets.TableWrapLayout;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+
+import java.util.Collection;
+import java.util.HashSet;
+
+/**
+ * Details page for the {@link UiElementNode} nodes in the tree view.
+ * <p/>
+ * See IDetailsBase for more details.
+ */
+class UiElementDetail implements IDetailsPage {
+
+ /** The master-detail part, composed of a main tree and an auxiliary detail part */
+ private ManifestSectionPart mMasterPart;
+
+ private Section mMasterSection;
+ private UiElementNode mCurrentUiElementNode;
+ private Composite mCurrentTable;
+ private boolean mIsDirty;
+
+ private IManagedForm mManagedForm;
+
+ private final UiTreeBlock mTree;
+
+ public UiElementDetail(UiTreeBlock tree) {
+ mTree = tree;
+ mMasterPart = mTree.getMasterPart();
+ mManagedForm = mMasterPart.getManagedForm();
+ }
+
+ /* (non-java doc)
+ * Initializes the part.
+ */
+ public void initialize(IManagedForm form) {
+ mManagedForm = form;
+ }
+
+ /* (non-java doc)
+ * Creates the contents of the page in the provided parent.
+ */
+ public void createContents(Composite parent) {
+ mMasterSection = createMasterSection(parent);
+ }
+
+ /* (non-java doc)
+ * Called when the provided part has changed selection state.
+ * <p/>
+ * Only reply when our master part originates the selection.
+ */
+ public void selectionChanged(IFormPart part, ISelection selection) {
+ if (part == mMasterPart &&
+ !selection.isEmpty() &&
+ selection instanceof ITreeSelection) {
+ ITreeSelection tree_selection = (ITreeSelection) selection;
+
+ Object first = tree_selection.getFirstElement();
+ if (first instanceof UiElementNode) {
+ UiElementNode ui_node = (UiElementNode) first;
+ createUiAttributeControls(mManagedForm, ui_node);
+ }
+ }
+ }
+
+ /* (non-java doc)
+ * Instructs it to commit the new (modified) data back into the model.
+ */
+ public void commit(boolean onSave) {
+
+ IStructuredModel model = mTree.getEditor().getModelForEdit();
+ try {
+ // Notify the model we're about to change data...
+ model.aboutToChangeModel();
+
+ if (mCurrentUiElementNode != null) {
+ mCurrentUiElementNode.commit();
+ }
+
+ // Finally reset the dirty flag if everything was saved properly
+ mIsDirty = false;
+ } catch (Exception e) {
+ AdtPlugin.log(e, "Detail node failed to commit XML attribute!"); //$NON-NLS-1$
+ } finally {
+ // Notify the model we're done modifying it. This must *always* be executed.
+ model.changedModel();
+ model.releaseFromEdit();
+ }
+ }
+
+ public void dispose() {
+ // pass
+ }
+
+
+ /* (non-java doc)
+ * Returns true if the part has been modified with respect to the data
+ * loaded from the model.
+ */
+ public boolean isDirty() {
+ if (mCurrentUiElementNode != null && mCurrentUiElementNode.isDirty()) {
+ markDirty();
+ }
+ return mIsDirty;
+ }
+
+ public boolean isStale() {
+ // pass
+ return false;
+ }
+
+ /**
+ * Called by the master part when the tree is refreshed after the framework resources
+ * have been reloaded.
+ */
+ public void refresh() {
+ if (mCurrentTable != null) {
+ mCurrentTable.dispose();
+ mCurrentTable = null;
+ }
+ mCurrentUiElementNode = null;
+ mMasterSection.getParent().pack(true /* changed */);
+ }
+
+ public void setFocus() {
+ // pass
+ }
+
+ public boolean setFormInput(Object input) {
+ // pass
+ return false;
+ }
+
+ /**
+ * Creates a TableWrapLayout in the DetailsPage, which in turns contains a Section.
+ *
+ * All the UI should be created in a layout which parent is the mSection itself.
+ * The hierarchy is:
+ * <pre>
+ * DetailPage
+ * + TableWrapLayout
+ * + Section (with title/description && fill_grab horizontal)
+ * + TableWrapLayout [*]
+ * + Labels/Forms/etc... [*]
+ * </pre>
+ * Both items marked with [*] are created by the derived classes to fit their needs.
+ *
+ * @param parent Parent of the mSection (from createContents)
+ * @return The new Section
+ */
+ private Section createMasterSection(Composite parent) {
+ TableWrapLayout layout = new TableWrapLayout();
+ layout.topMargin = 0;
+ parent.setLayout(layout);
+
+ FormToolkit toolkit = mManagedForm.getToolkit();
+ Section section = toolkit.createSection(parent, Section.TITLE_BAR);
+ section.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.TOP));
+ return section;
+ }
+
+ /**
+ * Create the ui attribute controls to edit the attributes for the given
+ * ElementDescriptor.
+ * <p/>
+ * This is called by the constructor.
+ * Derived classes can override this if necessary.
+ *
+ * @param managedForm The managed form
+ */
+ private void createUiAttributeControls(
+ final IManagedForm managedForm,
+ final UiElementNode ui_node) {
+
+ final ElementDescriptor elem_desc = ui_node.getDescriptor();
+ mMasterSection.setText(String.format("Attributes for %1$s", ui_node.getShortDescription()));
+
+ if (mCurrentUiElementNode != ui_node) {
+ // Before changing the table, commit all dirty state.
+ if (mIsDirty) {
+ commit(false);
+ }
+ if (mCurrentTable != null) {
+ mCurrentTable.dispose();
+ mCurrentTable = null;
+ }
+
+ // To iterate over all attributes, we use the {@link ElementDescriptor} instead
+ // of the {@link UiElementNode} because the attributes order is guaranteed in the
+ // descriptor but not in the node itself.
+ AttributeDescriptor[] attr_desc_list = ui_node.getAttributeDescriptors();
+
+ // If the attribute list contains at least one SeparatorAttributeDescriptor,
+ // sub-sections will be used. This needs to be known early as it influences the
+ // creation of the master table.
+ boolean useSubsections = false;
+ for (AttributeDescriptor attr_desc : attr_desc_list) {
+ if (attr_desc instanceof SeparatorAttributeDescriptor) {
+ // Sub-sections will be used. The default sections should no longer be
+ useSubsections = true;
+ break;
+ }
+ }
+
+ FormToolkit toolkit = managedForm.getToolkit();
+ Composite masterTable = SectionHelper.createTableLayout(mMasterSection,
+ toolkit, useSubsections ? 1 : 2 /* numColumns */);
+ mCurrentTable = masterTable;
+
+ mCurrentUiElementNode = ui_node;
+
+ if (elem_desc.getTooltip() != null) {
+ String tooltip;
+ if (Sdk.getCurrent() != null &&
+ Sdk.getCurrent().getDocumentationBaseUrl() != null) {
+ tooltip = DescriptorsUtils.formatFormText(elem_desc.getTooltip(),
+ elem_desc,
+ Sdk.getCurrent().getDocumentationBaseUrl());
+ } else {
+ tooltip = elem_desc.getTooltip();
+ }
+
+ try {
+ FormText text = SectionHelper.createFormText(masterTable, toolkit,
+ true /* isHtml */, tooltip, true /* setupLayoutData */);
+ text.addHyperlinkListener(mTree.getEditor().createHyperlinkListener());
+ Image icon = elem_desc.getIcon();
+ if (icon != null) {
+ text.setImage(DescriptorsUtils.IMAGE_KEY, icon);
+ }
+ } catch(Exception e) {
+ // The FormText parser is really really basic and will fail as soon as the
+ // HTML javadoc is ever so slightly malformatted.
+ AdtPlugin.log(e,
+ "Malformed javadoc, rejected by FormText for node %1$s: '%2$s'", //$NON-NLS-1$
+ ui_node.getDescriptor().getXmlName(),
+ tooltip);
+
+ // Fallback to a pure text tooltip, no fancy HTML
+ tooltip = DescriptorsUtils.formatTooltip(elem_desc.getTooltip());
+ Label label = SectionHelper.createLabel(masterTable, toolkit,
+ tooltip, tooltip);
+ }
+ }
+
+ Composite table = useSubsections ? null : masterTable;
+
+ for (AttributeDescriptor attr_desc : attr_desc_list) {
+ if (attr_desc instanceof XmlnsAttributeDescriptor) {
+ // Do not show hidden attributes
+ continue;
+ } else if (table == null || attr_desc instanceof SeparatorAttributeDescriptor) {
+ String title = null;
+ if (attr_desc instanceof SeparatorAttributeDescriptor) {
+ // xmlName is actually the label of the separator
+ title = attr_desc.getXmlLocalName();
+ } else {
+ title = String.format("Attributes from %1$s", elem_desc.getUiName());
+ }
+
+ table = createSubSectionTable(toolkit, masterTable, title);
+ if (attr_desc instanceof SeparatorAttributeDescriptor) {
+ continue;
+ }
+ }
+
+ UiAttributeNode ui_attr = ui_node.findUiAttribute(attr_desc);
+
+ if (ui_attr != null) {
+ ui_attr.createUiControl(table, managedForm);
+
+ if (ui_attr.getCurrentValue() != null &&
+ ui_attr.getCurrentValue().length() > 0) {
+ ((Section) table.getParent()).setExpanded(true);
+ }
+ } else {
+ // The XML has an extra unknown attribute.
+ // This is not expected to happen so it is ignored.
+ AdtPlugin.log(IStatus.INFO,
+ "Attribute %1$s not declared in node %2$s, ignored.", //$NON-NLS-1$
+ attr_desc.getXmlLocalName(),
+ ui_node.getDescriptor().getXmlName());
+ }
+ }
+
+ // Create a sub-section for the unknown attributes.
+ // It is initially hidden till there are some attributes to show here.
+ final Composite unknownTable = createSubSectionTable(toolkit, masterTable,
+ "Unknown XML Attributes");
+ unknownTable.getParent().setVisible(false); // set section to not visible
+ final HashSet<UiAttributeNode> reference = new HashSet<UiAttributeNode>();
+
+ final IUiUpdateListener updateListener = new IUiUpdateListener() {
+ public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state) {
+ if (state == UiUpdateState.ATTR_UPDATED) {
+ updateUnknownAttributesSection(ui_node, unknownTable, managedForm,
+ reference);
+ }
+ }
+ };
+ ui_node.addUpdateListener(updateListener);
+
+ // remove the listener when the UI is disposed
+ unknownTable.addDisposeListener(new DisposeListener() {
+ public void widgetDisposed(DisposeEvent e) {
+ ui_node.removeUpdateListener(updateListener);
+ }
+ });
+
+ updateUnknownAttributesSection(ui_node, unknownTable, managedForm, reference);
+
+ mMasterSection.getParent().pack(true /* changed */);
+ }
+ }
+
+ /**
+ * Create a sub Section and its embedding wrapper table with 2 columns.
+ * @return The table, child of a new section.
+ */
+ private Composite createSubSectionTable(FormToolkit toolkit,
+ Composite masterTable, String title) {
+
+ // The Section composite seems to ignore colspan when assigned a TableWrapData so
+ // if the parent is a table with more than one column an extra table with one column
+ // is inserted to respect colspan.
+ int parentNumCol = ((TableWrapLayout) masterTable.getLayout()).numColumns;
+ if (parentNumCol > 1) {
+ masterTable = SectionHelper.createTableLayout(masterTable, toolkit, 1);
+ TableWrapData twd = new TableWrapData(TableWrapData.FILL_GRAB);
+ twd.maxWidth = AndroidEditor.TEXT_WIDTH_HINT;
+ twd.colspan = parentNumCol;
+ masterTable.setLayoutData(twd);
+ }
+
+ Composite table;
+ Section section = toolkit.createSection(masterTable,
+ Section.TITLE_BAR | Section.TWISTIE);
+
+ // Add an expansion listener that will trigger a reflow on the parent
+ // ScrolledPageBook (which is actually a SharedScrolledComposite). This will
+ // recompute the correct size and adjust the scrollbar as needed.
+ section.addExpansionListener(new IExpansionListener() {
+ public void expansionStateChanged(ExpansionEvent e) {
+ reflowMasterSection();
+ }
+
+ public void expansionStateChanging(ExpansionEvent e) {
+ // pass
+ }
+ });
+
+ section.setText(title);
+ section.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB,
+ TableWrapData.TOP));
+ table = SectionHelper.createTableLayout(section, toolkit, 2 /* numColumns */);
+ return table;
+ }
+
+ /**
+ * Reflow the parent ScrolledPageBook (which is actually a SharedScrolledComposite).
+ * This will recompute the correct size and adjust the scrollbar as needed.
+ */
+ private void reflowMasterSection() {
+ for(Composite c = mMasterSection; c != null; c = c.getParent()) {
+ if (c instanceof SharedScrolledComposite) {
+ ((SharedScrolledComposite) c).reflow(true /* flushCache */);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Updates the unknown attributes section for the UI Node.
+ */
+ private void updateUnknownAttributesSection(UiElementNode ui_node,
+ final Composite unknownTable, final IManagedForm managedForm,
+ HashSet<UiAttributeNode> reference) {
+ Collection<UiAttributeNode> ui_attrs = ui_node.getUnknownUiAttributes();
+ Section section = ((Section) unknownTable.getParent());
+ boolean needs_reflow = false;
+
+ // The table was created hidden, show it if there are unknown attributes now
+ if (ui_attrs.size() > 0 && !section.isVisible()) {
+ section.setVisible(true);
+ needs_reflow = true;
+ }
+
+ // Compare the new attribute set with the old "reference" one
+ boolean has_differences = ui_attrs.size() != reference.size();
+ if (!has_differences) {
+ for (UiAttributeNode ui_attr : ui_attrs) {
+ if (!reference.contains(ui_attr)) {
+ has_differences = true;
+ break;
+ }
+ }
+ }
+
+ if (has_differences) {
+ needs_reflow = true;
+ reference.clear();
+
+ // Remove all children of the table
+ for (Control c : unknownTable.getChildren()) {
+ c.dispose();
+ }
+
+ // Recreate all attributes UI
+ for (UiAttributeNode ui_attr : ui_attrs) {
+ reference.add(ui_attr);
+ ui_attr.createUiControl(unknownTable, managedForm);
+
+ if (ui_attr.getCurrentValue() != null && ui_attr.getCurrentValue().length() > 0) {
+ section.setExpanded(true);
+ }
+ }
+ }
+
+ if (needs_reflow) {
+ reflowMasterSection();
+ }
+ }
+
+ /**
+ * Marks the part dirty. Called as a result of user interaction with the widgets in the
+ * section.
+ */
+ private void markDirty() {
+ if (!mIsDirty) {
+ mIsDirty = true;
+ mManagedForm.dirtyStateChanged();
+ }
+ }
+}
+
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiModelTreeContentProvider.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiModelTreeContentProvider.java
new file mode 100644
index 0000000..9f34d9e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiModelTreeContentProvider.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.ui.tree;
+
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.Viewer;
+
+import java.util.ArrayList;
+
+/**
+ * UiModelTreeContentProvider is a trivial implementation of {@link ITreeContentProvider}
+ * where elements are expected to be instances of {@link UiElementNode}.
+ */
+class UiModelTreeContentProvider implements ITreeContentProvider {
+
+ /** The root {@link UiElementNode} which contains all the elements that are to be
+ * manipulated by this tree view. In general this is the manifest UI node. */
+ private UiElementNode mUiRootNode;
+ /** The descriptor of the elements to be displayed as root in this tree view. All elements
+ * of the same type in the root will be displayed. */
+ private ElementDescriptor[] mDescriptorFilters;
+
+ public UiModelTreeContentProvider(UiElementNode uiRootNode,
+ ElementDescriptor[] descriptorFilters) {
+ mUiRootNode = uiRootNode;
+ mDescriptorFilters = descriptorFilters;
+ }
+
+ /* (non-java doc)
+ * Returns all the UI node children of the given element or null if not the right kind
+ * of object. */
+ public Object[] getChildren(Object parentElement) {
+ if (parentElement instanceof UiElementNode) {
+ UiElementNode node = (UiElementNode) parentElement;
+ return node.getUiChildren().toArray();
+ }
+ return null;
+ }
+
+ /* (non-java doc)
+ * Returns the parent of a given UI node or null if it's a root node or it's not the
+ * right kind of node. */
+ public Object getParent(Object element) {
+ if (element instanceof UiElementNode) {
+ UiElementNode node = (UiElementNode) element;
+ return node.getUiParent();
+ }
+ return null;
+ }
+
+ /* (non-java doc)
+ * Returns true if the UI node has any UI children nodes. */
+ public boolean hasChildren(Object element) {
+ if (element instanceof UiElementNode) {
+ UiElementNode node = (UiElementNode) element;
+ return node.getUiChildren().size() > 0;
+ }
+ return false;
+ }
+
+ /* (non-java doc)
+ * Get root elements for the tree. These are all the UI nodes that
+ * match the filter descriptor in the current root node.
+ * <p/>
+ * Although not documented, it seems this method should not return null.
+ * At worse, it should return new Object[0].
+ * <p/>
+ * inputElement is not currently used. The root node and the filter are given
+ * by the enclosing class.
+ */
+ public Object[] getElements(Object inputElement) {
+ ArrayList<UiElementNode> roots = new ArrayList<UiElementNode>();
+ if (mUiRootNode != null) {
+ for (UiElementNode ui_node : mUiRootNode.getUiChildren()) {
+ if (mDescriptorFilters == null || mDescriptorFilters.length == 0) {
+ roots.add(ui_node);
+ } else {
+ for (ElementDescriptor filter : mDescriptorFilters) {
+ if (ui_node.getDescriptor() == filter) {
+ roots.add(ui_node);
+ }
+ }
+ }
+ }
+ }
+
+ return roots.toArray();
+ }
+
+ public void dispose() {
+ // pass
+ }
+
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ // pass
+ }
+}
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiModelTreeLabelProvider.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiModelTreeLabelProvider.java
new file mode 100644
index 0000000..273a30b
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiModelTreeLabelProvider.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.ui.tree;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.editors.ui.ErrorImageComposite;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.swt.graphics.Image;
+
+/**
+ * UiModelTreeLabelProvider is a trivial implementation of {@link ILabelProvider}
+ * where elements are expected to derive from {@link UiElementNode} or
+ * from {@link ElementDescriptor}.
+ *
+ * It is used by both the master tree viewer and by the list in the Add... selection dialog.
+ */
+public class UiModelTreeLabelProvider implements ILabelProvider {
+
+ public UiModelTreeLabelProvider() {
+ }
+
+ /**
+ * Returns the element's logo with a fallback on the android logo.
+ */
+ public Image getImage(Object element) {
+ ElementDescriptor desc = null;
+ if (element instanceof ElementDescriptor) {
+ Image img = ((ElementDescriptor) element).getIcon();
+ if (img != null) {
+ return img;
+ }
+ } else if (element instanceof UiElementNode) {
+ UiElementNode node = (UiElementNode) element;
+ desc = node.getDescriptor();
+ if (desc != null) {
+ Image img = desc.getIcon();
+ if (img != null) {
+ if (node.hasError()) {
+ //TODO: cache image
+ return new ErrorImageComposite(img).createImage();
+ } else {
+ return img;
+ }
+ }
+ }
+ }
+ return AdtPlugin.getAndroidLogo();
+ }
+
+ /**
+ * Uses UiElementNode.shortDescription for the label for this tree item.
+ */
+ public String getText(Object element) {
+ if (element instanceof ElementDescriptor) {
+ ElementDescriptor desc = (ElementDescriptor) element;
+ return desc.getUiName();
+ } else if (element instanceof UiElementNode) {
+ UiElementNode node = (UiElementNode) element;
+ return node.getShortDescription();
+ }
+ return element.toString();
+ }
+
+ public void addListener(ILabelProviderListener listener) {
+ // pass
+ }
+
+ public void dispose() {
+ // pass
+ }
+
+ public boolean isLabelProperty(Object element, String property) {
+ // pass
+ return false;
+ }
+
+ public void removeListener(ILabelProviderListener listener) {
+ // pass
+ }
+}
+
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiTreeBlock.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiTreeBlock.java
new file mode 100644
index 0000000..fc384e8
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/ui/tree/UiTreeBlock.java
@@ -0,0 +1,882 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.ui.tree;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.sdk.Sdk.ITargetChangeListener;
+import com.android.ide.eclipse.editors.AndroidEditor;
+import com.android.ide.eclipse.editors.IconFactory;
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.editors.ui.SectionHelper;
+import com.android.ide.eclipse.editors.ui.SectionHelper.ManifestSectionPart;
+import com.android.ide.eclipse.editors.uimodel.IUiUpdateListener;
+import com.android.ide.eclipse.editors.uimodel.UiDocumentNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.IMenuListener;
+import org.eclipse.jface.action.IMenuManager;
+import org.eclipse.jface.action.MenuManager;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.jface.action.ToolBarManager;
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.ISelectionChangedListener;
+import org.eclipse.jface.viewers.ITreeSelection;
+import org.eclipse.jface.viewers.SelectionChangedEvent;
+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.viewers.ViewerComparator;
+import org.eclipse.jface.viewers.ViewerFilter;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.dnd.Clipboard;
+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.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.Menu;
+import org.eclipse.swt.widgets.ToolBar;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.ui.forms.DetailsPart;
+import org.eclipse.ui.forms.IDetailsPage;
+import org.eclipse.ui.forms.IDetailsPageProvider;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.MasterDetailsBlock;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.Section;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedList;
+
+/**
+ * {@link UiTreeBlock} is a {@link MasterDetailsBlock} which displays a tree view for
+ * a specific set of {@link UiElementNode}.
+ * <p/>
+ * For a given UI element node, the tree view displays all first-level children that
+ * match a given type (given by an {@link ElementDescriptor}. All children from these
+ * nodes are also displayed.
+ * <p/>
+ * In the middle next to the tree are some controls to add or delete tree nodes.
+ * On the left is a details part that displays all the visible UI attributes for a given
+ * selected UI element node.
+ */
+public final class UiTreeBlock extends MasterDetailsBlock implements ICommitXml {
+
+ /** Height hint for the tree view. Helps the grid layout resize properly on smaller screens. */
+ private static final int TREE_HEIGHT_HINT = 50;
+
+ /** Container editor */
+ AndroidEditor mEditor;
+ /** The root {@link UiElementNode} which contains all the elements that are to be
+ * manipulated by this tree view. In general this is the manifest UI node. */
+ private UiElementNode mUiRootNode;
+ /** The descriptor of the elements to be displayed as root in this tree view. All elements
+ * of the same type in the root will be displayed. */
+ private ElementDescriptor[] mDescriptorFilters;
+ /** The title for the master-detail part (displayed on the top "tab" on top of the tree) */
+ private String mTitle;
+ /** The description for the master-detail part (displayed on top of the tree view) */
+ private String mDescription;
+ /** The master-detail part, composed of a main tree and an auxiliary detail part */
+ private ManifestSectionPart mMasterPart;
+ /** The tree viewer in the master-detail part */
+ private TreeViewer mTreeViewer;
+ /** The "add" button for the tree view */
+ private Button mAddButton;
+ /** The "remove" button for the tree view */
+ private Button mRemoveButton;
+ /** The "up" button for the tree view */
+ private Button mUpButton;
+ /** The "down" button for the tree view */
+ private Button mDownButton;
+ /** The Managed Form used to create the master part */
+ private IManagedForm mManagedForm;
+ /** Reference to the details part of the tree master block. */
+ private DetailsPart mDetailsPart;
+ /** Reference to the clipboard for copy-paste */
+ private Clipboard mClipboard;
+ /** Listener to refresh the tree viewer when the parent's node has been updated */
+ private IUiUpdateListener mUiRefreshListener;
+ /** Listener to enable/disable the UI based on the application node's presence */
+ private IUiUpdateListener mUiEnableListener;
+ /** An adapter/wrapper to use the add/remove/up/down tree edit actions. */
+ private UiTreeActions mUiTreeActions;
+ /**
+ * True if the root node can be created on-demand (i.e. as needed as
+ * soon as children exist). False if an external entity controls the existence of the
+ * root node. In practise, this is false for the manifest application page (the actual
+ * "application" node is managed by the ApplicationToggle part) whereas it is true
+ * for all other tree pages.
+ */
+ private final boolean mAutoCreateRoot;
+
+
+ /**
+ * Creates a new {@link MasterDetailsBlock} that will display all UI nodes matching the
+ * given filter in the given root node.
+ *
+ * @param editor The parent manifest editor.
+ * @param uiRootNode The root {@link UiElementNode} which contains all the elements that are
+ * to be manipulated by this tree view. In general this is the manifest UI node or the
+ * application UI node. This cannot be null.
+ * @param autoCreateRoot True if the root node can be created on-demand (i.e. as needed as
+ * soon as children exist). False if an external entity controls the existence of the
+ * root node. In practise, this is false for the manifest application page (the actual
+ * "application" node is managed by the ApplicationToggle part) whereas it is true
+ * for all other tree pages.
+ * @param descriptorFilters A list of descriptors of the elements to be displayed as root in
+ * this tree view. Use null or an empty list to accept any kind of node.
+ * @param title Title for the section
+ * @param description Description for the section
+ */
+ public UiTreeBlock(AndroidEditor editor,
+ UiElementNode uiRootNode,
+ boolean autoCreateRoot,
+ ElementDescriptor[] descriptorFilters,
+ String title,
+ String description) {
+ mEditor = editor;
+ mUiRootNode = uiRootNode;
+ mAutoCreateRoot = autoCreateRoot;
+ mDescriptorFilters = descriptorFilters;
+ mTitle = title;
+ mDescription = description;
+ }
+
+ /** @returns The container editor */
+ AndroidEditor getEditor() {
+ return mEditor;
+ }
+
+ /** @returns The reference to the clipboard for copy-paste */
+ Clipboard getClipboard() {
+ return mClipboard;
+ }
+
+ /** @returns The master-detail part, composed of a main tree and an auxiliary detail part */
+ ManifestSectionPart getMasterPart() {
+ return mMasterPart;
+ }
+
+ @Override
+ protected void createMasterPart(final IManagedForm managedForm, Composite parent) {
+ FormToolkit toolkit = managedForm.getToolkit();
+
+ mManagedForm = managedForm;
+ mMasterPart = new ManifestSectionPart(parent, toolkit);
+ Section section = mMasterPart.getSection();
+ section.setText(mTitle);
+ section.setDescription(mDescription);
+ section.setLayout(new GridLayout());
+ section.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ Composite grid = SectionHelper.createGridLayout(section, toolkit, 2);
+
+ Tree tree = createTreeViewer(toolkit, grid, managedForm);
+ createButtons(toolkit, grid);
+ createTreeContextMenu(tree);
+ createSectionActions(section, toolkit);
+ }
+
+ private void createSectionActions(Section section, FormToolkit toolkit) {
+ ToolBarManager manager = new ToolBarManager(SWT.FLAT);
+ manager.removeAll();
+
+ ToolBar toolbar = manager.createControl(section);
+ section.setTextClient(toolbar);
+
+ ElementDescriptor[] descs = mDescriptorFilters;
+ if (descs == null && mUiRootNode != null) {
+ descs = mUiRootNode.getDescriptor().getChildren();
+ }
+
+ if (descs != null && descs.length > 1) {
+ for (ElementDescriptor desc : descs) {
+ manager.add(new DescriptorFilterAction(desc));
+ }
+ }
+
+ manager.add(new TreeSortAction());
+
+ manager.update(true /*force*/);
+ }
+
+ /**
+ * Creates the tree and its viewer
+ * @return The tree control
+ */
+ private Tree createTreeViewer(FormToolkit toolkit, Composite grid,
+ final IManagedForm managedForm) {
+ // Note: we *could* use a FilteredTree instead of the Tree+TreeViewer here.
+ // However the class must be adapted to create an adapted toolkit tree.
+ final Tree tree = toolkit.createTree(grid, SWT.MULTI);
+ GridData gd = new GridData(GridData.FILL_BOTH);
+ gd.widthHint = AndroidEditor.TEXT_WIDTH_HINT;
+ gd.heightHint = TREE_HEIGHT_HINT;
+ tree.setLayoutData(gd);
+
+ mTreeViewer = new TreeViewer(tree);
+ mTreeViewer.setContentProvider(new UiModelTreeContentProvider(
+ mUiRootNode, mDescriptorFilters));
+ mTreeViewer.setLabelProvider(new UiModelTreeLabelProvider());
+ mTreeViewer.setInput("unused"); //$NON-NLS-1$
+
+ // Create a listener that reacts to selections on the tree viewer.
+ // When a selection is made, ask the managed form to propagate an event to
+ // all parts in the managed form.
+ // This is picked up by UiElementDetail.selectionChanged().
+ mTreeViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+ public void selectionChanged(SelectionChangedEvent event) {
+ managedForm.fireSelectionChanged(mMasterPart, event.getSelection());
+ adjustTreeButtons(event.getSelection());
+ }
+ });
+
+ // Create three listeners:
+ // - One to refresh the tree viewer when the parent's node has been updated
+ // - One to refresh the tree viewer when the framework resources have changed
+ // - One to enable/disable the UI based on the application node's presence.
+ mUiRefreshListener = new IUiUpdateListener() {
+ public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state) {
+ mTreeViewer.refresh();
+ }
+ };
+
+ mUiEnableListener = new IUiUpdateListener() {
+ public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state) {
+ // The UiElementNode for the application XML node always exists, even
+ // if there is no corresponding XML node in the XML file.
+ //
+ // Normally, we enable the UI here if the XML node is not null.
+ //
+ // However if mAutoCreateRoot is true, the root node will be created on-demand
+ // so the tree/block is always enabled.
+ boolean exists = mAutoCreateRoot || (ui_node.getXmlNode() != null);
+ if (mMasterPart != null) {
+ Section section = mMasterPart.getSection();
+ if (section.getEnabled() != exists) {
+ section.setEnabled(exists);
+ for (Control c : section.getChildren()) {
+ c.setEnabled(exists);
+ }
+ }
+ }
+ }
+ };
+
+ /** Listener to update the root node if the target of the file is changed because of a
+ * SDK location change or a project target change */
+ final ITargetChangeListener targetListener = new ITargetChangeListener() {
+ public void onProjectTargetChange(IProject changedProject) {
+ if (changedProject == mEditor.getProject()) {
+ onTargetsLoaded();
+ }
+ }
+
+ public void onTargetsLoaded() {
+ // If a details part has been created, we need to "refresh" it too.
+ if (mDetailsPart != null) {
+ // The details part does not directly expose access to its internal
+ // page book. Instead it is possible to resize the page book to 0 and then
+ // back to its original value, which has the side effect of removing all
+ // existing cached pages.
+ int limit = mDetailsPart.getPageLimit();
+ mDetailsPart.setPageLimit(0);
+ mDetailsPart.setPageLimit(limit);
+ }
+ // Refresh the tree, preserving the selection if possible.
+ mTreeViewer.refresh();
+ }
+ };
+
+ // Setup the listeners
+ changeRootAndDescriptors(mUiRootNode, mDescriptorFilters, false /* refresh */);
+
+ // Listen on resource framework changes to refresh the tree
+ AdtPlugin.getDefault().addTargetListener(targetListener);
+
+ // Remove listeners when the tree widget gets disposed.
+ tree.addDisposeListener(new DisposeListener() {
+ public void widgetDisposed(DisposeEvent e) {
+ UiElementNode node = mUiRootNode.getUiParent() != null ?
+ mUiRootNode.getUiParent() :
+ mUiRootNode;
+
+ node.removeUpdateListener(mUiRefreshListener);
+ mUiRootNode.removeUpdateListener(mUiEnableListener);
+
+ AdtPlugin.getDefault().removeTargetListener(targetListener);
+ if (mClipboard != null) {
+ mClipboard.dispose();
+ mClipboard = null;
+ }
+ }
+ });
+
+ // Get a new clipboard reference. It is disposed when the tree is disposed.
+ mClipboard = new Clipboard(tree.getDisplay());
+
+ return tree;
+ }
+
+ /**
+ * Changes the UI root node and the descriptor filters of the tree.
+ * <p/>
+ * This removes the listeners attached to the old root node and reattaches them to the
+ * new one.
+ *
+ * @param uiRootNode The root {@link UiElementNode} which contains all the elements that are
+ * to be manipulated by this tree view. In general this is the manifest UI node or the
+ * application UI node. This cannot be null.
+ * @param descriptorFilters A list of descriptors of the elements to be displayed as root in
+ * this tree view. Use null or an empty list to accept any kind of node.
+ * @param forceRefresh If tree, forces the tree to refresh
+ */
+ public void changeRootAndDescriptors(UiElementNode uiRootNode,
+ ElementDescriptor[] descriptorFilters, boolean forceRefresh) {
+ UiElementNode node;
+
+ // Remove previous listeners if any
+ if (mUiRootNode != null) {
+ node = mUiRootNode.getUiParent() != null ? mUiRootNode.getUiParent() : mUiRootNode;
+ node.removeUpdateListener(mUiRefreshListener);
+ mUiRootNode.removeUpdateListener(mUiEnableListener);
+ }
+
+ mUiRootNode = uiRootNode;
+ mDescriptorFilters = descriptorFilters;
+
+ // Listen on structural changes on the root node of the tree
+ // If the node has a parent, listen on the parent instead.
+ node = mUiRootNode.getUiParent() != null ? mUiRootNode.getUiParent() : mUiRootNode;
+ node.addUpdateListener(mUiRefreshListener);
+
+ // Use the root node to listen to its presence.
+ mUiRootNode.addUpdateListener(mUiEnableListener);
+
+ // Initialize the enabled/disabled state
+ mUiEnableListener.uiElementNodeUpdated(mUiRootNode, null /* state, not used */);
+
+ if (forceRefresh) {
+ mTreeViewer.refresh();
+ }
+
+ createSectionActions(mMasterPart.getSection(), mManagedForm.getToolkit());
+ }
+
+ /**
+ * Creates the buttons next to the tree.
+ */
+ private void createButtons(FormToolkit toolkit, Composite grid) {
+
+ mUiTreeActions = new UiTreeActions();
+
+ Composite button_grid = SectionHelper.createGridLayout(grid, toolkit, 1);
+ button_grid.setLayoutData(new GridData(GridData.VERTICAL_ALIGN_BEGINNING));
+ mAddButton = toolkit.createButton(button_grid, "Add...", SWT.PUSH);
+ SectionHelper.addControlTooltip(mAddButton, "Adds a new element.");
+ mAddButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL |
+ GridData.VERTICAL_ALIGN_BEGINNING));
+
+ mAddButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ super.widgetSelected(e);
+ doTreeAdd();
+ }
+ });
+
+ mRemoveButton = toolkit.createButton(button_grid, "Remove...", SWT.PUSH);
+ SectionHelper.addControlTooltip(mRemoveButton, "Removes an existing selected element.");
+ mRemoveButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ mRemoveButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ super.widgetSelected(e);
+ doTreeRemove();
+ }
+ });
+
+ mUpButton = toolkit.createButton(button_grid, "Up", SWT.PUSH);
+ SectionHelper.addControlTooltip(mRemoveButton, "Moves the selected element up.");
+ mUpButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ mUpButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ super.widgetSelected(e);
+ doTreeUp();
+ }
+ });
+
+ mDownButton = toolkit.createButton(button_grid, "Down", SWT.PUSH);
+ SectionHelper.addControlTooltip(mRemoveButton, "Moves the selected element down.");
+ mDownButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ mDownButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ super.widgetSelected(e);
+ doTreeDown();
+ }
+ });
+
+ adjustTreeButtons(TreeSelection.EMPTY);
+ }
+
+ private void createTreeContextMenu(Tree tree) {
+ MenuManager menuManager = new MenuManager();
+ menuManager.setRemoveAllWhenShown(true);
+ menuManager.addMenuListener(new IMenuListener() {
+ /**
+ * The menu is about to be shown. The menu manager has already been
+ * requested to remove any existing menu item. This method gets the
+ * tree selection and if it is of the appropriate type it re-creates
+ * the necessary actions.
+ */
+ public void menuAboutToShow(IMenuManager manager) {
+ ISelection selection = mTreeViewer.getSelection();
+ if (!selection.isEmpty() && selection instanceof ITreeSelection) {
+ ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection);
+ doCreateMenuAction(manager, selected);
+ return;
+ }
+ doCreateMenuAction(manager, null /* ui_node */);
+ }
+ });
+ Menu contextMenu = menuManager.createContextMenu(tree);
+ tree.setMenu(contextMenu);
+ }
+
+ /**
+ * Adds the menu actions to the context menu when the given UI node is selected in
+ * the tree view.
+ *
+ * @param manager The context menu manager
+ * @param selected The UI nodes selected in the tree. Can be null, in which case the root
+ * is to be modified.
+ */
+ private void doCreateMenuAction(IMenuManager manager, ArrayList<UiElementNode> selected) {
+ if (selected != null) {
+ boolean hasXml = false;
+ for (UiElementNode uiNode : selected) {
+ if (uiNode.getXmlNode() != null) {
+ hasXml = true;
+ break;
+ }
+ }
+
+ if (hasXml) {
+ manager.add(new CopyCutAction(getEditor(), getClipboard(),
+ null, selected, true /* cut */));
+ manager.add(new CopyCutAction(getEditor(), getClipboard(),
+ null, selected, false /* cut */));
+
+ // Can't paste with more than one element selected (the selection is the target)
+ if (selected.size() <= 1) {
+ // Paste is not valid if it would add a second element on a terminal element
+ // which parent is a document -- an XML document can only have one child. This
+ // means paste is valid if the current UI node can have children or if the
+ // parent is not a document.
+ UiElementNode ui_root = selected.get(0).getUiRoot();
+ if (ui_root.getDescriptor().hasChildren() ||
+ !(ui_root.getUiParent() instanceof UiDocumentNode)) {
+ manager.add(new PasteAction(getEditor(), getClipboard(), selected.get(0)));
+ }
+ }
+ manager.add(new Separator());
+ }
+ }
+
+ // Append "add" and "remove" actions. They do the same thing as the add/remove
+ // buttons on the side.
+ Action action;
+ IconFactory factory = IconFactory.getInstance();
+
+ // "Add" makes sense only if there's 0 or 1 item selected since the
+ // one selected item becomes the target.
+ if (selected == null || selected.size() <= 1) {
+ manager.add(new Action("Add...", factory.getImageDescriptor("add")) { //$NON-NLS-1$
+ @Override
+ public void run() {
+ super.run();
+ doTreeAdd();
+ }
+ });
+ }
+
+ if (selected != null) {
+ if (selected != null) {
+ manager.add(new Action("Remove", factory.getImageDescriptor("delete")) { //$NON-NLS-1$
+ @Override
+ public void run() {
+ super.run();
+ doTreeRemove();
+ }
+ });
+ }
+ manager.add(new Separator());
+
+ manager.add(new Action("Up", factory.getImageDescriptor("up")) { //$NON-NLS-1$
+ @Override
+ public void run() {
+ super.run();
+ doTreeUp();
+ }
+ });
+ manager.add(new Action("Down", factory.getImageDescriptor("down")) { //$NON-NLS-1$
+ @Override
+ public void run() {
+ super.run();
+ doTreeDown();
+ }
+ });
+ }
+ }
+
+
+ /**
+ * This is called by the tree when a selection is made.
+ * It enables/disables the buttons associated with the tree depending on the current
+ * selection.
+ *
+ * @param selection The current tree selection (same as mTreeViewer.getSelection())
+ */
+ private void adjustTreeButtons(ISelection selection) {
+ mRemoveButton.setEnabled(!selection.isEmpty() && selection instanceof ITreeSelection);
+ mUpButton.setEnabled(!selection.isEmpty() && selection instanceof ITreeSelection);
+ mDownButton.setEnabled(!selection.isEmpty() && selection instanceof ITreeSelection);
+ }
+
+ /**
+ * An adapter/wrapper to use the add/remove/up/down tree edit actions.
+ */
+ private class UiTreeActions extends UiActions {
+ @Override
+ protected UiElementNode getRootNode() {
+ return mUiRootNode;
+ }
+
+ @Override
+ protected void selectUiNode(UiElementNode uiNodeToSelect) {
+ // Select the new item
+ if (uiNodeToSelect != null) {
+ LinkedList<UiElementNode> segments = new LinkedList<UiElementNode>();
+ for (UiElementNode ui_node = uiNodeToSelect; ui_node != mUiRootNode;
+ ui_node = ui_node.getUiParent()) {
+ segments.add(0, ui_node);
+ }
+ if (segments.size() > 0) {
+ mTreeViewer.setSelection(new TreeSelection(new TreePath(segments.toArray())));
+ } else {
+ mTreeViewer.setSelection(null);
+ }
+ }
+ }
+
+ @Override
+ public void commitPendingXmlChanges() {
+ commitManagedForm();
+ }
+ }
+
+ /**
+ * Filters an ITreeSelection to only keep the {@link UiElementNode}s (in case there's
+ * something else in there).
+ *
+ * @return A new list of {@link UiElementNode} with at least one item or null.
+ */
+ @SuppressWarnings("unchecked")
+ private ArrayList<UiElementNode> filterSelection(ITreeSelection selection) {
+ ArrayList<UiElementNode> selected = new ArrayList<UiElementNode>();
+
+ for (Iterator it = selection.iterator(); it.hasNext(); ) {
+ Object selectedObj = it.next();
+
+ if (selectedObj instanceof UiElementNode) {
+ selected.add((UiElementNode) selectedObj);
+ }
+ }
+
+ return selected.size() > 0 ? selected : null;
+ }
+
+ /**
+ * Called when the "Add..." button next to the tree view is selected.
+ *
+ * Displays a selection dialog that lets the user select which kind of node
+ * to create, depending on the current selection.
+ */
+ private void doTreeAdd() {
+ UiElementNode ui_node = mUiRootNode;
+ ISelection selection = mTreeViewer.getSelection();
+ if (!selection.isEmpty() && selection instanceof ITreeSelection) {
+ ITreeSelection tree_selection = (ITreeSelection) selection;
+ Object first = tree_selection.getFirstElement();
+ if (first != null && first instanceof UiElementNode) {
+ ui_node = (UiElementNode) first;
+ }
+ }
+
+ mUiTreeActions.doAdd(
+ ui_node,
+ mDescriptorFilters,
+ mTreeViewer.getControl().getShell(),
+ (ILabelProvider) mTreeViewer.getLabelProvider());
+ }
+
+ /**
+ * Called when the "Remove" button is selected.
+ *
+ * If the tree has a selection, remove it.
+ * This simply deletes the XML node attached to the UI node: when the XML model fires the
+ * update event, the tree will get refreshed.
+ */
+ protected void doTreeRemove() {
+ ISelection selection = mTreeViewer.getSelection();
+ if (!selection.isEmpty() && selection instanceof ITreeSelection) {
+ ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection);
+ mUiTreeActions.doRemove(selected, mTreeViewer.getControl().getShell());
+ }
+ }
+
+ /**
+ * Called when the "Up" button is selected.
+ * <p/>
+ * If the tree has a selection, move it up, either in the child list or as the last child
+ * of the previous parent.
+ */
+ protected void doTreeUp() {
+ ISelection selection = mTreeViewer.getSelection();
+ if (!selection.isEmpty() && selection instanceof ITreeSelection) {
+ ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection);
+ mUiTreeActions.doUp(selected);
+ }
+ }
+
+ /**
+ * Called when the "Down" button is selected.
+ *
+ * If the tree has a selection, move it down, either in the same child list or as the
+ * first child of the next parent.
+ */
+ protected void doTreeDown() {
+ ISelection selection = mTreeViewer.getSelection();
+ if (!selection.isEmpty() && selection instanceof ITreeSelection) {
+ ArrayList<UiElementNode> selected = filterSelection((ITreeSelection) selection);
+ mUiTreeActions.doDown(selected);
+ }
+ }
+
+ /**
+ * Commits the current managed form (the one associated with our master part).
+ * As a side effect, this will commit the current UiElementDetails page.
+ */
+ void commitManagedForm() {
+ if (mManagedForm != null) {
+ mManagedForm.commit(false /* onSave */);
+ }
+ }
+
+ /* Implements ICommitXml for CopyCutAction */
+ public void commitPendingXmlChanges() {
+ commitManagedForm();
+ }
+
+ @Override
+ protected void createToolBarActions(IManagedForm managedForm) {
+ // Pass. Not used, toolbar actions are defined by createSectionActions().
+ }
+
+ @Override
+ protected void registerPages(DetailsPart detailsPart) {
+ // Keep a reference on the details part (the super class doesn't provide a getter
+ // for it.)
+ mDetailsPart = detailsPart;
+
+ // The page selection mechanism does not use pages registered by association with
+ // a node class. Instead it uses a custom details page provider that provides a
+ // new UiElementDetail instance for each node instance. A limit of 5 pages is
+ // then set (the value is arbitrary but should be reasonable) for the internal
+ // page book.
+ detailsPart.setPageLimit(5);
+
+ final UiTreeBlock tree = this;
+
+ detailsPart.setPageProvider(new IDetailsPageProvider() {
+ public IDetailsPage getPage(Object key) {
+ if (key instanceof UiElementNode) {
+ return new UiElementDetail(tree);
+ }
+ return null;
+ }
+
+ public Object getPageKey(Object object) {
+ return object; // use node object as key
+ }
+ });
+ }
+
+ /**
+ * An alphabetic sort action for the tree viewer.
+ */
+ private class TreeSortAction extends Action {
+
+ private ViewerComparator mComparator;
+
+ public TreeSortAction() {
+ super("Sorts elements alphabetically.", AS_CHECK_BOX);
+ setImageDescriptor(IconFactory.getInstance().getImageDescriptor("az_sort")); //$NON-NLS-1$
+
+ if (mTreeViewer != null) {
+ boolean is_sorted = mTreeViewer.getComparator() != null;
+ setChecked(is_sorted);
+ }
+ }
+
+ /**
+ * Called when the button is selected. Toggles the tree viewer comparator.
+ */
+ @Override
+ public void run() {
+ if (mTreeViewer == null) {
+ notifyResult(false /*success*/);
+ return;
+ }
+
+ ViewerComparator comp = mTreeViewer.getComparator();
+ if (comp != null) {
+ // Tree is currently sorted.
+ // Save currently comparator and remove it
+ mComparator = comp;
+ mTreeViewer.setComparator(null);
+ } else {
+ // Tree is not currently sorted.
+ // Reuse or add a new comparator.
+ if (mComparator == null) {
+ mComparator = new ViewerComparator();
+ }
+ mTreeViewer.setComparator(mComparator);
+ }
+
+ notifyResult(true /*success*/);
+ }
+ }
+
+ /**
+ * A filter on descriptor for the tree viewer.
+ * <p/>
+ * The tree viewer will contain many of these actions and only one can be enabled at a
+ * given time. When no action is selected, everything is displayed.
+ * <p/>
+ * Since "radio"-like actions do not allow for unselecting all of them, we manually
+ * handle the exclusive radio button-like property: when an action is selected, it manually
+ * removes all other actions as needed.
+ */
+ private class DescriptorFilterAction extends Action {
+
+ private final ElementDescriptor mDescriptor;
+ private ViewerFilter mFilter;
+
+ public DescriptorFilterAction(ElementDescriptor descriptor) {
+ super(String.format("Displays only %1$s elements.", descriptor.getUiName()),
+ AS_CHECK_BOX);
+
+ mDescriptor = descriptor;
+ setImageDescriptor(descriptor.getImageDescriptor());
+ }
+
+ /**
+ * Called when the button is selected.
+ * <p/>
+ * Find any existing {@link DescriptorFilter}s and remove them. Install ours.
+ */
+ @Override
+ public void run() {
+ super.run();
+
+ if (isChecked()) {
+ if (mFilter == null) {
+ // create filter when required
+ mFilter = new DescriptorFilter(this);
+ }
+
+ // we add our filter first, otherwise the UI might show the full list
+ mTreeViewer.addFilter(mFilter);
+
+ // Then remove the any other filters except ours. There should be at most
+ // one other filter, since that's how the actions are made to look like
+ // exclusive radio buttons.
+ for (ViewerFilter filter : mTreeViewer.getFilters()) {
+ if (filter instanceof DescriptorFilter && filter != mFilter) {
+ DescriptorFilterAction action = ((DescriptorFilter) filter).getAction();
+ action.setChecked(false);
+ mTreeViewer.removeFilter(filter);
+ }
+ }
+ } else if (mFilter != null){
+ mTreeViewer.removeFilter(mFilter);
+ }
+ }
+
+ /**
+ * Filters the tree viewer for the given descriptor.
+ * <p/>
+ * The filter is linked to the action so that an action can iterate through the list
+ * of filters and un-select the actions.
+ */
+ private class DescriptorFilter extends ViewerFilter {
+
+ private final DescriptorFilterAction mAction;
+
+ public DescriptorFilter(DescriptorFilterAction action) {
+ mAction = action;
+ }
+
+ public DescriptorFilterAction getAction() {
+ return mAction;
+ }
+
+ /**
+ * Returns true if an element should be displayed, that if the element or
+ * any of its parent matches the requested descriptor.
+ */
+ @Override
+ public boolean select(Viewer viewer, Object parentElement, Object element) {
+ while (element instanceof UiElementNode) {
+ UiElementNode uiNode = (UiElementNode)element;
+ if (uiNode.getDescriptor() == mDescriptor) {
+ return true;
+ }
+ element = uiNode.getUiParent();
+ }
+ return false;
+ }
+ }
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/IUiSettableAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/IUiSettableAttributeNode.java
new file mode 100644
index 0000000..7fe44da
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/IUiSettableAttributeNode.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.ide.eclipse.editors.uimodel;
+
+/**
+ * This interface decoration indicates that a given UiAttributeNode can both
+ * set and get its current value.
+ */
+public interface IUiSettableAttributeNode {
+
+ /** Returns the current value of the node. */
+ public String getCurrentValue();
+
+ /** Sets the current value of the node. Cannot be null (use an empty string). */
+ public void setCurrentValue(String value);
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/IUiUpdateListener.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/IUiUpdateListener.java
new file mode 100644
index 0000000..12cb31b
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/IUiUpdateListener.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.uimodel;
+
+
+/**
+ * Listen to update notifications in UI nodes.
+ */
+public interface IUiUpdateListener {
+
+ /** Update state of the UI node */
+ public enum UiUpdateState {
+ /** The node's attributes have been updated. They may or may not actually have changed. */
+ ATTR_UPDATED,
+ /** The node sub-structure (i.e. child nodes) has changed */
+ CHILDREN_CHANGED,
+ /** The XML counterpart for the UI node has just been created. */
+ CREATED,
+ /** The XML counterpart for the UI node has just been deleted.
+ * Note that mandatory UI nodes are never actually deleted. */
+ DELETED
+ }
+
+ /**
+ * Indicates that an UiElementNode has been updated.
+ * <p/>
+ * This happens when an {@link UiElementNode} is refreshed to match the
+ * XML model. The actual UI element node may or may not have changed.
+ *
+ * @param ui_node The {@link UiElementNode} being updated.
+ */
+ public void uiElementNodeUpdated(UiElementNode ui_node, UiUpdateState state);
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiAbstractTextAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiAbstractTextAttributeNode.java
new file mode 100644
index 0000000..4a9fbb1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiAbstractTextAttributeNode.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.uimodel;
+
+import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor;
+
+import org.w3c.dom.Node;
+
+/**
+ * Represents an XML attribute in that can be modified using a simple text field
+ * in the XML editor's user interface.
+ * <p/>
+ * The XML attribute has no default value. When unset, the text field is blank.
+ * When updating the XML, if the field is empty, the attribute will be removed
+ * from the XML element.
+ * <p/>
+ * See {@link UiAttributeNode} for more information.
+ */
+public abstract class UiAbstractTextAttributeNode extends UiAttributeNode
+ implements IUiSettableAttributeNode {
+
+ protected static final String DEFAULT_VALUE = ""; //$NON-NLS-1$
+
+ /** Prevent internal listener from firing when internally modifying the text */
+ private boolean mInternalTextModification;
+ /** Last value read from the XML model. Cannot be null. */
+ private String mCurrentValue = DEFAULT_VALUE;
+
+ public UiAbstractTextAttributeNode(AttributeDescriptor attributeDescriptor,
+ UiElementNode uiParent) {
+ super(attributeDescriptor, uiParent);
+ }
+
+ /** Returns the current value of the node. */
+ @Override
+ public final String getCurrentValue() {
+ return mCurrentValue;
+ }
+
+ /** Sets the current value of the node. Cannot be null (use an empty string). */
+ public final void setCurrentValue(String value) {
+ mCurrentValue = value;
+ }
+
+ /** Returns if the attribute node is valid, and its UI has been created. */
+ public abstract boolean isValid();
+
+ /** Returns the text value present in the UI. */
+ public abstract String getTextWidgetValue();
+
+ /** Sets the text value to be displayed in the UI. */
+ public abstract void setTextWidgetValue(String value);
+
+
+ /**
+ * Updates the current text field's value when the XML has changed.
+ * <p/>
+ * The caller doesn't really know if attributes have changed,
+ * so it will call this to refresh the attribute anyway. The value
+ * is only set if it has changed.
+ * <p/>
+ * This also resets the "dirty" flag.
+ */
+ @Override
+ public void updateValue(Node xml_attribute_node) {
+ mCurrentValue = DEFAULT_VALUE;
+ if (xml_attribute_node != null) {
+ mCurrentValue = xml_attribute_node.getNodeValue();
+ }
+
+ if (isValid() && !getTextWidgetValue().equals(mCurrentValue)) {
+ try {
+ mInternalTextModification = true;
+ setTextWidgetValue(mCurrentValue);
+ setDirty(false);
+ } finally {
+ mInternalTextModification = false;
+ }
+ }
+ }
+
+ /* (non-java doc)
+ * Called by the user interface when the editor is saved or its state changed
+ * and the modified attributes must be committed (i.e. written) to the XML model.
+ */
+ @Override
+ public void commit() {
+ UiElementNode parent = getUiParent();
+ if (parent != null && isValid() && isDirty()) {
+ String value = getTextWidgetValue();
+ if (parent.commitAttributeToXml(this, value)) {
+ mCurrentValue = value;
+ setDirty(false);
+ }
+ }
+ }
+
+ protected final boolean isInInternalTextModification() {
+ return mInternalTextModification;
+ }
+
+ protected final void setInInternalTextModification(boolean internalTextModification) {
+ mInternalTextModification = internalTextModification;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiAttributeNode.java
new file mode 100644
index 0000000..5972f22
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiAttributeNode.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.uimodel;
+
+import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor;
+
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.ui.forms.IManagedForm;
+import org.w3c.dom.Node;
+
+/**
+ * Represents an XML attribute that can be modified by the XML editor's user interface.
+ * <p/>
+ * The characteristics of an {@link UiAttributeNode} are declared by a
+ * corresponding {@link AttributeDescriptor}.
+ * <p/>
+ * This is an abstract class. Derived classes must implement the creation of the UI
+ * and manage its synchronization with the XML.
+ */
+public abstract class UiAttributeNode {
+
+ private AttributeDescriptor mDescriptor;
+ private UiElementNode mUiParent;
+ private boolean mIsDirty;
+ private boolean mHasError;
+
+ /** Creates a new {@link UiAttributeNode} linked to a specific {@link AttributeDescriptor}
+ * and the corresponding runtine {@link UiElementNode} parent. */
+ public UiAttributeNode(AttributeDescriptor attributeDescriptor, UiElementNode uiParent) {
+ mDescriptor = attributeDescriptor;
+ mUiParent = uiParent;
+ }
+
+ /** Returns the {@link AttributeDescriptor} specific to this UI attribute node */
+ public final AttributeDescriptor getDescriptor() {
+ return mDescriptor;
+ }
+
+ /** Returns the {@link UiElementNode} that owns this {@link UiAttributeNode} */
+ public final UiElementNode getUiParent() {
+ return mUiParent;
+ }
+
+ /** Returns the current value of the node. */
+ public abstract String getCurrentValue();
+
+ /**
+ * @return True if the attribute has been changed since it was last loaded
+ * from the XML model.
+ */
+ public final boolean isDirty() {
+ return mIsDirty;
+ }
+
+ /**
+ * Sets whether the attribute is dirty and also notifies the editor some part's dirty
+ * flag as changed.
+ * <p/>
+ * Subclasses should set the to true as a result of user interaction with the widgets in
+ * the section and then should set to false when the commit() method completed.
+ */
+ public void setDirty(boolean isDirty) {
+ boolean old_value = mIsDirty;
+ mIsDirty = isDirty;
+ // TODO: for unknown attributes, getParent() != null && getParent().getEditor() != null
+ if (old_value != isDirty) {
+ getUiParent().getEditor().editorDirtyStateChanged();
+ }
+ }
+
+ /**
+ * Sets the error flag value.
+ * @param errorFlag the error flag
+ */
+ public final void setHasError(boolean errorFlag) {
+ mHasError = errorFlag;
+ }
+
+ /**
+ * Returns whether this node has errors.
+ */
+ public final boolean hasError() {
+ return mHasError;
+ }
+
+ /**
+ * Called once by the parent user interface to creates the necessary
+ * user interface to edit this attribute.
+ * <p/>
+ * This method can be called more than once in the life cycle of an UI node,
+ * typically when the UI is part of a master-detail tree, as pages are swapped.
+ *
+ * @param parent The composite where to create the user interface.
+ * @param managedForm The managed form owning this part.
+ */
+ public abstract void createUiControl(Composite parent, IManagedForm managedForm);
+
+ /**
+ * Used to get a list of all possible values for this UI attribute.
+ * <p/>
+ * This is used, among other things, by the XML Content Assists to complete values
+ * for an attribute.
+ * <p/>
+ * Implementations that do not have any known values should return null.
+ *
+ * @return A list of possible completion values or null.
+ */
+ public abstract String[] getPossibleValues();
+
+ /**
+ * Called when the XML is being loaded or has changed to
+ * update the value held by this user interface attribute node.
+ * <p/>
+ * The XML Node <em>may</em> be null, which denotes that the attribute is not
+ * specified in the XML model. In general, this means the "default" value of the
+ * attribute should be used.
+ * <p/>
+ * The caller doesn't really know if attributes have changed,
+ * so it will call this to refresh the attribute anyway. It's up to the
+ * UI implementation to minimize refreshes.
+ *
+ * @param xml_attribute_node
+ */
+ public abstract void updateValue(Node xml_attribute_node);
+
+ /**
+ * Called by the user interface when the editor is saved or its state changed
+ * and the modified attributes must be committed (i.e. written) to the XML model.
+ * <p/>
+ * Important behaviors:
+ * <ul>
+ * <li>The caller *must* have called IStructuredModel.aboutToChangeModel before.
+ * The implemented methods must assume it is safe to modify the XML model.
+ * <li>On success, the implementation *must* call setDirty(false).
+ * <li>On failure, the implementation can fail with an exception, which
+ * is trapped and logged by the caller, or do nothing, whichever is more
+ * appropriate.
+ * </ul>
+ */
+ public abstract void commit();
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiDocumentNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiDocumentNode.java
new file mode 100644
index 0000000..113738f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiDocumentNode.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.uimodel;
+
+import com.android.ide.eclipse.editors.descriptors.DocumentDescriptor;
+import com.android.ide.eclipse.editors.uimodel.IUiUpdateListener.UiUpdateState;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+
+/**
+ * Represents an XML document node that can be modified by the user interface in the XML editor.
+ * <p/>
+ * The structure of a given {@link UiDocumentNode} is declared by a corresponding
+ * {@link DocumentDescriptor}.
+ */
+public class UiDocumentNode extends UiElementNode {
+
+ /**
+ * Creates a new {@link UiDocumentNode} described by a given {@link DocumentDescriptor}.
+ *
+ * @param documentDescriptor The {@link DocumentDescriptor} for the XML node. Cannot be null.
+ */
+ public UiDocumentNode(DocumentDescriptor documentDescriptor) {
+ super(documentDescriptor);
+ }
+
+ /**
+ * Computes a short string describing the UI node suitable for tree views.
+ * Uses the element's attribute "android:name" if present, or the "android:label" one
+ * followed by the element's name.
+ *
+ * @return A short string describing the UI node suitable for tree views.
+ */
+ @Override
+ public String getShortDescription() {
+ return "Document"; //$NON-NLS-1$
+ }
+
+ /**
+ * Computes a "breadcrumb trail" description for this node.
+ *
+ * @param include_root Whether to include the root (e.g. "Manifest") or not. Has no effect
+ * when called on the root node itself.
+ * @return The "breadcrumb trail" description for this node.
+ */
+ @Override
+ public String getBreadcrumbTrailDescription(boolean include_root) {
+ return "Document"; //$NON-NLS-1$
+ }
+
+ /**
+ * This method throws an exception when attempted to assign a parent, since XML documents
+ * cannot have a parent. It is OK to assign null.
+ */
+ @Override
+ protected void setUiParent(UiElementNode parent) {
+ if (parent != null) {
+ // DEBUG. Change to log warning.
+ throw new UnsupportedOperationException("Documents can't have UI parents"); //$NON-NLS-1$
+ }
+ super.setUiParent(null);
+ }
+
+ /**
+ * Populate this element node with all values from the given XML node.
+ *
+ * This fails if the given XML node has a different element name -- it won't change the
+ * type of this ui node.
+ *
+ * This method can be both used for populating values the first time and updating values
+ * after the XML model changed.
+ *
+ * @param xml_node The XML node to mirror
+ * @return Returns true if the XML structure has changed (nodes added, removed or replaced)
+ */
+ @Override
+ public boolean loadFromXmlNode(Node xml_node) {
+ boolean structure_changed = (getXmlDocument() != xml_node);
+ setXmlDocument((Document) xml_node);
+ structure_changed |= super.loadFromXmlNode(xml_node);
+ if (structure_changed) {
+ invokeUiUpdateListeners(UiUpdateState.CHILDREN_CHANGED);
+ }
+ return structure_changed;
+ }
+
+ /**
+ * This method throws an exception if there is no underlying XML document.
+ * <p/>
+ * XML documents cannot be created per se -- they are a by-product of the StructuredEditor
+ * XML parser.
+ *
+ * @return The current value of getXmlDocument().
+ */
+ @Override
+ public Node createXmlNode() {
+ if (getXmlDocument() == null) {
+ // By design, a document node cannot be created, it is owned by the XML parser.
+ // By "design" this should never happen since the XML parser always creates an XML
+ // document container, even for an empty file.
+ throw new UnsupportedOperationException("Documents cannot be created"); //$NON-NLS-1$
+ }
+ return getXmlDocument();
+ }
+
+ /**
+ * This method throws an exception and does not even try to delete the XML document.
+ * <p/>
+ * XML documents cannot be deleted per se -- they are a by-product of the StructuredEditor
+ * XML parser.
+ *
+ * @return The removed node or null if it didn't exist in the firtst place.
+ */
+ @Override
+ public Node deleteXmlNode() {
+ // DEBUG. Change to log warning.
+ throw new UnsupportedOperationException("Documents cannot be deleted"); //$NON-NLS-1$
+ }
+}
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiElementNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiElementNode.java
new file mode 100644
index 0000000..3728886
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiElementNode.java
@@ -0,0 +1,1500 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.uimodel;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.sdk.AndroidTargetData;
+import com.android.ide.eclipse.editors.AndroidEditor;
+import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.editors.descriptors.SeparatorAttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.XmlnsAttributeDescriptor;
+import com.android.ide.eclipse.editors.layout.descriptors.CustomViewDescriptorService;
+import com.android.ide.eclipse.editors.layout.descriptors.LayoutDescriptors;
+import com.android.ide.eclipse.editors.manifest.descriptors.AndroidManifestDescriptors;
+import com.android.ide.eclipse.editors.resources.descriptors.ResourcesDescriptors;
+import com.android.ide.eclipse.editors.uimodel.IUiUpdateListener.UiUpdateState;
+import com.android.ide.eclipse.editors.xml.descriptors.XmlDescriptors;
+import com.android.sdklib.SdkConstants;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IFileEditorInput;
+import org.eclipse.ui.views.properties.IPropertyDescriptor;
+import org.eclipse.ui.views.properties.IPropertySource;
+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 java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.Map.Entry;
+
+/**
+ * Represents an XML node that can be modified by the user interface in the XML editor.
+ * <p/>
+ * Each tree viewer used in the application page's parts needs to keep a model representing
+ * each underlying node in the tree. This interface represents the base type for such a node.
+ * <p/>
+ * Each node acts as an intermediary model between the actual XML model (the real data support)
+ * and the tree viewers or the corresponding page parts.
+ * <p/>
+ * Element nodes don't contain data per se. Their data is contained in their attributes
+ * as well as their children's attributes, see {@link UiAttributeNode}.
+ * <p/>
+ * The structure of a given {@link UiElementNode} is declared by a corresponding
+ * {@link ElementDescriptor}.
+ * <p/>
+ * The class implements {@link IPropertySource}, in order to fill the Eclipse property tab when
+ * an element is selected. The {@link AttributeDescriptor} are used property descriptors.
+ */
+public class UiElementNode implements IPropertySource {
+
+ /** List of prefixes removed from android:id strings when creating short descriptions. */
+ private static String[] ID_PREFIXES = {
+ "@android:id/", //$NON-NLS-1$
+ "@+id/", "@id/", "@+", "@" }; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
+
+ /** The element descriptor for the node. Always present, never null. */
+ private ElementDescriptor mDescriptor;
+ /** The parent element node in the UI model. It is null for a root element or until
+ * the node is attached to its parent. */
+ private UiElementNode mUiParent;
+ /** The {@link AndroidEditor} handling the UI hierarchy. This is defined only for the
+ * root node. All children have the value set to null and query their parent. */
+ private AndroidEditor mEditor;
+ /** The XML {@link Document} model that is being mirror by the UI model. This is defined
+ * only for the root node. All children have the value set to null and query their parent. */
+ private Document mXmlDocument;
+ /** The XML {@link Node} mirror by this UI node. This can be null for mandatory UI node which
+ * have no corresponding XML node or for new UI nodes before their XML node is set. */
+ private Node mXmlNode;
+ /** The list of all UI children nodes. Can be empty but never null. There's one UI children
+ * node per existing XML children node. */
+ private ArrayList<UiElementNode> mUiChildren;
+ /** The list of <em>all</em> UI attributes, as declared in the {@link ElementDescriptor}.
+ * The list is always defined and never null. Unlike the UiElementNode children list, this
+ * is always defined, even for attributes that do not exist in the XML model -- that's because
+ * "missing" attributes in the XML model simply mean a default value is used. Also note that
+ * the underlying collection is a map, so order is not respected. To get the desired attribute
+ * order, iterate through the {@link ElementDescriptor}'s attribute list. */
+ private HashMap<AttributeDescriptor, UiAttributeNode> mUiAttributes;
+ private HashSet<UiAttributeNode> mUnknownUiAttributes;
+ /** A read-only view of the UI children node collection. */
+ private List<UiElementNode> mReadOnlyUiChildren;
+ /** A read-only view of the UI attributes collection. */
+ private Collection<UiAttributeNode> mReadOnlyUiAttributes;
+ /** A map of hidden attribute descriptors. Key is the XML name. */
+ private Map<String, AttributeDescriptor> mCachedHiddenAttributes;
+ /** An optional list of {@link IUiUpdateListener}. Most element nodes will not have any
+ * listeners attached, so the list is only created on demand and can be null. */
+ private ArrayList<IUiUpdateListener> mUiUpdateListeners;
+ /** Error Flag */
+ private boolean mHasError;
+ /** Temporary data used by the editors. This data is not sync'ed with the XML */
+ private Object mEditData;
+
+ /**
+ * Creates a new {@link UiElementNode} described by a given {@link ElementDescriptor}.
+ *
+ * @param elementDescriptor The {@link ElementDescriptor} for the XML node. Cannot be null.
+ */
+ public UiElementNode(ElementDescriptor elementDescriptor) {
+ mDescriptor = elementDescriptor;
+ clearContent();
+ }
+
+ /**
+ * Clears the {@link UiElementNode} by resetting the children list and
+ * the {@link UiAttributeNode}s list.
+ * Also resets the attached XML node, document, editor if any.
+ * <p/>
+ * The parent {@link UiElementNode} node is not reset so that it's position
+ * in the hierarchy be left intact, if any.
+ */
+ /* package */ void clearContent() {
+ mXmlNode = null;
+ mXmlDocument = null;
+ mEditor = null;
+ clearAttributes();
+ mReadOnlyUiChildren = null;
+ if (mUiChildren == null) {
+ mUiChildren = new ArrayList<UiElementNode>();
+ } else {
+ // We can't remove mandatory nodes, we just clear them.
+ for (int i = mUiChildren.size() - 1; i >= 0; --i) {
+ removeUiChildAtIndex(i);
+ }
+ }
+ }
+
+ /**
+ * Clears the internal list of attributes, the read-only cached version of it
+ * and the read-only cached hidden attribute list.
+ */
+ private void clearAttributes() {
+ mUiAttributes = null;
+ mReadOnlyUiAttributes = null;
+ mCachedHiddenAttributes = null;
+ mUnknownUiAttributes = new HashSet<UiAttributeNode>();
+ }
+
+ /**
+ * Gets or creates the internal UiAttributes list.
+ * <p/>
+ * When the descriptor derives from ViewElementDescriptor, this list depends on the
+ * current UiParent node.
+ *
+ * @return A new set of {@link UiAttributeNode} that matches the expected
+ * attributes for this node.
+ */
+ private HashMap<AttributeDescriptor, UiAttributeNode> getInternalUiAttributes() {
+ if (mUiAttributes == null) {
+ AttributeDescriptor[] attr_list = getAttributeDescriptors();
+ mUiAttributes = new HashMap<AttributeDescriptor, UiAttributeNode>(attr_list.length);
+ for (AttributeDescriptor desc : attr_list) {
+ UiAttributeNode ui_node = desc.createUiNode(this);
+ if (ui_node != null) { // Some AttributeDescriptors do not have UI associated
+ mUiAttributes.put(desc, ui_node);
+ }
+ }
+ }
+ return mUiAttributes;
+ }
+
+ /**
+ * Computes a short string describing the UI node suitable for tree views.
+ * Uses the element's attribute "android:name" if present, or the "android:label" one
+ * followed by the element's name.
+ *
+ * @return A short string describing the UI node suitable for tree views.
+ */
+ public String getShortDescription() {
+ if (mXmlNode != null && mXmlNode instanceof Element && mXmlNode.hasAttributes()) {
+
+ // Application and Manifest nodes have a special treatment: they are unique nodes
+ // so we don't bother trying to differentiate their strings and we fall back to
+ // just using the UI name below.
+ Element elem = (Element) mXmlNode;
+
+ String attr = elem.getAttributeNS(SdkConstants.NS_RESOURCES,
+ AndroidManifestDescriptors.ANDROID_NAME_ATTR);
+ if (attr == null || attr.length() == 0) {
+ attr = elem.getAttributeNS(SdkConstants.NS_RESOURCES,
+ AndroidManifestDescriptors.ANDROID_LABEL_ATTR);
+ }
+ if (attr == null || attr.length() == 0) {
+ attr = elem.getAttributeNS(SdkConstants.NS_RESOURCES,
+ XmlDescriptors.PREF_KEY_ATTR);
+ }
+ if (attr == null || attr.length() == 0) {
+ attr = elem.getAttribute(ResourcesDescriptors.NAME_ATTR);
+ }
+ if (attr == null || attr.length() == 0) {
+ attr = elem.getAttributeNS(SdkConstants.NS_RESOURCES,
+ LayoutDescriptors.ID_ATTR);
+
+ if (attr != null && attr.length() > 0) {
+ for (String prefix : ID_PREFIXES) {
+ if (attr.startsWith(prefix)) {
+ attr = attr.substring(prefix.length());
+ break;
+ }
+ }
+ }
+ }
+ if (attr != null && attr.length() > 0) {
+ return String.format("%1$s (%2$s)", attr, mDescriptor.getUiName());
+ }
+ }
+
+ return String.format("%1$s", mDescriptor.getUiName());
+ }
+
+ /**
+ * Computes a "breadcrumb trail" description for this node.
+ * It will look something like "Manifest > Application > .myactivity (Activity) > Intent-Filter"
+ *
+ * @param include_root Whether to include the root (e.g. "Manifest") or not. Has no effect
+ * when called on the root node itself.
+ * @return The "breadcrumb trail" description for this node.
+ */
+ public String getBreadcrumbTrailDescription(boolean include_root) {
+ StringBuilder sb = new StringBuilder(getShortDescription());
+
+ for (UiElementNode ui_node = getUiParent();
+ ui_node != null;
+ ui_node = ui_node.getUiParent()) {
+ if (!include_root && ui_node.getUiParent() == null) {
+ break;
+ }
+ sb.insert(0, String.format("%1$s > ", ui_node.getShortDescription())); //$NON-NLS-1$
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Sets the XML {@link Document}.
+ * <p/>
+ * The XML {@link Document} is initially null. The XML {@link Document} must be set only on the
+ * UI root element node (this method takes care of that.)
+ */
+ public void setXmlDocument(Document xml_doc) {
+ if (mUiParent == null) {
+ mXmlDocument = xml_doc;
+ } else {
+ mUiParent.setXmlDocument(xml_doc);
+ }
+ }
+
+ /**
+ * Returns the XML {@link Document}.
+ * <p/>
+ * The value is initially null until the UI node is attached to its UI parent -- the value
+ * of the document is then propagated.
+ *
+ * @return the XML {@link Document} or the parent's XML {@link Document} or null.
+ */
+ public Document getXmlDocument() {
+ if (mXmlDocument != null) {
+ return mXmlDocument;
+ } else if (mUiParent != null) {
+ return mUiParent.getXmlDocument();
+ }
+ return null;
+ }
+
+ /**
+ * Returns the XML node associated with this UI node.
+ * <p/>
+ * Some {@link ElementDescriptor} are declared as being "mandatory". This means the
+ * corresponding UI node will exist even if there is no corresponding XML node. Such structure
+ * is created and enforced by the parent of the tree, not the element themselves. However
+ * such nodes will likely not have an XML node associated, so getXmlNode() can return null.
+ *
+ * @return The associated XML node. Can be null for mandatory nodes.
+ */
+ public Node getXmlNode() {
+ return mXmlNode;
+ }
+
+ /**
+ * Returns the {@link ElementDescriptor} for this node. This is never null.
+ * <p/>
+ * Do not use this to call getDescriptor().getAttributes(), instead call
+ * getAttributeDescriptors() which can be overriden by derived classes.
+ */
+ public ElementDescriptor getDescriptor() {
+ return mDescriptor;
+ }
+
+ /**
+ * Returns the {@link AttributeDescriptor} array for the descriptor of this node.
+ * <p/>
+ * Use this instead of getDescriptor().getAttributes() -- derived classes can override
+ * this to manipulate the attribute descriptor list depending on the current UI node.
+ */
+ public AttributeDescriptor[] getAttributeDescriptors() {
+ return mDescriptor.getAttributes();
+ }
+
+ /**
+ * Returns the hidden {@link AttributeDescriptor} array for the descriptor of this node.
+ * This is a subset of the getAttributeDescriptors() list.
+ * <p/>
+ * Use this instead of getDescriptor().getHiddenAttributes() -- potentially derived classes
+ * could override this to manipulate the attribute descriptor list depending on the current
+ * UI node. There's no need for it right now so keep it private.
+ */
+ private Map<String, AttributeDescriptor> getHiddenAttributeDescriptors() {
+ if (mCachedHiddenAttributes == null) {
+ mCachedHiddenAttributes = new HashMap<String, AttributeDescriptor>();
+ for (AttributeDescriptor attr_desc : getAttributeDescriptors()) {
+ if (attr_desc instanceof XmlnsAttributeDescriptor) {
+ mCachedHiddenAttributes.put(
+ ((XmlnsAttributeDescriptor) attr_desc).getXmlNsName(),
+ attr_desc);
+ }
+ }
+ }
+ return mCachedHiddenAttributes;
+ }
+
+ /**
+ * Sets the parent of this UiElementNode.
+ * <p/>
+ * The root node has no parent.
+ */
+ protected void setUiParent(UiElementNode parent) {
+ mUiParent = parent;
+ // Invalidate the internal UiAttributes list, as it may depend on the actual UiParent.
+ clearAttributes();
+ }
+
+ /**
+ * @return The parent {@link UiElementNode} or null if this is the root node.
+ */
+ public UiElementNode getUiParent() {
+ return mUiParent;
+ }
+
+ /**
+ * Returns The root {@link UiElementNode}.
+ */
+ public UiElementNode getUiRoot() {
+ UiElementNode root = this;
+ while (root.mUiParent != null) {
+ root = root.mUiParent;
+ }
+
+ return root;
+ }
+
+ /**
+ * Returns the previous UI sibling of this UI node.
+ * If the node does not have a previous sibling, returns null.
+ */
+ public UiElementNode getUiPreviousSibling() {
+ if (mUiParent != null) {
+ List<UiElementNode> childlist = mUiParent.getUiChildren();
+ if (childlist != null && childlist.size() > 1 && childlist.get(0) != this) {
+ int index = childlist.indexOf(this);
+ return index > 0 ? childlist.get(index - 1) : null;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the next UI sibling of this UI node.
+ * If the node does not have a next sibling, returns null.
+ */
+ public UiElementNode getUiNextSibling() {
+ if (mUiParent != null) {
+ List<UiElementNode> childlist = mUiParent.getUiChildren();
+ if (childlist != null) {
+ int size = childlist.size();
+ if (size > 1 && childlist.get(size - 1) != this) {
+ int index = childlist.indexOf(this);
+ return index >= 0 && index < size - 1 ? childlist.get(index + 1) : null;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Sets the {@link AndroidEditor} handling this {@link UiElementNode} hierarchy.
+ * <p/>
+ * The editor must always be set on the root node. This method takes care of that.
+ */
+ public void setEditor(AndroidEditor editor) {
+ if (mUiParent == null) {
+ mEditor = editor;
+ } else {
+ mUiParent.setEditor(editor);
+ }
+ }
+
+ /**
+ * Returns the {@link AndroidEditor} that embeds this {@link UiElementNode}.
+ * <p/>
+ * The value is initially null until the node is attached to its parent -- the value
+ * of the root node is then propagated.
+ *
+ * @return The embedding {@link AndroidEditor} or null.
+ */
+ public AndroidEditor getEditor() {
+ return mUiParent == null ? mEditor : mUiParent.getEditor();
+ }
+
+ /**
+ * Returns the Android target data for the file being edited.
+ */
+ public AndroidTargetData getAndroidTarget() {
+ return getEditor().getTargetData();
+ }
+
+ /**
+ * @return A read-only version of the children collection.
+ */
+ public List<UiElementNode> getUiChildren() {
+ if (mReadOnlyUiChildren == null) {
+ mReadOnlyUiChildren = Collections.unmodifiableList(mUiChildren);
+ }
+ return mReadOnlyUiChildren;
+ }
+
+ /**
+ * @return A read-only version of the attributes collection.
+ */
+ public Collection<UiAttributeNode> getUiAttributes() {
+ if (mReadOnlyUiAttributes == null) {
+ mReadOnlyUiAttributes = Collections.unmodifiableCollection(
+ getInternalUiAttributes().values());
+ }
+ return mReadOnlyUiAttributes;
+ }
+
+ /**
+ * @return A read-only version of the unknown attributes collection.
+ */
+ public Collection<UiAttributeNode> getUnknownUiAttributes() {
+ return Collections.unmodifiableCollection(mUnknownUiAttributes);
+ }
+
+ /**
+ * Sets the error flag value.
+ * @param errorFlag the error flag
+ */
+ public final void setHasError(boolean errorFlag) {
+ mHasError = errorFlag;
+ }
+
+ /**
+ * Returns whether this node, its attributes, or one of the children nodes (and attributes)
+ * has errors.
+ */
+ public final boolean hasError() {
+ if (mHasError) {
+ return true;
+ }
+
+ // get the error value from the attributes.
+ Collection<UiAttributeNode> attributes = getInternalUiAttributes().values();
+ for (UiAttributeNode attribute : attributes) {
+ if (attribute.hasError()) {
+ return true;
+ }
+ }
+
+ // and now from the children.
+ for (UiElementNode child : mUiChildren) {
+ if (child.hasError()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Adds a new {@link IUiUpdateListener} to the internal update listener list.
+ */
+ public void addUpdateListener(IUiUpdateListener listener) {
+ if (mUiUpdateListeners == null) {
+ mUiUpdateListeners = new ArrayList<IUiUpdateListener>();
+ }
+ if (!mUiUpdateListeners.contains(listener)) {
+ mUiUpdateListeners.add(listener);
+ }
+ }
+
+ /**
+ * Removes an existing {@link IUiUpdateListener} from the internal update listener list.
+ * Does nothing if the list is empty or the listener is not registered.
+ */
+ public void removeUpdateListener(IUiUpdateListener listener) {
+ if (mUiUpdateListeners != null) {
+ mUiUpdateListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Finds a child node relative to this node using a path-like expression.
+ * F.ex. "node1/node2" would find a child "node1" that contains a child "node2" and
+ * returns the latter. If there are multiple nodes with the same name at the same
+ * level, always uses the first one found.
+ *
+ * @param path The path like expression to select a child node.
+ * @return The ui node found or null.
+ */
+ public UiElementNode findUiChildNode(String path) {
+ String[] items = path.split("/"); //$NON-NLS-1$
+ UiElementNode ui_node = this;
+ for (String item : items) {
+ boolean next_segment = false;
+ for (UiElementNode c : ui_node.mUiChildren) {
+ if (c.getDescriptor().getXmlName().equals(item)) {
+ ui_node = c;
+ next_segment = true;
+ break;
+ }
+ }
+ if (!next_segment) {
+ return null;
+ }
+ }
+ return ui_node;
+ }
+
+ /**
+ * Finds an {@link UiElementNode} which contains the give XML {@link Node}.
+ * Looks recursively in all children UI nodes.
+ *
+ * @param xmlNode The XML node to look for.
+ * @return The {@link UiElementNode} that contains xmlNode or null if not found,
+ */
+ public UiElementNode findXmlNode(Node xmlNode) {
+ if (xmlNode == null) {
+ return null;
+ }
+ if (getXmlNode() == xmlNode) {
+ return this;
+ }
+
+ for (UiElementNode uiChild : mUiChildren) {
+ UiElementNode found = uiChild.findXmlNode(xmlNode);
+ if (found != null) {
+ return found;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the {@link UiAttributeNode} matching this attribute descriptor or
+ * null if not found.
+ *
+ * @param attr_desc The {@link AttributeDescriptor} to match.
+ * @return the {@link UiAttributeNode} matching this attribute descriptor or null
+ * if not found.
+ */
+ public UiAttributeNode findUiAttribute(AttributeDescriptor attr_desc) {
+ return getInternalUiAttributes().get(attr_desc);
+ }
+
+ /**
+ * Populate this element node with all values from the given XML node.
+ *
+ * This fails if the given XML node has a different element name -- it won't change the
+ * type of this ui node.
+ *
+ * This method can be both used for populating values the first time and updating values
+ * after the XML model changed.
+ *
+ * @param xml_node The XML node to mirror
+ * @return Returns true if the XML structure has changed (nodes added, removed or replaced)
+ */
+ public boolean loadFromXmlNode(Node xml_node) {
+ boolean structure_changed = (mXmlNode != xml_node);
+ mXmlNode = xml_node;
+ if (xml_node != null) {
+ updateAttributeList(xml_node);
+ structure_changed |= updateElementList(xml_node);
+ invokeUiUpdateListeners(structure_changed ? UiUpdateState.CHILDREN_CHANGED
+ : UiUpdateState.ATTR_UPDATED);
+ }
+ return structure_changed;
+ }
+
+ /**
+ * Clears the UI node and reload it from the given XML node.
+ * <p/>
+ * This works by clearing all references to any previous XML or UI nodes and
+ * then reloads the XML document from scratch. The editor reference is kept.
+ * <p/>
+ * This is used in the special case where the ElementDescriptor structure has changed.
+ * Rather than try to diff inflated UI nodes (as loadFromXmlNode does), we don't bother
+ * and reload everything. This is not subtle and should be used very rarely.
+ *
+ * @param xml_node The XML node or document to reload. Can be null.
+ */
+ public void reloadFromXmlNode(Node xml_node) {
+ // The editor needs to be preserved, it is not affected by an XML change.
+ AndroidEditor editor = getEditor();
+ clearContent();
+ setEditor(editor);
+ if (xml_node != null) {
+ setXmlDocument(xml_node.getOwnerDocument());
+ }
+ // This will reload all the XML and recreate the UI structure from scratch.
+ loadFromXmlNode(xml_node);
+ }
+
+ /**
+ * Called by attributes when they want to commit their value
+ * to an XML node.
+ * <p/>
+ * For mandatory nodes, this makes sure the underlying XML element node
+ * exists in the model. If not, it is created and assigned as the underlying
+ * XML node.
+ * </br>
+ * For non-mandatory nodes, simply return the underlying XML node, which
+ * must always exists.
+ *
+ * @return The XML node matching this {@link UiElementNode} or null.
+ */
+ public Node prepareCommit() {
+ if (getDescriptor().isMandatory()) {
+ createXmlNode();
+ // The new XML node has been created.
+ // We don't need to refresh using loadFromXmlNode() since there are
+ // no attributes or elements that need to be loading into this node.
+ }
+ return getXmlNode();
+ }
+
+ /**
+ * Commits the attributes (all internal, inherited from UI parent & unknown attributes).
+ * This is called by the UI when the embedding part needs to be committed.
+ */
+ public void commit() {
+ for (UiAttributeNode ui_attr : getInternalUiAttributes().values()) {
+ ui_attr.commit();
+ }
+
+ for (UiAttributeNode ui_attr : mUnknownUiAttributes) {
+ ui_attr.commit();
+ }
+ }
+
+ /**
+ * Returns true if the part has been modified with respect to the data
+ * loaded from the model.
+ */
+ public boolean isDirty() {
+ for (UiAttributeNode ui_attr : getInternalUiAttributes().values()) {
+ if (ui_attr.isDirty()) {
+ return true;
+ }
+ }
+
+ for (UiAttributeNode ui_attr : mUnknownUiAttributes) {
+ if (ui_attr.isDirty()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Creates the underlying XML element node for this UI node if it doesn't already
+ * exists.
+ *
+ * @return The new value of getXmlNode() (can be null if creation failed)
+ */
+ public Node createXmlNode() {
+ if (mXmlNode != null) {
+ return null;
+ }
+ Node parentXmlNode = null;
+ if (mUiParent != null) {
+ parentXmlNode = mUiParent.prepareCommit();
+ if (parentXmlNode == null) {
+ // The parent failed to create its own backing XML node. Abort.
+ // No need to throw an exception, the parent will most likely
+ // have done so itself.
+ return null;
+ }
+ }
+
+ String element_name = getDescriptor().getXmlName();
+ Document doc = getXmlDocument();
+
+ // We *must* have a root node. If not, we need to abort.
+ if (doc == null) {
+ throw new RuntimeException(
+ String.format("Missing XML document for %1$s XML node.", element_name));
+ }
+
+ // If we get here and parent_xml_node is null, the node is to be created
+ // as the root node of the document (which can't be null, cf check above).
+ if (parentXmlNode == null) {
+ parentXmlNode = doc;
+ }
+
+ mXmlNode = doc.createElement(element_name);
+
+ Node xmlNextSibling = null;
+
+ UiElementNode uiNextSibling = getUiNextSibling();
+ if (uiNextSibling != null) {
+ xmlNextSibling = uiNextSibling.getXmlNode();
+ }
+
+ parentXmlNode.insertBefore(mXmlNode, xmlNextSibling);
+
+ // Insert a separator after the tag, to make it easier to read
+ Text sep = doc.createTextNode("\n");
+ parentXmlNode.appendChild(sep);
+
+ // Set all initial attributes in the XML node if they are not empty.
+ // Iterate on the descriptor list to get the desired order and then use the
+ // internal values, if any.
+ for (AttributeDescriptor attr_desc : getAttributeDescriptors()) {
+ if (attr_desc instanceof XmlnsAttributeDescriptor) {
+ XmlnsAttributeDescriptor desc = (XmlnsAttributeDescriptor) attr_desc;
+ Attr attr = doc.createAttributeNS(XmlnsAttributeDescriptor.XMLNS_URI,
+ desc.getXmlNsName());
+ attr.setValue(desc.getValue());
+ attr.setPrefix(desc.getXmlNsPrefix());
+ mXmlNode.getAttributes().setNamedItemNS(attr);
+ } else {
+ UiAttributeNode ui_attr = getInternalUiAttributes().get(attr_desc);
+ commitAttributeToXml(ui_attr, ui_attr.getCurrentValue());
+ }
+ }
+
+ invokeUiUpdateListeners(UiUpdateState.CREATED);
+ return mXmlNode;
+ }
+
+ /**
+ * Removes the XML node corresponding to this UI node if it exists
+ * and also removes all mirrored information in this UI node (i.e. children, attributes)
+ *
+ * @return The removed node or null if it didn't exist in the firtst place.
+ */
+ public Node deleteXmlNode() {
+ if (mXmlNode == null) {
+ return null;
+ }
+
+ // First clear the internals of the node and *then* actually deletes the XML
+ // node (because doing so will generate an update even and this node may be
+ // revisited via loadFromXmlNode).
+ Node old_xml_node = mXmlNode;
+ clearContent();
+
+ Node xml_parent = old_xml_node.getParentNode();
+ if (xml_parent == null) {
+ xml_parent = getXmlDocument();
+ }
+ old_xml_node = xml_parent.removeChild(old_xml_node);
+
+ invokeUiUpdateListeners(UiUpdateState.DELETED);
+ return old_xml_node;
+ }
+
+ /**
+ * Updates the element list for this UiElementNode.
+ * At the end, the list of children UiElementNode here will match the one from the
+ * provided XML {@link Node}:
+ * <ul>
+ * <li> Walk both the current ui children list and the xml children list at the same time.
+ * <li> If we have a new xml child but already reached the end of the ui child list, add the
+ * new xml node.
+ * <li> Otherwise, check if the xml node is referenced later in the ui child list and if so,
+ * move it here. It means the XML child list has been reordered.
+ * <li> Otherwise, this is a new XML node that we add in the middle of the ui child list.
+ * <li> At the end, we may have finished walking the xml child list but still have remaining
+ * ui children, simply delete them as they matching trailing xml nodes that have been
+ * removed unless they are mandatory ui nodes.
+ * </ul>
+ * Note that only the first case is used when populating the ui list the first time.
+ *
+ * @param xml_node The XML node to mirror
+ * @return True when the XML structure has changed.
+ */
+ protected boolean updateElementList(Node xml_node) {
+ boolean structure_changed = false;
+ int ui_index = 0;
+ Node xml_child = xml_node.getFirstChild();
+ while (xml_child != null) {
+ if (xml_child.getNodeType() == Node.ELEMENT_NODE) {
+ String element_name = xml_child.getNodeName();
+ UiElementNode ui_node = null;
+ if (mUiChildren.size() <= ui_index) {
+ // A new node is being added at the end of the list
+ ElementDescriptor desc = mDescriptor.findChildrenDescriptor(element_name,
+ false /* recursive */);
+ if (desc == null) {
+ // Unknown node. Create a temporary descriptor for it.
+ // most important we want to auto-add unknown attributes to it.
+ AndroidEditor editor = getEditor();
+ IEditorInput editorInput = editor.getEditorInput();
+ if (editorInput instanceof IFileEditorInput) {
+ IFileEditorInput fileInput = (IFileEditorInput)editorInput;
+ desc = CustomViewDescriptorService.getInstance().getDescriptor(
+ fileInput.getFile().getProject(), element_name);
+ if (desc == null) {
+ desc = new ElementDescriptor(element_name);
+ }
+ } else {
+ desc = new ElementDescriptor(element_name);
+ // TODO associate a new "?" icon to this descriptor.
+ }
+ }
+ structure_changed = true;
+ ui_node = appendNewUiChild(desc);
+ ui_index++;
+ } else {
+ // A new node is being inserted or moved.
+ // Note: mandatory nodes can be created without an XML node in which case
+ // getXmlNode() is null.
+ UiElementNode ui_child;
+ int n = mUiChildren.size();
+ for (int j = ui_index; j < n; j++) {
+ ui_child = mUiChildren.get(j);
+ if (ui_child.getXmlNode() != null && ui_child.getXmlNode() == xml_child) {
+ if (j > ui_index) {
+ // Found the same XML node at some later index, now move it here.
+ mUiChildren.remove(j);
+ mUiChildren.add(ui_index, ui_child);
+ structure_changed = true;
+ }
+ ui_node = ui_child;
+ ui_index++;
+ break;
+ }
+ }
+
+ if (ui_node == null) {
+ // Look for an unused mandatory node with no XML node attached
+ // referencing the same XML element name
+ for (int j = ui_index; j < n; j++) {
+ ui_child = mUiChildren.get(j);
+ if (ui_child.getXmlNode() == null &&
+ ui_child.getDescriptor().isMandatory() &&
+ ui_child.getDescriptor().getXmlName().equals(element_name)) {
+ if (j > ui_index) {
+ // Found it, now move it here
+ mUiChildren.remove(j);
+ mUiChildren.add(ui_index, ui_child);
+ }
+ // assign the XML node to this empty mandatory element.
+ ui_child.mXmlNode = xml_child;
+ structure_changed = true;
+ ui_node = ui_child;
+ ui_index++;
+ }
+ }
+ }
+
+ if (ui_node == null) {
+ // Inserting new node
+ ElementDescriptor desc = mDescriptor.findChildrenDescriptor(element_name,
+ false /* recursive */);
+ if (desc == null) {
+ // Unknown element. Simply ignore it.
+ AdtPlugin.log(IStatus.WARNING,
+ "AndroidManifest: Ignoring unknown '%s' XML element", //$NON-NLS-1$
+ element_name);
+ } else {
+ structure_changed = true;
+ ui_node = insertNewUiChild(ui_index, desc);
+ ui_index++;
+ }
+ }
+ }
+ if (ui_node != null) {
+ // If we touched an UI Node, even an existing one, refresh its content.
+ // For new nodes, this will populate them recursively.
+ structure_changed |= ui_node.loadFromXmlNode(xml_child);
+ }
+ }
+ xml_child = xml_child.getNextSibling();
+ }
+
+ // There might be extra UI nodes at the end if the XML node list got shorter.
+ for (int index = mUiChildren.size() - 1; index >= ui_index; --index) {
+ structure_changed |= removeUiChildAtIndex(index);
+ }
+
+ return structure_changed;
+ }
+
+ /**
+ * Internal helper to remove an UI child node given by its index in the
+ * internal child list.
+ *
+ * Also invokes the update listener on the node to be deleted.
+ *
+ * @param ui_index The index of the UI child to remove, range 0 .. mUiChildren.size()-1
+ * @return True if the structure has changed
+ * @throws IndexOutOfBoundsException if index is out of mUiChildren's bounds. Of course you
+ * know that could never happen unless the computer is on fire or something.
+ */
+ private boolean removeUiChildAtIndex(int ui_index) {
+ UiElementNode ui_node = mUiChildren.get(ui_index);
+ invokeUiUpdateListeners(UiUpdateState.DELETED);
+ if (ui_node.getDescriptor().isMandatory()) {
+ // We can't remove a mandatory node, we just clear its content.
+
+ // A mandatory node with no XML means it doesn't really exist, so it can't be
+ // deleted.
+ boolean xml_exists = (ui_node.getXmlNode() != null);
+
+ ui_node.clearContent();
+ return xml_exists;
+ } else {
+ mUiChildren.remove(ui_index);
+ return true;
+ }
+ }
+
+ /**
+ * Creates a new {@link UiElementNode} from the given {@link ElementDescriptor}
+ * and appends it to the end of the element children list.
+ *
+ * @param descriptor The {@link ElementDescriptor} that knows how to create the UI node.
+ * @return The new UI node that has been appended
+ */
+ public UiElementNode appendNewUiChild(ElementDescriptor descriptor) {
+ UiElementNode ui_node;
+ ui_node = descriptor.createUiNode();
+ mUiChildren.add(ui_node);
+ ui_node.setUiParent(this);
+ ui_node.invokeUiUpdateListeners(UiUpdateState.CREATED);
+ return ui_node;
+ }
+
+ /**
+ * Creates a new {@link UiElementNode} from the given {@link ElementDescriptor}
+ * and inserts it in the element children list at the specified position.
+ *
+ * @param index The position where to insert in the element children list.
+ * @param descriptor The {@link ElementDescriptor} that knows how to create the UI node.
+ * @return The new UI node.
+ */
+ public UiElementNode insertNewUiChild(int index, ElementDescriptor descriptor) {
+ UiElementNode ui_node;
+ ui_node = descriptor.createUiNode();
+ mUiChildren.add(index, ui_node);
+ ui_node.setUiParent(this);
+ ui_node.invokeUiUpdateListeners(UiUpdateState.CREATED);
+ return ui_node;
+ }
+
+ /**
+ * Updates the {@link UiAttributeNode} list for this {@link UiElementNode}.
+ * <p/>
+ * For a given {@link UiElementNode}, the attribute list always exists in
+ * full and is totally independent of whether the XML model actually
+ * has the corresponding attributes.
+ * <p/>
+ * For each attribute declared in this {@link UiElementNode}, get
+ * the corresponding XML attribute. It may not exist, in which case the
+ * value will be null. We don't really know if a value has changed, so
+ * the updateValue() is called on the UI sattribute in all cases.
+ *
+ * @param xmlNode The XML node to mirror
+ */
+ protected void updateAttributeList(Node xmlNode) {
+ NamedNodeMap xmlAttrMap = xmlNode.getAttributes();
+ HashSet<Node> visited = new HashSet<Node>();
+
+ // For all known (i.e. expected) UI attributes, find an existing XML attribute of
+ // same (uri, local name) and update the internal Ui attribute value.
+ for (UiAttributeNode uiAttr : getInternalUiAttributes().values()) {
+ AttributeDescriptor desc = uiAttr.getDescriptor();
+ if (!(desc instanceof SeparatorAttributeDescriptor)) {
+ Node xmlAttr = xmlAttrMap == null ? null :
+ xmlAttrMap.getNamedItemNS(desc.getNamespaceUri(), desc.getXmlLocalName());
+ uiAttr.updateValue(xmlAttr);
+ visited.add(xmlAttr);
+ }
+ }
+
+ // Clone the current list of unknown attributes. We'll then remove from this list when
+ // we still attributes which are still unknown. What will be left are the old unknown
+ // attributes that have been deleted in the current XML attribute list.
+ @SuppressWarnings("unchecked") //$NON-NLS-1$
+ HashSet<UiAttributeNode> deleted = (HashSet<UiAttributeNode>) mUnknownUiAttributes.clone();
+
+ // We need to ignore hidden attributes.
+ Map<String, AttributeDescriptor> hiddenAttrDesc = getHiddenAttributeDescriptors();
+
+ // Traverse the actual XML attribute list to find unknown attributes
+ if (xmlAttrMap != null) {
+ for (int i = 0; i < xmlAttrMap.getLength(); i++) {
+ Node xmlAttr = xmlAttrMap.item(i);
+ // Ignore attributes which have actual descriptors
+ if (visited.contains(xmlAttr)) {
+ continue;
+ }
+
+ String xmlFullName = xmlAttr.getNodeName();
+
+ // Ignore attributes which are hidden (based on the prefix:localName key)
+ if (hiddenAttrDesc.containsKey(xmlFullName)) {
+ continue;
+ }
+
+ String xmlAttrLocalName = xmlAttr.getLocalName();
+ String xmlNsUri = xmlAttr.getNamespaceURI();
+
+ UiAttributeNode uiAttr = null;
+ for (UiAttributeNode a : mUnknownUiAttributes) {
+ String aLocalName = a.getDescriptor().getXmlLocalName();
+ String aNsUri = a.getDescriptor().getNamespaceUri();
+ if (aLocalName.equals(xmlAttrLocalName) &&
+ (aNsUri == xmlNsUri || (aNsUri != null && aNsUri.equals(xmlNsUri)))) {
+ // This attribute is still present in the unknown list
+ uiAttr = a;
+ // It has not been deleted
+ deleted.remove(a);
+ break;
+ }
+ }
+ if (uiAttr == null) {
+ // Create a new unknown attribute
+ TextAttributeDescriptor desc = new TextAttributeDescriptor(
+ xmlAttrLocalName, // xml name
+ xmlFullName, // ui name
+ xmlNsUri, // NS uri
+ "Unknown XML attribute"); // tooltip, translatable
+ uiAttr = desc.createUiNode(this);
+ mUnknownUiAttributes.add(uiAttr);
+ }
+
+ uiAttr.updateValue(xmlAttr);
+ }
+
+ // Remove from the internal list unknown attributes that have been deleted from the xml
+ for (UiAttributeNode a : deleted) {
+ mUnknownUiAttributes.remove(a);
+ }
+ }
+ }
+
+ /**
+ * Invoke all registered {@link IUiUpdateListener} listening on this UI updates for this node.
+ */
+ protected void invokeUiUpdateListeners(UiUpdateState state) {
+ if (mUiUpdateListeners != null) {
+ for (IUiUpdateListener listener : mUiUpdateListeners) {
+ try {
+ listener.uiElementNodeUpdated(this, state);
+ } catch (Exception e) {
+ // prevent a crashing listener from crashing the whole invocation chain
+ AdtPlugin.log(e, "UIElement Listener failed: %s, state=%s", //$NON-NLS-1$
+ getBreadcrumbTrailDescription(true),
+ state.toString());
+ }
+ }
+ }
+ }
+
+ // --- for derived implementations only ---
+
+ // TODO doc
+ protected void setXmlNode(Node xml_node) {
+ mXmlNode = xml_node;
+ }
+
+ /**
+ * Sets the temporary data used by the editors.
+ * @param data the data.
+ */
+ public void setEditData(Object data) {
+ mEditData = data;
+ }
+
+ /**
+ * Returns the temporary data used by the editors for this object.
+ * @return the data, or <code>null</code> if none has been set.
+ */
+ public Object getEditData() {
+ return mEditData;
+ }
+
+ public void refreshUi() {
+ invokeUiUpdateListeners(UiUpdateState.ATTR_UPDATED);
+ }
+
+
+ // ------------- Helpers
+
+ /**
+ * Helper method to commit a single attribute value to XML.
+ * <p/>
+ * This method updates the XML regardless of the current XML value.
+ * Callers should check first if an update is needed.
+ * If the new value is empty, the XML attribute will be actually removed.
+ * <p/>
+ * Note that the caller MUST ensure that modifying the underlying XML model is
+ * safe and must take care of marking the model as dirty if necessary.
+ *
+ * @see AndroidEditor#editXmlModel(Runnable)
+ *
+ * @param uiAttr The attribute node to commit. Must be a child of this UiElementNode.
+ * @param newValue The new value to set.
+ * @return True if the XML attribute was modified or removed, false if nothing changed.
+ */
+ public boolean commitAttributeToXml(UiAttributeNode uiAttr, String newValue) {
+ // Get (or create) the underlying XML element node that contains the attributes.
+ Node element = prepareCommit();
+ if (element != null && uiAttr != null) {
+ String attrLocalName = uiAttr.getDescriptor().getXmlLocalName();
+ String attrNsUri = uiAttr.getDescriptor().getNamespaceUri();
+
+ NamedNodeMap attrMap = element.getAttributes();
+ if (newValue == null || newValue.length() == 0) {
+ // Remove attribute if it's empty
+ if (attrMap.getNamedItemNS(attrNsUri, attrLocalName) != null) {
+ attrMap.removeNamedItemNS(attrNsUri, attrLocalName);
+ return true;
+ }
+ } else {
+ // Add or replace an attribute
+ Document doc = element.getOwnerDocument();
+ if (doc != null) {
+ Attr attr = doc.createAttributeNS(attrNsUri, attrLocalName);
+ attr.setValue(newValue);
+ attr.setPrefix(lookupNamespacePrefix(element, attrNsUri));
+ attrMap.setNamedItemNS(attr);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Helper method to commit all dirty attributes values to XML.
+ * <p/>
+ * This method is useful if {@link #setAttributeValue(String, String, boolean)} has been
+ * called more than once and all the attributes marked as dirty must be commited to the
+ * XML. It calls {@link #commitAttributeToXml(UiAttributeNode, String)} on each dirty
+ * attribute.
+ * <p/>
+ * Note that the caller MUST ensure that modifying the underlying XML model is
+ * safe and must take care of marking the model as dirty if necessary.
+ *
+ * @see AndroidEditor#editXmlModel(Runnable)
+ *
+ * @return True if one or more values were actually modified or removed,
+ * false if nothing changed.
+ */
+ public boolean commitDirtyAttributesToXml() {
+ boolean result = false;
+ HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
+
+ for (Entry<AttributeDescriptor, UiAttributeNode> entry : attributeMap.entrySet()) {
+ UiAttributeNode ui_attr = entry.getValue();
+ if (ui_attr.isDirty()) {
+ result |= commitAttributeToXml(ui_attr, ui_attr.getCurrentValue());
+ ui_attr.setDirty(false);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Returns the namespace prefix matching the requested namespace URI.
+ * If no such declaration is found, returns the default "android" prefix.
+ *
+ * @param node The current node. Must not be null.
+ * @param nsUri The namespace URI of which the prefix is to be found,
+ * e.g. SdkConstants.NS_RESOURCES
+ * @return The first prefix declared or the default "android" prefix.
+ */
+ private String lookupNamespacePrefix(Node node, String nsUri) {
+ // Note: Node.lookupPrefix is not implemented in wst/xml/core NodeImpl.java
+ // The following code emulates this simple call:
+ // String prefix = node.lookupPrefix(SdkConstants.NS_RESOURCES);
+
+ // if the requested URI is null, it denotes an attribute with no namespace.
+ if (nsUri == null) {
+ return null;
+ }
+
+ // per XML specification, the "xmlns" URI is reserved
+ if (XmlnsAttributeDescriptor.XMLNS_URI.equals(nsUri)) {
+ return "xmlns"; //$NON-NLS-1$
+ }
+
+ HashSet<String> visited = new HashSet<String>();
+ Document doc = node == null ? null : node.getOwnerDocument();
+
+ for (; node != null && node.getNodeType() == Node.ELEMENT_NODE;
+ node = node.getParentNode()) {
+ NamedNodeMap attrs = node.getAttributes();
+ for (int n = attrs.getLength() - 1; n >= 0; --n) {
+ Node attr = attrs.item(n);
+ if ("xmlns".equals(attr.getPrefix())) { //$NON-NLS-1$
+ String uri = attr.getNodeValue();
+ String nsPrefix = attr.getLocalName();
+ if (SdkConstants.NS_RESOURCES.equals(uri)) {
+ return nsPrefix;
+ }
+ visited.add(nsPrefix);
+ }
+ }
+ }
+
+ // Use a sensible default prefix if we can't find one.
+ // We need to make sure the prefix is not one that was declared in the scope
+ // visited above.
+ String prefix = SdkConstants.NS_RESOURCES.equals(nsUri) ? "android" : "ns"; //$NON-NLS-1$ //$NON-NLS-2$
+ String base = prefix;
+ for (int i = 1; visited.contains(prefix); i++) {
+ prefix = base + Integer.toString(i);
+ }
+
+ // Also create & define this prefix/URI in the XML document as an attribute in the
+ // first element of the document.
+ if (doc != null) {
+ node = doc.getFirstChild();
+ while (node != null && node.getNodeType() != Node.ELEMENT_NODE) {
+ node = node.getNextSibling();
+ }
+ if (node != null) {
+ Attr attr = doc.createAttributeNS(XmlnsAttributeDescriptor.XMLNS_URI, prefix);
+ attr.setValue(nsUri);
+ attr.setPrefix("xmlns"); //$NON-NLS-1$
+ node.getAttributes().setNamedItemNS(attr);
+ }
+ }
+
+ return prefix;
+ }
+
+ /**
+ * Utility method to internally set the value of a text attribute for the current
+ * UiElementNode.
+ * <p/>
+ * This method is a helper. It silently ignores the errors such as the requested
+ * attribute not being present in the element or attribute not being settable.
+ * It accepts inherited attributes (such as layout).
+ * <p/>
+ * This does not commit to the XML model. It does mark the attribute node as dirty.
+ * This is up to the caller.
+ *
+ * @see #commitAttributeToXml(UiAttributeNode, String)
+ * @see #commitDirtyAttributesToXml()
+ *
+ * @param attrXmlName The XML name of the attribute to modify
+ * @param value The new value for the attribute. If set to null, the attribute is removed.
+ * @param override True if the value must be set even if one already exists.
+ * @return The {@link UiAttributeNode} that has been modified or null.
+ */
+ public UiAttributeNode setAttributeValue(String attrXmlName, String value, boolean override) {
+ HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
+
+ if (value == null) {
+ value = ""; //$NON-NLS-1$ -- this removes an attribute
+ }
+
+ for (Entry<AttributeDescriptor, UiAttributeNode> entry : attributeMap.entrySet()) {
+ AttributeDescriptor ui_desc = entry.getKey();
+ if (ui_desc.getXmlLocalName().equals(attrXmlName)) {
+ UiAttributeNode ui_attr = entry.getValue();
+ // Not all attributes are editable, ignore those which are not
+ if (ui_attr instanceof IUiSettableAttributeNode) {
+ String current = ui_attr.getCurrentValue();
+ // Only update (and mark as dirty) if the attribute did not have any
+ // value or if the value was different.
+ if (override || current == null || !current.equals(value)) {
+ ((IUiSettableAttributeNode) ui_attr).setCurrentValue(value);
+ // mark the attribute as dirty since their internal content
+ // as been modified, but not the underlying XML model
+ ui_attr.setDirty(true);
+ return ui_attr;
+ }
+ }
+ break;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Utility method to retrieve the internal value of an attribute.
+ * <p/>
+ * Note that this retrieves the *field* value if the attribute has some UI, and
+ * not the actual XML value. They may differ if the attribute is dirty.
+ *
+ * @param attrXmlName The XML name of the attribute to modify
+ * @return The current internal value for the attribute or null in case of error.
+ */
+ public String getAttributeValue(String attrXmlName) {
+ HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
+
+ for (Entry<AttributeDescriptor, UiAttributeNode> entry : attributeMap.entrySet()) {
+ AttributeDescriptor ui_desc = entry.getKey();
+ if (ui_desc.getXmlLocalName().equals(attrXmlName)) {
+ UiAttributeNode ui_attr = entry.getValue();
+ return ui_attr.getCurrentValue();
+ }
+ }
+ return null;
+ }
+
+ // ------ IPropertySource methods
+
+ public Object getEditableValue() {
+ return null;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.eclipse.ui.views.properties.IPropertySource#getPropertyDescriptors()
+ *
+ * Returns the property descriptor for this node. Since the descriptors are not linked to the
+ * data, the AttributeDescriptor are used directly.
+ */
+ public IPropertyDescriptor[] getPropertyDescriptors() {
+ List<IPropertyDescriptor> propDescs = new ArrayList<IPropertyDescriptor>();
+
+ // get the standard descriptors
+ HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
+ Set<AttributeDescriptor> keys = attributeMap.keySet();
+
+
+ // we only want the descriptor that do implement the IPropertyDescriptor interface.
+ for (AttributeDescriptor key : keys) {
+ if (key instanceof IPropertyDescriptor) {
+ propDescs.add((IPropertyDescriptor)key);
+ }
+ }
+
+ // now get the descriptor from the unknown attributes
+ for (UiAttributeNode unknownNode : mUnknownUiAttributes) {
+ if (unknownNode.getDescriptor() instanceof IPropertyDescriptor) {
+ propDescs.add((IPropertyDescriptor)unknownNode.getDescriptor());
+ }
+ }
+
+ // TODO cache this maybe, as it's not going to change (except for unknown descriptors)
+ return propDescs.toArray(new IPropertyDescriptor[propDescs.size()]);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.eclipse.ui.views.properties.IPropertySource#getPropertyValue(java.lang.Object)
+ *
+ * Returns the value of a given property. The id is the result of IPropertyDescriptor.getId(),
+ * which return the AttributeDescriptor itself.
+ */
+ public Object getPropertyValue(Object id) {
+ HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
+
+ UiAttributeNode attribute = attributeMap.get(id);
+
+ if (attribute == null) {
+ // look for the id in the unknown attributes.
+ for (UiAttributeNode unknownAttr : mUnknownUiAttributes) {
+ if (id == unknownAttr.getDescriptor()) {
+ return unknownAttr;
+ }
+ }
+ }
+
+ return attribute;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.eclipse.ui.views.properties.IPropertySource#isPropertySet(java.lang.Object)
+ *
+ * Returns whether the property is set. In our case this is if the string is non empty.
+ */
+ public boolean isPropertySet(Object id) {
+ HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
+
+ UiAttributeNode attribute = attributeMap.get(id);
+
+ if (attribute != null) {
+ return attribute.getCurrentValue().length() > 0;
+ }
+
+ // look for the id in the unknown attributes.
+ for (UiAttributeNode unknownAttr : mUnknownUiAttributes) {
+ if (id == unknownAttr.getDescriptor()) {
+ return unknownAttr.getCurrentValue().length() > 0;
+ }
+ }
+
+ return false;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.eclipse.ui.views.properties.IPropertySource#resetPropertyValue(java.lang.Object)
+ *
+ * Reset the property to its default value. For now we simply empty it.
+ */
+ public void resetPropertyValue(Object id) {
+ HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
+
+ UiAttributeNode attribute = attributeMap.get(id);
+ if (attribute != null) {
+ // TODO: reset the value of the attribute
+
+ return;
+ }
+
+ // look for the id in the unknown attributes.
+ for (UiAttributeNode unknownAttr : mUnknownUiAttributes) {
+ if (id == unknownAttr.getDescriptor()) {
+ // TODO: reset the value of the attribute
+
+ return;
+ }
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.eclipse.ui.views.properties.IPropertySource#setPropertyValue(java.lang.Object, java.lang.Object)
+ *
+ * Set the property value. id is the result of IPropertyDescriptor.getId(), which is the
+ * AttributeDescriptor itself. Value should be a String.
+ */
+ public void setPropertyValue(Object id, Object value) {
+ HashMap<AttributeDescriptor, UiAttributeNode> attributeMap = getInternalUiAttributes();
+
+ UiAttributeNode attribute = attributeMap.get(id);
+
+ if (attribute == null) {
+ // look for the id in the unknown attributes.
+ for (UiAttributeNode unknownAttr : mUnknownUiAttributes) {
+ if (id == unknownAttr.getDescriptor()) {
+ attribute = unknownAttr;
+ break;
+ }
+ }
+ }
+
+ if (attribute != null) {
+ final UiAttributeNode fAttribute = attribute;
+
+ // get the current value and compare it to the new value
+ String oldValue = fAttribute.getCurrentValue();
+ final String newValue = (String)value;
+
+ if (oldValue.equals(newValue)) {
+ return;
+ }
+
+ AndroidEditor editor = getEditor();
+ editor.editXmlModel(new Runnable() {
+ public void run() {
+ commitAttributeToXml(fAttribute, newValue);
+ }
+ });
+ }
+ }
+
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiFlagAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiFlagAttributeNode.java
new file mode 100644
index 0000000..ddcf0a0
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiFlagAttributeNode.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.uimodel;
+
+import com.android.ide.eclipse.adt.sdk.AndroidTargetData;
+import com.android.ide.eclipse.editors.AndroidEditor;
+import com.android.ide.eclipse.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.editors.descriptors.FlagAttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.editors.ui.SectionHelper;
+
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.resource.FontDescriptor;
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.swt.SWT;
+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.Font;
+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.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 org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.dialogs.SelectionStatusDialog;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.TableWrapData;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Represents an XML attribute that is defined by a set of flag values,
+ * i.e. enum names separated by pipe (|) characters.
+ *
+ * Note: in Android resources, a "flag" is a list of fixed values where one or
+ * more values can be selected using an "or", e.g. "align='left|top'".
+ * By contrast, an "enum" is a list of fixed values of which only one can be
+ * selected at a given time, e.g. "gravity='right'".
+ * <p/>
+ * This class handles the "flag" case.
+ * The "enum" case is done using {@link UiListAttributeNode}.
+ */
+public class UiFlagAttributeNode extends UiTextAttributeNode {
+
+ public UiFlagAttributeNode(FlagAttributeDescriptor attributeDescriptor,
+ UiElementNode uiParent) {
+ super(attributeDescriptor, uiParent);
+ }
+
+ /* (non-java doc)
+ * Creates a label widget and an associated text field.
+ * <p/>
+ * As most other parts of the android manifest editor, this assumes the
+ * parent uses a table layout with 2 columns.
+ */
+ @Override
+ public void createUiControl(Composite parent, IManagedForm managedForm) {
+ setManagedForm(managedForm);
+ FormToolkit toolkit = managedForm.getToolkit();
+ TextAttributeDescriptor desc = (TextAttributeDescriptor) getDescriptor();
+
+ Label label = toolkit.createLabel(parent, desc.getUiName());
+ label.setLayoutData(new TableWrapData(TableWrapData.LEFT, TableWrapData.MIDDLE));
+ SectionHelper.addControlTooltip(label, DescriptorsUtils.formatTooltip(desc.getTooltip()));
+
+ Composite composite = toolkit.createComposite(parent);
+ composite.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.MIDDLE));
+ GridLayout gl = new GridLayout(2, false);
+ gl.marginHeight = gl.marginWidth = 0;
+ composite.setLayout(gl);
+ // Fixes missing text borders under GTK... also requires adding a 1-pixel margin
+ // for the text field below
+ toolkit.paintBordersFor(composite);
+
+ final Text text = toolkit.createText(composite, getCurrentValue());
+ GridData gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalIndent = 1; // Needed by the fixed composite borders under GTK
+ text.setLayoutData(gd);
+ final Button selectButton = toolkit.createButton(composite, "Select...", SWT.PUSH);
+
+ setTextWidget(text);
+
+ selectButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ super.widgetSelected(e);
+
+ String currentText = getTextWidgetValue();
+
+ String result = showDialog(selectButton.getShell(), currentText);
+
+ if (result != null) {
+ setTextWidgetValue(result);
+ }
+ }
+ });
+ }
+
+ /**
+ * Get the flag names, either from the initial names set in the attribute
+ * or by querying the framework resource parser.
+ */
+ @Override
+ public String[] getPossibleValues() {
+ String attr_name = getDescriptor().getXmlLocalName();
+ String element_name = getUiParent().getDescriptor().getXmlName();
+
+ String[] values = null;
+
+ if (getDescriptor() instanceof FlagAttributeDescriptor &&
+ ((FlagAttributeDescriptor) getDescriptor()).getNames() != null) {
+ // Get enum values from the descriptor
+ values = ((FlagAttributeDescriptor) getDescriptor()).getNames();
+ }
+
+ if (values == null) {
+ // or from the AndroidTargetData
+ UiElementNode uiNode = getUiParent();
+ AndroidEditor editor = uiNode.getEditor();
+ AndroidTargetData data = editor.getTargetData();
+ if (data != null) {
+ values = data.getAttributeValues(element_name, attr_name);
+ }
+ }
+
+ return values;
+ }
+
+ /**
+ * Shows a dialog letting the user choose a set of enum, and returns a string
+ * containing the result.
+ */
+ public String showDialog(Shell shell, String currentValue) {
+ FlagSelectionDialog dlg = new FlagSelectionDialog(
+ shell, currentValue.trim().split("\\s*\\|\\s*")); //$NON-NLS-1$
+ dlg.open();
+ Object[] result = dlg.getResult();
+ if (result != null) {
+ StringBuilder buf = new StringBuilder();
+ for (Object name : result) {
+ if (name instanceof String) {
+ if (buf.length() > 0) {
+ buf.append("|"); //$NON-NLS-1$
+ }
+ buf.append(name);
+ }
+ }
+
+ return buf.toString();
+ }
+
+ return null;
+
+ }
+
+ /**
+ * Displays a list of flag names with checkboxes.
+ */
+ private class FlagSelectionDialog extends SelectionStatusDialog {
+
+ private Set<String> mCurrentSet;
+ private Table mTable;
+
+ public FlagSelectionDialog(Shell parentShell, String[] currentNames) {
+ super(parentShell);
+
+ mCurrentSet = new HashSet<String>();
+ for (String name : currentNames) {
+ if (name.length() > 0) {
+ mCurrentSet.add(name);
+ }
+ }
+
+ int shellStyle = getShellStyle();
+ setShellStyle(shellStyle | SWT.MAX | SWT.RESIZE);
+ }
+
+ @Override
+ protected void computeResult() {
+ if (mTable != null) {
+ ArrayList<String> results = new ArrayList<String>();
+
+ for (TableItem item : mTable.getItems()) {
+ if (item.getChecked()) {
+ results.add((String)item.getData());
+ }
+ }
+
+ setResult(results);
+ }
+ }
+
+ @Override
+ protected Control createDialogArea(Composite parent) {
+ Composite composite= new Composite(parent, SWT.NONE);
+ composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+ composite.setLayout(new GridLayout(1, true));
+ composite.setFont(parent.getFont());
+
+ Label label = new Label(composite, SWT.NONE);
+ label.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
+ label.setText(String.format("Select the flag values for attribute %1$s:",
+ ((FlagAttributeDescriptor) getDescriptor()).getUiName()));
+
+ mTable = new Table(composite, SWT.CHECK | SWT.BORDER);
+ GridData data = new GridData();
+ // The 60,18 hints are the ones used by AbstractElementListSelectionDialog
+ data.widthHint = convertWidthInCharsToPixels(60);
+ data.heightHint = convertHeightInCharsToPixels(18);
+ data.grabExcessVerticalSpace = true;
+ data.grabExcessHorizontalSpace = true;
+ data.horizontalAlignment = GridData.FILL;
+ data.verticalAlignment = GridData.FILL;
+ mTable.setLayoutData(data);
+
+ mTable.setHeaderVisible(false);
+ final TableColumn column = new TableColumn(mTable, SWT.NONE);
+
+ // List all the expected flag names and check those which are currently used
+ String[] names = getPossibleValues();
+ if (names != null) {
+ for (String name : names) {
+ TableItem item = new TableItem(mTable, SWT.NONE);
+ item.setText(name);
+ item.setData(name);
+
+ boolean hasName = mCurrentSet.contains(name);
+ item.setChecked(hasName);
+ if (hasName) {
+ mCurrentSet.remove(name);
+ }
+ }
+ }
+
+ // If there are unknown flag names currently used, display them at the end if the
+ // table already checked.
+ if (!mCurrentSet.isEmpty()) {
+ FontDescriptor fontDesc = JFaceResources.getDialogFontDescriptor();
+ fontDesc = fontDesc.withStyle(SWT.ITALIC);
+ Font font = fontDesc.createFont(JFaceResources.getDialogFont().getDevice());
+
+ for (String name : mCurrentSet) {
+ TableItem item = new TableItem(mTable, SWT.NONE);
+ item.setText(String.format("%1$s (unknown flag)", name));
+ item.setData(name);
+ item.setChecked(true);
+ item.setFont(font);
+ }
+ }
+
+ // Add a listener that will resize the column to the full width of the table
+ // so that only one column appears in the table even if the dialog is resized.
+ ControlAdapter listener = new ControlAdapter() {
+ @Override
+ public void controlResized(ControlEvent e) {
+ Rectangle r = mTable.getClientArea();
+ column.setWidth(r.width);
+ }
+ };
+
+ mTable.addControlListener(listener);
+ listener.controlResized(null /* event not used */);
+
+ // Add a selection listener that will check/uncheck items when they are double-clicked
+ mTable.addSelectionListener(new SelectionAdapter() {
+ /** 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());
+ }
+ super.widgetDefaultSelected(e);
+ }
+ });
+
+ Dialog.applyDialogFont(composite);
+ setHelpAvailable(false);
+
+ return composite;
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiListAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiListAttributeNode.java
new file mode 100644
index 0000000..c5c10aa
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiListAttributeNode.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.uimodel;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.sdk.AndroidTargetData;
+import com.android.ide.eclipse.editors.AndroidEditor;
+import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.editors.descriptors.ListAttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.XmlnsAttributeDescriptor;
+import com.android.ide.eclipse.editors.ui.SectionHelper;
+import com.android.sdklib.SdkConstants;
+
+import org.eclipse.core.runtime.IStatus;
+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.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.TableWrapData;
+
+/**
+ * Represents an XML attribute which has possible built-in values, and can be modified by
+ * an editable Combo box.
+ * <p/>
+ * See {@link UiTextAttributeNode} for more information.
+ */
+public class UiListAttributeNode extends UiAbstractTextAttributeNode {
+
+ protected Combo mCombo;
+
+ public UiListAttributeNode(ListAttributeDescriptor attributeDescriptor,
+ UiElementNode uiParent) {
+ super(attributeDescriptor, uiParent);
+ }
+
+ /* (non-java doc)
+ * Creates a label widget and an associated text field.
+ * <p/>
+ * As most other parts of the android manifest editor, this assumes the
+ * parent uses a table layout with 2 columns.
+ */
+ @Override
+ public final void createUiControl(final Composite parent, IManagedForm managedForm) {
+ FormToolkit toolkit = managedForm.getToolkit();
+ TextAttributeDescriptor desc = (TextAttributeDescriptor) getDescriptor();
+
+ Label label = toolkit.createLabel(parent, desc.getUiName());
+ label.setLayoutData(new TableWrapData(TableWrapData.LEFT, TableWrapData.MIDDLE));
+ SectionHelper.addControlTooltip(label, DescriptorsUtils.formatTooltip(desc.getTooltip()));
+
+ int style = SWT.DROP_DOWN;
+ mCombo = new Combo(parent, style);
+ TableWrapData twd = new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.MIDDLE);
+ twd.maxWidth = 100;
+ mCombo.setLayoutData(twd);
+
+ fillCombo();
+
+ setTextWidgetValue(getCurrentValue());
+
+ mCombo.addModifyListener(new ModifyListener() {
+ /**
+ * Sent when the text is modified, whether by the user via manual
+ * input or programmatic input via setText().
+ * <p/>
+ * Simply mark the attribute as dirty if it really changed.
+ * The container SectionPart will collect these flag and manage them.
+ */
+ public void modifyText(ModifyEvent e) {
+ if (!isInInternalTextModification() &&
+ !isDirty() &&
+ mCombo != null &&
+ getCurrentValue() != null &&
+ !mCombo.getText().equals(getCurrentValue())) {
+ setDirty(true);
+ }
+ }
+ });
+
+ // Remove self-reference when the widget is disposed
+ mCombo.addDisposeListener(new DisposeListener() {
+ public void widgetDisposed(DisposeEvent e) {
+ mCombo = null;
+ }
+ });
+ }
+
+ protected void fillCombo() {
+ String[] values = getPossibleValues();
+
+ if (values == null) {
+ AdtPlugin.log(IStatus.ERROR,
+ "FrameworkResourceManager did not provide values yet for %1$s",
+ getDescriptor().getXmlLocalName());
+ } else {
+ for (String value : values) {
+ mCombo.add(value);
+ }
+ }
+ }
+
+ /**
+ * Get the list values, either from the initial values set in the attribute
+ * or by querying the framework resource parser.
+ */
+ @Override
+ public String[] getPossibleValues() {
+ AttributeDescriptor descriptor = getDescriptor();
+ UiElementNode uiParent = getUiParent();
+
+ String attr_name = descriptor.getXmlLocalName();
+ String element_name = uiParent.getDescriptor().getXmlName();
+
+ // FrameworkResourceManager expects a specific prefix for the attribute.
+ String prefix = "";
+ if (SdkConstants.NS_RESOURCES.equals(descriptor.getNamespaceUri())) {
+ prefix = "android:"; //$NON-NLS-1$
+ } else if (XmlnsAttributeDescriptor.XMLNS_URI.equals(descriptor.getNamespaceUri())) {
+ prefix = "xmlns:"; //$NON-NLS-1$
+ }
+ attr_name = prefix + attr_name;
+
+ String[] values = null;
+
+ if (descriptor instanceof ListAttributeDescriptor &&
+ ((ListAttributeDescriptor) descriptor).getValues() != null) {
+ // Get enum values from the descriptor
+ values = ((ListAttributeDescriptor) descriptor).getValues();
+ }
+
+ if (values == null) {
+ // or from the AndroidTargetData
+ UiElementNode uiNode = getUiParent();
+ AndroidEditor editor = uiNode.getEditor();
+ AndroidTargetData data = editor.getTargetData();
+ if (data != null) {
+ // get the great-grand-parent descriptor.
+
+ // the parent should always exist.
+ UiElementNode grandParentNode = uiParent.getUiParent();
+
+ String greatGrandParentNodeName = null;
+ if (grandParentNode != null) {
+ UiElementNode greatGrandParentNode = grandParentNode.getUiParent();
+ if (greatGrandParentNode != null) {
+ greatGrandParentNodeName =
+ greatGrandParentNode.getDescriptor().getXmlName();
+ }
+ }
+
+ values = data.getAttributeValues(element_name, attr_name, greatGrandParentNodeName);
+ }
+ }
+
+ return values;
+ }
+
+ @Override
+ public String getTextWidgetValue() {
+ if (mCombo != null) {
+ return mCombo.getText();
+ }
+
+ return null;
+ }
+
+ @Override
+ public final boolean isValid() {
+ return mCombo != null;
+ }
+
+ @Override
+ public void setTextWidgetValue(String value) {
+ if (mCombo != null) {
+ mCombo.setText(value);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiResourceAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiResourceAttributeNode.java
new file mode 100644
index 0000000..1c1e1bd
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiResourceAttributeNode.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.uimodel;
+
+import com.android.ide.eclipse.adt.sdk.AndroidTargetData;
+import com.android.ide.eclipse.common.resources.IResourceRepository;
+import com.android.ide.eclipse.common.resources.ResourceType;
+import com.android.ide.eclipse.editors.AndroidEditor;
+import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.editors.resources.manager.ResourceManager;
+import com.android.ide.eclipse.editors.ui.SectionHelper;
+import com.android.ide.eclipse.editors.wizards.ReferenceChooserDialog;
+import com.android.ide.eclipse.editors.wizards.ResourceChooser;
+
+import org.eclipse.core.resources.IProject;
+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.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.TableWrapData;
+
+/**
+ * Represents an XML attribute for a resource that can be modified using a simple text field or
+ * a dialog to choose an existing resource.
+ * <p/>
+ * It can be configured to represent any kind of resource, by providing the desired
+ * {@link ResourceType} in the constructor.
+ * <p/>
+ * See {@link UiTextAttributeNode} for more information.
+ */
+public class UiResourceAttributeNode extends UiTextAttributeNode {
+
+ private ResourceType mType;
+
+ public UiResourceAttributeNode(ResourceType type,
+ AttributeDescriptor attributeDescriptor, UiElementNode uiParent) {
+ super(attributeDescriptor, uiParent);
+
+ mType = type;
+ }
+
+ /* (non-java doc)
+ * Creates a label widget and an associated text field.
+ * <p/>
+ * As most other parts of the android manifest editor, this assumes the
+ * parent uses a table layout with 2 columns.
+ */
+ @Override
+ public void createUiControl(final Composite parent, IManagedForm managedForm) {
+ setManagedForm(managedForm);
+ FormToolkit toolkit = managedForm.getToolkit();
+ TextAttributeDescriptor desc = (TextAttributeDescriptor) getDescriptor();
+
+ Label label = toolkit.createLabel(parent, desc.getUiName());
+ label.setLayoutData(new TableWrapData(TableWrapData.LEFT, TableWrapData.MIDDLE));
+ SectionHelper.addControlTooltip(label, DescriptorsUtils.formatTooltip(desc.getTooltip()));
+
+ Composite composite = toolkit.createComposite(parent);
+ composite.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.MIDDLE));
+ GridLayout gl = new GridLayout(2, false);
+ gl.marginHeight = gl.marginWidth = 0;
+ composite.setLayout(gl);
+ // Fixes missing text borders under GTK... also requires adding a 1-pixel margin
+ // for the text field below
+ toolkit.paintBordersFor(composite);
+
+ final Text text = toolkit.createText(composite, getCurrentValue());
+ GridData gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalIndent = 1; // Needed by the fixed composite borders under GTK
+ text.setLayoutData(gd);
+ Button browseButton = toolkit.createButton(composite, "Browse...", SWT.PUSH);
+
+ setTextWidget(text);
+
+ // TODO Add a validator using onAddModifyListener
+
+ browseButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ String result = showDialog(parent.getShell(), text.getText().trim());
+ if (result != null) {
+ text.setText(result);
+ }
+ }
+ });
+ }
+
+ /**
+ * Shows a dialog letting the user choose a set of enum, and returns a string
+ * containing the result.
+ */
+ public String showDialog(Shell shell, String currentValue) {
+ // we need to get the project of the file being edited.
+ UiElementNode uiNode = getUiParent();
+ AndroidEditor editor = uiNode.getEditor();
+ IProject project = editor.getProject();
+ if (project != null) {
+ // get the resource repository for this project and the system resources.
+ IResourceRepository projectRepository =
+ ResourceManager.getInstance().getProjectResources(project);
+
+ if (mType != null) {
+ // get the Target Data to get the system resources
+ AndroidTargetData data = editor.getTargetData();
+ IResourceRepository systemRepository = data.getSystemResources();
+
+ // open a resource chooser dialog for specified resource type.
+ ResourceChooser dlg = new ResourceChooser(mType,
+ projectRepository, systemRepository, shell);
+
+ dlg.setCurrentResource(currentValue);
+
+ if (dlg.open() == Window.OK) {
+ return dlg.getCurrentResource();
+ }
+ } else {
+ ReferenceChooserDialog dlg = new ReferenceChooserDialog(projectRepository,
+ shell);
+
+ dlg.setCurrentResource(currentValue);
+
+ if (dlg.open() == Window.OK) {
+ return dlg.getCurrentResource();
+ }
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public String[] getPossibleValues() {
+ // TODO: compute a list of existing resources for content assist completion
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiSeparatorAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiSeparatorAttributeNode.java
new file mode 100644
index 0000000..192f752
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiSeparatorAttributeNode.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.uimodel;
+
+import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.SeparatorAttributeDescriptor;
+
+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.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.TableWrapData;
+import org.eclipse.ui.forms.widgets.TableWrapLayout;
+import org.w3c.dom.Node;
+
+/**
+ * {@link UiSeparatorAttributeNode} does not represent any real attribute.
+ * <p/>
+ * It is used to separate groups of attributes visually.
+ */
+public class UiSeparatorAttributeNode extends UiAttributeNode {
+
+ /** Creates a new {@link UiAttributeNode} linked to a specific {@link AttributeDescriptor} */
+ public UiSeparatorAttributeNode(SeparatorAttributeDescriptor attrDesc,
+ UiElementNode uiParent) {
+ super(attrDesc, uiParent);
+ }
+
+ /** Returns the current value of the node. */
+ @Override
+ public String getCurrentValue() {
+ // There is no value here.
+ return null;
+ }
+
+ /**
+ * Sets whether the attribute is dirty and also notifies the editor some part's dirty
+ * flag as changed.
+ * <p/>
+ * Subclasses should set the to true as a result of user interaction with the widgets in
+ * the section and then should set to false when the commit() method completed.
+ */
+ @Override
+ public void setDirty(boolean isDirty) {
+ // This is never dirty.
+ }
+
+ /**
+ * Called once by the parent user interface to creates the necessary
+ * user interface to edit this attribute.
+ * <p/>
+ * This method can be called more than once in the life cycle of an UI node,
+ * typically when the UI is part of a master-detail tree, as pages are swapped.
+ *
+ * @param parent The composite where to create the user interface.
+ * @param managedForm The managed form owning this part.
+ */
+ @Override
+ public void createUiControl(Composite parent, IManagedForm managedForm) {
+ FormToolkit toolkit = managedForm.getToolkit();
+ Composite row = toolkit.createComposite(parent);
+
+ TableWrapData twd = new TableWrapData(TableWrapData.FILL_GRAB);
+ if (parent.getLayout() instanceof TableWrapLayout) {
+ twd.colspan = ((TableWrapLayout) parent.getLayout()).numColumns;
+ }
+ row.setLayoutData(twd);
+ row.setLayout(new GridLayout(3, false /* equal width */));
+
+ Label sep = toolkit.createSeparator(row, SWT.HORIZONTAL);
+ GridData gd = new GridData(SWT.LEFT, SWT.CENTER, false, false);
+ gd.widthHint = 16;
+ sep.setLayoutData(gd);
+
+ Label label = toolkit.createLabel(row, getDescriptor().getXmlLocalName());
+ label.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false));
+
+ sep = toolkit.createSeparator(row, SWT.HORIZONTAL);
+ sep.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
+ }
+
+ /** No completion values for this UI attribute. */
+ @Override
+ public String[] getPossibleValues() {
+ return null;
+ }
+
+ /**
+ * Called when the XML is being loaded or has changed to
+ * update the value held by this user interface attribute node.
+ * <p/>
+ * The XML Node <em>may</em> be null, which denotes that the attribute is not
+ * specified in the XML model. In general, this means the "default" value of the
+ * attribute should be used.
+ * <p/>
+ * The caller doesn't really know if attributes have changed,
+ * so it will call this to refresh the attribute anyway. It's up to the
+ * UI implementation to minimize refreshes.
+ *
+ * @param xml_attribute_node
+ */
+ @Override
+ public void updateValue(Node xml_attribute_node) {
+ // No value to update.
+ }
+
+ /**
+ * Called by the user interface when the editor is saved or its state changed
+ * and the modified attributes must be committed (i.e. written) to the XML model.
+ * <p/>
+ * Important behaviors:
+ * <ul>
+ * <li>The caller *must* have called IStructuredModel.aboutToChangeModel before.
+ * The implemented methods must assume it is safe to modify the XML model.
+ * <li>On success, the implementation *must* call setDirty(false).
+ * <li>On failure, the implementation can fail with an exception, which
+ * is trapped and logged by the caller, or do nothing, whichever is more
+ * appropriate.
+ * </ul>
+ */
+ @Override
+ public void commit() {
+ // No value to commit.
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiTextAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiTextAttributeNode.java
new file mode 100644
index 0000000..4c53f4c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiTextAttributeNode.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.uimodel;
+
+import com.android.ide.eclipse.editors.AndroidEditor;
+import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.editors.ui.SectionHelper;
+
+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.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.widgets.TableWrapData;
+
+/**
+ * Represents an XML attribute in that can be modified using a simple text field
+ * in the XML editor's user interface.
+ * <p/>
+ * The XML attribute has no default value. When unset, the text field is blank.
+ * When updating the XML, if the field is empty, the attribute will be removed
+ * from the XML element.
+ * <p/>
+ * See {@link UiAttributeNode} for more information.
+ */
+public class UiTextAttributeNode extends UiAbstractTextAttributeNode {
+
+ /** Text field */
+ private Text mText;
+ /** The managed form, set only once createUiControl has been called. */
+ private IManagedForm mManagedForm;
+
+ public UiTextAttributeNode(AttributeDescriptor attributeDescriptor, UiElementNode uiParent) {
+ super(attributeDescriptor, uiParent);
+ }
+
+ /* (non-java doc)
+ * Creates a label widget and an associated text field.
+ * <p/>
+ * As most other parts of the android manifest editor, this assumes the
+ * parent uses a table layout with 2 columns.
+ */
+ @Override
+ public void createUiControl(Composite parent, IManagedForm managedForm) {
+ setManagedForm(managedForm);
+ TextAttributeDescriptor desc = (TextAttributeDescriptor) getDescriptor();
+ Text text = SectionHelper.createLabelAndText(parent, managedForm.getToolkit(),
+ desc.getUiName(), getCurrentValue(),
+ DescriptorsUtils.formatTooltip(desc.getTooltip()));
+
+ setTextWidget(text);
+ }
+
+ /** No completion values for this UI attribute. */
+ @Override
+ public String[] getPossibleValues() {
+ return null;
+ }
+
+ /**
+ * Sets the internal managed form.
+ * This is usually set by createUiControl.
+ */
+ protected void setManagedForm(IManagedForm managedForm) {
+ mManagedForm = managedForm;
+ }
+
+ /**
+ * @return The managed form, set only once createUiControl has been called.
+ */
+ protected IManagedForm getManagedForm() {
+ return mManagedForm;
+ }
+
+ /* (non-java doc)
+ * Returns if the attribute node is valid, and its UI has been created.
+ */
+ @Override
+ public boolean isValid() {
+ return mText != null;
+ }
+
+ @Override
+ public String getTextWidgetValue() {
+ if (mText != null) {
+ return mText.getText();
+ }
+
+ return null;
+ }
+
+ @Override
+ public void setTextWidgetValue(String value) {
+ if (mText != null) {
+ mText.setText(value);
+ }
+ }
+
+ /**
+ * Sets the Text widget object, and prepares it to handle modification and synchronization
+ * with the XML node.
+ * @param textWidget
+ */
+ protected final void setTextWidget(Text textWidget) {
+ mText = textWidget;
+
+ if (textWidget != null) {
+ // Sets the with hint for the text field. Derived classes can always override it.
+ // This helps the grid layout to resize correctly on smaller screen sizes.
+ Object data = textWidget.getLayoutData();
+ if (data == null) {
+ } else if (data instanceof GridData) {
+ ((GridData)data).widthHint = AndroidEditor.TEXT_WIDTH_HINT;
+ } else if (data instanceof TableWrapData) {
+ ((TableWrapData)data).maxWidth = 100;
+ }
+
+ mText.addModifyListener(new ModifyListener() {
+ /**
+ * Sent when the text is modified, whether by the user via manual
+ * input or programmatic input via setText().
+ * <p/>
+ * Simply mark the attribute as dirty if it really changed.
+ * The container SectionPart will collect these flag and manage them.
+ */
+ public void modifyText(ModifyEvent e) {
+ if (!isInInternalTextModification() &&
+ !isDirty() &&
+ mText != null &&
+ getCurrentValue() != null &&
+ !mText.getText().equals(getCurrentValue())) {
+ setDirty(true);
+ }
+ }
+ });
+
+ // Remove self-reference when the widget is disposed
+ mText.addDisposeListener(new DisposeListener() {
+ public void widgetDisposed(DisposeEvent e) {
+ mText = null;
+ }
+ });
+ }
+
+ onAddValidators(mText);
+ }
+
+ /**
+ * Called after the text widget as been created.
+ * <p/>
+ * Derived classes typically want to:
+ * <li> Create a new {@link ModifyListener} and attach it to the given {@link Text} widget.
+ * <li> In the modify listener, call getManagedForm().getMessageManager().addMessage()
+ * and getManagedForm().getMessageManager().removeMessage() as necessary.
+ * <li> Call removeMessage in a new text.addDisposeListener.
+ * <li> Call the validator once to setup the initial messages as needed.
+ * <p/>
+ * The base implementation does nothing.
+ *
+ * @param text The {@link Text} widget to validate.
+ */
+ protected void onAddValidators(Text text) {
+ }
+
+ /**
+ * Returns the text widget.
+ */
+ protected final Text getTextWidget() {
+ return mText;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiTextValueNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiTextValueNode.java
new file mode 100644
index 0000000..5c1db05
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/uimodel/UiTextValueNode.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.uimodel;
+
+import com.android.ide.eclipse.editors.descriptors.TextValueDescriptor;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.Text;
+
+/**
+ * Represents an XML element value in that can be modified using a simple text field
+ * in the XML editor's user interface.
+ */
+public class UiTextValueNode extends UiTextAttributeNode {
+
+ public UiTextValueNode(TextValueDescriptor attributeDescriptor, UiElementNode uiParent) {
+ super(attributeDescriptor, uiParent);
+ }
+
+ /**
+ * Updates the current text field's value when the XML has changed.
+ * <p/>
+ * The caller doesn't really know if value of the element has changed,
+ * so it will call this to refresh the value anyway. The value
+ * is only set if it has changed.
+ * <p/>
+ * This also resets the "dirty" flag.
+ */
+ @Override
+ public void updateValue(Node xml_attribute_node) {
+ setCurrentValue(DEFAULT_VALUE);
+
+ // The argument xml_attribute_node is not used here. It should always be
+ // null since this is not an attribute. What we want is the "text value" of
+ // the parent element, which is actually the first text node of the element.
+
+ UiElementNode parent = getUiParent();
+ if (parent != null) {
+ Node xml_node = parent.getXmlNode();
+ if (xml_node != null) {
+ for (Node xml_child = xml_node.getFirstChild();
+ xml_child != null;
+ xml_child = xml_child.getNextSibling()) {
+ if (xml_child.getNodeType() == Node.TEXT_NODE) {
+ setCurrentValue(xml_child.getNodeValue());
+ break;
+ }
+ }
+ }
+ }
+
+ if (isValid() && !getTextWidgetValue().equals(getCurrentValue())) {
+ try {
+ setInInternalTextModification(true);
+ setTextWidgetValue(getCurrentValue());
+ setDirty(false);
+ } finally {
+ setInInternalTextModification(false);
+ }
+ }
+ }
+
+ /* (non-java doc)
+ * Called by the user interface when the editor is saved or its state changed
+ * and the modified "attributes" must be committed (i.e. written) to the XML model.
+ */
+ @Override
+ public void commit() {
+ UiElementNode parent = getUiParent();
+ if (parent != null && isValid() && isDirty()) {
+ // Get (or create) the underlying XML element node that contains the value.
+ Node element = parent.prepareCommit();
+ if (element != null) {
+ String value = getTextWidgetValue();
+
+ // Try to find an existing text child to update.
+ boolean updated = false;
+
+ for (Node xml_child = element.getFirstChild();
+ xml_child != null;
+ xml_child = xml_child.getNextSibling()) {
+ if (xml_child.getNodeType() == Node.TEXT_NODE) {
+ xml_child.setNodeValue(value);
+ updated = true;
+ break;
+ }
+ }
+
+ // If we didn't find a text child to update, we need to create one.
+ if (!updated) {
+ Document doc = element.getOwnerDocument();
+ if (doc != null) {
+ Text text = doc.createTextNode(value);
+ element.appendChild(text);
+ }
+ }
+
+ setCurrentValue(value);
+ }
+ }
+ setDirty(false);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ConfigurationSelector.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ConfigurationSelector.java
new file mode 100644
index 0000000..4a05b1e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ConfigurationSelector.java
@@ -0,0 +1,1278 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.wizards;
+
+import com.android.ide.eclipse.editors.resources.configurations.CountryCodeQualifier;
+import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration;
+import com.android.ide.eclipse.editors.resources.configurations.KeyboardStateQualifier;
+import com.android.ide.eclipse.editors.resources.configurations.LanguageQualifier;
+import com.android.ide.eclipse.editors.resources.configurations.NavigationMethodQualifier;
+import com.android.ide.eclipse.editors.resources.configurations.NetworkCodeQualifier;
+import com.android.ide.eclipse.editors.resources.configurations.PixelDensityQualifier;
+import com.android.ide.eclipse.editors.resources.configurations.RegionQualifier;
+import com.android.ide.eclipse.editors.resources.configurations.ResourceQualifier;
+import com.android.ide.eclipse.editors.resources.configurations.ScreenDimensionQualifier;
+import com.android.ide.eclipse.editors.resources.configurations.ScreenOrientationQualifier;
+import com.android.ide.eclipse.editors.resources.configurations.TextInputMethodQualifier;
+import com.android.ide.eclipse.editors.resources.configurations.TouchScreenQualifier;
+import com.android.ide.eclipse.editors.resources.configurations.KeyboardStateQualifier.KeyboardState;
+import com.android.ide.eclipse.editors.resources.configurations.NavigationMethodQualifier.NavigationMethod;
+import com.android.ide.eclipse.editors.resources.configurations.ScreenOrientationQualifier.ScreenOrientation;
+import com.android.ide.eclipse.editors.resources.configurations.TextInputMethodQualifier.TextInputMethod;
+import com.android.ide.eclipse.editors.resources.configurations.TouchScreenQualifier.TouchScreenType;
+import com.android.ide.eclipse.editors.resources.manager.ResourceManager;
+
+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.StructuredSelection;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.StackLayout;
+import org.eclipse.swt.events.ControlAdapter;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.FocusAdapter;
+import org.eclipse.swt.events.FocusEvent;
+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.events.VerifyEvent;
+import org.eclipse.swt.events.VerifyListener;
+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.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.Text;
+
+import java.util.HashMap;
+
+/**
+ * Custom UI widget to let user build a Folder configuration.
+ * <p/>
+ * To use this, instantiate somewhere in the UI and then:
+ * <ul>
+ * <li>Use {@link #setConfiguration(String)} or {@link #setConfiguration(FolderConfiguration)}.
+ * <li>Retrieve the configuration using {@link #getConfiguration(FolderConfiguration)}.
+ * </ul>
+ */
+public class ConfigurationSelector extends Composite {
+
+ public static final int WIDTH_HINT = 600;
+ public static final int HEIGHT_HINT = 250;
+
+ private Runnable mOnChangeListener;
+
+ private TableViewer mFullTableViewer;
+ private TableViewer mSelectionTableViewer;
+ private Button mAddButton;
+ private Button mRemoveButton;
+ private StackLayout mStackLayout;
+
+ private boolean mOnRefresh = false;
+
+ private final FolderConfiguration mBaseConfiguration = new FolderConfiguration();
+ private final FolderConfiguration mSelectedConfiguration = new FolderConfiguration();
+
+ private final HashMap<Class<? extends ResourceQualifier>, QualifierEditBase> mUiMap =
+ new HashMap<Class<? extends ResourceQualifier>, QualifierEditBase>();
+ private Composite mQualifierEditParent;
+
+ /**
+ * Basic of {@link VerifyListener} to only accept digits.
+ */
+ private static class DigitVerifier implements VerifyListener {
+ public void verifyText(VerifyEvent e) {
+ // check for digit only.
+ for (int i = 0 ; i < e.text.length(); i++) {
+ char letter = e.text.charAt(i);
+ if (letter < '0' || letter > '9') {
+ e.doit = false;
+ return;
+ }
+ }
+ }
+ }
+
+ /**
+ * Implementation of {@link VerifyListener} for Country Code qualifiers.
+ */
+ public static class MobileCodeVerifier extends DigitVerifier {
+ @Override
+ public void verifyText(VerifyEvent e) {
+ super.verifyText(e);
+
+ // basic tests passed?
+ if (e.doit) {
+ // check the max 3 digits.
+ if (e.text.length() - e.end + e.start +
+ ((Text)e.getSource()).getText().length() > 3) {
+ e.doit = false;
+ }
+ }
+ }
+ }
+
+ /**
+ * Implementation of {@link VerifyListener} for the Language and Region qualifiers.
+ */
+ public static class LanguageRegionVerifier implements VerifyListener {
+ public void verifyText(VerifyEvent e) {
+ // check for length
+ if (e.text.length() - e.end + e.start + ((Combo)e.getSource()).getText().length() > 2) {
+ e.doit = false;
+ return;
+ }
+
+ // check for lower case only.
+ for (int i = 0 ; i < e.text.length(); i++) {
+ char letter = e.text.charAt(i);
+ if ((letter < 'a' || letter > 'z') && (letter < 'A' || letter > 'Z')) {
+ e.doit = false;
+ return;
+ }
+ }
+ }
+ }
+
+ /**
+ * Implementation of {@link VerifyListener} for the Pixel Density qualifier.
+ */
+ public static class DensityVerifier extends DigitVerifier { }
+
+ /**
+ * Implementation of {@link VerifyListener} for the Screen Dimension qualifier.
+ */
+ public static class DimensionVerifier extends DigitVerifier { }
+
+ /**
+ * Enum for the state of the configuration being created.
+ */
+ public enum ConfigurationState {
+ OK, INVALID_CONFIG, REGION_WITHOUT_LANGUAGE;
+ }
+
+ public ConfigurationSelector(Composite parent) {
+ super(parent, SWT.NONE);
+
+ mBaseConfiguration.createDefault();
+
+ GridLayout gl = new GridLayout(4, false);
+ gl.marginWidth = gl.marginHeight = 0;
+ setLayout(gl);
+
+ // first column is the first table
+ final Table fullTable = new Table(this, SWT.SINGLE | SWT.FULL_SELECTION | SWT.BORDER);
+ fullTable.setLayoutData(new GridData(GridData.FILL_BOTH));
+ fullTable.setHeaderVisible(true);
+ fullTable.setLinesVisible(true);
+
+ // create the column
+ final TableColumn fullTableColumn = new TableColumn(fullTable, SWT.LEFT);
+ // set the header
+ fullTableColumn.setText("Available Qualifiers");
+
+ fullTable.addControlListener(new ControlAdapter() {
+ @Override
+ public void controlResized(ControlEvent e) {
+ Rectangle r = fullTable.getClientArea();
+ fullTableColumn.setWidth(r.width);
+ }
+ });
+
+ mFullTableViewer = new TableViewer(fullTable);
+ mFullTableViewer.setContentProvider(new QualifierContentProvider());
+ mFullTableViewer.setLabelProvider(new QualifierLabelProvider(
+ false /* showQualifierValue */));
+ mFullTableViewer.setInput(mBaseConfiguration);
+ mFullTableViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+ public void selectionChanged(SelectionChangedEvent event) {
+ ISelection selection = event.getSelection();
+ if (selection instanceof IStructuredSelection) {
+ IStructuredSelection structSelection = (IStructuredSelection)selection;
+ Object first = structSelection.getFirstElement();
+
+ if (first instanceof ResourceQualifier) {
+ mAddButton.setEnabled(true);
+ return;
+ }
+ }
+
+ mAddButton.setEnabled(false);
+ }
+ });
+
+ // 2nd column is the left/right arrow button
+ Composite buttonComposite = new Composite(this, SWT.NONE);
+ gl = new GridLayout(1, false);
+ gl.marginWidth = gl.marginHeight = 0;
+ buttonComposite.setLayout(gl);
+ buttonComposite.setLayoutData(new GridData(GridData.FILL_VERTICAL));
+
+ new Composite(buttonComposite, SWT.NONE);
+ mAddButton = new Button(buttonComposite, SWT.BORDER | SWT.PUSH);
+ mAddButton.setText("->");
+ mAddButton.setEnabled(false);
+ mAddButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ IStructuredSelection selection =
+ (IStructuredSelection)mFullTableViewer.getSelection();
+
+ Object first = selection.getFirstElement();
+ if (first instanceof ResourceQualifier) {
+ ResourceQualifier qualifier = (ResourceQualifier)first;
+
+ mBaseConfiguration.removeQualifier(qualifier);
+ mSelectedConfiguration.addQualifier(qualifier);
+
+ mFullTableViewer.refresh();
+ mSelectionTableViewer.refresh();
+ mSelectionTableViewer.setSelection(new StructuredSelection(qualifier), true);
+
+ onChange(false /* keepSelection */);
+ }
+ }
+ });
+
+ mRemoveButton = new Button(buttonComposite, SWT.BORDER | SWT.PUSH);
+ mRemoveButton.setText("<-");
+ mRemoveButton.setEnabled(false);
+ mRemoveButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ IStructuredSelection selection =
+ (IStructuredSelection)mSelectionTableViewer.getSelection();
+
+ Object first = selection.getFirstElement();
+ if (first instanceof ResourceQualifier) {
+ ResourceQualifier qualifier = (ResourceQualifier)first;
+
+ mSelectedConfiguration.removeQualifier(qualifier);
+ mBaseConfiguration.addQualifier(qualifier);
+
+ mFullTableViewer.refresh();
+ mSelectionTableViewer.refresh();
+
+ onChange(false /* keepSelection */);
+ }
+ }
+ });
+
+ // 3rd column is the selected config table
+ final Table selectionTable = new Table(this, SWT.SINGLE | SWT.FULL_SELECTION | SWT.BORDER);
+ selectionTable.setLayoutData(new GridData(GridData.FILL_BOTH));
+ selectionTable.setHeaderVisible(true);
+ selectionTable.setLinesVisible(true);
+
+ // create the column
+ final TableColumn selectionTableColumn = new TableColumn(selectionTable, SWT.LEFT);
+ // set the header
+ selectionTableColumn.setText("Chosen Qualifiers");
+
+ selectionTable.addControlListener(new ControlAdapter() {
+ @Override
+ public void controlResized(ControlEvent e) {
+ Rectangle r = selectionTable.getClientArea();
+ selectionTableColumn.setWidth(r.width);
+ }
+ });
+ mSelectionTableViewer = new TableViewer(selectionTable);
+ mSelectionTableViewer.setContentProvider(new QualifierContentProvider());
+ mSelectionTableViewer.setLabelProvider(new QualifierLabelProvider(
+ true /* showQualifierValue */));
+ mSelectionTableViewer.setInput(mSelectedConfiguration);
+ mSelectionTableViewer.addSelectionChangedListener(new ISelectionChangedListener() {
+ public void selectionChanged(SelectionChangedEvent event) {
+ // ignore selection changes during resfreshes in some cases.
+ if (mOnRefresh) {
+ return;
+ }
+
+ ISelection selection = event.getSelection();
+ if (selection instanceof IStructuredSelection) {
+ IStructuredSelection structSelection = (IStructuredSelection)selection;
+
+ if (structSelection.isEmpty() == false) {
+ Object first = structSelection.getFirstElement();
+
+ if (first instanceof ResourceQualifier) {
+ mRemoveButton.setEnabled(true);
+
+ QualifierEditBase composite = mUiMap.get(first.getClass());
+
+ if (composite != null) {
+ composite.setQualifier((ResourceQualifier)first);
+ }
+
+ mStackLayout.topControl = composite;
+ mQualifierEditParent.layout();
+
+ return;
+ }
+ } else {
+ mStackLayout.topControl = null;
+ mQualifierEditParent.layout();
+ }
+ }
+
+ mRemoveButton.setEnabled(false);
+ }
+ });
+
+ // 4th column is the detail of the selected qualifier
+ mQualifierEditParent = new Composite(this, SWT.NONE);
+ mQualifierEditParent.setLayout(mStackLayout = new StackLayout());
+ mQualifierEditParent.setLayoutData(new GridData(GridData.FILL_VERTICAL));
+
+ // create the UI for all the qualifiers, and associate them to the ResourceQualifer class.
+ mUiMap.put(CountryCodeQualifier.class, new MCCEdit(mQualifierEditParent));
+ mUiMap.put(NetworkCodeQualifier.class, new MNCEdit(mQualifierEditParent));
+ mUiMap.put(LanguageQualifier.class, new LanguageEdit(mQualifierEditParent));
+ mUiMap.put(RegionQualifier.class, new RegionEdit(mQualifierEditParent));
+ mUiMap.put(ScreenOrientationQualifier.class, new OrientationEdit(mQualifierEditParent));
+ mUiMap.put(PixelDensityQualifier.class, new PixelDensityEdit(mQualifierEditParent));
+ mUiMap.put(TouchScreenQualifier.class, new TouchEdit(mQualifierEditParent));
+ mUiMap.put(KeyboardStateQualifier.class, new KeyboardEdit(mQualifierEditParent));
+ mUiMap.put(TextInputMethodQualifier.class, new TextInputEdit(mQualifierEditParent));
+ mUiMap.put(NavigationMethodQualifier.class, new NavigationEdit(mQualifierEditParent));
+ mUiMap.put(ScreenDimensionQualifier.class, new ScreenDimensionEdit(mQualifierEditParent));
+ }
+
+ /**
+ * Sets a listener to be notified when the configuration changes.
+ * @param listener A {@link Runnable} whose <code>run()</code> method is called when the
+ * configuration is changed. The method is called from the UI thread.
+ */
+ public void setOnChangeListener(Runnable listener) {
+ mOnChangeListener = listener;
+ }
+
+ /**
+ * Initialize the UI with a given {@link FolderConfiguration}. This must
+ * be called from the UI thread.
+ * @param config The configuration.
+ */
+ public void setConfiguration(FolderConfiguration config) {
+ mSelectedConfiguration.set(config);
+ mSelectionTableViewer.refresh();
+
+ // create the base config, which is the default config minus the qualifiers
+ // in SelectedConfiguration
+ mBaseConfiguration.substract(mSelectedConfiguration);
+ mFullTableViewer.refresh();
+ }
+
+ /**
+ * Initialize the UI with the configuration represented by a resource folder name.
+ * This must be called from the UI thread.
+ *
+ * @param folderSegments the segments of the folder name,
+ * split using {@link FolderConfiguration#QUALIFIER_SEP}.
+ * @return true if success, or false if the folder name is not a valid name.
+ */
+ public boolean setConfiguration(String[] folderSegments) {
+ FolderConfiguration config = ResourceManager.getInstance().getConfig(folderSegments);
+
+ if (config == null) {
+ return false;
+ }
+
+ setConfiguration(config);
+
+ return true;
+ }
+
+ /**
+ * Initialize the UI with the configuration represented by a resource folder name.
+ * This must be called from the UI thread.
+ * @param folderName the name of the folder.
+ * @return true if success, or false if the folder name is not a valid name.
+ */
+ public boolean setConfiguration(String folderName) {
+ // split the name of the folder in segments.
+ String[] folderSegments = folderName.split(FolderConfiguration.QUALIFIER_SEP);
+
+ return setConfiguration(folderSegments);
+ }
+
+ /**
+ * Gets the configuration as setup by the widget.
+ * @param config the {@link FolderConfiguration} object to be filled with the information
+ * from the UI.
+ */
+ public void getConfiguration(FolderConfiguration config) {
+ config.set(mSelectedConfiguration);
+ }
+
+ /**
+ * Returns the state of the configuration being edited/created.
+ */
+ public ConfigurationState getState() {
+ if (mSelectedConfiguration.getInvalidQualifier() != null) {
+ return ConfigurationState.INVALID_CONFIG;
+ }
+
+ if (mSelectedConfiguration.checkRegion() == false) {
+ return ConfigurationState.REGION_WITHOUT_LANGUAGE;
+ }
+
+ return ConfigurationState.OK;
+ }
+
+ /**
+ * Returns the first invalid qualifier of the configuration being edited/created,
+ * or <code>null<code> if they are all valid (or if none exists).
+ * <p/>If {@link #getState()} return {@link ConfigurationState#INVALID_CONFIG} then this will
+ * not return <code>null</code>.
+ */
+ public ResourceQualifier getInvalidQualifier() {
+ return mSelectedConfiguration.getInvalidQualifier();
+ }
+
+ /**
+ * Handle changes in the configuration.
+ * @param keepSelection if <code>true</code> attemps to avoid triggering selection change in
+ * {@link #mSelectedConfiguration}.
+ */
+ private void onChange(boolean keepSelection) {
+ ISelection selection = null;
+ if (keepSelection) {
+ mOnRefresh = true;
+ selection = mSelectionTableViewer.getSelection();
+ }
+
+ mSelectionTableViewer.refresh(true);
+
+ if (keepSelection) {
+ mSelectionTableViewer.setSelection(selection);
+ mOnRefresh = false;
+ }
+
+ if (mOnChangeListener != null) {
+ mOnChangeListener.run();
+ }
+ }
+
+ /**
+ * Content provider around a {@link FolderConfiguration}.
+ */
+ private static class QualifierContentProvider implements IStructuredContentProvider {
+
+ private FolderConfiguration mInput;
+
+ public QualifierContentProvider() {
+ }
+
+ public void dispose() {
+ // pass
+ }
+
+ public Object[] getElements(Object inputElement) {
+ return mInput.getQualifiers();
+ }
+
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ mInput = null;
+ if (newInput instanceof FolderConfiguration) {
+ mInput = (FolderConfiguration)newInput;
+ }
+ }
+ }
+
+ /**
+ * Label provider for {@link ResourceQualifier} objects.
+ */
+ private static class QualifierLabelProvider implements ITableLabelProvider {
+
+ private final boolean mShowQualifierValue;
+
+ public QualifierLabelProvider(boolean showQualifierValue) {
+ mShowQualifierValue = showQualifierValue;
+ }
+
+ public String getColumnText(Object element, int columnIndex) {
+ // only one column, so we can ignore columnIndex
+ if (element instanceof ResourceQualifier) {
+ if (mShowQualifierValue) {
+ String value = ((ResourceQualifier)element).getStringValue();
+ if (value.length() == 0) {
+ return String.format("%1$s (?)",
+ ((ResourceQualifier)element).getShortName());
+ } else {
+ return value;
+ }
+
+ } else {
+ return ((ResourceQualifier)element).getShortName();
+ }
+ }
+
+ return null;
+ }
+
+ public Image getColumnImage(Object element, int columnIndex) {
+ // only one column, so we can ignore columnIndex
+ if (element instanceof ResourceQualifier) {
+ return ((ResourceQualifier)element).getIcon();
+ }
+
+ return null;
+ }
+
+ public void addListener(ILabelProviderListener listener) {
+ // pass
+ }
+
+ public void dispose() {
+ // pass
+ }
+
+ public boolean isLabelProperty(Object element, String property) {
+ // pass
+ return false;
+ }
+
+ public void removeListener(ILabelProviderListener listener) {
+ // pass
+ }
+ }
+
+ /**
+ * Base class for Edit widget for {@link ResourceQualifier}.
+ */
+ private abstract static class QualifierEditBase extends Composite {
+
+ public QualifierEditBase(Composite parent, String title) {
+ super(parent, SWT.NONE);
+ setLayout(new GridLayout(1, false));
+
+ new Label(this, SWT.NONE).setText(title);
+ }
+
+ public abstract void setQualifier(ResourceQualifier qualifier);
+ }
+
+ /**
+ * Edit widget for {@link CountryCodeQualifier}.
+ */
+ private class MCCEdit extends QualifierEditBase {
+
+ private Text mText;
+
+ public MCCEdit(Composite parent) {
+ super(parent, CountryCodeQualifier.NAME);
+
+ mText = new Text(this, SWT.BORDER);
+ mText.addVerifyListener(new MobileCodeVerifier());
+ mText.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ onTextChange();
+ }
+ });
+
+ mText.addFocusListener(new FocusAdapter() {
+ @Override
+ public void focusLost(FocusEvent e) {
+ onTextChange();
+ }
+ });
+
+ new Label(this, SWT.NONE).setText("(3 digit code)");
+ }
+
+ private void onTextChange() {
+ String value = mText.getText();
+
+ if (value.length() == 0) {
+ // empty string, means a qualifier with no value.
+ // Since the qualifier classes are immutable, and we don't want to
+ // remove the qualifier from the configuration, we create a new default one.
+ mSelectedConfiguration.setCountryCodeQualifier(new CountryCodeQualifier());
+ } else {
+ try {
+ CountryCodeQualifier qualifier = CountryCodeQualifier.getQualifier(
+ CountryCodeQualifier.getFolderSegment(Integer.parseInt(value)));
+ if (qualifier != null) {
+ mSelectedConfiguration.setCountryCodeQualifier(qualifier);
+ } else {
+ // Failure! Looks like the value is wrong
+ // (for instance not exactly 3 digits).
+ mSelectedConfiguration.setCountryCodeQualifier(new CountryCodeQualifier());
+ }
+ } catch (NumberFormatException nfe) {
+ // Looks like the code is not a number. This should not happen since the text
+ // field has a VerifyListener that prevents it.
+ mSelectedConfiguration.setCountryCodeQualifier(new CountryCodeQualifier());
+ }
+ }
+
+ // notify of change
+ onChange(true /* keepSelection */);
+ }
+
+ @Override
+ public void setQualifier(ResourceQualifier qualifier) {
+ CountryCodeQualifier q = (CountryCodeQualifier)qualifier;
+
+ mText.setText(Integer.toString(q.getCode()));
+ }
+ }
+
+ /**
+ * Edit widget for {@link NetworkCodeQualifier}.
+ */
+ private class MNCEdit extends QualifierEditBase {
+ private Text mText;
+
+ public MNCEdit(Composite parent) {
+ super(parent, NetworkCodeQualifier.NAME);
+
+ mText = new Text(this, SWT.BORDER);
+ mText.addVerifyListener(new MobileCodeVerifier());
+ mText.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ onTextChange();
+ }
+ });
+ mText.addFocusListener(new FocusAdapter() {
+ @Override
+ public void focusLost(FocusEvent e) {
+ onTextChange();
+ }
+ });
+
+ new Label(this, SWT.NONE).setText("(1-3 digit code)");
+ }
+
+ private void onTextChange() {
+ String value = mText.getText();
+
+ if (value.length() == 0) {
+ // empty string, means a qualifier with no value.
+ // Since the qualifier classes are immutable, and we don't want to
+ // remove the qualifier from the configuration, we create a new default one.
+ mSelectedConfiguration.setNetworkCodeQualifier(new NetworkCodeQualifier());
+ } else {
+ try {
+ NetworkCodeQualifier qualifier = NetworkCodeQualifier.getQualifier(
+ NetworkCodeQualifier.getFolderSegment(Integer.parseInt(value)));
+ if (qualifier != null) {
+ mSelectedConfiguration.setNetworkCodeQualifier(qualifier);
+ } else {
+ // Failure! Looks like the value is wrong
+ // (for instance not exactly 3 digits).
+ mSelectedConfiguration.setNetworkCodeQualifier(new NetworkCodeQualifier());
+ }
+ } catch (NumberFormatException nfe) {
+ // Looks like the code is not a number. This should not happen since the text
+ // field has a VerifyListener that prevents it.
+ mSelectedConfiguration.setNetworkCodeQualifier(new NetworkCodeQualifier());
+ }
+ }
+
+ // notify of change
+ onChange(true /* keepSelection */);
+ }
+
+ @Override
+ public void setQualifier(ResourceQualifier qualifier) {
+ NetworkCodeQualifier q = (NetworkCodeQualifier)qualifier;
+
+ mText.setText(Integer.toString(q.getCode()));
+ }
+ }
+
+ /**
+ * Edit widget for {@link LanguageQualifier}.
+ */
+ private class LanguageEdit extends QualifierEditBase {
+ private Combo mLanguage;
+
+ public LanguageEdit(Composite parent) {
+ super(parent, LanguageQualifier.NAME);
+
+ mLanguage = new Combo(this, SWT.DROP_DOWN);
+ mLanguage.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mLanguage.addVerifyListener(new LanguageRegionVerifier());
+ mLanguage.addSelectionListener(new SelectionListener() {
+ public void widgetDefaultSelected(SelectionEvent e) {
+ onLanguageChange();
+ }
+ public void widgetSelected(SelectionEvent e) {
+ onLanguageChange();
+ }
+ });
+ mLanguage.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ onLanguageChange();
+ }
+ });
+
+ new Label(this, SWT.NONE).setText("(2 letter code)");
+ }
+
+ private void onLanguageChange() {
+ // update the current config
+ String value = mLanguage.getText();
+
+ if (value.length() == 0) {
+ // empty string, means no qualifier.
+ // Since the qualifier classes are immutable, and we don't want to
+ // remove the qualifier from the configuration, we create a new default one.
+ mSelectedConfiguration.setLanguageQualifier(new LanguageQualifier());
+ } else {
+ LanguageQualifier qualifier = null;
+ String segment = LanguageQualifier.getFolderSegment(value);
+ if (segment != null) {
+ qualifier = LanguageQualifier.getQualifier(segment);
+ }
+
+ if (qualifier != null) {
+ mSelectedConfiguration.setLanguageQualifier(qualifier);
+ } else {
+ // Failure! Looks like the value is wrong (for instance a one letter string).
+ mSelectedConfiguration.setLanguageQualifier(new LanguageQualifier());
+ }
+ }
+
+ // notify of change
+ onChange(true /* keepSelection */);
+ }
+
+ @Override
+ public void setQualifier(ResourceQualifier qualifier) {
+ LanguageQualifier q = (LanguageQualifier)qualifier;
+
+ String value = q.getValue();
+ if (value != null) {
+ mLanguage.setText(value);
+ }
+ }
+ }
+
+ /**
+ * Edit widget for {@link RegionQualifier}.
+ */
+ private class RegionEdit extends QualifierEditBase {
+ private Combo mRegion;
+
+ public RegionEdit(Composite parent) {
+ super(parent, RegionQualifier.NAME);
+
+ mRegion = new Combo(this, SWT.DROP_DOWN);
+ mRegion.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mRegion.addVerifyListener(new LanguageRegionVerifier());
+ mRegion.addSelectionListener(new SelectionListener() {
+ public void widgetDefaultSelected(SelectionEvent e) {
+ onRegionChange();
+ }
+ public void widgetSelected(SelectionEvent e) {
+ onRegionChange();
+ }
+ });
+ mRegion.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ onRegionChange();
+ }
+ });
+
+ new Label(this, SWT.NONE).setText("(2 letter code)");
+ }
+
+ private void onRegionChange() {
+ // update the current config
+ String value = mRegion.getText();
+
+ if (value.length() == 0) {
+ // empty string, means no qualifier.
+ // Since the qualifier classes are immutable, and we don't want to
+ // remove the qualifier from the configuration, we create a new default one.
+ mSelectedConfiguration.setRegionQualifier(new RegionQualifier());
+ } else {
+ RegionQualifier qualifier = null;
+ String segment = RegionQualifier.getFolderSegment(value);
+ if (segment != null) {
+ qualifier = RegionQualifier.getQualifier(segment);
+ }
+
+ if (qualifier != null) {
+ mSelectedConfiguration.setRegionQualifier(qualifier);
+ } else {
+ // Failure! Looks like the value is wrong (for instance a one letter string).
+ mSelectedConfiguration.setRegionQualifier(new RegionQualifier());
+ }
+ }
+
+ // notify of change
+ onChange(true /* keepSelection */);
+ }
+
+ @Override
+ public void setQualifier(ResourceQualifier qualifier) {
+ RegionQualifier q = (RegionQualifier)qualifier;
+
+ String value = q.getValue();
+ if (value != null) {
+ mRegion.setText(q.getValue());
+ }
+ }
+ }
+
+ /**
+ * Edit widget for {@link ScreenOrientationQualifier}.
+ */
+ private class OrientationEdit extends QualifierEditBase {
+
+ private Combo mOrientation;
+
+ public OrientationEdit(Composite parent) {
+ super(parent, ScreenOrientationQualifier.NAME);
+
+ mOrientation = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY);
+ ScreenOrientation[] soValues = ScreenOrientation.values();
+ for (ScreenOrientation value : soValues) {
+ mOrientation.add(value.getDisplayValue());
+ }
+
+ mOrientation.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mOrientation.addSelectionListener(new SelectionListener() {
+ public void widgetDefaultSelected(SelectionEvent e) {
+ onOrientationChange();
+ }
+ public void widgetSelected(SelectionEvent e) {
+ onOrientationChange();
+ }
+ });
+ }
+
+ protected void onOrientationChange() {
+ // update the current config
+ int index = mOrientation.getSelectionIndex();
+
+ if (index != -1) {
+ mSelectedConfiguration.setScreenOrientationQualifier(new ScreenOrientationQualifier(
+ ScreenOrientation.getByIndex(index)));
+ } else {
+ // empty selection, means no qualifier.
+ // Since the qualifier classes are immutable, and we don't want to
+ // remove the qualifier from the configuration, we create a new default one.
+ mSelectedConfiguration.setScreenOrientationQualifier(
+ new ScreenOrientationQualifier());
+ }
+
+ // notify of change
+ onChange(true /* keepSelection */);
+ }
+
+ @Override
+ public void setQualifier(ResourceQualifier qualifier) {
+ ScreenOrientationQualifier q = (ScreenOrientationQualifier)qualifier;
+
+ ScreenOrientation value = q.getValue();
+ if (value == null) {
+ mOrientation.clearSelection();
+ } else {
+ mOrientation.select(ScreenOrientation.getIndex(value));
+ }
+ }
+ }
+
+ /**
+ * Edit widget for {@link PixelDensityQualifier}.
+ */
+ private class PixelDensityEdit extends QualifierEditBase {
+ private Text mText;
+
+ public PixelDensityEdit(Composite parent) {
+ super(parent, PixelDensityQualifier.NAME);
+
+ mText = new Text(this, SWT.BORDER);
+ mText.addVerifyListener(new DensityVerifier());
+ mText.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ onTextChange();
+ }
+ });
+ mText.addFocusListener(new FocusAdapter() {
+ @Override
+ public void focusLost(FocusEvent e) {
+ onTextChange();
+ }
+ });
+ }
+
+ private void onTextChange() {
+ String value = mText.getText();
+
+ if (value.length() == 0) {
+ // empty string, means a qualifier with no value.
+ // Since the qualifier classes are immutable, and we don't want to
+ // remove the qualifier from the configuration, we create a new default one.
+ mSelectedConfiguration.setPixelDensityQualifier(new PixelDensityQualifier());
+ } else {
+ try {
+ PixelDensityQualifier qualifier = PixelDensityQualifier.getQualifier(
+ PixelDensityQualifier.getFolderSegment(Integer.parseInt(value)));
+ if (qualifier != null) {
+ mSelectedConfiguration.setPixelDensityQualifier(qualifier);
+ } else {
+ // Failure! Looks like the value is wrong
+ // (for instance a one letter string).
+ // We do nothing in this case.
+ return;
+ }
+ } catch (NumberFormatException nfe) {
+ // Looks like the code is not a number. This should not happen since the text
+ // field has a VerifyListener that prevents it.
+ // We do nothing in this case.
+ return;
+ }
+ }
+
+ // notify of change
+ onChange(true /* keepSelection */);
+ }
+
+ @Override
+ public void setQualifier(ResourceQualifier qualifier) {
+ PixelDensityQualifier q = (PixelDensityQualifier)qualifier;
+
+ mText.setText(Integer.toString(q.getValue()));
+ }
+ }
+
+ /**
+ * Edit widget for {@link TouchScreenQualifier}.
+ */
+ private class TouchEdit extends QualifierEditBase {
+
+ private Combo mTouchScreen;
+
+ public TouchEdit(Composite parent) {
+ super(parent, TouchScreenQualifier.NAME);
+
+ mTouchScreen = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY);
+ TouchScreenType[] tstValues = TouchScreenType.values();
+ for (TouchScreenType value : tstValues) {
+ mTouchScreen.add(value.getDisplayValue());
+ }
+
+ mTouchScreen.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mTouchScreen.addSelectionListener(new SelectionListener() {
+ public void widgetDefaultSelected(SelectionEvent e) {
+ onTouchChange();
+ }
+ public void widgetSelected(SelectionEvent e) {
+ onTouchChange();
+ }
+ });
+ }
+
+ protected void onTouchChange() {
+ // update the current config
+ int index = mTouchScreen.getSelectionIndex();
+
+ if (index != -1) {
+ mSelectedConfiguration.setTouchTypeQualifier(new TouchScreenQualifier(
+ TouchScreenType.getByIndex(index)));
+ } else {
+ // empty selection, means no qualifier.
+ // Since the qualifier classes are immutable, and we don't want to
+ // remove the qualifier from the configuration, we create a new default one.
+ mSelectedConfiguration.setTouchTypeQualifier(new TouchScreenQualifier());
+ }
+
+ // notify of change
+ onChange(true /* keepSelection */);
+ }
+
+ @Override
+ public void setQualifier(ResourceQualifier qualifier) {
+ TouchScreenQualifier q = (TouchScreenQualifier)qualifier;
+
+ TouchScreenType value = q.getValue();
+ if (value == null) {
+ mTouchScreen.clearSelection();
+ } else {
+ mTouchScreen.select(TouchScreenType.getIndex(value));
+ }
+ }
+ }
+
+ /**
+ * Edit widget for {@link KeyboardStateQualifier}.
+ */
+ private class KeyboardEdit extends QualifierEditBase {
+
+ private Combo mKeyboard;
+
+ public KeyboardEdit(Composite parent) {
+ super(parent, KeyboardStateQualifier.NAME);
+
+ mKeyboard = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY);
+ KeyboardState[] ksValues = KeyboardState.values();
+ for (KeyboardState value : ksValues) {
+ mKeyboard.add(value.getDisplayValue());
+ }
+
+ mKeyboard.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mKeyboard.addSelectionListener(new SelectionListener() {
+ public void widgetDefaultSelected(SelectionEvent e) {
+ onKeyboardChange();
+ }
+ public void widgetSelected(SelectionEvent e) {
+ onKeyboardChange();
+ }
+ });
+ }
+
+ protected void onKeyboardChange() {
+ // update the current config
+ int index = mKeyboard.getSelectionIndex();
+
+ if (index != -1) {
+ mSelectedConfiguration.setKeyboardStateQualifier(new KeyboardStateQualifier(
+ KeyboardState.getByIndex(index)));
+ } else {
+ // empty selection, means no qualifier.
+ // Since the qualifier classes are immutable, and we don't want to
+ // remove the qualifier from the configuration, we create a new default one.
+ mSelectedConfiguration.setKeyboardStateQualifier(
+ new KeyboardStateQualifier());
+ }
+
+ // notify of change
+ onChange(true /* keepSelection */);
+ }
+
+ @Override
+ public void setQualifier(ResourceQualifier qualifier) {
+ KeyboardStateQualifier q = (KeyboardStateQualifier)qualifier;
+
+ KeyboardState value = q.getValue();
+ if (value == null) {
+ mKeyboard.clearSelection();
+ } else {
+ mKeyboard.select(KeyboardState.getIndex(value));
+ }
+ }
+ }
+
+ /**
+ * Edit widget for {@link TextInputMethodQualifier}.
+ */
+ private class TextInputEdit extends QualifierEditBase {
+
+ private Combo mTextInput;
+
+ public TextInputEdit(Composite parent) {
+ super(parent, TextInputMethodQualifier.NAME);
+
+ mTextInput = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY);
+ TextInputMethod[] timValues = TextInputMethod.values();
+ for (TextInputMethod value : timValues) {
+ mTextInput.add(value.getDisplayValue());
+ }
+
+ mTextInput.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mTextInput.addSelectionListener(new SelectionListener() {
+ public void widgetDefaultSelected(SelectionEvent e) {
+ onTextInputChange();
+ }
+ public void widgetSelected(SelectionEvent e) {
+ onTextInputChange();
+ }
+ });
+ }
+
+ protected void onTextInputChange() {
+ // update the current config
+ int index = mTextInput.getSelectionIndex();
+
+ if (index != -1) {
+ mSelectedConfiguration.setTextInputMethodQualifier(new TextInputMethodQualifier(
+ TextInputMethod.getByIndex(index)));
+ } else {
+ // empty selection, means no qualifier.
+ // Since the qualifier classes are immutable, and we don't want to
+ // remove the qualifier from the configuration, we create a new default one.
+ mSelectedConfiguration.setTextInputMethodQualifier(
+ new TextInputMethodQualifier());
+ }
+
+ // notify of change
+ onChange(true /* keepSelection */);
+ }
+
+ @Override
+ public void setQualifier(ResourceQualifier qualifier) {
+ TextInputMethodQualifier q = (TextInputMethodQualifier)qualifier;
+
+ TextInputMethod value = q.getValue();
+ if (value == null) {
+ mTextInput.clearSelection();
+ } else {
+ mTextInput.select(TextInputMethod.getIndex(value));
+ }
+ }
+ }
+
+ /**
+ * Edit widget for {@link NavigationMethodQualifier}.
+ */
+ private class NavigationEdit extends QualifierEditBase {
+
+ private Combo mNavigation;
+
+ public NavigationEdit(Composite parent) {
+ super(parent, NavigationMethodQualifier.NAME);
+
+ mNavigation = new Combo(this, SWT.DROP_DOWN | SWT.READ_ONLY);
+ NavigationMethod[] nmValues = NavigationMethod.values();
+ for (NavigationMethod value : nmValues) {
+ mNavigation.add(value.getDisplayValue());
+ }
+
+ mNavigation.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mNavigation.addSelectionListener(new SelectionListener() {
+ public void widgetDefaultSelected(SelectionEvent e) {
+ onNavigationChange();
+ }
+ public void widgetSelected(SelectionEvent e) {
+ onNavigationChange();
+ }
+ });
+ }
+
+ protected void onNavigationChange() {
+ // update the current config
+ int index = mNavigation.getSelectionIndex();
+
+ if (index != -1) {
+ mSelectedConfiguration.setNavigationMethodQualifier(new NavigationMethodQualifier(
+ NavigationMethod.getByIndex(index)));
+ } else {
+ // empty selection, means no qualifier.
+ // Since the qualifier classes are immutable, and we don't want to
+ // remove the qualifier from the configuration, we create a new default one.
+ mSelectedConfiguration.setNavigationMethodQualifier(
+ new NavigationMethodQualifier());
+ }
+
+ // notify of change
+ onChange(true /* keepSelection */);
+ }
+
+ @Override
+ public void setQualifier(ResourceQualifier qualifier) {
+ NavigationMethodQualifier q = (NavigationMethodQualifier)qualifier;
+
+ NavigationMethod value = q.getValue();
+ if (value == null) {
+ mNavigation.clearSelection();
+ } else {
+ mNavigation.select(NavigationMethod.getIndex(value));
+ }
+ }
+ }
+
+ /**
+ * Edit widget for {@link ScreenDimensionQualifier}.
+ */
+ private class ScreenDimensionEdit extends QualifierEditBase {
+
+ private Text mSize1;
+ private Text mSize2;
+
+ public ScreenDimensionEdit(Composite parent) {
+ super(parent, ScreenDimensionQualifier.NAME);
+
+ ModifyListener modifyListener = new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ onSizeChange();
+ }
+ };
+
+ FocusAdapter focusListener = new FocusAdapter() {
+ @Override
+ public void focusLost(FocusEvent e) {
+ onSizeChange();
+ }
+ };
+
+ mSize1 = new Text(this, SWT.BORDER);
+ mSize1.addVerifyListener(new DimensionVerifier());
+ mSize1.addModifyListener(modifyListener);
+ mSize1.addFocusListener(focusListener);
+
+ mSize2 = new Text(this, SWT.BORDER);
+ mSize2.addVerifyListener(new DimensionVerifier());
+ mSize2.addModifyListener(modifyListener);
+ mSize2.addFocusListener(focusListener);
+ }
+
+ private void onSizeChange() {
+ // update the current config
+ String size1 = mSize1.getText();
+ String size2 = mSize2.getText();
+
+ if (size1.length() == 0 || size2.length() == 0) {
+ // if one of the strings is empty, reset to no qualifier.
+ // Since the qualifier classes are immutable, and we don't want to
+ // remove the qualifier from the configuration, we create a new default one.
+ mSelectedConfiguration.setScreenDimensionQualifier(new ScreenDimensionQualifier());
+ } else {
+ ScreenDimensionQualifier qualifier = ScreenDimensionQualifier.getQualifier(size1,
+ size2);
+
+ if (qualifier != null) {
+ mSelectedConfiguration.setScreenDimensionQualifier(qualifier);
+ } else {
+ // Failure! Looks like the value is wrong, reset the qualifier
+ // Since the qualifier classes are immutable, and we don't want to
+ // remove the qualifier from the configuration, we create a new default one.
+ mSelectedConfiguration.setScreenDimensionQualifier(
+ new ScreenDimensionQualifier());
+ }
+ }
+
+ // notify of change
+ onChange(true /* keepSelection */);
+ }
+
+ @Override
+ public void setQualifier(ResourceQualifier qualifier) {
+ ScreenDimensionQualifier q = (ScreenDimensionQualifier)qualifier;
+
+ mSize1.setText(Integer.toString(q.getValue1()));
+ mSize2.setText(Integer.toString(q.getValue2()));
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/NewXmlFileCreationPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/NewXmlFileCreationPage.java
new file mode 100644
index 0000000..5781938
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/NewXmlFileCreationPage.java
@@ -0,0 +1,1159 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.ide.eclipse.editors.wizards;
+
+import com.android.ide.eclipse.adt.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.sdk.Sdk;
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.common.project.ProjectChooserHelper;
+import com.android.ide.eclipse.editors.descriptors.DocumentDescriptor;
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.editors.descriptors.IDescriptorProvider;
+import com.android.ide.eclipse.editors.menu.descriptors.MenuDescriptors;
+import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration;
+import com.android.ide.eclipse.editors.resources.configurations.ResourceQualifier;
+import com.android.ide.eclipse.editors.resources.descriptors.ResourcesDescriptors;
+import com.android.ide.eclipse.editors.resources.manager.ResourceFolderType;
+import com.android.ide.eclipse.editors.wizards.ConfigurationSelector.ConfigurationState;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.SdkConstants;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IAdaptable;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.wizard.WizardPage;
+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.Label;
+import org.eclipse.swt.widgets.Text;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+
+/**
+ * This is the single page of the {@link NewXmlFileWizard} which provides the ability to create
+ * skeleton XML resources files for Android projects.
+ * <p/>
+ * This page is used to select the project, the resource folder, resource type and file name.
+ */
+class NewXmlFileCreationPage extends WizardPage {
+
+ /**
+ * Information on one type of resource that can be created (e.g. menu, pref, layout, etc.)
+ */
+ static class TypeInfo {
+ private final String mUiName;
+ private final ResourceFolderType mResFolderType;
+ private final String mTooltip;
+ private final Object mRootSeed;
+ private Button mWidget;
+ private ArrayList<String> mRoots = new ArrayList<String>();
+ private final String mXmlns;
+ private final String mDefaultAttrs;
+ private final String mDefaultRoot;
+ private final int mTargetApiLevel;
+
+ public TypeInfo(String uiName,
+ String tooltip,
+ ResourceFolderType resFolderType,
+ Object rootSeed,
+ String defaultRoot,
+ String xmlns,
+ String defaultAttrs,
+ int targetApiLevel) {
+ mUiName = uiName;
+ mResFolderType = resFolderType;
+ mTooltip = tooltip;
+ mRootSeed = rootSeed;
+ mDefaultRoot = defaultRoot;
+ mXmlns = xmlns;
+ mDefaultAttrs = defaultAttrs;
+ mTargetApiLevel = targetApiLevel;
+ }
+
+ /** Returns the UI name for the resource type. Unique. Never null. */
+ String getUiName() {
+ return mUiName;
+ }
+
+ /** Returns the tooltip for the resource type. Can be null. */
+ String getTooltip() {
+ return mTooltip;
+ }
+
+ /**
+ * Returns the name of the {@link ResourceFolderType}.
+ * Never null but not necessarily unique,
+ * e.g. two types use {@link ResourceFolderType#XML}.
+ */
+ String getResFolderName() {
+ return mResFolderType.getName();
+ }
+
+ /**
+ * Returns the matching {@link ResourceFolderType}.
+ * Never null but not necessarily unique,
+ * e.g. two types use {@link ResourceFolderType#XML}.
+ */
+ ResourceFolderType getResFolderType() {
+ return mResFolderType;
+ }
+
+ /** Sets the radio button associate with the resource type. Can be null. */
+ void setWidget(Button widget) {
+ mWidget = widget;
+ }
+
+ /** Returns the radio button associate with the resource type. Can be null. */
+ Button getWidget() {
+ return mWidget;
+ }
+
+ /**
+ * Returns the seed used to fill the root element values.
+ * The seed might be either a String, a String array, an {@link ElementDescriptor},
+ * a {@link DocumentDescriptor} or null.
+ */
+ Object getRootSeed() {
+ return mRootSeed;
+ }
+
+ /** Returns the default root element that should be selected by default. Can be null. */
+ String getDefaultRoot() {
+ return mDefaultRoot;
+ }
+
+ /**
+ * Returns the list of all possible root elements for the resource type.
+ * This can be an empty ArrayList but not null.
+ * <p/>
+ * TODO: the root list SHOULD depend on the currently selected project, to include
+ * custom classes.
+ */
+ ArrayList<String> getRoots() {
+ return mRoots;
+ }
+
+ /**
+ * If the generated resource XML file requires an "android" XMLNS, this should be set
+ * to {@link SdkConstants#NS_RESOURCES}. When it is null, no XMLNS is generated.
+ */
+ String getXmlns() {
+ return mXmlns;
+ }
+
+ /**
+ * When not null, this represent extra attributes that must be specified in the
+ * root element of the generated XML file. When null, no extra attributes are inserted.
+ */
+ String getDefaultAttrs() {
+ return mDefaultAttrs;
+ }
+
+ /**
+ * The minimum API level required by the current SDK target to support this feature.
+ */
+ public int getTargetApiLevel() {
+ return mTargetApiLevel;
+ }
+ }
+
+ /**
+ * TypeInfo, information for each "type" of file that can be created.
+ */
+ private static final TypeInfo[] sTypes = {
+ new TypeInfo(
+ "Layout", // UI name
+ "An XML file that describes a screen layout.", // tooltip
+ ResourceFolderType.LAYOUT, // folder type
+ AndroidTargetData.DESCRIPTOR_LAYOUT, // root seed
+ "LinearLayout", // default root
+ SdkConstants.NS_RESOURCES, // xmlns
+ "android:layout_width=\"wrap_content\"\n" + // default attributes
+ "android:layout_height=\"wrap_content\"",
+ 1 // target API level
+ ),
+ new TypeInfo("Values", // UI name
+ "An XML file with simple values: colors, strings, dimensions, etc.", // tooltip
+ ResourceFolderType.VALUES, // folder type
+ ResourcesDescriptors.ROOT_ELEMENT, // root seed
+ null, // default root
+ null, // xmlns
+ null, // default attributes
+ 1 // target API level
+ ),
+ new TypeInfo("Menu", // UI name
+ "An XML file that describes an menu.", // tooltip
+ ResourceFolderType.MENU, // folder type
+ MenuDescriptors.MENU_ROOT_ELEMENT, // root seed
+ null, // default root
+ SdkConstants.NS_RESOURCES, // xmlns
+ null, // default attributes
+ 1 // target API level
+ ),
+ new TypeInfo("Gadget Provider", // UI name
+ "An XML file that describes a gadget provider.", // tooltip
+ ResourceFolderType.XML, // folder type
+ AndroidTargetData.DESCRIPTOR_GADGET_PROVIDER, // root seed
+ null, // default root
+ SdkConstants.NS_RESOURCES, // xmlns
+ null, // default attributes
+ 3 // target API level
+ ),
+ new TypeInfo("Preference", // UI name
+ "An XML file that describes preferences.", // tooltip
+ ResourceFolderType.XML, // folder type
+ AndroidTargetData.DESCRIPTOR_PREFERENCES, // root seed
+ AndroidConstants.CLASS_PREFERENCE_SCREEN, // default root
+ SdkConstants.NS_RESOURCES, // xmlns
+ null, // default attributes
+ 1 // target API level
+ ),
+ new TypeInfo("Searchable", // UI name
+ "An XML file that describes a searchable.", // tooltip
+ ResourceFolderType.XML, // folder type
+ AndroidTargetData.DESCRIPTOR_SEARCHABLE, // root seed
+ null, // default root
+ SdkConstants.NS_RESOURCES, // xmlns
+ null, // default attributes
+ 1 // target API level
+ ),
+ new TypeInfo("Animation", // UI name
+ "An XML file that describes an animation.", // tooltip
+ ResourceFolderType.ANIM, // folder type
+ // TODO reuse constants if we ever make an editor with descriptors for animations
+ new String[] { // root seed
+ "set", //$NON-NLS-1$
+ "alpha", //$NON-NLS-1$
+ "scale", //$NON-NLS-1$
+ "translate", //$NON-NLS-1$
+ "rotate" //$NON-NLS-1$
+ },
+ "set", //$NON-NLS-1$ // default root
+ null, // xmlns
+ null, // default attributes
+ 1 // target API level
+ ),
+ };
+
+ /** Number of columns in the grid layout */
+ final static int NUM_COL = 4;
+
+ /** Absolute destination folder root, e.g. "/res/" */
+ private static String sResFolderAbs = AndroidConstants.WS_RESOURCES + AndroidConstants.WS_SEP;
+ /** Relative destination folder root, e.g. "res/" */
+ private static String sResFolderRel = SdkConstants.FD_RESOURCES + AndroidConstants.WS_SEP;
+
+ private IProject mProject;
+ private Text mProjectTextField;
+ private Button mProjectBrowseButton;
+ private Text mFileNameTextField;
+ private Text mWsFolderPathTextField;
+ private Combo mRootElementCombo;
+ private IStructuredSelection mInitialSelection;
+ private ConfigurationSelector mConfigSelector;
+ private FolderConfiguration mTempConfig = new FolderConfiguration();
+ private boolean mInternalWsFolderPathUpdate;
+ private boolean mInternalTypeUpdate;
+ private boolean mInternalConfigSelectorUpdate;
+ private ProjectChooserHelper mProjectChooserHelper;
+
+
+ // --- UI creation ---
+
+ /**
+ * Constructs a new {@link NewXmlFileCreationPage}.
+ * <p/>
+ * Called by {@link NewXmlFileWizard#createMainPage()}.
+ */
+ protected NewXmlFileCreationPage(String pageName) {
+ super(pageName);
+ setPageComplete(false);
+ }
+
+ public void setInitialSelection(IStructuredSelection initialSelection) {
+ mInitialSelection = initialSelection;
+ }
+
+ /**
+ * Called by the parent Wizard to create the UI for this Wizard Page.
+ *
+ * {@inheritDoc}
+ *
+ * @see org.eclipse.jface.dialogs.IDialogPage#createControl(org.eclipse.swt.widgets.Composite)
+ */
+ public void createControl(Composite parent) {
+ Composite composite = new Composite(parent, SWT.NULL);
+ composite.setFont(parent.getFont());
+
+ initializeDialogUnits(parent);
+
+ composite.setLayout(new GridLayout(NUM_COL, false /*makeColumnsEqualWidth*/));
+ composite.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ createProjectGroup(composite);
+ createTypeGroup(composite);
+ createRootGroup(composite);
+
+ // Show description the first time
+ setErrorMessage(null);
+ setMessage(null);
+ setControl(composite);
+
+ // Update state the first time
+ initializeFromSelection(mInitialSelection);
+ initializeRootValues();
+ enableTypesBasedOnApi();
+ validatePage();
+ }
+
+ /**
+ * Returns the target project or null.
+ */
+ public IProject getProject() {
+ return mProject;
+ }
+
+ /**
+ * Returns the destination filename or an empty string.
+ */
+ public String getFileName() {
+ return mFileNameTextField == null ? "" : mFileNameTextField.getText(); //$NON-NLS-1$
+ }
+
+ /**
+ * Returns the destination folder path relative to the project or an empty string.
+ */
+ public String getWsFolderPath() {
+ return mWsFolderPathTextField == null ? "" : mWsFolderPathTextField.getText(); //$NON-NLS-1$
+ }
+
+
+ /**
+ * Returns an {@link IFile} on the destination file.
+ * <p/>
+ * Uses {@link #getProject()}, {@link #getWsFolderPath()} and {@link #getFileName()}.
+ * <p/>
+ * Returns null if the project, filename or folder are invalid and the destination file
+ * cannot be determined.
+ * <p/>
+ * The {@link IFile} is a resource. There might or might not be an actual real file.
+ */
+ public IFile getDestinationFile() {
+ IProject project = getProject();
+ String wsFolderPath = getWsFolderPath();
+ String fileName = getFileName();
+ if (project != null && wsFolderPath.length() > 0 && fileName.length() > 0) {
+ IPath dest = new Path(wsFolderPath).append(fileName);
+ IFile file = project.getFile(dest);
+ return file;
+ }
+ return null;
+ }
+
+ /**
+ * Returns the {@link TypeInfo} for the currently selected type radio button.
+ * Returns null if no radio button is selected.
+ *
+ * @return A {@link TypeInfo} or null.
+ */
+ public TypeInfo getSelectedType() {
+ TypeInfo type = null;
+ for (TypeInfo ti : sTypes) {
+ if (ti.getWidget().getSelection()) {
+ type = ti;
+ break;
+ }
+ }
+ return type;
+ }
+
+ /**
+ * Returns the selected root element string, if any.
+ *
+ * @return The selected root element string or null.
+ */
+ public String getRootElement() {
+ int index = mRootElementCombo.getSelectionIndex();
+ if (index >= 0) {
+ return mRootElementCombo.getItem(index);
+ }
+ return null;
+ }
+
+ // --- UI creation ---
+
+ /**
+ * Helper method to create a new GridData with an horizontal span.
+ *
+ * @param horizSpan The number of cells for the horizontal span.
+ * @return A new GridData with the horizontal span.
+ */
+ private GridData newGridData(int horizSpan) {
+ GridData gd = new GridData();
+ gd.horizontalSpan = horizSpan;
+ return gd;
+ }
+
+ /**
+ * Helper method to create a new GridData with an horizontal span and a style.
+ *
+ * @param horizSpan The number of cells for the horizontal span.
+ * @param style The style, e.g. {@link GridData#FILL_HORIZONTAL}
+ * @return A new GridData with the horizontal span and the style.
+ */
+ private GridData newGridData(int horizSpan, int style) {
+ GridData gd = new GridData(style);
+ gd.horizontalSpan = horizSpan;
+ return gd;
+ }
+
+ /**
+ * Helper method that creates an empty cell in the parent composite.
+ *
+ * @param parent The parent composite.
+ */
+ private void emptyCell(Composite parent) {
+ new Label(parent, SWT.NONE);
+ }
+
+ /**
+ * Pads the parent with empty cells to match the number of columns of the parent grid.
+ *
+ * @param parent A grid layout with NUM_COL columns
+ * @param col The current number of columns used.
+ * @return 0, the new number of columns used, for convenience.
+ */
+ private int padWithEmptyCells(Composite parent, int col) {
+ for (; col < NUM_COL; ++col) {
+ emptyCell(parent);
+ }
+ col = 0;
+ return col;
+ }
+
+ /**
+ * Creates the project & filename fields.
+ * <p/>
+ * The parent must be a GridLayout with NUM_COL colums.
+ */
+ private void createProjectGroup(Composite parent) {
+ int col = 0;
+
+ // project name
+ String tooltip = "The Android Project where the new resource file will be created.";
+ Label label = new Label(parent, SWT.NONE);
+ label.setText("Project");
+ label.setToolTipText(tooltip);
+ ++col;
+
+ mProjectTextField = new Text(parent, SWT.BORDER);
+ mProjectTextField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mProjectTextField.setToolTipText(tooltip);
+ mProjectTextField.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ onProjectFieldUpdated();
+ }
+ });
+ ++col;
+
+ mProjectBrowseButton = new Button(parent, SWT.NONE);
+ mProjectBrowseButton.setText("Browse...");
+ mProjectBrowseButton.setToolTipText("Allows you to select the Android project to modify.");
+ mProjectBrowseButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ onProjectBrowse();
+ }
+ });
+ mProjectChooserHelper = new ProjectChooserHelper(parent.getShell());
+ ++col;
+
+ col = padWithEmptyCells(parent, col);
+
+ // file name
+ tooltip = "The name of the resource file to create.";
+ label = new Label(parent, SWT.NONE);
+ label.setText("File");
+ label.setToolTipText(tooltip);
+ ++col;
+
+ mFileNameTextField = new Text(parent, SWT.BORDER);
+ mFileNameTextField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mFileNameTextField.setToolTipText(tooltip);
+ mFileNameTextField.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ validatePage();
+ }
+ });
+ ++col;
+
+ padWithEmptyCells(parent, col);
+ }
+
+ /**
+ * Creates the type field, {@link ConfigurationSelector} and the folder field.
+ * <p/>
+ * The parent must be a GridLayout with NUM_COL colums.
+ */
+ private void createTypeGroup(Composite parent) {
+ // separator
+ Label label = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL);
+ label.setLayoutData(newGridData(NUM_COL, GridData.GRAB_HORIZONTAL));
+
+ // label before type radios
+ label = new Label(parent, SWT.NONE);
+ label.setText("What type of resource would you like to create?");
+ label.setLayoutData(newGridData(NUM_COL));
+
+ // display the types on three columns of radio buttons.
+ emptyCell(parent);
+ Composite grid = new Composite(parent, SWT.NONE);
+ padWithEmptyCells(parent, 2);
+
+ grid.setLayout(new GridLayout(NUM_COL, true /*makeColumnsEqualWidth*/));
+
+ SelectionListener radioListener = new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // single-click. Only do something if activated.
+ if (e.getSource() instanceof Button) {
+ onRadioTypeUpdated((Button) e.getSource());
+ }
+ }
+ };
+
+ int n = sTypes.length;
+ int num_lines = (n + NUM_COL/2) / NUM_COL;
+ for (int line = 0, k = 0; line < num_lines; line++) {
+ for (int i = 0; i < NUM_COL; i++, k++) {
+ if (k < n) {
+ TypeInfo type = sTypes[k];
+ Button radio = new Button(grid, SWT.RADIO);
+ type.setWidget(radio);
+ radio.setSelection(false);
+ radio.setText(type.getUiName());
+ radio.setToolTipText(type.getTooltip());
+ radio.addSelectionListener(radioListener);
+ } else {
+ emptyCell(grid);
+ }
+ }
+ }
+
+ // label before configuration selector
+ label = new Label(parent, SWT.NONE);
+ label.setText("What type of resource configuration would you like?");
+ label.setLayoutData(newGridData(NUM_COL));
+
+ // configuration selector
+ emptyCell(parent);
+ mConfigSelector = new ConfigurationSelector(parent);
+ GridData gd = newGridData(2, GridData.GRAB_HORIZONTAL | GridData.GRAB_VERTICAL);
+ gd.widthHint = ConfigurationSelector.WIDTH_HINT;
+ gd.heightHint = ConfigurationSelector.HEIGHT_HINT;
+ mConfigSelector.setLayoutData(gd);
+ mConfigSelector.setOnChangeListener(new onConfigSelectorUpdated());
+ emptyCell(parent);
+
+ // folder name
+ String tooltip = "The folder where the file will be generated, relative to the project.";
+ label = new Label(parent, SWT.NONE);
+ label.setText("Folder");
+ label.setToolTipText(tooltip);
+
+ mWsFolderPathTextField = new Text(parent, SWT.BORDER);
+ mWsFolderPathTextField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mWsFolderPathTextField.setToolTipText(tooltip);
+ mWsFolderPathTextField.addModifyListener(new ModifyListener() {
+ public void modifyText(ModifyEvent e) {
+ onWsFolderPathUpdated();
+ }
+ });
+ }
+
+ /**
+ * Creates the root element combo.
+ * <p/>
+ * The parent must be a GridLayout with NUM_COL colums.
+ */
+ private void createRootGroup(Composite parent) {
+ // separator
+ Label label = new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL);
+ label.setLayoutData(newGridData(NUM_COL, GridData.GRAB_HORIZONTAL));
+
+ // label before the root combo
+ String tooltip = "The root element to create in the XML file.";
+ label = new Label(parent, SWT.NONE);
+ label.setText("Select the root element for the XML file:");
+ label.setLayoutData(newGridData(NUM_COL));
+ label.setToolTipText(tooltip);
+
+ // root combo
+ emptyCell(parent);
+
+ mRootElementCombo = new Combo(parent, SWT.DROP_DOWN | SWT.READ_ONLY);
+ mRootElementCombo.setEnabled(false);
+ mRootElementCombo.select(0);
+ mRootElementCombo.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mRootElementCombo.setToolTipText(tooltip);
+
+ padWithEmptyCells(parent, 2);
+ }
+
+ /**
+ * Called by {@link NewXmlFileWizard} to initialize the page with the selection
+ * received by the wizard -- typically the current user workbench selection.
+ * <p/>
+ * Things we expect to find out from the selection:
+ * <ul>
+ * <li>The project name, valid if it's an android nature.</li>
+ * <li>The current folder, valid if it's a folder under /res</li>
+ * <li>An existing filename, in which case the user will be asked whether to override it.</li>
+ * <ul>
+ *
+ * @param selection The selection when the wizard was initiated.
+ */
+ private void initializeFromSelection(IStructuredSelection selection) {
+ if (selection == null) {
+ return;
+ }
+
+
+ // Find the best match in the element list. In case there are multiple selected elements
+ // select the one that provides the most information and assign them a score,
+ // e.g. project=1 + folder=2 + file=4.
+ IProject targetProject = null;
+ String targetWsFolderPath = null;
+ String targetFileName = null;
+ int targetScore = 0;
+ for (Object element : selection.toList()) {
+ if (element instanceof IAdaptable) {
+ IResource res = (IResource) ((IAdaptable) element).getAdapter(IResource.class);
+ IProject project = res != null ? res.getProject() : null;
+
+ // Is this an Android project?
+ try {
+ if (project == null || !project.hasNature(AndroidConstants.NATURE)) {
+ continue;
+ }
+ } catch (CoreException e) {
+ // checking the nature failed, ignore this resource
+ continue;
+ }
+
+ int score = 1; // we have a valid project at least
+
+ IPath wsFolderPath = null;
+ String fileName = null;
+ if (res.getType() == IResource.FOLDER) {
+ wsFolderPath = res.getProjectRelativePath();
+ } else if (res.getType() == IResource.FILE) {
+ fileName = res.getName();
+ wsFolderPath = res.getParent().getProjectRelativePath();
+ }
+
+ // Disregard this folder selection if it doesn't point to /res/something
+ if (wsFolderPath != null &&
+ wsFolderPath.segmentCount() > 1 &&
+ SdkConstants.FD_RESOURCES.equals(wsFolderPath.segment(0))) {
+ score += 2;
+ } else {
+ wsFolderPath = null;
+ fileName = null;
+ }
+
+ score += fileName != null ? 4 : 0;
+
+ if (score > targetScore) {
+ targetScore = score;
+ targetProject = project;
+ targetWsFolderPath = wsFolderPath != null ? wsFolderPath.toString() : null;
+ targetFileName = fileName;
+ }
+ }
+ }
+
+ // Now set the UI accordingly
+ if (targetScore > 0) {
+ mProject = targetProject;
+ mProjectTextField.setText(targetProject != null ? targetProject.getName() : ""); //$NON-NLS-1$
+ mFileNameTextField.setText(targetFileName != null ? targetFileName : ""); //$NON-NLS-1$
+ mWsFolderPathTextField.setText(targetWsFolderPath != null ? targetWsFolderPath : ""); //$NON-NLS-1$
+ }
+ }
+
+ /**
+ * Initialize the root values of the type infos based on the current framework values.
+ */
+ private void initializeRootValues() {
+ for (TypeInfo type : sTypes) {
+ // Clear all the roots for this type
+ ArrayList<String> roots = type.getRoots();
+ if (roots.size() > 0) {
+ roots.clear();
+ }
+
+ // depending of the type of the seed, initialize the root in different ways
+ Object rootSeed = type.getRootSeed();
+
+ if (rootSeed instanceof String) {
+ // The seed is a single string, Add it as-is.
+ roots.add((String) rootSeed);
+ } else if (rootSeed instanceof String[]) {
+ // The seed is an array of strings. Add them as-is.
+ for (String value : (String[]) rootSeed) {
+ roots.add(value);
+ }
+ } else if (rootSeed instanceof Integer && mProject != null) {
+ // The seed is a descriptor reference defined in AndroidTargetData.DESCRIPTOR_*
+ // In this case add all the children element descriptors defined, recursively,
+ // and avoid infinite recursion by keeping track of what has already been added.
+
+ // Note: if project is null, the root list will be empty since it has been
+ // cleared above.
+
+ // get the AndroidTargetData from the project
+ IAndroidTarget target = Sdk.getCurrent().getTarget(mProject);
+ AndroidTargetData data = Sdk.getCurrent().getTargetData(target);
+
+ IDescriptorProvider provider = data.getDescriptorProvider((Integer)rootSeed);
+ ElementDescriptor descriptor = provider.getDescriptor();
+ if (descriptor != null) {
+ HashSet<ElementDescriptor> visited = new HashSet<ElementDescriptor>();
+ initRootElementDescriptor(roots, descriptor, visited);
+ }
+
+ // Sort alphabetically.
+ Collections.sort(roots);
+ }
+ }
+ }
+
+ /**
+ * Helper method to recursively insert all XML names for the given {@link ElementDescriptor}
+ * into the roots array list. Keeps track of visited nodes to avoid infinite recursion.
+ * Also avoids inserting the top {@link DocumentDescriptor} which is generally synthetic
+ * and not a valid root element.
+ */
+ private void initRootElementDescriptor(ArrayList<String> roots,
+ ElementDescriptor desc, HashSet<ElementDescriptor> visited) {
+ if (!(desc instanceof DocumentDescriptor)) {
+ String xmlName = desc.getXmlName();
+ if (xmlName != null && xmlName.length() > 0) {
+ roots.add(xmlName);
+ }
+ }
+
+ visited.add(desc);
+
+ for (ElementDescriptor child : desc.getChildren()) {
+ if (!visited.contains(child)) {
+ initRootElementDescriptor(roots, child, visited);
+ }
+ }
+ }
+
+ /**
+ * Callback called when the user edits the project text field.
+ */
+ private void onProjectFieldUpdated() {
+ String project = mProjectTextField.getText();
+
+ // Is this a valid project?
+ IJavaProject[] projects = mProjectChooserHelper.getAndroidProjects(null /*javaModel*/);
+ IProject found = null;
+ for (IJavaProject p : projects) {
+ if (p.getProject().getName().equals(project)) {
+ found = p.getProject();
+ break;
+ }
+ }
+
+ if (found != mProject) {
+ changeProject(found);
+ }
+ }
+
+ /**
+ * Callback called when the user uses the "Browse Projects" button.
+ */
+ private void onProjectBrowse() {
+ IJavaProject p = mProjectChooserHelper.chooseJavaProject(mProjectTextField.getText());
+ if (p != null) {
+ changeProject(p.getProject());
+ mProjectTextField.setText(mProject.getName());
+ }
+ }
+
+ /**
+ * Changes mProject to the given new project and update the UI accordingly.
+ */
+ private void changeProject(IProject newProject) {
+ mProject = newProject;
+
+ // enable types based on new API level
+ enableTypesBasedOnApi();
+
+ // update the Type with the new descriptors.
+ initializeRootValues();
+
+ // update the combo
+ updateRootCombo(getSelectedType());
+
+ validatePage();
+ }
+
+ /**
+ * Callback called when the Folder text field is changed, either programmatically
+ * or by the user.
+ */
+ private void onWsFolderPathUpdated() {
+ if (mInternalWsFolderPathUpdate) {
+ return;
+ }
+
+ String wsFolderPath = mWsFolderPathTextField.getText();
+
+ // This is a custom path, we need to sanitize it.
+ // First it should start with "/res/". Then we need to make sure there are no
+ // relative paths, things like "../" or "./" or even "//".
+ wsFolderPath = wsFolderPath.replaceAll("/+\\.\\./+|/+\\./+|//+|\\\\+|^/+", "/"); //$NON-NLS-1$ //$NON-NLS-2$
+ wsFolderPath = wsFolderPath.replaceAll("^\\.\\./+|^\\./+", ""); //$NON-NLS-1$ //$NON-NLS-2$
+ wsFolderPath = wsFolderPath.replaceAll("/+\\.\\.$|/+\\.$|/+$", ""); //$NON-NLS-1$ //$NON-NLS-2$
+
+ ArrayList<TypeInfo> matches = new ArrayList<TypeInfo>();
+
+ // We get "res/foo" from selections relative to the project when we want a "/res/foo" path.
+ if (wsFolderPath.startsWith(sResFolderRel)) {
+ wsFolderPath = sResFolderAbs + wsFolderPath.substring(sResFolderRel.length());
+
+ mInternalWsFolderPathUpdate = true;
+ mWsFolderPathTextField.setText(wsFolderPath);
+ mInternalWsFolderPathUpdate = false;
+ }
+
+ if (wsFolderPath.startsWith(sResFolderAbs)) {
+ wsFolderPath = wsFolderPath.substring(sResFolderAbs.length());
+
+ int pos = wsFolderPath.indexOf(AndroidConstants.WS_SEP_CHAR);
+ if (pos >= 0) {
+ wsFolderPath = wsFolderPath.substring(0, pos);
+ }
+
+ String[] folderSegments = wsFolderPath.split(FolderConfiguration.QUALIFIER_SEP);
+
+ if (folderSegments.length > 0) {
+ String folderName = folderSegments[0];
+
+ // update config selector
+ mInternalConfigSelectorUpdate = true;
+ mConfigSelector.setConfiguration(folderSegments);
+ mInternalConfigSelectorUpdate = false;
+
+ boolean selected = false;
+ for (TypeInfo type : sTypes) {
+ if (type.getResFolderName().equals(folderName)) {
+ matches.add(type);
+ selected |= type.getWidget().getSelection();
+ }
+ }
+
+ if (matches.size() == 1) {
+ // If there's only one match, select it if it's not already selected
+ if (!selected) {
+ selectType(matches.get(0));
+ }
+ } else if (matches.size() > 1) {
+ // There are multiple type candidates for this folder. This can happen
+ // for /res/xml for example. Check to see if one of them is currently
+ // selected. If yes, leave the selection unchanged. If not, deselect all type.
+ if (!selected) {
+ selectType(null);
+ }
+ } else {
+ // Nothing valid was selected.
+ selectType(null);
+ }
+ }
+ }
+
+ validatePage();
+ }
+
+ /**
+ * Callback called when one of the type radio button is changed.
+ *
+ * @param typeWidget The type radio button that changed.
+ */
+ private void onRadioTypeUpdated(Button typeWidget) {
+ // Do nothing if this is an internal modification or if the widget has been
+ // de-selected.
+ if (mInternalTypeUpdate || !typeWidget.getSelection()) {
+ return;
+ }
+
+ // Find type info that has just been enabled.
+ TypeInfo type = null;
+ for (TypeInfo ti : sTypes) {
+ if (ti.getWidget() == typeWidget) {
+ type = ti;
+ break;
+ }
+ }
+
+ if (type == null) {
+ return;
+ }
+
+ // update the combo
+
+ updateRootCombo(type);
+
+ // update the folder path
+
+ String wsFolderPath = mWsFolderPathTextField.getText();
+ String newPath = null;
+
+ mConfigSelector.getConfiguration(mTempConfig);
+ ResourceQualifier qual = mTempConfig.getInvalidQualifier();
+ if (qual == null) {
+ // The configuration is valid. Reformat the folder path using the canonical
+ // value from the configuration.
+
+ newPath = sResFolderAbs + mTempConfig.getFolderName(type.getResFolderType());
+ } else {
+ // The configuration is invalid. We still update the path but this time
+ // do it manually on the string.
+ if (wsFolderPath.startsWith(sResFolderAbs)) {
+ wsFolderPath.replaceFirst(
+ "^(" + sResFolderAbs +")[^-]*(.*)", //$NON-NLS-1$ //$NON-NLS-2$
+ "\\1" + type.getResFolderName() + "\\2"); //$NON-NLS-1$ //$NON-NLS-2$
+ } else {
+ newPath = sResFolderAbs + mTempConfig.getFolderName(type.getResFolderType());
+ }
+ }
+
+ if (newPath != null && !newPath.equals(wsFolderPath)) {
+ mInternalWsFolderPathUpdate = true;
+ mWsFolderPathTextField.setText(newPath);
+ mInternalWsFolderPathUpdate = false;
+ }
+
+ validatePage();
+ }
+
+ /**
+ * Helper method that fills the values of the "root element" combo box based
+ * on the currently selected type radio button. Also disables the combo is there's
+ * only one choice. Always select the first root element for the given type.
+ *
+ * @param type The currently selected {@link TypeInfo}. Cannot be null.
+ */
+ private void updateRootCombo(TypeInfo type) {
+ // reset all the values in the combo
+ mRootElementCombo.removeAll();
+
+ if (type != null) {
+ // get the list of roots. The list can be empty but not null.
+ ArrayList<String> roots = type.getRoots();
+
+ // enable the combo if there's more than one choice
+ mRootElementCombo.setEnabled(roots != null && roots.size() > 1);
+
+ for (String root : roots) {
+ mRootElementCombo.add(root);
+ }
+
+ int index = 0; // default is to select the first one
+ String defaultRoot = type.getDefaultRoot();
+ if (defaultRoot != null) {
+ index = roots.indexOf(defaultRoot);
+ }
+ mRootElementCombo.select(index < 0 ? 0 : index);
+ }
+ }
+
+ /**
+ * Callback called when the configuration has changed in the {@link ConfigurationSelector}.
+ */
+ private class onConfigSelectorUpdated implements Runnable {
+ public void run() {
+ if (mInternalConfigSelectorUpdate) {
+ return;
+ }
+
+ TypeInfo type = getSelectedType();
+
+ if (type != null) {
+ mConfigSelector.getConfiguration(mTempConfig);
+ StringBuffer sb = new StringBuffer(sResFolderAbs);
+ sb.append(mTempConfig.getFolderName(type.getResFolderType()));
+
+ mInternalWsFolderPathUpdate = true;
+ mWsFolderPathTextField.setText(sb.toString());
+ mInternalWsFolderPathUpdate = false;
+
+ validatePage();
+ }
+ }
+ }
+
+ /**
+ * Helper method to select on of the type radio buttons.
+ *
+ * @param type The TypeInfo matching the radio button to selected or null to deselect them all.
+ */
+ private void selectType(TypeInfo type) {
+ if (type == null || !type.getWidget().getSelection()) {
+ mInternalTypeUpdate = true;
+ for (TypeInfo type2 : sTypes) {
+ type2.getWidget().setSelection(type2 == type);
+ }
+ updateRootCombo(type);
+ mInternalTypeUpdate = false;
+ }
+ }
+
+ /**
+ * Helper method to enable the type radio buttons depending on the current API level.
+ * <p/>
+ * A type radio button is enabled either if:
+ * - if mProject is null, API level 1 is considered valid
+ * - if mProject is !null, the project->target->API must be >= to the type's API level.
+ */
+ private void enableTypesBasedOnApi() {
+
+ IAndroidTarget target = mProject != null ? Sdk.getCurrent().getTarget(mProject) : null;
+ int currentApiLevel = 1;
+ if (target != null) {
+ currentApiLevel = target.getApiVersionNumber();
+ }
+
+ for (TypeInfo type : sTypes) {
+ type.getWidget().setEnabled(type.getTargetApiLevel() <= currentApiLevel);
+ }
+ }
+
+ /**
+ * 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 project
+ if (getProject() == null) {
+ error = "Please select an Android project.";
+ }
+
+ // -- validate filename
+ if (error == null) {
+ String fileName = getFileName();
+ if (fileName == null || fileName.length() == 0) {
+ error = "A destination file name is required.";
+ } else if (!fileName.endsWith(AndroidConstants.DOT_XML)) {
+ error = String.format("The filename must end with %1$s.", AndroidConstants.DOT_XML);
+ }
+ }
+
+ // -- validate type
+ if (error == null) {
+ TypeInfo type = getSelectedType();
+
+ if (type == null) {
+ error = "One of the types must be selected (e.g. layout, values, etc.)";
+ }
+ }
+
+ // -- validate type API level
+ if (error == null) {
+ IAndroidTarget target = Sdk.getCurrent().getTarget(mProject);
+ int currentApiLevel = 1;
+ if (target != null) {
+ currentApiLevel = target.getApiVersionNumber();
+ }
+
+ TypeInfo type = getSelectedType();
+
+ if (type.getTargetApiLevel() > currentApiLevel) {
+ error = "The API level of the selected type (e.g. gadget, etc.) is not " +
+ "compatible with the API level of the project.";
+ }
+ }
+
+ // -- validate folder configuration
+ if (error == null) {
+ ConfigurationState state = mConfigSelector.getState();
+ if (state == ConfigurationState.INVALID_CONFIG) {
+ ResourceQualifier qual = mConfigSelector.getInvalidQualifier();
+ if (qual != null) {
+ error = String.format("The qualifier '%1$s' is invalid in the folder configuration.",
+ qual.getName());
+ }
+ } else if (state == ConfigurationState.REGION_WITHOUT_LANGUAGE) {
+ error = "The Region qualifier requires the Language qualifier.";
+ }
+ }
+
+ // -- validate generated path
+ if (error == null) {
+ String wsFolderPath = getWsFolderPath();
+ if (!wsFolderPath.startsWith(sResFolderAbs)) {
+ error = String.format("Target folder must start with %1$s.", sResFolderAbs);
+ }
+ }
+
+ // -- validate destination file doesn't exist
+ if (error == null) {
+ IFile file = getDestinationFile();
+ if (file != null && file.exists()) {
+ warning = "The destination file already exists";
+ }
+ }
+
+ // -- update UI & enable finish if there's no error
+ setPageComplete(error == null);
+ if (error != null) {
+ setMessage(error, WizardPage.ERROR);
+ } else if (warning != null) {
+ setMessage(warning, WizardPage.WARNING);
+ } else {
+ setErrorMessage(null);
+ setMessage(null);
+ }
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/NewXmlFileWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/NewXmlFileWizard.java
new file mode 100644
index 0000000..125102b
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/NewXmlFileWizard.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+
+package com.android.ide.eclipse.editors.wizards;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.editors.IconFactory;
+import com.android.ide.eclipse.editors.wizards.NewXmlFileCreationPage.TypeInfo;
+
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.wizard.Wizard;
+import org.eclipse.ui.INewWizard;
+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.ide.IDE;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * The "New Android XML File Wizard" provides the ability to create skeleton XML
+ * resources files for Android projects.
+ * <p/>
+ * The wizard has one page, {@link NewXmlFileCreationPage}, used to select the project,
+ * the resource folder, resource type and file name. It then creates the XML file.
+ */
+public class NewXmlFileWizard extends Wizard implements INewWizard {
+
+ private static final String PROJECT_LOGO_LARGE = "android_large"; //$NON-NLS-1$
+
+ protected static final String MAIN_PAGE_NAME = "newAndroidXmlFilePage"; //$NON-NLS-1$
+
+ private NewXmlFileCreationPage mMainPage;
+
+ public void init(IWorkbench workbench, IStructuredSelection selection) {
+ setHelpAvailable(false); // TODO have help
+ setWindowTitle("New Android XML File");
+ setImageDescriptor();
+
+ mMainPage = createMainPage();
+ mMainPage.setTitle("New Android XML File");
+ mMainPage.setDescription("Creates a new Android XML file.");
+ mMainPage.setInitialSelection(selection);
+ }
+
+ /**
+ * Creates the wizard page.
+ * <p/>
+ * Please do NOT override this method.
+ * <p/>
+ * This is protected so that it can be overridden by unit tests.
+ * However the contract of this class is private and NO ATTEMPT will be made
+ * to maintain compatibility between different versions of the plugin.
+ */
+ protected NewXmlFileCreationPage createMainPage() {
+ return new NewXmlFileCreationPage(MAIN_PAGE_NAME);
+ }
+
+ // -- Methods inherited from org.eclipse.jface.wizard.Wizard --
+ //
+ // The Wizard class implements most defaults and boilerplate code needed by
+ // IWizard
+
+ /**
+ * Adds pages to this wizard.
+ */
+ @Override
+ public void addPages() {
+ addPage(mMainPage);
+ }
+
+ /**
+ * Performs any actions appropriate in response to the user having pressed
+ * the Finish button, or refuse if finishing now is not permitted: here, it
+ * actually creates the workspace project and then switch to the Java
+ * perspective.
+ *
+ * @return True
+ */
+ @Override
+ public boolean performFinish() {
+ IFile file = createXmlFile();
+ if (file == null) {
+ return false;
+ } else {
+ // Open the file in an editor
+ IWorkbenchWindow win = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
+ if (win != null) {
+ IWorkbenchPage page = win.getActivePage();
+ if (page != null) {
+ try {
+ IDE.openEditor(page, file);
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, "Failed to create %1$s: missing type", //$NON-NLS-1$
+ file.getFullPath().toString());
+ }
+ }
+ }
+ return true;
+ }
+ }
+
+ // -- Custom Methods --
+
+ private IFile createXmlFile() {
+ IFile file = mMainPage.getDestinationFile();
+ String name = file.getFullPath().toString();
+ boolean need_delete = false;
+
+ if (file.exists()) {
+ if (!AdtPlugin.displayPrompt("New Android XML File",
+ String.format("Do you want to overwrite the file %1$s ?", name))) {
+ // abort if user selects cancel.
+ return null;
+ }
+ need_delete = true;
+ } else {
+ createWsParentDirectory(file.getParent());
+ }
+
+ TypeInfo type = mMainPage.getSelectedType();
+ if (type == null) {
+ // this is not expected to happen
+ AdtPlugin.log(IStatus.ERROR, "Failed to create %1$s: missing type", name); //$NON-NLS-1$
+ return null;
+ }
+ String xmlns = type.getXmlns();
+ String root = mMainPage.getRootElement();
+ if (root == null) {
+ // this is not expected to happen
+ AdtPlugin.log(IStatus.ERROR, "Failed to create %1$s: missing root element", //$NON-NLS-1$
+ file.toString());
+ return null;
+ }
+
+ StringBuilder sb = new StringBuilder("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"); //$NON-NLS-1$
+
+ sb.append('<').append(root);
+ if (xmlns != null) {
+ sb.append('\n').append(" xmlns:android=\"").append(xmlns).append("\""); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+
+ String attrs = type.getDefaultAttrs();
+ if (attrs != null) {
+ sb.append("\n "); //$NON-NLS-1$
+ sb.append(attrs.replace("\n", "\n ")); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+
+ sb.append(">\n"); //$NON-NLS-1$
+ sb.append("</").append(root).append(">\n"); //$NON-NLS-1$ //$NON-NLS-2$
+
+ String result = sb.toString();
+ String error = null;
+ try {
+ byte[] buf = result.getBytes("UTF8");
+ InputStream stream = new ByteArrayInputStream(buf);
+ if (need_delete) {
+ file.delete(IFile.KEEP_HISTORY | IFile.FORCE, null /*monitor*/);
+ }
+ file.create(stream, true /*force*/, null /*progres*/);
+ return file;
+ } catch (UnsupportedEncodingException e) {
+ error = e.getMessage();
+ } catch (CoreException e) {
+ error = e.getMessage();
+ }
+
+ error = String.format("Failed to generate %1$s: %2$s", name, error);
+ AdtPlugin.displayError("New Android XML File", error);
+ return null;
+ }
+
+ private boolean createWsParentDirectory(IContainer wsPath) {
+ if (wsPath.getType() == IContainer.FOLDER) {
+ if (wsPath == null || wsPath.exists()) {
+ return true;
+ }
+
+ IFolder folder = (IFolder) wsPath;
+ try {
+ if (createWsParentDirectory(wsPath.getParent())) {
+ folder.create(true /* force */, true /* local */, null /* monitor */);
+ return true;
+ }
+ } catch (CoreException e) {
+ e.printStackTrace();
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns an image descriptor for the wizard logo.
+ */
+ private void setImageDescriptor() {
+ ImageDescriptor desc = IconFactory.getInstance().getImageDescriptor(PROJECT_LOGO_LARGE);
+ setDefaultPageImageDescriptor(desc);
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ReferenceChooserDialog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ReferenceChooserDialog.java
new file mode 100644
index 0000000..6913ce0
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ReferenceChooserDialog.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.wizards;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.common.resources.IResourceRepository;
+import com.android.ide.eclipse.common.resources.ResourceItem;
+import com.android.ide.eclipse.common.resources.ResourceType;
+
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.dialogs.DialogSettings;
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.jface.dialogs.IDialogSettings;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.TreePath;
+import org.eclipse.jface.viewers.TreeSelection;
+import org.eclipse.jface.viewers.TreeViewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.ui.dialogs.FilteredTree;
+import org.eclipse.ui.dialogs.PatternFilter;
+import org.eclipse.ui.dialogs.SelectionStatusDialog;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A dialog to let the user choose a reference to a resource.
+ *
+ */
+public class ReferenceChooserDialog extends SelectionStatusDialog {
+
+ private static Pattern sResourcePattern = Pattern.compile("@(.*)/(.+)"); //$NON-NLS-1$
+ private static Pattern sInlineIdResourcePattern = Pattern.compile("@\\+id/(.+)"); //$NON-NLS-1$
+
+ private static IDialogSettings sDialogSettings = new DialogSettings("");
+
+ private IResourceRepository mResources;
+ private String mCurrentResource;
+
+ private FilteredTree mFilteredTree;
+
+ /**
+ * @param parent
+ */
+ public ReferenceChooserDialog(IResourceRepository resources, Shell parent) {
+ super(parent);
+
+ int shellStyle = getShellStyle();
+ setShellStyle(shellStyle | SWT.MAX | SWT.RESIZE);
+
+ setTitle("Reference Dialog");
+ setMessage(String.format("Choose a resource"));
+ mResources = resources;
+
+ setDialogBoundsSettings(sDialogSettings, getDialogBoundsStrategy());
+ }
+
+ public void setCurrentResource(String resource) {
+ mCurrentResource = resource;
+ }
+
+ public String getCurrentResource() {
+ return mCurrentResource;
+ }
+
+
+ /* (non-Javadoc)
+ * @see org.eclipse.ui.dialogs.SelectionStatusDialog#computeResult()
+ */
+ @Override
+ protected void computeResult() {
+ // get the selection
+ TreePath treeSelection = getSelection();
+ if (treeSelection != null) {
+ if (treeSelection.getSegmentCount() == 2) {
+ // get the resource type and the resource item
+ ResourceType resourceType = (ResourceType)treeSelection.getFirstSegment();
+ ResourceItem resourceItem = (ResourceItem)treeSelection.getLastSegment();
+
+ mCurrentResource = resourceType.getXmlString(resourceItem, false /* system */);
+ }
+ }
+ }
+
+ @Override
+ protected Control createDialogArea(Composite parent) {
+ Composite top = (Composite)super.createDialogArea(parent);
+
+ // create the standard message area
+ createMessageArea(top);
+
+ // create the filtered tree
+ createFilteredTree(top);
+
+ // setup the initial selection
+ setupInitialSelection();
+
+ return top;
+ }
+
+ private void createFilteredTree(Composite parent) {
+ mFilteredTree = new FilteredTree(parent, SWT.BORDER | SWT.SINGLE | SWT.FULL_SELECTION,
+ new PatternFilter());
+
+ GridData data = new GridData();
+ data.widthHint = convertWidthInCharsToPixels(60);
+ data.heightHint = convertHeightInCharsToPixels(18);
+ data.grabExcessVerticalSpace = true;
+ data.grabExcessHorizontalSpace = true;
+ data.horizontalAlignment = GridData.FILL;
+ data.verticalAlignment = GridData.FILL;
+ mFilteredTree.setLayoutData(data);
+ mFilteredTree.setFont(parent.getFont());
+
+ TreeViewer treeViewer = mFilteredTree.getViewer();
+ Tree tree = treeViewer.getTree();
+
+ tree.addSelectionListener(new SelectionListener() {
+ public void widgetDefaultSelected(SelectionEvent e) {
+ handleDoubleClick();
+ }
+
+ public void widgetSelected(SelectionEvent e) {
+ handleSelection();
+ }
+ });
+
+ treeViewer.setLabelProvider(new ResourceLabelProvider());
+ treeViewer.setContentProvider(new ResourceContentProvider(false /* fullLevels */));
+ treeViewer.setInput(mResources);
+ }
+
+ protected void handleSelection() {
+ validateCurrentSelection();
+ }
+
+ protected void handleDoubleClick() {
+ if (validateCurrentSelection()) {
+ buttonPressed(IDialogConstants.OK_ID);
+ }
+ }
+
+ /**
+ * Returns the selected item in the tree as a {@link TreePath} object.
+ * @return the <code>TreePath</code> object or <code>null</code> if there was no selection.
+ */
+ private TreePath getSelection() {
+ ISelection selection = mFilteredTree.getViewer().getSelection();
+ if (selection instanceof TreeSelection) {
+ TreeSelection treeSelection = (TreeSelection)selection;
+ TreePath[] treePaths = treeSelection.getPaths();
+
+ // the selection mode is SWT.SINGLE, so we just get the first one.
+ if (treePaths.length > 0) {
+ return treePaths[0];
+ }
+ }
+
+ return null;
+ }
+
+ private boolean validateCurrentSelection() {
+ TreePath treeSelection = getSelection();
+
+ IStatus status;
+ if (treeSelection != null) {
+ if (treeSelection.getSegmentCount() == 2) {
+ status = new Status(IStatus.OK, AdtPlugin.PLUGIN_ID,
+ IStatus.OK, "", //$NON-NLS-1$
+ null);
+ } else {
+ status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ IStatus.ERROR, "You must select a Resource Item",
+ null);
+ }
+ } else {
+ status = new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ IStatus.ERROR, "", //$NON-NLS-1$
+ null);
+ }
+
+ updateStatus(status);
+
+ return status.isOK();
+ }
+
+ /**
+ * Sets up the initial selection.
+ * <p/>
+ * This parses {@link #mCurrentResource} to find out the resource type and the resource name.
+ */
+ private void setupInitialSelection() {
+ // checks the inline id pattern first as it's more restrictive than the other one.
+ Matcher m = sInlineIdResourcePattern.matcher(mCurrentResource);
+ if (m.matches()) {
+ // get the matching name
+ String resourceName = m.group(1);
+
+ // setup initial selection
+ setupInitialSelection(ResourceType.ID, resourceName);
+ } else {
+ // attempts the inline id pattern
+ m = sResourcePattern.matcher(mCurrentResource);
+ if (m.matches()) {
+ // get the resource type.
+ ResourceType resourceType = ResourceType.getEnum(m.group(1));
+ if (resourceType != null) {
+ // get the matching name
+ String resourceName = m.group(2);
+
+ // setup initial selection
+ setupInitialSelection(resourceType, resourceName);
+ }
+ }
+ }
+ }
+
+ /**
+ * Sets up the initial selection based on a {@link ResourceType} and a resource name.
+ * @param resourceType the resource type.
+ * @param resourceName the resource name.
+ */
+ private void setupInitialSelection(ResourceType resourceType, String resourceName) {
+ // get all the resources of this type
+ ResourceItem[] resourceItems = mResources.getResources(resourceType);
+
+ for (ResourceItem resourceItem : resourceItems) {
+ if (resourceName.equals(resourceItem.getName())) {
+ // name of the resource match, we select it,
+ TreePath treePath = new TreePath(new Object[] { resourceType, resourceItem });
+ mFilteredTree.getViewer().setSelection(new TreeSelection(treePath));
+
+ // and we're done.
+ return;
+ }
+ }
+
+ // if we get here, the resource type is valid, but the resource is missing.
+ // we select and expand the resource type element.
+ TreePath treePath = new TreePath(new Object[] { resourceType });
+ mFilteredTree.getViewer().setSelection(new TreeSelection(treePath));
+ mFilteredTree.getViewer().setExpandedState(resourceType, true /* expanded */);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ResourceChooser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ResourceChooser.java
new file mode 100644
index 0000000..60a627b
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ResourceChooser.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.wizards;
+
+import com.android.ide.eclipse.common.resources.IResourceRepository;
+import com.android.ide.eclipse.common.resources.ResourceItem;
+import com.android.ide.eclipse.common.resources.ResourceType;
+
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.dialogs.AbstractElementListSelectionDialog;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A dialog to let the user select a resource based on a resource type.
+ */
+public class ResourceChooser extends AbstractElementListSelectionDialog {
+
+ private Pattern mProjectResourcePattern;
+
+ private ResourceType mResourceType;
+
+ private IResourceRepository mProjectResources;
+
+ // TODO: enable when we can display the system resources.
+ // private Pattern mSystemResourcePattern;
+ // private IResourceRepository mSystemResources;
+ // private Button mProjectButton;
+ // private Button mSystemButton;
+
+ private String mCurrentResource;
+
+ /**
+ * Creates a Resource Chooser dialog.
+ * @param type The type of the resource to choose
+ * @param project The repository for the project
+ * @param system The System resource repository
+ * @param parent the parent shell
+ */
+ public ResourceChooser(ResourceType type, IResourceRepository project,
+ IResourceRepository system, Shell parent) {
+ super(parent, new ResourceLabelProvider());
+
+ mResourceType = type;
+ mProjectResources = project;
+ // TODO: enable when we can display the system resources.
+ // mSystemResources = system;
+
+ mProjectResourcePattern = Pattern.compile(
+ "@" + mResourceType.getName() + "/(.+)"); //$NON-NLS-1$ //$NON-NLS-2$
+ // TODO: enable when we can display the system resources.
+ // mSystemResourcePattern = Pattern.compile(
+ // "@android:" + mResourceType.getName() + "/(.+)"); //$NON-NLS-1$ //$NON-NLS-2$
+
+ setTitle("Resource Chooser");
+ setMessage(String.format("Choose a %1$s resource",
+ mResourceType.getDisplayName().toLowerCase()));
+ }
+
+ public void setCurrentResource(String resource) {
+ mCurrentResource = resource;
+ }
+
+ public String getCurrentResource() {
+ return mCurrentResource;
+ }
+
+ @Override
+ protected void computeResult() {
+ Object[] elements = getSelectedElements();
+ if (elements.length == 1 && elements[0] instanceof ResourceItem) {
+ ResourceItem item = (ResourceItem)elements[0];
+
+ mCurrentResource = mResourceType.getXmlString(item,
+ // TODO: enable when we can display the system resources.
+ false /*mSystemButton.getSelection()*/);
+ }
+ }
+
+ @Override
+ protected Control createDialogArea(Composite parent) {
+ Composite top = (Composite)super.createDialogArea(parent);
+
+ createMessageArea(top);
+
+ // TODO: enable when we can display the system resources.
+ // createButtons(top);
+
+ createFilterText(top);
+ createFilteredList(top);
+
+ setupResourceListAndCurrent();
+
+ return top;
+ }
+
+ /**
+ * Creates the radio button to switch between project and system resources.
+ * @param top the parent composite
+ */
+ /* TODO: enable when we can display the system resources.
+ private void createButtons(Composite top) {
+ mProjectButton = new Button(top, SWT.RADIO);
+ mProjectButton.setText("Project Resources");
+ mProjectButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ super.widgetSelected(e);
+ setListElements(mProjectResources.getResources(mResourceType));
+ }
+ });
+ mSystemButton = new Button(top, SWT.RADIO);
+ mSystemButton.setText("System Resources");
+ mSystemButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ super.widgetSelected(e);
+ setListElements(mSystemResources.getResources(mResourceType));
+ }
+ });
+ }
+ */
+
+ /**
+ * Setups the current list based on the current resource.
+ */
+ private void setupResourceListAndCurrent() {
+ if (setupInitialSelection(mProjectResourcePattern, mProjectResources) == false) {
+ // if we couldn't understand the current value, we default to the project resources
+ ResourceItem[] items = mProjectResources.getResources(mResourceType);
+ setListElements(items);
+ }
+ /*
+ * TODO: enable when we can display the system resources.
+ if (setupInitialSelection(mProjectResourcePattern, mProjectResources) == false) {
+ if (setupInitialSelection(mSystemResourcePattern, mSystemResources) == false) {
+ // if we couldn't understand the current value, we default to the project resources
+ IResourceItem[] items = mProjectResources.getResources(mResourceType);
+ setListElements(items);
+ mProjectButton.setSelection(true);
+ } else {
+ mSystemButton.setSelection(true);
+ }
+ } else {
+ mProjectButton.setSelection(true);
+ }*/
+ }
+
+ /**
+ * Attempts to setup the list of element from a repository if the current resource
+ * matches the provided pattern.
+ * @param pattern the pattern to test the current value
+ * @param repository the repository to use if the pattern matches.
+ * @return true if success.
+ */
+ private boolean setupInitialSelection(Pattern pattern, IResourceRepository repository) {
+ Matcher m = pattern.matcher(mCurrentResource);
+ if (m.matches()) {
+ // we have a project resource, let's setup the list
+ ResourceItem[] items = repository.getResources(mResourceType);
+ setListElements(items);
+
+ // and let's look for the item we found
+ String name = m.group(1);
+
+ for (ResourceItem item : items) {
+ if (name.equals(item.getName())) {
+ setSelection(new Object[] { item });
+ break;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ResourceContentProvider.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ResourceContentProvider.java
new file mode 100644
index 0000000..7c6a539
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ResourceContentProvider.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.wizards;
+
+import com.android.ide.eclipse.common.resources.IResourceRepository;
+import com.android.ide.eclipse.common.resources.ResourceItem;
+import com.android.ide.eclipse.common.resources.ResourceType;
+import com.android.ide.eclipse.editors.resources.manager.ConfigurableResourceItem;
+import com.android.ide.eclipse.editors.resources.manager.ResourceFile;
+
+import org.eclipse.jface.viewers.ITreeContentProvider;
+import org.eclipse.jface.viewers.Viewer;
+
+/**
+ * Content provider for the Resource Explorer TreeView.
+ * Each level of the tree is represented by a different class.
+ * <ul>
+ * <li>{@link ResourceType}. This represents the list of existing Resource Type present
+ * in the resources. This can be matched to the subclasses inside the class <code>R</code>
+ * </li>
+ * <ul>
+ * <li>{@link ResourceItem}. This represents one resource (which can existing in various alternate
+ * versions). This is similar to the resource Ids defined as <code>R.sometype.id</code>.
+ * </li>
+ * <ul>
+ * <li>{@link ResourceFile}. (optional) This represents a particular version of the
+ * {@link ResourceItem}. It is displayed as a list of resource qualifier.
+ * </li>
+ * </ul>
+ * </ul>
+ * </ul>
+ *
+ * @see ResourceLabelProvider
+ */
+public class ResourceContentProvider implements ITreeContentProvider {
+
+ /**
+ * The current ProjectResources being displayed.
+ */
+ private IResourceRepository mResources;
+
+ private boolean mFullLevels;
+
+ /**
+ * Constructs a new content providers for resource display.
+ * @param fullLevels if <code>true</code> the content provider will suppport all 3 levels. If
+ * <code>false</code>, only two levels are provided.
+ */
+ public ResourceContentProvider(boolean fullLevels) {
+ mFullLevels = fullLevels;
+ }
+
+ public Object[] getChildren(Object parentElement) {
+ if (parentElement instanceof ResourceType) {
+ return mResources.getResources((ResourceType)parentElement);
+ } else if (mFullLevels && parentElement instanceof ConfigurableResourceItem) {
+ return ((ConfigurableResourceItem)parentElement).getSourceFileArray();
+ }
+ return null;
+ }
+
+ public Object getParent(Object element) {
+ // pass
+ return null;
+ }
+
+ public boolean hasChildren(Object element) {
+ if (element instanceof ResourceType) {
+ return mResources.hasResources((ResourceType)element);
+ } else if (mFullLevels && element instanceof ConfigurableResourceItem) {
+ return ((ConfigurableResourceItem)element).hasAlternates();
+ }
+ return false;
+ }
+
+ public Object[] getElements(Object inputElement) {
+ if (inputElement instanceof IResourceRepository) {
+ if ((IResourceRepository)inputElement == mResources) {
+ // get the top level resources.
+ return mResources.getAvailableResourceTypes();
+ }
+ }
+
+ return new Object[0];
+ }
+
+ public void dispose() {
+ // pass
+ }
+
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ if (newInput instanceof IResourceRepository) {
+ mResources = (IResourceRepository)newInput;
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ResourceLabelProvider.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ResourceLabelProvider.java
new file mode 100644
index 0000000..024d084
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/wizards/ResourceLabelProvider.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.wizards;
+
+import com.android.ide.eclipse.common.resources.IIdResourceItem;
+import com.android.ide.eclipse.common.resources.ResourceItem;
+import com.android.ide.eclipse.common.resources.ResourceType;
+import com.android.ide.eclipse.editors.resources.manager.ConfigurableResourceItem;
+import com.android.ide.eclipse.editors.resources.manager.IdResourceItem;
+import com.android.ide.eclipse.editors.resources.manager.ResourceFile;
+
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.ui.ISharedImages;
+import org.eclipse.ui.PlatformUI;
+
+/**
+ * Label provider for the Resource Explorer TreeView.
+ * Each level of the tree is represented by a different class.
+ * <ul>
+ * <li>{@link ResourceType}. This represents the list of existing Resource Type present
+ * in the resources. This can be matched to the subclasses inside the class <code>R</code>
+ * </li>
+ * <ul>
+ * <li>{@link ResourceItem}. This represents one resource. The actual type can be
+ * {@link ConfigurableResourceItem} (which can exist in various alternate versions),
+ * or {@link IdResourceItem}.
+ * This is similar to the resource Ids defined as <code>R.sometype.id</code>.
+ * </li>
+ * <ul>
+ * <li>{@link ResourceFile}. This represents a particular version of the {@link ResourceItem}.
+ * It is displayed as a list of resource qualifier.
+ * </li>
+ * </ul>
+ * </ul>
+ * </ul>
+ *
+ * @see ResourceContentProvider
+ */
+public class ResourceLabelProvider implements ILabelProvider, ITableLabelProvider {
+ private Image mWarningImage;
+
+ public ResourceLabelProvider() {
+ mWarningImage = PlatformUI.getWorkbench().getSharedImages().getImageDescriptor(
+ ISharedImages.IMG_OBJS_WARN_TSK).createImage();
+ }
+
+ /**
+ * @see #getColumnImage(Object, int)
+ */
+ public Image getImage(Object element) {
+ // pass
+ return null;
+ }
+
+ /**
+ * @see #getColumnText(Object, int)
+ */
+ public String getText(Object element) {
+ return getColumnText(element, 0);
+ }
+
+ public void addListener(ILabelProviderListener listener) {
+ // pass
+ }
+
+ public void dispose() {
+ mWarningImage.dispose();
+ }
+
+ public boolean isLabelProperty(Object element, String property) {
+ return false;
+ }
+
+ public void removeListener(ILabelProviderListener listener) {
+ // pass
+ }
+
+ public Image getColumnImage(Object element, int columnIndex) {
+ if (columnIndex == 1) {
+ if (element instanceof ConfigurableResourceItem) {
+ ConfigurableResourceItem item = (ConfigurableResourceItem)element;
+ if (item.hasDefault() == false) {
+ return mWarningImage;
+ }
+ }
+ }
+ return null;
+ }
+
+ public String getColumnText(Object element, int columnIndex) {
+ switch (columnIndex) {
+ case 0:
+ if (element instanceof ResourceType) {
+ return ((ResourceType)element).getDisplayName();
+ } else if (element instanceof ResourceItem) {
+ return ((ResourceItem)element).getName();
+ } else if (element instanceof ResourceFile) {
+ return ((ResourceFile)element).getFolder().getConfiguration().toDisplayString();
+ }
+ break;
+ case 1:
+ if (element instanceof ConfigurableResourceItem) {
+ ConfigurableResourceItem item = (ConfigurableResourceItem)element;
+ int count = item.getAlternateCount();
+ if (count > 0) {
+ if (item.hasDefault()) {
+ count++;
+ }
+ return String.format("%1$d version(s)", count);
+ }
+ } else if (element instanceof IIdResourceItem) {
+ IIdResourceItem idResource = (IIdResourceItem)element;
+ if (idResource.isDeclaredInline()) {
+ return "Declared inline";
+ }
+ }
+ return null;
+ }
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/XmlContentAssist.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/XmlContentAssist.java
new file mode 100644
index 0000000..f28b523
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/XmlContentAssist.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.xml;
+
+import com.android.ide.eclipse.adt.sdk.AndroidTargetData;
+import com.android.ide.eclipse.editors.AndroidContentAssist;
+
+/**
+ * Content Assist Processor for /res/xml XML files
+ */
+class XmlContentAssist extends AndroidContentAssist {
+
+ /**
+ * Constructor for LayoutContentAssist
+ */
+ public XmlContentAssist() {
+ super(AndroidTargetData.DESCRIPTOR_XML);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/XmlEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/XmlEditor.java
new file mode 100644
index 0000000..d7f6119
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/XmlEditor.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.xml;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.sdk.Sdk;
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.editors.AndroidEditor;
+import com.android.ide.eclipse.editors.FirstElementParser;
+import com.android.ide.eclipse.editors.descriptors.DocumentDescriptor;
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.editors.uimodel.UiDocumentNode;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.SdkConstants;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.part.FileEditorInput;
+import org.w3c.dom.Document;
+
+/**
+ * Multi-page form editor for /res/xml XML files.
+ */
+public class XmlEditor extends AndroidEditor {
+
+ public static final String ID = AndroidConstants.EDITORS_NAMESPACE + ".xml.XmlEditor"; //$NON-NLS-1$
+
+ /** Root node of the UI element hierarchy */
+ private UiDocumentNode mUiRootNode;
+
+ /**
+ * Creates the form editor for resources XML files.
+ */
+ public XmlEditor() {
+ super();
+ }
+
+ /**
+ * Returns the root node of the UI element hierarchy, which here
+ * is the document node.
+ */
+ @Override
+ public UiDocumentNode getUiRootNode() {
+ return mUiRootNode;
+ }
+
+ // ---- Static ----
+
+ /**
+ * Indicates if this is a file that this {@link XmlEditor} can handle.
+ * <p/>
+ * The {@link XmlEditor} can handle XML files that have a <searchable> or
+ * <Preferences> root XML element with the adequate xmlns:android attribute.
+ *
+ * @return True if the {@link XmlEditor} can handle that file.
+ */
+ public static boolean canHandleFile(IFile file) {
+ // we need the target of the file's project to access the descriptors.
+ IProject project = file.getProject();
+ IAndroidTarget target = Sdk.getCurrent().getTarget(project);
+ if (target != null) {
+ AndroidTargetData data = Sdk.getCurrent().getTargetData(target);
+
+ FirstElementParser.Result result = FirstElementParser.parse(
+ file.getLocation().toOSString(),
+ SdkConstants.NS_RESOURCES);
+
+ if (result != null) {
+ String name = result.getElement();
+ if (name != null && result.getXmlnsPrefix() != null) {
+ DocumentDescriptor desc = data.getXmlDescriptors().getDescriptor();
+ for (ElementDescriptor elem : desc.getChildren()) {
+ if (elem.getXmlName().equals(name)) {
+ // This is an element that this document can handle
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ // ---- Base Class Overrides ----
+
+ /**
+ * Returns whether the "save as" operation is supported by this editor.
+ * <p/>
+ * Save-As is a valid operation for the ManifestEditor since it acts on a
+ * single source file.
+ *
+ * @see IEditorPart
+ */
+ @Override
+ public boolean isSaveAsAllowed() {
+ return true;
+ }
+
+ /**
+ * Create the various form pages.
+ */
+ @Override
+ protected void createFormPages() {
+ try {
+ addPage(new XmlTreePage(this));
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, "Error creating nested page"); //$NON-NLS-1$
+ }
+
+ }
+
+ /* (non-java doc)
+ * Change the tab/title name to include the project name.
+ */
+ @Override
+ protected void setInput(IEditorInput input) {
+ super.setInput(input);
+ if (input instanceof FileEditorInput) {
+ FileEditorInput fileInput = (FileEditorInput) input;
+ IFile file = fileInput.getFile();
+ setPartName(String.format("%1$s", file.getName()));
+ }
+ }
+
+ /**
+ * Processes the new XML Model, which XML root node is given.
+ *
+ * @param xml_doc The XML document, if available, or null if none exists.
+ */
+ @Override
+ protected void xmlModelChanged(Document xml_doc) {
+ // init the ui root on demand
+ initUiRootNode(false /*force*/);
+
+ mUiRootNode.loadFromXmlNode(xml_doc);
+
+ super.xmlModelChanged(xml_doc);
+ }
+
+ /**
+ * Creates the initial UI Root Node, including the known mandatory elements.
+ * @param force if true, a new UiRootNode is recreated even if it already exists.
+ */
+ @Override
+ protected void initUiRootNode(boolean force) {
+ // The root UI node is always created, even if there's no corresponding XML node.
+ if (mUiRootNode == null || force) {
+ Document doc = null;
+ if (mUiRootNode != null) {
+ doc = mUiRootNode.getXmlDocument();
+ }
+
+ // get the target data from the opened file (and its project)
+ AndroidTargetData data = getTargetData();
+
+ DocumentDescriptor desc;
+ if (data == null) {
+ desc = new DocumentDescriptor("temp", null /*children*/);
+ } else {
+ desc = data.getXmlDescriptors().getDescriptor();
+ }
+
+ mUiRootNode = (UiDocumentNode) desc.createUiNode();
+ mUiRootNode.setEditor(this);
+
+ onDescriptorsChanged(doc);
+ }
+ }
+
+ // ---- Local Methods ----
+
+ /**
+ * Reloads the UI manifest node from the XML, and calls the pages to update.
+ */
+ private void onDescriptorsChanged(Document document) {
+ if (document != null) {
+ mUiRootNode.loadFromXmlNode(document);
+ } else {
+ mUiRootNode.reloadFromXmlNode(mUiRootNode.getXmlNode());
+ }
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/XmlSourceViewerConfig.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/XmlSourceViewerConfig.java
new file mode 100644
index 0000000..d25c812
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/XmlSourceViewerConfig.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.xml;
+
+
+import com.android.ide.eclipse.editors.AndroidSourceViewerConfig;
+
+/**
+ * Source Viewer Configuration that calls in XmlContentAssist.
+ */
+public class XmlSourceViewerConfig extends AndroidSourceViewerConfig {
+
+ public XmlSourceViewerConfig() {
+ super(new XmlContentAssist());
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/XmlTreePage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/XmlTreePage.java
new file mode 100644
index 0000000..91ce6dd
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/XmlTreePage.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.xml;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.editors.ui.tree.UiTreeBlock;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+import org.eclipse.ui.forms.IManagedForm;
+import org.eclipse.ui.forms.editor.FormPage;
+import org.eclipse.ui.forms.widgets.ScrolledForm;
+
+/**
+ * Page for the xml form editor.
+ */
+public final class XmlTreePage extends FormPage {
+ /** Page id used for switching tabs programmatically */
+ public final static String PAGE_ID = "xml_tree_page"; //$NON-NLS-1$
+
+ /** Container editor */
+ XmlEditor mEditor;
+
+ public XmlTreePage(XmlEditor editor) {
+ super(editor, PAGE_ID, "Structure"); // tab's label, keep it short
+ mEditor = editor;
+ }
+
+ /**
+ * Creates the content in the form hosted in this page.
+ *
+ * @param managedForm the form hosted in this page.
+ */
+ @Override
+ protected void createFormContent(IManagedForm managedForm) {
+ super.createFormContent(managedForm);
+ ScrolledForm form = managedForm.getForm();
+ form.setText("Android Xml");
+ form.setImage(AdtPlugin.getAndroidLogo());
+
+ UiElementNode rootNode = mEditor.getUiRootNode();
+ UiTreeBlock block = new UiTreeBlock(mEditor, rootNode,
+ true /* autoCreateRoot */,
+ null /* no element filters */,
+ "Xml Elements",
+ "List of all xml elements in this XML file.");
+ block.createContent(managedForm);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/descriptors/XmlDescriptors.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/descriptors/XmlDescriptors.java
new file mode 100644
index 0000000..7929b5a
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/editors/xml/descriptors/XmlDescriptors.java
@@ -0,0 +1,364 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.xml.descriptors;
+
+import com.android.ide.eclipse.common.AndroidConstants;
+import com.android.ide.eclipse.common.resources.DeclareStyleableInfo;
+import com.android.ide.eclipse.common.resources.ViewClassInfo;
+import com.android.ide.eclipse.common.resources.DeclareStyleableInfo.AttributeInfo;
+import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.editors.descriptors.DocumentDescriptor;
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.editors.descriptors.IDescriptorProvider;
+import com.android.ide.eclipse.editors.descriptors.SeparatorAttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.XmlnsAttributeDescriptor;
+import com.android.ide.eclipse.editors.layout.descriptors.ViewElementDescriptor;
+import com.android.sdklib.SdkConstants;
+
+import java.util.ArrayList;
+import java.util.Map;
+
+
+/**
+ * Description of the /res/xml structure.
+ * Currently supports the <searchable> and <preferences> root nodes.
+ */
+public final class XmlDescriptors implements IDescriptorProvider {
+
+ // Public attributes names, attributes descriptors and elements descriptors referenced
+ // elsewhere.
+ public static final String PREF_KEY_ATTR = "key"; //$NON-NLS-1$
+
+ /** The root document descriptor for both searchable and preferences. */
+ private DocumentDescriptor mDescriptor = new DocumentDescriptor("xml_doc", null /* children */); //$NON-NLS-1$
+
+ /** The root document descriptor for searchable. */
+ private DocumentDescriptor mSearchDescriptor = new DocumentDescriptor("xml_doc", null /* children */); //$NON-NLS-1$
+
+ /** The root document descriptor for preferences. */
+ private DocumentDescriptor mPrefDescriptor = new DocumentDescriptor("xml_doc", null /* children */); //$NON-NLS-1$
+
+ /** The root document descriptor for gadget provider. */
+ private DocumentDescriptor mGadgetDescriptor = new DocumentDescriptor("xml_doc", null /* children */); //$NON-NLS-1$
+
+ /** @return the root descriptor for both searchable and preferences. */
+ public DocumentDescriptor getDescriptor() {
+ return mDescriptor;
+ }
+
+ public ElementDescriptor[] getRootElementDescriptors() {
+ return mDescriptor.getChildren();
+ }
+
+ /** @return the root descriptor for searchable. */
+ public DocumentDescriptor getSearchableDescriptor() {
+ return mSearchDescriptor;
+ }
+
+ /** @return the root descriptor for preferences. */
+ public DocumentDescriptor getPreferencesDescriptor() {
+ return mPrefDescriptor;
+ }
+
+ /** @return the root descriptor for gadget providers. */
+ public DocumentDescriptor getGadgetDescriptor() {
+ return mGadgetDescriptor;
+ }
+
+ public IDescriptorProvider getSearchableProvider() {
+ return new IDescriptorProvider() {
+ public ElementDescriptor getDescriptor() {
+ return mSearchDescriptor;
+ }
+
+ public ElementDescriptor[] getRootElementDescriptors() {
+ return mSearchDescriptor.getChildren();
+ }
+ };
+ }
+
+ public IDescriptorProvider getPreferencesProvider() {
+ return new IDescriptorProvider() {
+ public ElementDescriptor getDescriptor() {
+ return mPrefDescriptor;
+ }
+
+ public ElementDescriptor[] getRootElementDescriptors() {
+ return mPrefDescriptor.getChildren();
+ }
+ };
+ }
+
+ public IDescriptorProvider getGadgetProvider() {
+ return new IDescriptorProvider() {
+ public ElementDescriptor getDescriptor() {
+ return mGadgetDescriptor;
+ }
+
+ public ElementDescriptor[] getRootElementDescriptors() {
+ return mGadgetDescriptor.getChildren();
+ }
+ };
+ }
+
+ /**
+ * Updates the document descriptor.
+ * <p/>
+ * It first computes the new children of the descriptor and then updates them
+ * all at once.
+ *
+ * @param searchableStyleMap The map style=>attributes for <searchable> from the attrs.xml file
+ * @param gadgetStyleMap The map style=>attributes for <gadget-provider> from the attrs.xml file
+ * @param prefs The list of non-group preference descriptions
+ * @param prefGroups The list of preference group descriptions
+ */
+ public synchronized void updateDescriptors(
+ Map<String, DeclareStyleableInfo> searchableStyleMap,
+ Map<String, DeclareStyleableInfo> gadgetStyleMap,
+ ViewClassInfo[] prefs, ViewClassInfo[] prefGroups) {
+
+ XmlnsAttributeDescriptor xmlns = new XmlnsAttributeDescriptor(
+ "android", //$NON-NLS-1$
+ SdkConstants.NS_RESOURCES);
+
+ ElementDescriptor searchable = createSearchable(searchableStyleMap, xmlns);
+ ElementDescriptor gadget = createGadgetProviderInfo(gadgetStyleMap, xmlns);
+ ElementDescriptor preferences = createPreference(prefs, prefGroups, xmlns);
+ ArrayList<ElementDescriptor> list = new ArrayList<ElementDescriptor>();
+ if (searchable != null) {
+ list.add(searchable);
+ mSearchDescriptor.setChildren(new ElementDescriptor[]{ searchable });
+ }
+ if (gadget != null) {
+ list.add(gadget);
+ mGadgetDescriptor.setChildren(new ElementDescriptor[]{ gadget });
+ }
+ if (preferences != null) {
+ list.add(preferences);
+ mPrefDescriptor.setChildren(new ElementDescriptor[]{ preferences });
+ }
+
+ if (list.size() > 0) {
+ mDescriptor.setChildren(list.toArray(new ElementDescriptor[list.size()]));
+ }
+ }
+
+ //-------------------------
+ // Creation of <searchable>
+ //-------------------------
+
+ /**
+ * Returns the new ElementDescriptor for <searchable>
+ */
+ private ElementDescriptor createSearchable(
+ Map<String, DeclareStyleableInfo> searchableStyleMap,
+ XmlnsAttributeDescriptor xmlns) {
+
+ ElementDescriptor action_key = createElement(searchableStyleMap,
+ "SearchableActionKey", //$NON-NLS-1$ styleName
+ "actionkey", //$NON-NLS-1$ xmlName
+ "Action Key", // uiName
+ null, // sdk url
+ null, // extraAttribute
+ null, // childrenElements
+ false /* mandatory */ );
+
+ ElementDescriptor searchable = createElement(searchableStyleMap,
+ "Searchable", //$NON-NLS-1$ styleName
+ "searchable", //$NON-NLS-1$ xmlName
+ "Searchable", // uiName
+ null, // sdk url
+ xmlns, // extraAttribute
+ new ElementDescriptor[] { action_key }, // childrenElements
+ false /* mandatory */ );
+ return searchable;
+ }
+
+ /**
+ * Returns the new ElementDescriptor for <gadget-provider>
+ */
+ private ElementDescriptor createGadgetProviderInfo(
+ Map<String, DeclareStyleableInfo> gadgetStyleMap,
+ XmlnsAttributeDescriptor xmlns) {
+
+ if (gadgetStyleMap == null) {
+ return null;
+ }
+
+ ElementDescriptor gadget = createElement(gadgetStyleMap,
+ "GadgetProviderInfo", //$NON-NLS-1$ styleName
+ "gadget-provider", //$NON-NLS-1$ xmlName
+ "Gadget Provider", // uiName
+ null, // sdk url
+ xmlns, // extraAttribute
+ null, // childrenElements
+ false /* mandatory */ );
+ return gadget;
+ }
+
+ /**
+ * Returns a new ElementDescriptor constructed from the information given here
+ * and the javadoc & attributes extracted from the style map if any.
+ */
+ private ElementDescriptor createElement(
+ Map<String, DeclareStyleableInfo> styleMap, String styleName,
+ String xmlName, String uiName, String sdkUrl,
+ AttributeDescriptor extraAttribute,
+ ElementDescriptor[] childrenElements, boolean mandatory) {
+
+ ElementDescriptor element = new ElementDescriptor(xmlName, uiName, null, sdkUrl,
+ null, childrenElements, mandatory);
+
+ return updateElement(element, styleMap, styleName, extraAttribute);
+ }
+
+ /**
+ * Updates an ElementDescriptor with the javadoc & attributes extracted from the style
+ * map if any.
+ */
+ private ElementDescriptor updateElement(ElementDescriptor element,
+ Map<String, DeclareStyleableInfo> styleMap,
+ String styleName,
+ AttributeDescriptor extraAttribute) {
+ ArrayList<AttributeDescriptor> descs = new ArrayList<AttributeDescriptor>();
+
+ DeclareStyleableInfo style = styleMap != null ? styleMap.get(styleName) : null;
+ if (style != null) {
+ DescriptorsUtils.appendAttributes(descs,
+ null, // elementName
+ SdkConstants.NS_RESOURCES,
+ style.getAttributes(),
+ null, // requiredAttributes
+ null); // overrides
+ element.setTooltip(style.getJavaDoc());
+ }
+
+ if (extraAttribute != null) {
+ descs.add(extraAttribute);
+ }
+
+ element.setAttributes(descs.toArray(new AttributeDescriptor[descs.size()]));
+ return element;
+ }
+
+ //--------------------------
+ // Creation of <Preferences>
+ //--------------------------
+
+ /**
+ * Returns the new ElementDescriptor for <Preferences>
+ */
+ private ElementDescriptor createPreference(ViewClassInfo[] prefs,
+ ViewClassInfo[] prefGroups, XmlnsAttributeDescriptor xmlns) {
+
+ ArrayList<ElementDescriptor> newPrefs = new ArrayList<ElementDescriptor>();
+ if (prefs != null) {
+ for (ViewClassInfo info : prefs) {
+ ElementDescriptor desc = convertPref(info);
+ newPrefs.add(desc);
+ }
+ }
+
+ ElementDescriptor topPreferences = null;
+
+ ArrayList<ElementDescriptor> newGroups = new ArrayList<ElementDescriptor>();
+ if (prefGroups != null) {
+ for (ViewClassInfo info : prefGroups) {
+ ElementDescriptor desc = convertPref(info);
+ newGroups.add(desc);
+
+ if (info.getCanonicalClassName() == AndroidConstants.CLASS_PREFERENCES) {
+ topPreferences = desc;
+ }
+ }
+ }
+
+ ArrayList<ElementDescriptor> everything = new ArrayList<ElementDescriptor>();
+ everything.addAll(newGroups);
+ everything.addAll(newPrefs);
+ ElementDescriptor[] newArray = everything.toArray(new ElementDescriptor[everything.size()]);
+
+ // Link all groups to everything else here.. recursively
+ for (ElementDescriptor layoutDesc : newGroups) {
+ layoutDesc.setChildren(newArray);
+ }
+
+ // The "top" element to be returned corresponds to the class "Preferences".
+ // Its descriptor has already been created. However the root one also needs
+ // the hidden xmlns:android definition..
+ if (topPreferences != null) {
+ AttributeDescriptor[] attrs = topPreferences.getAttributes();
+ AttributeDescriptor[] newAttrs = new AttributeDescriptor[attrs.length + 1];
+ System.arraycopy(attrs, 0, newAttrs, 0, attrs.length);
+ newAttrs[attrs.length] = xmlns;
+ return new ElementDescriptor(
+ topPreferences.getXmlName(),
+ topPreferences.getUiName(),
+ topPreferences.getTooltip(),
+ topPreferences.getSdkUrl(),
+ newAttrs,
+ topPreferences.getChildren(),
+ false /* mandatory */);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Creates an element descriptor from a given {@link ViewClassInfo}.
+ */
+ private ElementDescriptor convertPref(ViewClassInfo info) {
+ String xml_name = info.getShortClassName();
+ String tooltip = info.getJavaDoc();
+
+ // Process all Preference attributes
+ ArrayList<AttributeDescriptor> attributes = new ArrayList<AttributeDescriptor>();
+ DescriptorsUtils.appendAttributes(attributes,
+ null, // elementName
+ SdkConstants.NS_RESOURCES,
+ info.getAttributes(),
+ null, // requiredAttributes
+ null); // overrides
+
+ for (ViewClassInfo link = info.getSuperClass();
+ link != null;
+ link = link.getSuperClass()) {
+ AttributeInfo[] attrList = link.getAttributes();
+ if (attrList.length > 0) {
+ attributes.add(new SeparatorAttributeDescriptor(
+ String.format("Attributes from %1$s", link.getShortClassName())));
+ DescriptorsUtils.appendAttributes(attributes,
+ null, // elementName
+ SdkConstants.NS_RESOURCES,
+ attrList,
+ null, // requiredAttributes
+ null); // overrides
+ }
+ }
+
+ return new ViewElementDescriptor(xml_name,
+ xml_name, // ui_name
+ info.getCanonicalClassName(),
+ tooltip,
+ null, // sdk_url
+ attributes.toArray(new AttributeDescriptor[attributes.size()]),
+ null,
+ null, // children
+ false /* mandatory */);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/AndroidManifest.template b/eclipse/plugins/com.android.ide.eclipse.adt/templates/AndroidManifest.template
new file mode 100644
index 0000000..b43e75f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/AndroidManifest.template
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="PACKAGE"
+ android:versionCode="1"
+ android:versionName="1.0">
+ <application android:icon="@drawable/icon" android:label="APPLICATION_NAME">
+ACTIVITIES
+ </application>
+USES-SDK
+</manifest>
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/activity.template b/eclipse/plugins/com.android.ide.eclipse.adt/templates/activity.template
new file mode 100644
index 0000000..e91d602
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/activity.template
@@ -0,0 +1,7 @@
+ <activity android:name=".ACTIVITY_NAME"
+ android:label="APPLICATION_NAME">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+INTENT_FILTERS
+ </intent-filter>
+ </activity> \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/icon.png b/eclipse/plugins/com.android.ide.eclipse.adt/templates/icon.png
new file mode 100644
index 0000000..7502484
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/icon.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/java_file.template b/eclipse/plugins/com.android.ide.eclipse.adt/templates/java_file.template
new file mode 100644
index 0000000..173ff96
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/java_file.template
@@ -0,0 +1,13 @@
+package PACKAGE;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+public class ACTIVITY_NAME extends Activity {
+ /** Called when the activity is first created. */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.main);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/launcher_intent_filter.template b/eclipse/plugins/com.android.ide.eclipse.adt/templates/launcher_intent_filter.template
new file mode 100644
index 0000000..f5681be
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/launcher_intent_filter.template
@@ -0,0 +1 @@
+ <category android:name="android.intent.category.LAUNCHER" /> \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/layout.template b/eclipse/plugins/com.android.ide.eclipse.adt/templates/layout.template
new file mode 100644
index 0000000..5f4c383
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/layout.template
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ >
+<TextView
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/hello"
+ />
+</LinearLayout>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/preference_intent_filter.template b/eclipse/plugins/com.android.ide.eclipse.adt/templates/preference_intent_filter.template
new file mode 100644
index 0000000..609bb2c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/preference_intent_filter.template
@@ -0,0 +1 @@
+ <category android:name="android.intent.category.PREFERENCE" /> \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/string.template b/eclipse/plugins/com.android.ide.eclipse.adt/templates/string.template
new file mode 100644
index 0000000..3a6e4b2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/string.template
@@ -0,0 +1 @@
+ <string name="STRING_NAME">STRING_CONTENT</string> \ No newline at end of file
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/strings.template b/eclipse/plugins/com.android.ide.eclipse.adt/templates/strings.template
new file mode 100644
index 0000000..b980ace
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/strings.template
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+STRINGS
+</resources>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/templates/uses-sdk.template b/eclipse/plugins/com.android.ide.eclipse.adt/templates/uses-sdk.template
new file mode 100644
index 0000000..8adae71
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/templates/uses-sdk.template
@@ -0,0 +1 @@
+ <uses-sdk android:minSdkVersion="MIN_SDK_VERSION" />
diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/.classpath b/eclipse/plugins/com.android.ide.eclipse.ddms/.classpath
new file mode 100644
index 0000000..280621c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.ddms/.classpath
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+ <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+ <classpathentry kind="lib" path="libs/jfreechart-1.0.9.jar"/>
+ <classpathentry kind="lib" path="libs/jcommon-1.0.12.jar"/>
+ <classpathentry kind="lib" path="libs/jfreechart-1.0.9-swt.jar"/>
+ <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/.project b/eclipse/plugins/com.android.ide.eclipse.ddms/.project
new file mode 100644
index 0000000..2e9f996
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.ddms/.project
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>ddms-plugin</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.pde.ManifestBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.pde.SchemaBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.eclipse.pde.PluginNature</nature>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+</projectDescription>
diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/META-INF/MANIFEST.MF b/eclipse/plugins/com.android.ide.eclipse.ddms/META-INF/MANIFEST.MF
new file mode 100644
index 0000000..09b8085
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.ddms/META-INF/MANIFEST.MF
@@ -0,0 +1,23 @@
+Manifest-Version: 1.0
+Bundle-ManifestVersion: 2
+Bundle-Name: Dalvik Debug Monitor Service
+Bundle-SymbolicName: com.android.ide.eclipse.ddms;singleton:=true
+Bundle-Version: 0.9.0.qualifier
+Bundle-Activator: com.android.ide.eclipse.ddms.DdmsPlugin
+Bundle-Vendor: The Android Open Source Project
+Bundle-Localization: plugin
+Require-Bundle: org.eclipse.ui,
+ org.eclipse.core.runtime,
+ org.eclipse.ui.console
+Eclipse-LazyStart: true
+Export-Package: com.android.ddmlib,
+ com.android.ddmlib.log,
+ com.android.ddmlib.testrunner,
+ com.android.ddmuilib,
+ com.android.ddmuilib.console,
+ com.android.ide.eclipse.ddms,
+ com.android.ide.eclipse.ddms.views
+Bundle-ClassPath: libs/jcommon-1.0.12.jar,
+ libs/jfreechart-1.0.9.jar,
+ libs/jfreechart-1.0.9-swt.jar,
+ .
diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/MODULE_LICENSE_APACHE2 b/eclipse/plugins/com.android.ide.eclipse.ddms/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.ddms/MODULE_LICENSE_APACHE2
diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/build.properties b/eclipse/plugins/com.android.ide.eclipse.ddms/build.properties
new file mode 100644
index 0000000..1c4c896
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.ddms/build.properties
@@ -0,0 +1,10 @@
+source.. = src/
+output.. = bin/
+bin.includes = META-INF/,\
+ icons/,\
+ plugin.xml,\
+ ., \
+ libs/jcommon-1.0.12.jar,\
+ libs/jfreechart-1.0.9-swt.jar,\
+ libs/jfreechart-1.0.9.jar
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/icons/android.png b/eclipse/plugins/com.android.ide.eclipse.ddms/icons/android.png
new file mode 100644
index 0000000..3779d4d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.ddms/icons/android.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/icons/capture.png b/eclipse/plugins/com.android.ide.eclipse.ddms/icons/capture.png
new file mode 100644
index 0000000..d75e7a9
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.ddms/icons/capture.png
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/plugin.xml b/eclipse/plugins/com.android.ide.eclipse.ddms/plugin.xml
new file mode 100644
index 0000000..27fadf2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.ddms/plugin.xml
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<?eclipse version="3.2"?>
+<plugin>
+ <extension
+ point="org.eclipse.ui.views">
+ <category
+ name="Android"
+ id="com.android.ide.eclipse.ddms.views.category">
+ </category>
+ <view
+ allowMultiple="false"
+ category="com.android.ide.eclipse.ddms.views.category"
+ class="com.android.ide.eclipse.ddms.views.DeviceView"
+ icon="icons/device.png"
+ id="com.android.ide.eclipse.ddms.views.DeviceView"
+ name="Devices">
+ </view>
+ <view
+ allowMultiple="false"
+ category="com.android.ide.eclipse.ddms.views.category"
+ class="com.android.ide.eclipse.ddms.views.LogCatView"
+ icon="icons/android.png"
+ id="com.android.ide.eclipse.ddms.views.LogCatView"
+ name="LogCat"/>
+ <!-- Disabled for now due to AWT/SWT bridge issue on Leopard.
+ <view
+ allowMultiple="false"
+ category="com.android.ide.eclipse.ddms.views.category"
+ class="com.android.ide.eclipse.ddms.views.EventLogView"
+ icon="icons/android.png"
+ id="com.android.ide.eclipse.ddms.views.EventLogView"
+ name="Event Log"/> -->
+ <view
+ allowMultiple="false"
+ category="com.android.ide.eclipse.ddms.views.category"
+ class="com.android.ide.eclipse.ddms.views.ThreadView"
+ icon="icons/thread.png"
+ id="com.android.ide.eclipse.ddms.views.ThreadView"
+ name="Threads"/>
+ <view
+ allowMultiple="false"
+ category="com.android.ide.eclipse.ddms.views.category"
+ class="com.android.ide.eclipse.ddms.views.HeapView"
+ icon="icons/heap.png"
+ id="com.android.ide.eclipse.ddms.views.HeapView"
+ name="Heap"/>
+ <view
+ allowMultiple="false"
+ category="com.android.ide.eclipse.ddms.views.category"
+ class="com.android.ide.eclipse.ddms.views.FileExplorerView"
+ icon="icons/android.png"
+ id="com.android.ide.eclipse.ddms.views.FileExplorerView"
+ name="File Explorer"/>
+ <view
+ allowMultiple="false"
+ category="com.android.ide.eclipse.ddms.views.category"
+ class="com.android.ide.eclipse.ddms.views.EmulatorControlView"
+ icon="icons/emulator.png"
+ id="com.android.ide.eclipse.ddms.views.EmulatorControlView"
+ name="Emulator Control"/>
+ </extension>
+ <extension
+ point="org.eclipse.ui.perspectives">
+ <perspective
+ class="com.android.ide.eclipse.ddms.Perspective"
+ icon="icons/android.png"
+ id="com.android.ide.eclipse.ddms.Perspective"
+ name="DDMS"/>
+ </extension>
+ <extension
+ point="org.eclipse.core.runtime.preferences">
+ <initializer class="com.android.ide.eclipse.ddms.preferences.PreferenceInitializer"/>
+ </extension>
+ <extension
+ point="org.eclipse.ui.perspectiveExtensions">
+ <perspectiveExtension targetID="org.eclipse.jdt.ui.JavaPerspective">
+ <perspectiveShortcut id="com.android.ide.eclipse.ddms.Perspective"/>
+ </perspectiveExtension>
+ <perspectiveExtension targetID="org.eclipse.ui.resourcePerspective">
+ <perspectiveShortcut id="com.android.ide.eclipse.ddms.Perspective"/>
+ </perspectiveExtension>
+ <perspectiveExtension targetID="org.eclipse.debug.ui.DebugPerspective">
+ <perspectiveShortcut id="com.android.ide.eclipse.ddms.Perspective"/>
+ <view id="com.android.ide.eclipse.ddms.views.LogCatView"
+ relative="org.eclipse.ui.views.ProblemView"
+ relationship="stack" />
+ </perspectiveExtension>
+ </extension>
+ <extension
+ point="org.eclipse.ui.preferencePages">
+ <page
+ category="com.android.ide.eclipse.preferences.main"
+ class="com.android.ide.eclipse.ddms.preferences.PreferencePage"
+ id="com.android.ide.eclipse.ddms.preferences.PreferencePage"
+ name="DDMS"/>
+ <page
+ category="com.android.ide.eclipse.preferences.main"
+ class="com.android.ide.eclipse.ddms.preferences.LogCatPreferencePage"
+ id="com.android.ide.eclipse.ddms.preferences.LogCatPreferencePage"
+ name="LogCat"/>
+ </extension>
+</plugin>
diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/CommonAction.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/CommonAction.java
new file mode 100644
index 0000000..30ca4cb
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/CommonAction.java
@@ -0,0 +1,69 @@
+/*
+ * 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.ide.eclipse.ddms;
+
+import com.android.ddmuilib.actions.ICommonAction;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.resource.ImageDescriptor;
+
+/**
+ * Basic action extending the jFace Action class in order to implement
+ * ICommonAction.
+ */
+public class CommonAction extends Action implements ICommonAction {
+
+ private Runnable mRunnable;
+
+ public CommonAction() {
+ super();
+ }
+
+ public CommonAction(String text) {
+ super(text);
+ }
+
+ /**
+ * @param text
+ * @param image
+ */
+ public CommonAction(String text, ImageDescriptor image) {
+ super(text, image);
+ }
+
+ /**
+ * @param text
+ * @param style
+ */
+ public CommonAction(String text, int style) {
+ super(text, style);
+ }
+
+ @Override
+ public void run() {
+ if (mRunnable != null) {
+ mRunnable.run();
+ }
+ }
+
+ /**
+ * Sets the {@link Runnable}.
+ * @see ICommonAction#setRunnable(Runnable)
+ */
+ public void setRunnable(Runnable runnable) {
+ mRunnable = runnable;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/DdmsPlugin.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/DdmsPlugin.java
new file mode 100644
index 0000000..ccadce6
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/DdmsPlugin.java
@@ -0,0 +1,565 @@
+/*
+ * 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.ide.eclipse.ddms;
+
+import com.android.ddmlib.AndroidDebugBridge;
+import com.android.ddmlib.Client;
+import com.android.ddmlib.DdmPreferences;
+import com.android.ddmlib.Device;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener;
+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.ide.eclipse.ddms.preferences.PreferenceInitializer;
+import com.android.ide.eclipse.ddms.views.DeviceView;
+
+import org.eclipse.core.runtime.Preferences;
+import org.eclipse.core.runtime.Preferences.IPropertyChangeListener;
+import org.eclipse.core.runtime.Preferences.PropertyChangeEvent;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.preference.IPreferenceStore;
+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.util.ArrayList;
+import java.util.Calendar;
+
+/**
+ * The activator class controls the plug-in life cycle
+ */
+public final class DdmsPlugin extends AbstractUIPlugin implements IDeviceChangeListener,
+ IUiSelectionListener {
+
+ // The plug-in ID
+ public static final String PLUGIN_ID = "com.android.ide.eclipse.ddms"; // $NON-NLS-1$
+
+ private static final String ADB_LOCATION = PLUGIN_ID + ".adb"; // $NON-NLS-1$
+
+ /** The singleton instance */
+ private static DdmsPlugin sPlugin;
+
+ /** Location of the adb command line executable */
+ private static String sAdbLocation;
+
+ /**
+ * Debug Launcher for already running apps
+ */
+ private static IDebugLauncher sRunningAppDebugLauncher;
+
+ /** Console for DDMS log message */
+ private MessageConsole mDdmsConsole;
+
+ /** Image loader object */
+ private ImageLoader mLoader;
+
+ private Device mCurrentDevice;
+ private Client mCurrentClient;
+ private boolean mListeningToUiSelection = false;
+
+ private final ArrayList<ISelectionListener> mListeners = new ArrayList<ISelectionListener>();
+
+ private Color mRed;
+
+ private boolean mDdmlibInitialized;
+
+ /**
+ * Interface to provide debugger launcher for running apps.
+ */
+ public interface IDebugLauncher {
+ public boolean debug(String packageName, int port);
+ }
+
+ /**
+ * Classes which implement this interface provide methods that deals
+ * with {@link Device} 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 Device} is selected.
+ * @param selectedDevice the selected device. If null, no devices are selected.
+ */
+ public void selectionChanged(Device 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() {
+ public void run() {
+ errorConsoleStream.setColor(mRed);
+ }
+ });
+
+ // set up the ddms log to use the ddms console.
+ Log.setLogOutput(new ILogOutput() {
+ public void printLog(LogLevel logLevel, String tag, String message) {
+ if (logLevel.getPriority() >= LogLevel.ERROR.getPriority()) {
+ printToStream(errorConsoleStream, tag, message);
+ ConsolePlugin.getDefault().getConsoleManager().showConsoleView(mDdmsConsole);
+ } else {
+ printToStream(consoleStream, tag, message);
+ }
+ }
+
+ 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() {
+ public void run() {
+ Shell shell = display.getActiveShell();
+ if (logLevel == LogLevel.ERROR) {
+ MessageDialog.openError(shell, tag, message);
+ } else {
+ MessageDialog.openWarning(shell, tag, message);
+ }
+ }
+ });
+ }
+
+ });
+
+ // create the loader that's able to load the images
+ mLoader = new ImageLoader(this);
+
+ // set the listener for the preference change
+ Preferences prefs = getPluginPreferences();
+ prefs.addPropertyChangeListener(new IPropertyChangeListener() {
+ 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));
+ }
+ }
+ });
+
+ // read the adb location from the prefs to attempt to start it properly without
+ // having to wait for ADT to start
+ sAdbLocation = eclipseStore.getString(ADB_LOCATION);
+
+ // start it in a thread to return from start() asap.
+ new Thread() {
+ @Override
+ public void run() {
+ // init ddmlib if needed
+ getDefault().initDdmlib();
+
+ // create and start the first bridge
+ AndroidDebugBridge.createBridge(sAdbLocation, true /* forceNewBridge */);
+ }
+ }.start();
+ }
+
+ 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;
+ }
+
+ /** Return the image loader for the plugin */
+ public static ImageLoader getImageLoader() {
+ if (sPlugin != null) {
+ return sPlugin.mLoader;
+ }
+ return null;
+ }
+
+ public static String getAdb() {
+ return sAdbLocation;
+ }
+
+ /**
+ * 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 setAdb(String adb, boolean startAdb) {
+ sAdbLocation = adb;
+
+ // store the location for future ddms only start.
+ sPlugin.getPreferenceStore().setValue(ADB_LOCATION, sAdbLocation);
+
+ // starts the server in a thread in case this is blocking.
+ if (startAdb) {
+ new Thread() {
+ @Override
+ public void run() {
+ // init ddmlib if needed
+ getDefault().initDdmlib();
+
+ // create and start the bridge
+ AndroidDebugBridge.createBridge(sAdbLocation, false /* forceNewBridge */);
+ }
+ }.start();
+ }
+ }
+
+ private synchronized void initDdmlib() {
+ if (mDdmlibInitialized == false) {
+ // set the preferences.
+ PreferenceInitializer.setupPreferences();
+
+ // init the lib
+ AndroidDebugBridge.init(true /* debugger support */);
+
+ mDdmlibInitialized = true;
+ }
+ }
+
+ /**
+ * Sets the launcher responsible for connecting the debugger to running applications.
+ * @param launcher The launcher.
+ */
+ public static void setRunningAppDebugLauncher(IDebugLauncher launcher) {
+ sRunningAppDebugLauncher = launcher;
+
+ // if the process view is already running, give it the launcher.
+ // This method could be called from a non ui thread, so we make sure to do that
+ // in the ui thread.
+ Display display = getDisplay();
+ if (display != null && display.isDisposed() == false) {
+ display.asyncExec(new Runnable() {
+ public void run() {
+ DeviceView dv = DeviceView.getInstance();
+ if (dv != null) {
+ dv.setDebugLauncher(sRunningAppDebugLauncher);
+ }
+ }
+ });
+ }
+ }
+
+ public static IDebugLauncher getRunningAppDebugLauncher() {
+ return sRunningAppDebugLauncher;
+ }
+
+ 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}.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param device the new device.
+ *
+ * @see IDeviceChangeListener#deviceConnected(Device)
+ */
+ public void deviceConnected(Device 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}.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param device the new device.
+ *
+ * @see IDeviceChangeListener#deviceDisconnected(Device)
+ */
+ public void deviceDisconnected(Device 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
+ Device[] devices = bridge.getDevices();
+
+ // check if we still have devices
+ if (devices.length == 0) {
+ handleDefaultSelection((Device)null);
+ } else {
+ handleDefaultSelection(devices[0]);
+ }
+ } else {
+ handleDefaultSelection((Device)null);
+ }
+ }
+ }
+ }
+
+ /**
+ * Sent when a device data changed, or when clients are started/terminated on the device.
+ * <p/>
+ * 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(Device)
+ */
+ public void deviceChanged(Device 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 Device} 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 synchronized void selectionChanged(Device 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 Device} and {@link Client}.
+ * @param device the selected device
+ */
+ private void handleDefaultSelection(final Device 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() {
+ 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);
+ 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("[%1$tF %1$tT]", c);
+ }
+
+ return String.format("[%1$tF %1$tT - %2$s]", c, tag);
+ }
+
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/ImageLoader.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/ImageLoader.java
new file mode 100644
index 0000000..a70405d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/ImageLoader.java
@@ -0,0 +1,67 @@
+/*
+ * 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.ide.eclipse.ddms;
+
+import com.android.ddmuilib.IImageLoader;
+
+import org.eclipse.core.runtime.Plugin;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.widgets.Display;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+/**
+ * Implementation of the IImageLoader interface for the eclipse plugin.
+ */
+public class ImageLoader implements IImageLoader {
+
+ private URL mBaseUrl;
+
+ public ImageLoader(Plugin plugin) {
+ mBaseUrl = plugin.getBundle().getEntry("/"); // $NON-NLS-1$
+ }
+
+ /**
+ * default method. only need a filename. the 2 interface methods call this one.
+ * @param filename the filename of the image to load. The filename is searched for under /icons.
+ * @return
+ */
+ public ImageDescriptor loadDescriptor(String filename) {
+ try {
+ URL newUrl = new URL(mBaseUrl, "/icons/" + filename); // $NON-NLS-1$
+ return ImageDescriptor.createFromURL(newUrl);
+ } catch (MalformedURLException e) {
+ // we'll just return null;
+ }
+ return null;
+ }
+
+ public ImageDescriptor loadDescriptor(String filename, Display display) {
+ return loadDescriptor(filename);
+ }
+
+
+ public Image loadImage(String filename, Display display) {
+ ImageDescriptor descriptor = loadDescriptor(filename);
+ if (descriptor !=null) {
+ return descriptor.createImage();
+ }
+ return null;
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/Perspective.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/Perspective.java
new file mode 100644
index 0000000..4c01e9b
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/Perspective.java
@@ -0,0 +1,80 @@
+/*
+ * 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.ide.eclipse.ddms;
+
+import com.android.ide.eclipse.ddms.views.DeviceView;
+import com.android.ide.eclipse.ddms.views.EmulatorControlView;
+import com.android.ide.eclipse.ddms.views.FileExplorerView;
+import com.android.ide.eclipse.ddms.views.HeapView;
+import com.android.ide.eclipse.ddms.views.LogCatView;
+import com.android.ide.eclipse.ddms.views.ThreadView;
+
+import org.eclipse.ui.IFolderLayout;
+import org.eclipse.ui.IPageLayout;
+import org.eclipse.ui.IPerspectiveFactory;
+
+public class Perspective implements IPerspectiveFactory {
+
+ public void createInitialLayout(IPageLayout layout) {
+ // create a default layout that looks like the stand alone DDMS.
+
+ // no editor window
+ layout.setEditorAreaVisible(false);
+
+ String editorArea = layout.getEditorArea();
+ IFolderLayout folder;
+
+ folder = layout.createFolder("logcat", IPageLayout.BOTTOM, 0.8f, //$NON-NLS-1$
+ editorArea);
+ folder.addPlaceholder(LogCatView.ID + ":*"); //$NON-NLS-1$
+ folder.addView(LogCatView.ID);
+
+ folder = layout.createFolder("devices", IPageLayout.LEFT, 0.3f, //$NON-NLS-1$
+ editorArea);
+ folder.addPlaceholder(DeviceView.ID + ":*"); //$NON-NLS-1$
+ folder.addView(DeviceView.ID);
+
+ folder = layout.createFolder("emulator", IPageLayout.BOTTOM, 0.5f, //$NON-NLS-1$
+ "devices");
+ folder.addPlaceholder(EmulatorControlView.ID + ":*"); //$NON-NLS-1$
+ folder.addView(EmulatorControlView.ID);
+
+ folder = layout.createFolder("ddms-detail", IPageLayout.RIGHT, 0.5f, //$NON-NLS-1$
+ editorArea);
+ folder.addPlaceholder(ThreadView.ID + ":*"); //$NON-NLS-1$
+ folder.addView(ThreadView.ID);
+ folder.addView(HeapView.ID);
+ folder.addView(FileExplorerView.ID);
+
+ layout.addPerspectiveShortcut("org.eclipse.ui.resourcePerspective"); //$NON-NLS-1$
+ layout.addPerspectiveShortcut("org.eclipse.debug.ui.DebugPerspective"); //$NON-NLS-1$
+ layout.addPerspectiveShortcut("org.eclipse.jdt.ui.JavaPerspective"); //$NON-NLS-1$
+
+ layout.addShowViewShortcut(DeviceView.ID);
+ layout.addShowViewShortcut(FileExplorerView.ID);
+ layout.addShowViewShortcut(HeapView.ID);
+ layout.addShowViewShortcut(LogCatView.ID);
+ layout.addShowViewShortcut(ThreadView.ID);
+
+ layout.addShowViewShortcut(IPageLayout.ID_RES_NAV);
+ layout.addShowViewShortcut(IPageLayout.ID_BOOKMARKS);
+ layout.addShowViewShortcut(IPageLayout.ID_OUTLINE);
+ layout.addShowViewShortcut(IPageLayout.ID_PROP_SHEET);
+ layout.addShowViewShortcut(IPageLayout.ID_PROBLEM_VIEW);
+ layout.addShowViewShortcut(IPageLayout.ID_PROGRESS_VIEW);
+ layout.addShowViewShortcut(IPageLayout.ID_TASK_LIST);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/preferences/LogCatPreferencePage.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/preferences/LogCatPreferencePage.java
new file mode 100644
index 0000000..909207d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/preferences/LogCatPreferencePage.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.ddms.preferences;
+
+import com.android.ide.eclipse.ddms.DdmsPlugin;
+import com.android.ide.eclipse.ddms.views.LogCatView;
+
+import org.eclipse.core.runtime.Preferences;
+import org.eclipse.core.runtime.Preferences.IPropertyChangeListener;
+import org.eclipse.core.runtime.Preferences.PropertyChangeEvent;
+import org.eclipse.jface.preference.FieldEditorPreferencePage;
+import org.eclipse.jface.preference.FontFieldEditor;
+import org.eclipse.swt.SWTError;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.FontData;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.IWorkbenchPreferencePage;
+
+/**
+ * Preference Pane for LogCat.
+ */
+public class LogCatPreferencePage extends FieldEditorPreferencePage implements
+ IWorkbenchPreferencePage {
+
+ public LogCatPreferencePage() {
+ super(GRID);
+ setPreferenceStore(DdmsPlugin.getDefault().getPreferenceStore());
+ }
+
+ @Override
+ protected void createFieldEditors() {
+ FontFieldEditor ffe = new FontFieldEditor(PreferenceInitializer.ATTR_LOGCAT_FONT,
+ "Display Font:", getFieldEditorParent());
+ addField(ffe);
+
+ Preferences prefs = DdmsPlugin.getDefault().getPluginPreferences();
+ prefs.addPropertyChangeListener(new IPropertyChangeListener() {
+ public void propertyChange(PropertyChangeEvent event) {
+ // get the name of the property that changed.
+ String property = event.getProperty();
+
+ if (PreferenceInitializer.ATTR_LOGCAT_FONT.equals(property)) {
+ try {
+ FontData fdat = new FontData((String)event.getNewValue());
+ LogCatView.setFont(new Font(getFieldEditorParent().getDisplay(), fdat));
+ } catch (IllegalArgumentException e) {
+ // Looks like the data from the store is not valid.
+ // We do nothing (default font will be used).
+ } catch (SWTError e2) {
+ // Looks like the Font() constructor failed.
+ // We do nothing in this case, the logcat view will use the default font.
+ }
+ }
+ }
+ });
+ }
+
+ public void init(IWorkbench workbench) {
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/preferences/PreferenceInitializer.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/preferences/PreferenceInitializer.java
new file mode 100644
index 0000000..b53d85c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/preferences/PreferenceInitializer.java
@@ -0,0 +1,102 @@
+/*
+ * 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.ide.eclipse.ddms.preferences;
+
+import com.android.ide.eclipse.ddms.DdmsPlugin;
+import com.android.ddmlib.DdmPreferences;
+import com.android.ddmuilib.DdmUiPreferences;
+
+import org.eclipse.core.runtime.preferences.AbstractPreferenceInitializer;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.FontData;
+
+/**
+ * Class used to initialize default preference values.
+ */
+public class PreferenceInitializer extends AbstractPreferenceInitializer {
+
+ public final static String ATTR_LOG_LEVEL =
+ DdmsPlugin.PLUGIN_ID + ".logLevel"; //$NON-NLS-1$
+
+ public final static String ATTR_DEBUG_PORT_BASE =
+ DdmsPlugin.PLUGIN_ID + ".adbDebugBasePort"; //$NON-NLS-1$
+
+ public final static String ATTR_SELECTED_DEBUG_PORT =
+ DdmsPlugin.PLUGIN_ID + ".debugSelectedPort"; //$NON-NLS-1$
+
+ public final static String ATTR_DEFAULT_THREAD_UPDATE =
+ DdmsPlugin.PLUGIN_ID + ".defaultThreadUpdateEnabled"; //$NON-NLS-1$
+
+ public final static String ATTR_DEFAULT_HEAP_UPDATE =
+ DdmsPlugin.PLUGIN_ID + ".defaultHeapUpdateEnabled"; //$NON-NLS-1$
+
+ public final static String ATTR_THREAD_INTERVAL =
+ DdmsPlugin.PLUGIN_ID + ".threadStatusInterval"; //$NON-NLS-1$
+
+ public final static String ATTR_IMAGE_SAVE_DIR =
+ DdmsPlugin.PLUGIN_ID + ".imageSaveDir"; //$NON-NLS-1$
+
+ public final static String ATTR_LAST_IMAGE_SAVE_DIR =
+ DdmsPlugin.PLUGIN_ID + ".lastImageSaveDir"; //$NON-NLS-1$
+
+ public final static String ATTR_LOGCAT_FONT =
+ DdmsPlugin.PLUGIN_ID + ".logcatFont"; //$NON-NLS-1$
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.eclipse.core.runtime.preferences.AbstractPreferenceInitializer
+ * #initializeDefaultPreferences()
+ */
+ @Override
+ public void initializeDefaultPreferences() {
+ IPreferenceStore store = DdmsPlugin.getDefault().getPreferenceStore();
+
+ store.setDefault(ATTR_DEBUG_PORT_BASE, DdmPreferences.DEFAULT_DEBUG_PORT_BASE);
+
+ store.setDefault(ATTR_SELECTED_DEBUG_PORT, DdmPreferences.DEFAULT_SELECTED_DEBUG_PORT);
+
+ store.setDefault(ATTR_DEFAULT_THREAD_UPDATE, DdmPreferences.DEFAULT_INITIAL_THREAD_UPDATE);
+ store.setDefault(ATTR_DEFAULT_HEAP_UPDATE,
+ DdmPreferences.DEFAULT_INITIAL_HEAP_UPDATE);
+
+ store.setDefault(ATTR_THREAD_INTERVAL, DdmUiPreferences.DEFAULT_THREAD_REFRESH_INTERVAL);
+
+ String homeDir = System.getProperty("user.home"); //$NON-NLS-1$
+ store.setDefault(ATTR_IMAGE_SAVE_DIR, homeDir);
+
+ store.setDefault(ATTR_LOG_LEVEL, DdmPreferences.DEFAULT_LOG_LEVEL.getStringValue());
+
+ store.setDefault(ATTR_LOGCAT_FONT,
+ new FontData("Courier", 10, SWT.NORMAL).toString()); //$NON-NLS-1$
+ }
+
+ /**
+ * Initializes the preferences of ddmlib and ddmuilib with values from the eclipse store.
+ */
+ public synchronized static void setupPreferences() {
+ IPreferenceStore store = DdmsPlugin.getDefault().getPreferenceStore();
+
+ DdmPreferences.setDebugPortBase(store.getInt(ATTR_DEBUG_PORT_BASE));
+ DdmPreferences.setSelectedDebugPort(store.getInt(ATTR_SELECTED_DEBUG_PORT));
+ DdmPreferences.setLogLevel(store.getString(ATTR_LOG_LEVEL));
+ DdmPreferences.setInitialThreadUpdate(store.getBoolean(ATTR_DEFAULT_THREAD_UPDATE));
+ DdmPreferences.setInitialHeapUpdate(store.getBoolean(ATTR_DEFAULT_HEAP_UPDATE));
+ DdmUiPreferences.setThreadRefreshInterval(store.getInt(ATTR_THREAD_INTERVAL));
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/preferences/PreferencePage.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/preferences/PreferencePage.java
new file mode 100644
index 0000000..86e87c7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/preferences/PreferencePage.java
@@ -0,0 +1,82 @@
+/*
+ * 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.ide.eclipse.ddms.preferences;
+
+import com.android.ide.eclipse.ddms.DdmsPlugin;
+import com.android.ddmlib.Log.LogLevel;
+import com.android.ddmuilib.PortFieldEditor;
+
+import org.eclipse.jface.preference.BooleanFieldEditor;
+import org.eclipse.jface.preference.FieldEditorPreferencePage;
+import org.eclipse.jface.preference.IntegerFieldEditor;
+import org.eclipse.jface.preference.RadioGroupFieldEditor;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.IWorkbenchPreferencePage;
+
+public class PreferencePage extends FieldEditorPreferencePage implements
+ IWorkbenchPreferencePage {
+
+ public PreferencePage() {
+ super(GRID);
+ setPreferenceStore(DdmsPlugin.getDefault().getPreferenceStore());
+ }
+
+ /**
+ * Creates the field editors. Field editors are abstractions of the common
+ * GUI blocks needed to manipulate various types of preferences. Each field
+ * editor knows how to save and restore itself.
+ */
+ @Override
+ public void createFieldEditors() {
+ IntegerFieldEditor ife;
+
+ ife = new PortFieldEditor(PreferenceInitializer.ATTR_DEBUG_PORT_BASE,
+ "ADB debugger base port:", getFieldEditorParent());
+ addField(ife);
+
+ BooleanFieldEditor bfe;
+
+ bfe = new BooleanFieldEditor(PreferenceInitializer.ATTR_DEFAULT_THREAD_UPDATE,
+ "Thread updates enabled by default", getFieldEditorParent());
+ addField(bfe);
+
+ bfe = new BooleanFieldEditor(PreferenceInitializer.ATTR_DEFAULT_HEAP_UPDATE,
+ "Heap updates enabled by default", getFieldEditorParent());
+ addField(bfe);
+
+ ife = new IntegerFieldEditor(PreferenceInitializer.ATTR_THREAD_INTERVAL,
+ "Thread status refresh interval (seconds):", getFieldEditorParent());
+ ife.setValidRange(1, 60);
+ addField(ife);
+
+ RadioGroupFieldEditor rgfe = new RadioGroupFieldEditor(PreferenceInitializer.ATTR_LOG_LEVEL,
+ "Logging Level", 1, new String[][] {
+ { "Verbose", LogLevel.VERBOSE.getStringValue() },
+ { "Debug", LogLevel.DEBUG.getStringValue() },
+ { "Info", LogLevel.INFO.getStringValue() },
+ { "Warning", LogLevel.WARN.getStringValue() },
+ { "Error", LogLevel.ERROR.getStringValue() },
+ { "Assert", LogLevel.ASSERT.getStringValue() }
+ },
+ getFieldEditorParent(), true);
+ addField(rgfe);
+
+ }
+
+ public void init(IWorkbench workbench) {
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/DeviceView.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/DeviceView.java
new file mode 100644
index 0000000..62a528a
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/DeviceView.java
@@ -0,0 +1,329 @@
+/*
+ * 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.ide.eclipse.ddms.views;
+
+import com.android.ddmlib.Client;
+import com.android.ddmlib.ClientData;
+import com.android.ddmlib.AndroidDebugBridge;
+import com.android.ddmlib.Device;
+import com.android.ddmuilib.DevicePanel;
+import com.android.ddmuilib.ScreenShotDialog;
+import com.android.ddmuilib.DevicePanel.IUiSelectionListener;
+import com.android.ide.eclipse.ddms.DdmsPlugin;
+import com.android.ide.eclipse.ddms.DdmsPlugin.IDebugLauncher;
+
+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.MessageDialog;
+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.PlatformUI;
+import org.eclipse.ui.part.ViewPart;
+
+public class DeviceView extends ViewPart implements IUiSelectionListener {
+
+ private final static boolean USE_SELECTED_DEBUG_PORT = true;
+
+ public static final String ID =
+ "com.android.ide.eclipse.ddms.views.DeviceView"; //$NON-NLS-1$
+
+ private DevicePanel mDeviceList;
+ private Action mResetAdbAction;
+ private Action mCaptureAction;
+ private Action mUpdateThreadAction;
+ private Action mUpdateHeapAction;
+ private Action mGcAction;
+ private Action mKillAppAction;
+ private Action mDebugAction;
+ private IDebugLauncher mDebugLauncher;
+
+ private static DeviceView sThis;
+
+ public DeviceView() {
+ // the view is declared with allowMultiple="false" so we
+ // can safely do this.
+ sThis = this;
+ }
+
+ public static DeviceView getInstance() {
+ return sThis;
+ }
+
+ /**
+ * Sets the {@link IDebugLauncher}.
+ * @param debugLauncher
+ */
+ public void setDebugLauncher(DdmsPlugin.IDebugLauncher debugLauncher) {
+ mDebugLauncher = debugLauncher;
+ if (mDebugAction != null && mDeviceList != null) {
+ Client currentClient = mDeviceList.getSelectedClient();
+ if (currentClient != null) {
+ mDebugAction.setEnabled(true);
+ }
+ }
+ }
+
+ @Override
+ public void createPartControl(Composite parent) {
+ mDeviceList = new DevicePanel(DdmsPlugin.getImageLoader(), USE_SELECTED_DEBUG_PORT);
+ mDeviceList.createPanel(parent);
+ mDeviceList.addSelectionListener(this);
+
+ DdmsPlugin plugin = DdmsPlugin.getDefault();
+ mDeviceList.addSelectionListener(plugin);
+ plugin.setListeningState(true);
+
+ mCaptureAction = new Action("Screen Capture") {
+ @Override
+ public void run() {
+ ScreenShotDialog dlg = new ScreenShotDialog(
+ DdmsPlugin.getDisplay().getActiveShell());
+ dlg.open(mDeviceList.getSelectedDevice());
+ }
+ };
+ mCaptureAction.setToolTipText("Screen Capture");
+ mCaptureAction.setImageDescriptor(
+ DdmsPlugin.getImageLoader().loadDescriptor("capture.png")); //$NON-NLS-1$
+
+ mResetAdbAction = new Action("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() {
+ public void run() {
+ Shell shell = display.getActiveShell();
+ MessageDialog.openError(shell, "Adb Error",
+ "Adb failed to restart!\n\nMake sure the plugin is properly configured.");
+ }
+ });
+ }
+ }
+ }
+ };
+ mResetAdbAction.setToolTipText("Reset the adb host daemon");
+ mResetAdbAction.setImageDescriptor(PlatformUI.getWorkbench()
+ .getSharedImages().getImageDescriptor(
+ ISharedImages.IMG_OBJS_WARN_TSK));
+
+ mKillAppAction = new Action() {
+ @Override
+ public void run() {
+ mDeviceList.killSelectedClient();
+ }
+ };
+
+ mKillAppAction.setText("Stop Process");
+ mKillAppAction.setToolTipText("Stop Process");
+ mKillAppAction.setImageDescriptor(DdmsPlugin.getImageLoader()
+ .loadDescriptor(DevicePanel.ICON_HALT));
+
+ mGcAction = new Action() {
+ @Override
+ public void run() {
+ mDeviceList.forceGcOnSelectedClient();
+ }
+ };
+
+ mGcAction.setText("Cause GC");
+ mGcAction.setToolTipText("Cause GC");
+ mGcAction.setImageDescriptor(DdmsPlugin.getImageLoader()
+ .loadDescriptor(DevicePanel.ICON_GC));
+
+ mUpdateHeapAction = new Action("Update Heap", IAction.AS_CHECK_BOX) {
+ @Override
+ public void run() {
+ boolean enable = mUpdateHeapAction.isChecked();
+ mDeviceList.setEnabledHeapOnSelectedClient(enable);
+ }
+ };
+ mUpdateHeapAction.setToolTipText("Update Heap");
+ mUpdateHeapAction.setImageDescriptor(DdmsPlugin.getImageLoader()
+ .loadDescriptor(DevicePanel.ICON_HEAP));
+
+ mUpdateThreadAction = new Action("Update Threads", IAction.AS_CHECK_BOX) {
+ @Override
+ public void run() {
+ boolean enable = mUpdateThreadAction.isChecked();
+ mDeviceList.setEnabledThreadOnSelectedClient(enable);
+ }
+ };
+ mUpdateThreadAction.setToolTipText("Update Threads");
+ mUpdateThreadAction.setImageDescriptor(DdmsPlugin.getImageLoader()
+ .loadDescriptor(DevicePanel.ICON_THREAD));
+
+ // check if there's already a debug launcher set up in the plugin class
+ mDebugLauncher = DdmsPlugin.getRunningAppDebugLauncher();
+
+ mDebugAction = new Action("Debug Process") {
+ @Override
+ public void run() {
+ if (mDebugLauncher != null) {
+ Client currentClient = mDeviceList.getSelectedClient();
+ if (currentClient != null) {
+ ClientData clientData = currentClient.getClientData();
+
+ // make sure the client can be debugged
+ switch (clientData.getDebuggerConnectionStatus()) {
+ case ClientData.DEBUGGER_ERROR: {
+ Display display = DdmsPlugin.getDisplay();
+ Shell shell = display.getActiveShell();
+ MessageDialog.openError(shell, "Process Debug",
+ "The process debug port is already in use!");
+ return;
+ }
+ case ClientData.DEBUGGER_ATTACHED: {
+ Display display = DdmsPlugin.getDisplay();
+ Shell shell = display.getActiveShell();
+ MessageDialog.openError(shell, "Process Debug",
+ "The process is already being debugged!");
+ return;
+ }
+ }
+
+ // get the name of the client
+ String packageName = clientData.getClientDescription();
+ if (packageName != null) {
+ if (mDebugLauncher.debug(packageName,
+ currentClient.getDebuggerListenPort()) == false) {
+
+ // 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, "Process Debug",
+ String.format(
+ "No opened project found for %1$s. Debug session failed!",
+ packageName));
+ }
+ }
+ }
+ }
+ }
+ };
+ mDebugAction.setToolTipText("Debug the selected process, provided its source project is present and opened in the workspace.");
+ mDebugAction.setImageDescriptor(DdmsPlugin.getImageLoader()
+ .loadDescriptor("debug-attach.png")); //$NON-NLS-1$
+ if (mDebugLauncher == null) {
+ mDebugAction.setEnabled(false);
+ }
+
+ placeActions();
+ }
+
+ @Override
+ public void setFocus() {
+ mDeviceList.setFocus();
+ }
+
+ /**
+ * Sent when a new {@link Device} 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(Device 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(mDebugLauncher != null);
+ mKillAppAction.setEnabled(true);
+ mGcAction.setEnabled(true);
+
+ mUpdateHeapAction.setEnabled(true);
+ mUpdateHeapAction.setChecked(selectedClient.isHeapUpdateEnabled());
+
+ mUpdateThreadAction.setEnabled(true);
+ mUpdateThreadAction.setChecked(selectedClient.isThreadUpdateEnabled());
+ } 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);
+ }
+ }
+
+ private void doSelectionChanged(Device selectedDevice) {
+ mCaptureAction.setEnabled(selectedDevice != null);
+ }
+
+ /**
+ * Place the actions in the ui.
+ */
+ private final void placeActions() {
+ IActionBars actionBars = getViewSite().getActionBars();
+
+ // first in the menu
+ IMenuManager menuManager = actionBars.getMenuManager();
+ menuManager.add(mDebugAction);
+ menuManager.add(new Separator());
+ menuManager.add(mUpdateThreadAction);
+ menuManager.add(mUpdateHeapAction);
+ menuManager.add(new Separator());
+ menuManager.add(mGcAction);
+ menuManager.add(new Separator());
+ menuManager.add(mKillAppAction);
+ menuManager.add(new Separator());
+ menuManager.add(mCaptureAction);
+ menuManager.add(new Separator());
+ menuManager.add(mResetAdbAction);
+
+ // and then in the toolbar
+ IToolBarManager toolBarManager = actionBars.getToolBarManager();
+ toolBarManager.add(mDebugAction);
+ toolBarManager.add(new Separator());
+ toolBarManager.add(mUpdateThreadAction);
+ toolBarManager.add(mUpdateHeapAction);
+ toolBarManager.add(new Separator());
+ toolBarManager.add(mKillAppAction);
+ toolBarManager.add(new Separator());
+ toolBarManager.add(mCaptureAction);
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/EmulatorControlView.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/EmulatorControlView.java
new file mode 100644
index 0000000..ca9a691
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/EmulatorControlView.java
@@ -0,0 +1,43 @@
+/*
+ * 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.ide.eclipse.ddms.views;
+
+import com.android.ddmuilib.EmulatorControlPanel;
+import com.android.ide.eclipse.ddms.DdmsPlugin;
+
+import org.eclipse.swt.widgets.Composite;
+
+public class EmulatorControlView extends SelectionDependentViewPart {
+
+ public static final String ID =
+ "com.android.ide.eclipse.ddms.views.EmulatorControlView"; //$NON-NLS-1$
+
+ private EmulatorControlPanel mPanel;
+
+ @Override
+ public void createPartControl(Composite parent) {
+ mPanel = new EmulatorControlPanel(DdmsPlugin.getImageLoader());
+ mPanel.createPanel(parent);
+ setSelectionDependentPanel(mPanel);
+ }
+
+ @Override
+ public void setFocus() {
+ mPanel.setFocus();
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/EventLogView.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/EventLogView.java
new file mode 100644
index 0000000..3a74e42
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/EventLogView.java
@@ -0,0 +1,114 @@
+/*
+ * 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.ide.eclipse.ddms.views;
+
+import com.android.ddmuilib.log.event.EventLogPanel;
+import com.android.ide.eclipse.ddms.CommonAction;
+import com.android.ide.eclipse.ddms.DdmsPlugin;
+import com.android.ide.eclipse.ddms.ImageLoader;
+
+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 = DdmsPlugin.getImageLoader();
+
+ // create the external actions
+ CommonAction optionsAction = new CommonAction("Options...");
+ optionsAction.setToolTipText("Opens the options panel");
+ optionsAction.setImageDescriptor(loader
+ .loadDescriptor("edit.png")); // $NON-NLS-1$
+
+ CommonAction clearLogAction = new CommonAction("Clear Log");
+ clearLogAction.setToolTipText("Clears the event log");
+ clearLogAction.setImageDescriptor(loader
+ .loadDescriptor("clear.png")); // $NON-NLS-1$
+
+ CommonAction saveAction = new CommonAction("Save Log");
+ saveAction.setToolTipText("Saves the event log");
+ saveAction.setImageDescriptor(loader
+ .loadDescriptor("save.png")); // $NON-NLS-1$
+
+ CommonAction loadAction = new CommonAction("Load Log");
+ loadAction.setToolTipText("Loads an event log");
+ loadAction.setImageDescriptor(loader
+ .loadDescriptor("load.png")); // $NON-NLS-1$
+
+ CommonAction importBugAction = new CommonAction("Import Bug Report Log");
+ importBugAction.setToolTipText("Imports a bug report.");
+ importBugAction.setImageDescriptor(loader
+ .loadDescriptor("importBug.png")); // $NON-NLS-1$
+
+ placeActions(optionsAction, clearLogAction, saveAction, loadAction, importBugAction);
+
+ mLogPanel = new EventLogPanel(DdmsPlugin.getImageLoader());
+ 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/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/FileExplorerView.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/FileExplorerView.java
new file mode 100644
index 0000000..4f0dd2e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/FileExplorerView.java
@@ -0,0 +1,165 @@
+/*
+ * 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.ide.eclipse.ddms.views;
+
+import com.android.ddmlib.Client;
+import com.android.ddmlib.Device;
+import com.android.ddmuilib.explorer.DeviceExplorer;
+import com.android.ide.eclipse.ddms.CommonAction;
+import com.android.ide.eclipse.ddms.DdmsPlugin;
+import com.android.ide.eclipse.ddms.DdmsPlugin.ISelectionListener;
+
+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;
+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 =
+ "com.android.ide.eclipse.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) {
+ 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.setImages(PlatformUI.getWorkbench()
+ .getSharedImages().getImage(ISharedImages.IMG_OBJ_FILE),
+ PlatformUI.getWorkbench() .getSharedImages().getImage(
+ ISharedImages.IMG_OBJ_FOLDER),
+ DdmsPlugin.getImageLoader().loadDescriptor("android.png") //$NON-NLS-1$
+ .createImage(),
+ PlatformUI.getWorkbench() .getSharedImages().getImage(
+ ISharedImages.IMG_OBJ_ELEMENT));
+
+ // creates the actions
+ CommonAction pushAction = new CommonAction("Push File...") {
+ @Override
+ public void run() {
+ mExplorer.pushIntoSelection();
+ }
+ };
+ pushAction.setToolTipText("Push a file onto the device");
+ pushAction.setImageDescriptor(DdmsPlugin.getImageLoader()
+ .loadDescriptor("push.png")); //$NON-NLS-1$
+ pushAction.setEnabled(false);
+
+ CommonAction pullAction = new CommonAction("Pull File...") {
+ @Override
+ public void run() {
+ mExplorer.pullSelection();
+ }
+ };
+ pullAction.setToolTipText("Pull a file from the device");
+ pullAction.setImageDescriptor(DdmsPlugin.getImageLoader()
+ .loadDescriptor("pull.png")); //$NON-NLS-1$
+ pullAction.setEnabled(false);
+
+ CommonAction deleteAction = new CommonAction("Delete") {
+ @Override
+ public void run() {
+ mExplorer.deleteSelection();
+ }
+ };
+ deleteAction.setToolTipText("Delete the selection");
+ deleteAction.setImageDescriptor(DdmsPlugin.getImageLoader()
+ .loadDescriptor("delete.png")); //$NON-NLS-1$
+ deleteAction.setEnabled(false);
+
+ // set up the actions in the explorer
+ mExplorer.setActions(pushAction, pullAction, deleteAction);
+
+ // 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);
+
+ toolBarManager.add(pullAction);
+ toolBarManager.add(pushAction);
+ toolBarManager.add(new Separator());
+ toolBarManager.add(deleteAction);
+
+ 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.
+ */
+ public void selectionChanged(Client selectedClient) {
+ // pass
+ }
+
+ /**
+ * Sent when a new {@link Device} is selected.
+ * @param selectedDevice the selected device.
+ */
+ public void selectionChanged(Device selectedDevice) {
+ mExplorer.switchDevice(selectedDevice);
+ }
+
+ /**
+ * Sent when there is no current selection.
+ */
+ public void selectionRemoved() {
+
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/HeapView.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/HeapView.java
new file mode 100644
index 0000000..5745e8e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/HeapView.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.ide.eclipse.ddms.views;
+
+import com.android.ddmuilib.HeapPanel;
+
+import org.eclipse.swt.widgets.Composite;
+
+public class HeapView extends TableView {
+
+ public static final String ID = "com.android.ide.eclipse.ddms.views.HeapView"; //$NON-NLS-1$
+ private HeapPanel mPanel;
+
+ public HeapView() {
+ }
+
+ @Override
+ public void createPartControl(Composite parent) {
+ mPanel = new HeapPanel();
+ 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/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/LogCatView.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/LogCatView.java
new file mode 100644
index 0000000..d3053f1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/LogCatView.java
@@ -0,0 +1,328 @@
+/*
+ * 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.ide.eclipse.ddms.views;
+
+import com.android.ide.eclipse.ddms.CommonAction;
+import com.android.ide.eclipse.ddms.DdmsPlugin;
+import com.android.ide.eclipse.ddms.ImageLoader;
+import com.android.ide.eclipse.ddms.preferences.PreferenceInitializer;
+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 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.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.actions.ActionFactory;
+
+import java.util.ArrayList;
+
+/**
+ * The log cat view displays log output from the current device selection.
+ *
+ */
+public final class LogCatView extends SelectionDependentViewPart {
+
+ public static final String ID =
+ "com.android.ide.eclipse.ddms.views.LogCatView"; // $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$
+
+ private static LogCatView 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 {
+
+ 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<LogFilter> list =
+ new ArrayList<LogFilter>(filters.length);
+
+ for (String f : filters) {
+ if (f.length() > 0) {
+ LogFilter logFilter = new LogFilter();
+ if (logFilter.loadFromString(f)) {
+ list.add(logFilter);
+ }
+ }
+ }
+
+ return list.toArray(new LogFilter[list.size()]);
+ }
+
+ public void saveFilters(LogFilter[] filters) {
+ StringBuilder sb = new StringBuilder();
+ for (LogFilter f : filters) {
+ String filterString = f.toString();
+ sb.append(filterString);
+ sb.append('|');
+ }
+
+ DdmsPlugin.getDefault().getPreferenceStore().setValue(PREFS_FILTERS, sb.toString());
+ }
+
+ public boolean requiresDefaultFilter() {
+ return true;
+ }
+ }
+
+ public LogCatView() {
+ 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 LogCatView 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 = DdmsPlugin.getImageLoader();
+
+ 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("Create Filter") {
+ @Override
+ public void run() {
+ mLogPanel.addFilter();
+ }
+ };
+ mCreateFilterAction.setToolTipText("Create Filter");
+ mCreateFilterAction.setImageDescriptor(loader
+ .loadDescriptor("add.png")); // $NON-NLS-1$
+
+ mEditFilterAction = new CommonAction("Edit Filter") {
+ @Override
+ public void run() {
+ mLogPanel.editFilter();
+ }
+ };
+ mEditFilterAction.setToolTipText("Edit Filter");
+ mEditFilterAction.setImageDescriptor(loader
+ .loadDescriptor("edit.png")); // $NON-NLS-1$
+
+ mDeleteFilterAction = new CommonAction("Delete Filter") {
+ @Override
+ public void run() {
+ mLogPanel.deleteFilter();
+ }
+ };
+ mDeleteFilterAction.setToolTipText("Delete Filter");
+ mDeleteFilterAction.setImageDescriptor(loader
+ .loadDescriptor("delete.png")); // $NON-NLS-1$
+
+ mExportAction = new CommonAction("Export Selection As Text...") {
+ @Override
+ public void run() {
+ mLogPanel.save();
+ }
+ };
+ mExportAction.setToolTipText("Export Selection As Text...");
+ 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 i = 0 ; i < mLogLevelActions.length; i++) {
+ Action a = mLogLevelActions[i];
+ if (a == this) {
+ a.setChecked(true);
+
+ // set the log level
+ mLogPanel.setCurrentFilterLogLevel(i+2);
+ } else {
+ a.setChecked(false);
+ }
+ }
+ }
+ };
+
+ mLogLevelActions[i].setToolTipText(name);
+ mLogLevelActions[i].setImageDescriptor(loader.loadDescriptor(mLogLevelIcons[i]));
+ }
+
+ mClearAction = new Action("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(loader, colors, new FilterStorage(), LogPanel.FILTER_MANUAL);
+ 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("Copy") {
+ @Override
+ public void run() {
+ mLogPanel.copy(mClipboard);
+ }
+ });
+
+ // setup the select all action
+ actionBars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(),
+ new Action("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);
+ }
+ }
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/NativeHeapView.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/NativeHeapView.java
new file mode 100644
index 0000000..ed5aacb
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/NativeHeapView.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.ide.eclipse.ddms.views;
+
+import com.android.ddmuilib.NativeHeapPanel;
+
+import org.eclipse.swt.widgets.Composite;
+
+public class NativeHeapView extends TableView {
+
+ public static final String ID =
+ "com.android.ide.eclipse.ddms.views.NativeHeapView"; // $NON-NLS-1$
+ private NativeHeapPanel mPanel;
+
+ public NativeHeapView() {
+ }
+
+ @Override
+ public void createPartControl(Composite parent) {
+ mPanel = new NativeHeapPanel();
+ 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/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/SelectionDependentViewPart.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/SelectionDependentViewPart.java
new file mode 100644
index 0000000..48b2689
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/SelectionDependentViewPart.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.ddms.views;
+
+import com.android.ide.eclipse.ddms.DdmsPlugin;
+import com.android.ide.eclipse.ddms.DdmsPlugin.ISelectionListener;
+import com.android.ddmlib.Client;
+import com.android.ddmlib.Device;
+import com.android.ddmuilib.SelectionDependentPanel;
+
+import org.eclipse.ui.part.ViewPart;
+
+/**
+ * A Workbench {@link ViewPart} that requires {@link Device}/{@link Client} selection notifications
+ * from {@link DdmsPlugin} through the {@link ISelectionListener} interface.
+ */
+public abstract class SelectionDependentViewPart extends ViewPart implements ISelectionListener {
+
+ private SelectionDependentPanel mPanel;
+
+ protected final void setSelectionDependentPanel(SelectionDependentPanel panel) {
+ // remember the panel
+ mPanel = panel;
+
+ // and add ourself as listener of selection events.
+ DdmsPlugin.getDefault().addSelectionListener(this);
+ }
+
+ @Override
+ public void dispose() {
+ DdmsPlugin.getDefault().removeSelectionListener(this);
+ super.dispose();
+ }
+
+ /**
+ * Sent when a new {@link Client} is selected.
+ * @param selectedClient The selected client.
+ *
+ * @see ISelectionListener
+ */
+ public final void selectionChanged(Client selectedClient) {
+ mPanel.clientSelected(selectedClient);
+ }
+
+ /**
+ * Sent when a new {@link Device} is selected.
+ * @param selectedDevice the selected device.
+ *
+ * @see ISelectionListener
+ */
+ public final void selectionChanged(Device selectedDevice) {
+ mPanel.deviceSelected(selectedDevice);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/TableView.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/TableView.java
new file mode 100644
index 0000000..0fda35d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/TableView.java
@@ -0,0 +1,96 @@
+/*
+ * 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.ide.eclipse.ddms.views;
+
+import com.android.ddmuilib.ITableFocusListener;
+import com.android.ddmuilib.TablePanel;
+import com.android.ddmuilib.ITableFocusListener.IFocusedTableActivator;
+
+import org.eclipse.jface.action.Action;
+import org.eclipse.swt.dnd.Clipboard;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.ui.IActionBars;
+import org.eclipse.ui.actions.ActionFactory;
+
+/**
+ * Base class for view containing Table that needs to support copy, and select all.
+ */
+public abstract class TableView extends SelectionDependentViewPart {
+
+ /** Activator for the current Table that has the focus */
+ IFocusedTableActivator mActivator = null;
+
+ private Clipboard mClipboard;
+
+ private Action mCopyAction;
+ private Action mSelectAllAction;
+
+ /**
+ * Setup the listener for the Table objects of <code>Panel</code>, and setup
+ * the copy and select all actions.
+ * @param panel The panel to setup
+ * @param parent The parent composite of the Panel's content.
+ */
+ void setupTableFocusListener(TablePanel panel, Composite parent) {
+ panel.setTableFocusListener(new ITableFocusListener() {
+ public void focusGained(IFocusedTableActivator activator) {
+ mActivator = activator;
+ mCopyAction.setEnabled(true);
+ mSelectAllAction.setEnabled(true);
+ }
+
+ public void focusLost(IFocusedTableActivator activator) {
+ if (activator == mActivator) {
+ mActivator = null;
+ mCopyAction.setEnabled(false);
+ mSelectAllAction.setEnabled(false);
+ }
+ }
+ });
+
+ // setup the copy action
+ mClipboard = new Clipboard(parent.getDisplay());
+ IActionBars actionBars = getViewSite().getActionBars();
+ actionBars.setGlobalActionHandler(ActionFactory.COPY.getId(),
+ mCopyAction = new Action("Copy") {
+ @Override
+ public void run() {
+ if (mActivator != null) {
+ mActivator.copy(mClipboard);
+ }
+ }
+ });
+
+ // setup the select all action
+ actionBars.setGlobalActionHandler(ActionFactory.SELECT_ALL.getId(),
+ mSelectAllAction = new Action("Select All") {
+ @Override
+ public void run() {
+ if (mActivator != null) {
+ mActivator.selectAll();
+ }
+ }
+ });
+
+ }
+
+ @Override
+ public void dispose() {
+ super.dispose();
+ mClipboard.dispose();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/ThreadView.java b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/ThreadView.java
new file mode 100644
index 0000000..cd24458
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.ddms/src/com/android/ide/eclipse/ddms/views/ThreadView.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.ide.eclipse.ddms.views;
+
+import com.android.ddmuilib.ThreadPanel;
+
+import org.eclipse.swt.widgets.Composite;
+
+public class ThreadView extends TableView {
+
+ public static final String ID =
+ "com.android.ide.eclipse.ddms.views.ThreadView"; // $NON-NLS-1$
+ private ThreadPanel mPanel;
+
+ public ThreadView() {
+ }
+
+ @Override
+ public void createPartControl(Composite parent) {
+ mPanel = new ThreadPanel();
+ 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/eclipse/plugins/com.android.ide.eclipse.tests/.classpath b/eclipse/plugins/com.android.ide.eclipse.tests/.classpath
new file mode 100644
index 0000000..4088683
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/.classpath
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry kind="src" path="unittests"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+ <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+ <classpathentry kind="lib" path="kxml2-2.3.0.jar"/>
+ <classpathentry combineaccessrules="false" kind="src" path="/SdkLib"/>
+ <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/.project b/eclipse/plugins/com.android.ide.eclipse.tests/.project
new file mode 100644
index 0000000..99e4964
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/.project
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>adt-tests</name>
+ <comment></comment>
+ <projects>
+ <project>SdkLib</project>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.pde.ManifestBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.pde.SchemaBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.eclipse.pde.PluginNature</nature>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+</projectDescription>
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/META-INF/MANIFEST.MF b/eclipse/plugins/com.android.ide.eclipse.tests/META-INF/MANIFEST.MF
new file mode 100644
index 0000000..266008c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/META-INF/MANIFEST.MF
@@ -0,0 +1,19 @@
+Manifest-Version: 1.0
+Bundle-ManifestVersion: 2
+Bundle-Name: Android Plugin Tests
+Bundle-SymbolicName: com.android.ide.eclipse.tests
+Bundle-Version: 0.9.0.qualifier
+Bundle-Activator: com.android.ide.eclipse.tests.AndroidTestPlugin
+Require-Bundle: org.eclipse.ui,
+ org.eclipse.core.runtime,
+ org.eclipse.core.resources,
+ com.android.ide.eclipse.adt,
+ org.junit,
+ org.eclipse.jdt.core,
+ org.eclipse.jdt.launching,
+ org.eclipse.ui.views,
+ com.android.ide.eclipse.ddms
+Eclipse-LazyStart: true
+Bundle-Vendor: The Android Open Source Project
+Bundle-ClassPath: kxml2-2.3.0.jar,
+ .
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/MODULE_LICENSE_EPL b/eclipse/plugins/com.android.ide.eclipse.tests/MODULE_LICENSE_EPL
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/MODULE_LICENSE_EPL
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/NOTICE b/eclipse/plugins/com.android.ide.eclipse.tests/NOTICE
new file mode 100644
index 0000000..49c101d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/NOTICE
@@ -0,0 +1,224 @@
+*Eclipse Public License - v 1.0*
+
+THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
+PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF
+THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
+
+*1. DEFINITIONS*
+
+"Contribution" means:
+
+a) in the case of the initial Contributor, the initial code and
+documentation distributed under this Agreement, and
+b) in the case of each subsequent Contributor:
+
+i) changes to the Program, and
+
+ii) additions to the Program;
+
+where such changes and/or additions to the Program originate from and
+are distributed by that particular Contributor. A Contribution
+'originates' from a Contributor if it was added to the Program by such
+Contributor itself or anyone acting on such Contributor's behalf.
+Contributions do not include additions to the Program which: (i) are
+separate modules of software distributed in conjunction with the Program
+under their own license agreement, and (ii) are not derivative works of
+the Program.
+
+"Contributor" means any person or entity that distributes the Program.
+
+"Licensed Patents " mean patent claims licensable by a Contributor which
+are necessarily infringed by the use or sale of its Contribution alone
+or when combined with the Program.
+
+"Program" means the Contributions distributed in accordance with this
+Agreement.
+
+"Recipient" means anyone who receives the Program under this Agreement,
+including all Contributors.
+
+*2. GRANT OF RIGHTS*
+
+a) Subject to the terms of this Agreement, each Contributor hereby
+grants Recipient a non-exclusive, worldwide, royalty-free copyright
+license to reproduce, prepare derivative works of, publicly display,
+publicly perform, distribute and sublicense the Contribution of such
+Contributor, if any, and such derivative works, in source code and
+object code form.
+
+b) Subject to the terms of this Agreement, each Contributor hereby
+grants Recipient a non-exclusive, worldwide, royalty-free patent license
+under Licensed Patents to make, use, sell, offer to sell, import and
+otherwise transfer the Contribution of such Contributor, if any, in
+source code and object code form. This patent license shall apply to the
+combination of the Contribution and the Program if, at the time the
+Contribution is added by the Contributor, such addition of the
+Contribution causes such combination to be covered by the Licensed
+Patents. The patent license shall not apply to any other combinations
+which include the Contribution. No hardware per se is licensed hereunder.
+
+c) Recipient understands that although each Contributor grants the
+licenses to its Contributions set forth herein, no assurances are
+provided by any Contributor that the Program does not infringe the
+patent or other intellectual property rights of any other entity. Each
+Contributor disclaims any liability to Recipient for claims brought by
+any other entity based on infringement of intellectual property rights
+or otherwise. As a condition to exercising the rights and licenses
+granted hereunder, each Recipient hereby assumes sole responsibility to
+secure any other intellectual property rights needed, if any. For
+example, if a third party patent license is required to allow Recipient
+to distribute the Program, it is Recipient's responsibility to acquire
+that license before distributing the Program.
+
+d) Each Contributor represents that to its knowledge it has sufficient
+copyright rights in its Contribution, if any, to grant the copyright
+license set forth in this Agreement.
+
+*3. REQUIREMENTS*
+
+A Contributor may choose to distribute the Program in object code form
+under its own license agreement, provided that:
+
+a) it complies with the terms and conditions of this Agreement; and
+
+b) its license agreement:
+
+i) effectively disclaims on behalf of all Contributors all warranties
+and conditions, express and implied, including warranties or conditions
+of title and non-infringement, and implied warranties or conditions of
+merchantability and fitness for a particular purpose;
+
+ii) effectively excludes on behalf of all Contributors all liability for
+damages, including direct, indirect, special, incidental and
+consequential damages, such as lost profits;
+
+iii) states that any provisions which differ from this Agreement are
+offered by that Contributor alone and not by any other party; and
+
+iv) states that source code for the Program is available from such
+Contributor, and informs licensees how to obtain it in a reasonable
+manner on or through a medium customarily used for software exchange.
+
+When the Program is made available in source code form:
+
+a) it must be made available under this Agreement; and
+
+b) a copy of this Agreement must be included with each copy of the Program.
+
+Contributors may not remove or alter any copyright notices contained
+within the Program.
+
+Each Contributor must identify itself as the originator of its
+Contribution, if any, in a manner that reasonably allows subsequent
+Recipients to identify the originator of the Contribution.
+
+*4. COMMERCIAL DISTRIBUTION*
+
+Commercial distributors of software may accept certain responsibilities
+with respect to end users, business partners and the like. While this
+license is intended to facilitate the commercial use of the Program, the
+Contributor who includes the Program in a commercial product offering
+should do so in a manner which does not create potential liability for
+other Contributors. Therefore, if a Contributor includes the Program in
+a commercial product offering, such Contributor ("Commercial
+Contributor") hereby agrees to defend and indemnify every other
+Contributor ("Indemnified Contributor") against any losses, damages and
+costs (collectively "Losses") arising from claims, lawsuits and other
+legal actions brought by a third party against the Indemnified
+Contributor to the extent caused by the acts or omissions of such
+Commercial Contributor in connection with its distribution of the
+Program in a commercial product offering. The obligations in this
+section do not apply to any claims or Losses relating to any actual or
+alleged intellectual property infringement. In order to qualify, an
+Indemnified Contributor must: a) promptly notify the Commercial
+Contributor in writing of such claim, and b) allow the Commercial
+Contributor to control, and cooperate with the Commercial Contributor
+in, the defense and any related settlement negotiations. The Indemnified
+Contributor may participate in any such claim at its own expense.
+
+For example, a Contributor might include the Program in a commercial
+product offering, Product X. That Contributor is then a Commercial
+Contributor. If that Commercial Contributor then makes performance
+claims, or offers warranties related to Product X, those performance
+claims and warranties are such Commercial Contributor's responsibility
+alone. Under this section, the Commercial Contributor would have to
+defend claims against the other Contributors related to those
+performance claims and warranties, and if a court requires any other
+Contributor to pay any damages as a result, the Commercial Contributor
+must pay those damages.
+
+*5. NO WARRANTY*
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED
+ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES
+OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR
+A PARTICULAR PURPOSE. Each Recipient is solely responsible for
+determining the appropriateness of using and distributing the Program
+and assumes all risks associated with its exercise of rights under this
+Agreement , including but not limited to the risks and costs of program
+errors, compliance with applicable laws, damage to or loss of data,
+programs or equipment, and unavailability or interruption of operations.
+
+*6. DISCLAIMER OF LIABILITY*
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR
+ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING
+WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR
+DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED
+HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+*7. GENERAL*
+
+If any provision of this Agreement is invalid or unenforceable under
+applicable law, it shall not affect the validity or enforceability of
+the remainder of the terms of this Agreement, and without further action
+by the parties hereto, such provision shall be reformed to the minimum
+extent necessary to make such provision valid and enforceable.
+
+If Recipient institutes patent litigation against any entity (including
+a cross-claim or counterclaim in a lawsuit) alleging that the Program
+itself (excluding combinations of the Program with other software or
+hardware) infringes such Recipient's patent(s), then such Recipient's
+rights granted under Section 2(b) shall terminate as of the date such
+litigation is filed.
+
+All Recipient's rights under this Agreement shall terminate if it fails
+to comply with any of the material terms or conditions of this Agreement
+and does not cure such failure in a reasonable period of time after
+becoming aware of such noncompliance. If all Recipient's rights under
+this Agreement terminate, Recipient agrees to cease use and distribution
+of the Program as soon as reasonably practicable. However, Recipient's
+obligations under this Agreement and any licenses granted by Recipient
+relating to the Program shall continue and survive.
+
+Everyone is permitted to copy and distribute copies of this Agreement,
+but in order to avoid inconsistency the Agreement is copyrighted and may
+only be modified in the following manner. The Agreement Steward reserves
+the right to publish new versions (including revisions) of this
+Agreement from time to time. No one other than the Agreement Steward has
+the right to modify this Agreement. The Eclipse Foundation is the
+initial Agreement Steward. The Eclipse Foundation may assign the
+responsibility to serve as the Agreement Steward to a suitable separate
+entity. Each new version of the Agreement will be given a distinguishing
+version number. The Program (including Contributions) may always be
+distributed subject to the version of the Agreement under which it was
+received. In addition, after a new version of the Agreement is
+published, Contributor may elect to distribute the Program (including
+its Contributions) under the new version. Except as expressly stated in
+Sections 2(a) and 2(b) above, Recipient receives no rights or licenses
+to the intellectual property of any Contributor under this Agreement,
+whether expressly, by implication, estoppel or otherwise. All rights in
+the Program not expressly granted under this Agreement are reserved.
+
+This Agreement is governed by the laws of the State of New York and the
+intellectual property laws of the United States of America. No party to
+this Agreement will bring a legal action under this Agreement more than
+one year after the cause of action arose. Each party waives its rights
+to a jury trial in any resulting litigation.
+
+
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/README.txt b/eclipse/plugins/com.android.ide.eclipse.tests/README.txt
new file mode 100644
index 0000000..f5899e3
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/README.txt
@@ -0,0 +1,72 @@
+This project contains the tests for the Android Eclipse Plugins.
+
+You can do two things:
+1- Run the full "eclipse plugin" suite
+2- Run independent JUnit tests (not as plugin)
+
+------------------------------------------
+1- Running the full "eclipse plugin" suite
+------------------------------------------
+
+Steps to run the test suite:
+
+A- In Eclipse, import following projects from //device/tools/eclipse/plugins:
+ - adt-tests
+ - adt
+ - common
+ - editors
+
+B- Create a new "JUnit Plug-in Test" run configuration via the "Run > Open Run Dialog..." menu
+Set the launch configuration's data as follows:
+i. "Test" tab:
+ Select "Run a single test"
+ Project: adt-tests
+ Test class: com.android.ide.eclipse.tests.UnitTests
+ Test runner: JUnit 3
+ii. "Arguments" tab:
+ Set "VM Arguments" to
+"-Dtest_data=<adt>/plugins/com.android.ide.eclipse.tests/unittests/data/"
+replacing "<adt>" with absolute filesystem path to the android plugin source location
+
+All other fields can be left with their default values
+
+C. Run the newly created launch configuration
+
+Running the tests will run a secondary instance of Eclipse.
+
+Please note the following constraints to be aware of when writing tests to run within a plugin environment:
+
+a. Access restrictions: cannot access package or protected members in a different
+plugin, even if they are in the same declared package
+b. Using classloader.getResource or getResourceAsStream to access test data will
+likely fail in the plugin environment. Instead, use AdtTestData to access test files
+in conjunction with the "test_data" environment variable mentioned above
+
+
+-------------------------------------------
+2- Run independent JUnit tests (not plugin)
+-------------------------------------------
+
+A- In Eclipse, import following projects from //device/tools/eclipse/plugins:
+ - adt-tests
+ - adt
+ - common
+ - editors
+
+B- Select the "unittests" source folder, right-click and select
+ "Run As > JUnit Test" (i.e. not the plugin tests)
+
+This creates a debug configuration of type "JUnit Test" running all tests
+in the source folder "unittests". The runtime must be JUnit 3.
+
+Note: this method runs the tests within a regular JVM environment (ie not within
+an Eclipse instance). This method has the advantage of being quicker than running
+as a JUnit plugin test, and requires less potential set-up, but has the
+disadvantage of not properly replicating how the tests will be run in the
+continuous test environment. Tests that pass when run as "JUnit Tests" can
+fail when run as "JUnit Plugin Tests", due to the extra constraints imposed by
+running within an Eclipse plug-in noted in section 1.
+
+
+
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/build.properties b/eclipse/plugins/com.android.ide.eclipse.tests/build.properties
new file mode 100644
index 0000000..cbfd993
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/build.properties
@@ -0,0 +1,10 @@
+source.. = src/,\
+ unittests/
+output.. = bin/
+bin.includes = META-INF/,\
+ .,\
+ test.xml,\
+ prefs.template,\
+ unittest.xml,\
+ kxml2-2.3.0.jar,\
+ unittests/data/
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/not_source_folder/jar/example/Class1.java b/eclipse/plugins/com.android.ide.eclipse.tests/not_source_folder/jar/example/Class1.java
new file mode 100644
index 0000000..3cf1027
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/not_source_folder/jar/example/Class1.java
@@ -0,0 +1,35 @@
+/*
+ * 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 jar.example;
+
+public class Class1 {
+
+ public static final int sStaticField = 1;
+
+ /** constructor */
+ public Class1() {
+ int a = 1;
+ }
+
+ public static class InnerStaticClass1 {
+
+ }
+
+ public class InnerClass2 {
+
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/not_source_folder/jar/example/Class2.java b/eclipse/plugins/com.android.ide.eclipse.tests/not_source_folder/jar/example/Class2.java
new file mode 100644
index 0000000..4d15c47
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/not_source_folder/jar/example/Class2.java
@@ -0,0 +1,21 @@
+/*
+ * 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 jar.example;
+
+public class Class2 extends Class1 {
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/prefs.template b/eclipse/plugins/com.android.ide.eclipse.tests/prefs.template
new file mode 100644
index 0000000..e0037de
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/prefs.template
@@ -0,0 +1,3 @@
+#Wed Feb 20 16:56:40 PST 2008
+com.android.ide.eclipse.adt.sdk=sdk_home
+eclipse.preferences.version=1
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/wizards/newproject/StubSampleProjectCreationPage.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/wizards/newproject/StubSampleProjectCreationPage.java
new file mode 100644
index 0000000..42f8df0
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/wizards/newproject/StubSampleProjectCreationPage.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License"); you
+ * may not use this file except in compliance with the License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.android.ide.eclipse.adt.wizards.newproject;
+
+import java.io.File;
+
+/**
+ * Stub class for project creation page Returns canned responses for creating a
+ * sample project
+ */
+public class StubSampleProjectCreationPage extends NewProjectCreationPage {
+
+ private String mSampleProjectName;
+ private String mOsSdkLocation;
+
+ public StubSampleProjectCreationPage(String pageName,
+ String sampleProjectName, String osSdkLocation) {
+ super(pageName);
+ this.mSampleProjectName = sampleProjectName;
+ this.mOsSdkLocation = osSdkLocation;
+ }
+
+ @Override
+ public String getProjectName() {
+ return mSampleProjectName;
+ }
+
+ @Override
+ public String getPackageName() {
+ return "com.android.samples";
+ }
+
+ @Override
+ public String getActivityName() {
+ return mSampleProjectName;
+ }
+
+ @Override
+ public String getApplicationName() {
+ return mSampleProjectName;
+ }
+
+ @Override
+ public boolean isNewProject() {
+ return false;
+ }
+
+ @Override
+ public String getProjectLocation() {
+ return mOsSdkLocation + File.separator + "samples" + File.separator + mSampleProjectName;
+ }
+
+ @Override
+ public String getSourceFolder() {
+ return "src";
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/wizards/newproject/StubSampleProjectWizard.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/wizards/newproject/StubSampleProjectWizard.java
new file mode 100644
index 0000000..40cd636
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/adt/wizards/newproject/StubSampleProjectWizard.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License"); you
+ * may not use this file except in compliance with the License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.android.ide.eclipse.adt.wizards.newproject;
+
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.jface.operation.IRunnableWithProgress;
+import org.eclipse.jface.wizard.IWizardContainer;
+import org.eclipse.jface.wizard.IWizardPage;
+import org.eclipse.swt.widgets.Shell;
+
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * Stub class for project creation wizard Created so project creation logic can
+ * be run without UI creation/manipulation Returns canned responses for creating
+ * a sample project
+ */
+public class StubSampleProjectWizard extends NewProjectWizard {
+
+ private final String mSampleProjectName;
+ private final String mOsSdkLocation;
+
+ /**
+ * Constructor
+ *
+ * @param sampleProjectName
+ * @param osSdkLocation
+ */
+ public StubSampleProjectWizard(String sampleProjectName, String osSdkLocation) {
+ this.mSampleProjectName = sampleProjectName;
+ this.mOsSdkLocation = osSdkLocation;
+ }
+
+ /**
+ * Override parent to return stub page
+ */
+ @Override
+ protected NewProjectCreationPage createMainPage() {
+ return new StubSampleProjectCreationPage(MAIN_PAGE_NAME,
+ mSampleProjectName, mOsSdkLocation);
+ }
+
+ /**
+ * Overrides parent to return dummy wizard container
+ */
+ @Override
+ public IWizardContainer getContainer() {
+ return new IWizardContainer() {
+
+ public IWizardPage getCurrentPage() {
+ return null;
+ }
+
+ public Shell getShell() {
+ return null;
+ }
+
+ public void showPage(IWizardPage page) {
+ // pass
+ }
+
+ public void updateButtons() {
+ // pass
+ }
+
+ public void updateMessage() {
+ // pass
+ }
+
+ public void updateTitleBar() {
+ // pass
+ }
+
+ public void updateWindowTitle() {
+ // pass
+ }
+
+ /**
+ * Executes runnable on current thread
+ */
+ public void run(boolean fork, boolean cancelable,
+ IRunnableWithProgress runnable)
+ throws InvocationTargetException, InterruptedException {
+ runnable.run(new NullProgressMonitor());
+ }
+
+ };
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/AdtTestData.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/AdtTestData.java
new file mode 100644
index 0000000..262ef65
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/AdtTestData.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License"); you
+ * may not use this file except in compliance with the License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.android.ide.eclipse.tests;
+
+import java.io.File;
+import java.net.URL;
+import java.util.logging.Logger;
+
+/**
+ * Helper class for retrieving test data
+ *
+ * All tests which need to retrieve test data files should go through this class
+ *
+ */
+public class AdtTestData {
+
+ /** singleton instance */
+ private static AdtTestData sInstance = null;
+ private static final Logger sLogger = Logger.getLogger(AdtTestData.class.getName());
+
+ /** the absolute file path to the /data directory in this test
+ * environment.
+ */
+ private String mOsRootDataPath;
+
+
+ private AdtTestData() {
+ // can set test_data env variable to override default behavior of
+ // finding data using class loader
+ // useful when running in plugin environment, where test data is inside
+ // bundled jar, and must be extracted to temp filesystem location to be
+ // accessed normally
+ mOsRootDataPath = System.getProperty("test_data");
+ if (mOsRootDataPath == null) {
+ sLogger.info("Cannot find test_data directory, init to class loader");
+ URL url = this.getClass().getClassLoader().getResource("data"); //$NON-NLS-1$
+ mOsRootDataPath = url.getFile();
+ }
+ if (!mOsRootDataPath.endsWith(File.separator)) {
+ sLogger.info("Fixing test_data env variable does not end with path separator");
+ mOsRootDataPath = mOsRootDataPath.concat(File.separator);
+ }
+ }
+
+ /** Get the singleton instance of AdtTestData */
+ public static AdtTestData getInstance() {
+ if (sInstance == null) {
+ sInstance = new AdtTestData();
+ }
+ return sInstance;
+ }
+
+ /** Returns the absolute file path to a file located in this plugins
+ * "data" directory
+ * @param osRelativePath - string path to file contained in /data. Must
+ * use path separators appropriate to host OS
+ * @return String
+ */
+ public String getTestFilePath(String osRelativePath) {
+ return mOsRootDataPath + osRelativePath;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/AllTests.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/AllTests.java
new file mode 100644
index 0000000..fb5504c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/AllTests.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License"); you
+ * may not use this file except in compliance with the License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.android.ide.eclipse.tests;
+
+import junit.framework.TestSuite;
+
+
+/**
+ * Container TestSuite for all eclipse tests, both functional and unit
+ */
+public class AllTests extends TestSuite {
+
+ public AllTests() {
+
+ }
+
+ /**
+ * Returns a suite of test cases to be run.
+ */
+ public static TestSuite suite() {
+ TestSuite suite = new TestSuite();
+ suite.addTest(FuncTests.suite());
+ suite.addTest(UnitTests.suite());
+ return suite;
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/AndroidTestPlugin.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/AndroidTestPlugin.java
new file mode 100644
index 0000000..45a6fbc
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/AndroidTestPlugin.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License"); you
+ * may not use this file except in compliance with the License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.android.ide.eclipse.tests;
+
+import org.eclipse.ui.plugin.AbstractUIPlugin;
+import org.osgi.framework.BundleContext;
+
+/**
+ * The activator class controls the plug-in life cycle
+ */
+public class AndroidTestPlugin extends AbstractUIPlugin {
+
+ // The plug-in ID
+ public static final String PLUGIN_ID = "com.android.ide.eclipse.adt.tests";
+
+ // The shared instance
+ private static AndroidTestPlugin sPlugin;
+
+ /**
+ * The constructor
+ */
+ public AndroidTestPlugin() {
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.eclipse.ui.plugin.AbstractUIPlugin#start(org.osgi.framework.BundleContext)
+ */
+ @Override
+ public void start(BundleContext context) throws Exception {
+ super.start(context);
+ sPlugin = this;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.eclipse.ui.plugin.AbstractUIPlugin#stop(org.osgi.framework.BundleContext)
+ */
+ @Override
+ public void stop(BundleContext context) throws Exception {
+ sPlugin = null;
+ super.stop(context);
+ }
+
+ /**
+ * Returns the shared instance
+ *
+ * @return the shared instance
+ */
+ public static AndroidTestPlugin getDefault() {
+ return sPlugin;
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/EclipseTestCollector.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/EclipseTestCollector.java
new file mode 100644
index 0000000..29538bb
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/EclipseTestCollector.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License"); you
+ * may not use this file except in compliance with the License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.android.ide.eclipse.tests;
+
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
+
+import org.eclipse.core.runtime.Plugin;
+
+import java.lang.reflect.Modifier;
+import java.net.URL;
+import java.util.Enumeration;
+import java.util.logging.Logger;
+
+/**
+ * Class for collecting all test cases in an eclipse plugin
+ *
+ */
+public class EclipseTestCollector {
+
+ private static final Logger sLogger = Logger.getLogger(EclipseTestCollector.class.getName());
+
+ /**
+ * Constructor
+ */
+ public EclipseTestCollector() {
+
+ }
+
+ /**
+ * Searches through given plugin, adding all TestCase classes to given suite
+ * @param suite - TestSuite to add to
+ * @param plugin - Plugin to search for tests
+ * @param expectedPackage - expected package for tests. Only test classes
+ * that start with this package name will be added to suite
+ */
+ public void addTestCases(TestSuite suite, Plugin plugin, String expectedPackage) {
+ if (plugin != null) {
+ Enumeration entries = plugin.getBundle().findEntries("/", "*.class", true);
+
+ while (entries.hasMoreElements()) {
+ URL entry = (URL)entries.nextElement();
+ String filePath = entry.getPath().replace(".class", "");
+ try {
+ Class testClass = getClass(filePath, expectedPackage);
+ if (isTestClass(testClass)) {
+ suite.addTestSuite(testClass);
+ }
+ }
+ catch (ClassNotFoundException e) {
+ // ignore, this is not the class we're looking for
+ //sLogger.log(Level.INFO, "Could not load class " + filePath);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns true if given class shouk\ld be added to suite
+ * @param testClass
+ * @return
+ */
+ protected boolean isTestClass(Class testClass) {
+ return TestCase.class.isAssignableFrom(testClass) &&
+ Modifier.isPublic(testClass.getModifiers()) &&
+ hasPublicConstructor(testClass);
+ }
+
+ /**
+ * Returns true if given class has a public constructor
+ * @param testClass
+ * @return
+ */
+ protected boolean hasPublicConstructor(Class testClass) {
+ try {
+ TestSuite.getTestConstructor(testClass);
+ } catch(NoSuchMethodException e) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Load the class given by the plugin aka bundle file path
+ * @param filePath - path of class in bundle
+ * @param expectedPackage - expected package of class
+ * @return
+ * @throws ClassNotFoundException
+ */
+ protected Class getClass(String filePath, String expectedPackage) throws ClassNotFoundException {
+ String dotPath = filePath.replace('/', '.');
+ // remove the output folders, by finding where package name starts
+ int index = dotPath.indexOf(expectedPackage);
+ if (index == -1) {
+ throw new ClassNotFoundException();
+ }
+ String packagePath = dotPath.substring(index);
+ return Class.forName(packagePath);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/FuncTestCase.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/FuncTestCase.java
new file mode 100644
index 0000000..63f17ab
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/FuncTestCase.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License"); you
+ * may not use this file except in compliance with the License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.android.ide.eclipse.tests;
+
+import junit.framework.TestCase;
+
+/**
+ * Generic superclass for Eclipse Android functional test cases, that provides
+ * common facilities
+ */
+public class FuncTestCase extends TestCase {
+
+ private String mOsSdkLocation;
+
+ /**
+ * Constructor
+ *
+ * @throws IllegalArgumentException if environment variable "sdk_home" is
+ * not set
+ */
+ protected FuncTestCase() {
+ mOsSdkLocation = System.getProperty("sdk_home");
+ if (mOsSdkLocation == null || mOsSdkLocation.length() < 1) {
+ throw new IllegalArgumentException(
+ "Environment variable sdk_home is not set");
+ }
+ }
+
+ /**
+ * Returns the absolute file system path of the Android SDK location to use
+ * for this test
+ */
+ protected String getOsSdkLocation() {
+ return mOsSdkLocation;
+ }
+
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/FuncTests.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/FuncTests.java
new file mode 100644
index 0000000..08405e8
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/FuncTests.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License"); you
+ * may not use this file except in compliance with the License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.android.ide.eclipse.tests;
+
+import com.android.ide.eclipse.tests.functests.sampleProjects.SampleProjectTest;
+
+import junit.framework.TestSuite;
+
+/**
+ * Container TestSuite for all eclipse tests to be run
+ */
+
+public class FuncTests extends TestSuite {
+
+ static final String FUNC_TEST_PACKAGE = "com.android.ide.eclipse.tests.functests";
+
+ public FuncTests() {
+
+ }
+
+ /**
+ * Returns a suite of test cases to be run.
+ * Needed for JUnit3 compliant command line test runner
+ */
+ public static TestSuite suite() {
+ TestSuite suite = new TestSuite();
+
+ suite.addTestSuite(SampleProjectTest.class);
+
+ return suite;
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/UnitTests.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/UnitTests.java
new file mode 100644
index 0000000..ac928db
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/UnitTests.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License"); you
+ * may not use this file except in compliance with the License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.android.ide.eclipse.tests;
+
+
+import junit.framework.Test;
+import junit.framework.TestSuite;
+
+/**
+ * Container TestSuite for all eclipse unit tests to be run
+ *
+ * Uses Eclipse OSGI to find and then run all junit.junit.framework.Tests in
+ * this plugin, excluding tests in the FuncTests.FUNC_TEST_PACKAGE package
+ *
+ * Since it uses Eclipse OSGI, it must be run in a Eclipse plugin environment
+ * i.e. from Eclipse workbench, this suite must be run using the
+ * "JUnit Plug-in Test" launch configuration as opposed to as a "JUnit Test"
+ *
+ */
+public class UnitTests {
+ private static final String TEST_PACKAGE = "com.android.ide.eclipse";
+
+ public static Test suite() {
+ TestSuite suite = new TestSuite();
+
+ UnitTestCollector collector = new UnitTestCollector();
+ collector.addTestCases(suite, AndroidTestPlugin.getDefault(), TEST_PACKAGE);
+ return suite;
+ }
+
+ /**
+ * Specialized test collector which will skip adding functional tests
+ */
+ private static class UnitTestCollector extends EclipseTestCollector {
+ /**
+ * Override parent class to exclude functional tests
+ */
+ @Override
+ protected boolean isTestClass(Class testClass) {
+ return super.isTestClass(testClass) &&
+ !testClass.getPackage().getName().startsWith(FuncTests.FUNC_TEST_PACKAGE);
+ }
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/functests/sampleProjects/SampleProjectTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/functests/sampleProjects/SampleProjectTest.java
new file mode 100644
index 0000000..98817c6
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/src/com/android/ide/eclipse/tests/functests/sampleProjects/SampleProjectTest.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License"); you
+ * may not use this file except in compliance with the License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.android.ide.eclipse.tests.functests.sampleProjects;
+
+import com.android.ide.eclipse.adt.project.ProjectHelper;
+import com.android.ide.eclipse.adt.wizards.newproject.StubSampleProjectWizard;
+import com.android.ide.eclipse.tests.FuncTestCase;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResourceChangeEvent;
+import org.eclipse.core.resources.IResourceChangeListener;
+import org.eclipse.core.resources.IResourceDelta;
+import org.eclipse.core.resources.IResourceDeltaVisitor;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.swt.widgets.Display;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Test case that verifies all SDK sample projects can be imported, built in
+ * Eclipse
+ *
+ * TODO: add support for deploying apps onto emulator and verifying successful
+ * execution there
+ *
+ */
+public class SampleProjectTest extends FuncTestCase {
+
+ private static final Logger sLogger = Logger.getLogger(SampleProjectTest.class.getName());
+
+ /**
+ * Tests the sample project with the given name
+ *
+ * @param name - name of sample project to test
+ */
+ protected void doTestSampleProject(String name) {
+ try {
+
+ StubSampleProjectWizard newProjCreator = new StubSampleProjectWizard(
+ name, getOsSdkLocation());
+ newProjCreator.init(null, null);
+ newProjCreator.performFinish();
+
+ IProject iproject = validateProjectExists(name);
+
+ validateNoProblems(iproject);
+
+ }
+ catch (CoreException e) {
+ fail("Unexpected exception when creating sample project: " + e.toString());
+ }
+ }
+
+ public void testApiDemos() {
+ doTestSampleProject("ApiDemos");
+ }
+
+ public void testHelloActivity() {
+ doTestSampleProject("HelloActivity");
+ }
+
+ public void testLunarLander() {
+ doTestSampleProject("LunarLander");
+ }
+
+ public void testNotePad() {
+ doTestSampleProject("NotePad");
+ }
+
+ public void testSkeletonApp() {
+ doTestSampleProject("SkeletonApp");
+ }
+
+ public void testSnake() {
+ doTestSampleProject("Snake");
+ }
+
+ private IProject validateProjectExists(String name) {
+ IProject iproject = getIProject(name);
+ assertTrue(iproject.exists());
+ assertTrue(iproject.isOpen());
+ return iproject;
+ }
+
+ private IProject getIProject(String name) {
+ IProject iproject = ResourcesPlugin.getWorkspace().getRoot()
+ .getProject(name);
+ return iproject;
+ }
+
+ private void validateNoProblems(IProject iproject) throws CoreException {
+ waitForBuild(iproject);
+ assertFalse(ProjectHelper.hasError(iproject, true));
+ }
+
+
+ /**
+ * Waits for build to complete.
+ *
+ * @param iproject
+ */
+ private void waitForBuild(final IProject iproject) {
+
+ final BuiltProjectDeltaVisitor deltaVisitor = new BuiltProjectDeltaVisitor(iproject);
+ IResourceChangeListener newBuildListener = new IResourceChangeListener() {
+
+ public void resourceChanged(IResourceChangeEvent event) {
+ try {
+ event.getDelta().accept(deltaVisitor);
+ }
+ catch (CoreException e) {
+ fail();
+ }
+ }
+
+ };
+ iproject.getWorkspace().addResourceChangeListener(newBuildListener,
+ IResourceChangeEvent.POST_BUILD);
+
+ // poll build listener to determine when build is done
+ // loop max of 1200 times * 50 ms = 60 seconds
+ final int maxWait = 1200;
+ for (int i=0; i < maxWait; i++) {
+ if (deltaVisitor.isProjectBuilt()) {
+ return;
+ }
+ try {
+ Thread.sleep(50);
+ }
+ catch (InterruptedException e) {
+
+ }
+ if (Display.getCurrent() != null) {
+ Display.getCurrent().readAndDispatch();
+ }
+ }
+
+ sLogger.log(Level.SEVERE, "expected build event never happened?");
+ fail("expected build event never happened for " + iproject.getName());
+
+ }
+
+ /**
+ * Scans a given IResourceDelta looking for a "build event" change for given IProject
+ *
+ */
+ private class BuiltProjectDeltaVisitor implements IResourceDeltaVisitor {
+
+ private IProject mIProject;
+ private boolean mIsBuilt;
+
+ public BuiltProjectDeltaVisitor(IProject iproject) {
+ mIProject = iproject;
+ mIsBuilt = false;
+ }
+
+ public boolean visit(IResourceDelta delta) {
+ if (mIProject.equals(delta.getResource())) {
+ setBuilt(true);
+ return false;
+ }
+ return true;
+ }
+
+ private synchronized void setBuilt(boolean b) {
+ mIsBuilt = b;
+ }
+
+ public synchronized boolean isProjectBuilt() {
+ return mIsBuilt;
+ }
+
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/test.xml b/eclipse/plugins/com.android.ide.eclipse.tests/test.xml
new file mode 100644
index 0000000..792ebc2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/test.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- test launcher file for Android Eclipse functional tests -->
+<project name="testsuite" default="run" basedir=".">
+ <!--The following properties should be passed into this script, set to some default value for now -->
+ <property name="eclipse.home" value="/opt/eclipse" />
+ <property name="sdk_home" value="/tmp/sdk" />
+ <property name="eclipse_test" value="${eclipse.home}/plugins/org.eclipse.test_3.2.0" />
+
+ <!-- eclipse scripts use an annoying mixture of eclipse-home and eclipse.home -->
+ <!-- lets define both...-->
+ <property name="eclipse-home" value="${eclipse.home}" />
+ <property name="test-folder" value="${eclipse.home}/test_folder" />
+
+ <!-- sets the properties eclipse.home, and library-file -->
+ <property name="plugin-name" value="com.android.ide.eclipse.tests" />
+ <property name="library-file" value="${eclipse_test}/library.xml" />
+
+ <!-- location of adt preference file (within workspace) -->
+ <property name="prefs_path" value="${test-folder}/.metadata/.plugins/org.eclipse.core.runtime/.settings/com.android.ide.eclipse.adt.prefs" />
+ <!-- location of pref template file relative to this file -->
+
+ <property name="prefs_template" value="prefs.template" />
+
+ <!-- This target holds all initialization code that needs to be done for -->
+ <!-- all tests that are to be run. -->
+ <target name="init">
+ <tstamp />
+ <echo message="eclipse.home: ${eclipse.home}" />
+ <echo message="libfile: ${library-file}" />
+
+ <!-- delete test eclipse workspace -->
+ <delete dir="${test-folder}" quiet="true" />
+
+ <!-- delete test results dir -->
+ <delete dir="${eclipse.home}/results" quiet="true" />
+
+ <!-- Copy a preference file into the test workspace. -->
+ <!-- This is done to ensure Android SDK preference is set on startup -->
+ <copy file="${prefs_template}" tofile="${prefs_path}" />
+ <!-- replace sdk path placeholder token with actual sdk path -->
+ <replace file="${prefs_path}" token="sdk_home" value="${sdk_home}" />
+
+ <!-- if this is on windows, escape the drive and file separators -->
+ <replace file="${prefs_path}" token="\" value="\\" />
+ <replace file="${prefs_path}" token=":" value="\:" />
+ </target>
+
+ <!-- This target defines the tests that need to be run. -->
+ <target name="suite">
+ <!-- launch as ui-test ie as a test which needs Eclipse workbench -->
+ <ant target="ui-test" antfile="${library-file}" dir="${eclipse.home}">
+ <property name="data-dir" value="${test-folder}" />
+ <property name="plugin-name" value="${plugin-name}" />
+ <property name="classname" value="com.android.ide.eclipse.tests.FuncTests" />
+ <!-- pass extra vm arg to set sdk_home env variable -->
+ <property name="extraVMargs" value="-Dsdk_home=${sdk_home}" />
+ </ant>
+ </target>
+
+ <!-- This target holds code to cleanup the testing environment after -->
+ <!-- after all of the tests have been run. You can use this target to -->
+ <!-- delete temporary files that have been created. -->
+ <target name="cleanup">
+ </target>
+
+ <!-- This target runs the test suite. Any actions that need to happen -->
+ <!-- after all the tests have been run should go here. -->
+ <target name="run" depends="init,suite,cleanup">
+ <ant target="collect" antfile="${library-file}" dir="${eclipse.home}/results">
+ <property name="includes" value="com*.xml" />
+ <property name="output-file" value="${plugin-name}.xml" />
+ </ant>
+ </target>
+</project>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittest.xml b/eclipse/plugins/com.android.ide.eclipse.tests/unittest.xml
new file mode 100644
index 0000000..83e00ec
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittest.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- test launcher file for Android Eclipse unit tests -->
+<project name="testsuite" default="run" basedir=".">
+ <!--The following properties should be passed into this script, set to some default value for now -->
+ <property name="eclipse.home" value="/opt/eclipse" />
+ <property name="eclipse_test" value="${eclipse.home}/plugins/org.eclipse.test_3.2.0" />
+
+ <!-- eclipse scripts use an annoying mixture of eclipse-home and eclipse.home -->
+ <!-- lets define both...-->
+ <property name="eclipse-home" value="${eclipse.home}" />
+ <property name="test-folder" value="${eclipse.home}/unittest_ws" />
+
+ <!-- sets the properties eclipse.home, and library-file -->
+ <property name="plugin-name" value="com.android.ide.eclipse.tests" />
+ <property name="library-file" value="${eclipse_test}/library.xml" />
+
+ <!-- This target holds all initialization code that needs to be done for -->
+ <!-- all tests that are to be run. -->
+ <target name="init">
+ <ant antfile="test.xml" target="init">
+ <property name="test-folder" value="${test-folder}" />
+ </ant>
+ </target>
+
+ <!-- This target defines the tests that need to be run. -->
+ <target name="suite">
+ <!-- need to launch as ui-test since all ADT plugins depend on ui-->
+ <!-- otherwise other plugins will not load -->
+ <ant target="ui-test" antfile="${library-file}" dir="${eclipse.home}">
+ <property name="data-dir" value="${test-folder}" />
+ <property name="plugin-name" value="${plugin-name}" />
+ <property name="classname" value="com.android.ide.eclipse.tests.UnitTests" />
+ <!-- pass extra vm arg to set test_data env variable -->
+ <property name="extraVMargs" value="-Dtest_data=${test_data}" />
+ </ant>
+ </target>
+
+ <!-- This target holds code to cleanup the testing environment after -->
+ <!-- after all of the tests have been run. You can use this target to -->
+ <!-- delete temporary files that have been created. -->
+ <target name="cleanup">
+ </target>
+
+ <!-- This target runs the test suite. Any actions that need to happen -->
+ <!-- after all the tests have been run should go here. -->
+ <target name="run" depends="init,suite,cleanup">
+ <ant target="collect" antfile="${library-file}" dir="${eclipse.home}/results">
+ <property name="includes" value="com*.xml" />
+ <property name="output-file" value="${plugin-name}.xml" />
+ </ant>
+ </target>
+</project>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/build/BaseBuilderTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/build/BaseBuilderTest.java
new file mode 100644
index 0000000..0860e40
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/build/BaseBuilderTest.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.build;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import junit.framework.TestCase;
+
+public class BaseBuilderTest extends TestCase {
+
+ public void testParseAaptOutput() {
+ Pattern p = Pattern.compile( "^(.+):(\\d+):\\s(.+)$"); //$NON-NLS-1$
+ String s = "C:\\java\\workspace-android\\AndroidApp\\res\\values\\strings.xml:11: WARNING: empty 'some warning text";
+
+ Matcher m = p.matcher(s);
+ assertEquals(true, m.matches());
+ assertEquals("C:\\java\\workspace-android\\AndroidApp\\res\\values\\strings.xml", m.group(1));
+ assertEquals("11", m.group(2));
+ assertEquals("WARNING: empty 'some warning text", m.group(3));
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/project/ProjectHelperTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/project/ProjectHelperTest.java
new file mode 100644
index 0000000..8c52d81
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/project/ProjectHelperTest.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.project;
+
+import com.android.ide.eclipse.mock.ClasspathEntryMock;
+import com.android.ide.eclipse.mock.JavaProjectMock;
+
+import org.eclipse.core.runtime.Path;
+import org.eclipse.jdt.core.IClasspathEntry;
+import org.eclipse.jdt.core.JavaModelException;
+
+import junit.framework.TestCase;
+
+public class ProjectHelperTest extends TestCase {
+
+ /** The old container id */
+ private final static String OLD_CONTAINER_ID =
+ "com.android.ide.eclipse.adt.project.AndroidClasspathContainerInitializer"; //$NON-NLS-1$
+
+ /** The container id for the android framework jar file */
+ private final static String CONTAINER_ID =
+ "com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"; //$NON-NLS-1$
+
+ @Override
+ public void setUp() throws Exception {
+ // pass for now
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ // pass for now
+ }
+
+ public final void testFixProjectClasspathEntriesFromOldContainer() throws JavaModelException {
+ // create a project with a path to an android .zip
+ JavaProjectMock javaProject = new JavaProjectMock(
+ new IClasspathEntry[] {
+ new ClasspathEntryMock(new Path("Project/src"), //$NON-NLS-1$
+ IClasspathEntry.CPE_SOURCE),
+ new ClasspathEntryMock(new Path(OLD_CONTAINER_ID),
+ IClasspathEntry.CPE_CONTAINER),
+ },
+ new Path("Project/bin"));
+
+ ProjectHelper.fixProjectClasspathEntries(javaProject);
+
+ IClasspathEntry[] fixedEntries = javaProject.getRawClasspath();
+ assertEquals(3, fixedEntries.length);
+ assertEquals("Project/src", fixedEntries[0].getPath().toString());
+ assertEquals(OLD_CONTAINER_ID, fixedEntries[1].getPath().toString());
+ assertEquals(CONTAINER_ID, fixedEntries[2].getPath().toString());
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/sdk/AndroidJarLoaderTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/sdk/AndroidJarLoaderTest.java
new file mode 100644
index 0000000..8af7e02
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/sdk/AndroidJarLoaderTest.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.sdk;
+
+import com.android.ide.eclipse.adt.sdk.IAndroidClassLoader.IClassDescriptor;
+import com.android.ide.eclipse.tests.AdtTestData;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import junit.framework.TestCase;
+
+/**
+ * Unit Test for {@link AndroidJarLoader}.
+ *
+ * Uses the classes jar.example.Class1/Class2 stored in tests/data/jar_example.jar.
+ */
+public class AndroidJarLoaderTest extends TestCase {
+
+ private AndroidJarLoader mFrameworkClassLoader;
+
+ /** Creates an instance of {@link AndroidJarLoader} on our test data JAR */
+ @Override
+ public void setUp() throws Exception {
+ String jarfilePath = AdtTestData.getInstance().getTestFilePath("jar_example.jar"); //$NON-NLS-1$
+ mFrameworkClassLoader = new AndroidJarLoader(jarfilePath);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mFrameworkClassLoader = null;
+ System.gc();
+ }
+
+ /** Preloads classes. They should load just fine. */
+ public final void testPreLoadClasses() throws Exception {
+ mFrameworkClassLoader.preLoadClasses("jar.example.", null, null); //$NON-NLS-1$
+ HashMap<String, Class<?>> map = getPrivateClassCache();
+ assertEquals(0, map.size());
+ HashMap<String,byte[]> data = getPrivateEntryCache();
+ assertTrue(data.containsKey("jar.example.Class1")); //$NON-NLS-1$
+ assertTrue(data.containsKey("jar.example.Class2")); //$NON-NLS-1$
+ assertTrue(data.containsKey("jar.example.Class1$InnerStaticClass1")); //$NON-NLS-1$
+ assertTrue(data.containsKey("jar.example.Class1$InnerClass2")); //$NON-NLS-1$
+ assertEquals(4, data.size());
+ }
+
+ /** Preloads a class not in the JAR. Preloading does nothing in this case. */
+ public final void testPreLoadClasses_classNotFound() throws Exception {
+ mFrameworkClassLoader.preLoadClasses("not.a.package.", null, null); //$NON-NLS-1$
+ HashMap<String, Class<?>> map = getPrivateClassCache();
+ assertEquals(0, map.size());
+ HashMap<String,byte[]> data = getPrivateEntryCache();
+ assertEquals(0, data.size());
+ }
+
+ /** Finds a class we just preloaded. It should work. */
+ public final void testFindClass_classFound() throws Exception {
+ Class<?> c = _findClass(mFrameworkClassLoader, "jar.example.Class2"); //$NON-NLS-1$
+ assertEquals("jar.example.Class2", c.getName()); //$NON-NLS-1$
+ HashMap<String, Class<?>> map = getPrivateClassCache();
+ assertTrue(map.containsKey("jar.example.Class1")); //$NON-NLS-1$
+ assertTrue(map.containsKey("jar.example.Class2")); //$NON-NLS-1$
+ assertEquals(2, map.size());
+ }
+
+ /** call the protected method findClass */
+ private Class<?> _findClass(AndroidJarLoader jarLoader, String name) throws Exception {
+ Method findClassMethod = AndroidJarLoader.class.getDeclaredMethod(
+ "findClass", String.class); //$NON-NLS-1$
+ findClassMethod.setAccessible(true);
+ try {
+ return (Class<?>)findClassMethod.invoke(jarLoader, name);
+ }
+ catch (InvocationTargetException e) {
+ throw (Exception)e.getCause();
+ }
+ }
+
+ /** Trying to find a class that we fail to preload should throw a CNFE. */
+ public final void testFindClass_classNotFound() throws Exception {
+ try {
+ // Will throw ClassNotFoundException
+ _findClass(mFrameworkClassLoader, "not.a.valid.ClassName"); //$NON-NLS-1$
+ } catch (ClassNotFoundException e) {
+ // check the message in the CNFE
+ assertEquals("not.a.valid.ClassName", e.getMessage()); //$NON-NLS-1$
+ return;
+ }
+ // Exception not thrown - this is a failure
+ fail("Expected ClassNotFoundException not thrown");
+ }
+
+ public final void testFindClassesDerivingFrom() throws Exception {
+ HashMap<String, ArrayList<IClassDescriptor>> found =
+ mFrameworkClassLoader.findClassesDerivingFrom("jar.example.", new String[] { //$NON-NLS-1$
+ "jar.example.Class1", //$NON-NLS-1$
+ "jar.example.Class2" }); //$NON-NLS-1$
+
+ assertTrue(found.containsKey("jar.example.Class1")); //$NON-NLS-1$
+ assertTrue(found.containsKey("jar.example.Class2")); //$NON-NLS-1$
+ assertEquals(2, found.size());
+ // Only Class2 derives from Class1..
+ // Class1 and Class1$InnerStaticClass1 derive from Object and are thus ignored.
+ // Class1$InnerClass2 should never be seen either.
+ assertEquals("jar.example.Class2", //$NON-NLS-1$
+ found.get("jar.example.Class1").get(0).getCanonicalName()); //$NON-NLS-1$
+ assertEquals(1, found.get("jar.example.Class1").size()); //$NON-NLS-1$
+ assertEquals(0, found.get("jar.example.Class2").size()); //$NON-NLS-1$
+ }
+
+ // --- Utilities ---
+
+ /**
+ * Retrieves the private mFrameworkClassLoader.mClassCache field using reflection.
+ *
+ * @throws NoSuchFieldException
+ * @throws SecurityException
+ * @throws IllegalAccessException
+ * @throws IllegalArgumentException
+ */
+ @SuppressWarnings("unchecked")
+ private HashMap<String, Class<?> > getPrivateClassCache()
+ throws SecurityException, NoSuchFieldException,
+ IllegalArgumentException, IllegalAccessException {
+ Field field = AndroidJarLoader.class.getDeclaredField("mClassCache"); //$NON-NLS-1$
+ field.setAccessible(true);
+ return (HashMap<String, Class<?>>) field.get(mFrameworkClassLoader);
+ }
+
+ /**
+ * Retrieves the private mFrameworkClassLoader.mEntryCache field using reflection.
+ *
+ * @throws NoSuchFieldException
+ * @throws SecurityException
+ * @throws IllegalAccessException
+ * @throws IllegalArgumentException
+ */
+ @SuppressWarnings("unchecked")
+ private HashMap<String,byte[]> getPrivateEntryCache()
+ throws SecurityException, NoSuchFieldException,
+ IllegalArgumentException, IllegalAccessException {
+ Field field = AndroidJarLoader.class.getDeclaredField("mEntryCache"); //$NON-NLS-1$
+ field.setAccessible(true);
+ return (HashMap<String, byte[]>) field.get(mFrameworkClassLoader);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/sdk/LayoutParamsParserTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/sdk/LayoutParamsParserTest.java
new file mode 100644
index 0000000..cedf4d4
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/adt/sdk/LayoutParamsParserTest.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.sdk;
+
+import com.android.ide.eclipse.adt.sdk.AndroidJarLoader.ClassWrapper;
+import com.android.ide.eclipse.adt.sdk.IAndroidClassLoader.IClassDescriptor;
+import com.android.ide.eclipse.common.resources.AttrsXmlParser;
+import com.android.ide.eclipse.common.resources.ViewClassInfo;
+import com.android.ide.eclipse.common.resources.ViewClassInfo.LayoutParamsInfo;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.TreeMap;
+
+import junit.framework.TestCase;
+
+/**
+ * Test the inner private methods of PlatformDataParser.
+ *
+ * Convention: method names that start with an underscore are actually local wrappers
+ * that call private methods from {@link AndroidTargetParser} using reflection.
+ * This is inspired by the Python coding rule which mandates underscores prefixes for
+ * "private" methods.
+ */
+public class LayoutParamsParserTest extends TestCase {
+
+ private static class MockFrameworkClassLoader extends AndroidJarLoader {
+ MockFrameworkClassLoader() {
+ super(null /* osFrameworkLocation */);
+ }
+
+ @Override
+ public HashMap<String, ArrayList<IClassDescriptor>> findClassesDerivingFrom(
+ String rootPackage, String[] superClasses) throws ClassFormatError {
+ return new HashMap<String, ArrayList<IClassDescriptor>>();
+ }
+ }
+
+ private static class MockAttrsXmlPath {
+ public String getPath() {
+ ClassLoader cl = this.getClass().getClassLoader();
+ URL res = cl.getResource("data/mock_attrs.xml"); //$NON-NLS-1$
+ return res.getFile();
+ }
+ }
+
+ private static class MockLayoutParamsParser extends LayoutParamsParser {
+ public MockLayoutParamsParser() {
+ super(new MockFrameworkClassLoader(),
+ new AttrsXmlParser(new MockAttrsXmlPath().getPath()).preload());
+
+ mTopViewClass = new ClassWrapper(mock_android.view.View.class);
+ mTopGroupClass = new ClassWrapper(mock_android.view.ViewGroup.class);
+ mTopLayoutParamsClass = new ClassWrapper(mock_android.view.ViewGroup.LayoutParams.class);
+
+ mViewList = new ArrayList<IClassDescriptor>();
+ mGroupList = new ArrayList<IClassDescriptor>();
+ mViewMap = new TreeMap<String, ExtViewClassInfo>();
+ mGroupMap = new TreeMap<String, ExtViewClassInfo>();
+ mLayoutParamsMap = new HashMap<String, LayoutParamsInfo>();
+ }
+ }
+
+ private MockLayoutParamsParser mParser;
+
+ @Override
+ public void setUp() throws Exception {
+ mParser = new MockLayoutParamsParser();
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ }
+
+ public final void testFindLayoutParams() throws Exception {
+ assertEquals(mock_android.view.ViewGroup.LayoutParams.class,
+ ((ClassWrapper)_findLayoutParams(mock_android.view.ViewGroup.class)).wrappedClass());
+
+ assertEquals(mock_android.widget.LinearLayout.LayoutParams.class,
+ ((ClassWrapper)_findLayoutParams(mock_android.widget.LinearLayout.class)).wrappedClass());
+
+ assertEquals(mock_android.widget.TableLayout.LayoutParams.class,
+ ((ClassWrapper)_findLayoutParams(mock_android.widget.TableLayout.class)).wrappedClass());
+ }
+
+ public final void testGetLayoutParamsInfo() throws Exception {
+ LayoutParamsInfo info1 = _getLayoutParamsInfo(
+ mock_android.view.ViewGroup.LayoutParams.class);
+ assertNotNull(info1);
+ // ViewGroup.LayoutData has Object for superClass, which we don't map
+ assertNull(info1.getSuperClass());
+
+ LayoutParamsInfo info2 = _getLayoutParamsInfo(
+ mock_android.widget.LinearLayout.LayoutParams.class);
+ assertNotNull(info2);
+ // LinearLayout.LayoutData links to ViewGroup.LayoutParams
+ assertSame(info1, info2.getSuperClass());
+
+ LayoutParamsInfo info3 = _getLayoutParamsInfo(
+ mock_android.widget.TableLayout.LayoutParams.class);
+ assertNotNull(info3);
+ // TableLayout.LayoutData does not link to ViewGroup.LayoutParams nor
+ // LinearLayout.LayoutParams
+ assertNotSame(info1, info3.getSuperClass());
+ assertNotSame(info2, info3.getSuperClass());
+ // TableLayout.LayoutParams => ViewGroup.MarginLayoutParams => ViewGroup.LayoutParams
+ assertSame(info1, info3.getSuperClass().getSuperClass());
+ }
+
+ public final void testGetLayoutClasses() throws Exception {
+ // _getLayoutClasses();
+ }
+
+ //---- access to private methods
+
+ /** Calls the private constructor of the parser */
+ @SuppressWarnings("unused")
+ private AndroidTargetParser _Constructor(String osJarPath) throws Exception {
+ Constructor<AndroidTargetParser> constructor =
+ AndroidTargetParser.class.getDeclaredConstructor(String.class);
+ constructor.setAccessible(true);
+ return constructor.newInstance(osJarPath);
+ }
+
+ /** calls the private getLayoutClasses() of the parser */
+ @SuppressWarnings("unused")
+ private void _getLayoutClasses() throws Exception {
+ Method method = AndroidTargetParser.class.getDeclaredMethod("getLayoutClasses"); //$NON-NLS-1$
+ method.setAccessible(true);
+ method.invoke(mParser);
+ }
+
+ /** calls the private addGroup() of the parser */
+ @SuppressWarnings("unused")
+ private ViewClassInfo _addGroup(Class<?> groupClass) throws Exception {
+ Method method = LayoutParamsParser.class.getDeclaredMethod("addGroup", //$NON-NLS-1$
+ IClassDescriptor.class);
+ method.setAccessible(true);
+ return (ViewClassInfo) method.invoke(mParser, new ClassWrapper(groupClass));
+ }
+
+ /** calls the private addLayoutParams() of the parser */
+ @SuppressWarnings("unused")
+ private LayoutParamsInfo _addLayoutParams(Class<?> groupClass) throws Exception {
+ Method method = LayoutParamsParser.class.getDeclaredMethod("addLayoutParams", //$NON-NLS-1$
+ IClassDescriptor.class);
+ method.setAccessible(true);
+ return (LayoutParamsInfo) method.invoke(mParser, new ClassWrapper(groupClass));
+ }
+
+ /** calls the private getLayoutParamsInfo() of the parser */
+ private LayoutParamsInfo _getLayoutParamsInfo(Class<?> layoutParamsClass) throws Exception {
+ Method method = LayoutParamsParser.class.getDeclaredMethod("getLayoutParamsInfo", //$NON-NLS-1$
+ IClassDescriptor.class);
+ method.setAccessible(true);
+ return (LayoutParamsInfo) method.invoke(mParser, new ClassWrapper(layoutParamsClass));
+ }
+
+ /** calls the private findLayoutParams() of the parser */
+ private IClassDescriptor _findLayoutParams(Class<?> groupClass) throws Exception {
+ Method method = LayoutParamsParser.class.getDeclaredMethod("findLayoutParams", //$NON-NLS-1$
+ IClassDescriptor.class);
+ method.setAccessible(true);
+ return (IClassDescriptor) method.invoke(mParser, new ClassWrapper(groupClass));
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/common/project/AndroidManifestHelperTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/common/project/AndroidManifestHelperTest.java
new file mode 100644
index 0000000..6604264
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/common/project/AndroidManifestHelperTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.common.project;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+
+import junit.framework.TestCase;
+
+public class AndroidManifestHelperTest extends TestCase {
+ private File mFile;
+ private AndroidManifestHelper mManifest;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mFile = File.createTempFile("androidManifest", "xml"); //$NON-NLS-1$ //$NON-NLS-2$
+ assertNotNull(mFile);
+
+ FileWriter fw = new FileWriter(mFile);
+ fw.write("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"); //$NON-NLS-1$
+ fw.write("<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n"); //$NON-NLS-1$
+ fw.write(" package=\"com.android.testapp\">\n"); //$NON-NLS-1$
+ fw.write(" <application android:icon=\"@drawable/icon\">\n"); //$NON-NLS-1$
+ fw.write(" <activity android:name=\".MainActivity\" android:label=\"@string/app_name\">\n"); //$NON-NLS-1$
+ fw.write(" <intent-filter>\n"); //$NON-NLS-1$
+ fw.write(" <action android:name=\"android.intent.action.MAIN\" />\n"); //$NON-NLS-1$
+ fw.write(" <category android:name=\"android.intent.category.LAUNCHER\" />\"\n"); //$NON-NLS-1$
+ fw.write(" <category android:name=\"android.intent.category.DEFAULT\" />\n"); //$NON-NLS-1$
+ fw.write(" </intent-filter>\n"); //$NON-NLS-1$
+ fw.write(" </activity>\n"); //$NON-NLS-1$
+ fw.write(" <activity android:name=\".OptionsActivity\" android:label=\"@string/options\"\n"); //$NON-NLS-1$
+ fw.write(" android:theme=\"@style/Theme.Floating\">\n"); //$NON-NLS-1$
+ fw.write(" <intent-filter>\n"); //$NON-NLS-1$
+ fw.write(" <action android:name=\"com.android.mandelbrot.action.EDIT_OPTIONS\" />\n"); //$NON-NLS-1$
+ fw.write(" <category android:name=\"android.intent.category.PREFERENCE_CATEGORY\" />\n"); //$NON-NLS-1$
+ fw.write(" </intent-filter>\n"); //$NON-NLS-1$
+ fw.write(" </activity>\n"); //$NON-NLS-1$
+ fw.write(" <activity android:name=\".InfoActivity\" android:label=\"@string/options\"\n"); //$NON-NLS-1$
+ fw.write(" android:theme=\"@style/Theme.Floating\">\n"); //$NON-NLS-1$
+ fw.write(" <intent-filter>\n"); //$NON-NLS-1$
+ fw.write(" <action android:name=\"com.android.mandelbrot.action.DISPLAY_INFO\" />\n"); //$NON-NLS-1$
+ fw.write(" </intent-filter>\n"); //$NON-NLS-1$
+ fw.write(" </activity>\n"); //$NON-NLS-1$
+ fw.write(" </application>\n"); //$NON-NLS-1$
+ fw.write("</manifest>\n"); //$NON-NLS-1$
+ fw.flush();
+ fw.close();
+
+ mManifest = new AndroidManifestHelper(mFile.getAbsolutePath());
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ assertTrue(mFile.delete());
+ super.tearDown();
+ }
+
+ public void testExists() {
+ assertTrue(mManifest.exists());
+ }
+
+ public void testNotExists() throws IOException {
+ File f = File.createTempFile("androidManifest2", "xml"); //$NON-NLS-1$ //$NON-NLS-2$
+ assertTrue(f.delete());
+ AndroidManifestHelper manifest = new AndroidManifestHelper(f.getAbsolutePath());
+ assertFalse(manifest.exists());
+ }
+
+ public void testGetPackageName() {
+ assertEquals("com.android.testapp", mManifest.getPackageName());
+ }
+
+ public void testGetActivityName() {
+ assertEquals("", mManifest.getActivityName(0)); //$NON-NLS-1$
+ assertEquals(".MainActivity", mManifest.getActivityName(1)); //$NON-NLS-1$
+ assertEquals(".OptionsActivity", mManifest.getActivityName(2)); //$NON-NLS-1$
+ assertEquals(".InfoActivity", mManifest.getActivityName(3)); //$NON-NLS-1$
+ assertEquals("", mManifest.getActivityName(4)); //$NON-NLS-1$
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/common/resources/AttrsXmlParserTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/common/resources/AttrsXmlParserTest.java
new file mode 100644
index 0000000..8338453
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/common/resources/AttrsXmlParserTest.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.common.resources;
+
+
+import com.android.ide.eclipse.common.resources.DeclareStyleableInfo.AttributeInfo;
+import com.android.ide.eclipse.common.resources.DeclareStyleableInfo.AttributeInfo.Format;
+import com.android.ide.eclipse.tests.AdtTestData;
+
+import org.w3c.dom.Document;
+
+import java.lang.reflect.Method;
+import java.util.Map;
+
+import junit.framework.TestCase;
+
+public class AttrsXmlParserTest extends TestCase {
+
+ private AttrsXmlParser mParser;
+ private String mFilePath;
+
+ @Override
+ public void setUp() throws Exception {
+ mFilePath = AdtTestData.getInstance().getTestFilePath("mock_attrs.xml"); //$NON-NLS-1$
+ mParser = new AttrsXmlParser(mFilePath);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ }
+
+ public final void testGetDocument() throws Exception {
+ assertNotNull(_getDocument());
+ }
+
+ public void testGetOsAttrsXmlPath() throws Exception {
+ assertEquals(mFilePath, mParser.getOsAttrsXmlPath());
+ }
+
+ public final void testPreload() throws Exception {
+ assertSame(mParser, mParser.preload());
+ }
+
+
+ public final void testLoadViewAttributes() throws Exception {
+ mParser.preload();
+ ViewClassInfo info = new ViewClassInfo(
+ false /* isLayout */,
+ "mock_android.something.Theme", //$NON-NLS-1$
+ "Theme"); //$NON-NLS-1$
+ mParser.loadViewAttributes(info);
+
+ assertEquals("These are the standard attributes that make up a complete theme.", //$NON-NLS-1$
+ info.getJavaDoc());
+ AttributeInfo[] attrs = info.getAttributes();
+ assertEquals(1, attrs.length);
+ assertEquals("scrollbarSize", info.getAttributes()[0].getName());
+ assertEquals(1, info.getAttributes()[0].getFormats().length);
+ assertEquals(Format.DIMENSION, info.getAttributes()[0].getFormats()[0]);
+ }
+
+ public final void testEnumFlagValues() throws Exception {
+ /* The XML being read contains:
+ <!-- Standard orientation constant. -->
+ <attr name="orientation">
+ <!-- Defines an horizontal widget. -->
+ <enum name="horizontal" value="0" />
+ <!-- Defines a vertical widget. -->
+ <enum name="vertical" value="1" />
+ </attr>
+ */
+
+ mParser.preload();
+ Map<String, Map<String, Integer>> attrMap = mParser.getEnumFlagValues();
+ assertTrue(attrMap.containsKey("orientation"));
+
+ Map<String, Integer> valueMap = attrMap.get("orientation");
+ assertTrue(valueMap.containsKey("horizontal"));
+ assertTrue(valueMap.containsKey("vertical"));
+ assertEquals(Integer.valueOf(0), valueMap.get("horizontal"));
+ assertEquals(Integer.valueOf(1), valueMap.get("vertical"));
+ }
+
+ public final void testDeprecated() throws Exception {
+ mParser.preload();
+
+ DeclareStyleableInfo dep = mParser.getDeclareStyleableList().get("DeprecatedTest");
+ assertNotNull(dep);
+
+ AttributeInfo[] attrs = dep.getAttributes();
+ assertEquals(4, attrs.length);
+
+ assertEquals("deprecated-inline", attrs[0].getName());
+ assertEquals("In-line deprecated.", attrs[0].getDeprecatedDoc());
+ assertEquals("Deprecated comments using delimiters.", attrs[0].getJavaDoc());
+
+ assertEquals("deprecated-multiline", attrs[1].getName());
+ assertEquals("Multi-line version of deprecated that works till the next tag.",
+ attrs[1].getDeprecatedDoc());
+ assertEquals("Deprecated comments on their own line.", attrs[1].getJavaDoc());
+
+ assertEquals("deprecated-not", attrs[2].getName());
+ assertEquals(null, attrs[2].getDeprecatedDoc());
+ assertEquals("This attribute is not deprecated.", attrs[2].getJavaDoc());
+
+ assertEquals("deprecated-no-javadoc", attrs[3].getName());
+ assertEquals("There is no other javadoc here.", attrs[3].getDeprecatedDoc());
+ assertEquals("", attrs[3].getJavaDoc());
+ }
+
+ //---- access to private methods
+
+ private Document _getDocument() throws Exception {
+ Method method = AttrsXmlParser.class.getDeclaredMethod("getDocument"); //$NON-NLS-1$
+ method.setAccessible(true);
+ return (Document) method.invoke(mParser);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/descriptors/DescriptorsUtilsTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/descriptors/DescriptorsUtilsTest.java
new file mode 100644
index 0000000..69c3ed8
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/descriptors/DescriptorsUtilsTest.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.ide.eclipse.editors.descriptors;
+
+import junit.framework.TestCase;
+
+/**
+ * Unit tests for DescriptorsUtils in the editors plugin
+ */
+public class DescriptorsUtilsTest extends TestCase {
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ public void testPrettyAttributeUiName() {
+ assertEquals("", DescriptorsUtils.prettyAttributeUiName(""));
+
+ assertEquals("Max width for view",
+ DescriptorsUtils.prettyAttributeUiName("maxWidthForView"));
+
+ assertEquals("Layout width",
+ DescriptorsUtils.prettyAttributeUiName("layout_width"));
+
+ // X Y and Z are capitalized when used as single words (so "T" becomes "t")
+ assertEquals("Axis X", DescriptorsUtils.prettyAttributeUiName("axisX"));
+ assertEquals("Axis Y", DescriptorsUtils.prettyAttributeUiName("axisY"));
+ assertEquals("Axis Z", DescriptorsUtils.prettyAttributeUiName("axisZ"));
+ assertEquals("Axis t", DescriptorsUtils.prettyAttributeUiName("axisT"));
+
+ assertEquals("The X axis", DescriptorsUtils.prettyAttributeUiName("theXAxis"));
+ assertEquals("The Y axis", DescriptorsUtils.prettyAttributeUiName("theYAxis"));
+ assertEquals("The Z axis", DescriptorsUtils.prettyAttributeUiName("theZAxis"));
+ assertEquals("The t axis", DescriptorsUtils.prettyAttributeUiName("theTAxis"));
+ }
+
+ public void testCapitalize() {
+ assertEquals("UPPER", DescriptorsUtils.capitalize("UPPER"));
+ assertEquals("Lower", DescriptorsUtils.capitalize("lower"));
+ assertEquals("Capital", DescriptorsUtils.capitalize("Capital"));
+ assertEquals("CamelCase", DescriptorsUtils.capitalize("camelCase"));
+ assertEquals("", DescriptorsUtils.capitalize(""));
+ }
+
+ public void testFormatTooltip() {
+ assertEquals("", DescriptorsUtils.formatTooltip(""));
+
+ assertEquals("\"application\"",
+ DescriptorsUtils.formatTooltip(
+ "<code>application</code>"));
+
+ assertEquals("android.content.Intent",
+ DescriptorsUtils.formatTooltip(
+ "{@link android.content.Intent}"));
+
+ assertEquals("FLAG_ACTIVITY_SINGLE_TOP",
+ DescriptorsUtils.formatTooltip(
+ "{@link android.content.Intent#FLAG_ACTIVITY_SINGLE_TOP}"));
+
+ assertEquals("activity-alias",
+ DescriptorsUtils.formatTooltip(
+ "{@link \t #AndroidManifestActivityAlias \tactivity-alias }"));
+
+ assertEquals("\"permission\"",
+ DescriptorsUtils.formatTooltip(
+ "{@link #AndroidManifestPermission &lt;permission&gt;}"));
+
+ assertEquals("and etc.",
+ DescriptorsUtils.formatTooltip(
+ "{@link #IntentCategory <category> and etc. }"));
+
+ assertEquals("Activity.onNewIntent()",
+ DescriptorsUtils.formatTooltip(
+ "{@link android.app.Activity#onNewIntent Activity.onNewIntent()}"));
+ }
+
+ public void testFormatFormText() {
+ ElementDescriptor desc = new ElementDescriptor("application");
+ desc.setSdkUrl(DescriptorsUtils.MANIFEST_SDK_URL + "TagApplication");
+ String docBaseUrl = "http://base";
+ assertEquals("<form><li style=\"image\" value=\"image\"></li></form>", DescriptorsUtils.formatFormText("", desc, docBaseUrl));
+
+ assertEquals("<form><li style=\"image\" value=\"image\"><a href=\"http://base/reference/android/R.styleable.html#TagApplication\">application</a></li></form>",
+ DescriptorsUtils.formatFormText(
+ "<code>application</code>",
+ desc, docBaseUrl));
+
+ assertEquals("<form><li style=\"image\" value=\"image\"><b>android.content.Intent</b></li></form>",
+ DescriptorsUtils.formatFormText(
+ "{@link android.content.Intent}",
+ desc, docBaseUrl));
+
+ assertEquals("<form><li style=\"image\" value=\"image\"><a href=\"http://base/reference/android/R.styleable.html#AndroidManifestPermission\">AndroidManifestPermission</a></li></form>",
+ DescriptorsUtils.formatFormText(
+ "{@link #AndroidManifestPermission}",
+ desc, docBaseUrl));
+
+ assertEquals("<form><li style=\"image\" value=\"image\"><a href=\"http://base/reference/android/R.styleable.html#AndroidManifestPermission\">\"permission\"</a></li></form>",
+ DescriptorsUtils.formatFormText(
+ "{@link #AndroidManifestPermission &lt;permission&gt;}",
+ desc, docBaseUrl));
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/layout/UiElementPullParserTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/layout/UiElementPullParserTest.java
new file mode 100644
index 0000000..b0deda0
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/layout/UiElementPullParserTest.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.layout;
+
+import com.android.ide.eclipse.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.editors.descriptors.TextAttributeDescriptor;
+import com.android.ide.eclipse.editors.mock.MockXmlNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+import com.android.sdklib.SdkConstants;
+
+import org.w3c.dom.Node;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.util.HashMap;
+
+import junit.framework.TestCase;
+
+public class UiElementPullParserTest extends TestCase {
+
+ private UiElementNode ui;
+ private HashMap<String, String> button1Map;
+ private HashMap<String, String> button2Map;
+ private HashMap<String, String> textMap;
+
+ @Override
+ protected void setUp() throws Exception {
+ // set up some basic descriptors.
+ // We have button, textview, linear layout, relative layout.
+ // only the layouts have children (all 4 descriptors possible)
+ // Also add some dummy attributes.
+ ElementDescriptor buttonDescriptor = new ElementDescriptor("Button", "Button", "", "",
+ new AttributeDescriptor[] {
+ new TextAttributeDescriptor("name", "name", SdkConstants.NS_RESOURCES, ""),
+ new TextAttributeDescriptor("text", "text", SdkConstants.NS_RESOURCES, ""),
+ },
+ new ElementDescriptor[] {}, false);
+
+ ElementDescriptor textDescriptor = new ElementDescriptor("TextView", "TextView", "", "",
+ new AttributeDescriptor[] {
+ new TextAttributeDescriptor("name", "name", SdkConstants.NS_RESOURCES, ""),
+ new TextAttributeDescriptor("text", "text", SdkConstants.NS_RESOURCES, ""), },
+ new ElementDescriptor[] {}, false);
+
+ ElementDescriptor linearDescriptor = new ElementDescriptor("LinearLayout", "Linear Layout",
+ "", "",
+ new AttributeDescriptor[] {
+ new TextAttributeDescriptor("orientation", "orientation",
+ SdkConstants.NS_RESOURCES, ""),
+ },
+ new ElementDescriptor[] { }, false);
+
+ ElementDescriptor relativeDescriptor = new ElementDescriptor("RelativeLayout",
+ "Relative Layout", "", "",
+ new AttributeDescriptor[] {
+ new TextAttributeDescriptor("orientation", "orientation",
+ SdkConstants.NS_RESOURCES, ""),
+ },
+ new ElementDescriptor[] { }, false);
+
+ ElementDescriptor[] a = new ElementDescriptor[] {
+ buttonDescriptor, textDescriptor, linearDescriptor, relativeDescriptor
+ };
+
+ linearDescriptor.setChildren(a);
+ relativeDescriptor.setChildren(a);
+
+ // document descriptor
+ ElementDescriptor rootDescriptor = new ElementDescriptor("root", "", "", "",
+ new AttributeDescriptor[] { }, a, false);
+
+
+ ui = new UiElementNode(rootDescriptor);
+
+ /* create a dummy XML file.
+ * <LinearLayout android:orientation="vertical">
+ * <Button android:name="button1" android:text="button1text"/>
+ * <RelativeLayout android:orientation="toto">
+ * <Button android:name="button2" android:text="button2text"/>
+ * <TextView android:name="text1" android:text="text1text"/>
+ * </RelativeLayout>
+ * </LinearLayout>
+ */
+ MockXmlNode button1 = new MockXmlNode(null /* namespace */, "Button", Node.ELEMENT_NODE,
+ null);
+ button1.addAttributes(SdkConstants.NS_RESOURCES, "name", "button1");
+ button1.addAttributes(SdkConstants.NS_RESOURCES, "text", "button1text");
+
+ // create a map of the attributes we add to the multi-attribute nodes so that
+ // we can more easily test the values when we parse the XML.
+ // This is due to some attributes showing in a certain order for a node and in a different
+ // order in another node. Since the order doesn't matter, we just simplify the test.
+ button1Map = new HashMap<String, String>();
+ button1Map.put("name", "button1");
+ button1Map.put("text", "button1text");
+
+ MockXmlNode button2 = new MockXmlNode(null /* namespace */, "Button", Node.ELEMENT_NODE,
+ null);
+ button2.addAttributes(SdkConstants.NS_RESOURCES, "name", "button2");
+ button2.addAttributes(SdkConstants.NS_RESOURCES, "text", "button2text");
+
+ button2Map = new HashMap<String, String>();
+ button2Map.put("name", "button2");
+ button2Map.put("text", "button2text");
+
+ MockXmlNode text = new MockXmlNode(null /* namespace */, "TextView", Node.ELEMENT_NODE,
+ null);
+ text.addAttributes(SdkConstants.NS_RESOURCES, "name", "text1");
+ text.addAttributes(SdkConstants.NS_RESOURCES, "text", "text1text");
+
+ textMap = new HashMap<String, String>();
+ textMap.put("name", "text1");
+ textMap.put("text", "text1text");
+
+ MockXmlNode relative = new MockXmlNode(null /* namespace */, "RelativeLayout",
+ Node.ELEMENT_NODE, new MockXmlNode[] { button2, text });
+ relative.addAttributes(SdkConstants.NS_RESOURCES, "orientation", "toto");
+
+ MockXmlNode linear = new MockXmlNode(null /* namespace */, "LinearLayout",
+ Node.ELEMENT_NODE, new MockXmlNode[] { button1, relative });
+ linear.addAttributes(SdkConstants.NS_RESOURCES, "orientation", "vertical");
+
+ MockXmlNode root = new MockXmlNode(null /* namespace */, "root", Node.ELEMENT_NODE,
+ new MockXmlNode[] { linear });
+
+ // put the namespace/prefix in place
+ root.setPrefix(SdkConstants.NS_RESOURCES, "android");
+
+ // load the xml into the UiElementNode
+ ui.loadFromXmlNode(root);
+
+ super.setUp();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ public void testParser() {
+ try {
+ // wrap the parser around the ui element node, and start parsing
+ UiElementPullParser parser = new UiElementPullParser(ui);
+
+ assertEquals(XmlPullParser.START_DOCUMENT, parser.getEventType());
+
+ // top level Linear layout
+ assertEquals(XmlPullParser.START_TAG, parser.next());
+ assertEquals("LinearLayout", parser.getName());
+ assertEquals(1, parser.getAttributeCount());
+ assertEquals("orientation", parser.getAttributeName(0));
+ assertEquals(SdkConstants.NS_RESOURCES, parser.getAttributeNamespace(0));
+ assertEquals("android", parser.getAttributePrefix(0));
+ assertEquals("vertical", parser.getAttributeValue(0));
+
+ // Button
+ assertEquals(XmlPullParser.START_TAG, parser.next());
+ assertEquals("Button", parser.getName());
+ assertEquals(2, parser.getAttributeCount());
+ check(parser, 0, button1Map);
+ check(parser, 1, button1Map);
+ // end of button
+ assertEquals(XmlPullParser.END_TAG, parser.next());
+
+ // Relative Layout
+ assertEquals(XmlPullParser.START_TAG, parser.next());
+ assertEquals("RelativeLayout", parser.getName());
+ assertEquals(1, parser.getAttributeCount());
+ assertEquals("orientation", parser.getAttributeName(0));
+ assertEquals(SdkConstants.NS_RESOURCES, parser.getAttributeNamespace(0));
+ assertEquals("android", parser.getAttributePrefix(0));
+ assertEquals("toto", parser.getAttributeValue(0));
+
+ // Button
+ assertEquals(XmlPullParser.START_TAG, parser.next());
+ assertEquals("Button", parser.getName());
+ assertEquals(2, parser.getAttributeCount());
+ check(parser, 0, button2Map);
+ check(parser, 1, button2Map);
+ // end of button
+ assertEquals(XmlPullParser.END_TAG, parser.next());
+
+ // TextView
+ assertEquals(XmlPullParser.START_TAG, parser.next());
+ assertEquals("TextView", parser.getName());
+ assertEquals(2, parser.getAttributeCount());
+ check(parser, 0, textMap);
+ check(parser, 1, textMap);
+ // end of TextView
+ assertEquals(XmlPullParser.END_TAG, parser.next());
+
+ // end of RelativeLayout
+ assertEquals(XmlPullParser.END_TAG, parser.next());
+
+
+ // end of top level linear layout
+ assertEquals(XmlPullParser.END_TAG, parser.next());
+
+ assertEquals(XmlPullParser.END_DOCUMENT, parser.next());
+ } catch (XmlPullParserException e) {
+ e.printStackTrace();
+ assertTrue(false);
+ }
+ }
+
+ /**
+ * Receives a {@link XmlPullParser} at the START_TAG level, and checks the i-th attribute
+ * to be present in the {@link HashMap} with the proper (name, value)
+ * @param parser
+ * @param i
+ * @param map
+ */
+ private void check(UiElementPullParser parser, int i, HashMap<String, String> map) {
+ String name = parser.getAttributeName(i);
+ String value = parser.getAttributeValue(i);
+
+ String referenceValue = map.get(name);
+ assertNotNull(referenceValue);
+ assertEquals(referenceValue, value);
+
+ assertEquals(SdkConstants.NS_RESOURCES, parser.getAttributeNamespace(i));
+ assertEquals("android", parser.getAttributePrefix(i));
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/manifest/model/UiElementNodeTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/manifest/model/UiElementNodeTest.java
new file mode 100644
index 0000000..e75d9ef
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/manifest/model/UiElementNodeTest.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.manifest.model;
+
+import com.android.ide.eclipse.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.editors.mock.MockXmlNode;
+import com.android.ide.eclipse.editors.uimodel.UiElementNode;
+
+import org.w3c.dom.Node;
+
+import java.util.Iterator;
+
+import junit.framework.TestCase;
+
+public class UiElementNodeTest extends TestCase {
+
+ private ElementDescriptor e;
+ private UiElementNode ui;
+
+ @Override
+ protected void setUp() throws Exception {
+ e = new ElementDescriptor("manifest", new ElementDescriptor[] {
+ new ElementDescriptor("application", new ElementDescriptor[] {
+ new ElementDescriptor("provider"),
+ new ElementDescriptor("activity", new ElementDescriptor[] {
+ new ElementDescriptor("intent-filter")
+ }),
+ }),
+ new ElementDescriptor("permission")
+ });
+
+ ui = new UiElementNode(e);
+
+ super.setUp();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ // pass
+ }
+
+ /**
+ * Check initialization values for ui node
+ */
+ public void testInit() {
+ assertSame(e, ui.getDescriptor());
+ assertNull(ui.getUiParent());
+ assertEquals(0, ui.getUiChildren().size());
+ assertEquals(0, ui.getUiAttributes().size());
+ }
+
+ /**
+ * loadFrom() does nothing if the root node doesn't match what's expected
+ */
+ public void testLoadFrom_InvalidRoot() {
+ assertEquals(0, ui.getUiChildren().size());
+ MockXmlNode root = new MockXmlNode(null /* namespace */, "blah", Node.ELEMENT_NODE, null);
+ ui.loadFromXmlNode(root);
+ assertEquals(0, ui.getUiChildren().size());
+ }
+
+ /**
+ * UiElementNode.loadFrom should be used to populate an empty ui node from an
+ * existing XML node tree.
+ */
+ public void testLoadFrom_NewTree_1_Node() {
+ MockXmlNode root = new MockXmlNode(null /* namespace */, "manifest", Node.ELEMENT_NODE,
+ new MockXmlNode[] {
+ new MockXmlNode(null /* namespace */, "application", Node.ELEMENT_NODE, null)
+ });
+
+ // get /manifest
+ ui.loadFromXmlNode(root);
+ assertEquals("manifest", ui.getDescriptor().getXmlName());
+ assertEquals(1, ui.getUiChildren().size());
+ assertEquals(0, ui.getUiAttributes().size());
+
+ // get /manifest/application
+ Iterator<UiElementNode> ui_child_it = ui.getUiChildren().iterator();
+ UiElementNode application = ui_child_it.next();
+ assertEquals("application", application.getDescriptor().getXmlName());
+ assertEquals(0, application.getUiChildren().size());
+ assertEquals(0, application.getUiAttributes().size());
+ }
+
+
+ public void testLoadFrom_NewTree_2_Nodes() {
+ MockXmlNode root = new MockXmlNode(null /* namespace */, "manifest", Node.ELEMENT_NODE,
+ new MockXmlNode[] {
+ new MockXmlNode(null /* namespace */, "application", Node.ELEMENT_NODE, null),
+ new MockXmlNode(null /* namespace */, "permission", Node.ELEMENT_NODE, null),
+ });
+
+ // get /manifest
+ ui.loadFromXmlNode(root);
+ assertEquals("manifest", ui.getDescriptor().getXmlName());
+ assertEquals(2, ui.getUiChildren().size());
+ assertEquals(0, ui.getUiAttributes().size());
+
+ // get /manifest/application
+ Iterator<UiElementNode> ui_child_it = ui.getUiChildren().iterator();
+ UiElementNode application = ui_child_it.next();
+ assertEquals("application", application.getDescriptor().getXmlName());
+ assertEquals(0, application.getUiChildren().size());
+ assertEquals(0, application.getUiAttributes().size());
+
+ // get /manifest/permission
+ UiElementNode first_permission = ui_child_it.next();
+ assertEquals("permission", first_permission.getDescriptor().getXmlName());
+ assertEquals(0, first_permission.getUiChildren().size());
+ assertEquals(0, first_permission.getUiAttributes().size());
+ }
+
+ public void testLoadFrom_NewTree_N_Nodes() {
+ MockXmlNode root = new MockXmlNode(null /* namespace */, "manifest", Node.ELEMENT_NODE,
+ new MockXmlNode[] {
+ new MockXmlNode(null /* namespace */, "application", Node.ELEMENT_NODE,
+ new MockXmlNode[] {
+ new MockXmlNode(null /* namespace */, "activity", Node.ELEMENT_NODE,
+ null),
+ new MockXmlNode(null /* namespace */, "activity", Node.ELEMENT_NODE,
+ new MockXmlNode[] {
+ new MockXmlNode(null /* namespace */, "intent-filter",
+ Node.ELEMENT_NODE, null),
+ }),
+ new MockXmlNode(null /* namespace */, "provider", Node.ELEMENT_NODE,
+ null),
+ new MockXmlNode(null /* namespace */, "provider", Node.ELEMENT_NODE,
+ null),
+ }),
+ new MockXmlNode(null /* namespace */, "permission", Node.ELEMENT_NODE,
+ null),
+ new MockXmlNode(null /* namespace */, "permission", Node.ELEMENT_NODE,
+ null),
+ });
+
+ // get /manifest
+ ui.loadFromXmlNode(root);
+ assertEquals("manifest", ui.getDescriptor().getXmlName());
+ assertEquals(3, ui.getUiChildren().size());
+ assertEquals(0, ui.getUiAttributes().size());
+
+ // get /manifest/application
+ Iterator<UiElementNode> ui_child_it = ui.getUiChildren().iterator();
+ UiElementNode application = ui_child_it.next();
+ assertEquals("application", application.getDescriptor().getXmlName());
+ assertEquals(4, application.getUiChildren().size());
+ assertEquals(0, application.getUiAttributes().size());
+
+ // get /manifest/application/activity #1
+ Iterator<UiElementNode> app_child_it = application.getUiChildren().iterator();
+ UiElementNode first_activity = app_child_it.next();
+ assertEquals("activity", first_activity.getDescriptor().getXmlName());
+ assertEquals(0, first_activity.getUiChildren().size());
+ assertEquals(0, first_activity.getUiAttributes().size());
+
+ // get /manifest/application/activity #2
+ UiElementNode second_activity = app_child_it.next();
+ assertEquals("activity", second_activity.getDescriptor().getXmlName());
+ assertEquals(1, second_activity.getUiChildren().size());
+ assertEquals(0, second_activity.getUiAttributes().size());
+
+ // get /manifest/application/activity #2/intent-filter #1
+ Iterator<UiElementNode> activity_child_it = second_activity.getUiChildren().iterator();
+ UiElementNode intent_filter = activity_child_it.next();
+ assertEquals("intent-filter", intent_filter.getDescriptor().getXmlName());
+ assertEquals(0, intent_filter.getUiChildren().size());
+ assertEquals(0, intent_filter.getUiAttributes().size());
+
+ // get /manifest/application/provider #1
+ UiElementNode first_provider = app_child_it.next();
+ assertEquals("provider", first_provider.getDescriptor().getXmlName());
+ assertEquals(0, first_provider.getUiChildren().size());
+ assertEquals(0, first_provider.getUiAttributes().size());
+
+ // get /manifest/application/provider #2
+ UiElementNode second_provider = app_child_it.next();
+ assertEquals("provider", second_provider.getDescriptor().getXmlName());
+ assertEquals(0, second_provider.getUiChildren().size());
+ assertEquals(0, second_provider.getUiAttributes().size());
+
+ // get /manifest/permission #1
+ UiElementNode first_permission = ui_child_it.next();
+ assertEquals("permission", first_permission.getDescriptor().getXmlName());
+ assertEquals(0, first_permission.getUiChildren().size());
+ assertEquals(0, first_permission.getUiAttributes().size());
+
+ // get /manifest/permission #1
+ UiElementNode second_permission = ui_child_it.next();
+ assertEquals("permission", second_permission.getDescriptor().getXmlName());
+ assertEquals(0, second_permission.getUiChildren().size());
+ assertEquals(0, second_permission.getUiAttributes().size());
+ }
+
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/mock/MockNamedNodeMap.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/mock/MockNamedNodeMap.java
new file mode 100644
index 0000000..b1f089b
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/mock/MockNamedNodeMap.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.mock;
+
+import org.w3c.dom.DOMException;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+
+import sun.reflect.generics.reflectiveObjects.NotImplementedException;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+class MockNamedNodeMap implements NamedNodeMap {
+
+ /** map for access by namespace/name */
+ private final HashMap<String, HashMap<String, Node>> mNodeMap =
+ new HashMap<String, HashMap<String, Node>>();
+
+ /** list for access by index */
+ private final ArrayList<Node> mNodeList = new ArrayList<Node>();
+
+ public MockXmlNode addAttribute(String namespace, String localName, String value) {
+ MockXmlNode node = new MockXmlNode(namespace, localName, value);
+
+ if (namespace == null) {
+ namespace = ""; // no namespace
+ }
+
+ // get the map for the namespace
+ HashMap<String, Node> map = mNodeMap.get(namespace);
+ if (map == null) {
+ map = new HashMap<String, Node>();
+ mNodeMap.put(namespace, map);
+ }
+
+
+ map.put(localName, node);
+ mNodeList.add(node);
+
+ return node;
+ }
+
+ // --------- NamedNodeMap -------
+
+ public int getLength() {
+ return mNodeList.size();
+ }
+
+ public Node getNamedItem(String name) {
+ HashMap<String, Node> map = mNodeMap.get(""); // no namespace
+ if (map != null) {
+ return map.get(name);
+ }
+
+ return null;
+ }
+
+ public Node getNamedItemNS(String namespaceURI, String localName) throws DOMException {
+ if (namespaceURI == null) {
+ namespaceURI = ""; //no namespace
+ }
+
+ HashMap<String, Node> map = mNodeMap.get(namespaceURI);
+ if (map != null) {
+ return map.get(localName);
+ }
+
+ return null;
+ }
+
+ public Node item(int index) {
+ return mNodeList.get(index);
+ }
+
+ public Node removeNamedItem(String name) throws DOMException {
+ throw new NotImplementedException();
+ }
+
+ public Node removeNamedItemNS(String namespaceURI, String localName) throws DOMException {
+ throw new NotImplementedException();
+ }
+
+ public Node setNamedItem(Node arg) throws DOMException {
+ throw new NotImplementedException();
+ }
+
+ public Node setNamedItemNS(Node arg) throws DOMException {
+ throw new NotImplementedException();
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/mock/MockNodeList.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/mock/MockNodeList.java
new file mode 100644
index 0000000..d766af7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/mock/MockNodeList.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.mock;
+
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.util.ArrayList;
+
+
+/**
+ * A quick mock implementation of NodeList on top of ArrayList.
+ */
+public class MockNodeList implements NodeList {
+
+ ArrayList<MockXmlNode> mChildren;
+
+ /**
+ * Constructs a node list from a given children list.
+ *
+ * @param children The children list. Can be null.
+ */
+ public MockNodeList(MockXmlNode[] children) {
+ mChildren = new ArrayList<MockXmlNode>();
+ if (children != null) {
+ for (MockXmlNode n : children) {
+ mChildren.add(n);
+ }
+ }
+ }
+
+ public int getLength() {
+ return mChildren.size();
+ }
+
+ public Node item(int index) {
+ if (index >= 0 && index < mChildren.size()) {
+ return mChildren.get(index);
+ }
+ return null;
+ }
+
+ public ArrayList<MockXmlNode> getArrayList() {
+ return mChildren;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/mock/MockXmlNode.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/mock/MockXmlNode.java
new file mode 100644
index 0000000..4a9dbbd
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/mock/MockXmlNode.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.mock;
+
+import org.w3c.dom.DOMException;
+import org.w3c.dom.Document;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.w3c.dom.UserDataHandler;
+
+import java.util.HashMap;
+
+import sun.reflect.generics.reflectiveObjects.NotImplementedException;
+
+
+/**
+ * A mock XML node with only a minimal set of information.
+ */
+public class MockXmlNode implements Node {
+
+ MockNodeList mNodeList;
+ private String mLocalName;
+ private String mNamespace;
+ private short mNodeType;
+ private MockXmlNode mParent;
+ private MockXmlNode mPreviousSibling;
+ private MockXmlNode mNextSibling;
+ private String mAttrValue;
+ private MockNamedNodeMap mAttributes;
+
+ // namespace stuff only set in the root node
+ /** map from namespace to prefix. */
+ private HashMap<String, String> mNsMap = null;
+
+ /**
+ * Constructs a node from a given children list.
+ *
+ * @param namespace The namespace of the node or null if none
+ * @param localName The XML local node name.
+ * @param node_type One of Node.xxx_NODE constants, e.g. Node.ELEMENT_NODE
+ * @param children The children list. Can be null.
+ */
+ public MockXmlNode(String namespace, String localName, short node_type,
+ MockXmlNode[] children) {
+ mLocalName = localName;
+ mNamespace = namespace;
+ mNodeType = node_type;
+ mNodeList = new MockNodeList(children);
+ fixNavigation();
+ }
+
+ /**
+ * Constructs an attribute node
+ *
+ * @param namespace The namespace of the node or null if none
+ * @param localName The XML local node name.
+ * @param value the value of the attribute
+ */
+ public MockXmlNode(String namespace, String localName, String value) {
+ mLocalName = localName;
+ mNamespace = namespace;
+ mAttrValue = value;
+ mNodeType = Node.ATTRIBUTE_NODE;
+ mNodeList = new MockNodeList(new MockXmlNode[0]);
+ fixNavigation();
+ }
+
+ private void fixNavigation() {
+ MockXmlNode prev = null;
+ for (MockXmlNode n : mNodeList.getArrayList()) {
+ n.mParent = this;
+ n.mPreviousSibling = prev;
+ if (prev != null) {
+ prev.mNextSibling = n;
+ }
+ n.fixNavigation();
+ prev = n;
+ }
+ }
+
+ public void addAttributes(String namespaceURI, String localName, String value) {
+ if (mAttributes == null) {
+ mAttributes = new MockNamedNodeMap();
+ }
+
+ MockXmlNode node = mAttributes.addAttribute(namespaceURI, localName, value);
+ node.mParent = this;
+ }
+
+ public void setPrefix(String namespace, String prefix) {
+ if (mNsMap == null) {
+ mNsMap = new HashMap<String, String>();
+ }
+
+ mNsMap.put(namespace, prefix);
+ }
+
+ public String getPrefix(String namespace) {
+ if (mNsMap != null) {
+ return mNsMap.get(namespace);
+ }
+
+ return mParent.getPrefix(namespace);
+ }
+
+
+ // ----------- Node methods
+
+ public Node appendChild(Node newChild) throws DOMException {
+ mNodeList.getArrayList().add((MockXmlNode) newChild);
+ return newChild;
+ }
+
+ public NamedNodeMap getAttributes() {
+ return mAttributes;
+ }
+
+ public NodeList getChildNodes() {
+ return mNodeList;
+ }
+
+ public Node getFirstChild() {
+ if (mNodeList.getLength() > 0) {
+ return mNodeList.item(0);
+ }
+ return null;
+ }
+
+ public Node getLastChild() {
+ if (mNodeList.getLength() > 0) {
+ return mNodeList.item(mNodeList.getLength() - 1);
+ }
+ return null;
+ }
+
+ public Node getNextSibling() {
+ return mNextSibling;
+ }
+
+ public String getNodeName() {
+ return mLocalName;
+ }
+
+ public String getLocalName() {
+ return mLocalName;
+ }
+
+ public short getNodeType() {
+ return mNodeType;
+ }
+
+ public Node getParentNode() {
+ return mParent;
+ }
+
+ public Node getPreviousSibling() {
+ return mPreviousSibling;
+ }
+
+ public boolean hasChildNodes() {
+ return mNodeList.getLength() > 0;
+ }
+
+ public boolean hasAttributes() {
+ // TODO Auto-generated method stub
+ throw new NotImplementedException();
+ //return false;
+ }
+
+ public boolean isSameNode(Node other) {
+ return this == other;
+ }
+
+ public String getNodeValue() throws DOMException {
+ return mAttrValue;
+ }
+
+ public String getPrefix() {
+ return getPrefix(getNamespaceURI());
+ }
+
+ public String getNamespaceURI() {
+ return mNamespace;
+ }
+
+
+ // --- methods not implemented ---
+
+ public Node cloneNode(boolean deep) {
+ throw new NotImplementedException();
+ }
+
+ public short compareDocumentPosition(Node other) throws DOMException {
+ throw new NotImplementedException();
+ }
+
+ public String getBaseURI() {
+ throw new NotImplementedException();
+ }
+
+ public Object getFeature(String feature, String version) {
+ throw new NotImplementedException();
+ }
+
+ public Document getOwnerDocument() {
+ throw new NotImplementedException();
+ }
+
+ public String getTextContent() throws DOMException {
+ throw new NotImplementedException();
+ }
+
+ public Object getUserData(String key) {
+ throw new NotImplementedException();
+ }
+
+ public Node insertBefore(Node newChild, Node refChild)
+ throws DOMException {
+ throw new NotImplementedException();
+ }
+
+ public boolean isDefaultNamespace(String namespaceURI) {
+ throw new NotImplementedException();
+ }
+
+ public boolean isEqualNode(Node arg) {
+ throw new NotImplementedException();
+ }
+
+ public boolean isSupported(String feature, String version) {
+ throw new NotImplementedException();
+ }
+
+ public String lookupNamespaceURI(String prefix) {
+ throw new NotImplementedException();
+ }
+
+ public String lookupPrefix(String namespaceURI) {
+ throw new NotImplementedException();
+ }
+
+ public void normalize() {
+ throw new NotImplementedException();
+ }
+
+ public Node removeChild(Node oldChild) throws DOMException {
+ throw new NotImplementedException();
+ }
+
+ public Node replaceChild(Node newChild, Node oldChild)
+ throws DOMException {
+ throw new NotImplementedException();
+ }
+
+ public void setNodeValue(String nodeValue) throws DOMException {
+ throw new NotImplementedException();
+ }
+
+ public void setPrefix(String prefix) throws DOMException {
+ throw new NotImplementedException();
+ }
+
+ public void setTextContent(String textContent) throws DOMException {
+ throw new NotImplementedException();
+ }
+
+ public Object setUserData(String key, Object data,
+ UserDataHandler handler) {
+ throw new NotImplementedException();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/CountryCodeQualifierTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/CountryCodeQualifierTest.java
new file mode 100644
index 0000000..bedaa0d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/CountryCodeQualifierTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.configurations;
+
+import junit.framework.TestCase;
+
+public class CountryCodeQualifierTest extends TestCase {
+
+ private CountryCodeQualifier mccq;
+ private FolderConfiguration config;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mccq = new CountryCodeQualifier();
+ config = new FolderConfiguration();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ mccq = null;
+ config = null;
+ }
+
+ public void testCheckAndSet() {
+ assertEquals(true, mccq.checkAndSet("mcc123", config));//$NON-NLS-1$
+ assertTrue(config.getCountryCodeQualifier() != null);
+ assertEquals(123, config.getCountryCodeQualifier().getCode());
+ assertEquals("mcc123", config.getCountryCodeQualifier().toString()); //$NON-NLS-1$
+ }
+
+ public void testFailures() {
+ assertEquals(false, mccq.checkAndSet("", config));//$NON-NLS-1$
+ assertEquals(false, mccq.checkAndSet("mcc", config));//$NON-NLS-1$
+ assertEquals(false, mccq.checkAndSet("MCC123", config));//$NON-NLS-1$
+ assertEquals(false, mccq.checkAndSet("123", config));//$NON-NLS-1$
+ assertEquals(false, mccq.checkAndSet("mccsdf", config));//$NON-NLS-1$
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/KeyboardStateQualifierTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/KeyboardStateQualifierTest.java
new file mode 100644
index 0000000..e15434e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/KeyboardStateQualifierTest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.configurations;
+
+import junit.framework.TestCase;
+
+public class KeyboardStateQualifierTest extends TestCase {
+
+ private KeyboardStateQualifier ksq;
+ private FolderConfiguration config;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ ksq = new KeyboardStateQualifier();
+ config = new FolderConfiguration();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ ksq = null;
+ config = null;
+ }
+
+ public void testExposed() {
+ assertEquals(true, ksq.checkAndSet("keysexposed", config)); //$NON-NLS-1$
+ assertTrue(config.getKeyboardStateQualifier() != null);
+ assertEquals(KeyboardStateQualifier.KeyboardState.EXPOSED,
+ config.getKeyboardStateQualifier().getValue());
+ assertEquals("keysexposed", config.getKeyboardStateQualifier().toString()); //$NON-NLS-1$
+ }
+
+ public void testHidden() {
+ assertEquals(true, ksq.checkAndSet("keyshidden", config)); //$NON-NLS-1$
+ assertTrue(config.getKeyboardStateQualifier() != null);
+ assertEquals(KeyboardStateQualifier.KeyboardState.HIDDEN,
+ config.getKeyboardStateQualifier().getValue());
+ assertEquals("keyshidden", config.getKeyboardStateQualifier().toString()); //$NON-NLS-1$
+ }
+
+ public void testFailures() {
+ assertEquals(false, ksq.checkAndSet("", config));//$NON-NLS-1$
+ assertEquals(false, ksq.checkAndSet("KEYSEXPOSED", config));//$NON-NLS-1$
+ assertEquals(false, ksq.checkAndSet("other", config));//$NON-NLS-1$
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/LanguageQualifierTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/LanguageQualifierTest.java
new file mode 100644
index 0000000..d00972f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/LanguageQualifierTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.configurations;
+
+import junit.framework.TestCase;
+
+public class LanguageQualifierTest extends TestCase {
+
+ private FolderConfiguration config;
+ private LanguageQualifier lq;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ config = new FolderConfiguration();
+ lq = new LanguageQualifier();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ config = null;
+ lq = null;
+ }
+
+ public void testCheckAndSet() {
+ assertEquals(true, lq.checkAndSet("en", config)); //$NON-NLS-1$
+ assertTrue(config.getLanguageQualifier() != null);
+ assertEquals("en", config.getLanguageQualifier().toString()); //$NON-NLS-1$
+
+ }
+
+ public void testFailures() {
+ assertEquals(false, lq.checkAndSet("", config)); //$NON-NLS-1$
+ assertEquals(false, lq.checkAndSet("EN", config)); //$NON-NLS-1$
+ assertEquals(false, lq.checkAndSet("abc", config)); //$NON-NLS-1$
+ }
+}
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/NavigationMethodQualifierTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/NavigationMethodQualifierTest.java
new file mode 100644
index 0000000..d672f1f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/NavigationMethodQualifierTest.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.configurations;
+
+import junit.framework.TestCase;
+
+public class NavigationMethodQualifierTest extends TestCase {
+
+ private FolderConfiguration config;
+ private NavigationMethodQualifier nmq;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ config = new FolderConfiguration();
+ nmq = new NavigationMethodQualifier();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ config = null;
+ nmq = null;
+ }
+
+ public void testDPad() {
+ assertEquals(true, nmq.checkAndSet("dpad", config)); //$NON-NLS-1$
+ assertTrue(config.getNavigationMethodQualifier() != null);
+ assertEquals(NavigationMethodQualifier.NavigationMethod.DPAD,
+ config.getNavigationMethodQualifier().getValue());
+ assertEquals("dpad", config.getNavigationMethodQualifier().toString()); //$NON-NLS-1$
+ }
+
+ public void testTrackball() {
+ assertEquals(true, nmq.checkAndSet("trackball", config)); //$NON-NLS-1$
+ assertTrue(config.getNavigationMethodQualifier() != null);
+ assertEquals(NavigationMethodQualifier.NavigationMethod.TRACKBALL,
+ config.getNavigationMethodQualifier().getValue());
+ assertEquals("trackball", config.getNavigationMethodQualifier().toString()); //$NON-NLS-1$
+ }
+
+ public void testWheel() {
+ assertEquals(true, nmq.checkAndSet("wheel", config)); //$NON-NLS-1$
+ assertTrue(config.getNavigationMethodQualifier() != null);
+ assertEquals(NavigationMethodQualifier.NavigationMethod.WHEEL,
+ config.getNavigationMethodQualifier().getValue());
+ assertEquals("wheel", config.getNavigationMethodQualifier().toString()); //$NON-NLS-1$
+ }
+
+ public void testFailures() {
+ assertEquals(false, nmq.checkAndSet("", config));//$NON-NLS-1$
+ assertEquals(false, nmq.checkAndSet("WHEEL", config));//$NON-NLS-1$
+ assertEquals(false, nmq.checkAndSet("other", config));//$NON-NLS-1$
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/NetworkCodeQualifierTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/NetworkCodeQualifierTest.java
new file mode 100644
index 0000000..4567dff
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/NetworkCodeQualifierTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.configurations;
+
+import junit.framework.TestCase;
+
+public class NetworkCodeQualifierTest extends TestCase {
+
+ private NetworkCodeQualifier mncq;
+ private FolderConfiguration config;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mncq = new NetworkCodeQualifier();
+ config = new FolderConfiguration();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ mncq = null;
+ config = null;
+ }
+
+ public void testCheckAndSet() {
+ assertEquals(true, mncq.checkAndSet("mnc123", config));//$NON-NLS-1$
+ assertTrue(config.getNetworkCodeQualifier() != null);
+ assertEquals(123, config.getNetworkCodeQualifier().getCode());
+ assertEquals("mnc123", config.getNetworkCodeQualifier().toString()); //$NON-NLS-1$
+ }
+
+ public void testFailures() {
+ assertEquals(false, mncq.checkAndSet("", config));//$NON-NLS-1$
+ assertEquals(false, mncq.checkAndSet("mnc", config));//$NON-NLS-1$
+ assertEquals(false, mncq.checkAndSet("MNC123", config));//$NON-NLS-1$
+ assertEquals(false, mncq.checkAndSet("123", config));//$NON-NLS-1$
+ assertEquals(false, mncq.checkAndSet("mncsdf", config));//$NON-NLS-1$
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/PixelDensityQualifierTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/PixelDensityQualifierTest.java
new file mode 100644
index 0000000..2c4cd2f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/PixelDensityQualifierTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.configurations;
+
+import junit.framework.TestCase;
+
+public class PixelDensityQualifierTest extends TestCase {
+
+ private PixelDensityQualifier pdq;
+ private FolderConfiguration config;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ pdq = new PixelDensityQualifier();
+ config = new FolderConfiguration();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ pdq = null;
+ config = null;
+ }
+
+ public void testCheckAndSet() {
+ assertEquals(true, pdq.checkAndSet("123dpi", config));//$NON-NLS-1$
+ assertTrue(config.getPixelDensityQualifier() != null);
+ assertEquals(123, config.getPixelDensityQualifier().getValue());
+ assertEquals("123dpi", config.getPixelDensityQualifier().toString()); //$NON-NLS-1$
+ }
+
+ public void testFailures() {
+ assertEquals(false, pdq.checkAndSet("", config));//$NON-NLS-1$
+ assertEquals(false, pdq.checkAndSet("dpi", config));//$NON-NLS-1$
+ assertEquals(false, pdq.checkAndSet("123DPI", config));//$NON-NLS-1$
+ assertEquals(false, pdq.checkAndSet("123", config));//$NON-NLS-1$
+ assertEquals(false, pdq.checkAndSet("sdfdpi", config));//$NON-NLS-1$
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/RegionQualifierTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/RegionQualifierTest.java
new file mode 100644
index 0000000..8a9e6f8
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/RegionQualifierTest.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.configurations;
+
+import junit.framework.TestCase;
+
+public class RegionQualifierTest extends TestCase {
+
+ private RegionQualifier rq;
+ private FolderConfiguration config;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ rq = new RegionQualifier();
+ config = new FolderConfiguration();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ rq = null;
+ config = null;
+ }
+
+ public void testCheckAndSet() {
+ assertEquals(true, rq.checkAndSet("rUS", config));//$NON-NLS-1$
+ assertTrue(config.getRegionQualifier() != null);
+ assertEquals("US", config.getRegionQualifier().getValue()); //$NON-NLS-1$
+ assertEquals("rUS", config.getRegionQualifier().toString()); //$NON-NLS-1$
+ }
+
+ public void testFailures() {
+ assertEquals(false, rq.checkAndSet("", config));//$NON-NLS-1$
+ assertEquals(false, rq.checkAndSet("rus", config));//$NON-NLS-1$
+ assertEquals(false, rq.checkAndSet("rUSA", config));//$NON-NLS-1$
+ assertEquals(false, rq.checkAndSet("abc", config));//$NON-NLS-1$
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/ScreenDimensionQualifierTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/ScreenDimensionQualifierTest.java
new file mode 100644
index 0000000..681d4e0
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/ScreenDimensionQualifierTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.configurations;
+
+import junit.framework.TestCase;
+
+public class ScreenDimensionQualifierTest extends TestCase {
+
+ private ScreenDimensionQualifier sdq;
+ private FolderConfiguration config;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ sdq = new ScreenDimensionQualifier();
+ config = new FolderConfiguration();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ sdq = null;
+ config = null;
+ }
+
+ public void testCheckAndSet() {
+ assertEquals(true, sdq.checkAndSet("400x200", config));//$NON-NLS-1$
+ assertTrue(config.getScreenDimensionQualifier() != null);
+ assertEquals(400, config.getScreenDimensionQualifier().getValue1());
+ assertEquals(200, config.getScreenDimensionQualifier().getValue2());
+ assertEquals("400x200", config.getScreenDimensionQualifier().toString()); //$NON-NLS-1$
+ }
+
+ public void testFailures() {
+ assertEquals(false, sdq.checkAndSet("", config));//$NON-NLS-1$
+ assertEquals(false, sdq.checkAndSet("400X200", config));//$NON-NLS-1$
+ assertEquals(false, sdq.checkAndSet("x200", config));//$NON-NLS-1$
+ assertEquals(false, sdq.checkAndSet("ax200", config));//$NON-NLS-1$
+ assertEquals(false, sdq.checkAndSet("400x", config));//$NON-NLS-1$
+ assertEquals(false, sdq.checkAndSet("400xa", config));//$NON-NLS-1$
+ assertEquals(false, sdq.checkAndSet("other", config));//$NON-NLS-1$
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/ScreenOrientationQualifierTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/ScreenOrientationQualifierTest.java
new file mode 100644
index 0000000..28f9961
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/ScreenOrientationQualifierTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.configurations;
+
+import junit.framework.TestCase;
+
+public class ScreenOrientationQualifierTest extends TestCase {
+
+ private ScreenOrientationQualifier soq;
+ private FolderConfiguration config;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ soq = new ScreenOrientationQualifier();
+ config = new FolderConfiguration();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ soq = null;
+ config = null;
+ }
+
+ public void testPortrait() {
+ assertEquals(true, soq.checkAndSet("port", config)); //$NON-NLS-1$
+ assertTrue(config.getScreenOrientationQualifier() != null);
+ assertEquals(ScreenOrientationQualifier.ScreenOrientation.PORTRAIT,
+ config.getScreenOrientationQualifier().getValue());
+ assertEquals("port", config.getScreenOrientationQualifier().toString()); //$NON-NLS-1$
+ }
+
+ public void testLanscape() {
+ assertEquals(true, soq.checkAndSet("land", config)); //$NON-NLS-1$
+ assertTrue(config.getScreenOrientationQualifier() != null);
+ assertEquals(ScreenOrientationQualifier.ScreenOrientation.LANDSCAPE,
+ config.getScreenOrientationQualifier().getValue());
+ assertEquals("land", config.getScreenOrientationQualifier().toString()); //$NON-NLS-1$
+ }
+
+ public void testSquare() {
+ assertEquals(true, soq.checkAndSet("square", config)); //$NON-NLS-1$
+ assertTrue(config.getScreenOrientationQualifier() != null);
+ assertEquals(ScreenOrientationQualifier.ScreenOrientation.SQUARE,
+ config.getScreenOrientationQualifier().getValue());
+ assertEquals("square", config.getScreenOrientationQualifier().toString()); //$NON-NLS-1$
+ }
+
+ public void testFailures() {
+ assertEquals(false, soq.checkAndSet("", config));//$NON-NLS-1$
+ assertEquals(false, soq.checkAndSet("PORT", config));//$NON-NLS-1$
+ assertEquals(false, soq.checkAndSet("landscape", config));//$NON-NLS-1$
+ assertEquals(false, soq.checkAndSet("portrait", config));//$NON-NLS-1$
+ assertEquals(false, soq.checkAndSet("other", config));//$NON-NLS-1$
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/TextInputMethodQualifierTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/TextInputMethodQualifierTest.java
new file mode 100644
index 0000000..28f7871
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/TextInputMethodQualifierTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.configurations;
+
+import junit.framework.TestCase;
+
+public class TextInputMethodQualifierTest extends TestCase {
+
+ private TextInputMethodQualifier timq;
+ private FolderConfiguration config;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ timq = new TextInputMethodQualifier();
+ config = new FolderConfiguration();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ timq = null;
+ config = null;
+ }
+
+ public void testQuerty() {
+ assertEquals(true, timq.checkAndSet("qwerty", config)); //$NON-NLS-1$
+ assertTrue(config.getTextInputMethodQualifier() != null);
+ assertEquals(TextInputMethodQualifier.TextInputMethod.QWERTY,
+ config.getTextInputMethodQualifier().getValue());
+ assertEquals("qwerty", config.getTextInputMethodQualifier().toString()); //$NON-NLS-1$
+ }
+
+ public void test12Key() {
+ assertEquals(true, timq.checkAndSet("12key", config)); //$NON-NLS-1$
+ assertTrue(config.getTextInputMethodQualifier() != null);
+ assertEquals(TextInputMethodQualifier.TextInputMethod.TWELVEKEYS,
+ config.getTextInputMethodQualifier().getValue());
+ assertEquals("12key", config.getTextInputMethodQualifier().toString()); //$NON-NLS-1$
+ }
+
+ public void testNoKey() {
+ assertEquals(true, timq.checkAndSet("nokeys", config)); //$NON-NLS-1$
+ assertTrue(config.getTextInputMethodQualifier() != null);
+ assertEquals(TextInputMethodQualifier.TextInputMethod.NOKEY,
+ config.getTextInputMethodQualifier().getValue());
+ assertEquals("nokeys", config.getTextInputMethodQualifier().toString()); //$NON-NLS-1$
+ }
+
+ public void testFailures() {
+ assertEquals(false, timq.checkAndSet("", config));//$NON-NLS-1$
+ assertEquals(false, timq.checkAndSet("QWERTY", config));//$NON-NLS-1$
+ assertEquals(false, timq.checkAndSet("12keys", config));//$NON-NLS-1$
+ assertEquals(false, timq.checkAndSet("*12key", config));//$NON-NLS-1$
+ assertEquals(false, timq.checkAndSet("other", config));//$NON-NLS-1$
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/TouchScreenQualifierTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/TouchScreenQualifierTest.java
new file mode 100644
index 0000000..9a788ad
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/configurations/TouchScreenQualifierTest.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.configurations;
+
+import junit.framework.TestCase;
+
+public class TouchScreenQualifierTest extends TestCase {
+
+ private TouchScreenQualifier tsq;
+ private FolderConfiguration config;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ tsq = new TouchScreenQualifier();
+ config = new FolderConfiguration();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ tsq = null;
+ config = null;
+ }
+
+ public void testNoTouch() {
+ assertEquals(true, tsq.checkAndSet("notouch", config)); //$NON-NLS-1$
+ assertTrue(config.getTouchTypeQualifier() != null);
+ assertEquals(TouchScreenQualifier.TouchScreenType.NOTOUCH,
+ config.getTouchTypeQualifier().getValue());
+ assertEquals("notouch", config.getTouchTypeQualifier().toString()); //$NON-NLS-1$
+ }
+
+ public void testFinger() {
+ assertEquals(true, tsq.checkAndSet("finger", config)); //$NON-NLS-1$
+ assertTrue(config.getTouchTypeQualifier() != null);
+ assertEquals(TouchScreenQualifier.TouchScreenType.FINGER,
+ config.getTouchTypeQualifier().getValue());
+ assertEquals("finger", config.getTouchTypeQualifier().toString()); //$NON-NLS-1$
+ }
+
+ public void testStylus() {
+ assertEquals(true, tsq.checkAndSet("stylus", config)); //$NON-NLS-1$
+ assertTrue(config.getTouchTypeQualifier() != null);
+ assertEquals(TouchScreenQualifier.TouchScreenType.STYLUS,
+ config.getTouchTypeQualifier().getValue());
+ assertEquals("stylus", config.getTouchTypeQualifier().toString()); //$NON-NLS-1$
+ }
+
+ public void testFailures() {
+ assertEquals(false, tsq.checkAndSet("", config));//$NON-NLS-1$
+ assertEquals(false, tsq.checkAndSet("STYLUS", config));//$NON-NLS-1$
+ assertEquals(false, tsq.checkAndSet("other", config));//$NON-NLS-1$
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/manager/ConfigMatchTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/manager/ConfigMatchTest.java
new file mode 100644
index 0000000..25a86c3
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/manager/ConfigMatchTest.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.manager;
+
+import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration;
+import com.android.ide.eclipse.editors.resources.configurations.ResourceQualifier;
+import com.android.ide.eclipse.editors.resources.configurations.KeyboardStateQualifier.KeyboardState;
+import com.android.ide.eclipse.editors.resources.configurations.NavigationMethodQualifier.NavigationMethod;
+import com.android.ide.eclipse.editors.resources.configurations.ScreenOrientationQualifier.ScreenOrientation;
+import com.android.ide.eclipse.editors.resources.configurations.TextInputMethodQualifier.TextInputMethod;
+import com.android.ide.eclipse.editors.resources.configurations.TouchScreenQualifier.TouchScreenType;
+import com.android.ide.eclipse.editors.resources.manager.files.IAbstractFolder;
+import com.android.ide.eclipse.editors.resources.manager.files.IFileWrapper;
+import com.android.ide.eclipse.editors.resources.manager.files.IFolderWrapper;
+import com.android.ide.eclipse.mock.FileMock;
+import com.android.ide.eclipse.mock.FolderMock;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+
+import junit.framework.TestCase;
+
+public class ConfigMatchTest extends TestCase {
+ private static final String SEARCHED_FILENAME = "main.xml"; //$NON-NLS-1$
+ private static final String MISC1_FILENAME = "foo.xml"; //$NON-NLS-1$
+ private static final String MISC2_FILENAME = "bar.xml"; //$NON-NLS-1$
+
+ private ProjectResources mResources;
+ private ResourceQualifier[] mQualifierList;
+ private FolderConfiguration config4;
+ private FolderConfiguration config3;
+ private FolderConfiguration config2;
+ private FolderConfiguration config1;
+
+ @SuppressWarnings("unchecked")
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ // create a Resource Manager to get a list of qualifier as instantiated by the real code.
+ // Thanks for QualifierListTest we know this contains all the qualifiers.
+ ResourceManager manager = ResourceManager.getInstance();
+ Field qualifierListField = ResourceManager.class.getDeclaredField("mQualifiers");
+ assertNotNull(qualifierListField);
+ qualifierListField.setAccessible(true);
+
+ // get the actual list.
+ mQualifierList = (ResourceQualifier[])qualifierListField.get(manager);
+
+ // create the project resources.
+ mResources = new ProjectResources(false /* isFrameworkRepository */);
+
+ // create 2 arrays of IResource. one with the filename being looked up, and one without.
+ // Since the required API uses IResource, we can use MockFolder for them.
+ FileMock[] validMemberList = new FileMock[] {
+ new FileMock(MISC1_FILENAME),
+ new FileMock(SEARCHED_FILENAME),
+ new FileMock(MISC2_FILENAME),
+ };
+ FileMock[] invalidMemberList = new FileMock[] {
+ new FileMock(MISC1_FILENAME),
+ new FileMock(MISC2_FILENAME),
+ };
+
+ // add multiple ResourceFolder to the project resource.
+ FolderConfiguration defaultConfig = getConfiguration(
+ null, // country code
+ null, // network code
+ null, // language
+ null, // region
+ null, // screen orientation
+ null, // dpi
+ null, // touch mode
+ null, // keyboard state
+ null, // text input
+ null, // navigation
+ null); // screen size
+
+ addFolder(mResources, defaultConfig, validMemberList);
+
+ config1 = getConfiguration(
+ null, // country code
+ null, // network code
+ "en", // language
+ null, // region
+ null, // screen orientation
+ null, // dpi
+ null, // touch mode
+ KeyboardState.EXPOSED.getValue(), // keyboard state
+ null, // text input
+ null, // navigation
+ null); // screen size
+
+ addFolder(mResources, config1, validMemberList);
+
+ config2 = getConfiguration(
+ null, // country code
+ null, // network code
+ "en", // language
+ null, // region
+ null, // screen orientation
+ null, // dpi
+ null, // touch mode
+ KeyboardState.HIDDEN.getValue(), // keyboard state
+ null, // text input
+ null, // navigation
+ null); // screen size
+
+ addFolder(mResources, config2, validMemberList);
+
+ config3 = getConfiguration(
+ null, // country code
+ null, // network code
+ "en", // language
+ null, // region
+ ScreenOrientation.LANDSCAPE.getValue(), // screen orientation
+ null, // dpi
+ null, // touch mode
+ null, // keyboard state
+ null, // text input
+ null, // navigation
+ null); // screen size
+
+ addFolder(mResources, config3, validMemberList);
+
+ config4 = getConfiguration(
+ "mcc310", // country code
+ "mnc435", // network code
+ "en", // language
+ "rUS", // region
+ ScreenOrientation.LANDSCAPE.getValue(), // screen orientation
+ "160dpi", // dpi
+ TouchScreenType.FINGER.getValue(), // touch mode
+ KeyboardState.EXPOSED.getValue(), // keyboard state
+ TextInputMethod.QWERTY.getValue(), // text input
+ NavigationMethod.DPAD.getValue(), // navigation
+ "480x320"); // screen size
+
+ addFolder(mResources, config4, invalidMemberList);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ mResources = null;
+ }
+
+ public void test1() {
+ FolderConfiguration testConfig = getConfiguration(
+ "mcc310", // country code
+ "mnc435", // network code
+ "en", // language
+ "rUS", // region
+ ScreenOrientation.LANDSCAPE.getValue(), // screen orientation
+ "160dpi", // dpi
+ TouchScreenType.FINGER.getValue(), // touch mode
+ KeyboardState.EXPOSED.getValue(), // keyboard state
+ TextInputMethod.QWERTY.getValue(), // text input
+ NavigationMethod.DPAD.getValue(), // navigation
+ "480x320"); // screen size
+
+ ResourceFile result = mResources.getMatchingFile(SEARCHED_FILENAME,
+ ResourceFolderType.LAYOUT, testConfig);
+
+ boolean bresult = result.getFolder().getConfiguration().equals(config3);
+ assertEquals(bresult, true);
+ }
+
+ /**
+ * Creates a {@link FolderConfiguration}.
+ * @param qualifierValues The list of qualifier values. The length must equals the total number
+ * of Qualifiers. <code>null</code> is permitted and will make the FolderConfiguration not use
+ * this particular qualifier.
+ */
+ private FolderConfiguration getConfiguration(String... qualifierValues) {
+ FolderConfiguration config = new FolderConfiguration();
+
+ // those must be of the same length
+ assertEquals(qualifierValues.length, mQualifierList.length);
+
+ int index = 0;
+
+ for (ResourceQualifier qualifier : mQualifierList) {
+ String value = qualifierValues[index++];
+ if (value != null) {
+ assertTrue(qualifier.checkAndSet(value, config));
+ }
+ }
+
+ return config;
+ }
+
+ /**
+ * Adds a folder to the given {@link ProjectResources} with the given
+ * {@link FolderConfiguration}. The folder is filled with files from the provided list.
+ * @param resources the {@link ProjectResources} in which to add the folder.
+ * @param config the {@link FolderConfiguration} for the created folder.
+ * @param memberList the list of files for the folder.
+ */
+ private void addFolder(ProjectResources resources, FolderConfiguration config,
+ FileMock[] memberList) throws Exception {
+
+ // figure out the folder name based on the configuration
+ String folderName = "layout";
+ if (config.isDefault() == false) {
+ folderName += "-" + config.toString();
+ }
+
+ // create the folder mock
+ FolderMock folder = new FolderMock(folderName, memberList);
+
+ // add it to the resource, and get back a ResourceFolder object.
+ ResourceFolder resFolder = _addProjectResourceFolder(resources, config, folder);
+
+ // and fill it with files from the list.
+ for (FileMock file : memberList) {
+ resFolder.addFile(new SingleResourceFile(new IFileWrapper(file), resFolder));
+ }
+ }
+
+ /** Calls ProjectResource.add method via reflection to circumvent access
+ * restrictions that are enforced when running in the plug-in environment
+ * ie cannot access package or protected members in a different plug-in, even
+ * if they are in the same declared package as the accessor
+ */
+ private ResourceFolder _addProjectResourceFolder(ProjectResources resources,
+ FolderConfiguration config, FolderMock folder) throws Exception {
+
+ Method addMethod = ProjectResources.class.getDeclaredMethod("add",
+ ResourceFolderType.class, FolderConfiguration.class,
+ IAbstractFolder.class);
+ addMethod.setAccessible(true);
+ ResourceFolder resFolder = (ResourceFolder)addMethod.invoke(resources,
+ ResourceFolderType.LAYOUT, config, new IFolderWrapper(folder));
+ return resFolder;
+ }
+
+
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/manager/QualifierListTest.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/manager/QualifierListTest.java
new file mode 100644
index 0000000..6a555a4
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/editors/resources/manager/QualifierListTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.editors.resources.manager;
+
+import com.android.ide.eclipse.editors.resources.configurations.FolderConfiguration;
+import com.android.ide.eclipse.editors.resources.configurations.ResourceQualifier;
+
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+
+import junit.framework.TestCase;
+
+public class QualifierListTest extends TestCase {
+
+ private ResourceManager mManager;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+
+ mManager = ResourceManager.getInstance();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ mManager = null;
+ }
+
+ @SuppressWarnings("unchecked")
+ public void testQualifierList() {
+ try {
+ // get the list of qualifier in the resource manager
+ Field qualifierListField = ResourceManager.class.getDeclaredField("mQualifiers");
+ assertNotNull(qualifierListField);
+ qualifierListField.setAccessible(true);
+
+ // get the actual list.
+ ResourceQualifier[] qualifierList =
+ (ResourceQualifier[])qualifierListField.get(mManager);
+
+ // now get the number of qualifier in the FolderConfiguration
+ Field qualCountField = FolderConfiguration.class.getDeclaredField("INDEX_COUNT");
+ assertNotNull(qualCountField);
+ qualCountField.setAccessible(true);
+
+ // get the constant value
+ Integer count = (Integer)qualCountField.get(null);
+
+ // now compare
+ assertEquals(count.intValue(), qualifierList.length);
+ } catch (SecurityException e) {
+ assertTrue(false);
+ } catch (NoSuchFieldException e) {
+ assertTrue(false);
+ } catch (IllegalArgumentException e) {
+ assertTrue(false);
+ } catch (IllegalAccessException e) {
+ assertTrue(false);
+ }
+ }
+}
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/ClasspathEntryMock.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/ClasspathEntryMock.java
new file mode 100644
index 0000000..010aadf
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/ClasspathEntryMock.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.mock;
+
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.jdt.core.IAccessRule;
+import org.eclipse.jdt.core.IClasspathAttribute;
+import org.eclipse.jdt.core.IClasspathEntry;
+
+import sun.reflect.generics.reflectiveObjects.NotImplementedException;
+
+public class ClasspathEntryMock implements IClasspathEntry {
+
+ private int mKind;
+ private IPath mPath;
+
+ public ClasspathEntryMock(IPath path, int kind) {
+ mPath = path;
+ mKind = kind;
+ }
+
+ public int getEntryKind() {
+ return mKind;
+ }
+
+ public IPath getPath() {
+ return mPath;
+ }
+
+ // -------- UNIMPLEMENTED METHODS ----------------
+
+ public boolean combineAccessRules() {
+ throw new NotImplementedException();
+ }
+
+ public IAccessRule[] getAccessRules() {
+ throw new NotImplementedException();
+ }
+
+ public int getContentKind() {
+ throw new NotImplementedException();
+ }
+
+ public IPath[] getExclusionPatterns() {
+ throw new NotImplementedException();
+ }
+
+ public IClasspathAttribute[] getExtraAttributes() {
+ throw new NotImplementedException();
+ }
+
+ public IPath[] getInclusionPatterns() {
+ throw new NotImplementedException();
+ }
+
+ public IPath getOutputLocation() {
+ throw new NotImplementedException();
+ }
+
+ public IClasspathEntry getResolvedEntry() {
+ throw new NotImplementedException();
+ }
+
+ public IPath getSourceAttachmentPath() {
+ throw new NotImplementedException();
+ }
+
+ public IPath getSourceAttachmentRootPath() {
+ throw new NotImplementedException();
+ }
+
+ public boolean isExported() {
+ throw new NotImplementedException();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FileMock.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FileMock.java
new file mode 100644
index 0000000..a95286c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FileMock.java
@@ -0,0 +1,447 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.mock;
+
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFileState;
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IProjectDescription;
+import org.eclipse.core.resources.IResourceProxy;
+import org.eclipse.core.resources.IResourceProxyVisitor;
+import org.eclipse.core.resources.IResourceVisitor;
+import org.eclipse.core.resources.IWorkspace;
+import org.eclipse.core.resources.ResourceAttributes;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.core.runtime.QualifiedName;
+import org.eclipse.core.runtime.content.IContentDescription;
+import org.eclipse.core.runtime.jobs.ISchedulingRule;
+
+import sun.reflect.generics.reflectiveObjects.NotImplementedException;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.net.URI;
+import java.util.Map;
+
+/**
+ * Mock implementation of {@link IFile}.
+ * <p/>Supported methods:
+ * <ul>
+ * </ul>
+ */
+public class FileMock implements IFile {
+
+ private String mName;
+
+ public FileMock(String name) {
+ mName = name;
+ }
+
+ // -------- MOCKED METHODS ----------------
+
+ public String getName() {
+ return mName;
+ }
+
+ // -------- UNIMPLEMENTED METHODS ----------------
+
+ public void appendContents(InputStream source, int updateFlags, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void appendContents(InputStream source, boolean force, boolean keepHistory,
+ IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void create(InputStream source, boolean force, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void create(InputStream source, int updateFlags, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void createLink(IPath localLocation, int updateFlags, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void createLink(URI location, int updateFlags, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void delete(boolean force, boolean keepHistory, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public String getCharset() throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public String getCharset(boolean checkImplicit) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public String getCharsetFor(Reader reader) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public IContentDescription getContentDescription() throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public InputStream getContents() throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public InputStream getContents(boolean force) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public int getEncoding() throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public IPath getFullPath() {
+ throw new NotImplementedException();
+ }
+
+ public IFileState[] getHistory(IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public boolean isReadOnly() {
+ throw new NotImplementedException();
+ }
+
+ public void move(IPath destination, boolean force, boolean keepHistory, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void setCharset(String newCharset) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void setCharset(String newCharset, IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void setContents(InputStream source, int updateFlags, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void setContents(IFileState source, int updateFlags, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void setContents(InputStream source, boolean force, boolean keepHistory,
+ IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void setContents(IFileState source, boolean force, boolean keepHistory,
+ IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void accept(IResourceVisitor visitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void accept(IResourceProxyVisitor visitor, int memberFlags) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void accept(IResourceVisitor visitor, int depth, boolean includePhantoms)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void accept(IResourceVisitor visitor, int depth, int memberFlags) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void clearHistory(IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void copy(IPath destination, boolean force, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void copy(IPath destination, int updateFlags, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void copy(IProjectDescription description, boolean force, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void copy(IProjectDescription description, int updateFlags, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public IMarker createMarker(String type) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public IResourceProxy createProxy() {
+ throw new NotImplementedException();
+ }
+
+ public void delete(boolean force, IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void delete(int updateFlags, IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void deleteMarkers(String type, boolean includeSubtypes, int depth) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public boolean exists() {
+ throw new NotImplementedException();
+ }
+
+ public IMarker findMarker(long id) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public IMarker[] findMarkers(String type, boolean includeSubtypes, int depth)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public int findMaxProblemSeverity(String type, boolean includeSubtypes, int depth)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public String getFileExtension() {
+ throw new NotImplementedException();
+ }
+
+ public long getLocalTimeStamp() {
+ throw new NotImplementedException();
+ }
+
+ public IPath getLocation() {
+ return new Path(mName);
+ }
+
+ public URI getLocationURI() {
+ throw new NotImplementedException();
+ }
+
+ public IMarker getMarker(long id) {
+ throw new NotImplementedException();
+ }
+
+ public long getModificationStamp() {
+ throw new NotImplementedException();
+ }
+
+ public IContainer getParent() {
+ throw new NotImplementedException();
+ }
+
+ public String getPersistentProperty(QualifiedName key) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public IProject getProject() {
+ throw new NotImplementedException();
+ }
+
+ public IPath getProjectRelativePath() {
+ throw new NotImplementedException();
+ }
+
+ public IPath getRawLocation() {
+ throw new NotImplementedException();
+ }
+
+ public URI getRawLocationURI() {
+ throw new NotImplementedException();
+ }
+
+ public ResourceAttributes getResourceAttributes() {
+ throw new NotImplementedException();
+ }
+
+ public Object getSessionProperty(QualifiedName key) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public int getType() {
+ throw new NotImplementedException();
+ }
+
+ public IWorkspace getWorkspace() {
+ throw new NotImplementedException();
+ }
+
+ public boolean isAccessible() {
+ throw new NotImplementedException();
+ }
+
+ public boolean isDerived() {
+ throw new NotImplementedException();
+ }
+
+ public boolean isLinked() {
+ throw new NotImplementedException();
+ }
+
+ public boolean isLinked(int options) {
+ throw new NotImplementedException();
+ }
+
+ public boolean isLocal(int depth) {
+ throw new NotImplementedException();
+ }
+
+ public boolean isPhantom() {
+ throw new NotImplementedException();
+ }
+
+ public boolean isSynchronized(int depth) {
+ throw new NotImplementedException();
+ }
+
+ public boolean isTeamPrivateMember() {
+ throw new NotImplementedException();
+ }
+
+ public void move(IPath destination, boolean force, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void move(IPath destination, int updateFlags, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void move(IProjectDescription description, int updateFlags, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void move(IProjectDescription description, boolean force, boolean keepHistory,
+ IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void refreshLocal(int depth, IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void revertModificationStamp(long value) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void setDerived(boolean isDerived) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void setLocal(boolean flag, int depth, IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public long setLocalTimeStamp(long value) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void setPersistentProperty(QualifiedName key, String value) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void setReadOnly(boolean readOnly) {
+ throw new NotImplementedException();
+ }
+
+ public void setResourceAttributes(ResourceAttributes attributes) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void setSessionProperty(QualifiedName key, Object value) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void setTeamPrivateMember(boolean isTeamPrivate) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void touch(IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ @SuppressWarnings("unchecked")
+ public Object getAdapter(Class adapter) {
+ throw new NotImplementedException();
+ }
+
+ public boolean contains(ISchedulingRule rule) {
+ throw new NotImplementedException();
+ }
+
+ public boolean isConflicting(ISchedulingRule rule) {
+ throw new NotImplementedException();
+ }
+
+ public Map getPersistentProperties() throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public Map getSessionProperties() throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public boolean isDerived(int options) {
+ throw new NotImplementedException();
+ }
+
+ public boolean isHidden() {
+ throw new NotImplementedException();
+ }
+
+ public void setHidden(boolean isHidden) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FolderMock.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FolderMock.java
new file mode 100644
index 0000000..223deb0
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/FolderMock.java
@@ -0,0 +1,451 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.mock;
+
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IProjectDescription;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IResourceProxy;
+import org.eclipse.core.resources.IResourceProxyVisitor;
+import org.eclipse.core.resources.IResourceVisitor;
+import org.eclipse.core.resources.IWorkspace;
+import org.eclipse.core.resources.ResourceAttributes;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.QualifiedName;
+import org.eclipse.core.runtime.jobs.ISchedulingRule;
+
+import sun.reflect.generics.reflectiveObjects.NotImplementedException;
+
+import java.net.URI;
+import java.util.Map;
+
+/**
+ * Mock implementation of {@link IFolder}.
+ * <p/>Supported methods:
+ * <ul>
+ * <li>{@link #getName()}</li>
+ * <li>{@link #members()}</li>
+ * </ul>
+ */
+public final class FolderMock implements IFolder {
+
+ private String mName;
+ private IResource[] mMembers;
+
+ public FolderMock(String name) {
+ mName = name;
+ mMembers = new IResource[0];
+ }
+
+ public FolderMock(String name, IResource[] members) {
+ mName = name;
+ mMembers = members;
+ }
+
+ // -------- MOCKED METHODS ----------------
+
+ public String getName() {
+ return mName;
+ }
+
+ public IResource[] members() throws CoreException {
+ return mMembers;
+ }
+
+ // -------- UNIMPLEMENTED METHODS ----------------
+
+ public void create(boolean force, boolean local, IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void create(int updateFlags, boolean local, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void createLink(IPath localLocation, int updateFlags, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void createLink(URI location, int updateFlags, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void delete(boolean force, boolean keepHistory, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public IFile getFile(String name) {
+ throw new NotImplementedException();
+ }
+
+ public IFolder getFolder(String name) {
+ throw new NotImplementedException();
+ }
+
+ public void move(IPath destination, boolean force, boolean keepHistory, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public boolean exists(IPath path) {
+ throw new NotImplementedException();
+ }
+
+ public IFile[] findDeletedMembersWithHistory(int depth, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public IResource findMember(String name) {
+ throw new NotImplementedException();
+ }
+
+ public IResource findMember(IPath path) {
+ throw new NotImplementedException();
+ }
+
+ public IResource findMember(String name, boolean includePhantoms) {
+ throw new NotImplementedException();
+ }
+
+ public IResource findMember(IPath path, boolean includePhantoms) {
+ throw new NotImplementedException();
+ }
+
+ public String getDefaultCharset() throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public String getDefaultCharset(boolean checkImplicit) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public IFile getFile(IPath path) {
+ throw new NotImplementedException();
+ }
+
+ public IFolder getFolder(IPath path) {
+ throw new NotImplementedException();
+ }
+
+ public IResource[] members(boolean includePhantoms) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public IResource[] members(int memberFlags) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void setDefaultCharset(String charset) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void setDefaultCharset(String charset, IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void accept(IResourceVisitor visitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void accept(IResourceProxyVisitor visitor, int memberFlags) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void accept(IResourceVisitor visitor, int depth, boolean includePhantoms)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void accept(IResourceVisitor visitor, int depth, int memberFlags) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void clearHistory(IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void copy(IPath destination, boolean force, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void copy(IPath destination, int updateFlags, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void copy(IProjectDescription description, boolean force, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void copy(IProjectDescription description, int updateFlags, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public IMarker createMarker(String type) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public IResourceProxy createProxy() {
+ throw new NotImplementedException();
+ }
+
+ public void delete(boolean force, IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void delete(int updateFlags, IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void deleteMarkers(String type, boolean includeSubtypes, int depth) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public boolean exists() {
+ throw new NotImplementedException();
+ }
+
+ public IMarker findMarker(long id) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public IMarker[] findMarkers(String type, boolean includeSubtypes, int depth)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public int findMaxProblemSeverity(String type, boolean includeSubtypes, int depth)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public String getFileExtension() {
+ throw new NotImplementedException();
+ }
+
+ public IPath getFullPath() {
+ throw new NotImplementedException();
+ }
+
+ public long getLocalTimeStamp() {
+ throw new NotImplementedException();
+ }
+
+ public IPath getLocation() {
+ throw new NotImplementedException();
+ }
+
+ public URI getLocationURI() {
+ throw new NotImplementedException();
+ }
+
+ public IMarker getMarker(long id) {
+ throw new NotImplementedException();
+ }
+
+ public long getModificationStamp() {
+ throw new NotImplementedException();
+ }
+
+ public IContainer getParent() {
+ throw new NotImplementedException();
+ }
+
+ public String getPersistentProperty(QualifiedName key) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public IProject getProject() {
+ throw new NotImplementedException();
+ }
+
+ public IPath getProjectRelativePath() {
+ throw new NotImplementedException();
+ }
+
+ public IPath getRawLocation() {
+ throw new NotImplementedException();
+ }
+
+ public URI getRawLocationURI() {
+ throw new NotImplementedException();
+ }
+
+ public ResourceAttributes getResourceAttributes() {
+ throw new NotImplementedException();
+ }
+
+ public Object getSessionProperty(QualifiedName key) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public int getType() {
+ throw new NotImplementedException();
+ }
+
+ public IWorkspace getWorkspace() {
+ throw new NotImplementedException();
+ }
+
+ public boolean isAccessible() {
+ throw new NotImplementedException();
+ }
+
+ public boolean isDerived() {
+ throw new NotImplementedException();
+ }
+
+ public boolean isLinked() {
+ throw new NotImplementedException();
+ }
+
+ public boolean isLinked(int options) {
+ throw new NotImplementedException();
+ }
+
+ public boolean isLocal(int depth) {
+ throw new NotImplementedException();
+ }
+
+ public boolean isPhantom() {
+ throw new NotImplementedException();
+ }
+
+ public boolean isReadOnly() {
+ throw new NotImplementedException();
+ }
+
+ public boolean isSynchronized(int depth) {
+ throw new NotImplementedException();
+ }
+
+ public boolean isTeamPrivateMember() {
+ throw new NotImplementedException();
+ }
+
+ public void move(IPath destination, boolean force, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void move(IPath destination, int updateFlags, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void move(IProjectDescription description, int updateFlags, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void move(IProjectDescription description, boolean force, boolean keepHistory,
+ IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void refreshLocal(int depth, IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void revertModificationStamp(long value) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void setDerived(boolean isDerived) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void setLocal(boolean flag, int depth, IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public long setLocalTimeStamp(long value) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void setPersistentProperty(QualifiedName key, String value) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void setReadOnly(boolean readOnly) {
+ throw new NotImplementedException();
+ }
+
+ public void setResourceAttributes(ResourceAttributes attributes) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void setSessionProperty(QualifiedName key, Object value) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void setTeamPrivateMember(boolean isTeamPrivate) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void touch(IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ @SuppressWarnings("unchecked")
+ public Object getAdapter(Class adapter) {
+ throw new NotImplementedException();
+ }
+
+ public boolean contains(ISchedulingRule rule) {
+ throw new NotImplementedException();
+ }
+
+ public boolean isConflicting(ISchedulingRule rule) {
+ throw new NotImplementedException();
+ }
+
+ public Map getPersistentProperties() throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public Map getSessionProperties() throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public boolean isDerived(int options) {
+ throw new NotImplementedException();
+ }
+
+ public boolean isHidden() {
+ throw new NotImplementedException();
+ }
+
+ public void setHidden(boolean isHidden) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/JavaProjectMock.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/JavaProjectMock.java
new file mode 100644
index 0000000..f23d2c1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/JavaProjectMock.java
@@ -0,0 +1,414 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.mock;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.jobs.ISchedulingRule;
+import org.eclipse.jdt.core.IBuffer;
+import org.eclipse.jdt.core.IClasspathEntry;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IJavaModel;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IOpenable;
+import org.eclipse.jdt.core.IPackageFragment;
+import org.eclipse.jdt.core.IPackageFragmentRoot;
+import org.eclipse.jdt.core.IRegion;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.ITypeHierarchy;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.core.WorkingCopyOwner;
+import org.eclipse.jdt.core.eval.IEvaluationContext;
+
+import sun.reflect.generics.reflectiveObjects.NotImplementedException;
+
+import java.util.Map;
+
+public class JavaProjectMock implements IJavaProject {
+
+ private IProject mProject;
+
+ private IClasspathEntry[] mEntries;
+ private IPath mOutputLocation;
+ private String mCompilerCompliance = "1.4"; //$NON-NLS-1
+ private String mCompilerSource = "1.4"; //$NON-NLS-1
+ private String mCompilerTarget = "1.4"; //$NON-NLS-1
+
+ public JavaProjectMock(IClasspathEntry[] entries, IPath outputLocation) {
+ mEntries = entries;
+ mOutputLocation = outputLocation;
+ }
+
+ public IProject getProject() {
+ if (mProject == null) {
+ mProject = new ProjectMock();
+ }
+
+ return mProject;
+ }
+
+
+ public void setRawClasspath(IClasspathEntry[] entries, IProgressMonitor monitor)
+ throws JavaModelException {
+ mEntries = entries;
+ }
+
+ public void setRawClasspath(IClasspathEntry[] entries, IPath outputLocation,
+ IProgressMonitor monitor) throws JavaModelException {
+ mEntries = entries;
+ mOutputLocation = outputLocation;
+ }
+
+ public IClasspathEntry[] getRawClasspath() throws JavaModelException {
+ return mEntries;
+ }
+
+ public IPath getOutputLocation() throws JavaModelException {
+ return mOutputLocation;
+ }
+
+ public String getOption(String optionName, boolean inheritJavaCoreOptions) {
+ if (optionName.equals(JavaCore.COMPILER_COMPLIANCE)) {
+ return mCompilerCompliance;
+ } else if (optionName.equals(JavaCore.COMPILER_SOURCE)) {
+ return mCompilerSource;
+ } else if (optionName.equals(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM)) {
+ return mCompilerTarget;
+ }
+
+ return null;
+ }
+
+ public void setOption(String optionName, String optionValue) {
+ if (optionName.equals(JavaCore.COMPILER_COMPLIANCE)) {
+ mCompilerCompliance = optionValue;
+ } else if (optionName.equals(JavaCore.COMPILER_SOURCE)) {
+ mCompilerSource = optionValue;
+ } else if (optionName.equals(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM)) {
+ mCompilerTarget = optionValue;
+ } else {
+ throw new NotImplementedException();
+ }
+ }
+
+
+ // -------- UNIMPLEMENTED METHODS ----------------
+
+ public IClasspathEntry decodeClasspathEntry(String encodedEntry) {
+ throw new NotImplementedException();
+ }
+
+ public String encodeClasspathEntry(IClasspathEntry classpathEntry) {
+ throw new NotImplementedException();
+ }
+
+ public IJavaElement findElement(IPath path) throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public IJavaElement findElement(IPath path, WorkingCopyOwner owner) throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public IPackageFragment findPackageFragment(IPath path) throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public IPackageFragmentRoot findPackageFragmentRoot(IPath path) throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public IPackageFragmentRoot[] findPackageFragmentRoots(IClasspathEntry entry) {
+ throw new NotImplementedException();
+ }
+
+ public IType findType(String fullyQualifiedName) throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public IType findType(String fullyQualifiedName, IProgressMonitor progressMonitor)
+ throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public IType findType(String fullyQualifiedName, WorkingCopyOwner owner)
+ throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public IType findType(String packageName, String typeQualifiedName) throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public IType findType(String fullyQualifiedName, WorkingCopyOwner owner,
+ IProgressMonitor progressMonitor) throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public IType findType(String packageName, String typeQualifiedName,
+ IProgressMonitor progressMonitor) throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public IType findType(String packageName, String typeQualifiedName, WorkingCopyOwner owner)
+ throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public IType findType(String packageName, String typeQualifiedName, WorkingCopyOwner owner,
+ IProgressMonitor progressMonitor) throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public IPackageFragmentRoot[] getAllPackageFragmentRoots() throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public Object[] getNonJavaResources() throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ @SuppressWarnings("unchecked")
+ public Map getOptions(boolean inheritJavaCoreOptions) {
+ throw new NotImplementedException();
+ }
+
+ public IPackageFragmentRoot getPackageFragmentRoot(String jarPath) {
+ throw new NotImplementedException();
+ }
+
+ public IPackageFragmentRoot getPackageFragmentRoot(IResource resource) {
+ throw new NotImplementedException();
+ }
+
+ public IPackageFragmentRoot[] getPackageFragmentRoots() throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public IPackageFragmentRoot[] getPackageFragmentRoots(IClasspathEntry entry) {
+ throw new NotImplementedException();
+ }
+
+ public IPackageFragment[] getPackageFragments() throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public String[] getRequiredProjectNames() throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public IClasspathEntry[] getResolvedClasspath(boolean ignoreUnresolvedEntry)
+ throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public boolean hasBuildState() {
+ throw new NotImplementedException();
+ }
+
+ public boolean hasClasspathCycle(IClasspathEntry[] entries) {
+ throw new NotImplementedException();
+ }
+
+ public boolean isOnClasspath(IJavaElement element) {
+ throw new NotImplementedException();
+ }
+
+ public boolean isOnClasspath(IResource resource) {
+ throw new NotImplementedException();
+ }
+
+ public IEvaluationContext newEvaluationContext() {
+ throw new NotImplementedException();
+ }
+
+ public ITypeHierarchy newTypeHierarchy(IRegion region, IProgressMonitor monitor)
+ throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public ITypeHierarchy newTypeHierarchy(IRegion region, WorkingCopyOwner owner,
+ IProgressMonitor monitor) throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public ITypeHierarchy newTypeHierarchy(IType type, IRegion region, IProgressMonitor monitor)
+ throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public ITypeHierarchy newTypeHierarchy(IType type, IRegion region, WorkingCopyOwner owner,
+ IProgressMonitor monitor) throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public IPath readOutputLocation() {
+ throw new NotImplementedException();
+ }
+
+ public IClasspathEntry[] readRawClasspath() {
+ throw new NotImplementedException();
+ }
+
+ @SuppressWarnings("unchecked")
+ public void setOptions(Map newOptions) {
+ throw new NotImplementedException();
+ }
+
+ public void setOutputLocation(IPath path, IProgressMonitor monitor) throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public void setRawClasspath(IClasspathEntry[] entries, boolean canModifyResources,
+ IProgressMonitor monitor) throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public void setRawClasspath(IClasspathEntry[] entries, IPath outputLocation,
+ boolean canModifyResources, IProgressMonitor monitor) throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public IJavaElement[] getChildren() throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public boolean hasChildren() throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public boolean exists() {
+ throw new NotImplementedException();
+ }
+
+ public IJavaElement getAncestor(int ancestorType) {
+ throw new NotImplementedException();
+ }
+
+ public String getAttachedJavadoc(IProgressMonitor monitor) throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public IResource getCorrespondingResource() throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public String getElementName() {
+ throw new NotImplementedException();
+ }
+
+ public int getElementType() {
+ throw new NotImplementedException();
+ }
+
+ public String getHandleIdentifier() {
+ throw new NotImplementedException();
+ }
+
+ public IJavaModel getJavaModel() {
+ throw new NotImplementedException();
+ }
+
+ public IJavaProject getJavaProject() {
+ throw new NotImplementedException();
+ }
+
+ public IOpenable getOpenable() {
+ throw new NotImplementedException();
+ }
+
+ public IJavaElement getParent() {
+ throw new NotImplementedException();
+ }
+
+ public IPath getPath() {
+ throw new NotImplementedException();
+ }
+
+ public IJavaElement getPrimaryElement() {
+ throw new NotImplementedException();
+ }
+
+ public IResource getResource() {
+ throw new NotImplementedException();
+ }
+
+ public ISchedulingRule getSchedulingRule() {
+ throw new NotImplementedException();
+ }
+
+ public IResource getUnderlyingResource() throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public boolean isReadOnly() {
+ throw new NotImplementedException();
+ }
+
+ public boolean isStructureKnown() throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ @SuppressWarnings("unchecked")
+ public Object getAdapter(Class adapter) {
+ throw new NotImplementedException();
+ }
+
+ public void close() throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public String findRecommendedLineSeparator() throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public IBuffer getBuffer() throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public boolean hasUnsavedChanges() throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public boolean isConsistent() throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public boolean isOpen() {
+ throw new NotImplementedException();
+ }
+
+ public void makeConsistent(IProgressMonitor progress) throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public void open(IProgressMonitor progress) throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public void save(IProgressMonitor progress, boolean force) throws JavaModelException {
+ throw new NotImplementedException();
+ }
+
+ public IJavaElement findElement(String bindingKey, WorkingCopyOwner owner)
+ throws JavaModelException {
+ throw new NotImplementedException();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/ProjectMock.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/ProjectMock.java
new file mode 100644
index 0000000..4c409dc
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/com/android/ide/eclipse/mock/ProjectMock.java
@@ -0,0 +1,499 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.mock;
+
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IProjectDescription;
+import org.eclipse.core.resources.IProjectNature;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IResourceProxy;
+import org.eclipse.core.resources.IResourceProxyVisitor;
+import org.eclipse.core.resources.IResourceVisitor;
+import org.eclipse.core.resources.IWorkspace;
+import org.eclipse.core.resources.ResourceAttributes;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IPluginDescriptor;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.QualifiedName;
+import org.eclipse.core.runtime.content.IContentTypeMatcher;
+import org.eclipse.core.runtime.jobs.ISchedulingRule;
+
+import sun.reflect.generics.reflectiveObjects.NotImplementedException;
+
+import java.net.URI;
+import java.util.Map;
+
+public class ProjectMock implements IProject {
+
+ public void build(int kind, IProgressMonitor monitor) throws CoreException {
+ // pass
+ }
+
+ @SuppressWarnings("unchecked")
+ public void build(int kind, String builderName, Map args, IProgressMonitor monitor)
+ throws CoreException {
+ // pass
+ }
+
+ // -------- UNIMPLEMENTED METHODS ----------------
+
+
+ public void close(IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void create(IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void create(IProjectDescription description, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void delete(boolean deleteContent, boolean force, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public IContentTypeMatcher getContentTypeMatcher() throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public IProjectDescription getDescription() throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public IFile getFile(String name) {
+ throw new NotImplementedException();
+ }
+
+ public IFolder getFolder(String name) {
+ throw new NotImplementedException();
+ }
+
+ public IProjectNature getNature(String natureId) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ @SuppressWarnings("deprecation")
+ public IPath getPluginWorkingLocation(IPluginDescriptor plugin) {
+ throw new NotImplementedException();
+ }
+
+ public IProject[] getReferencedProjects() throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public IProject[] getReferencingProjects() {
+ throw new NotImplementedException();
+ }
+
+ public IPath getWorkingLocation(String id) {
+ throw new NotImplementedException();
+ }
+
+ public boolean hasNature(String natureId) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public boolean isNatureEnabled(String natureId) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public boolean isOpen() {
+ throw new NotImplementedException();
+ }
+
+ public void move(IProjectDescription description, boolean force, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void open(IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void open(int updateFlags, IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void setDescription(IProjectDescription description, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void setDescription(IProjectDescription description, int updateFlags,
+ IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public boolean exists(IPath path) {
+ throw new NotImplementedException();
+ }
+
+ public IFile[] findDeletedMembersWithHistory(int depth, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public IResource findMember(String name) {
+ throw new NotImplementedException();
+ }
+
+ public IResource findMember(IPath path) {
+ throw new NotImplementedException();
+ }
+
+ public IResource findMember(String name, boolean includePhantoms) {
+ throw new NotImplementedException();
+ }
+
+ public IResource findMember(IPath path, boolean includePhantoms) {
+ throw new NotImplementedException();
+ }
+
+ public String getDefaultCharset() throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public String getDefaultCharset(boolean checkImplicit) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public IFile getFile(IPath path) {
+ throw new NotImplementedException();
+ }
+
+ public IFolder getFolder(IPath path) {
+ throw new NotImplementedException();
+ }
+
+ public IResource[] members() throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public IResource[] members(boolean includePhantoms) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public IResource[] members(int memberFlags) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void setDefaultCharset(String charset) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void setDefaultCharset(String charset, IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void accept(IResourceVisitor visitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void accept(IResourceProxyVisitor visitor, int memberFlags) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void accept(IResourceVisitor visitor, int depth, boolean includePhantoms)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void accept(IResourceVisitor visitor, int depth, int memberFlags) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void clearHistory(IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void copy(IPath destination, boolean force, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void copy(IPath destination, int updateFlags, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void copy(IProjectDescription description, boolean force, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void copy(IProjectDescription description, int updateFlags, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public IMarker createMarker(String type) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public IResourceProxy createProxy() {
+ throw new NotImplementedException();
+ }
+
+ public void delete(boolean force, IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void delete(int updateFlags, IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void deleteMarkers(String type, boolean includeSubtypes, int depth) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public boolean exists() {
+ throw new NotImplementedException();
+ }
+
+ public IMarker findMarker(long id) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public IMarker[] findMarkers(String type, boolean includeSubtypes, int depth)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public int findMaxProblemSeverity(String type, boolean includeSubtypes, int depth)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public String getFileExtension() {
+ throw new NotImplementedException();
+ }
+
+ public IPath getFullPath() {
+ throw new NotImplementedException();
+ }
+
+ public long getLocalTimeStamp() {
+ throw new NotImplementedException();
+ }
+
+ public IPath getLocation() {
+ throw new NotImplementedException();
+ }
+
+ public URI getLocationURI() {
+ throw new NotImplementedException();
+ }
+
+ public IMarker getMarker(long id) {
+ throw new NotImplementedException();
+ }
+
+ public long getModificationStamp() {
+ throw new NotImplementedException();
+ }
+
+ public String getName() {
+ throw new NotImplementedException();
+ }
+
+ public IContainer getParent() {
+ throw new NotImplementedException();
+ }
+
+ public String getPersistentProperty(QualifiedName key) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public IProject getProject() {
+ throw new NotImplementedException();
+ }
+
+ public IPath getProjectRelativePath() {
+ throw new NotImplementedException();
+ }
+
+ public IPath getRawLocation() {
+ throw new NotImplementedException();
+ }
+
+ public URI getRawLocationURI() {
+ throw new NotImplementedException();
+ }
+
+ public ResourceAttributes getResourceAttributes() {
+ throw new NotImplementedException();
+ }
+
+ public Object getSessionProperty(QualifiedName key) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public int getType() {
+ throw new NotImplementedException();
+ }
+
+ public IWorkspace getWorkspace() {
+ throw new NotImplementedException();
+ }
+
+ public boolean isAccessible() {
+ throw new NotImplementedException();
+ }
+
+ public boolean isDerived() {
+ throw new NotImplementedException();
+ }
+
+ public boolean isLinked() {
+ throw new NotImplementedException();
+ }
+
+ public boolean isLinked(int options) {
+ throw new NotImplementedException();
+ }
+
+ public boolean isLocal(int depth) {
+ throw new NotImplementedException();
+ }
+
+ public boolean isPhantom() {
+ throw new NotImplementedException();
+ }
+
+ public boolean isReadOnly() {
+ throw new NotImplementedException();
+ }
+
+ public boolean isSynchronized(int depth) {
+ throw new NotImplementedException();
+ }
+
+ public boolean isTeamPrivateMember() {
+ throw new NotImplementedException();
+ }
+
+ public void move(IPath destination, boolean force, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void move(IPath destination, int updateFlags, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void move(IProjectDescription description, int updateFlags, IProgressMonitor monitor)
+ throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void move(IProjectDescription description, boolean force, boolean keepHistory,
+ IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void refreshLocal(int depth, IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void revertModificationStamp(long value) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void setDerived(boolean isDerived) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void setLocal(boolean flag, int depth, IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public long setLocalTimeStamp(long value) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void setPersistentProperty(QualifiedName key, String value) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void setReadOnly(boolean readOnly) {
+ throw new NotImplementedException();
+ }
+
+ public void setResourceAttributes(ResourceAttributes attributes) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void setSessionProperty(QualifiedName key, Object value) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void setTeamPrivateMember(boolean isTeamPrivate) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public void touch(IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public Object getAdapter(Class adapter) {
+ throw new NotImplementedException();
+ }
+
+ public boolean contains(ISchedulingRule rule) {
+ throw new NotImplementedException();
+ }
+
+ public boolean isConflicting(ISchedulingRule rule) {
+ throw new NotImplementedException();
+ }
+
+ public void create(IProjectDescription description, int updateFlags,
+ IProgressMonitor monitor) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public Map getPersistentProperties() throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public Map getSessionProperties() throws CoreException {
+ throw new NotImplementedException();
+ }
+
+ public boolean isDerived(int options) {
+ throw new NotImplementedException();
+ }
+
+ public boolean isHidden() {
+ throw new NotImplementedException();
+ }
+
+ public void setHidden(boolean isHidden) throws CoreException {
+ throw new NotImplementedException();
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/jar_example.jar b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/jar_example.jar
new file mode 100644
index 0000000..f95b595
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/jar_example.jar
Binary files differ
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/jar_example.jardesc b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/jar_example.jardesc
new file mode 100644
index 0000000..14cd44f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/jar_example.jardesc
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="ISO-8859-1" standalone="no"?>
+<jardesc>
+ <jar path="jar_example.jar"/>
+ <options buildIfNeeded="true" compress="true" descriptionLocation="/common/tests/data/jar_example.jardesc" exportErrors="false" exportWarnings="true" includeDirectoryEntries="false" overwrite="false" saveDescription="true" storeRefactorings="false" useSourceFolders="false"/>
+ <storedRefactorings deprecationInfo="true" structuralOnly="false"/>
+ <selectedProjects/>
+ <manifest generateManifest="true" manifestLocation="" manifestVersion="1.0" reuseManifest="false" saveManifest="false" usesManifest="true">
+ <sealing sealJar="false">
+ <packagesToSeal/>
+ <packagesToUnSeal/>
+ </sealing>
+ </manifest>
+ <selectedElements exportClassFiles="true" exportJavaFiles="false" exportOutputFolder="false">
+ <javaElement handleIdentifier="=common/tests&lt;jar.example"/>
+ </selectedElements>
+</jardesc>
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/mock_attrs.xml b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/mock_attrs.xml
new file mode 100644
index 0000000..aa9a1f7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/data/mock_attrs.xml
@@ -0,0 +1,340 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * 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.
+ */
+-->
+<resources>
+ <!-- WARNING !!! THIS IS A MOCK FILE. DO NOT USE FOR DOCUMENTATION PURPOSES.
+ This file has been trimmed down to only extract a number of interesting cases
+ for unit tests.
+
+ What this contains:
+ - View
+ - ViewGroup
+ - some attributes which format are defined in Theme
+ - orientation, gravity and layout_gravity defined before they are used
+ - ViewGroup_Layout
+ - ViewGroup_MarginLayout
+ - LinearLayout
+ - LinearLayout_Layout
+ - TableLayout
+
+ Note that TableLayout does not have a TableLayout_Layout definition here
+ where these is a class TableLayout.LayoutData.
+ -->
+
+ <!-- These are the standard attributes that make up a complete theme. -->
+ <declare-styleable name="Theme">
+
+ <!-- Defines the scrollbars size. -->
+ <attr name="scrollbarSize" format="dimension" />
+
+ </declare-styleable>
+
+
+ <!-- Standard orientation constant. -->
+ <attr name="orientation">
+ <!-- Defines an horizontal widget. -->
+ <enum name="horizontal" value="0" />
+ <!-- Defines a vertical widget. -->
+ <enum name="vertical" value="1" />
+ </attr>
+
+ <!-- Specifies how to place an object, both
+ its x and y axis, within a larger containing object. -->
+ <attr name="gravity">
+ <!-- Push object to the top of its container, not changing its size. -->
+ <flag name="top" value="0x30" />
+ <!-- Push object to the bottom of its container, not changing its size. -->
+ <flag name="bottom" value="0x50" />
+ <!-- Push object to the left of its container, not changing its size. -->
+ <flag name="left" value="0x03" />
+ <!-- Push object to the right of its container, not changing its size. -->
+ <flag name="right" value="0x05" />
+ <!-- Place object in the vertical center of its container, not changing its size. -->
+ <flag name="center_vertical" value="0x10" />
+ <!-- Grow the vertical size of the object if needed so it completely fills its container. -->
+ <flag name="fill_vertical" value="0x70" />
+ <!-- Place object in the horizontal center of its container, not changing its size. -->
+ <flag name="center_horizontal" value="0x01" />
+ <!-- Grow the horizontal size of the object if needed so it completely fills its container. -->
+ <flag name="fill_horizontal" value="0x07" />
+ <!-- Place the object in the center of its container in both the vertical and horizontal axis, not changing its size. -->
+ <flag name="center" value="0x11" />
+ <!-- Grow the horizontal and vertical size of the object if needed so it completely fills its container. -->
+ <flag name="fill" value="0x77" />
+ </attr>
+
+ <!-- Standard gravity constant that a child can supply to its parent.
+ Defines how to place an object, both
+ its x and y axis, within a larger containing object. -->
+ <attr name="layout_gravity">
+ <!-- Push object to the top of its container, not changing its size. -->
+ <flag name="top" value="0x30" />
+ <!-- Push object to the bottom of its container, not changing its size. -->
+ <flag name="bottom" value="0x50" />
+ <!-- Push object to the left of its container, not changing its size. -->
+ <flag name="left" value="0x03" />
+ <!-- Push object to the right of its container, not changing its size. -->
+ <flag name="right" value="0x05" />
+ <!-- Place object in the vertical center of its container, not changing its size. -->
+ <flag name="center_vertical" value="0x10" />
+ <!-- Grow the vertical size of the object if needed so it completely fills its container. -->
+ <flag name="fill_vertical" value="0x70" />
+ <!-- Place object in the horizontal center of its container, not changing its size. -->
+ <flag name="center_horizontal" value="0x01" />
+ <!-- Grow the horizontal size of the object if needed so it completely fills its container. -->
+ <flag name="fill_horizontal" value="0x07" />
+ <!-- Place the object in the center of its container in both the vertical and horizontal axis, not changing its size. -->
+ <flag name="center" value="0x11" />
+ <!-- Grow the horizontal and vertical size of the object if needed so it completely fills its container. -->
+ <flag name="fill" value="0x77" />
+ </attr>
+
+ <declare-styleable name="View">
+ <!-- NOTE: View does not have a javadoc. Do not place a comment BEFORE View to make sure it
+ is NOT interpreted as Javadoc -->
+
+ <!-- Supply an identifier name for this view, to later retrieve it
+ with {@link android.view.View#findViewById View.findViewById()} or
+ {@link android.app.Activity#findViewById Activity.findViewById()}.
+ This must be a
+ resource reference; typically you set this using the
+ <code>@+</code> syntax to create a new ID resources.
+ For example: <code>android:id="@+id/my_id"</code> which
+ allows you to later retrieve the view
+ with <code>findViewById(R.id.my_id)</code>. -->
+ <attr name="id" format="reference" />
+
+ <!-- Supply a tag for this view containing a String, to be retrieved
+ later with {@link android.view.View#getTag View.getTag()} or
+ searched for with {@link android.view.View#findViewWithTag
+ View.findViewWithTag()}. It is generally preferable to use
+ IDs (through the android:id attribute) instead of tags because
+ they are faster and allow for compile-time type checking. -->
+ <attr name="tag" format="string" />
+
+ <!-- The initial horizontal scroll offset, in pixels.-->
+ <attr name="scrollX" format="dimension" />
+
+ <!-- The initial vertical scroll offset, in pixels. -->
+ <attr name="scrollY" format="dimension" />
+
+ <!-- A drawable to use as the background. This can be either a reference
+ to a full drawable resource (such as a PNG image, 9-patch,
+ XML state list description, etc), or a solid color such as "#ff000000"
+ (black). -->
+ <attr name="background" format="reference|color" />
+
+ <!-- Boolean that controls whether a view can take focus. By default the user can not
+ move focus to a view; by setting this attribute to true the view is
+ allowed to take focus. This value does not impact the behavior of
+ directly calling {@link android.view.View#requestFocus}, which will
+ always request focus regardless of this view. It only impacts where
+ focus navigation will try to move focus. -->
+ <attr name="focusable" format="boolean" />
+
+ <!-- Sets the circumstances under which this view will take focus. There are
+ two choices: "weak" or "normal". The default value is "normal" for
+ any focusable views. The focus type only applies if the view
+ has been set to be focusable. -->
+ <attr name="focusType">
+ <!-- This view is focusable, but only if none of its descendants are already focused. -->
+ <enum name="normal" value="0" />
+ <!-- This view will always claim to be focusable. -->
+ <enum name="weak" value="1" />
+ </attr>
+
+ <!-- Controls the initial visibility of the view. -->
+ <attr name="visibility">
+ <!-- Visible on screen; the default value. -->
+ <enum name="visible" value="0" />
+ <!-- Not displayed, but taken into account during layout (space is left for it). -->
+ <enum name="invisible" value="1" />
+ <!-- Completely hidden, as if the view had not been added. -->
+ <enum name="gone" value="2" />
+ </attr>
+
+ <!-- Defines which scrollbars should be displayed on scrolling or not. -->
+ <attr name="scrollbars">
+ <!-- No scrollbar is displayed. -->
+ <flag name="none" value="0x00000000" />
+ <!-- Displays horizontal scrollbar only. -->
+ <flag name="horizontal" value="0x00000100" />
+ <!-- Displays vertical scrollbar only. -->
+ <flag name="vertical" value="0x00000200" />
+ </attr>
+
+ <!-- Sets the width of vertical scrollbars and height of horizontal scrollbars. -->
+ <attr name="scrollbarSize" />
+
+ <!-- Text to display. (copied from TextView for the extra localization) -->
+ <attr name="text" format="string" localization="suggested" />
+
+ </declare-styleable>
+
+ <!-- Attributes that can be used with a {@link android.view.ViewGroup} or any
+ of its subclasses. Also see {@link #ViewGroup_Layout} for
+ attributes that this class processes in its children. -->
+ <declare-styleable name="ViewGroup">
+ <!-- Defines whether a child is limited to draw inside of its bounds or not.
+ This is useful with animations that scale the size of the children to more
+ than 100% for instance. In such a case, this property should be set to false
+ to allow the children to draw outside of their bounds. The default value of
+ this property is true. -->
+ <attr name="clipChildren" format="boolean" />
+ <!-- Defines the layout animation to use the first time the ViewGroup is laid out.
+ Layout animations can also be started manually after the first layout. -->
+ <attr name="layoutAnimation" format="reference" />
+ <!-- Defines whether a child's animation should be kept when it is over. Keeping
+ the animations is useful with animation whose final state is different from
+ the initial state of the View. This is particularly useful with animation
+ whose fillAfter property is enabled. This property is set to false by default. -->
+ <attr name="persistentDrawingCache">
+ <!-- The drawing cache is not persisted after use. -->
+ <flag name="none" value="0x0" />
+ <!-- The drawing cache is persisted after a layout animation. -->
+ <flag name="animation" value="0x1" />
+ <!-- The drawing cache is persisted after a scroll. -->
+ <flag name="scrolling" value="0x2" />
+ <!-- The drawing cache is always persisted. -->
+ <flag name="all" value="0x3" />
+ </attr>
+ </declare-styleable>
+
+ <!-- This is the basic set of layout attributes that are common to all
+ layout managers. These attributes are specified with the rest of
+ a view's normal attributes (such as {@link android.R.attr#background},
+ but will be parsed by the view's parent and ignored by the child.
+ <p>The values defined here correspond to the base layout attribute
+ class {@link android.view.ViewGroup.LayoutParams}. -->
+ <declare-styleable name="ViewGroup_Layout">
+ <!-- Specifies the basic width of the view. This is a required attribute
+ for any view inside of a containing layout manager. Its value may
+ be a dimension (such as "12dip") for a constant width or one of
+ the special constants. -->
+ <attr name="layout_width" format="dimension">
+ <!-- The view should be as big as its parent (minus padding). -->
+ <enum name="fill_parent" value="-1" />
+ <!-- The view should be only big enough to enclose its content (plus padding). -->
+ <enum name="wrap_content" value="-2" />
+ </attr>
+
+ <!-- Specifies the basic height of the view. This is a required attribute
+ for any view inside of a containing layout manager. Its value may
+ be a dimension (such as "12dip") for a constant height or one of
+ the special constants. -->
+ <attr name="layout_height" format="dimension">
+ <!-- The view should be as big as its parent (minus padding). -->
+ <enum name="fill_parent" value="-1" />
+ <!-- The view should be only big enough to enclose its content (plus padding). -->
+ <enum name="wrap_content" value="-2" />
+ </attr>
+ </declare-styleable>
+
+ <!-- This is the basic set of layout attributes for layout managers that
+ wish to place margins around their child views.
+ These attributes are specified with the rest of
+ a view's normal attributes (such as {@link android.R.attr#background},
+ but will be parsed by the view's parent and ignored by the child.
+ <p>The values defined here correspond to the base layout attribute
+ class {@link android.view.ViewGroup.MarginLayoutParams}. -->
+ <declare-styleable name="ViewGroup_MarginLayout">
+ <attr name="layout_width" />
+ <attr name="layout_height" />
+ <!-- Specifies extra space on the left side of this view.
+ This space is outside this view's bounds. -->
+ <attr name="layout_marginLeft" format="dimension" />
+ <!-- Specifies extra space on the top side of this view.
+ This space is outside this view's bounds. -->
+ <attr name="layout_marginTop" format="dimension" />
+ <!-- Specifies extra space on the right side of this view.
+ This space is outside this view's bounds. -->
+ <attr name="layout_marginRight" format="dimension" />
+ <!-- Specifies extra space on the bottom side of this view.
+ This space is outside this view's bounds. -->
+ <attr name="layout_marginBottom" format="dimension" />
+ </declare-styleable>
+
+ <!-- This is a linear layout. -->
+ <declare-styleable name="LinearLayout">
+ <!-- Should the layout be a column or a row? Use "horizontal"
+ for a row, "vertical" for a column. The default is
+ horizontal. -->
+ <attr name="orientation" />
+ <attr name="baselineAligned" format="boolean|reference" />
+ <!-- When a linear layout is part of another layout that is baseline
+ aligned, it can specify which of its children to baseline align to
+ (i.e which child TextView).-->
+ <attr name="baselineAlignedChildIndex" format="integer|color" min="0"/>
+ <!-- Defines the maximum weight sum. If unspecified, the sum is computed
+ by adding the layout_weight of all of the children. This can be
+ used for instance to give a single child 50% of the total available
+ space by giving it a layout_weight of 0.5 and setting the weightSum
+ to 1.0. -->
+ <attr name="weightSum" format="float" />
+ </declare-styleable>
+
+ <declare-styleable name="LinearLayout_Layout">
+ <attr name="layout_width" />
+ <attr name="layout_height" />
+ <attr name="layout_weight" format="float" />
+ <attr name="layout_gravity" />
+ </declare-styleable>
+
+ <declare-styleable name="TableLayout">
+ <!-- The 0 based index of the columns to stretch. The column indices
+ must be separated by a comma: 1, 2, 5. Illegal and duplicate
+ indices are ignored. You can stretch all columns by using the
+ value "*" instead. Note that a column can be marked stretchable
+ and shrinkable at the same time. -->
+ <attr name="stretchColumns" format="string" />
+ <!-- The 0 based index of the columns to shrink. The column indices
+ must be separated by a comma: 1, 2, 5. Illegal and duplicate
+ indices are ignored. You can shrink all columns by using the
+ value "*" instead. Note that a column can be marked stretchable
+ and shrinkable at the same time. -->
+ <attr name="shrinkColumns" format="string" />
+ <!-- The 0 based index of the columns to collapse. The column indices
+ must be separated by a comma: 1, 2, 5. Illegal and duplicate
+ indices are ignored. -->
+ <attr name="collapseColumns" format="string" />
+ </declare-styleable>
+
+ <!-- Test for deprecated attributes. -->
+ <declare-styleable name="DeprecatedTest">
+ <!-- Deprecated comments using delimiters.
+ Ignored. {@deprecated In-line deprecated.} {@ignore Ignored}.
+ -->
+ <attr name="deprecated-inline" />
+
+ <!-- Deprecated comments on their own line.
+ @deprecated Multi-line version of deprecated
+ that works till the next tag.
+ @ignore This tag must be ignored
+ -->
+ <attr name="deprecated-multiline" />
+
+ <!-- This attribute is not deprecated. -->
+ <attr name="deprecated-not" />
+
+ <!-- {@deprecated There is no other javadoc here. } -->
+ <attr name="deprecated-no-javadoc" format="boolean" />
+
+ </declare-styleable>
+
+</resources>
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/mock_android/view/View.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/mock_android/view/View.java
new file mode 100644
index 0000000..a80a98d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/mock_android/view/View.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2008 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 mock_android.view;
+
+public class View {
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/mock_android/view/ViewGroup.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/mock_android/view/ViewGroup.java
new file mode 100644
index 0000000..466470f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/mock_android/view/ViewGroup.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2008 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 mock_android.view;
+
+public class ViewGroup extends View {
+
+ public class MarginLayoutParams extends LayoutParams {
+
+ }
+
+ public class LayoutParams {
+
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/mock_android/widget/LinearLayout.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/mock_android/widget/LinearLayout.java
new file mode 100644
index 0000000..3870a63
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/mock_android/widget/LinearLayout.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2008 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 mock_android.widget;
+
+import mock_android.view.ViewGroup;
+
+public class LinearLayout extends ViewGroup {
+
+ public class LayoutParams extends mock_android.view.ViewGroup.LayoutParams {
+
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.tests/unittests/mock_android/widget/TableLayout.java b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/mock_android/widget/TableLayout.java
new file mode 100644
index 0000000..e455e7d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.tests/unittests/mock_android/widget/TableLayout.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2008 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 mock_android.widget;
+
+import mock_android.view.ViewGroup;
+
+public class TableLayout extends ViewGroup {
+
+ public class LayoutParams extends MarginLayoutParams {
+
+ }
+
+}